Repository: skupperproject/skupper
Pull request #1736 (2024-10-18 16:02:40)

[v2] pkg kube cleanup
Remove unused v1 wrappers & utils for coverage assessment, etc. 


Patch:
diff --git a/Dockerfile.controller b/Dockerfile.controller
index d7164564..1d41b421 100644
--- a/Dockerfile.controller
+++ b/Dockerfile.controller
@@ -1,12 +1,15 @@
 FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
 
+ARG TARGETOS
+ARG TARGETARCH
+
 WORKDIR /go/src/app
 COPY go.mod .
 COPY go.sum .
 RUN go mod download
 COPY . .
 
-RUN make GOOS=$TARGETOS GOARCH=$TARGETARCH
+RUN make build-controller GOOS=$TARGETOS GOARCH=$TARGETARCH
 
 FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9-minimal
 
diff --git a/cmd/config-sync/collector.go b/cmd/config-sync/collector.go
index a7f0400c..26e0bc3e 100644
--- a/cmd/config-sync/collector.go
+++ b/cmd/config-sync/collector.go
@@ -43,11 +43,17 @@ func siteCollector(ctx context.Context, cli *internalclient.KubeClient) {
 	if platform != types.PlatformKubernetes {
 		return
 	}
-	current, err := kube.GetDeployment(deploymentName(), cli.Namespace, cli.Kube)
+	current, err := cli.Kube.AppsV1().Deployments(cli.Namespace).Get(context.TODO(), deploymentName(), metav1.GetOptions{})
 	if err != nil {
 		log.Fatal("Failed to get transport deployment", err.Error())
 	}
-	owner := kube.GetDeploymentOwnerReference(current)
+
+	owner := metav1.OwnerReference{
+		APIVersion: "apps/v1",
+		Kind:       "Deployment",
+		Name:       current.ObjectMeta.Name,
+		UID:        current.ObjectMeta.UID,
+	}
 
 	existing, err := kube.NewConfigMap(types.NetworkStatusConfigMapName, &siteData, nil, nil, &owner, cli.Namespace, cli.Kube)
 	if err != nil && existing == nil {
@@ -66,7 +72,7 @@ func siteCollector(ctx context.Context, cli *internalclient.KubeClient) {
 }
 
 func startFlowController(ctx context.Context, cli *internalclient.KubeClient) error {
-	deployment, err := kube.GetDeployment(deploymentName(), cli.Namespace, cli.Kube)
+	deployment, err := cli.Kube.AppsV1().Deployments(cli.Namespace).Get(context.TODO(), deploymentName(), metav1.GetOptions{})
 	if err != nil {
 		return fmt.Errorf("failed to get transport deployment: %s", err)
 	}
diff --git a/cmd/config-sync/main.go b/cmd/config-sync/main.go
index 5b9ebc99..a9541c7b 100644
--- a/cmd/config-sync/main.go
+++ b/cmd/config-sync/main.go
@@ -53,18 +53,13 @@ func main() {
 	// set up signals so we handle the first shutdown signal gracefully
 	stopCh := SetupSignalHandler()
 
-	namespace, err := kube.CurrentNamespace()
-	if err != nil {
-		log.Fatal("Error determining namespace: ", err.Error())
-	}
-
-	cli, err := internalclient.NewClient(namespace, "", "")
+	cli, err := internalclient.NewClient("", "", "")
 	if err != nil {
 		log.Fatal("Error getting van client: ", err.Error())
 	}
 
 	log.Println("CONFIG_SYNC: Waiting for Skupper router to be ready")
-	_, err = kube.WaitForPodsSelectorStatus(namespace, cli.Kube, "skupper.io/component=router", corev1.PodRunning, time.Second*180, time.Second*5)
+	_, err = kube.WaitForPodsSelectorStatus(cli.GetNamespace(), cli.Kube, "skupper.io/component=router", corev1.PodRunning, time.Second*180, time.Second*5)
 	if err != nil {
 		log.Fatal("Error waiting for router pods to be ready ", err.Error())
 	}
@@ -84,7 +79,7 @@ func main() {
 		routerConfigMap = "skupper-internal" // change defult?
 	}
 
-	configSync := newConfigSync(cli, namespace, SHARED_TLS_DIRECTORY, routerConfigMap)
+	configSync := newConfigSync(cli, cli.GetNamespace(), SHARED_TLS_DIRECTORY, routerConfigMap)
 	log.Println("CONFIG_SYNC: Starting controller loop...")
 	configSync.start(stopCh)
 
diff --git a/internal/cmd/skupper/common/constants.go b/internal/cmd/skupper/common/constants.go
index 9d54da19..4e9e31e7 100644
--- a/internal/cmd/skupper/common/constants.go
+++ b/internal/cmd/skupper/common/constants.go
@@ -5,4 +5,5 @@ var (
 	OutputTypes     = []string{"json", "yaml"}
 	ListenerTypes   = []string{"tcp"}
 	ConnectorTypes  = []string{"tcp"}
+	WorkloadTypes   = []string{"deployment", "service", "daemonset", "statefulset"}
 )
diff --git a/internal/cmd/skupper/connector/kube/connector_create.go b/internal/cmd/skupper/connector/kube/connector_create.go
index f84c24b7..ad7672e2 100644
--- a/internal/cmd/skupper/connector/kube/connector_create.go
+++ b/internal/cmd/skupper/connector/kube/connector_create.go
@@ -12,6 +12,7 @@ import (
 	"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"
+	pkgUtils "github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -57,8 +58,9 @@ func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error {
 	numberValidator := validator.NewNumberValidator()
 	connectorTypeValidator := validator.NewOptionValidator(common.ConnectorTypes)
 	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
-	workloadStringValidator := validator.NewWorkloadStringValidator()
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
+	workloadStringValidator := validator.NewWorkloadStringValidator(common.WorkloadTypes)
+	selectorStringValidator := validator.NewSelectorStringValidator()
 
 	// Validate arguments name and port
 	if len(args) < 2 {
@@ -94,7 +96,6 @@ func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error {
 			validationErrors = append(validationErrors, fmt.Errorf("there is already a connector %s created for namespace %s", cmd.name, cmd.namespace))
 		}
 	}
-
 	// Validate flags
 	if cmd.Flags != nil && cmd.Flags.RoutingKey != "" {
 		ok, err := resourceStringValidator.Evaluate(cmd.Flags.RoutingKey)
@@ -126,18 +127,67 @@ func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error {
 		if cmd.Flags.Workload != "" || cmd.Flags.Host != "" {
 			validationErrors = append(validationErrors, fmt.Errorf("If selector is configured, cannot configure workload or host"))
 		}
-		ok, err := workloadStringValidator.Evaluate(cmd.Flags.Selector)
+		ok, err := selectorStringValidator.Evaluate(cmd.Flags.Selector)
 		if !ok {
 			validationErrors = append(validationErrors, fmt.Errorf("selector is not valid: %s", err))
 		}
+		cmd.selector = cmd.Flags.Selector
 	}
 	if cmd.Flags != nil && cmd.Flags.Workload != "" {
 		if cmd.Flags.Selector != "" || cmd.Flags.Host != "" {
 			validationErrors = append(validationErrors, fmt.Errorf("If workload is configured, cannot configure selector or host"))
 		}
-		ok, err := workloadStringValidator.Evaluate(cmd.Flags.Workload)
+		//workload get resource-type/resource-name and find selector labels
+		resourceType, resourceName, ok, err := workloadStringValidator.Evaluate(cmd.Flags.Workload)
 		if !ok {
 			validationErrors = append(validationErrors, fmt.Errorf("workload is not valid: %s", err))
+		} else {
+			switch resourceType {
+			case "deployment":
+				deployment, err := cmd.KubeClient.AppsV1().Deployments(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get Deployment specified by workload: %s", err))
+				} else {
+					if deployment.Spec.Selector.MatchLabels != nil {
+						cmd.selector = pkgUtils.StringifySelector(deployment.Spec.Selector.MatchLabels)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector Matchlabels found"))
+					}
+				}
+			case "service":
+				service, err := cmd.KubeClient.CoreV1().Services(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get Service specified by workload: %s", err))
+				} else {
+					if service.Spec.Selector != nil {
+						cmd.selector = pkgUtils.StringifySelector(service.Spec.Selector)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector labels found"))
+					}
+				}
+			case "daemonset":
+				daemonSet, err := cmd.KubeClient.AppsV1().DaemonSets(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get DaemonSet specified by workload: %s", err))
+				} else {
+					if daemonSet.Spec.Selector.MatchLabels != nil {
+						cmd.selector = pkgUtils.StringifySelector(daemonSet.Spec.Selector.MatchLabels)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector Matchlabels found"))
+					}
+				}
+			case "statefulset":
+				statefulSet, err := cmd.KubeClient.AppsV1().StatefulSets(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get StatefulSet specified by workload: %s", err))
+				} else {
+					if statefulSet.Spec.Selector.MatchLabels != nil {
+						cmd.selector = pkgUtils.StringifySelector(statefulSet.Spec.Selector.MatchLabels)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector Matchlabels found"))
+					}
+				}
+			}
 		}
 	}
 	//TBD what is valid timeout
diff --git a/internal/cmd/skupper/connector/kube/connector_update.go b/internal/cmd/skupper/connector/kube/connector_update.go
index 3a5e3019..9589649f 100644
--- a/internal/cmd/skupper/connector/kube/connector_update.go
+++ b/internal/cmd/skupper/connector/kube/connector_update.go
@@ -11,6 +11,7 @@ import (
 	"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"
+	pkgUtils "github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -32,14 +33,16 @@ type ConnectorUpdates struct {
 }
 
 type CmdConnectorUpdate struct {
-	client          skupperv1alpha1.SkupperV1alpha1Interface
-	CobraCmd        *cobra.Command
-	Flags           *common.CommandConnectorUpdateFlags
-	namespace       string
-	name            string
-	resourceVersion string
-	newSettings     ConnectorUpdates
-	KubeClient      kubernetes.Interface
+	client           skupperv1alpha1.SkupperV1alpha1Interface
+	CobraCmd         *cobra.Command
+	Flags            *common.CommandConnectorUpdateFlags
+	namespace        string
+	name             string
+	resourceVersion  string
+	newSettings      ConnectorUpdates
+	existingHost     string
+	existingSelector string
+	KubeClient       kubernetes.Interface
 }
 
 func NewCmdConnectorUpdate() *CmdConnectorUpdate {
@@ -63,8 +66,9 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 	numberValidator := validator.NewNumberValidator()
 	connectorTypeValidator := validator.NewOptionValidator(common.ConnectorTypes)
 	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
-	workloadStringValidator := validator.NewWorkloadStringValidator()
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
+	workloadStringValidator := validator.NewWorkloadStringValidator(common.WorkloadTypes)
+	selectorStringValidator := validator.NewSelectorStringValidator()
 
 	// Validate arguments name
 	if len(args) < 1 {
@@ -90,12 +94,14 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 		} 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.selector = connector.Spec.Selector
 			cmd.newSettings.includeNotReady = connector.Spec.IncludeNotReady
+			cmd.newSettings.routingKey = connector.Spec.RoutingKey
+
+			cmd.existingHost = connector.Spec.Host
+			cmd.existingSelector = connector.Spec.Selector
 		}
 	}
 
@@ -143,33 +149,83 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 		cmd.newSettings.host = cmd.Flags.Host
 	}
 	if cmd.Flags != nil && cmd.Flags.Selector != "" {
-		ok, err := workloadStringValidator.Evaluate(cmd.Flags.Selector)
+		ok, err := selectorStringValidator.Evaluate(cmd.Flags.Selector)
 		if !ok {
 			validationErrors = append(validationErrors, fmt.Errorf("selector is not valid: %s", err))
 		}
 		cmd.newSettings.selector = cmd.Flags.Selector
 	}
+	//workload get resource-type/resource-name and find selector labels
 	if cmd.Flags != nil && cmd.Flags.Workload != "" {
-		ok, err := workloadStringValidator.Evaluate(cmd.Flags.Workload)
+		resourceType, resourceName, ok, err := workloadStringValidator.Evaluate(cmd.Flags.Workload)
 		if !ok {
 			validationErrors = append(validationErrors, fmt.Errorf("workload is not valid: %s", err))
+		} else {
+			switch resourceType {
+			case "deployment":
+				deployment, err := cmd.KubeClient.AppsV1().Deployments(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get Deployment specified by workload: %s", err))
+				} else {
+					if deployment.Spec.Selector.MatchLabels != nil {
+						cmd.newSettings.workload = pkgUtils.StringifySelector(deployment.Spec.Selector.MatchLabels)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector Matchlabels found"))
+					}
+				}
+			case "service":
+				service, err := cmd.KubeClient.CoreV1().Services(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get Service specified by workload: %s", err))
+				} else {
+					if service.Spec.Selector != nil {
+						cmd.newSettings.workload = pkgUtils.StringifySelector(service.Spec.Selector)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector labels found"))
+					}
+				}
+			case "daemonset":
+				daemonSet, err := cmd.KubeClient.AppsV1().DaemonSets(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get DaemonSet specified by workload: %s", err))
+				} else {
+					if daemonSet.Spec.Selector.MatchLabels != nil {
+						cmd.newSettings.workload = pkgUtils.StringifySelector(daemonSet.Spec.Selector.MatchLabels)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector Matchlabels found"))
+					}
+				}
+			case "statefulset":
+				statefulSet, err := cmd.KubeClient.AppsV1().StatefulSets(cmd.namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
+				if err != nil {
+					validationErrors = append(validationErrors, fmt.Errorf("failed trying to get StatefulSet specified by workload: %s", err))
+				} else {
+					if statefulSet.Spec.Selector.MatchLabels != nil {
+						cmd.newSettings.workload = pkgUtils.StringifySelector(statefulSet.Spec.Selector.MatchLabels)
+					} else {
+						validationErrors = append(validationErrors, fmt.Errorf("workload, no selector Matchlabels found"))
+					}
+				}
+			}
 		}
-		cmd.newSettings.workload = cmd.Flags.Workload
 	}
 	//Can only have one workload/Selector/host set
-	if cmd.Flags != nil && cmd.newSettings.host != "" {
-		if cmd.newSettings.workload != "" || cmd.newSettings.selector != "" {
+	if cmd.newSettings.host == "" && cmd.newSettings.selector == "" && cmd.newSettings.workload == "" {
+		// no host/selector/workload being modified use existing values
+		cmd.newSettings.selector = cmd.existingSelector
+		cmd.newSettings.host = cmd.existingHost
+	} else {
+		if cmd.newSettings.host != "" && (cmd.newSettings.workload != "" || cmd.newSettings.selector != "") {
 			validationErrors = append(validationErrors, fmt.Errorf("If host is configured, cannot configure workload or selector"))
 		}
-	}
-	if cmd.Flags != nil && cmd.newSettings.selector != "" {
-		if cmd.newSettings.host != "" || cmd.newSettings.workload != "" {
+		if cmd.newSettings.selector != "" && (cmd.newSettings.host != "" || cmd.newSettings.workload != "") {
 			validationErrors = append(validationErrors, fmt.Errorf("If selector is configured, cannot configure workload or host"))
 		}
-	}
-	if cmd.Flags != nil && cmd.newSettings.workload != "" {
-		if cmd.newSettings.selector != "" || cmd.newSettings.host != "" {
-			validationErrors = append(validationErrors, fmt.Errorf("If workload is configured, cannot configure selector or host"))
+		if cmd.newSettings.workload != "" {
+			if cmd.newSettings.selector != "" || cmd.newSettings.host != "" {
+				validationErrors = append(validationErrors, fmt.Errorf("If workload is configured, cannot configure selector or host"))
+			}
+			cmd.newSettings.selector = cmd.newSettings.workload
 		}
 	}
 	if cmd.Flags != nil && cmd.Flags.Output != "" {
@@ -180,7 +236,6 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 			cmd.newSettings.output = cmd.Flags.Output
 		}
 	}
-
 	return validationErrors
 }
 
diff --git a/pkg/kube/containers.go b/pkg/kube/containers.go
deleted file mode 100644
index 31ac48e4..00000000
--- a/pkg/kube/containers.go
+++ /dev/null
@@ -1,210 +0,0 @@
-package kube
-
-import (
-	"github.com/skupperproject/skupper/api/types"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/util/intstr"
-)
-
-func setResourceRequests(container *corev1.Container, resources types.Resources) {
-	requests := corev1.ResourceList{}
-	if resources.HasCpuRequest() {
-		requests[corev1.ResourceCPU] = resources.GetCpuRequest()
-	}
-	if resources.HasMemoryRequest() {
-		requests[corev1.ResourceMemory] = resources.GetMemoryRequest()
-	}
-	limits := corev1.ResourceList{}
-	if resources.HasCpuLimit() {
-		limits[corev1.ResourceCPU] = resources.GetCpuLimit()
-	}
-	if resources.HasMemoryLimit() {
-		limits[corev1.ResourceMemory] = resources.GetMemoryLimit()
-	}
-	if len(requests) > 0 || len(limits) > 0 {
-		container.Resources = corev1.ResourceRequirements{
-			Requests: requests,
-			Limits:   limits,
-		}
-	}
-}
-
-func controllerStartupProbe() *corev1.Probe {
-	return &corev1.Probe{
-		InitialDelaySeconds: 1,
-		PeriodSeconds:       1,
-		FailureThreshold:    60,
-		Handler: corev1.Handler{
-			HTTPGet: &corev1.HTTPGetAction{
-				Port: intstr.FromInt(8182),
-				Path: "/healthz",
-			},
-		},
-	}
-}
-
-func controllerLivenessProbe() *corev1.Probe {
-	return &corev1.Probe{
-		InitialDelaySeconds: 60,
-		Handler: corev1.Handler{
-			HTTPGet: &corev1.HTTPGetAction{
-				Port: intstr.FromInt(8182),
-				Path: "/healthz",
-			},
-		},
-	}
-}
-
-func controllerReadinessProbe() *corev1.Probe {
-	return &corev1.Probe{
-		InitialDelaySeconds: 5,
-		PeriodSeconds:       5,
-		Handler: corev1.Handler{
-			HTTPGet: &corev1.HTTPGetAction{
-				Port: intstr.FromInt(8182),
-				Path: "/healthz",
-			},
-		},
-	}
-}
-
-func CheckProbesForControllerContainer(c *corev1.Container) bool {
-	updated := false
-	if c.StartupProbe == nil {
-		c.StartupProbe = controllerStartupProbe()
-		updated = true
-	}
-	if c.LivenessProbe == nil {
-		c.LivenessProbe = controllerLivenessProbe()
-		updated = true
-	}
-	if c.ReadinessProbe == nil {
-		c.ReadinessProbe = controllerReadinessProbe()
-		updated = true
-	}
-	return updated
-}
-
-func ContainerForController(ds types.DeploymentSpec) corev1.Container {
-	container := corev1.Container{
-		Image:           ds.Image.Name,
-		ImagePullPolicy: GetPullPolicy(ds.Image.PullPolicy),
-		Name:            types.ControllerContainerName,
-		Env:             ds.EnvVar,
-		Ports:           ds.Ports,
-		StartupProbe:    controllerStartupProbe(),
-		LivenessProbe:   controllerLivenessProbe(),
-		ReadinessProbe:  controllerReadinessProbe(),
-	}
-	setResourceRequests(&container, &ds)
-	return container
-}
-
-func transportLivenessProbe() *corev1.Probe {
-	return &corev1.Probe{
-		InitialDelaySeconds: 60,
-		Handler: corev1.Handler{
-			HTTPGet: &corev1.HTTPGetAction{
-				Port: intstr.FromInt(int(types.TransportLivenessPort)),
-				Path: "/healthz",
-			},
-		},
-	}
-}
-
-func transportReadinessProbe() *corev1.Probe {
-	return &corev1.Probe{
-		InitialDelaySeconds: 1,
-		Handler: corev1.Handler{
-			HTTPGet: &corev1.HTTPGetAction{
-				Port: intstr.FromInt(int(types.TransportLivenessPort)),
-				Path: "/healthz",
-			},
-		},
-	}
-}
-
-func CheckProbesForTransportContainer(c *corev1.Container) bool {
-	updated := false
-	if c.LivenessProbe == nil {
-		c.LivenessProbe = transportLivenessProbe()
-		updated = true
-	}
-	if c.ReadinessProbe == nil {
-		c.ReadinessProbe = transportReadinessProbe()
-		updated = true
-	}
-	return updated
-}
-
-func ContainerForTransport(ds types.DeploymentSpec) corev1.Container {
-	container := corev1.Container{
-		Image:           ds.Image.Name,
-		ImagePullPolicy: GetPullPolicy(ds.Image.PullPolicy),
-		Name:            types.TransportContainerName,
-		LivenessProbe:   transportLivenessProbe(),
-		ReadinessProbe:  transportReadinessProbe(),
-		Env:             ds.EnvVar,
-		Ports:           ds.Ports,
-	}
-	setResourceRequests(&container, &ds)
-	return container
-}
-
-func configSyncReadinessProbe() *corev1.Probe {
-	return &corev1.Probe{
-		InitialDelaySeconds: 1,
-		Handler: corev1.Handler{
-			HTTPGet: &corev1.HTTPGetAction{
-				Port: intstr.FromInt(9191),
-				Path: "/healthz",
-			},
-		},
-	}
-}
-
-func CheckProbesForConfigSync(c *corev1.Container) bool {
-	if c.ReadinessProbe == nil {
-		c.ReadinessProbe = configSyncReadinessProbe()
-		return true
-	}
-	return false
-}
-
-func ContainerForConfigSync(ds types.DeploymentSpec) *corev1.Container {
-	container := corev1.Container{
-		Image:           ds.Image.Name,
-		ImagePullPolicy: GetPullPolicy(ds.Image.PullPolicy),
-		Name:            types.ConfigSyncContainerName,
-		ReadinessProbe:  configSyncReadinessProbe(),
-	}
-
-	setResourceRequests(&container, &ds)
-	return &container
-}
-
-func ContainerForFlowCollector(ds types.DeploymentSpec) *corev1.Container {
-	container := corev1.Container{
-		Image:           ds.Image.Name,
-		ImagePullPolicy: GetPullPolicy(ds.Image.PullPolicy),
-		Name:            types.FlowCollectorContainerName,
-		Env:             ds.EnvVar,
-	}
-	setResourceRequests(&container, &ds)
-	return &container
-}
-
-func ContainerForPrometheusServer(ds types.DeploymentSpec) corev1.Container {
-	// --web.config.file=/path_to/web-config.yaml for tls server config and basic user auth
-
-	container := corev1.Container{
-		Name:            types.PrometheusContainerName,
-		Image:           ds.Image.Name,
-		Args:            []string{"--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.path=/prometheus/", "--web.config.file=/etc/prometheus/web-config.yml"},
-		Env:             ds.EnvVar,
-		VolumeMounts:    []corev1.VolumeMount{},
-		ImagePullPolicy: GetPullPolicy(ds.Image.PullPolicy),
-	}
-	setResourceRequests(&container, &ds)
-	return container
-}
diff --git a/pkg/kube/contour.go b/pkg/kube/contour.go
deleted file mode 100644
index 4e735ff1..00000000
--- a/pkg/kube/contour.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package kube
-
-import (
-	"context"
-
-	internalclient "github.com/skupperproject/skupper/internal/kube/client"
-	"k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/dynamic"
-)
-
-var httpProxyResource = schema.GroupVersionResource{
-	Group:    "projectcontour.io",
-	Version:  "v1",
-	Resource: "httpproxies",
-}
-var httpProxyGVK = schema.GroupVersionKind{
-	Group:   httpProxyResource.Group,
-	Version: httpProxyResource.Version,
-	Kind:    "HTTPProxy",
-}
-
-func (p *IngressRoute) readFromContourProxy(obj *unstructured.Unstructured) error {
-	host, _, err := unstructured.NestedString(obj.UnstructuredContent(), "spec", "virtualhost", "fqdn")
-	if err != nil {
-		return err
-	}
-	p.Host = host
-	services, _, err := unstructured.NestedSlice(obj.UnstructuredContent(), "spec", "tcpproxy", "services")
-	if err != nil {
-		return err
-	}
-	if len(services) > 0 {
-		service, ok := services[0].(map[string]interface{})
-		if ok {
-			name, _, err := unstructured.NestedString(service, "name")
-			if err != nil {
-				return err
-			}
-			port, _, err := unstructured.NestedInt64(service, "port")
-			if err != nil {
-				return err
-			}
-			p.ServiceName = name
-			p.ServicePort = int(port)
-		}
-	}
-	return nil
-}
-
-func (p *IngressRoute) writeToContourProxy(obj *unstructured.Unstructured) error {
-	err := unstructured.SetNestedField(obj.UnstructuredContent(), p.Host, "spec", "virtualhost", "fqdn")
-	if err != nil {
-		return err
-	}
-	err = unstructured.SetNestedField(obj.UnstructuredContent(), true, "spec", "virtualhost", "tls", "passthrough")
-	if err != nil {
-		return err
-	}
-	services := []interface{}{
-		map[string]interface{}{
-			"name": p.ServiceName,
-			"port": int64(p.ServicePort),
-		},
-	}
-	err = unstructured.SetNestedSlice(obj.UnstructuredContent(), services, "spec", "tcpproxy", "services")
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-func GetContourProxy(client dynamic.Interface, namespace string, name string) (*IngressRoute, error) {
-	obj, err := client.Resource(httpProxyResource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		return nil, nil
-	} else if err != nil {
-		return nil, err
-	}
-	proxy := IngressRoute{}
-	err = proxy.readFromContourProxy(obj)
-	if err != nil {
-		return nil, err
-	}
-	return &proxy, nil
-}
-
-func CreateContourProxy(client dynamic.Interface, namespace string, name string, def IngressRoute, labels map[string]string, owner *metav1.OwnerReference) error {
-	obj := unstructured.Unstructured{}
-	obj.SetGroupVersionKind(httpProxyGVK)
-	obj.SetName(name)
-	obj.SetOwnerReferences([]metav1.OwnerReference{*owner})
-	obj.SetLabels(labels)
-	err := def.writeToContourProxy(&obj)
-	if err != nil {
-		return err
-	}
-	_, err = client.Resource(httpProxyResource).Namespace(namespace).Create(context.TODO(), &obj, metav1.CreateOptions{})
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-func CreateContourProxies(routes []IngressRoute, labels map[string]string, owner *metav1.OwnerReference, client dynamic.Interface, namespace string) error {
-	for _, route := range routes {
-		err := CreateContourProxy(client, namespace, route.Name, route, labels, owner)
-		if err != nil && !errors.IsAlreadyExists(err) {
-			return err
-		}
-	}
-	return nil
-}
-
-func UpdateContourProxyService(clients internalclient.Clients, namespace string, proxyName string, serviceName string) error {
-	dc := clients.GetDiscoveryClient()
-	if dc == nil {
-		return nil
-	}
-	_, err := dc.ServerResourcesForGroupVersion("projectcontour.io/v1")
-	if err != nil {
-		//assume resource is not present, so don't return error
-		return nil
-	}
-	client := clients.GetDynamicClient()
-	obj, err := client.Resource(httpProxyResource).Namespace(namespace).Get(context.TODO(), proxyName, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		return err
-	}
-	proxy := IngressRoute{}
-	err = proxy.readFromContourProxy(obj)
-	if err != nil {
-		return err
-	}
-	proxy.ServiceName = serviceName
-	err = proxy.writeToContourProxy(obj)
-	if err != nil {
-		return err
-	}
-	_, err = client.Resource(httpProxyResource).Namespace(namespace).Update(context.TODO(), obj, metav1.UpdateOptions{})
-	return err
-}
diff --git a/pkg/kube/deployments.go b/pkg/kube/deployments.go
deleted file mode 100644
index faf0c0f0..00000000
--- a/pkg/kube/deployments.go
+++ /dev/null
@@ -1,140 +0,0 @@
-package kube
-
-import (
-	"context"
-	"time"
-
-	"github.com/skupperproject/skupper/pkg/utils"
-
-	appsv1 "k8s.io/api/apps/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes"
-)
-
-func GetDeploymentOwnerReference(dep *appsv1.Deployment) metav1.OwnerReference {
-	return metav1.OwnerReference{
-		APIVersion: "apps/v1",
-		Kind:       "Deployment",
-		Name:       dep.ObjectMeta.Name,
-		UID:        dep.ObjectMeta.UID,
-	}
-}
-
-func DeleteDeployment(name string, namespace string, cli kubernetes.Interface) error {
-	_, err := cli.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-	if err == nil {
-		err = cli.AppsV1().Deployments(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
-	}
-	return err
-}
-
-// TODO, pass full client object with namespace and clientset
-func GetDeployment(name string, namespace string, cli kubernetes.Interface) (*appsv1.Deployment, error) {
-	existing, err := cli.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-	if err != nil {
-		return nil, err
-	} else {
-		return existing, err
-	}
-}
-
-func GetContainerPort(deployment *appsv1.Deployment) map[int]int {
-	if len(deployment.Spec.Template.Spec.Containers) > 0 && len(deployment.Spec.Template.Spec.Containers[0].Ports) > 0 {
-		return GetAllContainerPorts(deployment.Spec.Template.Spec.Containers[0])
-	} else {
-		return map[int]int{}
-	}
-}
-
-func GetContainerPortForStatefulSet(statefulSet *appsv1.StatefulSet) map[int]int {
-	if len(statefulSet.Spec.Template.Spec.Containers) > 0 && len(statefulSet.Spec.Template.Spec.Containers[0].Ports) > 0 {
-		return GetAllContainerPorts(statefulSet.Spec.Template.Spec.Containers[0])
-	} else {
-		return map[int]int{}
-	}
-}
-
-func GetContainerPortForDaemonSet(daemonSet *appsv1.DaemonSet) map[int]int {
-	if len(daemonSet.Spec.Template.Spec.Containers) > 0 && len(daemonSet.Spec.Template.Spec.Containers[0].Ports) > 0 {
-		return GetAllContainerPorts(daemonSet.Spec.Template.Spec.Containers[0])
-	} else {
-		return map[int]int{}
-	}
-}
-
-// WaitDeploymentReadyReplicas waits till given deployment contains the expected
-// number of readyReplicas, or until it times out
-func WaitDeploymentReadyReplicas(name string, namespace string, readyReplicas int, cli kubernetes.Interface, timeout, interval time.Duration) (*appsv1.Deployment, error) {
-	var dep *appsv1.Deployment
-	var err error
-
-	ctx, cancel := context.WithTimeout(context.TODO(), timeout)
-	defer cancel()
-	err = utils.RetryWithContext(ctx, interval, func() (bool, error) {
-		dep, err = cli.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-		if err != nil {
-			// dep does not exist yet
-			return false, nil
-		}
-		return dep.Status.ReadyReplicas == int32(readyReplicas), nil
-	})
-
-	return dep, err
-}
-
-func WaitStatefulSetReadyReplicas(name string, namespace string, readyReplicas int, cli kubernetes.Interface, timeout, interval time.Duration) (*appsv1.StatefulSet, error) {
-	var ss *appsv1.StatefulSet
-	var err error
-
-	ctx, cancel := context.WithTimeout(context.TODO(), timeout)
-	defer cancel()
-	err = utils.RetryWithContext(ctx, interval, func() (bool, error) {
-		ss, err = cli.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-		if err != nil {
-			// ss does not exist yet
-			return false, nil
-		}
-		return ss.Status.ReadyReplicas == int32(readyReplicas), nil
-	})
-
-	return ss, err
-}
-
-// WaitDeploymentReady waits till given deployment contains at least one ReadyReplicas, or until it times out
-func WaitDeploymentReady(name string, namespace string, cli kubernetes.Interface, timeout, interval time.Duration) (*appsv1.Deployment, error) {
-	var dep *appsv1.Deployment
-	var err error
-
-	ctx, cancel := context.WithTimeout(context.TODO(), timeout)
-	defer cancel()
-	err = utils.RetryWithContext(ctx, interval, func() (bool, error) {
-		dep, err = cli.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-		if errors.IsNotFound(err) {
-			// dep does not exist yet
-			return false, nil
-		} else if err != nil {
-			return false, err
-		}
-		return dep.Status.ReadyReplicas > 0, nil
-	})
-
-	return dep, err
-}
-
-func WaitDaemonSetReady(name string, namespace string, cli kubernetes.Interface, timeout, interval time.Duration) (*appsv1.DaemonSet, error) {
-	var dep *appsv1.DaemonSet
-	var err error
-
-	ctx, cancel := context.WithTimeout(context.TODO(), timeout)
-	defer cancel()
-	err = utils.RetryWithContext(ctx, interval, func() (bool, error) {
-		dep, err = cli.AppsV1().DaemonSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-		if err != nil {
-			return false, nil
-		}
-		return dep.Status.NumberReady > 0, nil
-	})
-
-	return dep, err
-}
diff --git a/pkg/kube/ingress.go b/pkg/kube/ingress.go
deleted file mode 100644
index 99654080..00000000
--- a/pkg/kube/ingress.go
+++ /dev/null
@@ -1,450 +0,0 @@
-/*
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package kube
-
-import (
-	"context"
-	"fmt"
-	"reflect"
-	"strings"
-	"time"
-
-	internalclient "github.com/skupperproject/skupper/internal/kube/client"
-	corev1 "k8s.io/api/core/v1"
-	networkingv1 "k8s.io/api/networking/v1beta1"
-	"k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/util/intstr"
-	"k8s.io/client-go/discovery"
-	"k8s.io/client-go/dynamic"
-)
-
-type IngressRoute struct {
-	Name        string
-	Host        string
-	ServiceName string
-	ServicePort int
-	Resolve     bool
-}
-
-func (r *IngressRoute) toRule() networkingv1.IngressRule {
-	return networkingv1.IngressRule{
-		Host: r.Host,
-		IngressRuleValue: networkingv1.IngressRuleValue{
-			HTTP: &networkingv1.HTTPIngressRuleValue{
-				Paths: []networkingv1.HTTPIngressPath{
-					{
-						Path: "/",
-						Backend: networkingv1.IngressBackend{
-							ServiceName: r.ServiceName,
-							ServicePort: intstr.FromInt(r.ServicePort),
-						},
-					},
-				},
-			},
-		},
-	}
-}
-
-func fromRule(rule *networkingv1.IngressRule) IngressRoute {
-	return IngressRoute{
-		Host:        rule.Host,
-		ServiceName: rule.IngressRuleValue.HTTP.Paths[0].Backend.ServiceName,
-		ServicePort: rule.IngressRuleValue.HTTP.Paths[0].Backend.ServicePort.IntValue(),
-	}
-}
-
-func getStatus(ingress *networkingv1.Ingress) *corev1.LoadBalancerIngress {
-	if len(ingress.Status.LoadBalancer.Ingress) > 0 {
-		status := ingress.Status.LoadBalancer.Ingress[0]
-		if status.IP != "" || status.Hostname != "" {
-			return &status
-		}
-	}
-	return nil
-}
-
-func addNginxIngressAnnotations(sslPassthrough bool, annotations map[string]string) {
-	annotations["kubernetes.io/ingress.class"] = "nginx"
-	if sslPassthrough {
-		annotations["nginx.ingress.kubernetes.io/ssl-passthrough"] = "true"
-		annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "true"
-	}
-}
-
-func UpdateIngressRuleServiceName(ingressName string, hostPrefix string, serviceName string, namespace string, clients internalclient.Clients) error {
-	if useV1API(clients.GetDiscoveryClient()) {
-		return updateIngressRuleServiceNameV1(clients.GetDynamicClient(), namespace, ingressName, hostPrefix, serviceName)
-	} else {
-		cli := clients.GetKubeClient()
-		current, err := cli.NetworkingV1beta1().Ingresses(namespace).Get(context.TODO(), ingressName, metav1.GetOptions{})
-		if errors.IsNotFound(err) {
-			return nil
-		} else if err != nil {
-			return err
-		}
-		for index, rule := range current.Spec.Rules {
-			if strings.HasPrefix(rule.Host, hostPrefix) {
-				rule.IngressRuleValue.HTTP.Paths[0].Backend.ServiceName = serviceName
-				current.Spec.Rules[index] = rule
-				_, err = cli.NetworkingV1beta1().Ingresses(namespace).Update(context.TODO(), current, metav1.UpdateOptions{})
-				if err != nil {
-					return err
-				}
-				return nil
-			}
-		}
-		return nil
-	}
-}
-
-func CreateIngress(name string, routes []IngressRoute, isNginx bool, sslPassthrough bool, ownerRefs []metav1.OwnerReference, namespace string, annotations map[string]string, labels map[string]string, clients internalclient.Clients) error {
-	if useV1API(clients.GetDiscoveryClient()) {
-		if isNginx {
-			if annotations == nil {
-				annotations = map[string]string{}
-			}
-			addNginxIngressAnnotations(sslPassthrough, annotations)
-		}
-		resolve := false
-		for _, route := range routes {
-			if route.Resolve {
-				resolve = true
-			}
-		}
-		return ensureIngressRoutesV1(clients.GetDynamicClient(), namespace, name, routes, annotations, labels, ownerRefs, resolve)
-	}
-	cli := clients.GetKubeClient()
-	ingress := networkingv1.Ingress{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:        name,
-			Annotations: map[string]string{},
-			Labels:      labels,
-		},
-		Spec: networkingv1.IngressSpec{
-			Rules: []networkingv1.IngressRule{},
-		},
-	}
-	if isNginx {
-		addNginxIngressAnnotations(sslPassthrough, ingress.ObjectMeta.Annotations)
-	}
-
-	for key, value := range annotations {
-		ingress.ObjectMeta.Annotations[key] = value
-	}
-
-	if ownerRefs != nil {
-		ingress.ObjectMeta.OwnerReferences = ownerRefs
-	}
-	resolve := false
-	for _, route := range routes {
-		ingress.Spec.Rules = append(ingress.Spec.Rules, route.toRule())
-		if route.Resolve {
-			resolve = true
-		}
-	}
-	updated, err := cli.NetworkingV1beta1().Ingresses(namespace).Create(context.TODO(), &ingress, metav1.CreateOptions{})
-	if err != nil {
-		return err
-	}
-	if resolve {
-		hostOrIp := getStatus(updated)
-		for i := 0; i < 60 && hostOrIp == nil; i++ {
-			if i%10 == 0 {
-				fmt.Printf("Waiting for ingress host/ip...")
-				fmt.Println()
-			}
-			time.Sleep(time.Second)
-			updated, err = cli.NetworkingV1beta1().Ingresses(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-			if err != nil {
-				return err
-			}
-			hostOrIp = getStatus(updated)
-		}
-		if hostOrIp == nil {
-			return fmt.Errorf("Could not resolve host or ip of ingress")
-		}
-		updated.Spec.Rules = nil
-		for _, route := range routes {
-			if route.Resolve {
-				base := hostOrIp.Hostname
-				if hostOrIp.IP != "" {
-					base = hostOrIp.IP + ".nip.io"
-				}
-				route.Host = strings.Join([]string{route.Host, namespace, base}, ".")
-			}
-			updated.Spec.Rules = append(updated.Spec.Rules, route.toRule())
-		}
-		_, err = cli.NetworkingV1beta1().Ingresses(namespace).Update(context.TODO(), updated, metav1.UpdateOptions{})
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func GetIngressRoutes(name string, namespace string, clients internalclient.Clients) ([]IngressRoute, error) {
-	if useV1API(clients.GetDiscoveryClient()) {
-		return getIngressRoutesV1(clients.GetDynamicClient(), namespace, name)
-	}
-	cli := clients.GetKubeClient()
-	var routes []IngressRoute
-	ingress, err := cli.NetworkingV1beta1().Ingresses(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		return routes, nil
-	} else if err != nil {
-		return routes, err
-	}
-	for _, rule := range ingress.Spec.Rules {
-		routes = append(routes, fromRule(&rule))
-	}
-	return routes, nil
-}
-
-var ingressResource = schema.GroupVersionResource{
-	Group:    "networking.k8s.io",
-	Version:  "v1",
-	Resource: "ingresses",
-}
-var ingressGVK = schema.GroupVersionKind{
-	Group:   ingressResource.Group,
-	Version: ingressResource.Version,
-	Kind:    "Ingress",
-}
-
-func getIngressRoutesV1(client dynamic.Interface, namespace string, name string) ([]IngressRoute, error) {
-	obj, err := client.Resource(ingressResource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		return nil, nil
-	} else if err != nil {
-		return nil, err
-	}
-	return readIngressRules(obj)
-}
-
-func appendIngressHostSuffix(desired []IngressRoute, suffix string) []IngressRoute {
-	var updated []IngressRoute
-	for _, r := range desired {
-		r.Host = r.Host + suffix
-		updated = append(updated, r)
-	}
-	return updated
-}
-
-func ensureIngressRoutesV1(client dynamic.Interface, namespace string, name string, routes []IngressRoute, annotations map[string]string, labels map[string]string, ownerRefs []metav1.OwnerReference, resolve bool) error {
-	obj, err := client.Resource(ingressResource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		obj = &unstructured.Unstructured{}
-		ingressClassName, ok := annotations["kubernetes.io/ingress.class"]
-		if ok {
-			delete(annotations, "kubernetes.io/ingress.class")
-		}
-		obj.SetGroupVersionKind(ingressGVK)
-		obj.SetName(name)
-		obj.SetOwnerReferences(ownerRefs)
-		obj.SetAnnotations(annotations)
-		obj.SetLabels(labels)
-		err := writeIngressRules(routes, obj)
-		if err != nil {
-			return err
-		}
-		err = unstructured.SetNestedField(obj.UnstructuredContent(), ingressClassName, "spec", "ingressClassName")
-		if err != nil {
-			return err
-		}
-		_, err = client.Resource(ingressResource).Namespace(namespace).Create(context.TODO(), obj, metav1.CreateOptions{})
-		if err != nil {
-			return err
-		}
-		return nil
-	} else if err != nil {
-		return err
-	}
-	update := false
-	if !reflect.DeepEqual(obj.GetOwnerReferences(), ownerRefs) {
-		obj.SetOwnerReferences(ownerRefs)
-		update = true
-	}
-	if !reflect.DeepEqual(obj.GetAnnotations(), annotations) {
-		obj.SetAnnotations(annotations)
-		update = true
-	}
-	existing, err := readIngressRules(obj)
-	base := ""
-	if resolve {
-		base = resolveBaseHostOrIp(obj)
-		if base != "" {
-			base = "." + namespace + "." + base
-			routes = appendIngressHostSuffix(routes, base)
-		}
-	}
-	if err != nil || !reflect.DeepEqual(existing, routes) {
-		err = writeIngressRules(routes, obj)
-		if err != nil {
-			return err
-		}
-		update = true
-	}
-	if update {
-		_, err = client.Resource(ingressResource).Namespace(namespace).Update(context.TODO(), obj, metav1.UpdateOptions{})
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func updateIngressRuleServiceNameV1(client dynamic.Interface, namespace string, ingressName string, hostPrefix string, serviceName string) error {
-	obj, err := client.Resource(ingressResource).Namespace(namespace).Get(context.TODO(), ingressName, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		return err
-	}
-	routes, err := readIngressRules(obj)
-	if err != nil {
-		return err
-	}
-	for index, route := range routes {
-		if strings.HasPrefix(route.Host, hostPrefix) {
-			route.ServiceName = serviceName
-			routes[index] = route
-			err = writeIngressRules(routes, obj)
-			if err != nil {
-				return err
-			}
-			_, err = client.Resource(ingressResource).Namespace(namespace).Update(context.TODO(), obj, metav1.UpdateOptions{})
-			if err != nil {
-				return err
-			}
-			return nil
-		}
-	}
-	return nil
-}
-
-func writeIngressRules(routes []IngressRoute, obj *unstructured.Unstructured) error {
-	var rules []interface{}
-	for _, route := range routes {
-		rules = append(rules, route.toUntypedRule())
-	}
-	err := unstructured.SetNestedSlice(obj.UnstructuredContent(), rules, "spec", "rules")
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-func readIngressRules(obj *unstructured.Unstructured) ([]IngressRoute, error) {
-	rules, _, err := unstructured.NestedSlice(obj.UnstructuredContent(), "spec", "rules")
-	if err != nil {
-		return nil, err
-	}
-	var routes []IngressRoute
-	for _, r := range rules {
-		route := IngressRoute{}
-		err := route.fromUntypedRule(r)
-		if err != nil {
-			return routes, err
-		}
-		routes = append(routes, route)
-	}
-	return routes, nil
-}
-
-func resolveBaseHostOrIp(obj *unstructured.Unstructured) string {
-	ingresses, _, err := unstructured.NestedSlice(obj.UnstructuredContent(), "status", "loadBalancer", "ingress")
-	if err != nil || len(ingresses) == 0 {
-		return ""
-	}
-	ingress, ok := ingresses[0].(map[string]interface{})
-	if !ok {
-		return ""
-	}
-	host, _, _ := unstructured.NestedString(ingress, "host")
-	if host != "" {
-		return host
-	}
-	ip, _, _ := unstructured.NestedString(ingress, "ip")
-	if ip != "" {
-		return ip + ".nip.io"
-	}
-	return ""
-}
-
-func (r *IngressRoute) fromUntypedRule(u interface{}) error {
-	rule, ok := u.(map[string]interface{})
-	if !ok {
-		return fmt.Errorf("Could not parse rule")
-	}
-	host, _, err := unstructured.NestedString(rule, "host")
-	if err != nil {
-		return err
-	}
-	paths, _, err := unstructured.NestedSlice(rule, "http", "paths")
-	if err != nil {
-		return err
-	}
-	if len(paths) == 0 {
-		return fmt.Errorf("No paths in rule")
-	}
-	path := paths[0].(map[string]interface{})
-	serviceName, _, err := unstructured.NestedString(path, "backend", "service", "name")
-	if err != nil {
-		return err
-	}
-	servicePort, _, err := unstructured.NestedInt64(path, "backend", "service", "port", "number")
-	if err != nil {
-		return err
-	}
-
-	r.Name = strings.Split(host, ".")[0]
-	r.Host = host
-	r.ServiceName = serviceName
-	r.ServicePort = int(servicePort)
-	return nil
-}
-
-func (r *IngressRoute) toUntypedRule() interface{} {
-	return map[string]interface{}{
-		"host": r.Host,
-		"http": map[string]interface{}{
-			"paths": []interface{}{
-				map[string]interface{}{
-					"path":     "/",
-					"pathType": "Prefix",
-					"backend": map[string]interface{}{
-						"service": map[string]interface{}{
-							"name": r.ServiceName,
-							"port": map[string]interface{}{
-								"number": int64(r.ServicePort),
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-}
-
-func useV1API(dc discovery.DiscoveryInterface) bool {
-	if dc == nil {
-		return false
-	}
-	_, err := dc.ServerResourcesForGroupVersion("networking.k8s.io/v1beta1")
-	return err != nil
-}
diff --git a/pkg/kube/namespaces.go b/pkg/kube/namespaces.go
deleted file mode 100644
index c74ad0c1..00000000
--- a/pkg/kube/namespaces.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package kube
-
-import (
-	"os"
-)
-
-const NamespaceFile string = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
-
-func CurrentNamespace() (string, error) {
-	namespace := os.Getenv("NAMESPACE")
-	if namespace != "" {
-		return namespace, nil
-	}
-	_, err := os.Stat(NamespaceFile)
-	if err == nil {
-		raw, err := os.ReadFile(NamespaceFile)
-		if err != nil {
-			return namespace, err
-		}
-		namespace = string(raw)
-		return namespace, nil
-	} else {
-		return namespace, err
-	}
-
-}
diff --git a/pkg/kube/routes.go b/pkg/kube/routes.go
deleted file mode 100644
index 0fa26152..00000000
--- a/pkg/kube/routes.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package kube
-
-import (
-	"context"
-	"fmt"
-
-	routev1 "github.com/openshift/api/route/v1"
-	routev1client "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-)
-
-func GetRoute(name string, namespace string, rc routev1client.RouteV1Interface) (*routev1.Route, error) {
-	current, err := rc.Routes(namespace).Get(context.TODO(), name, metav1.GetOptions{})
-	return current, err
-}
-
-func CreateRoute(route *routev1.Route, namespace string, rc routev1client.RouteV1Interface) (*routev1.Route, error) {
-	current, err := rc.Routes(namespace).Get(context.TODO(), route.Name, metav1.GetOptions{})
-	if err == nil {
-		return current, errors.NewAlreadyExists(schema.GroupResource{Group: "openshift.io", Resource: "routes"}, route.Name)
-	} else if errors.IsNotFound(err) {
-		created, err := rc.Routes(namespace).Create(context.TODO(), route, metav1.CreateOptions{})
-		if err != nil {
-			return nil, err
-		} else {
-			return created, nil
-		}
-	} else {
-		return nil, fmt.Errorf("Failed while checking route: %w", err)
-	}
-}
-
-func UpdateTargetServiceForRoute(routeName string, serviceName string, namespace string, rc routev1client.RouteV1Interface) error {
-	return updateTargetServiceForRoute(true, routeName, serviceName, namespace, rc)
-}
-
-func UpdateTargetServiceForRouteIfExists(routeName string, serviceName string, namespace string, rc routev1client.RouteV1Interface) error {
-	if rc == nil {
-		return nil
-	}
-	return updateTargetServiceForRoute(false, routeName, serviceName, namespace, rc)
-}
-
-func updateTargetServiceForRoute(failIfNotExists bool, routeName string, serviceName string, namespace string, rc routev1client.RouteV1Interface) error {
-	current, err := rc.Routes(namespace).Get(context.TODO(), routeName, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
-		if failIfNotExists {
-			return err
-		} else {
-			return nil
-		}
-	} else if err != nil {
-		return err
-	}
-	current.Spec.To.Name = serviceName
-	_, err = rc.Routes(namespace).Update(context.TODO(), current, metav1.UpdateOptions{})
-	if err != nil {
-		return err
-	}
-	return nil
-}
diff --git a/pkg/kube/securedaccess/access.go b/pkg/kube/securedaccess/access.go
index 6fc56b27..19dc7989 100644
--- a/pkg/kube/securedaccess/access.go
+++ b/pkg/kube/securedaccess/access.go
@@ -424,11 +424,13 @@ func updatePorts(spec *corev1.ServiceSpec, desired []skupperv1alpha1.SecuredAcce
 	var ports []corev1.ServicePort
 	for _, actual := range spec.Ports {
 		if port, ok := expected[actual.Name]; ok {
-			ports = append(ports, port)
-			delete(expected, actual.Name)
-			if actual != port {
+			if equivalentPorts(port, actual) {
+				ports = append(ports, actual)
+			} else {
+				ports = append(ports, port)
 				changed = true
 			}
+			delete(expected, actual.Name)
 		} else {
 			changed = true
 		}
@@ -443,6 +445,18 @@ func updatePorts(spec *corev1.ServiceSpec, desired []skupperv1alpha1.SecuredAcce
 	return changed
 }
 
+func equivalentPorts(desired corev1.ServicePort, actual corev1.ServicePort) bool {
+	return desired.Name == actual.Name && desired.Port == actual.Port && equivalentTargetPorts(desired, actual) && equivalentProtocols(desired.Protocol, actual.Protocol)
+}
+
+func equivalentTargetPorts(desired corev1.ServicePort, actual corev1.ServicePort) bool {
+	return desired.TargetPort == actual.TargetPort || (desired.TargetPort.IntVal == 0 && actual.TargetPort.IntVal == desired.Port)
+}
+
+func equivalentProtocols(desired corev1.Protocol, actual corev1.Protocol) bool {
+	return desired == actual || (desired == "" && actual == corev1.ProtocolTCP)
+}
+
 func toServicePorts(desired map[string]skupperv1alpha1.SecuredAccessPort) map[string]corev1.ServicePort {
 	results := map[string]corev1.ServicePort{}
 	for name, details := range desired {
diff --git a/pkg/kube/securedaccess/nodeport.go b/pkg/kube/securedaccess/nodeport.go
index fcb5e96d..162f685d 100644
--- a/pkg/kube/securedaccess/nodeport.go
+++ b/pkg/kube/securedaccess/nodeport.go
@@ -23,11 +23,13 @@ func newNodeportAccess(m *SecuredAccessManager, clusterHost string) AccessType {
 func (o *NodeportAccessType) RealiseAndResolve(access *skupperv1alpha1.SecuredAccess, svc *corev1.Service) ([]skupperv1alpha1.Endpoint, error) {
 	var endpoints []skupperv1alpha1.Endpoint
 	for _, p := range svc.Spec.Ports {
-		endpoints = append(endpoints, skupperv1alpha1.Endpoint{
-			Name: p.Name,
-			Host: o.clusterHost,
-			Port: strconv.Itoa(int(p.NodePort)),
-		})
+		if p.NodePort != 0 {
+			endpoints = append(endpoints, skupperv1alpha1.Endpoint{
+				Name: p.Name,
+				Host: o.clusterHost,
+				Port: strconv.Itoa(int(p.NodePort)),
+			})
+		}
 	}
 	return endpoints, nil
 }
diff --git a/pkg/kube/site/attached_connector.go b/pkg/kube/site/attached_connector.go
index de46a813..00d15ff4 100644
--- a/pkg/kube/site/attached_connector.go
+++ b/pkg/kube/site/attached_connector.go
@@ -174,11 +174,17 @@ func (a *AttachedConnector) IncludeNotReady() bool {
 	return false
 }
 
-func (a *AttachedConnector) Id() string {
+func (a *AttachedConnector) Attr() slog.Attr {
 	if definition := a.activeDefinition(); definition != nil {
-		return fmt.Sprintf("AttachedConnector %s/%s", definition.Name, definition.Namespace)
-	}
-	return ""
+		return slog.Group("AttachedConnector",
+			slog.Bool("Active", true),
+			slog.String("Name", definition.Name),
+			slog.String("Namespace", definition.Namespace))
+	}
+	return slog.Group("AttachedConnector",
+		slog.Bool("Active", false),
+		slog.String("Name", a.name),
+		slog.String("Namespace", a.namespace))
 }
 
 func error_(errors []string) error {
diff --git a/pkg/kube/site/bindings.go b/pkg/kube/site/bindings.go
index f59cf0cd..8ea508ab 100644
--- a/pkg/kube/site/bindings.go
+++ b/pkg/kube/site/bindings.go
@@ -143,7 +143,6 @@ type TargetSelection interface {
 
 type TargetSelectionImpl struct {
 	watcher         *PodWatcher
-	stopCh          chan struct{}
 	site            *Site
 	selector        string
 	name            string
@@ -156,16 +155,17 @@ func (w *TargetSelectionImpl) Selector() string {
 }
 
 func (w *TargetSelectionImpl) Close() {
-	//w.watcher.Close()
-	close(w.stopCh)
+	w.watcher.Close()
 }
 
 func (w *TargetSelectionImpl) IncludeNotReady() bool {
 	return w.includeNotReady
 }
 
-func (w *TargetSelectionImpl) Id() string {
-	return fmt.Sprintf("Connector %s/%s", w.name, w.namespace)
+func (w *TargetSelectionImpl) Attr() slog.Attr {
+	return slog.Group("Connector",
+		slog.String("Name", w.name),
+		slog.String("Namespace", w.namespace))
 }
 
 func (w *TargetSelectionImpl) List() []skupperv1alpha1.PodDetails {
@@ -182,19 +182,16 @@ func (w *TargetSelectionImpl) Updated(pods []skupperv1alpha1.PodDetails) error {
 		return w.site.updateConnectorConfiguredStatus(connector, err)
 	}
 	if len(pods) == 0 {
-		bindings_logger.Info("No pods available for target selection",
-			slog.String("id", w.Id()))
+		bindings_logger.Debug("No pods available for target selection", w.Attr())
 		return w.site.updateConnectorConfiguredStatus(connector, fmt.Errorf("No matches for selector"))
 	}
-	bindings_logger.Info("No pods available for target selection",
-		slog.String("id", w.Id()))
 	return w.site.updateConnectorConfiguredStatus(connector, nil)
 }
 
 type PodWatchingContext interface {
 	Selector() string
 	IncludeNotReady() bool
-	Id() string
+	Attr() slog.Attr
 	Updated(pods []skupperv1alpha1.PodDetails) error
 }
 
@@ -209,23 +206,23 @@ func (w *PodWatcher) pods() []skupperv1alpha1.PodDetails {
 	for _, pod := range w.watcher.List() {
 		if kube.IsPodReady(pod) || w.context.IncludeNotReady() {
 			if kube.IsPodRunning(pod) && pod.DeletionTimestamp == nil {
-				bindings_logger.Info("Pod selected for connector",
+				bindings_logger.Debug("Pod selected for connector",
 					slog.String("pod", pod.ObjectMeta.Name),
-					slog.String("connector", w.context.Id()))
+					w.context.Attr())
 				targets = append(targets, skupperv1alpha1.PodDetails{
 					UID:  string(pod.UID),
 					Name: pod.Name,
 					IP:   pod.Status.PodIP,
 				})
 			} else {
-				bindings_logger.Info("Pod not running for connector",
+				bindings_logger.Debug("Pod not running for connector",
 					slog.String("pod", pod.ObjectMeta.Name),
-					slog.String("connector", w.context.Id()))
+					w.context.Attr())
 			}
 		} else {
-			bindings_logger.Info("Pod not ready for connector",
+			bindings_logger.Debug("Pod not ready for connector",
 				slog.String("pod", pod.ObjectMeta.Name),
-				slog.String("connector", w.context.Id()))
+				w.context.Attr())
 		}
 	}
 	return targets
diff --git a/pkg/kube/site/site.go b/pkg/kube/site/site.go
index 0c2e45a9..97f947ee 100644
--- a/pkg/kube/site/site.go
+++ b/pkg/kube/site/site.go
@@ -416,7 +416,6 @@ func (s *Site) Select(connector *skupperv1alpha1.Connector) TargetSelection {
 		return nil
 	}
 	handler := &TargetSelectionImpl{
-		stopCh:          make(chan struct{}),
 		site:            s,
 		name:            name,
 		selector:        selector,
diff --git a/pkg/utils/validator/simple_validator.go b/pkg/utils/validator/simple_validator.go
index 88630d1d..427eaddd 100644
--- a/pkg/utils/validator/simple_validator.go
+++ b/pkg/utils/validator/simple_validator.go
@@ -3,6 +3,7 @@ package validator
 import (
 	"fmt"
 	"regexp"
+	"strings"
 	"time"
 )
 
@@ -38,8 +39,8 @@ func NewResourceStringValidator() *StringValidator {
 	}
 }
 
-// TBD what are valid characters for workload field
-func NewWorkloadStringValidator() *StringValidator {
+// TBD what are valid characters for selector field
+func NewSelectorStringValidator() *StringValidator {
 	re, err := regexp.Compile("^[A-Za-z0-9=:./-]+$")
 	if err != nil {
 		fmt.Printf("Error compiling regex: %v", err)
@@ -169,3 +170,45 @@ func (i DurationValidator) Evaluate(value time.Duration) (bool, error) {
 
 	return true, nil
 }
+
+type WorkloadValidator struct {
+	Expression     *regexp.Regexp
+	AllowedOptions []string
+}
+
+func NewWorkloadStringValidator(validOptions []string) *WorkloadValidator {
+	re, err := regexp.Compile("^[A-Za-z0-9.-_]+$")
+	if err != nil {
+		fmt.Printf("Error compiling regex: %v", err)
+		return nil
+	}
+	return &WorkloadValidator{
+		Expression:     re,
+		AllowedOptions: validOptions,
+	}
+}
+
+func (s WorkloadValidator) Evaluate(value interface{}) (string, string, bool, error) {
+
+	v, ok := value.(string)
+
+	if !ok {
+		return "", "", false, fmt.Errorf("value is not a string")
+	}
+
+	// workload has two parts <resource-type>/<resource-name>
+	resource := strings.Split(v, "/")
+	if len(resource) != 2 {
+		return "", "", false, fmt.Errorf("workload must include <resource-type>/<resource-name>")
+	}
+
+	if s.Expression.MatchString(resource[1]) {
+		resourceType := strings.ToLower(resource[0])
+		for _, option := range s.AllowedOptions {
+			if option == resourceType {
+				return option, resource[1], true, nil
+			}
+		}
+	}
+	return "", "", false, fmt.Errorf("value does not match this regular expression: %s", s.Expression)
+}


Test patch:
diff --git a/internal/cmd/skupper/connector/kube/connector_create_test.go b/internal/cmd/skupper/connector/kube/connector_create_test.go
index 84ee98a2..5e98dd09 100644
--- a/internal/cmd/skupper/connector/kube/connector_create_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_create_test.go
@@ -10,6 +10,7 @@ import (
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
 	"gotest.tools/assert"
+	appsv1 "k8s.io/api/apps/v1"
 	v12 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -42,7 +43,6 @@ func TestCmdConnectorCreate_ValidateInput(t *testing.T) {
 					Spec: v1alpha1.ConnectorSpec{
 						Port:     8080,
 						Type:     "tcp",
-						Host:     "test",
 						Selector: "app=mySelector",
 					},
 					Status: v1alpha1.ConnectorStatus{
@@ -180,7 +180,7 @@ func TestCmdConnectorCreate_ValidateInput(t *testing.T) {
 				Timeout:  1 * time.Minute,
 			},
 			expectedErrors: []string{
-				"workload is not valid: value does not match this regular expression: ^[A-Za-z0-9=:./-]+$"},
+				"workload is not valid: workload must include <resource-type>/<resource-name>"},
 		},
 		{
 			name: "selector is not valid",
@@ -196,8 +196,8 @@ func TestCmdConnectorCreate_ValidateInput(t *testing.T) {
 			name: "timeout is not valid",
 			args: []string{"bad-timeout", "8080"},
 			flags: common.CommandConnectorCreateFlags{
-				Workload: "workload",
-				Timeout:  0 * time.Second,
+				Host:    "host",
+				Timeout: 0 * time.Second,
 			},
 			expectedErrors: []string{"timeout is not valid: duration must not be less than 10s; got 0s"},
 		},
@@ -205,9 +205,9 @@ func TestCmdConnectorCreate_ValidateInput(t *testing.T) {
 			name: "output is not valid",
 			args: []string{"bad-output", "1234"},
 			flags: common.CommandConnectorCreateFlags{
-				Workload: "workload",
-				Timeout:  10 * time.Second,
-				Output:   "not-supported",
+				Host:    "host",
+				Timeout: 10 * time.Second,
+				Output:  "not-supported",
 			},
 			expectedErrors: []string{
 				"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
@@ -234,30 +234,311 @@ func TestCmdConnectorCreate_ValidateInput(t *testing.T) {
 				Workload: "deployment/test",
 				Host:     "test",
 			},
+			k8sObjects: []runtime.Object{
+				&appsv1.Deployment{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Deployment",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "test",
+						Namespace: "test",
+					},
+					Spec: appsv1.DeploymentSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
+				},
+			},
 			expectedErrors: []string{
 				"If host is configured, cannot configure workload or selector",
 				"If workload is configured, cannot configure selector or host"},
 		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			command, err := newCmdConnectorCreateWithMocks("test", test.k8sObjects, test.skupperObjects, "")
+			assert.Assert(t, err)
+
+			command.Flags = &test.flags
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdConnectorCreate_ValidateWorkload(t *testing.T) {
+	type test struct {
+		name             string
+		args             []string
+		flags            common.CommandConnectorCreateFlags
+		k8sObjects       []runtime.Object
+		skupperObjects   []runtime.Object
+		expectedErrors   []string
+		expectedSelector string
+	}
+
+	testTable := []test{
 		{
-			name: "flags all valid",
-			args: []string{"my-connector-flags", "8080"},
-			flags: common.CommandConnectorCreateFlags{
-				RoutingKey:      "routingkeyname",
-				TlsSecret:       "secretname",
-				ConnectorType:   "tcp",
-				IncludeNotReady: true,
-				Timeout:         30 * time.Second,
-				Output:          "json",
+			name: "workload-no-deployment",
+			args: []string{"workload-deployment", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "deployment/backend",
+			},
+			expectedErrors: []string{
+				"failed trying to get Deployment specified by workload: deployments.apps \"backend\" not found"},
+		},
+		{
+			name: "workload-deployment-no-labels",
+			args: []string{"workload-deployment-no-labels", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "deployment/backend",
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.Deployment{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Deployment",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DeploymentSpec{
+						Selector: &v1.LabelSelector{},
+					},
+				},
+			},
+			expectedErrors: []string{"workload, no selector Matchlabels found"},
+		},
+		{
+			name: "workload-deployment",
+			args: []string{"workload-deployment", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "deployment/backend",
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.Deployment{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Deployment",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DeploymentSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
+				},
+			},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
+		},
+		{
+			name: "workload-no-service",
+			args: []string{"workload-no-service", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "service/backend",
+			},
+			expectedErrors: []string{
+				"failed trying to get Service specified by workload: services \"backend\" not found",
+			},
+		},
+		{
+			name: "workload-service-no-labels",
+			args: []string{"workload-service-no-labels", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "service/backend",
+			},
+			k8sObjects: []runtime.Object{
+				&v12.Service{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Service",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: v12.ServiceSpec{},
+				},
+			},
+			expectedErrors: []string{"workload, no selector labels found"},
+		},
+		{
+			name: "workload-service",
+			args: []string{"workload-service", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "service/backend",
+			},
+			k8sObjects: []runtime.Object{
+				&v12.Service{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Service",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: v12.ServiceSpec{
+						Selector: map[string]string{
+							"app": "backend",
+						},
+					},
+				},
+			},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
+		},
+		{
+			name: "workload-no-daemonset",
+			args: []string{"workload-no-daemonset", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "daemonset/backend",
+			},
+			expectedErrors: []string{
+				"failed trying to get DaemonSet specified by workload: daemonsets.apps \"backend\" not found",
+			},
+		},
+		{
+			name: "workload-daemonset-no-labels",
+			args: []string{"workload-daemonset-no-labels", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "daemonset/backend",
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.DaemonSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "daemonset",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DaemonSetSpec{
+						Selector: &v1.LabelSelector{},
+					},
+				},
+			},
+			expectedErrors: []string{"workload, no selector Matchlabels found"},
+		},
+		{
+			name: "workload-daemonset",
+			args: []string{"workload-daemonset", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "DaemonSet/backend",
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.DaemonSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "DaemonSet",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DaemonSetSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
+				},
+			},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
+		},
+		{
+			name: "workload-no-statefulset",
+			args: []string{"workload-no-statefulset", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "StatefulSet/backend",
+			},
+			expectedErrors: []string{
+				"failed trying to get StatefulSet specified by workload: statefulsets.apps \"backend\" not found",
+			},
+		},
+		{
+			name: "workload-statefulset-no-labels",
+			args: []string{"workload-statefulset-no-labels", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "statefulset/backend",
 			},
 			k8sObjects: []runtime.Object{
-				&v12.Secret{
+				&appsv1.StatefulSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "statefulset",
+					},
 					ObjectMeta: v1.ObjectMeta{
-						Name:      "secretname",
+						Name:      "backend",
 						Namespace: "test",
 					},
+					Spec: appsv1.StatefulSetSpec{
+						Selector: &v1.LabelSelector{},
+					},
+				},
+			},
+			expectedErrors: []string{"workload, no selector Matchlabels found"},
+		},
+		{
+			name: "workload-statefulset",
+			args: []string{"workload-statefulset", "1234"}, flags: common.CommandConnectorCreateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "statefulset/backend",
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.StatefulSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "StatefulSet",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.StatefulSetSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
 				},
 			},
-			expectedErrors: []string{},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
 		},
 	}
 
@@ -275,6 +556,8 @@ func TestCmdConnectorCreate_ValidateInput(t *testing.T) {
 
 			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
 
+			//validate selector is correct
+			assert.Check(t, command.selector == test.expectedSelector)
 		})
 	}
 }
diff --git a/internal/cmd/skupper/connector/kube/connector_update_test.go b/internal/cmd/skupper/connector/kube/connector_update_test.go
index 3f716aa3..712ce9ee 100644
--- a/internal/cmd/skupper/connector/kube/connector_update_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_update_test.go
@@ -10,6 +10,7 @@ import (
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
 	"gotest.tools/assert"
+	appsv1 "k8s.io/api/apps/v1"
 	v12 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -193,7 +194,7 @@ func TestCmdConnectorUpdate_ValidateInput(t *testing.T) {
 				},
 			},
 			expectedErrors: []string{
-				"workload is not valid: value does not match this regular expression: ^[A-Za-z0-9=:./-]+$"},
+				"workload is not valid: workload must include <resource-type>/<resource-name>"},
 		},
 		{
 			name: "selector is not valid",
@@ -281,6 +282,25 @@ func TestCmdConnectorUpdate_ValidateInput(t *testing.T) {
 					},
 				},
 			},
+			k8sObjects: []runtime.Object{
+				&appsv1.Deployment{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Deployment",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "test",
+						Namespace: "test",
+					},
+					Spec: appsv1.DeploymentSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
+				},
+			},
 			expectedErrors: []string{
 				"If host is configured, cannot configure workload or selector",
 				"If workload is configured, cannot configure selector or host"},
@@ -400,6 +420,508 @@ func TestCmdConnectorUpdate_ValidateInput(t *testing.T) {
 	}
 }
 
+func TestCmdConnectorUpdate_ValidateWorkload(t *testing.T) {
+	type test struct {
+		name             string
+		args             []string
+		flags            common.CommandConnectorUpdateFlags
+		k8sObjects       []runtime.Object
+		skupperObjects   []runtime.Object
+		expectedErrors   []string
+		expectedSelector string
+	}
+
+	testTable := []test{
+		{
+			name: "workload-no-deployment",
+			args: []string{"workload-deployment"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "deployment/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-deployment",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				"failed trying to get Deployment specified by workload: deployments.apps \"backend\" not found",
+			},
+		},
+		{
+			name: "workload-deployment-no-labels",
+			args: []string{"workload-deployment-no-labels"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "deployment/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-deployment-no-labels",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.Deployment{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Deployment",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DeploymentSpec{
+						Selector: &v1.LabelSelector{},
+					},
+				},
+			},
+			expectedErrors: []string{"workload, no selector Matchlabels found"},
+		},
+		{
+			name: "workload-deployment",
+			args: []string{"workload-deployment"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "deployment/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-deployment",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.Deployment{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Deployment",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DeploymentSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
+				},
+			},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
+		},
+		{
+			name: "workload-no-service",
+			args: []string{"workload-no-service"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "service/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-no-service",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				"failed trying to get Service specified by workload: services \"backend\" not found",
+			},
+		},
+		{
+			name: "workload-service-no-labels",
+			args: []string{"workload-service-no-labels"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "service/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-service-no-labels",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&v12.Service{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Service",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: v12.ServiceSpec{},
+				},
+			},
+			expectedErrors: []string{"workload, no selector labels found"},
+		},
+		{
+			name: "workload-service",
+			args: []string{"workload-service"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "service/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-service",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&v12.Service{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "Service",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: v12.ServiceSpec{
+						Selector: map[string]string{
+							"app": "backend",
+						},
+					},
+				},
+			},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
+		},
+		{
+			name: "workload-no-daemonset",
+			args: []string{"workload-no-daemonset"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "daemonset/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-no-daemonset",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				"failed trying to get DaemonSet specified by workload: daemonsets.apps \"backend\" not found",
+			},
+		},
+		{
+			name: "workload-daemonset-no-labels",
+			args: []string{"workload-daemonset-no-labels"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "daemonset/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-daemonset-no-labels",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.DaemonSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "daemonset",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DaemonSetSpec{
+						Selector: &v1.LabelSelector{},
+					},
+				},
+			},
+			expectedErrors: []string{"workload, no selector Matchlabels found"},
+		},
+		{
+			name: "workload-daemonset",
+			args: []string{"workload-daemonset"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "DaemonSet/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-daemonset",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.DaemonSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "DaemonSet",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.DaemonSetSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
+				},
+			},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
+		},
+		{
+			name: "workload-no-statefulset",
+			args: []string{"workload-no-statefulset"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "StatefulSet/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-no-statefulset",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				"failed trying to get StatefulSet specified by workload: statefulsets.apps \"backend\" not found",
+			},
+		},
+		{
+			name: "workload-statefulset-no-labels",
+			args: []string{"workload-statefulset-no-labels"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "statefulset/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-statefulset-no-labels",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.StatefulSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "statefulset",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.StatefulSetSpec{
+						Selector: &v1.LabelSelector{},
+					},
+				},
+			},
+			expectedErrors: []string{"workload, no selector Matchlabels found"},
+		},
+		{
+			name: "workload-statefulset",
+			args: []string{"workload-statefulset"}, flags: common.CommandConnectorUpdateFlags{
+				Timeout:  10 * time.Second,
+				Output:   "json",
+				Workload: "statefulset/backend",
+			},
+			skupperObjects: []runtime.Object{
+				&v1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "workload-statefulset",
+						Namespace: "test",
+					},
+					Status: v1alpha1.ConnectorStatus{
+						Status: v1alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&appsv1.StatefulSet{
+					TypeMeta: v1.TypeMeta{
+						APIVersion: "apps/v1",
+						Kind:       "StatefulSet",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend",
+						Namespace: "test",
+					},
+					Spec: appsv1.StatefulSetSpec{
+						Selector: &v1.LabelSelector{
+							MatchLabels: map[string]string{
+								"app": "backend",
+							},
+						},
+					},
+				},
+			},
+			expectedErrors:   []string{},
+			expectedSelector: "app=backend",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			command, err := newCmdConnectorUpdateWithMocks("test", test.k8sObjects, test.skupperObjects, "")
+			assert.Assert(t, err)
+
+			command.Flags = &test.flags
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+			//validate selector is correct
+			assert.Check(t, command.newSettings.selector == test.expectedSelector)
+		})
+	}
+}
+
 func TestCmdConnectorUpdate_Run(t *testing.T) {
 	type test struct {
 		name                string
diff --git a/pkg/kube/deployments_test.go b/pkg/kube/deployments_test.go
deleted file mode 100644
index 6d310749..00000000
--- a/pkg/kube/deployments_test.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package kube_test
-
-import (
-	"fmt"
-	"reflect"
-	"testing"
-
-	"github.com/skupperproject/skupper/pkg/kube"
-	"gotest.tools/assert"
-	v1 "k8s.io/api/apps/v1"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-const (
-	NS string = "ns1"
-)
-
-// TestGetContainerPort validates the behavior of GetContainerPort
-// based on a test table with different inputs and expectedErr outcomes
-func TestGetContainerPort(t *testing.T) {
-
-	type test struct {
-		name           string
-		deployment     *v1.Deployment
-		expectedResult map[int]int
-	}
-
-	// helper function to compose test table
-	newDeployment := func(name string, containers int, ports ...int) *v1.Deployment {
-		containerPorts := []corev1.ContainerPort{}
-
-		for idx, port := range ports {
-			containerPorts = append(containerPorts, corev1.ContainerPort{
-				Name:          fmt.Sprintf("port%d", idx),
-				ContainerPort: int32(port),
-			})
-		}
-
-		depContainers := []corev1.Container{}
-		for i := 1; i <= containers; i++ {
-			depContainers = append(depContainers, corev1.Container{
-				Name:  fmt.Sprintf("container%d", i),
-				Ports: containerPorts,
-			})
-		}
-
-		return &v1.Deployment{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      name,
-				Namespace: NS,
-			},
-			Spec: v1.DeploymentSpec{
-				Template: corev1.PodTemplateSpec{
-					Spec: corev1.PodSpec{
-						Containers: depContainers,
-					},
-				},
-			},
-		}
-	}
-
-	testTable := []test{
-		{"no-container-no-port", newDeployment("dep0", 0), map[int]int{}},
-		{"one-container-no-port", newDeployment("dep1", 1), map[int]int{}},
-		{"one-container-one-port", newDeployment("dep1", 1, 8080), map[int]int{8080: 8080}},
-		{"one-container-multiple-ports", newDeployment("dep2", 1, 8080, 8081, 8082), map[int]int{8080: 8080, 8081: 8081, 8082: 8082}},
-		{"multiple-containers-multiple-ports", newDeployment("dep3", 3, 8080, 8081, 8082), map[int]int{8080: 8080, 8081: 8081, 8082: 8082}},
-	}
-
-	for _, test := range testTable {
-		t.Run(test.name, func(t *testing.T) {
-			assert.Assert(t, reflect.DeepEqual(test.expectedResult, kube.GetContainerPort(test.deployment)))
-		})
-	}
-
-}
diff --git a/pkg/kube/securedaccess/access_test.go b/pkg/kube/securedaccess/access_test.go
index 348efc48..07b65c75 100644
--- a/pkg/kube/securedaccess/access_test.go
+++ b/pkg/kube/securedaccess/access_test.go
@@ -47,6 +47,7 @@ func TestSecuredAccessGeneral(t *testing.T) {
 		expectedEndpoints    []skupperv1alpha1.Endpoint
 		expectedError        string
 		defaultDomain        string
+		nodePorts            map[string]int32
 	}{
 		{
 			name: "loadbalancer",
@@ -1111,9 +1112,12 @@ func TestSecuredAccessGeneral(t *testing.T) {
 				{
 					Name: "port1",
 					Host: "mycluster.com",
-					Port: "0", //FIXME
+					Port: "33333",
 				},
 			},
+			nodePorts: map[string]int32{
+				"port1": 33333,
+			},
 		},
 		{
 			name: "local",
@@ -1843,6 +1847,20 @@ func TestSecuredAccessGeneral(t *testing.T) {
 						actual, err = m.clients.GetKubeClient().CoreV1().Services(desired.Namespace).UpdateStatus(context.Background(), actual, metav1.UpdateOptions{})
 						assert.Assert(t, err)
 					}
+					if tt.nodePorts != nil {
+						var updatedPorts []corev1.ServicePort
+						for _, port := range actual.Spec.Ports {
+							if nodePort, ok := tt.nodePorts[port.Name]; ok {
+								port.NodePort = nodePort
+							}
+							updatedPorts = append(updatedPorts, port)
+						}
+						if !reflect.DeepEqual(updatedPorts, actual.Spec.Ports) {
+							actual.Spec.Ports = updatedPorts
+							actual, err = m.clients.GetKubeClient().CoreV1().Services(desired.Namespace).Update(context.Background(), actual, metav1.UpdateOptions{})
+							assert.Assert(t, err)
+						}
+					}
 					err = m.CheckService(serviceKey(actual), actual)
 					assert.Assert(t, err)
 				}
diff --git a/pkg/utils/validator/simple_validator_test.go b/pkg/utils/validator/simple_validator_test.go
index f504a5cb..6ecbe050 100644
--- a/pkg/utils/validator/simple_validator_test.go
+++ b/pkg/utils/validator/simple_validator_test.go
@@ -184,18 +184,18 @@ func TestNewResourceStringValidator_Evaluate(t *testing.T) {
 	}
 }
 
-func TestNewWorkloadStringValidator(t *testing.T) {
+func TestNewSelectorStringValidator(t *testing.T) {
 
-	t.Run("Test New Resource String Validator constructor", func(t *testing.T) {
+	t.Run("Test New Selector String Validator constructor", func(t *testing.T) {
 
 		validRegexp, _ := regexp.Compile("^[A-Za-z0-9=:./-]+$")
 		expectedResult := &StringValidator{validRegexp}
-		actualResult := NewWorkloadStringValidator()
+		actualResult := NewSelectorStringValidator()
 		assert.Assert(t, reflect.DeepEqual(actualResult, expectedResult))
 	})
 }
 
-func TestNewWorkloadStringValidator_Evaluate(t *testing.T) {
+func TestNewSelectorStringValidator_Evaluate(t *testing.T) {
 	type test struct {
 		name   string
 		value  interface{}
@@ -217,7 +217,7 @@ func TestNewWorkloadStringValidator_Evaluate(t *testing.T) {
 
 	for _, test := range testTable {
 		t.Run(test.name, func(t *testing.T) {
-			stringValidator := NewWorkloadStringValidator()
+			stringValidator := NewSelectorStringValidator()
 			expectedResult := test.result
 			actualResult, _ := stringValidator.Evaluate(test.value)
 			assert.Assert(t, reflect.DeepEqual(actualResult, expectedResult))
@@ -266,3 +266,44 @@ func TestNewFilePathStringValidator_Evaluate(t *testing.T) {
 		})
 	}
 }
+
+func TestNewWorkloadStringValidator(t *testing.T) {
+
+	t.Run("Test New Workload String Validator constructor", func(t *testing.T) {
+
+		validRegexp, _ := regexp.Compile("^[A-Za-z0-9.-_]+$")
+		expectedResult := &WorkloadValidator{validRegexp, []string{"a", "b"}}
+		actualResult := NewWorkloadStringValidator([]string{"a", "b"})
+		assert.Assert(t, reflect.DeepEqual(actualResult, expectedResult))
+	})
+}
+func TestWorkloadStringValidator_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: "a/name", result: true},
+		{name: "string without /", value: "aname", result: false},
+		{name: "string with numbers", value: "b/name123", result: true},
+		{name: "number", value: 123, result: false},
+		{name: "nil value", value: nil, result: false},
+		{name: "string without name", value: "a/", result: false},
+		{name: "string without type", value: "/name", result: false},
+		{name: "string non matching type", value: "c/name", result: false},
+		{name: "bad value", value: "a/name@#", result: false},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			stringValidator := NewWorkloadStringValidator([]string{"a", "b"})
+			expectedResult := test.result
+			_, _, actualResult, _ := stringValidator.Evaluate(test.value)
+			assert.Assert(t, reflect.DeepEqual(actualResult, expectedResult))
+		})
+	}
+}