Repository: skupperproject/skupper
Pull request #1524 (2024-07-01 20:49:54)

V2 connector commands
 Add connector create, update, delete, and status commands.
 Move files to kube directory.
 Add unit tests.



nluaces: Hi @lynnemorrison, I agree with your comment (https://github.com/skupperproject/skupper/pull/1516#issuecomment-2217780039) let's check that the site exists here too. 
lynnemorrison: added code to use new spinner function. 

Patch:
diff --git a/internal/cmd/skupper/connector/connector.go b/internal/cmd/skupper/connector/connector.go
index 80192c9e..d383d69a 100644
--- a/internal/cmd/skupper/connector/connector.go
+++ b/internal/cmd/skupper/connector/connector.go
@@ -1,23 +1,27 @@
 package connector
 
-import "github.com/spf13/cobra"
+import (
+	"github.com/skupperproject/skupper/internal/cmd/skupper/connector/kube"
+	"github.com/spf13/cobra"
+)
 
 func NewCmdConnector() *cobra.Command {
 
 	cmd := &cobra.Command{
-		Use:     "connector",
-		Short:   "",
-		Long:    ``,
-		Example: ``,
+		Use:   "connector",
+		Short: "Binds target workloads in the local site to listeners in remote sites.",
+		Long:  `A connector is a endpoint in the local site and binds to listeners in remote sites`,
+		Example: `skupper connector create my-connector 8080
+skupper connector status my-connector`,
 	}
 
-	connectorCreateCommand := NewCmdConnectorCreate()
-	connectorGetCommand := NewCmdConnectorGet()
-	connectorUpdateCommand := NewCmdConnectorUpdate()
-	connectorDeleteCommand := NewCmdConnectorDelete()
+	connectorCreateCommand := kube.NewCmdConnectorCreate()
+	connectorStatusCommand := kube.NewCmdConnectorStatus()
+	connectorUpdateCommand := kube.NewCmdConnectorUpdate()
+	connectorDeleteCommand := kube.NewCmdConnectorDelete()
 
 	cmd.AddCommand(&connectorCreateCommand.CobraCmd)
-	cmd.AddCommand(&connectorGetCommand.CobraCmd)
+	cmd.AddCommand(&connectorStatusCommand.CobraCmd)
 	cmd.AddCommand(&connectorUpdateCommand.CobraCmd)
 	cmd.AddCommand(&connectorDeleteCommand.CobraCmd)
 
diff --git a/internal/cmd/skupper/connector/connector_create.go b/internal/cmd/skupper/connector/connector_create.go
deleted file mode 100644
index 82326a16..00000000
--- a/internal/cmd/skupper/connector/connector_create.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package connector
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	connectorCreateLong    = ""
-	connectorCreateExample = ""
-)
-
-type CmdConnectorCreate struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdConnectorCreate() *CmdConnectorCreate {
-
-	skupperCmd := CmdConnectorCreate{}
-
-	cmd := cobra.Command{
-		Use:     "create",
-		Short:   "",
-		Long:    connectorCreateLong,
-		Example: connectorCreateExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdConnectorCreate) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdConnectorCreate) AddFlags()                                            {}
-func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdConnectorCreate) InputToOptions()                                      {}
-func (cmd *CmdConnectorCreate) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdConnectorCreate) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/connector/connector_delete.go b/internal/cmd/skupper/connector/connector_delete.go
deleted file mode 100644
index 6d01b744..00000000
--- a/internal/cmd/skupper/connector/connector_delete.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package connector
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	connectorDeleteLong    = ""
-	connectorDeleteExample = ""
-)
-
-type CmdConnectorDelete struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdConnectorDelete() *CmdConnectorDelete {
-
-	skupperCmd := CmdConnectorDelete{}
-
-	cmd := cobra.Command{
-		Use:     "delete",
-		Short:   "",
-		Long:    connectorDeleteLong,
-		Example: connectorDeleteExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdConnectorDelete) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdConnectorDelete) AddFlags()                                            {}
-func (cmd *CmdConnectorDelete) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdConnectorDelete) InputToOptions()                                      {}
-func (cmd *CmdConnectorDelete) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdConnectorDelete) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/connector/connector_get.go b/internal/cmd/skupper/connector/connector_get.go
deleted file mode 100644
index d91321b8..00000000
--- a/internal/cmd/skupper/connector/connector_get.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package connector
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	connectorGetLong    = ""
-	connectorGetExample = ""
-)
-
-type CmdConnectorGet struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdConnectorGet() *CmdConnectorGet {
-
-	skupperCmd := CmdConnectorGet{}
-
-	cmd := cobra.Command{
-		Use:     "get",
-		Short:   "",
-		Long:    connectorGetLong,
-		Example: connectorGetExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdConnectorGet) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdConnectorGet) AddFlags()                                            {}
-func (cmd *CmdConnectorGet) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdConnectorGet) InputToOptions()                                      {}
-func (cmd *CmdConnectorGet) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdConnectorGet) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/connector/connector_update.go b/internal/cmd/skupper/connector/connector_update.go
deleted file mode 100644
index dbf18d94..00000000
--- a/internal/cmd/skupper/connector/connector_update.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package connector
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	connectorUpdateLong    = ""
-	connectorUpdateExample = ""
-)
-
-type CmdConnectorUpdate struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdConnectorUpdate() *CmdConnectorUpdate {
-
-	skupperCmd := CmdConnectorUpdate{}
-
-	cmd := cobra.Command{
-		Use:     "update",
-		Short:   "",
-		Long:    connectorUpdateLong,
-		Example: connectorUpdateExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdConnectorUpdate) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdConnectorUpdate) AddFlags()                                            {}
-func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdConnectorUpdate) InputToOptions()                                      {}
-func (cmd *CmdConnectorUpdate) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdConnectorUpdate) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/connector/kube/connector_create.go b/internal/cmd/skupper/connector/kube/connector_create.go
new file mode 100644
index 00000000..e669fe0b
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_create.go
@@ -0,0 +1,257 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+var (
+	connectorCreateLong    = "Clients at this site use the connector host and port to establish connections to the remote service."
+	connectorCreateExample = "skupper connector create database 5432"
+)
+
+type ConnectorCreate struct {
+	routingKey      string
+	host            string
+	selector        string
+	tlsSecret       string
+	connectorType   string
+	includeNotReady bool
+	workload        string
+	timeout         time.Duration
+	output          string
+}
+
+type CmdConnectorCreate struct {
+	client     skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd   cobra.Command
+	flags      ConnectorCreate
+	namespace  string
+	name       string
+	port       int
+	output     string
+	KubeClient kubernetes.Interface
+}
+
+func NewCmdConnectorCreate() *CmdConnectorCreate {
+
+	skupperCmd := CmdConnectorCreate{flags: ConnectorCreate{}}
+
+	cmd := cobra.Command{
+		Use:     "create <name> <port>",
+		Short:   "create a connector",
+		Long:    connectorCreateLong,
+		Example: connectorCreateExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			return skupperCmd.WaitUntilReady()
+		},
+	}
+
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdConnectorCreate) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+	cmd.KubeClient = cli.Kube
+}
+
+func (cmd *CmdConnectorCreate) AddFlags() {
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.routingKey, "routing-key", "r", "", "The identifier used to route traffic from Connectors to connectors")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.host, "host", "", "The hostname or IP address of the local Connector")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.tlsSecret, "tls-secret", "t", "", "The name of a Kubernetes secret containing TLS credentials")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.connectorType, "type", "tcp", "The Connector type. Choices: [tcp].")
+	cmd.CobraCmd.Flags().BoolVarP(&cmd.flags.includeNotReady, "include-not-ready", "i", false, "If true, include server pods that are not in the ready state.")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.selector, "selector", "s", "", "A Kubernetes label selector for specifying target server pods.")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.workload, "workload", "w", "", "A Kubernetes label selector for specifying target server pods.")
+	cmd.CobraCmd.Flags().DurationVar(&cmd.flags.timeout, "timeout", 60*time.Second, "Raise an error if the operation does not complete in the given period of time.")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml")
+}
+
+func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+	numberValidator := validator.NewNumberValidator()
+	connectorTypeValidator := validator.NewOptionValidator(connectorTypes)
+	outputTypeValidator := validator.NewOptionValidator(utils.OutputTypes)
+	workloadStringValidator := validator.NewWorkloadStringValidator()
+
+	// Validate arguments name and port
+	if len(args) < 2 {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name and port must be configured"))
+	} else if len(args) > 2 {
+		validationErrors = append(validationErrors, fmt.Errorf("only two arguments are allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must not be empty"))
+	} else if args[1] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector port must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+		} else {
+			cmd.name = args[0]
+		}
+
+		cmd.port, err = strconv.Atoi(args[1])
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("connector port is not valid: %s", err))
+		}
+		ok, err = numberValidator.Evaluate(cmd.port)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector port is not valid: %s", err))
+		}
+	}
+
+	// Validate there is already a site defined in the namespace before a connector can be created
+	siteList, _ := cmd.client.Sites(cmd.namespace).List(context.TODO(), metav1.ListOptions{})
+	if siteList == nil || len(siteList.Items) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("A site must exist in namespace %s before a connector can be created", cmd.namespace))
+	}
+
+	// Validate if there is already a Connector with this name in the namespace
+	if cmd.name != "" {
+		connector, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if connector != nil && !errors.IsNotFound(err) && connector.Status.StatusMessage == "Ok" {
+			validationErrors = append(validationErrors, fmt.Errorf("there is already a connector %s created for namespace %s", cmd.name, cmd.namespace))
+		}
+	}
+
+	// Validate flags
+	if cmd.flags.routingKey != "" {
+		ok, err := resourceStringValidator.Evaluate(cmd.flags.routingKey)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("routing key is not valid: %s", err))
+		}
+	}
+	//TBD what characters are not allowed for host flag
+
+	if cmd.flags.tlsSecret != "" {
+		// check that the secret exists
+		_, err := cmd.KubeClient.CoreV1().Secrets(cmd.namespace).Get(context.TODO(), cmd.flags.tlsSecret, metav1.GetOptions{})
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("tls-secret is not valid: does not exist"))
+		}
+	}
+	if cmd.flags.connectorType != "" {
+		ok, err := connectorTypeValidator.Evaluate(cmd.flags.connectorType)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector type is not valid: %s", err))
+		}
+	}
+	if cmd.flags.selector != "" {
+		ok, err := workloadStringValidator.Evaluate(cmd.flags.selector)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("selector is not valid: %s", err))
+		}
+	}
+	//TBD no workload in connector CRD
+	if cmd.flags.workload != "" {
+		ok, err := workloadStringValidator.Evaluate(cmd.flags.workload)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("workload is not valid: %s", err))
+		}
+	}
+	//TBD what is valid timeout
+	if cmd.flags.timeout <= 0*time.Minute {
+		validationErrors = append(validationErrors, fmt.Errorf("timeout is not valid"))
+	}
+	// workload, selector or host must be specified
+	if cmd.flags.workload == "" && cmd.flags.selector == "" && cmd.flags.host == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("One of the following options must be set: workload, selector, host"))
+	}
+	if cmd.flags.output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.flags.output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.output = cmd.flags.output
+		}
+	}
+	return validationErrors
+}
+
+func (cmd *CmdConnectorCreate) Run() error {
+
+	resource := v1alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v1alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.name,
+			Namespace: cmd.namespace,
+		},
+		Spec: v1alpha1.ConnectorSpec{
+			Host:            cmd.flags.host,
+			Port:            cmd.port,
+			RoutingKey:      cmd.flags.routingKey,
+			TlsCredentials:  cmd.flags.tlsSecret,
+			Type:            cmd.flags.connectorType,
+			IncludeNotReady: cmd.flags.includeNotReady,
+			//Workload:       cmd.flags.workload,
+			Selector: cmd.flags.selector,
+		},
+	}
+
+	if cmd.output != "" {
+		encodedOutput, err := utils.Encode(cmd.output, resource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		_, err := cmd.client.Connectors(cmd.namespace).Create(context.TODO(), &resource, metav1.CreateOptions{})
+		return err
+	}
+}
+
+func (cmd *CmdConnectorCreate) WaitUntilReady() error {
+	// the site resource was not created
+	if cmd.output != "" {
+		return nil
+	}
+
+	waitTime := int(cmd.flags.timeout.Seconds())
+	err := utils.NewSpinnerWithTimeout("Waiting for create to complete...", waitTime, func() error {
+
+		resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+
+		if resource != nil && resource.Status.StatusMessage == "Ok" {
+			return nil
+		}
+
+		return fmt.Errorf("error getting the resource")
+	})
+
+	if err != nil {
+		return fmt.Errorf("Connector %q not ready yet, check the logs for more information\n", cmd.name)
+	}
+
+	fmt.Printf("Connector %q is ready\n", cmd.name)
+	return nil
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_delete.go b/internal/cmd/skupper/connector/kube/connector_delete.go
new file mode 100644
index 00000000..d0012bcf
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_delete.go
@@ -0,0 +1,129 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var (
+	connectorDeleteLong    = "Delete a connector <name>"
+	connectorDeleteExample = "skupper connector delete database"
+)
+
+type ConnectorDelete struct {
+	timeout time.Duration
+}
+
+type CmdConnectorDelete struct {
+	client    skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd  cobra.Command
+	flags     ConnectorDelete
+	namespace string
+	name      string
+}
+
+func NewCmdConnectorDelete() *CmdConnectorDelete {
+
+	skupperCmd := CmdConnectorDelete{}
+
+	cmd := cobra.Command{
+		Use:     "delete <name>",
+		Short:   "delete a connector",
+		Long:    connectorDeleteLong,
+		Example: connectorDeleteExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			return skupperCmd.WaitUntilReady()
+		},
+	}
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdConnectorDelete) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+}
+
+func (cmd *CmdConnectorDelete) AddFlags() {
+	cmd.CobraCmd.Flags().DurationVarP(&cmd.flags.timeout, "timeout", "t", 60*time.Second, "Raise an error if the operation does not complete in the given period of time.")
+}
+
+func (cmd *CmdConnectorDelete) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+
+	// Validate arguments name
+	if len(args) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must be specified"))
+	} else if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+		} else {
+			cmd.name = args[0]
+		}
+		// Validate that there is already a connector with this name in the namespace
+		if cmd.name != "" {
+			connector, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+			if err != nil {
+				validationErrors = append(validationErrors, err)
+			} else if connector == nil {
+				validationErrors = append(validationErrors, fmt.Errorf("connector %s does not exist in namespace %s", cmd.name, cmd.namespace))
+			}
+		}
+
+		//TBD what is valid timeout
+		if cmd.flags.timeout <= 0*time.Minute {
+			validationErrors = append(validationErrors, fmt.Errorf("timeout is not valid"))
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdConnectorDelete) Run() error {
+	err := cmd.client.Connectors(cmd.namespace).Delete(context.TODO(), cmd.name, metav1.DeleteOptions{})
+	return err
+}
+
+func (cmd *CmdConnectorDelete) WaitUntilReady() error {
+	waitTime := int(cmd.flags.timeout.Seconds())
+	err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
+
+		resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if err == nil && resource != nil {
+			return fmt.Errorf("error deleting the resource")
+		} else {
+			return nil
+		}
+	})
+
+	if err != nil {
+		return fmt.Errorf("Connector %q not deleted yet, check the logs for more information %s\n", cmd.name, err)
+	}
+
+	fmt.Printf("Connector %q deleted\n", cmd.name)
+	return nil
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_status.go b/internal/cmd/skupper/connector/kube/connector_status.go
new file mode 100644
index 00000000..18d1d2fa
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_status.go
@@ -0,0 +1,156 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"text/tabwriter"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var (
+	connectorStatusLong    = "Display status of all connectors or a specific connector"
+	connectorStatusExample = "skupper connector status backend"
+)
+
+type ConnectorStatus struct {
+	output string
+}
+
+type CmdConnectorStatus struct {
+	client    skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd  cobra.Command
+	flags     ConnectorStatus
+	namespace string
+	name      string
+	output    string
+}
+
+func NewCmdConnectorStatus() *CmdConnectorStatus {
+
+	skupperCmd := CmdConnectorStatus{flags: ConnectorStatus{}}
+
+	cmd := cobra.Command{
+		Use:     "status <name>",
+		Short:   "get status of connectors",
+		Long:    connectorStatusLong,
+		Example: connectorStatusExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+	}
+
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdConnectorStatus) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+}
+
+func (cmd *CmdConnectorStatus) AddFlags() {
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "print status of connectors Choices: json, yaml")
+}
+
+func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+	outputTypeValidator := validator.NewOptionValidator(utils.OutputTypes)
+
+	// Validate arguments name if specified
+	if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if len(args) == 1 {
+		if args[0] == "" {
+			validationErrors = append(validationErrors, fmt.Errorf("connector name must not be empty"))
+		} else {
+			ok, err := resourceStringValidator.Evaluate(args[0])
+			if !ok {
+				validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+			} else {
+				cmd.name = args[0]
+			}
+		}
+	}
+
+	// Validate that there is a connector with this name in the namespace
+	if cmd.name != "" {
+		connector, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if connector == nil || errors.IsNotFound(err) {
+			validationErrors = append(validationErrors, fmt.Errorf("connector %s does not exist in namespace %s", cmd.name, cmd.namespace))
+		}
+	}
+
+	if cmd.flags.output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.flags.output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.output = cmd.flags.output
+		}
+	}
+
+	return validationErrors
+}
+func (cmd *CmdConnectorStatus) Run() error {
+	if cmd.name == "" {
+		resources, err := cmd.client.Connectors(cmd.namespace).List(context.TODO(), metav1.ListOptions{})
+		if err != nil || resources == nil || len(resources.Items) == 0 {
+			fmt.Println("No connectors found")
+			return err
+		}
+		if cmd.output != "" {
+			for _, resource := range resources.Items {
+				encodedOutput, err := utils.Encode(cmd.output, resource)
+				if err != nil {
+					return err
+				}
+				fmt.Println(encodedOutput)
+			}
+		} else {
+			tw := tabwriter.NewWriter(os.Stdout, 8, 8, 1, '\t', tabwriter.TabIndent)
+			_, _ = fmt.Fprintln(tw, fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t%s",
+				"NAME", "STATUS", "ROUTING-KEY", "SELECTOR", "HOST", "PORT", "LISTENERS"))
+			for _, resource := range resources.Items {
+				fmt.Fprintln(tw, fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%d",
+					resource.Name, resource.Status.StatusMessage, resource.Spec.RoutingKey, resource.Spec.Selector, resource.Spec.Host, resource.Spec.Port))
+			}
+			_ = tw.Flush()
+		}
+	} else {
+		resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if resource == nil || errors.IsNotFound(err) {
+			fmt.Println("No connectors found")
+			return err
+		}
+		if cmd.output != "" {
+			encodedOutput, err := utils.Encode(cmd.output, resource)
+			if err != nil {
+				return err
+			}
+			fmt.Println(encodedOutput)
+		} else {
+			tw := tabwriter.NewWriter(os.Stdout, 8, 8, 1, '\t', tabwriter.TabIndent)
+			fmt.Fprintln(tw, fmt.Sprintf("Name:\t%s\nStatus:\t%s\nRouting key:\t%s\nSelector:\t%s\nHost:\t%s\nPort:\t%d\nListeners:\n",
+				resource.Name, resource.Status.StatusMessage, resource.Spec.RoutingKey, resource.Spec.Selector, resource.Spec.Host, resource.Spec.Port))
+			_ = tw.Flush()
+		}
+	}
+
+	return nil
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_update.go b/internal/cmd/skupper/connector/kube/connector_update.go
new file mode 100644
index 00000000..d03fd74a
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_update.go
@@ -0,0 +1,270 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+var (
+	connectorUpdateLong = `Clients at this site use the connector host and port to establish connections to the remote service.
+	The user can change port, host name, TLS secret, selector, connector type and routing key`
+	connectorUpdateExample = "skupper connector update database --host mysql --port 3306"
+
+	connectorTypes = []string{"tcp"}
+)
+
+type ConnectorUpdates struct {
+	routingKey      string
+	host            string
+	tlsSecret       string
+	connectorType   string
+	port            int
+	workload        string
+	selector        string
+	includeNotReady bool
+	timeout         time.Duration
+	output          string
+}
+
+type CmdConnectorUpdate struct {
+	client          skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd        cobra.Command
+	flags           ConnectorUpdates
+	namespace       string
+	name            string
+	resourceVersion string
+	newSettings     ConnectorUpdates
+	KubeClient      kubernetes.Interface
+}
+
+func NewCmdConnectorUpdate() *CmdConnectorUpdate {
+
+	skupperCmd := CmdConnectorUpdate{flags: ConnectorUpdates{}}
+
+	cmd := cobra.Command{
+		Use:     "update <name>",
+		Short:   "update a connector",
+		Long:    connectorUpdateLong,
+		Example: connectorUpdateExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			return skupperCmd.WaitUntilReady()
+		},
+	}
+
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdConnectorUpdate) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+	cmd.KubeClient = cli.Kube
+}
+
+func (cmd *CmdConnectorUpdate) AddFlags() {
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.routingKey, "routing-key", "r", "", "The identifier used to route traffic from connectors to connectors")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.host, "host", "", "The hostname or IP address of the local connector")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.tlsSecret, "tls-secret", "t", "", "The name of a Kubernetes secret containing TLS credentials")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.connectorType, "type", "tcp", "The connector type. Choices: [tcp|http].")
+	cmd.CobraCmd.Flags().IntVar(&cmd.flags.port, "port", 0, "The port of the local connector")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.selector, "selector", "s", "", "A Kubernetes label selector for specifying target server pods.")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.workload, "workload", "w", "", "A Kubernetes resource name that identifies a workload")
+	cmd.CobraCmd.Flags().BoolVarP(&cmd.flags.includeNotReady, "include-not-ready", "i", false, "If set, include server pods that are not in the ready state.")
+	cmd.CobraCmd.Flags().DurationVar(&cmd.flags.timeout, "timeout", 60*time.Second, "Raise an error if the operation does not complete in the given period of time.")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "Print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml")
+}
+
+func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+	numberValidator := validator.NewNumberValidator()
+	connectorTypeValidator := validator.NewOptionValidator(connectorTypes)
+	outputTypeValidator := validator.NewOptionValidator(utils.OutputTypes)
+	workloadStringValidator := validator.NewWorkloadStringValidator()
+
+	// Validate arguments name
+	if len(args) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must be configured"))
+	} else if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+		} else {
+			cmd.name = args[0]
+		}
+	}
+
+	// Validate that there is already a connector with this name in the namespace
+	if cmd.name != "" {
+		connector, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if connector == nil || errors.IsNotFound(err) {
+			validationErrors = append(validationErrors, fmt.Errorf("connector %s must exist in namespace %s to be updated", cmd.name, cmd.namespace))
+		} else {
+			// save existing values
+			cmd.resourceVersion = connector.ResourceVersion
+			cmd.newSettings.host = connector.Spec.Host
+			cmd.newSettings.port = connector.Spec.Port
+			cmd.newSettings.tlsSecret = connector.Spec.TlsCredentials
+			cmd.newSettings.connectorType = connector.Spec.Type
+			//cmd.newSettings.workload = connector.Spec.Workload
+			cmd.newSettings.selector = connector.Spec.Selector
+			cmd.newSettings.includeNotReady = connector.Spec.IncludeNotReady
+		}
+	}
+
+	// Validate flags
+	if cmd.flags.routingKey != "" {
+		ok, err := resourceStringValidator.Evaluate(cmd.flags.routingKey)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("routing key is not valid: %s", err))
+		} else {
+			cmd.newSettings.routingKey = cmd.flags.routingKey
+		}
+	}
+	//TBD what characters are not allowed for host flag
+	if cmd.flags.host != "" {
+		cmd.newSettings.host = cmd.flags.host
+	}
+	if cmd.flags.tlsSecret != "" {
+		_, err := cmd.KubeClient.CoreV1().Secrets(cmd.namespace).Get(context.TODO(), cmd.flags.tlsSecret, metav1.GetOptions{})
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("tls-secret is not valid: does not exist"))
+		} else {
+			cmd.newSettings.tlsSecret = cmd.flags.tlsSecret
+		}
+	}
+	if cmd.flags.connectorType != "" {
+		ok, err := connectorTypeValidator.Evaluate(cmd.flags.connectorType)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector type is not valid: %s", err))
+		} else {
+			cmd.newSettings.connectorType = cmd.flags.connectorType
+		}
+	}
+	if cmd.flags.port != 0 {
+		ok, err := numberValidator.Evaluate(cmd.flags.port)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector port is not valid: %s", err))
+		} else {
+			cmd.newSettings.port = cmd.flags.port
+		}
+	}
+	//TBD what are valid values here
+	if cmd.flags.selector != "" {
+		ok, err := workloadStringValidator.Evaluate(cmd.flags.selector)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("selector is not valid: %s", err))
+		}
+		cmd.newSettings.selector = cmd.flags.selector
+	}
+	if cmd.flags.workload != "" {
+		ok, err := workloadStringValidator.Evaluate(cmd.flags.workload)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("workload is not valid: %s", err))
+		}
+		cmd.newSettings.selector = cmd.flags.workload
+	}
+	//TBD what is valid timeout
+	if cmd.flags.timeout <= 0*time.Minute {
+		validationErrors = append(validationErrors, fmt.Errorf("timeout is not valid"))
+	}
+	if cmd.flags.output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.flags.output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.newSettings.output = cmd.flags.output
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdConnectorUpdate) Run() error {
+
+	resource := v1alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v1alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:            cmd.name,
+			Namespace:       cmd.namespace,
+			ResourceVersion: cmd.resourceVersion,
+		},
+		Spec: v1alpha1.ConnectorSpec{
+			Host:           cmd.newSettings.host,
+			Port:           cmd.newSettings.port,
+			RoutingKey:     cmd.newSettings.routingKey,
+			TlsCredentials: cmd.newSettings.tlsSecret,
+			Type:           cmd.newSettings.connectorType,
+			//Workload:       cmd.newSettings.workload,
+			Selector:        cmd.newSettings.selector,
+			IncludeNotReady: cmd.newSettings.includeNotReady,
+		},
+	}
+
+	if cmd.newSettings.output != "" {
+		encodedOutput, err := utils.Encode(cmd.newSettings.output, resource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		_, err := cmd.client.Connectors(cmd.namespace).Update(context.TODO(), &resource, metav1.UpdateOptions{})
+		return err
+	}
+}
+
+func (cmd *CmdConnectorUpdate) WaitUntilReady() error {
+
+	// the site resource was not created
+	if cmd.newSettings.output != "" {
+		return nil
+	}
+
+	waitTime := int(cmd.flags.timeout.Seconds())
+	err := utils.NewSpinnerWithTimeout("Waiting for create to complete...", waitTime, func() error {
+
+		resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+
+		if resource != nil && resource.Status.StatusMessage == "Ok" {
+			return nil
+		}
+
+		return fmt.Errorf("error getting the resource")
+	})
+
+	if err != nil {
+		return fmt.Errorf("Connector %q not ready yet, check the logs for more information\n", cmd.name)
+	}
+
+	fmt.Printf("Connector %q is ready\n", cmd.name)
+	return nil
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_create.go b/internal/cmd/skupper/listener/kube/listener_create.go
new file mode 100644
index 00000000..bfa29f2b
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_create.go
@@ -0,0 +1,234 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+var (
+	listenerCreateLong    = "Clients at this site use the listener host and port to establish connections to the remote service."
+	listenerCreateExample = "skupper listener create database 5432"
+)
+
+type ListenerCreate struct {
+	routingKey   string
+	host         string
+	tlsSecret    string
+	listenerType string
+	timeout      time.Duration
+	output       string
+}
+
+type CmdListenerCreate struct {
+	client     skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd   cobra.Command
+	flags      ListenerCreate
+	namespace  string
+	name       string
+	port       int
+	output     string
+	KubeClient kubernetes.Interface
+}
+
+func NewCmdListenerCreate() *CmdListenerCreate {
+
+	skupperCmd := CmdListenerCreate{flags: ListenerCreate{}}
+
+	cmd := cobra.Command{
+		Use:     "create <name> <port>",
+		Short:   "create a listener",
+		Long:    listenerCreateLong,
+		Example: listenerCreateExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			return skupperCmd.WaitUntilReady()
+		},
+	}
+
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdListenerCreate) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+	cmd.KubeClient = cli.Kube
+}
+
+func (cmd *CmdListenerCreate) AddFlags() {
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.routingKey, "routing-key", "r", "", "The identifier used to route traffic from listeners to connectors")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.host, "host", "", "The hostname or IP address of the local listener")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.tlsSecret, "tls-secret", "t", "", "The name of a Kubernetes secret containing TLS credentials")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.listenerType, "type", "tcp", "The listener type. Choices: [tcp].")
+	cmd.CobraCmd.Flags().DurationVar(&cmd.flags.timeout, "timeout", 60*time.Second, "Raise an error if the operation does not complete in the given period of time.")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml")
+}
+
+func (cmd *CmdListenerCreate) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+	numberValidator := validator.NewNumberValidator()
+	listenerTypeValidator := validator.NewOptionValidator(utils.ListenerTypes)
+	outputTypeValidator := validator.NewOptionValidator(utils.OutputTypes)
+
+	// Validate arguments name and port
+	if len(args) < 2 {
+		validationErrors = append(validationErrors, fmt.Errorf("listener name and port must be configured"))
+	} else if len(args) > 2 {
+		validationErrors = append(validationErrors, fmt.Errorf("only two arguments are allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("listener name must not be empty"))
+	} else if args[1] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("listener port must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("listener name is not valid: %s", err))
+		} else {
+			cmd.name = args[0]
+		}
+
+		cmd.port, err = strconv.Atoi(args[1])
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("listener port is not valid: %s", err))
+		}
+		ok, err = numberValidator.Evaluate(cmd.port)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("listener port is not valid: %s", err))
+		}
+	}
+
+	// Validate there is already a site defined in the namespace before a listener can be created
+	siteList, _ := cmd.client.Sites(cmd.namespace).List(context.TODO(), metav1.ListOptions{})
+	if siteList == nil || len(siteList.Items) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("A site must exist in namespace %s before a listener can be created", cmd.namespace))
+	}
+
+	// Validate if there is already a listener with this name in the namespace
+	if cmd.name != "" {
+		listener, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if listener != nil && !errors.IsNotFound(err) && listener.Status.StatusMessage == "Ok" {
+			validationErrors = append(validationErrors, fmt.Errorf("there is already a listener %s created for namespace %s", cmd.name, cmd.namespace))
+		}
+	}
+
+	// Validate flags
+	if cmd.flags.routingKey != "" {
+		ok, err := resourceStringValidator.Evaluate(cmd.flags.routingKey)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("routing key is not valid: %s", err))
+		}
+	}
+
+	//TBD what characters are not allowed for host flag
+
+	if cmd.flags.tlsSecret != "" {
+		// check that the secret exists
+		_, err := cmd.KubeClient.CoreV1().Secrets(cmd.namespace).Get(context.TODO(), cmd.flags.tlsSecret, metav1.GetOptions{})
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("tls-secret is not valid: does not exist"))
+		}
+	}
+
+	if cmd.flags.listenerType != "" {
+		ok, err := listenerTypeValidator.Evaluate(cmd.flags.listenerType)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("listener type is not valid: %s", err))
+		}
+	}
+
+	//TBD what is valid timeout
+	if cmd.flags.timeout <= 0*time.Minute {
+		validationErrors = append(validationErrors, fmt.Errorf("timeout is not valid"))
+	}
+
+	if cmd.flags.output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.flags.output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.output = cmd.flags.output
+		}
+	}
+	return validationErrors
+}
+
+func (cmd *CmdListenerCreate) Run() error {
+
+	resource := v1alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v1alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.name,
+			Namespace: cmd.namespace,
+		},
+		Spec: v1alpha1.ListenerSpec{
+			Host:           cmd.flags.host,
+			Port:           cmd.port,
+			RoutingKey:     cmd.flags.routingKey,
+			TlsCredentials: cmd.flags.tlsSecret,
+			Type:           cmd.flags.listenerType,
+		},
+	}
+
+	if cmd.output != "" {
+		encodedOutput, err := utils.Encode(cmd.output, resource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		_, err := cmd.client.Listeners(cmd.namespace).Create(context.TODO(), &resource, metav1.CreateOptions{})
+		return err
+	}
+}
+
+func (cmd *CmdListenerCreate) WaitUntilReady() error {
+	// the site resource was not created
+	if cmd.output != "" {
+		return nil
+	}
+
+	waitTime := int(cmd.flags.timeout.Seconds())
+	err := utils.NewSpinnerWithTimeout("Waiting for create to complete...", waitTime, func() error {
+
+		resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+
+		if resource != nil && resource.Status.StatusMessage == "Ok" {
+			return nil
+		}
+
+		return fmt.Errorf("error getting the resource")
+	})
+
+	if err != nil {
+		return fmt.Errorf("Listener %q not ready yet, check the logs for more information\n", cmd.name)
+	}
+
+	fmt.Printf("Listener %q is ready\n", cmd.name)
+	return nil
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_delete.go b/internal/cmd/skupper/listener/kube/listener_delete.go
new file mode 100644
index 00000000..cfa58619
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_delete.go
@@ -0,0 +1,130 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var (
+	listenerDeleteLong    = "Delete a listener <name>"
+	listenerDeleteExample = "skupper listener delete database"
+)
+
+type ListenerDelete struct {
+	timeout time.Duration
+}
+
+type CmdListenerDelete struct {
+	client    skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd  cobra.Command
+	flags     ListenerDelete
+	namespace string
+	name      string
+}
+
+func NewCmdListenerDelete() *CmdListenerDelete {
+
+	skupperCmd := CmdListenerDelete{}
+
+	cmd := cobra.Command{
+		Use:     "delete <name>",
+		Short:   "delete a listener",
+		Long:    listenerDeleteLong,
+		Example: listenerDeleteExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			return skupperCmd.WaitUntilReady()
+		},
+	}
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdListenerDelete) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+}
+
+func (cmd *CmdListenerDelete) AddFlags() {
+	cmd.CobraCmd.Flags().DurationVarP(&cmd.flags.timeout, "timeout", "t", 60*time.Second, "Raise an error if the operation does not complete in the given period of time.")
+}
+
+func (cmd *CmdListenerDelete) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+
+	// Validate arguments name
+	if len(args) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("listener name must be specified"))
+	} else if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("listener name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("listener name is not valid: %s", err))
+		} else {
+			cmd.name = args[0]
+		}
+
+		if cmd.name != "" {
+			// Validate that there is already a listener with this name in the namespace
+			listener, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+			if err != nil {
+				validationErrors = append(validationErrors, err)
+			} else if listener == nil {
+				validationErrors = append(validationErrors, fmt.Errorf("listener %s does not exist in namespace %s", cmd.name, cmd.namespace))
+			}
+		}
+
+		//TBD what is valid timeout
+		if cmd.flags.timeout <= 0*time.Minute {
+			validationErrors = append(validationErrors, fmt.Errorf("timeout is not valid"))
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdListenerDelete) Run() error {
+	err := cmd.client.Listeners(cmd.namespace).Delete(context.TODO(), cmd.name, metav1.DeleteOptions{})
+	return err
+}
+
+func (cmd *CmdListenerDelete) WaitUntilReady() error {
+	waitTime := int(cmd.flags.timeout.Seconds())
+	err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
+
+		resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if err == nil && resource != nil {
+			return fmt.Errorf("error deleting the resource")
+		} else {
+			return nil
+		}
+	})
+
+	if err != nil {
+		return fmt.Errorf("Listener %q not deleted yet, check the logs for more information %s\n", cmd.name, err)
+	}
+
+	fmt.Printf("Listener %q deleted\n", cmd.name)
+	return nil
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_status.go b/internal/cmd/skupper/listener/kube/listener_status.go
new file mode 100644
index 00000000..eb1c595d
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_status.go
@@ -0,0 +1,156 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"text/tabwriter"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var (
+	listenerStatusLong    = "Display status of all listeners or a specific listener"
+	listenerStatusExample = "skupper listener status backend"
+)
+
+type ListenerStatus struct {
+	output string
+}
+
+type CmdListenerStatus struct {
+	client    skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd  cobra.Command
+	flags     ListenerStatus
+	namespace string
+	name      string
+	output    string
+}
+
+func NewCmdListenerStatus() *CmdListenerStatus {
+
+	skupperCmd := CmdListenerStatus{flags: ListenerStatus{}}
+
+	cmd := cobra.Command{
+		Use:     "status <name>",
+		Short:   "get status of listeners",
+		Long:    listenerStatusLong,
+		Example: listenerStatusExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+	}
+
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdListenerStatus) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+}
+
+func (cmd *CmdListenerStatus) AddFlags() {
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "print status of listeners Choices: json, yaml")
+}
+
+func (cmd *CmdListenerStatus) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+	outputTypeValidator := validator.NewOptionValidator(utils.OutputTypes)
+
+	// Validate arguments name if specified
+	if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if len(args) == 1 {
+		if args[0] == "" {
+			validationErrors = append(validationErrors, fmt.Errorf("listener name must not be empty"))
+		} else {
+			ok, err := resourceStringValidator.Evaluate(args[0])
+			if !ok {
+				validationErrors = append(validationErrors, fmt.Errorf("listener name is not valid: %s", err))
+			} else {
+				cmd.name = args[0]
+			}
+		}
+	}
+
+	// Validate that there is no listener with this name in the namespace
+	if cmd.name != "" {
+		listener, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if listener == nil || errors.IsNotFound(err) {
+			validationErrors = append(validationErrors, fmt.Errorf("listener %s does not exist in namespace %s", cmd.name, cmd.namespace))
+		}
+	}
+
+	if cmd.flags.output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.flags.output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.output = cmd.flags.output
+		}
+	}
+
+	return validationErrors
+}
+func (cmd *CmdListenerStatus) Run() error {
+	if cmd.name == "" {
+		resources, err := cmd.client.Listeners(cmd.namespace).List(context.TODO(), metav1.ListOptions{})
+		if err != nil || resources == nil || len(resources.Items) == 0 {
+			fmt.Println("No listeners found")
+			return err
+		}
+		if cmd.output != "" {
+			for _, resource := range resources.Items {
+				encodedOutput, err := utils.Encode(cmd.output, resource)
+				if err != nil {
+					return err
+				}
+				fmt.Println(encodedOutput)
+			}
+		} else {
+			tw := tabwriter.NewWriter(os.Stdout, 8, 8, 1, '\t', tabwriter.TabIndent)
+			_, _ = fmt.Fprintln(tw, fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s",
+				"NAME", "STATUS", "ROUTING-KEY", "HOST", "PORT", "MATCHING-CONNECTORS"))
+			for _, resource := range resources.Items {
+				fmt.Fprintln(tw, fmt.Sprintf("%s\t%s\t%s\t%s\t%d",
+					resource.Name, resource.Status.StatusMessage, resource.Spec.RoutingKey, resource.Spec.Host, resource.Spec.Port))
+			}
+			_ = tw.Flush()
+		}
+	} else {
+		resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if resource == nil || errors.IsNotFound(err) {
+			fmt.Println("No listeners found")
+			return err
+		}
+		if cmd.output != "" {
+			encodedOutput, err := utils.Encode(cmd.output, resource)
+			if err != nil {
+				return err
+			}
+			fmt.Println(encodedOutput)
+		} else {
+			tw := tabwriter.NewWriter(os.Stdout, 8, 8, 1, '\t', tabwriter.TabIndent)
+			fmt.Fprintln(tw, fmt.Sprintf("Name:\t%s\nStatus:\t%s\nRouting key:\t%s\nHost:\t%s\nPort:\t%d\nConnectors:\t\n",
+				resource.Name, resource.Status.StatusMessage, resource.Spec.RoutingKey, resource.Spec.Host, resource.Spec.Port))
+			_ = tw.Flush()
+		}
+	}
+
+	return nil
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_update.go b/internal/cmd/skupper/listener/kube/listener_update.go
new file mode 100644
index 00000000..6bceb437
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_update.go
@@ -0,0 +1,239 @@
+package kube
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+var (
+	listenerUpdateLong = `Clients at this site use the listener host and port to establish connections to the remote service.
+	The user can change port, host name, TLS secret, listener type and routing key`
+	listenerUpdateExample = "skupper listener update database --host mysql --port 3306"
+)
+
+type ListenerUpdates struct {
+	routingKey   string
+	host         string
+	tlsSecret    string
+	listenerType string
+	port         int
+	timeout      time.Duration
+	output       string
+}
+
+type CmdListenerUpdate struct {
+	client          skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd        cobra.Command
+	flags           ListenerUpdates
+	namespace       string
+	name            string
+	resourceVersion string
+	newSettings     ListenerUpdates
+	KubeClient      kubernetes.Interface
+}
+
+func NewCmdListenerUpdate() *CmdListenerUpdate {
+
+	skupperCmd := CmdListenerUpdate{flags: ListenerUpdates{}}
+
+	cmd := cobra.Command{
+		Use:     "update <name>",
+		Short:   "update a listener",
+		Long:    listenerUpdateLong,
+		Example: listenerUpdateExample,
+		PreRun:  skupperCmd.NewClient,
+		Run: func(cmd *cobra.Command, args []string) {
+			utils.HandleErrorList(skupperCmd.ValidateInput(args))
+			utils.HandleError(skupperCmd.Run())
+		},
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			return skupperCmd.WaitUntilReady()
+		},
+	}
+
+	skupperCmd.CobraCmd = cmd
+	skupperCmd.AddFlags()
+
+	return &skupperCmd
+}
+
+func (cmd *CmdListenerUpdate) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	utils.HandleError(err)
+
+	cmd.client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.namespace = cli.Namespace
+	cmd.KubeClient = cli.Kube
+}
+
+func (cmd *CmdListenerUpdate) AddFlags() {
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.routingKey, "routing-key", "r", "", "The identifier used to route traffic from listeners to connectors")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.host, "host", "", "The hostname or IP address of the local listener")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.tlsSecret, "tls-secret", "", "The name of a Kubernetes secret containing TLS credentials")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.listenerType, "type", "t", "tcp", "The listener type. Choices: [tcp|http].")
+	cmd.CobraCmd.Flags().IntVar(&cmd.flags.port, "port", 0, "The port of the local listener")
+	cmd.CobraCmd.Flags().DurationVar(&cmd.flags.timeout, "timeout", 60*time.Second, "Raise an error if the operation does not complete in the given period of time.")
+	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml")
+}
+
+func (cmd *CmdListenerUpdate) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+	numberValidator := validator.NewNumberValidator()
+	listenerTypeValidator := validator.NewOptionValidator(utils.ListenerTypes)
+	outputTypeValidator := validator.NewOptionValidator(utils.OutputTypes)
+
+	// Validate arguments name
+	if len(args) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("listener name must be configured"))
+	} else if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("listener name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("listener name is not valid: %s", err))
+		} else {
+			cmd.name = args[0]
+		}
+	}
+
+	// Validate that there is already a listener with this name in the namespace
+	if cmd.name != "" {
+		listener, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if listener == nil || errors.IsNotFound(err) {
+			validationErrors = append(validationErrors, fmt.Errorf("listener %s must exist in namespace %s to be updated", cmd.name, cmd.namespace))
+		} else {
+			// save existing values
+			cmd.resourceVersion = listener.ResourceVersion
+			cmd.newSettings.host = listener.Spec.Host
+			cmd.newSettings.port = listener.Spec.Port
+			cmd.newSettings.tlsSecret = listener.Spec.TlsCredentials
+			cmd.newSettings.listenerType = listener.Spec.Type
+		}
+	}
+
+	// Validate flags
+	if cmd.flags.routingKey != "" {
+		ok, err := resourceStringValidator.Evaluate(cmd.flags.routingKey)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("routing key is not valid: %s", err))
+		} else {
+			cmd.newSettings.routingKey = cmd.flags.routingKey
+		}
+	}
+	// TBD what validation should be done
+	if cmd.flags.host != "" {
+		cmd.newSettings.host = cmd.flags.host
+	}
+	if cmd.flags.tlsSecret != "" {
+		_, err := cmd.KubeClient.CoreV1().Secrets(cmd.namespace).Get(context.TODO(), cmd.flags.tlsSecret, metav1.GetOptions{})
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("tls-secret is not valid: does not exist"))
+		} else {
+			cmd.newSettings.tlsSecret = cmd.flags.tlsSecret
+		}
+	}
+	if cmd.flags.listenerType != "" {
+		ok, err := listenerTypeValidator.Evaluate(cmd.flags.listenerType)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("listener type is not valid: %s", err))
+		} else {
+			cmd.newSettings.listenerType = cmd.flags.listenerType
+		}
+	}
+	if cmd.flags.port != 0 {
+		ok, err := numberValidator.Evaluate(cmd.flags.port)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("listener port is not valid: %s", err))
+		} else {
+			cmd.newSettings.port = cmd.flags.port
+		}
+	}
+	//TBD what is valid timeout
+	if cmd.flags.timeout <= 0*time.Minute {
+		validationErrors = append(validationErrors, fmt.Errorf("timeout is not valid"))
+	}
+	if cmd.flags.output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.flags.output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.newSettings.output = cmd.flags.output
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdListenerUpdate) Run() error {
+
+	resource := v1alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v1alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:            cmd.name,
+			Namespace:       cmd.namespace,
+			ResourceVersion: cmd.resourceVersion},
+		Spec: v1alpha1.ListenerSpec{
+			Host:           cmd.newSettings.host,
+			Port:           cmd.newSettings.port,
+			RoutingKey:     cmd.newSettings.routingKey,
+			TlsCredentials: cmd.newSettings.tlsSecret,
+			Type:           cmd.newSettings.listenerType,
+		},
+	}
+
+	if cmd.newSettings.output != "" {
+		encodedOutput, err := utils.Encode(cmd.newSettings.output, resource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		_, err := cmd.client.Listeners(cmd.namespace).Update(context.TODO(), &resource, metav1.UpdateOptions{})
+		return err
+	}
+}
+
+func (cmd *CmdListenerUpdate) WaitUntilReady() error {
+
+	// the site resource was not created
+	if cmd.newSettings.output != "" {
+		return nil
+	}
+
+	waitTime := int(cmd.flags.timeout.Seconds())
+	err := utils.NewSpinnerWithTimeout("Waiting for update to complete...", waitTime, func() error {
+
+		resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+
+		if resource != nil && resource.Status.StatusMessage == "Ok" {
+			return nil
+		}
+
+		return fmt.Errorf("error getting the resource")
+	})
+
+	if err != nil {
+		return fmt.Errorf("Listener %q not ready yet, check the logs for more information\n", cmd.name)
+	}
+
+	fmt.Printf("Listener %q is ready\n", cmd.name)
+	return nil
+}
diff --git a/internal/cmd/skupper/listener/listener.go b/internal/cmd/skupper/listener/listener.go
index 26bc4be8..c6b59b61 100644
--- a/internal/cmd/skupper/listener/listener.go
+++ b/internal/cmd/skupper/listener/listener.go
@@ -1,23 +1,27 @@
 package listener
 
-import "github.com/spf13/cobra"
+import (
+	"github.com/skupperproject/skupper/internal/cmd/skupper/listener/kube"
+	"github.com/spf13/cobra"
+)
 
 func NewCmdListener() *cobra.Command {
 
 	cmd := &cobra.Command{
-		Use:     "listener",
-		Short:   "",
-		Long:    ``,
-		Example: ``,
+		Use:   "listener",
+		Short: "Binds target workloads in the local site to target workloads in remote sites.",
+		Long:  `A listener is a connection endpoint in the local site and binds to target workloads in remote sites`,
+		Example: `skupper listener create my-listener 8080
+skupper listener status my-listener`,
 	}
 
-	listenerCreateCommand := NewCmdListenerCreate()
-	listenerGetCommand := NewCmdListenerGet()
-	listenerUpdateCommand := NewCmdListenerUpdate()
-	listenerDeleteCommand := NewCmdListenerDelete()
+	listenerCreateCommand := kube.NewCmdListenerCreate()
+	listenerStatusCommand := kube.NewCmdListenerStatus()
+	listenerUpdateCommand := kube.NewCmdListenerUpdate()
+	listenerDeleteCommand := kube.NewCmdListenerDelete()
 
 	cmd.AddCommand(&listenerCreateCommand.CobraCmd)
-	cmd.AddCommand(&listenerGetCommand.CobraCmd)
+	cmd.AddCommand(&listenerStatusCommand.CobraCmd)
 	cmd.AddCommand(&listenerUpdateCommand.CobraCmd)
 	cmd.AddCommand(&listenerDeleteCommand.CobraCmd)
 
diff --git a/internal/cmd/skupper/listener/listener_create.go b/internal/cmd/skupper/listener/listener_create.go
deleted file mode 100644
index 724a1640..00000000
--- a/internal/cmd/skupper/listener/listener_create.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package listener
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	listenerCreateLong    = ""
-	listenerCreateExample = ""
-)
-
-type CmdListenerCreate struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdListenerCreate() *CmdListenerCreate {
-
-	skupperCmd := CmdListenerCreate{}
-
-	cmd := cobra.Command{
-		Use:     "create",
-		Short:   "",
-		Long:    listenerCreateLong,
-		Example: listenerCreateExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdListenerCreate) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdListenerCreate) AddFlags()                                            {}
-func (cmd *CmdListenerCreate) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdListenerCreate) InputToOptions()                                      {}
-func (cmd *CmdListenerCreate) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdListenerCreate) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/listener/listener_delete.go b/internal/cmd/skupper/listener/listener_delete.go
deleted file mode 100644
index 39d5ba95..00000000
--- a/internal/cmd/skupper/listener/listener_delete.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package listener
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	listenerDeleteLong    = ""
-	listenerDeleteExample = ""
-)
-
-type CmdListenerDelete struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdListenerDelete() *CmdListenerDelete {
-
-	skupperCmd := CmdListenerDelete{}
-
-	cmd := cobra.Command{
-		Use:     "delete",
-		Short:   "",
-		Long:    listenerDeleteLong,
-		Example: listenerDeleteExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdListenerDelete) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdListenerDelete) AddFlags()                                            {}
-func (cmd *CmdListenerDelete) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdListenerDelete) InputToOptions()                                      {}
-func (cmd *CmdListenerDelete) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdListenerDelete) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/listener/listener_get.go b/internal/cmd/skupper/listener/listener_get.go
deleted file mode 100644
index c7967e6e..00000000
--- a/internal/cmd/skupper/listener/listener_get.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package listener
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	listenerGetLong    = ""
-	listenerGetExample = ""
-)
-
-type CmdListenerGet struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdListenerGet() *CmdListenerGet {
-
-	skupperCmd := CmdListenerGet{}
-
-	cmd := cobra.Command{
-		Use:     "get",
-		Short:   "",
-		Long:    listenerGetLong,
-		Example: listenerGetExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdListenerGet) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdListenerGet) AddFlags()                                            {}
-func (cmd *CmdListenerGet) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdListenerGet) InputToOptions()                                      {}
-func (cmd *CmdListenerGet) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdListenerGet) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/listener/listener_update.go b/internal/cmd/skupper/listener/listener_update.go
deleted file mode 100644
index def5b1f2..00000000
--- a/internal/cmd/skupper/listener/listener_update.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package listener
-
-import (
-	"fmt"
-
-	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
-	"github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/spf13/cobra"
-)
-
-var (
-	listenerUpdateLong    = ""
-	listenerUpdateExample = ""
-)
-
-type CmdListenerUpdate struct {
-	client   *client.KubeClient
-	CobraCmd cobra.Command
-}
-
-func NewCmdListenerUpdate() *CmdListenerUpdate {
-
-	skupperCmd := CmdListenerUpdate{}
-
-	cmd := cobra.Command{
-		Use:     "update",
-		Short:   "",
-		Long:    listenerUpdateLong,
-		Example: listenerUpdateExample,
-		PreRun:  skupperCmd.NewClient,
-		Run: func(cmd *cobra.Command, args []string) {
-			utils.HandleErrorList(skupperCmd.ValidateInput(args))
-			skupperCmd.InputToOptions()
-			utils.HandleError(skupperCmd.Run())
-		},
-	}
-
-	skupperCmd.CobraCmd = cmd
-
-	return &skupperCmd
-}
-
-func (cmd *CmdListenerUpdate) NewClient(cobraCommand *cobra.Command, args []string) {}
-func (cmd *CmdListenerUpdate) AddFlags()                                            {}
-func (cmd *CmdListenerUpdate) ValidateInput(args []string) []error                  { return nil }
-func (cmd *CmdListenerUpdate) InputToOptions()                                      {}
-func (cmd *CmdListenerUpdate) Run() error                                           { fmt.Println("Not implemented yet."); return nil }
-func (cmd *CmdListenerUpdate) WaitUntilReady() error                                { return nil }
diff --git a/internal/cmd/skupper/site/kube/site_create.go b/internal/cmd/skupper/site/kube/site_create.go
index cbf3b210..bf15c6c3 100644
--- a/internal/cmd/skupper/site/kube/site_create.go
+++ b/internal/cmd/skupper/site/kube/site_create.go
@@ -6,6 +6,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"k8s.io/client-go/kubernetes"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
 	"github.com/skupperproject/skupper/internal/kube/client"
@@ -32,6 +33,7 @@ type CreateFlags struct {
 
 type CmdSiteCreate struct {
 	Client             skupperv1alpha1.SkupperV1alpha1Interface
+	KubeClient         kubernetes.Interface
 	CobraCmd           cobra.Command
 	flags              CreateFlags
 	options            map[string]string
@@ -73,6 +75,7 @@ func (cmd *CmdSiteCreate) NewClient(cobraCommand *cobra.Command, args []string)
 	utils.HandleError(err)
 
 	cmd.Client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.KubeClient = cli.GetKubeClient()
 	cmd.Namespace = cli.Namespace
 }
 
@@ -81,7 +84,7 @@ func (cmd *CmdSiteCreate) AddFlags() {
 	cmd.CobraCmd.Flags().StringVar(&cmd.flags.linkAccessType, "link-access-type", "", `configure external access for links from remote sites.
 Choices: [route|loadbalancer]. Default: On OpenShift, route is the default; 
 for other Kubernetes flavors, loadbalancer is the default.`)
-	cmd.CobraCmd.Flags().StringVar(&cmd.flags.serviceAccount, "service-account", "skupper-controller", "the Kubernetes service account under which to run the Skupper controller")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.serviceAccount, "service-account", "", "the Kubernetes service account under which to run the Skupper controller")
 	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml")
 }
 
@@ -123,8 +126,8 @@ func (cmd *CmdSiteCreate) ValidateInput(args []string) []error {
 	}
 
 	if cmd.flags.serviceAccount != "" {
-		ok, err := resourceStringValidator.Evaluate(cmd.flags.serviceAccount)
-		if !ok {
+		svcAccount, err := cmd.KubeClient.CoreV1().ServiceAccounts(cmd.Namespace).Get(context.TODO(), cmd.flags.serviceAccount, metav1.GetOptions{})
+		if err != nil || svcAccount == nil {
 			validationErrors = append(validationErrors, fmt.Errorf("service account name is not valid: %s", err))
 		}
 	}
diff --git a/internal/cmd/skupper/site/kube/site_update.go b/internal/cmd/skupper/site/kube/site_update.go
index 7d8415d4..f1ef7a08 100644
--- a/internal/cmd/skupper/site/kube/site_update.go
+++ b/internal/cmd/skupper/site/kube/site_update.go
@@ -3,6 +3,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"k8s.io/client-go/kubernetes"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
 	"github.com/skupperproject/skupper/internal/kube/client"
@@ -27,6 +28,7 @@ type UpdateFlags struct {
 
 type CmdSiteUpdate struct {
 	Client             skupperv1alpha1.SkupperV1alpha1Interface
+	KubeClient         kubernetes.Interface
 	CobraCmd           cobra.Command
 	flags              UpdateFlags
 	options            map[string]string
@@ -68,6 +70,7 @@ func (cmd *CmdSiteUpdate) NewClient(cobraCommand *cobra.Command, args []string)
 	utils.HandleError(err)
 
 	cmd.Client = cli.GetSkupperClient().SkupperV1alpha1()
+	cmd.KubeClient = cli.GetKubeClient()
 	cmd.Namespace = cli.Namespace
 }
 
@@ -76,14 +79,13 @@ func (cmd *CmdSiteUpdate) AddFlags() {
 	cmd.CobraCmd.Flags().StringVar(&cmd.flags.linkAccessType, "link-access-type", "", `configure external access for links from remote sites.
 Choices: [route|loadbalancer]. Default: On OpenShift, route is the default; 
 for other Kubernetes flavors, loadbalancer is the default.`)
-	cmd.CobraCmd.Flags().StringVar(&cmd.flags.serviceAccount, "service-account", "skupper-controller", "the Kubernetes service account under which to run the Skupper controller")
+	cmd.CobraCmd.Flags().StringVar(&cmd.flags.serviceAccount, "service-account", "", "the Kubernetes service account under which to run the Skupper controller")
 	cmd.CobraCmd.Flags().StringVarP(&cmd.flags.output, "output", "o", "", "print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml")
 }
 
 func (cmd *CmdSiteUpdate) ValidateInput(args []string) []error {
 
 	var validationErrors []error
-	resourceStringValidator := validator.NewResourceStringValidator()
 	linkAccessTypeValidator := validator.NewOptionValidator(utils.LinkAccessTypes)
 	outputTypeValidator := validator.NewOptionValidator(utils.OutputTypes)
 
@@ -128,8 +130,8 @@ func (cmd *CmdSiteUpdate) ValidateInput(args []string) []error {
 	}
 
 	if cmd.flags.serviceAccount != "" {
-		ok, err := resourceStringValidator.Evaluate(cmd.flags.serviceAccount)
-		if !ok {
+		svcAccount, err := cmd.KubeClient.CoreV1().ServiceAccounts(cmd.Namespace).Get(context.TODO(), cmd.flags.serviceAccount, metav1.GetOptions{})
+		if err != nil || svcAccount == nil {
 			validationErrors = append(validationErrors, fmt.Errorf("service account name is not valid: %s", err))
 		}
 	}
diff --git a/internal/cmd/skupper/utils/constants.go b/internal/cmd/skupper/utils/constants.go
index 579b289c..cd44e14d 100644
--- a/internal/cmd/skupper/utils/constants.go
+++ b/internal/cmd/skupper/utils/constants.go
@@ -3,4 +3,5 @@ package utils
 var (
 	LinkAccessTypes = []string{"route", "loadbalancer", "default"}
 	OutputTypes     = []string{"json", "yaml"}
+	ListenerTypes   = []string{"tcp"}
 )
diff --git a/internal/non-kube/client/compat/container_mock.go b/internal/non-kube/client/compat/container_mock.go
index f357e0a7..6f1a7868 100644
--- a/internal/non-kube/client/compat/container_mock.go
+++ b/internal/non-kube/client/compat/container_mock.go
@@ -12,12 +12,12 @@ import (
 	"github.com/go-openapi/runtime"
 	"github.com/go-openapi/strfmt"
 	"github.com/google/uuid"
-	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper-libpod/v4/client/containers_compat"
 	"github.com/skupperproject/skupper-libpod/v4/client/networks_compat"
 	"github.com/skupperproject/skupper-libpod/v4/client/system_compat"
 	"github.com/skupperproject/skupper-libpod/v4/client/volumes_compat"
 	"github.com/skupperproject/skupper-libpod/v4/models"
+	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/skupperproject/skupper/pkg/container"
 )
diff --git a/internal/non-kube/client/compat/network.go b/internal/non-kube/client/compat/network.go
index 409aae00..fd7b3066 100644
--- a/internal/non-kube/client/compat/network.go
+++ b/internal/non-kube/client/compat/network.go
@@ -6,9 +6,9 @@ import (
 
 	"github.com/go-openapi/runtime"
 	"github.com/go-openapi/strfmt"
-	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper-libpod/v4/client/networks_compat"
 	"github.com/skupperproject/skupper-libpod/v4/models"
+	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/container"
 	"github.com/skupperproject/skupper/pkg/utils"
 )
diff --git a/internal/non-kube/client/compat/volume.go b/internal/non-kube/client/compat/volume.go
index 8549d2cf..1aa1065f 100644
--- a/internal/non-kube/client/compat/volume.go
+++ b/internal/non-kube/client/compat/volume.go
@@ -6,9 +6,9 @@ import (
 
 	"github.com/go-openapi/runtime"
 	"github.com/go-openapi/strfmt"
-	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper-libpod/v4/client/volumes_compat"
 	"github.com/skupperproject/skupper-libpod/v4/models"
+	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/container"
 )
 
diff --git a/pkg/apis/skupper/v1alpha1/types.go b/pkg/apis/skupper/v1alpha1/types.go
index 99a474ea..67a1869f 100644
--- a/pkg/apis/skupper/v1alpha1/types.go
+++ b/pkg/apis/skupper/v1alpha1/types.go
@@ -147,7 +147,6 @@ type Listener struct {
 	Status        Status       `json:"status,omitempty"`
 }
 
-
 func (l *Listener) Protocol() corev1.Protocol {
 	if l.Spec.Type == "udp" {
 		return corev1.ProtocolUDP
@@ -266,9 +265,9 @@ type AccessTokenList struct {
 }
 
 type AccessTokenSpec struct {
-	Url    string `json:"url"`
-	Code   string `json:"code"`
-	Ca     string `json:"ca"`
+	Url  string `json:"url"`
+	Code string `json:"code"`
+	Ca   string `json:"ca"`
 }
 
 type AccessTokenStatus struct {
@@ -306,7 +305,7 @@ type AccessGrantStatus struct {
 	Url        string `json:"url"`
 	Code       string `json:"code"`
 	Ca         string `json:"ca"`
-	Redeemed  int    `json:"redeemed,omitempty"`
+	Redeemed   int    `json:"redeemed,omitempty"`
 	Expiration string `json:"expiration,omitempty"`
 	Status     string `json:"status,omitempty"`
 }
diff --git a/pkg/kube/site/ports.go b/pkg/kube/site/ports.go
index ed0248de..e05e59b9 100644
--- a/pkg/kube/site/ports.go
+++ b/pkg/kube/site/ports.go
@@ -63,7 +63,6 @@ func (p ExposedPorts) Unexpose(host string, portname string) *ExposedPortSet {
 	return nil
 }
 
-
 func toServicePorts(desired map[string]Port) map[string]corev1.ServicePort {
 	results := map[string]corev1.ServicePort{}
 	for name, details := range desired {
diff --git a/pkg/site/bindings.go b/pkg/site/bindings.go
index b7f1b9e5..24a72b11 100644
--- a/pkg/site/bindings.go
+++ b/pkg/site/bindings.go
@@ -7,8 +7,8 @@ import (
 	"github.com/skupperproject/skupper/pkg/qdr"
 )
 
-type ListenerConfiguration func (siteId string, listener *skupperv1alpha1.Listener, config *qdr.BridgeConfig)
-type ConnectorConfiguration func (siteId string, connector *skupperv1alpha1.Connector, config *qdr.BridgeConfig)
+type ListenerConfiguration func(siteId string, listener *skupperv1alpha1.Listener, config *qdr.BridgeConfig)
+type ConnectorConfiguration func(siteId string, connector *skupperv1alpha1.Connector, config *qdr.BridgeConfig)
 
 type BindingEventHandler interface {
 	ListenerUpdated(listener *skupperv1alpha1.Listener)
@@ -22,7 +22,7 @@ type Bindings struct {
 	connectors map[string]*skupperv1alpha1.Connector
 	listeners  map[string]*skupperv1alpha1.Listener
 	handler    BindingEventHandler
-	configure  struct{
+	configure  struct {
 		listener  ListenerConfiguration
 		connector ConnectorConfiguration
 	}
diff --git a/pkg/utils/validator/simple_validator.go b/pkg/utils/validator/simple_validator.go
index 15617b0f..6e318d87 100644
--- a/pkg/utils/validator/simple_validator.go
+++ b/pkg/utils/validator/simple_validator.go
@@ -37,6 +37,17 @@ func NewResourceStringValidator() *StringValidator {
 	}
 }
 
+func NewWorkloadStringValidator() *StringValidator {
+	re, err := regexp.Compile("^[A-Za-z0-9=:./-]+$")
+	if err != nil {
+		fmt.Printf("Error compiling regex: %v", err)
+		return nil
+	}
+	return &StringValidator{
+		Expression: re,
+	}
+}
+
 func (s StringValidator) Evaluate(value interface{}) (bool, error) {
 	v, ok := value.(string)
 


Test patch:
diff --git a/internal/cmd/skupper/connector/connector_create_test.go b/internal/cmd/skupper/connector/connector_create_test.go
deleted file mode 100644
index d7cfddd5..00000000
--- a/internal/cmd/skupper/connector/connector_create_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package connector
diff --git a/internal/cmd/skupper/connector/connector_delete_test.go b/internal/cmd/skupper/connector/connector_delete_test.go
deleted file mode 100644
index d7cfddd5..00000000
--- a/internal/cmd/skupper/connector/connector_delete_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package connector
diff --git a/internal/cmd/skupper/connector/connector_get_test.go b/internal/cmd/skupper/connector/connector_get_test.go
deleted file mode 100644
index d7cfddd5..00000000
--- a/internal/cmd/skupper/connector/connector_get_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package connector
diff --git a/internal/cmd/skupper/connector/connector_update_test.go b/internal/cmd/skupper/connector/connector_update_test.go
deleted file mode 100644
index d7cfddd5..00000000
--- a/internal/cmd/skupper/connector/connector_update_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package connector
diff --git a/internal/cmd/skupper/connector/kube/connector_create_test.go b/internal/cmd/skupper/connector/kube/connector_create_test.go
new file mode 100644
index 00000000..183ad460
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_create_test.go
@@ -0,0 +1,924 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"github.com/spf13/pflag"
+	"gotest.tools/assert"
+	v12 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	kubefake "k8s.io/client-go/kubernetes/fake"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdConnectorCreate_NewCmdConnectorCreate(t *testing.T) {
+
+	t.Run("connector command", func(t *testing.T) {
+
+		result := NewCmdConnectorCreate()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+		assert.Check(t, result.CobraCmd.PostRunE != nil)
+		assert.Check(t, result.CobraCmd.Flags() != nil)
+	})
+
+}
+
+func TestCmdConnectorCreate_AddFlags(t *testing.T) {
+
+	expectedFlagsWithDefaultValue := map[string]interface{}{
+		"routing-key":       "",
+		"host":              "",
+		"tls-secret":        "",
+		"type":              "tcp",
+		"workload":          "",
+		"selector":          "",
+		"include-not-ready": "false",
+		"timeout":           "1m0s",
+		"output":            "",
+	}
+	var flagList []string
+
+	cmd := newCmdConnectorCreateWithMocks()
+
+	t.Run("add flags", func(t *testing.T) {
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+		})
+
+		assert.Check(t, len(flagList) == 0)
+
+		cmd.AddFlags()
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] != nil)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] == flag.DefValue)
+		})
+
+		assert.Check(t, len(flagList) == len(expectedFlagsWithDefaultValue))
+
+	})
+
+}
+
+func TestCmdConnectorCreate_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdConnectorCreate)
+		expectedErrors []string
+	}
+
+	command := &CmdConnectorCreate{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "connector is not created because there is already the same connector in the namespace",
+			args: []string{"my-connector", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("*", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					switch action.(type) {
+					case testing2.GetAction:
+						connector := v1alpha1.Connector{
+							ObjectMeta: v1.ObjectMeta{
+								Name:      "my-connector",
+								Namespace: "test",
+							},
+							Spec: v1alpha1.ConnectorSpec{
+								Port:     8080,
+								Type:     "tcp",
+								Host:     "test",
+								Selector: "mySelector",
+							},
+							Status: v1alpha1.Status{
+								StatusMessage: "Ok",
+							},
+						}
+						return true, &connector, nil
+					case testing2.ListAction:
+						site := v1alpha1.SiteList{
+							Items: []v1alpha1.Site{
+								{
+									ObjectMeta: v1.ObjectMeta{
+										Name:      "site1",
+										Namespace: "test",
+									},
+								},
+							},
+						}
+						return true, &site, nil
+					}
+					return false, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"there is already a connector my-connector created for namespace test"},
+		},
+		{
+			name: "connector no site",
+			args: []string{"connector-site", "8090"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					host:    "127.0.0.1",
+					timeout: 1 * time.Minute,
+				}
+
+			},
+			expectedErrors: []string{"A site must exist in namespace test before a connector can be created"},
+		},
+		{
+			name: "connector name and port are not specified",
+			args: []string{},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector name and port must be configured"},
+		},
+		{
+			name: "connector name empty",
+			args: []string{"", "8090"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name: "connector port empty",
+			args: []string{"my-name-port-empty", ""},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector port must not be empty"},
+		},
+		{
+			name: "connector port not positive",
+			args: []string{"my-port-positive", "-45"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector port is not valid: value is not positive"},
+		},
+		{
+			name: "connector name and port are not specified",
+			args: []string{},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector name and port must be configured"},
+		},
+		{
+			name: "connector port is not specified",
+			args: []string{"my-name"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector name and port must be configured"},
+		},
+		{
+			name: "more than two arguments are specified",
+			args: []string{"my", "connector", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"only two arguments are allowed for this command"},
+		},
+		{
+			name: "connector name is not valid.",
+			args: []string{"my new connector", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "port is not valid.",
+			args: []string{"my-connector-port", "abcd"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "backend",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{"connector port is not valid: strconv.Atoi: parsing \"abcd\": invalid syntax"},
+		},
+		{
+			name: "connector type is not valid",
+			args: []string{"my-connector-type", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					connectorType: "not-valid",
+					selector:      "backend",
+					timeout:       60 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"connector type is not valid: value not-valid not allowed. It should be one of this options: [tcp]"},
+		},
+		{
+			name: "routing key is not valid",
+			args: []string{"my-connector-rk", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					routingKey: "not-valid$",
+					selector:   "backend",
+					timeout:    5 * time.Minute,
+				}
+			},
+			expectedErrors: []string{
+				"routing key is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "tls-secret does not exist",
+			args: []string{"my-connector-tls", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					tlsSecret: "not-valid",
+					selector:  "backend",
+					timeout:   30 * time.Second,
+				}
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("secret not found")
+				})
+				command.KubeClient = fakeKubeClient
+			},
+			expectedErrors: []string{
+				"tls-secret is not valid: does not exist"},
+		},
+		{
+			name: "workload is not valid",
+			args: []string{"bad-workload", "1234"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					workload: "@345",
+					timeout:  30 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"workload is not valid: value does not match this regular expression: ^[A-Za-z0-9=:./-]+$"},
+		},
+		{
+			name: "selector is not valid",
+			args: []string{"bad-selector", "1234"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					selector: "@#$%",
+					timeout:  30 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"selector is not valid: value does not match this regular expression: ^[A-Za-z0-9=:./-]+$"},
+		},
+		{
+			name: "timeout is not valid",
+			args: []string{"bad-timeout", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					workload: "workload",
+					timeout:  0 * time.Second}
+			},
+			expectedErrors: []string{
+				"timeout is not valid"},
+		},
+		{
+			name: "output is not valid",
+			args: []string{"bad-output", "1234"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					output:   "not-supported",
+					selector: "backend",
+					timeout:  30 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name: "missing selector/host/workload",
+			args: []string{"missing", "1234"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					output:  "json",
+					timeout: 30 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"One of the following options must be set: workload, selector, host"},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-connector-flags", "8080"},
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorCreate{
+					host:            "hostname",
+					routingKey:      "routingkeyname",
+					tlsSecret:       "secretname",
+					connectorType:   "tcp",
+					selector:        "backend",
+					includeNotReady: true,
+					timeout:         30 * time.Second,
+					output:          "json",
+				}
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.KubeClient = fakeKubeClient
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdConnectorCreate_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdConnectorCreate)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("create", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					createAction, ok := action.(testing2.CreateActionImpl)
+					if !ok {
+						return
+					}
+					createdObject := createAction.GetObject()
+
+					connector, ok := createdObject.(*v1alpha1.Connector)
+					if !ok {
+						return
+					}
+
+					if connector.Name != "my-connector" {
+						return true, nil, fmt.Errorf("unexpected name value")
+					}
+					if connector.Spec.Type != "tcp" {
+						return true, nil, fmt.Errorf("unexpected type value")
+					}
+					if connector.Spec.Port != 8080 {
+						return true, nil, fmt.Errorf("unexpected port value")
+					}
+					if connector.Spec.Host != "hostname" {
+						return true, nil, fmt.Errorf("unexpected host value")
+					}
+					if connector.Spec.RoutingKey != "keyname" {
+						return true, nil, fmt.Errorf("unexpected routing key value")
+					}
+					if connector.Spec.TlsCredentials != "secretname" {
+						return true, nil, fmt.Errorf("unexpected tls-secret value")
+					}
+					// TBD workload not in connector CRD
+					//if connector.Spec.Workload != "deployment/backend" {
+					//	return true, nil, fmt.Errorf("unexpected workload value")
+					//}
+					if connector.Spec.Selector != "backend" {
+						return true, nil, fmt.Errorf("unexpected selector value")
+					}
+					if connector.Spec.IncludeNotReady != true {
+						return true, nil, fmt.Errorf("unexpected Include Not Ready value")
+					}
+					return true, nil, nil
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.name = "my-connector"
+				command.port = 8080
+				command.flags.connectorType = "tcp"
+				command.flags.host = "hostname"
+				command.flags.routingKey = "keyname"
+				command.flags.tlsSecret = "secretname"
+				command.flags.includeNotReady = true
+				command.flags.workload = "deployment/backend"
+				command.flags.selector = "backend"
+			},
+		},
+		{
+			name: "run fails",
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("create", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("error")
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.name = "my-fail-connector"
+				command.output = "json"
+			},
+			errorMessage: "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdConnectorCreateWithMocks()
+		test.setUpMock(cmd)
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdConnectorCreate_WaitUntilReady(t *testing.T) {
+	type test struct {
+		name        string
+		setUpMock   func(command *CmdConnectorCreate)
+		expectError bool
+	}
+
+	testTable := []test{
+		{
+			name: "connector is not ready",
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-connector",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "connector is not returned",
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("it failed")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "connector is ready",
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-connector",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: false,
+		},
+		{
+			name: "connector is ready yaml output",
+			setUpMock: func(command *CmdConnectorCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-connector",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+				command.output = "yaml"
+			},
+			expectError: false,
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdConnectorCreateWithMocks()
+		cmd.name = "my-connector"
+		cmd.flags = ConnectorCreate{timeout: 1 * time.Second}
+
+		test.setUpMock(cmd)
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.WaitUntilReady()
+			if err != nil {
+				assert.Check(t, test.expectError)
+			}
+
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdConnectorCreateWithMocks() *CmdConnectorCreate {
+	cmdConnectorCreate := &CmdConnectorCreate{
+		client:     &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace:  "test",
+		KubeClient: kubefake.NewSimpleClientset(),
+	}
+
+	return cmdConnectorCreate
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_delete_test.go b/internal/cmd/skupper/connector/kube/connector_delete_test.go
new file mode 100644
index 00000000..a96e1845
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_delete_test.go
@@ -0,0 +1,315 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"github.com/spf13/pflag"
+	"gotest.tools/assert"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdConnectorDelete_NewCmdConnectorDelete(t *testing.T) {
+
+	t.Run("Delete command", func(t *testing.T) {
+
+		result := NewCmdConnectorDelete()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+		assert.Check(t, result.CobraCmd.PostRunE != nil)
+		assert.Check(t, result.CobraCmd.Flags() != nil)
+	})
+
+}
+
+func TestCmdConnectorDelete_AddFlags(t *testing.T) {
+
+	expectedFlagsWithDefaultValue := map[string]interface{}{
+		"timeout": "1m0s",
+	}
+	var flagList []string
+
+	cmd := newCmdConnectorDeleteWithMocks()
+
+	t.Run("add flags", func(t *testing.T) {
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+		})
+
+		assert.Check(t, len(flagList) == 0)
+
+		cmd.AddFlags()
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] != nil)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] == flag.DefValue)
+		})
+
+		assert.Check(t, len(flagList) == len(expectedFlagsWithDefaultValue))
+	})
+
+}
+func TestCmdConnectorDelete_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdConnectorDelete)
+		expectedErrors []string
+	}
+
+	command := &CmdConnectorDelete{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "connector is not Deleted because get connector returned error",
+			args: []string{"my-connector"},
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorDelete{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"NotFound"},
+		},
+		{
+			name: "connector is not Deleted because connector does not exist in the namespace",
+			args: []string{"my-connector"},
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorDelete{timeout: 30 * time.Second}
+			},
+			expectedErrors: []string{"connector my-connector does not exist in namespace test"},
+		},
+		{
+			name: "connector name is not specified",
+			args: []string{},
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorDelete{timeout: 1 * time.Second}
+			},
+			expectedErrors: []string{"connector name must be specified"},
+		},
+		{
+			name: "connector name is nil",
+			args: []string{""},
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorDelete{timeout: 1 * time.Second}
+			},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name: "connector name is not valid",
+			args: []string{"my name"},
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorDelete{timeout: 1 * time.Second}
+			},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "more than one argument is specified",
+			args: []string{"my", "connector"},
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorDelete{timeout: 1 * time.Second}
+			},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name: "timeout is not valid",
+			args: []string{"bad-timeout"},
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorDelete{timeout: 0 * time.Second}
+			},
+			expectedErrors: []string{
+				"timeout is not valid"},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdConnectorDelete_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdConnectorDelete)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("Delete", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-connector"
+			},
+		},
+		{
+			name: "run fails",
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("Delete", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("error")
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-fail-connector"
+			},
+			errorMessage: "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdConnectorDeleteWithMocks()
+		test.setUpMock(cmd)
+
+		//create a connector
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdConnectorDelete_WaitUntilReady(t *testing.T) {
+	type test struct {
+		name        string
+		setUpMock   func(command *CmdConnectorDelete)
+		expectError bool
+	}
+
+	testTable := []test{
+		{
+			name: "error deleting connector",
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, &v1alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-connector",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "connector is not returned",
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("it failed")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "connector is deleted",
+			setUpMock: func(command *CmdConnectorDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: false,
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdConnectorDeleteWithMocks()
+		cmd.name = "my-connector"
+		cmd.flags = ConnectorDelete{timeout: 5 * time.Second}
+
+		test.setUpMock(cmd)
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.WaitUntilReady()
+			if err != nil {
+				assert.Check(t, test.expectError)
+			}
+
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdConnectorDeleteWithMocks() *CmdConnectorDelete {
+
+	cmdConnectorDelete := &CmdConnectorDelete{
+		client:    &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace: "test",
+	}
+
+	return cmdConnectorDelete
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_status_test.go b/internal/cmd/skupper/connector/kube/connector_status_test.go
new file mode 100644
index 00000000..dc770ce6
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_status_test.go
@@ -0,0 +1,451 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"gotest.tools/assert"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdConnectorStatus_NewCmdConnectorStatus(t *testing.T) {
+
+	t.Run("Status command", func(t *testing.T) {
+
+		result := NewCmdConnectorStatus()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+	})
+
+}
+
+func TestCmdConnectorStatus_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdConnectorStatus)
+		expectedErrors []string
+	}
+
+	command := &CmdConnectorStatus{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "connector is not shown because connector does not exist in the namespace",
+			args: []string{"my-connector"},
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"connector my-connector does not exist in namespace test"},
+		},
+		{
+			name: "connector name is nil",
+			args: []string{""},
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name: "more than one argument is specified",
+			args: []string{"my", "connector"},
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name: "connector name is not valid.",
+			args: []string{"my new connector"},
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "no args",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{},
+		},
+		{
+			name: "bad output status",
+			args: []string{"out-connector"},
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags.output = "not-supported"
+			},
+			expectedErrors: []string{"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name: "good output status",
+			args: []string{"out-connector"},
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags.output = "json"
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdConnectorStatus_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdConnectorStatus)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-connector"
+			},
+		},
+		{
+			name: "run fails no connectors found",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+			},
+			errorMessage: "NotFound",
+		},
+		{
+			name: "run fails connector doesn't exist",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-fail-connector"
+			},
+			errorMessage: "NotFound",
+		},
+		{
+			name: "runs ok, returns 1 connectors",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					connector := v1alpha1.ConnectorList{
+						Items: []v1alpha1.Connector{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:     8080,
+									Type:     "tcp",
+									Host:     "test",
+									Selector: "backend",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &connector, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-connector"
+			},
+		},
+		{
+			name: "runs ok, returns 1 connectors yaml",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					connector := v1alpha1.ConnectorList{
+						Items: []v1alpha1.Connector{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:     8080,
+									Type:     "tcp",
+									Host:     "test",
+									Selector: "backend",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &connector, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-connector"
+				command.output = "yaml"
+			},
+		},
+		{
+			name: "runs ok, returns all connectors",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					connector := v1alpha1.ConnectorList{
+						Items: []v1alpha1.Connector{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector1",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:       8080,
+									Type:       "tcp",
+									Host:       "test1",
+									RoutingKey: "key1",
+									Selector:   "backend1",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector2",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:       8888,
+									Type:       "tcp",
+									Host:       "test2",
+									RoutingKey: "key2",
+									Selector:   "backend2",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &connector, nil
+				})
+				command.client = fakeSkupperClient
+			},
+		},
+		{
+			name: "runs ok, returns all connectors json",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					connector := v1alpha1.ConnectorList{
+						Items: []v1alpha1.Connector{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector1",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:     8080,
+									Type:     "tcp",
+									Host:     "test1",
+									Selector: "backend1",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector2",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:       8888,
+									Type:       "tcp",
+									Host:       "test2",
+									RoutingKey: "key2",
+									Selector:   "backend2",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &connector, nil
+				})
+				command.client = fakeSkupperClient
+				command.output = "json"
+			},
+		},
+		{
+			name: "runs ok, returns all connectors output bad",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					connector := v1alpha1.ConnectorList{
+						Items: []v1alpha1.Connector{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector1",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:     8080,
+									Type:     "tcp",
+									Host:     "test1",
+									Selector: "backend1",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector2",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:       8888,
+									Type:       "tcp",
+									Host:       "test2",
+									RoutingKey: "key2",
+									Selector:   "backend2",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &connector, nil
+				})
+				command.client = fakeSkupperClient
+				command.output = "bad"
+			},
+			errorMessage: "format bad not supported",
+		},
+		{
+			name: "runs ok, returns 1 connectors bad output",
+			setUpMock: func(command *CmdConnectorStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					connector := v1alpha1.ConnectorList{
+						Items: []v1alpha1.Connector{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-connector",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ConnectorSpec{
+									Port:     8080,
+									Type:     "tcp",
+									Host:     "test",
+									Selector: "backend",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &connector, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-connector"
+				command.output = "bad"
+			},
+			errorMessage: "format bad not supported",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdConnectorStatusWithMocks()
+		test.setUpMock(cmd)
+
+		//create a connector
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdConnectorStatusWithMocks() *CmdConnectorStatus {
+
+	cmdConnectorStatus := &CmdConnectorStatus{
+		client:    &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace: "test",
+	}
+
+	return cmdConnectorStatus
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_update_test.go b/internal/cmd/skupper/connector/kube/connector_update_test.go
new file mode 100644
index 00000000..d40d29a0
--- /dev/null
+++ b/internal/cmd/skupper/connector/kube/connector_update_test.go
@@ -0,0 +1,546 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"github.com/spf13/pflag"
+	"gotest.tools/assert"
+	v12 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	kubefake "k8s.io/client-go/kubernetes/fake"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdConnectorUpdate_NewCmdConnectorUpdate(t *testing.T) {
+
+	t.Run("Update command", func(t *testing.T) {
+
+		result := NewCmdConnectorUpdate()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+		assert.Check(t, result.CobraCmd.PostRunE != nil)
+		assert.Check(t, result.CobraCmd.Flags() != nil)
+	})
+
+}
+
+func TestCmdConnectorUpdate_AddFlags(t *testing.T) {
+
+	expectedFlagsWithDefaultValue := map[string]interface{}{
+		"routing-key":       "",
+		"host":              "",
+		"tls-secret":        "",
+		"type":              "tcp",
+		"port":              "0",
+		"workload":          "",
+		"selector":          "",
+		"include-not-ready": "false",
+		"timeout":           "1m0s",
+		"output":            "",
+	}
+	var flagList []string
+
+	cmd := newCmdConnectorUpdateWithMocks()
+
+	t.Run("add flags", func(t *testing.T) {
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+		})
+
+		assert.Check(t, len(flagList) == 0)
+
+		cmd.AddFlags()
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] != nil)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] == flag.DefValue)
+		})
+
+		assert.Check(t, len(flagList) == len(expectedFlagsWithDefaultValue))
+
+	})
+}
+
+func TestCmdConnectorUpdate_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdConnectorUpdate)
+		expectedErrors []string
+	}
+
+	command := &CmdConnectorUpdate{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "connector is not updated because get connector returned error",
+			args: []string{"my-connector"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"connector my-connector must exist in namespace test to be updated"},
+		},
+		{
+			name: "connector name is not specified",
+			args: []string{},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"connector name must be configured"},
+		},
+		{
+			name: "connector name is nil",
+			args: []string{""},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name: "more than one argument is specified",
+			args: []string{"my", "connector"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name: "connector name is not valid.",
+			args: []string{"my new connector"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "connector type is not valid",
+			args: []string{"my-connector-type"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					connectorType: "not-valid",
+					timeout:       1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{
+				"connector type is not valid: value not-valid not allowed. It should be one of this options: [tcp]"},
+		},
+		{
+			name: "routing key is not valid",
+			args: []string{"my-connector-rk"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					routingKey: "not-valid$",
+					timeout:    60 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"routing key is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "tls-secret is not valid",
+			args: []string{"my-connector-tls"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{tlsSecret: ":not-valid"}
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("secret not found")
+				})
+				command.KubeClient = fakeKubeClient
+				command.flags = ConnectorUpdates{
+					tlsSecret: "test-tls",
+					timeout:   5 * time.Second}
+			},
+			expectedErrors: []string{
+				"tls-secret is not valid: does not exist"},
+		},
+		{
+			name: "port is not valid",
+			args: []string{"my-connector-port"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					port:    -1,
+					timeout: 40 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"connector port is not valid: value is not positive"},
+		},
+		{
+			name: "workload is not valid",
+			args: []string{"bad-workload"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					workload: "!workload",
+					timeout:  70 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"workload is not valid: value does not match this regular expression: ^[A-Za-z0-9=:./-]+$"},
+		},
+		{
+			name: "selector is not valid",
+			args: []string{"bad-selector"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					selector: "@#$%",
+					timeout:  1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{
+				"selector is not valid: value does not match this regular expression: ^[A-Za-z0-9=:./-]+$"},
+		},
+		{
+			name: "timeout is not valid",
+			args: []string{"bad-timeout"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					selector: "selector",
+					timeout:  0 * time.Second}
+			},
+			expectedErrors: []string{
+				"timeout is not valid"},
+		},
+		{
+			name: "output is not valid",
+			args: []string{"bad-output"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					output:  "not-supported",
+					timeout: 1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{
+				"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-connector-flags"},
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ConnectorUpdates{
+					host:            "hostname",
+					routingKey:      "routingkeyname",
+					tlsSecret:       "secretname",
+					port:            1234,
+					connectorType:   "tcp",
+					selector:        "backend",
+					includeNotReady: false,
+					timeout:         5 * time.Second,
+					output:          "json",
+				}
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.KubeClient = fakeKubeClient
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdConnectorUpdate_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdConnectorUpdate)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+
+				fakeSkupperClient.Fake.PrependReactor("update", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					UpdateAction, ok := action.(testing2.UpdateActionImpl)
+					if !ok {
+						return
+					}
+					UpdatedObject := UpdateAction.GetObject()
+
+					connector, ok := UpdatedObject.(*v1alpha1.Connector)
+					if !ok {
+						return
+					}
+					if connector.Name != "my-connector" {
+						return true, nil, fmt.Errorf("unexpected name value")
+					}
+					if connector.Spec.Type != "tcp" {
+						return true, nil, fmt.Errorf("unexpected type value")
+					}
+					if connector.Spec.Port != 8080 {
+						return true, nil, fmt.Errorf("unexpected port value")
+					}
+					if connector.Spec.Host != "hostname" {
+						return true, nil, fmt.Errorf("unexpected host value")
+					}
+					if connector.Spec.RoutingKey != "keyname" {
+						return true, nil, fmt.Errorf("unexpected routing key value")
+					}
+					if connector.Spec.TlsCredentials != "secretname" {
+						return true, nil, fmt.Errorf("unexpected tls-secret value")
+					}
+					if connector.Spec.Selector != "backend" {
+						return true, nil, fmt.Errorf("unexpected selector value")
+					}
+					return true, nil, nil
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-connector"
+				command.newSettings.port = 8080
+				command.newSettings.connectorType = "tcp"
+				command.newSettings.host = "hostname"
+				command.newSettings.routingKey = "keyname"
+				command.newSettings.tlsSecret = "secretname"
+				command.newSettings.selector = "backend"
+			},
+		},
+		{
+			name: "run fails",
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("update", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("error")
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.name = "my-fail-connector"
+				command.newSettings.output = "json"
+			},
+			errorMessage: "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdConnectorUpdateWithMocks()
+		test.setUpMock(cmd)
+
+		//create a connector
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdConnectorUpdate_WaitUntilReady(t *testing.T) {
+	type test struct {
+		name        string
+		setUpMock   func(command *CmdConnectorUpdate)
+		expectError bool
+	}
+
+	testTable := []test{
+		{
+			name: "connector is not ready",
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-connector",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "connector is not returned",
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("it failed")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "connector is ready",
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-connector",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: false,
+		},
+		{
+			name: "connector is ready json output",
+			setUpMock: func(command *CmdConnectorUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "connectors", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-connector",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+				command.newSettings.output = "json"
+			},
+			expectError: false,
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdConnectorUpdateWithMocks()
+		cmd.flags = ConnectorUpdates{timeout: 5 * time.Second}
+		cmd.name = "my-connector"
+
+		test.setUpMock(cmd)
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.WaitUntilReady()
+			if err != nil {
+				assert.Check(t, test.expectError)
+			}
+
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdConnectorUpdateWithMocks() *CmdConnectorUpdate {
+
+	cmdConnectorUpdate := &CmdConnectorUpdate{
+		client:     &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace:  "test",
+		KubeClient: kubefake.NewSimpleClientset(),
+	}
+
+	return cmdConnectorUpdate
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_create_test.go b/internal/cmd/skupper/listener/kube/listener_create_test.go
new file mode 100644
index 00000000..82c55eab
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_create_test.go
@@ -0,0 +1,783 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"github.com/spf13/pflag"
+	"gotest.tools/assert"
+	v12 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	kubefake "k8s.io/client-go/kubernetes/fake"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdListenerCreate_NewCmdListenerCreate(t *testing.T) {
+
+	t.Run("listener command", func(t *testing.T) {
+
+		result := NewCmdListenerCreate()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+		assert.Check(t, result.CobraCmd.PostRunE != nil)
+		assert.Check(t, result.CobraCmd.Flags() != nil)
+	})
+
+}
+
+func TestCmdListenerCreate_AddFlags(t *testing.T) {
+
+	expectedFlagsWithDefaultValue := map[string]interface{}{
+		"routing-key": "",
+		"host":        "",
+		"tls-secret":  "",
+		"type":        "tcp",
+		"timeout":     "1m0s",
+		"output":      "",
+	}
+	var flagList []string
+
+	cmd := newCmdListenerCreateWithMocks()
+
+	t.Run("add flags", func(t *testing.T) {
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+		})
+
+		assert.Check(t, len(flagList) == 0)
+
+		cmd.AddFlags()
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] != nil)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] == flag.DefValue)
+		})
+
+		assert.Check(t, len(flagList) == len(expectedFlagsWithDefaultValue))
+
+	})
+
+}
+
+func TestCmdListenerCreate_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdListenerCreate)
+		expectedErrors []string
+	}
+
+	command := &CmdListenerCreate{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "listener is not created because there is already the same listener in the namespace",
+			args: []string{"my-listener", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("*", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					switch action.(type) {
+					case testing2.GetAction:
+						listener := v1alpha1.Listener{
+							ObjectMeta: v1.ObjectMeta{
+								Name:      "my-listener",
+								Namespace: "test",
+							},
+							Spec: v1alpha1.ListenerSpec{
+								Port: 8080,
+								Type: "tcp",
+								Host: "test",
+							},
+							Status: v1alpha1.Status{
+								StatusMessage: "Ok",
+							},
+						}
+						return true, &listener, nil
+					case testing2.ListAction:
+						site := v1alpha1.SiteList{
+							Items: []v1alpha1.Site{
+								{
+									ObjectMeta: v1.ObjectMeta{
+										Name:      "site1",
+										Namespace: "test",
+									},
+								},
+							},
+						}
+						return true, &site, nil
+					}
+					return false, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"there is already a listener my-listener created for namespace test"},
+		},
+		{
+			name: "listener no site",
+			args: []string{"listener-site", "8090"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"A site must exist in namespace test before a listener can be created"},
+		},
+		{
+			name: "listener name and port are not specified",
+			args: []string{},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name and port must be configured"},
+		},
+		{
+			name: "listener name empty",
+			args: []string{"", "8090"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name: "listener port empty",
+			args: []string{"my-name-port-empty", ""},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener port must not be empty"},
+		},
+		{
+			name: "listener port not positive",
+			args: []string{"my-port-positive", "-45"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener port is not valid: value is not positive"},
+		},
+		{
+			name: "listener name and port are not specified",
+			args: []string{},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name and port must be configured"},
+		},
+		{
+			name: "listener port is not specified",
+			args: []string{"my-name"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name and port must be configured"},
+		},
+		{
+			name: "more than two arguments are specified",
+			args: []string{"my", "listener", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"only two arguments are allowed for this command"},
+		},
+		{
+			name: "listener name is not valid.",
+			args: []string{"my new listener", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "port is not valid.",
+			args: []string{"my-listener-port", "abcd"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener port is not valid: strconv.Atoi: parsing \"abcd\": invalid syntax"},
+		},
+		{
+			name: "listener type is not valid",
+			args: []string{"my-listener-type", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{
+					listenerType: "not-valid",
+					timeout:      20 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"listener type is not valid: value not-valid not allowed. It should be one of this options: [tcp]"},
+		},
+		{
+			name: "routing key is not valid",
+			args: []string{"my-listener-rk", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{
+					routingKey: "not-valid$",
+					timeout:    60 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"routing key is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "tls-secret does not exist",
+			args: []string{"my-listener-tls", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("secret not found")
+				})
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{
+					tlsSecret: "not-valid",
+					timeout:   1 * time.Minute,
+				}
+			},
+			expectedErrors: []string{
+				"tls-secret is not valid: does not exist"},
+		},
+		{
+			name: "timeout is not valid",
+			args: []string{"bad-timeout", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{timeout: 0 * time.Second}
+			},
+			expectedErrors: []string{
+				"timeout is not valid"},
+		},
+		{
+			name: "output is not valid",
+			args: []string{"bad-output", "1234"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{
+					output:  "not-supported",
+					timeout: 30 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-listener-flags", "8080"},
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "sites", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					site := v1alpha1.SiteList{
+						Items: []v1alpha1.Site{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "site1",
+									Namespace: "test",
+								},
+							},
+						},
+					}
+					return true, &site, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerCreate{
+					host:         "hostname",
+					routingKey:   "routingkeyname",
+					tlsSecret:    "secretname",
+					listenerType: "tcp",
+					timeout:      1 * time.Minute,
+					output:       "json",
+				}
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.KubeClient = fakeKubeClient
+				command.flags = ListenerCreate{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdListenerCreate_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdListenerCreate)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("create", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					createAction, ok := action.(testing2.CreateActionImpl)
+					if !ok {
+						return
+					}
+					createdObject := createAction.GetObject()
+
+					listener, ok := createdObject.(*v1alpha1.Listener)
+					if !ok {
+						return
+					}
+
+					if listener.Name != "my-listener" {
+						return true, nil, fmt.Errorf("unexpected name value")
+					}
+					if listener.Spec.Type != "tcp" {
+						return true, nil, fmt.Errorf("unexpected type value")
+					}
+					if listener.Spec.Port != 8080 {
+						return true, nil, fmt.Errorf("unexpected port value")
+					}
+					if listener.Spec.Host != "hostname" {
+						return true, nil, fmt.Errorf("unexpected host value")
+					}
+					if listener.Spec.RoutingKey != "keyname" {
+						return true, nil, fmt.Errorf("unexpected routing key value")
+					}
+					if listener.Spec.TlsCredentials != "secretname" {
+						return true, nil, fmt.Errorf("unexpected tls-secret value")
+					}
+					return true, nil, nil
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.name = "my-listener"
+				command.port = 8080
+				command.flags.listenerType = "tcp"
+				command.flags.host = "hostname"
+				command.flags.routingKey = "keyname"
+				command.flags.tlsSecret = "secretname"
+			},
+		},
+		{
+			name: "run fails",
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("create", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("error")
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.name = "my-fail-listener"
+				command.output = "json"
+			},
+			errorMessage: "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdListenerCreateWithMocks()
+		test.setUpMock(cmd)
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdListenerCreate_WaitUntilReady(t *testing.T) {
+	type test struct {
+		name        string
+		setUpMock   func(command *CmdListenerCreate)
+		expectError bool
+	}
+
+	testTable := []test{
+		{
+			name: "listener is not ready",
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-listener",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "listener is not returned",
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("it failed")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "listener is ready",
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-listener",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: false,
+		},
+		{
+			name: "listener is ready yaml output",
+			setUpMock: func(command *CmdListenerCreate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-listener",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+				command.output = "yaml"
+			},
+			expectError: false,
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdListenerCreateWithMocks()
+		cmd.name = "my-listener"
+		cmd.port = 8080
+		cmd.flags = ListenerCreate{timeout: 1 * time.Second}
+
+		test.setUpMock(cmd)
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.WaitUntilReady()
+			if err != nil {
+				assert.Check(t, test.expectError)
+			}
+
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdListenerCreateWithMocks() *CmdListenerCreate {
+	cmdListenerCreate := &CmdListenerCreate{
+		client:     &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace:  "test",
+		KubeClient: kubefake.NewSimpleClientset(),
+	}
+
+	return cmdListenerCreate
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_delete_test.go b/internal/cmd/skupper/listener/kube/listener_delete_test.go
new file mode 100644
index 00000000..9618dc91
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_delete_test.go
@@ -0,0 +1,316 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"github.com/spf13/pflag"
+	"gotest.tools/assert"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdListenerDelete_NewCmdListenerDelete(t *testing.T) {
+
+	t.Run("Delete command", func(t *testing.T) {
+
+		result := NewCmdListenerDelete()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+		assert.Check(t, result.CobraCmd.PostRunE != nil)
+		assert.Check(t, result.CobraCmd.Flags() != nil)
+	})
+
+}
+
+func TestCmdListenerDelete_AddFlags(t *testing.T) {
+
+	expectedFlagsWithDefaultValue := map[string]interface{}{
+		"timeout": "1m0s",
+	}
+	var flagList []string
+
+	cmd := newCmdListenerDeleteWithMocks()
+
+	t.Run("add flags", func(t *testing.T) {
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+		})
+
+		assert.Check(t, len(flagList) == 0)
+
+		cmd.AddFlags()
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] != nil)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] == flag.DefValue)
+		})
+
+		assert.Check(t, len(flagList) == len(expectedFlagsWithDefaultValue))
+	})
+
+}
+func TestCmdListenerDelete_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdListenerDelete)
+		expectedErrors []string
+	}
+
+	command := &CmdListenerDelete{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "listener is not deleted because listener does not exist in the namespace",
+			args: []string{"my-listener"},
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerDelete{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"NotFound"},
+		},
+		{
+			name: "listener is not deleted because listener does not exist in the namespace",
+			args: []string{"my-listener"},
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerDelete{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener my-listener does not exist in namespace test"},
+		},
+		{
+			name: "listener name is not specified",
+			args: []string{},
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerDelete{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name must be specified"},
+		},
+		{
+			name: "listener name is nil",
+			args: []string{""},
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerDelete{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name: "listener name is not valid",
+			args: []string{"my name"},
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerDelete{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"listener name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "more than one argument is specified",
+			args: []string{"my", "listener"},
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerDelete{timeout: 1 * time.Minute}
+			},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name: "timeout is not valid",
+			args: []string{"bad-timeout"},
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerDelete{timeout: 0 * time.Second}
+			},
+			expectedErrors: []string{
+				"timeout is not valid"},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdListenerDelete_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdListenerDelete)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("Delete", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-listener"
+			},
+		},
+		{
+			name: "run fails",
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("Delete", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("error")
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-fail-listener"
+			},
+			errorMessage: "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdListenerDeleteWithMocks()
+		test.setUpMock(cmd)
+
+		//create a listener
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdListenerDelete_WaitUntilReady(t *testing.T) {
+	type test struct {
+		name        string
+		setUpMock   func(command *CmdListenerDelete)
+		expectError bool
+	}
+
+	testTable := []test{
+		{
+			name: "error deleting listener",
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-listener",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "listener is not returned",
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("it failed")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "listener is deleted",
+			setUpMock: func(command *CmdListenerDelete) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: false,
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdListenerDeleteWithMocks()
+		cmd.name = "my-listener"
+		cmd.flags = ListenerDelete{timeout: 5 * time.Second}
+
+		test.setUpMock(cmd)
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.WaitUntilReady()
+			if err != nil {
+				assert.Check(t, test.expectError)
+			}
+
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdListenerDeleteWithMocks() *CmdListenerDelete {
+
+	cmdListenerDelete := &CmdListenerDelete{
+		client:    &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace: "test",
+	}
+
+	return cmdListenerDelete
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_status_test.go b/internal/cmd/skupper/listener/kube/listener_status_test.go
new file mode 100644
index 00000000..20d4e48f
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_status_test.go
@@ -0,0 +1,441 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"gotest.tools/assert"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdListenerStatus_NewCmdListenerStatus(t *testing.T) {
+
+	t.Run("Status command", func(t *testing.T) {
+
+		result := NewCmdListenerStatus()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+	})
+
+}
+
+func TestCmdListenerStatus_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdListenerStatus)
+		expectedErrors []string
+	}
+
+	command := &CmdListenerStatus{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "listener is not shown because listener does not exist in the namespace",
+			args: []string{"my-listener"},
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"listener my-listener does not exist in namespace test"},
+		},
+		{
+			name: "listener name is nil",
+			args: []string{""},
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name: "more than one argument is specified",
+			args: []string{"my", "listener"},
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name: "listener name is not valid.",
+			args: []string{"my new listener"},
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{"listener name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "no args",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+			},
+			expectedErrors: []string{},
+		},
+		{
+			name: "bad output status",
+			args: []string{"out-listener"},
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags.output = "not-supported"
+			},
+			expectedErrors: []string{"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name: "good output status",
+			args: []string{"out-listener"},
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags.output = "json"
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdListenerStatus_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdListenerStatus)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-listener"
+			},
+		},
+		{
+			name: "run fails",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+			},
+			errorMessage: "NotFound",
+		},
+		{
+			name: "run fails listener doesn't exist",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-fail-listener"
+			},
+			errorMessage: "NotFound",
+		},
+		{
+			name: "runs ok, returns 1 listener",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					listener := v1alpha1.ListenerList{
+						Items: []v1alpha1.Listener{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port: 8080,
+									Type: "tcp",
+									Host: "test",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &listener, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-listener"
+			},
+		},
+		{
+			name: "runs ok, returns 1 listener output yaml",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					listener := v1alpha1.ListenerList{
+						Items: []v1alpha1.Listener{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port: 8080,
+									Type: "tcp",
+									Host: "test",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &listener, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-listener"
+				command.output = "yaml"
+			},
+		},
+		{
+			name: "runs ok, returns all listeners",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					listener := v1alpha1.ListenerList{
+						Items: []v1alpha1.Listener{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener1",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port: 8080,
+									Type: "tcp",
+									Host: "test1",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener2",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port:       8888,
+									Type:       "tcp",
+									Host:       "test2",
+									RoutingKey: "key2",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &listener, nil
+				})
+				command.client = fakeSkupperClient
+			},
+		},
+		{
+			name: "runs ok, returns all listeners output json",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					listener := v1alpha1.ListenerList{
+						Items: []v1alpha1.Listener{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener1",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port: 8080,
+									Type: "tcp",
+									Host: "test1",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener2",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port:       8888,
+									Type:       "tcp",
+									Host:       "test2",
+									RoutingKey: "key2",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &listener, nil
+				})
+				command.client = fakeSkupperClient
+				command.output = "json"
+			},
+		},
+		{
+			name: "runs ok, returns all listeners output bad",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					listener := v1alpha1.ListenerList{
+						Items: []v1alpha1.Listener{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener1",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port: 8080,
+									Type: "tcp",
+									Host: "test1",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener2",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port:       8888,
+									Type:       "tcp",
+									Host:       "test2",
+									RoutingKey: "key2",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &listener, nil
+				})
+				command.client = fakeSkupperClient
+				command.output = "un-supported"
+			},
+			errorMessage: "format un-supported not supported",
+		},
+		{
+			name: "runs ok, returns 1 listeners output bad",
+			setUpMock: func(command *CmdListenerStatus) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("list", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					listener := v1alpha1.ListenerList{
+						Items: []v1alpha1.Listener{
+							{
+								ObjectMeta: v1.ObjectMeta{
+									Name:      "my-listener",
+									Namespace: "test",
+								},
+								Spec: v1alpha1.ListenerSpec{
+									Port: 8080,
+									Type: "tcp",
+									Host: "test",
+								},
+								Status: v1alpha1.Status{
+									StatusMessage: "Ok",
+								},
+							},
+						},
+					}
+					return true, &listener, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-listener"
+				command.output = "bad"
+			},
+			errorMessage: "format bad not supported",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdListenerStatusWithMocks()
+		test.setUpMock(cmd)
+
+		//create a listener
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdListenerStatusWithMocks() *CmdListenerStatus {
+
+	cmdListenerStatus := &CmdListenerStatus{
+		client:    &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace: "test",
+	}
+
+	return cmdListenerStatus
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_update_test.go b/internal/cmd/skupper/listener/kube/listener_update_test.go
new file mode 100644
index 00000000..c4a97d86
--- /dev/null
+++ b/internal/cmd/skupper/listener/kube/listener_update_test.go
@@ -0,0 +1,517 @@
+package kube
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/utils"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v1alpha1/fake"
+	"github.com/spf13/pflag"
+	"gotest.tools/assert"
+	v12 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	kubefake "k8s.io/client-go/kubernetes/fake"
+	testing2 "k8s.io/client-go/testing"
+)
+
+func TestCmdListenerUpdate_NewCmdListenerUpdate(t *testing.T) {
+
+	t.Run("Update command", func(t *testing.T) {
+
+		result := NewCmdListenerUpdate()
+
+		assert.Check(t, result.CobraCmd.Use != "")
+		assert.Check(t, result.CobraCmd.Short != "")
+		assert.Check(t, result.CobraCmd.Long != "")
+		assert.Check(t, result.CobraCmd.Example != "")
+		assert.Check(t, result.CobraCmd.PreRun != nil)
+		assert.Check(t, result.CobraCmd.Run != nil)
+		assert.Check(t, result.CobraCmd.PostRunE != nil)
+		assert.Check(t, result.CobraCmd.Flags() != nil)
+	})
+
+}
+
+func TestCmdListenerUpdate_AddFlags(t *testing.T) {
+
+	expectedFlagsWithDefaultValue := map[string]interface{}{
+		"routing-key": "",
+		"host":        "",
+		"tls-secret":  "",
+		"type":        "tcp",
+		"port":        "0",
+		"timeout":     "1m0s",
+		"output":      "",
+	}
+	var flagList []string
+
+	cmd := newCmdListenerUpdateWithMocks()
+
+	t.Run("add flags", func(t *testing.T) {
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+		})
+
+		assert.Check(t, len(flagList) == 0)
+
+		cmd.AddFlags()
+
+		cmd.CobraCmd.Flags().VisitAll(func(flag *pflag.Flag) {
+			flagList = append(flagList, flag.Name)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] != nil)
+			assert.Check(t, expectedFlagsWithDefaultValue[flag.Name] == flag.DefValue)
+		})
+
+		assert.Check(t, len(flagList) == len(expectedFlagsWithDefaultValue))
+
+	})
+}
+
+func TestCmdListenerUpdate_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		setUpMock      func(command *CmdListenerUpdate)
+		expectedErrors []string
+	}
+
+	command := &CmdListenerUpdate{
+		namespace: "test",
+	}
+
+	testTable := []test{
+		{
+			name: "listener is not updated because listener does not exist in the namespace",
+			args: []string{"my-listener"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("NotFound")
+				})
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"listener my-listener must exist in namespace test to be updated"},
+		},
+		{
+			name: "listener name is not specified",
+			args: []string{},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"listener name must be configured"},
+		},
+		{
+			name: "listener name is nil",
+			args: []string{""},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name: "more than one argument is specified",
+			args: []string{"my", "listener"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name: "listener name is not valid.",
+			args: []string{"my new listener"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{timeout: 5 * time.Second}
+			},
+			expectedErrors: []string{"listener name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "listener type is not valid",
+			args: []string{"my-listener-type"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{
+					listenerType: "not-valid",
+					timeout:      60 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"listener type is not valid: value not-valid not allowed. It should be one of this options: [tcp]"},
+		},
+		{
+			name: "routing key is not valid",
+			args: []string{"my-listener-rk"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{
+					routingKey: "not-valid$",
+					timeout:    30 * time.Second}
+			},
+			expectedErrors: []string{
+				"routing key is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name: "tls-secret is not valid",
+			args: []string{"my-listener-tls"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("secret not found")
+				})
+				command.client = fakeSkupperClient
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{
+					tlsSecret: ":not-valid",
+					timeout:   5 * time.Minute,
+				}
+			},
+			expectedErrors: []string{
+				"tls-secret is not valid: does not exist"},
+		},
+		{
+			name: "port is not valid",
+			args: []string{"my-listener-port"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{
+					port:    -1,
+					timeout: 60 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"listener port is not valid: value is not positive"},
+		},
+		{
+			name: "timeout is not valid",
+			args: []string{"bad-timeout"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{timeout: 0 * time.Second}
+			},
+			expectedErrors: []string{
+				"timeout is not valid"},
+		},
+		{
+			name: "output is not valid",
+			args: []string{"bad-output"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{
+					output:  "not-supported",
+					timeout: 1 * time.Second,
+				}
+			},
+			expectedErrors: []string{
+				"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-listener-flags"},
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				command.client = fakeSkupperClient
+				command.flags = ListenerUpdates{
+					host:         "hostname",
+					routingKey:   "routingkeyname",
+					tlsSecret:    "secretname",
+					port:         1234,
+					listenerType: "tcp",
+					timeout:      1 * time.Second,
+					output:       "json",
+				}
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.KubeClient = fakeKubeClient
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			if test.setUpMock != nil {
+				test.setUpMock(command)
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdListenerUpdate_Run(t *testing.T) {
+	type test struct {
+		name         string
+		setUpMock    func(command *CmdListenerUpdate)
+		errorMessage string
+	}
+
+	testTable := []test{
+		{
+			name: "runs ok",
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+
+				fakeSkupperClient.Fake.PrependReactor("update", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					UpdateAction, ok := action.(testing2.UpdateActionImpl)
+					if !ok {
+						t.Log("failed 1", UpdateAction, ok)
+						return
+					}
+					UpdatedObject := UpdateAction.GetObject()
+
+					listener, ok := UpdatedObject.(*v1alpha1.Listener)
+					if !ok {
+						t.Log("failed 2", listener, ok)
+						return
+					}
+					if listener.Name != "my-listener" {
+						t.Log("failed 3", listener)
+						return true, nil, fmt.Errorf("unexpected name value")
+					}
+					if listener.Spec.Type != "tcp" {
+						t.Log("failed 4", listener)
+						return true, nil, fmt.Errorf("unexpected type value")
+					}
+					if listener.Spec.Port != 8080 {
+						return true, nil, fmt.Errorf("unexpected port value")
+					}
+					if listener.Spec.Host != "hostname" {
+						return true, nil, fmt.Errorf("unexpected host value")
+					}
+					if listener.Spec.RoutingKey != "keyname" {
+						return true, nil, fmt.Errorf("unexpected routing key value")
+					}
+					if listener.Spec.TlsCredentials != "secretname" {
+						return true, nil, fmt.Errorf("unexpected tls-secret value")
+					}
+					return true, nil, nil
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					secret := v12.Secret{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "secretname",
+						},
+					}
+					return true, &secret, nil
+				})
+				command.client = fakeSkupperClient
+				command.name = "my-listener"
+				command.newSettings.port = 8080
+				command.newSettings.listenerType = "tcp"
+				command.newSettings.host = "hostname"
+				command.newSettings.routingKey = "keyname"
+				command.newSettings.tlsSecret = "secretname"
+			},
+		},
+		{
+			name: "run fails",
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("update", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("error")
+				})
+				fakeKubeClient := kubefake.NewSimpleClientset()
+				fakeKubeClient.Fake.ClearActions()
+				fakeKubeClient.Fake.PrependReactor("get", "secrets", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, nil
+				})
+				command.KubeClient = fakeKubeClient
+				command.client = fakeSkupperClient
+				command.name = "my-fail-listener"
+				command.newSettings.output = "json"
+			},
+			errorMessage: "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdListenerUpdateWithMocks()
+		test.setUpMock(cmd)
+
+		//create a listener
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdListenerUpdate_WaitUntilReady(t *testing.T) {
+	type test struct {
+		name        string
+		setUpMock   func(command *CmdListenerUpdate)
+		expectError bool
+	}
+
+	testTable := []test{
+		{
+			name: "listener is not ready",
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-listener",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "listener is not returned",
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+					return true, nil, fmt.Errorf("it failed")
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: true,
+		},
+		{
+			name: "listener is ready",
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-listener",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+			},
+			expectError: false,
+		},
+		{
+			name: "listener is ready json output",
+			setUpMock: func(command *CmdListenerUpdate) {
+				fakeSkupperClient := &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}}
+				fakeSkupperClient.Fake.ClearActions()
+				fakeSkupperClient.Fake.PrependReactor("get", "listeners", func(action testing2.Action) (handled bool, ret runtime.Object, err error) {
+
+					return true, &v1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "my-listener",
+							Namespace: "test",
+						},
+						Status: v1alpha1.Status{
+							StatusMessage: "Ok",
+						},
+					}, nil
+				})
+				command.client = fakeSkupperClient
+				command.newSettings.output = "json"
+			},
+			expectError: false,
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := newCmdListenerUpdateWithMocks()
+		cmd.name = "my-listener"
+		cmd.flags = ListenerUpdates{timeout: 5 * time.Second}
+
+		test.setUpMock(cmd)
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.WaitUntilReady()
+			if err != nil {
+				assert.Check(t, test.expectError)
+			}
+
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdListenerUpdateWithMocks() *CmdListenerUpdate {
+
+	cmdListenerUpdate := &CmdListenerUpdate{
+		client:     &fake.FakeSkupperV1alpha1{Fake: &testing2.Fake{}},
+		namespace:  "test",
+		KubeClient: kubefake.NewSimpleClientset(),
+	}
+
+	return cmdListenerUpdate
+}
+
+func errorsToMessages(errs []error) []string {
+	msgs := make([]string, len(errs))
+	for i, err := range errs {
+		msgs[i] = err.Error()
+	}
+	return msgs
+}
diff --git a/internal/cmd/skupper/listener/listener_create_test.go b/internal/cmd/skupper/listener/listener_create_test.go
deleted file mode 100644
index 11979b9f..00000000
--- a/internal/cmd/skupper/listener/listener_create_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package listener
diff --git a/internal/cmd/skupper/listener/listener_delete_test.go b/internal/cmd/skupper/listener/listener_delete_test.go
deleted file mode 100644
index 11979b9f..00000000
--- a/internal/cmd/skupper/listener/listener_delete_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package listener
diff --git a/internal/cmd/skupper/listener/listener_get_test.go b/internal/cmd/skupper/listener/listener_get_test.go
deleted file mode 100644
index 11979b9f..00000000
--- a/internal/cmd/skupper/listener/listener_get_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package listener
diff --git a/internal/cmd/skupper/listener/listener_update_test.go b/internal/cmd/skupper/listener/listener_update_test.go
deleted file mode 100644
index 11979b9f..00000000
--- a/internal/cmd/skupper/listener/listener_update_test.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package listener
diff --git a/internal/cmd/skupper/site/kube/site_create_test.go b/internal/cmd/skupper/site/kube/site_create_test.go
index 9c86a07a..5322785b 100644
--- a/internal/cmd/skupper/site/kube/site_create_test.go
+++ b/internal/cmd/skupper/site/kube/site_create_test.go
@@ -39,7 +39,7 @@ func TestCmdSiteCreate_AddFlags(t *testing.T) {
 	expectedFlagsWithDefaultValue := map[string]interface{}{
 		"enable-link-access": "false",
 		"link-access-type":   "",
-		"service-account":    "skupper-controller",
+		"service-account":    "",
 		"output":             "",
 	}
 	var flagList []string
@@ -117,7 +117,7 @@ func TestCmdSiteCreate_ValidateInput(t *testing.T) {
 			name:           "service account name is not valid.",
 			args:           []string{"my-site"},
 			flags:          &CreateFlags{serviceAccount: "not valid service account name"},
-			expectedErrors: []string{"service account name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+			expectedErrors: []string{"service account name is not valid: serviceaccounts \"not valid service account name\" not found"},
 		},
 		{
 			name:  "link access type is not valid",
@@ -147,6 +147,8 @@ func TestCmdSiteCreate_ValidateInput(t *testing.T) {
 			fakeSkupperClient, err := fakeclient.NewFakeClient(command.Namespace, test.k8sObjects, test.skupperObjects, "")
 			assert.Assert(t, err)
 			command.Client = fakeSkupperClient.GetSkupperClient().SkupperV1alpha1()
+			command.KubeClient = fakeSkupperClient.GetKubeClient()
+
 			if test.flags != nil {
 				command.flags = *test.flags
 			}
@@ -437,13 +439,14 @@ func TestCmdSiteCreate_WaitUntilReady(t *testing.T) {
 
 func newCmdSiteCreateWithMocks(namespace string) (*CmdSiteCreate, error) {
 
-	vanclient, err := fakeclient.NewFakeClient(namespace, nil, nil, "")
+	client, err := fakeclient.NewFakeClient(namespace, nil, nil, "")
 	if err != nil {
 		return nil, err
 	}
 	cmdSiteCreate := &CmdSiteCreate{
-		Client:    vanclient.GetSkupperClient().SkupperV1alpha1(),
-		Namespace: namespace,
+		Client:     client.GetSkupperClient().SkupperV1alpha1(),
+		KubeClient: client.GetKubeClient(),
+		Namespace:  namespace,
 	}
 
 	return cmdSiteCreate, nil
diff --git a/internal/cmd/skupper/site/kube/site_update_test.go b/internal/cmd/skupper/site/kube/site_update_test.go
index 2a894fe7..80210de1 100644
--- a/internal/cmd/skupper/site/kube/site_update_test.go
+++ b/internal/cmd/skupper/site/kube/site_update_test.go
@@ -36,7 +36,7 @@ func TestCmdSiteUpdate_AddFlags(t *testing.T) {
 	expectedFlagsWithDefaultValue := map[string]interface{}{
 		"enable-link-access": "false",
 		"link-access-type":   "",
-		"service-account":    "skupper-controller",
+		"service-account":    "",
 		"output":             "",
 	}
 	var flagList []string
@@ -165,7 +165,7 @@ func TestCmdSiteUpdate_ValidateInput(t *testing.T) {
 				},
 			},
 			skupperError:   "",
-			expectedErrors: []string{"service account name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+			expectedErrors: []string{"service account name is not valid: serviceaccounts \"not valid service account name\" not found"},
 		},
 		{
 			name:       "link access type is not valid",
@@ -355,6 +355,7 @@ func TestCmdSiteUpdate_ValidateInput(t *testing.T) {
 			fakeSkupperClient, err := fakeclient.NewFakeClient(command.Namespace, test.k8sObjects, test.skupperObjects, test.skupperError)
 			assert.Assert(t, err)
 			command.Client = fakeSkupperClient.GetSkupperClient().SkupperV1alpha1()
+			command.KubeClient = fakeSkupperClient.GetKubeClient()
 
 			if test.flags != nil {
 				command.flags = *test.flags
diff --git a/internal/non-kube/client/compat/container_test.go b/internal/non-kube/client/compat/container_test.go
index 6631ab13..9fba557b 100644
--- a/internal/non-kube/client/compat/container_test.go
+++ b/internal/non-kube/client/compat/container_test.go
@@ -13,8 +13,8 @@ import (
 	"time"
 
 	"github.com/go-openapi/runtime"
-	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper-libpod/v4/client/containers_compat"
+	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/container"
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/skupperproject/skupper/pkg/utils"
diff --git a/pkg/utils/validator/simple_validator_test.go b/pkg/utils/validator/simple_validator_test.go
index 9a04388b..be31d6e6 100644
--- a/pkg/utils/validator/simple_validator_test.go
+++ b/pkg/utils/validator/simple_validator_test.go
@@ -1,10 +1,11 @@
 package validator
 
 import (
-	"gotest.tools/assert"
 	"reflect"
 	"regexp"
 	"testing"
+
+	"gotest.tools/assert"
 )
 
 func TestNewStringValidator(t *testing.T) {
@@ -156,3 +157,44 @@ func TestNewResourceStringValidator_Evaluate(t *testing.T) {
 		})
 	}
 }
+
+func TestNewWorkloadStringValidator(t *testing.T) {
+
+	t.Run("Test New Resource String Validator constructor", func(t *testing.T) {
+
+		validRegexp, _ := regexp.Compile("^[A-Za-z0-9=:./-]+$")
+		expectedResult := &StringValidator{validRegexp}
+		actualResult := NewWorkloadStringValidator()
+		assert.Assert(t, reflect.DeepEqual(actualResult, expectedResult))
+	})
+}
+
+func TestNewWorkloadStringValidator_Evaluate(t *testing.T) {
+	type test struct {
+		name   string
+		value  interface{}
+		result bool
+	}
+
+	testTable := []test{
+		{name: "empty string", value: "", result: false},
+		{name: "valid value", value: "provided-value", result: true},
+		{name: "string with spaces", value: "provided value", result: false},
+		{name: "string with numbers", value: "site123", result: true},
+		{name: "number", value: 123, result: false},
+		{name: "nil value", value: nil, result: false},
+		{name: "string with underscore", value: "abc_def", result: false},
+		{name: "string with equal", value: "abc=def", result: true},
+		{name: "string with slash", value: "abc/def", result: true},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			stringValidator := NewWorkloadStringValidator()
+			expectedResult := test.result
+			actualResult, _ := stringValidator.Evaluate(test.value)
+			assert.Assert(t, reflect.DeepEqual(actualResult, expectedResult))
+		})
+	}
+}