Repository: skupperproject/skupper
Pull request #1828 (2024-12-10 18:23:21)

Collector logging
Reopens https://github.com/skupperproject/skupper/pull/1783

I believe we've sorted out that this sort of a protocol-level vanflow logging option is useful for debugging different scenarios, but is not a particularly nice solution for users looking for an audit trail for service traffic. I've opened https://github.com/skupperproject/skupper/issues/1827 to tackle that use case.

c-kruse: Example network-observer startup logs: https://gist.githubusercontent.com/c-kruse/ac0d7da6c5261a45708b03cf4c0c3730/raw/fe3a60d6a3e94c24a3fccb9c3607ab0c47fc5a65/gistfile1.txt

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index b442e31f..45513863 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,6 +3,7 @@ orbs:
     docker: circleci/docker@1.0.1
     kube-orb: circleci/kubernetes@0.11.0
     go: circleci/go@1.7.1
+    helm: circleci/helm@3.1.0
 
 executors:
   go_cimg:
@@ -111,11 +112,16 @@ workflows:
           <<: *run_for_numeric_tags
           requires:
             - publish-oci-images
+      - generate-skupper-setup:
+          <<: *run_for_numeric_tags
+          requires:
+             - generate-manifest
       - publish-github-release-artifacts:
           <<: *run_for_numeric_tags
           requires:
             - publish-oci-images
             - generate-manifest
+            - generate-skupper-setup
           context:
             - skupper-org
   build:
@@ -203,6 +209,29 @@ jobs:
           paths:
             - skupper-manifest
 
+  generate-skupper-setup:
+    executor:
+      name: go_cimg
+    steps:
+      - checkout
+      - go/mod-download-cached
+      - run:
+          name: Install Helm CLI
+          command: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
+      - run: make generate-skupper-deployment-cluster-scoped
+      - run: make generate-skupper-deployment-namespace-scoped
+      - run: mkdir skupper-setup
+      - run: cp ./*.yaml skupper-setup
+      - run:
+          name: Verify skupper-setup Directory Contents
+          command: |
+            echo "Checking contents of skupper-setup directory:"
+            ls -l skupper-setup || echo "Directory skupper-setup does not exist or is empty!"
+      - persist_to_workspace:
+          root: .
+          paths:
+            - skupper-setup
+
   build-oci-images:
     executor: local_cluster_test_executor
     parameters:
@@ -341,6 +370,7 @@ jobs:
             done
             cd ${BASEDIR}
             cp "${BASEDIR}/skupper-manifest/manifest.json" "${BASEDIR}/archives"
+            cp skupper-setup/*.yaml archives
             ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace -prerelease -draft ${VERSION} "${BASEDIR}/archives"
 
   publish-github-prerelease-artifacts:
diff --git a/.gitignore b/.gitignore
index 88d33a1c..31adc73b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,5 @@ vendor/
 # Generated files
 manifest.json
 oci-archives/
+skupper-setup-cluster-scope.yaml
+skupper-setup-namespace-scope.yaml
diff --git a/Makefile b/Makefile
index 181d68d8..d2ba627e 100644
--- a/Makefile
+++ b/Makefile
@@ -119,6 +119,12 @@ generate-doc: build-doc-generator
 update-helm-crd:
 	./scripts/update-helm-crds.sh
 
+generate-skupper-deployment-cluster-scoped:
+	helm template ./charts/skupper-setup  --include-crds --set scope=cluster > skupper-setup-cluster-scope.yaml
+
+generate-skupper-deployment-namespace-scoped:
+	helm template ./charts/skupper-setup  --include-crds --set scope=namespace > skupper-setup-namespace-scope.yaml
+
 clean:
 	rm -rf skupper controller kube-adaptor \
 		network-observer generate-doc \
diff --git a/deployments/helm/Chart.yaml b/charts/skupper-setup/Chart.yaml
similarity index 66%
rename from deployments/helm/Chart.yaml
rename to charts/skupper-setup/Chart.yaml
index 31e77b1b..767500d3 100644
--- a/deployments/helm/Chart.yaml
+++ b/charts/skupper-setup/Chart.yaml
@@ -1,4 +1,5 @@
 apiVersion: v2
 name: skupper-setup
 description:  Helm chart for setting up Skupper.
-version: 0.1.0
\ No newline at end of file
+version: 0.1.0-devel
+appVersion: v2-latest
\ No newline at end of file
diff --git a/deployments/helm/README.md b/charts/skupper-setup/README.md
similarity index 100%
rename from deployments/helm/README.md
rename to charts/skupper-setup/README.md
diff --git a/deployments/helm/crds/skupper_access_grant_crd.yaml b/charts/skupper-setup/crds/skupper_access_grant_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_access_grant_crd.yaml
rename to charts/skupper-setup/crds/skupper_access_grant_crd.yaml
diff --git a/deployments/helm/crds/skupper_access_token_crd.yaml b/charts/skupper-setup/crds/skupper_access_token_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_access_token_crd.yaml
rename to charts/skupper-setup/crds/skupper_access_token_crd.yaml
diff --git a/deployments/helm/crds/skupper_attached_connector_binding_crd.yaml b/charts/skupper-setup/crds/skupper_attached_connector_binding_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_attached_connector_binding_crd.yaml
rename to charts/skupper-setup/crds/skupper_attached_connector_binding_crd.yaml
diff --git a/deployments/helm/crds/skupper_attached_connector_crd.yaml b/charts/skupper-setup/crds/skupper_attached_connector_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_attached_connector_crd.yaml
rename to charts/skupper-setup/crds/skupper_attached_connector_crd.yaml
diff --git a/deployments/helm/crds/skupper_certificate_crd.yaml b/charts/skupper-setup/crds/skupper_certificate_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_certificate_crd.yaml
rename to charts/skupper-setup/crds/skupper_certificate_crd.yaml
diff --git a/deployments/helm/crds/skupper_connector_crd.yaml b/charts/skupper-setup/crds/skupper_connector_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_connector_crd.yaml
rename to charts/skupper-setup/crds/skupper_connector_crd.yaml
diff --git a/deployments/helm/crds/skupper_link_crd.yaml b/charts/skupper-setup/crds/skupper_link_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_link_crd.yaml
rename to charts/skupper-setup/crds/skupper_link_crd.yaml
diff --git a/deployments/helm/crds/skupper_listener_crd.yaml b/charts/skupper-setup/crds/skupper_listener_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_listener_crd.yaml
rename to charts/skupper-setup/crds/skupper_listener_crd.yaml
diff --git a/deployments/helm/crds/skupper_router_access_crd.yaml b/charts/skupper-setup/crds/skupper_router_access_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_router_access_crd.yaml
rename to charts/skupper-setup/crds/skupper_router_access_crd.yaml
diff --git a/deployments/helm/crds/skupper_secured_access_crd.yaml b/charts/skupper-setup/crds/skupper_secured_access_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_secured_access_crd.yaml
rename to charts/skupper-setup/crds/skupper_secured_access_crd.yaml
diff --git a/deployments/helm/crds/skupper_site_crd.yaml b/charts/skupper-setup/crds/skupper_site_crd.yaml
similarity index 100%
rename from deployments/helm/crds/skupper_site_crd.yaml
rename to charts/skupper-setup/crds/skupper_site_crd.yaml
diff --git a/deployments/helm/templates/deploy_controller.yaml b/charts/skupper-setup/templates/deploy_controller.yaml
similarity index 100%
rename from deployments/helm/templates/deploy_controller.yaml
rename to charts/skupper-setup/templates/deploy_controller.yaml
diff --git a/deployments/helm/values.yaml b/charts/skupper-setup/values.yaml
similarity index 100%
rename from deployments/helm/values.yaml
rename to charts/skupper-setup/values.yaml
diff --git a/cmd/controller/deploy_cluster_scope.yaml b/cmd/controller/deploy_cluster_scope.yaml
deleted file mode 100644
index aef4ab13..00000000
--- a/cmd/controller/deploy_cluster_scope.yaml
+++ /dev/null
@@ -1,234 +0,0 @@
----
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: skupper
----
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: skupper-controller
-  namespace: skupper
-  labels:
-    application: skupper-controller
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    application: skupper-controller
-  name: skupper-controller
-rules:
-- apiGroups:
-  - ""
-  resources:
-  - nodes
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  - pods
-  - pods/exec
-  - services
-  - secrets
-  - serviceaccounts
-  - events
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - update
-  - delete
-  - patch
-- apiGroups:
-  - apps
-  resources:
-  - deployments
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - update
-  - delete
-  - patch
-- apiGroups:
-  - route.openshift.io
-  resources:
-  - routes
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-    - apps.openshift.io
-  resources:
-    - deploymentconfigs
-  verbs:
-    - get
-    - list
-    - watch
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingresses
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - projectcontour.io
-  resources:
-  - httpproxies
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - gateway.networking.k8s.io
-  resources:
-  - gateways
-  - tlsroutes
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-  - patch
-- apiGroups:
-  - coordination.k8s.io
-  resources:
-  - leases
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - rbac.authorization.k8s.io
-  resources:
-  - rolebindings
-  - roles
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - skupper.io
-  resources:
-  - sites
-  - sites/status
-  - links
-  - links/status
-  - accesstokens
-  - accesstokens/status
-  - accessgrants
-  - accessgrants/status
-  - listeners
-  - listeners/status
-  - connectors
-  - connectors/status
-  - attachedconnectors
-  - attachedconnectors/status
-  - attachedconnectorbindings
-  - attachedconnectorbindings/status
-  - routeraccesses
-  - routeraccesses/status
-  - securedaccesses
-  - securedaccesses/status
-  - certificates
-  - certificates/status
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - update
-  - delete
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  labels:
-    application: skupper-controller
-  name: skupper-controller
-subjects:
-- kind: ServiceAccount
-  name: skupper-controller
-  namespace: skupper
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: skupper-controller
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: skupper-controller
-  namespace: skupper
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      application: skupper-controller
-  template:
-    metadata:
-      labels:
-        app.kubernetes.io/part-of: skupper
-        application: skupper-controller
-    spec:
-      serviceAccountName: skupper-controller
-      # Prevent kubernetes from injecting env vars for grant service
-      # as these then collide with those that actually configure the
-      # controller:
-      enableServiceLinks: false
-      # Please ensure that you can use SeccompProfile and do not use
-      # if your project must work on old Kubernetes
-      # versions < 1.19 or on vendors versions which
-      # do NOT support this field by default
-      securityContext:
-        runAsNonRoot: true
-        seccompProfile:
-          type: RuntimeDefault
-      containers:
-      - name: controller
-        image: quay.io/skupper/controller:v2-latest
-        imagePullPolicy: Always
-        command: ["/app/controller"]
-        args: ["-enable-grants", "-grant-server-autoconfigure"]
-        env:
-        - name: SKUPPER_ADAPTOR_IMAGE
-          value: quay.io/skupper/kube-adaptor:v2-latest
-        securityContext:
-          capabilities:
-            drop:
-            - ALL
-          runAsNonRoot: true
-          allowPrivilegeEscalation: false
-        volumeMounts:
-        - name: tls-credentials
-          mountPath: /etc/controller
-      volumes:
-      - name: tls-credentials
-        emptyDir: {}
diff --git a/cmd/controller/deploy_namespace_scope.yaml b/cmd/controller/deploy_namespace_scope.yaml
deleted file mode 100644
index f8d238cc..00000000
--- a/cmd/controller/deploy_namespace_scope.yaml
+++ /dev/null
@@ -1,226 +0,0 @@
----
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: skupper-controller
-  labels:
-    application: skupper-controller
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  labels:
-    application: skupper-controller
-  name: skupper-controller
-rules:
-- apiGroups:
-  - ""
-  resources:
-  - nodes
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  - pods
-  - pods/exec
-  - services
-  - secrets
-  - serviceaccounts
-  - events
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - update
-  - delete
-  - patch
-- apiGroups:
-  - apps
-  resources:
-  - deployments
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - update
-  - delete
-  - patch
-- apiGroups:
-  - route.openshift.io
-  resources:
-  - routes
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-    - apps.openshift.io
-  resources:
-    - deploymentconfigs
-  verbs:
-    - get
-    - list
-    - watch
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingresses
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - projectcontour.io
-  resources:
-  - httpproxies
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - gateway.networking.k8s.io
-  resources:
-  - gateways
-  - tlsroutes
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-  - patch
-- apiGroups:
-  - coordination.k8s.io
-  resources:
-  - leases
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - rbac.authorization.k8s.io
-  resources:
-  - rolebindings
-  - roles
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - delete
-  - update
-- apiGroups:
-  - skupper.io
-  resources:
-  - sites
-  - sites/status
-  - links
-  - links/status
-  - accesstokens
-  - accesstokens/status
-  - accessgrants
-  - accessgrants/status
-  - listeners
-  - listeners/status
-  - connectors
-  - connectors/status
-  - attachedconnectors
-  - attachedconnectors/status
-  - attachedconnectorbindings
-  - attachedconnectorbindings/status
-  - routeraccesses
-  - routeraccesses/status
-  - securedaccesses
-  - securedaccesses/status
-  - certificates
-  - certificates/status
-  verbs:
-  - get
-  - list
-  - watch
-  - create
-  - update
-  - delete
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  labels:
-    application: skupper-controller
-  name: skupper-controller
-subjects:
-- kind: ServiceAccount
-  name: skupper-controller
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: skupper-controller
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: skupper-controller
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      application: skupper-controller
-  template:
-    metadata:
-      labels:
-        app.kubernetes.io/part-of: skupper
-        application: skupper-controller
-    spec:
-      serviceAccountName: skupper-controller
-      # Prevent kubernetes from injecting env vars for grant service
-      # as these then collide with those that actually configure the
-      # controller:
-      enableServiceLinks: false
-      securityContext:
-        runAsNonRoot: true
-        seccompProfile:
-          type: RuntimeDefault
-      containers:
-      - name: controller
-        image: quay.io/skupper/controller:v2-latest
-        imagePullPolicy: Always
-        command: ["/app/controller"]
-        args: ["-enable-grants", "-grant-server-autoconfigure"]
-        env:
-        - name: SKUPPER_ADAPTOR_IMAGE
-          value: quay.io/skupper/kube-adaptor:v2-latest
-        - name: WATCH_NAMESPACE
-          valueFrom:
-             fieldRef:
-               fieldPath: metadata.namespace
-        securityContext:
-          capabilities:
-            drop:
-            - ALL
-          runAsNonRoot: true
-          allowPrivilegeEscalation: false
-        volumeMounts:
-        - name: tls-credentials
-          mountPath: /etc/controller
-      volumes:
-      - name: tls-credentials
-        emptyDir: {}
diff --git a/cmd/network-observer/config.go b/cmd/network-observer/config.go
index 5b30d3d8..3d2baa92 100644
--- a/cmd/network-observer/config.go
+++ b/cmd/network-observer/config.go
@@ -12,9 +12,9 @@ import (
 )
 
 type Config struct {
-	APIListenAddress     string
-	APIDisableAccessLogs bool
-	APITLS               TLSSpec
+	APIListenAddress    string
+	APIEnableAccessLogs bool
+	APITLS              TLSSpec
 
 	EnableConsole   bool
 	ConsoleLocation string
@@ -24,6 +24,8 @@ type Config struct {
 	RouterTLS     TLSSpec
 	FlowRecordTTL time.Duration
 
+	VanflowLoggingProfile string
+
 	EnableProfile bool
 	CORSAllowAll  bool
 }
diff --git a/cmd/network-observer/internal/api/types_gen.go b/cmd/network-observer/internal/api/types_gen.go
index 0a890a81..2c0197bb 100644
--- a/cmd/network-observer/internal/api/types_gen.go
+++ b/cmd/network-observer/internal/api/types_gen.go
@@ -114,6 +114,8 @@ type ApplicationFlowRecord struct {
 	// Identity The unique identifier for the record.
 	Identity          string `json:"identity"`
 	Method            string `json:"method"`
+	Octets            uint64 `json:"octets"`
+	OctetsReverse     uint64 `json:"octetsReverse"`
 	Protocol          string `json:"protocol"`
 	RoutingKey        string `json:"routingKey"`
 	SourceProcessId   string `json:"sourceProcessId"`
diff --git a/cmd/network-observer/internal/collector/collector.go b/cmd/network-observer/internal/collector/collector.go
index bdf49d7c..eb7411f8 100644
--- a/cmd/network-observer/internal/collector/collector.go
+++ b/cmd/network-observer/internal/collector/collector.go
@@ -23,7 +23,7 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-func New(logger *slog.Logger, factory session.ContainerFactory, reg *prometheus.Registry, flowRecordTTL time.Duration) *Collector {
+func New(logger *slog.Logger, factory session.ContainerFactory, reg *prometheus.Registry, flowRecordTTL time.Duration, flowLogger func(vanflow.RecordMessage)) *Collector {
 	sessionCtr := factory.Create()
 
 	collector := &Collector{
@@ -37,6 +37,7 @@ func New(logger *slog.Logger, factory session.ContainerFactory, reg *prometheus.
 		recordRouting:  make(eventsource.RecordStoreMap),
 		metrics:        register(reg),
 		metricsAdaptor: opmetrics.New(reg),
+		flowLogging:    flowLogger,
 	}
 
 	collector.Records = store.NewSyncMapStore(store.SyncMapStoreConfig{
@@ -61,6 +62,7 @@ func New(logger *slog.Logger, factory session.ContainerFactory, reg *prometheus.
 type Collector struct {
 	logger        *slog.Logger
 	flowRecordTTL time.Duration
+	flowLogging   func(vanflow.RecordMessage)
 
 	session   session.Container
 	discovery *eventsource.Discovery
@@ -375,6 +377,9 @@ func (c *Collector) discoveryHandler(ctx context.Context) func(eventsource.Info)
 			}
 		}
 
+		if c.flowLogging != nil {
+			client.OnRecord(c.flowLogging)
+		}
 		client.OnRecord(router.Route)
 
 		for _, address := range addresses {
diff --git a/cmd/network-observer/internal/flowlog/logging.go b/cmd/network-observer/internal/flowlog/logging.go
new file mode 100644
index 00000000..f386b902
--- /dev/null
+++ b/cmd/network-observer/internal/flowlog/logging.go
@@ -0,0 +1,241 @@
+package flowlog
+
+import (
+	"context"
+	"encoding/json"
+	"hash/fnv"
+	"log/slog"
+	"slices"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/skupperproject/skupper/pkg/vanflow"
+	"golang.org/x/time/rate"
+)
+
+const (
+	recordTypeMatchesAll string = "internal.flowlog.matchAll"
+	doNotSample                 = staticSampler(false)
+)
+
+// Rule specifies how particular vanflow record types should be logged.
+type Rule struct {
+	// Priority of the rule. Lowest matching a record type wins.
+	Priority int
+	// Match is the set of record types the rule applies to
+	Match RecordTypeSet
+	// Strategy for sampling records
+	Strategy SampleStrategy
+}
+
+type MessageHandler func(vanflow.RecordMessage)
+
+// New creates a MessageHandler given a set of rules and a log output function
+func New(ctx context.Context, logFn func(msg string, args ...any), rules []Rule) MessageHandler {
+	handler := &handler{
+		logFn: logFn,
+	}
+
+	for _, rule := range rules {
+		if rule.Strategy == nil || rule.Match == nil {
+			continue
+		}
+		handler.rules = append(handler.rules, rule)
+	}
+	slices.SortFunc(handler.rules, func(l, r Rule) int {
+		return l.Priority - r.Priority
+	})
+	go handler.report(ctx)
+	return handler.handle
+}
+
+type SampleStrategy interface {
+	// Sample returns true when the record should be logged
+	Sample(r vanflow.Record) bool
+}
+
+type staticSampler bool
+
+func (s staticSampler) Sample(vanflow.Record) bool {
+	return bool(s)
+}
+
+// Unlimited SampleStrategy always samples
+func Unlimited() SampleStrategy {
+	return staticSampler(true)
+}
+
+type rateLimited struct {
+	limiter *rate.Limiter
+}
+
+// RateLimited SampleStrategy samples events up to a limit (in events per
+// second). Events exceeding the limit are not logged.
+func RateLimited(limit float64, burst int) SampleStrategy {
+	return rateLimited{
+		limiter: rate.NewLimiter(rate.Limit(limit), burst),
+	}
+}
+
+func (r rateLimited) Sample(vanflow.Record) bool {
+	return r.limiter.Allow()
+}
+
+// TransportFlowHash uses a deterministic hash based on a TransportBiflow ID.
+// Uses the AppBiflow Parent field (Transport ID) so that ideally related flows
+// are sampled together.
+func TransportFlowHash(percent float64, parent SampleStrategy) SampleStrategy {
+	if percent < 0 || percent >= 1.0 {
+		panic("percent must be value in range [0, 1)")
+	}
+	if parent == nil {
+		parent = Unlimited()
+	}
+	return hashBasedSampler{
+		parent: parent,
+		mod:    10_000,
+		q:      uint32(percent * 10_000),
+	}
+}
+
+type hashBasedSampler struct {
+	parent SampleStrategy
+	mod    uint32
+	q      uint32
+}
+
+func (h hashBasedSampler) Sample(r vanflow.Record) bool {
+	var transportID string
+	switch flow := r.(type) {
+	case vanflow.TransportBiflowRecord:
+		transportID = flow.ID
+	case vanflow.AppBiflowRecord:
+		if flow.Parent == nil {
+			return false
+		}
+		transportID = *flow.Parent
+	default:
+		return false
+	}
+	hash := fnv.New32a()
+	hash.Write([]byte(transportID))
+	m := hash.Sum32() % h.mod
+	if h.q >= m {
+		return h.parent.Sample(r)
+	}
+	return false
+}
+
+// RecordTypeSet specifies the Record Types that a Rule is applicable for.
+type RecordTypeSet map[vanflow.TypeMeta]struct{}
+
+func (r RecordTypeSet) matchesAll() bool {
+	_, ok := r[vanflow.TypeMeta{APIVersion: recordTypeMatchesAll}]
+	return ok
+}
+
+func NewRecordTypeSet(records ...vanflow.Record) RecordTypeSet {
+	set := RecordTypeSet{}
+	for _, r := range records {
+		set[r.GetTypeMeta()] = struct{}{}
+	}
+	return set
+}
+
+// NewRecordTypeSetAll returns a special RecordTypeSet that matches records of
+// all types
+func NewRecordTypeSetAll() RecordTypeSet {
+	set := RecordTypeSet{}
+	set[vanflow.TypeMeta{APIVersion: recordTypeMatchesAll}] = struct{}{}
+	return set
+}
+
+type handler struct {
+	logFn func(msg string, args ...any)
+	rules []Rule
+
+	resolved sync.Map
+	sampled  sync.Map
+}
+
+func (h *handler) report(ctx context.Context) {
+	ticker := time.NewTicker(time.Minute)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-ticker.C:
+			h.logReport()
+		}
+	}
+}
+
+func (h *handler) logReport() {
+	sampleCounts := make(map[string]int)
+	h.sampled.Range(func(k, v any) bool {
+		h.sampled.Delete(k)
+		typ, ct := k.(vanflow.TypeMeta), v.(*atomic.Int64)
+		sampleCounts[typ.String()] = int(ct.Load())
+		return true
+	})
+
+	if len(sampleCounts) == 0 {
+		return
+	}
+	var counts []any
+	for typ, count := range sampleCounts {
+		counts = append(counts, slog.Int(typ, count))
+
+	}
+	h.logFn("some vanflow records were not logged", counts...)
+}
+
+func (h *handler) resolve(typ vanflow.TypeMeta) SampleStrategy {
+	r, ok := h.resolved.Load(typ)
+	if ok {
+		return r.(SampleStrategy)
+	}
+	var strategy SampleStrategy = doNotSample
+	for _, rule := range h.rules {
+		if rule.Match.matchesAll() {
+			strategy = rule.Strategy
+			break
+		}
+		if _, ok := rule.Match[typ]; ok {
+			strategy = rule.Strategy
+			break
+		}
+	}
+	h.resolved.Store(typ, strategy)
+	return strategy
+}
+
+func (h *handler) handle(msg vanflow.RecordMessage) {
+	attrs := slog.Group("message", slog.String("to", msg.To), slog.String("subject", msg.Subject))
+	for _, record := range msg.Records {
+		typ := record.GetTypeMeta()
+		strategy := h.resolve(typ)
+		if !strategy.Sample(record) {
+			if strategy != doNotSample {
+				prev, _ := h.sampled.LoadOrStore(typ, new(atomic.Int64))
+				prev.(*atomic.Int64).Add(1)
+			}
+			continue
+		}
+
+		// TODO(ck) more efficient slog.LogValuer for vanflow records?
+		raw, _ := json.Marshal(record)
+		var out map[string]any
+		json.Unmarshal(raw, &out)
+		recordValues := make([]any, 0, len(out))
+		for k, v := range out {
+			if v == nil {
+				continue
+			}
+			recordValues = append(recordValues, slog.Any(k, v))
+		}
+		h.logFn(record.GetTypeMeta().String(), slog.Group("record", recordValues...), attrs)
+	}
+}
diff --git a/cmd/network-observer/internal/server/views/flows.go b/cmd/network-observer/internal/server/views/flows.go
index f8ce4fef..1098b60f 100644
--- a/cmd/network-observer/internal/server/views/flows.go
+++ b/cmd/network-observer/internal/server/views/flows.go
@@ -26,6 +26,7 @@ func NewConnectionsSliceProvider(stor store.Interface) func([]store.Entry) []api
 		return results
 	}
 }
+
 func NewConnectionsProvider(stor store.Interface) func(collector.ConnectionRecord) (api.ConnectionRecord, bool) {
 	traceProvider := newTraceProvider(stor)
 	return func(conn collector.ConnectionRecord) (api.ConnectionRecord, bool) {
@@ -199,6 +200,8 @@ func NewRequestProvider(stor store.Interface) func(collector.RequestRecord) (api
 		out.StartTime, out.EndTime = vanflowTimes(record.BaseRecord)
 		setOpt(&out.Method, record.Method)
 		setOpt(&out.Status, record.Result)
+		setOpt(&out.Octets, record.Octets)
+		setOpt(&out.OctetsReverse, record.OctetsReverse)
 
 		if record.EndTime != nil && record.StartTime != nil && record.EndTime.After(record.StartTime.Time) {
 			if record.EndTime != nil && record.StartTime != nil {
diff --git a/cmd/network-observer/main.go b/cmd/network-observer/main.go
index 270965ae..84986d31 100755
--- a/cmd/network-observer/main.go
+++ b/cmd/network-observer/main.go
@@ -19,7 +19,9 @@ import (
 
 	"github.com/skupperproject/skupper/cmd/network-observer/internal/api"
 	"github.com/skupperproject/skupper/cmd/network-observer/internal/collector"
+	"github.com/skupperproject/skupper/cmd/network-observer/internal/flowlog"
 	"github.com/skupperproject/skupper/cmd/network-observer/internal/server"
+	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/session"
 	"github.com/skupperproject/skupper/pkg/version"
 )
@@ -44,7 +46,27 @@ func run(cfg Config) error {
 		return fmt.Errorf("failed to load router tls configuration: %s", err)
 	}
 
-	collector := collector.New(logger.With(slog.String("component", "collector")), session.NewContainerFactory(cfg.RouterURL, sessionConfig), reg, cfg.FlowRecordTTL)
+	flowLogger := func(vanflow.RecordMessage) {}
+	vanflowSLog := logger.With(slog.String("component", "vanflow"))
+	switch cfg.VanflowLoggingProfile {
+	case "silent":
+	case "minimal":
+		flowLogger = flowlog.New(ctx, vanflowSLog.Info, loggingProfileMinimal)
+	case "moderate":
+		flowLogger = flowlog.New(ctx, vanflowSLog.Info, loggingProfileModerate)
+	case "all":
+		flowLogger = flowlog.New(ctx, vanflowSLog.Info, loggingProfileAll)
+	default:
+		return fmt.Errorf("unknown logging profile: %s", cfg.VanflowLoggingProfile)
+	}
+
+	collector := collector.New(
+		logger.With(slog.String("component", "collector")),
+		session.NewContainerFactory(cfg.RouterURL, sessionConfig),
+		reg,
+		cfg.FlowRecordTTL,
+		flowLogger,
+	)
 
 	collectorAPI := server.New(
 		logger.With(slog.String("component", "api")),
@@ -77,7 +99,7 @@ func run(cfg Config) error {
 		apiMux.PathPrefix("/").Handler(handleConsoleAssets(cfg.ConsoleLocation))
 	}
 
-	if !cfg.APIDisableAccessLogs {
+	if cfg.APIEnableAccessLogs {
 		mux.Use(func(next http.Handler) http.Handler {
 			return handlers.LoggingHandler(os.Stdout, next)
 		})
@@ -182,7 +204,7 @@ func main() {
 	flags.BoolVar(&cfg.RouterTLS.SkipVerify, "router-tls-insecure", false, "Set to skip verification of the router certificate and host name")
 
 	flags.StringVar(&cfg.APIListenAddress, "listen", ":8080", "The address that the API Server will listen on")
-	flags.BoolVar(&cfg.APIDisableAccessLogs, "disable-access-logs", false, "Disables access logging for the API Server")
+	flags.BoolVar(&cfg.APIEnableAccessLogs, "enable-access-logs", false, "Enable access logging for the API Server")
 	flags.StringVar(&cfg.APITLS.Cert, "tls-cert", "", "Path to the API Server certificate file")
 	flags.StringVar(&cfg.APITLS.Key, "tls-key", "", "Path to the API Server certificate key file matching tls-cert")
 
@@ -194,6 +216,8 @@ func main() {
 	flags.BoolVar(&cfg.CORSAllowAll, "cors-allow-all", false, "Development option to allow all origins")
 	flags.BoolVar(&cfg.EnableProfile, "profile", false, "Exposes the runtime profiling facilities from net/http/pprof on http://localhost:9970")
 
+	flags.StringVar(&cfg.VanflowLoggingProfile, "vanflow-logging-profile", "silent", "Controls low level vanflow record logging. Options are silent, minimal, moderate and all")
+
 	flags.Parse(os.Args[1:])
 	if *isVersion {
 		fmt.Println(version.Version)
diff --git a/cmd/network-observer/profiles.go b/cmd/network-observer/profiles.go
new file mode 100644
index 00000000..f80a091d
--- /dev/null
+++ b/cmd/network-observer/profiles.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+	"github.com/skupperproject/skupper/cmd/network-observer/internal/flowlog"
+	"github.com/skupperproject/skupper/pkg/vanflow"
+)
+
+var (
+	// loggingProfileMinimal logs 1 vanflow event per second (with bursts up to 32)
+	// reduces Link Record noise to 1 every ~20s.
+	// excludes network flow records
+	loggingProfileMinimal = []flowlog.Rule{
+		{
+			Priority: 5,
+			Match: flowlog.NewRecordTypeSet(
+				vanflow.SiteRecord{}, vanflow.RouterRecord{},
+				vanflow.ProcessRecord{}, vanflow.ConnectorRecord{},
+				vanflow.ListenerRecord{}, vanflow.RouterAccessRecord{},
+				vanflow.LogRecord{},
+			),
+			Strategy: flowlog.RateLimited(1.0, 32),
+		}, {
+			Priority: 1,
+			Match:    flowlog.NewRecordTypeSet(vanflow.LinkRecord{}),
+			Strategy: flowlog.RateLimited(0.05, 32),
+		},
+	}
+	// loggingProfileModerate is similar to minimal but doubles rate and burst
+	// limits. Also samples 1 in every 10 network flows up to 2 events per second.
+	loggingProfileModerate = []flowlog.Rule{
+		{
+			Priority: 5,
+			Match:    flowlog.NewRecordTypeSetAll(),
+			Strategy: flowlog.RateLimited(2.0, 64),
+		}, {
+			Priority: 1,
+			Match:    flowlog.NewRecordTypeSet(vanflow.LinkRecord{}),
+			Strategy: flowlog.RateLimited(0.1, 64),
+		}, {
+			Priority: 1,
+			Match: flowlog.NewRecordTypeSet(
+				vanflow.AppBiflowRecord{},
+				vanflow.TransportBiflowRecord{},
+			),
+			Strategy: flowlog.TransportFlowHash(0.1, flowlog.RateLimited(2.0, 64)),
+		},
+	}
+	// loggingProfileAll logs all vanflow events.
+	loggingProfileAll = []flowlog.Rule{
+		{
+			Match:    flowlog.NewRecordTypeSetAll(),
+			Strategy: flowlog.Unlimited(),
+		},
+	}
+)
diff --git a/cmd/network-observer/spec/openapi.yaml b/cmd/network-observer/spec/openapi.yaml
index 4f657497..dfb35d1b 100644
--- a/cmd/network-observer/spec/openapi.yaml
+++ b/cmd/network-observer/spec/openapi.yaml
@@ -1378,6 +1378,8 @@ components:
             - status
             - traceRouters
             - traceSites
+            - octets
+            - octetsReverse
           properties:
             connectionId:
               type: string
@@ -1412,6 +1414,12 @@ components:
             status:
               type: string
               example: 200
+            octets:
+              type: integer
+              format: uint64
+            octetsReverse:
+              type: integer
+              format: uint64
             traceRouters:
               type: array
               description: Ordered array of the names of routers involved in proxying the connection
diff --git a/cmd/skupper/README.md b/cmd/skupper/README.md
index 9fdcf93c..1392cdd2 100644
--- a/cmd/skupper/README.md
+++ b/cmd/skupper/README.md
@@ -5,26 +5,8 @@ kubectl create namespace skupper
 kubectl config set-context --current --namespace skupper
 ```
 
-Apply the CRDs in the cluster:
+Apply the CRDs in the cluster and deploy the skupper controller ([instructions](../../charts/skupper-setup/README.md)):
 
-```
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_access_grant_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_access_token_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_attached_connector_anchor_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_attached_connector_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_certificate_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_connector_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_link_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_listener_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_router_access_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_secured_access_crd.yaml
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/api/types/crds/skupper_site_crd.yaml
-```
-
-Deploy the Skupper controller in the cluster:
-```
-kubectl apply -f https://raw.githubusercontent.com/skupperproject/skupper/v2/cmd/controller/deploy_cluster_scope.yaml
-```
 
 # Deploy application in two namespaces (or contexts)
 
diff --git a/internal/cmd/skupper/common/constants.go b/internal/cmd/skupper/common/constants.go
index a7f33d25..378d7fc9 100644
--- a/internal/cmd/skupper/common/constants.go
+++ b/internal/cmd/skupper/common/constants.go
@@ -15,3 +15,7 @@ const (
 	Sites          string = "sites"
 	RouterAccesses string = "routerAccesses"
 )
+
+const (
+	SiteConfigNameKey string = "name"
+)
diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go
index 5c8bcbd0..f7520297 100644
--- a/internal/cmd/skupper/common/flags.go
+++ b/internal/cmd/skupper/common/flags.go
@@ -82,6 +82,10 @@ for other Kubernetes flavors, loadbalancer is the default.`
 	FlagNameWait       = "wait"
 	FlagDescWait       = "Wait for the given status before exiting. Choices: configured, ready, none"
 	FlagDescDeleteWait = "Wait for deletion to complete before exiting"
+
+	FlagNameAll       = "all"
+	FlagDescAll       = "delete all skupper resources in current namespace"
+	FlagDescDeleteAll = "delete all skupper resources associated with site in current namespace"
 )
 
 type CommandSiteCreateFlags struct {
@@ -109,10 +113,15 @@ type CommandSiteUpdateFlags struct {
 }
 
 type CommandSiteDeleteFlags struct {
+	All     bool
 	Timeout time.Duration
 	Wait    bool
 }
 
+type CommandSiteStatusFlags struct {
+	Output string
+}
+
 type CommandLinkGenerateFlags struct {
 	TlsCredentials     string
 	Cost               string
@@ -132,6 +141,7 @@ type CommandLinkDeleteFlags struct {
 	Timeout time.Duration
 	Wait    bool
 }
+
 type CommandLinkStatusFlags struct {
 	Output string
 }
@@ -141,6 +151,7 @@ type CommandTokenIssueFlags struct {
 	Timeout            time.Duration
 	ExpirationWindow   time.Duration
 	RedemptionsAllowed int
+	Cost               string
 }
 
 type CommandTokenRedeemFlags struct {
diff --git a/internal/cmd/skupper/connector/kube/connector_create.go b/internal/cmd/skupper/connector/kube/connector_create.go
index c2d3c5e7..72827154 100644
--- a/internal/cmd/skupper/connector/kube/connector_create.go
+++ b/internal/cmd/skupper/connector/kube/connector_create.go
@@ -326,9 +326,9 @@ func (cmd *CmdConnectorCreate) WaitUntil() error {
 	})
 
 	if err != nil && connectorCondition == nil {
-		return fmt.Errorf("Connector %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Connector %q is not yet %s, check the status for more information\n", cmd.name, cmd.status)
 	} else if err != nil && connectorCondition.Status == metav1.ConditionFalse {
-		return fmt.Errorf("Connector %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Connector %q is not yet %s: %s\n", cmd.name, cmd.status, connectorCondition.Message)
 	}
 
 	fmt.Printf("Connector %q is %s.\n", cmd.name, cmd.status)
diff --git a/internal/cmd/skupper/connector/kube/connector_update.go b/internal/cmd/skupper/connector/kube/connector_update.go
index b15a2360..14d14f60 100644
--- a/internal/cmd/skupper/connector/kube/connector_update.go
+++ b/internal/cmd/skupper/connector/kube/connector_update.go
@@ -330,9 +330,9 @@ func (cmd *CmdConnectorUpdate) WaitUntil() error {
 	})
 
 	if err != nil && connectorCondition == nil {
-		return fmt.Errorf("Connector %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Connector %q is not yet %s, check the status for more information\n", cmd.name, cmd.status)
 	} else if err != nil && connectorCondition.Status == metav1.ConditionFalse {
-		return fmt.Errorf("Connector %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Connector %q is not yet %s: %s\n", cmd.name, cmd.status, connectorCondition.Message)
 	}
 
 	fmt.Printf("Connector %q is %s.\n", cmd.name, cmd.status)
diff --git a/internal/cmd/skupper/connector/nonkube/connector_create.go b/internal/cmd/skupper/connector/nonkube/connector_create.go
index e4075963..2e3d2cab 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_create.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_create.go
@@ -112,7 +112,10 @@ func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error {
 		validationErrors = append(validationErrors, fmt.Errorf("host name must be configured: an IP address or hostname is expected"))
 	}
 	if cmd.Flags.TlsCredentials != "" {
-		// TBD what is valid TlsCredentials
+		ok, err := resourceStringValidator.Evaluate(cmd.Flags.TlsCredentials)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("tlsCredentials value is not valid: %s", err))
+		}
 	}
 
 	return validationErrors
diff --git a/internal/cmd/skupper/connector/nonkube/connector_delete.go b/internal/cmd/skupper/connector/nonkube/connector_delete.go
index eea74970..43f9d808 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_delete.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_delete.go
@@ -31,6 +31,7 @@ func (cmd *CmdConnectorDelete) NewClient(cobraCommand *cobra.Command, args []str
 
 func (cmd *CmdConnectorDelete) ValidateInput(args []string) []error {
 	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: false, LogWarning: false}
 	resourceStringValidator := validator.NewResourceStringValidator()
 
 	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameContext) != nil && cmd.CobraCmd.Flag(common.FlagNameContext).Value.String() != "" {
@@ -57,6 +58,14 @@ func (cmd *CmdConnectorDelete) ValidateInput(args []string) []error {
 		}
 	}
 
+	if cmd.connectorName != "" {
+		// Validate that there is already a connector with this name
+		connector, err := cmd.connectorHandler.Get(cmd.connectorName, opts)
+		if connector == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("connector %s does not exist", cmd.connectorName))
+		}
+	}
+
 	return validationErrors
 }
 
diff --git a/internal/cmd/skupper/connector/nonkube/connector_status.go b/internal/cmd/skupper/connector/nonkube/connector_status.go
index 2ad5f1d8..014c1141 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_status.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_status.go
@@ -35,7 +35,7 @@ func (cmd *CmdConnectorStatus) NewClient(cobraCommand *cobra.Command, args []str
 
 func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error {
 	var validationErrors []error
-	opts := fs.GetOptions{RuntimeFirst: true}
+	opts := fs.GetOptions{RuntimeFirst: true, LogWarning: false}
 	resourceStringValidator := validator.NewResourceStringValidator()
 	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
 
@@ -75,7 +75,7 @@ func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error {
 }
 
 func (cmd *CmdConnectorStatus) Run() error {
-	opts := fs.GetOptions{RuntimeFirst: true}
+	opts := fs.GetOptions{RuntimeFirst: true, LogWarning: true}
 	if cmd.connectorName == "" {
 		connectors, err := cmd.connectorHandler.List()
 		if connectors == nil || err != nil {
@@ -122,8 +122,8 @@ func (cmd *CmdConnectorStatus) Run() error {
 				status = "Ok"
 			}
 			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\n",
-				connector.Name, status, connector.Spec.RoutingKey, connector.Spec.Host, connector.Spec.Port))
+			fmt.Fprintln(tw, fmt.Sprintf("Name:\t%s\nStatus:\t%s\nRouting key:\t%s\nHost:\t%s\nPort:\t%d\nTlsCredentials:\t%s",
+				connector.Name, status, connector.Spec.RoutingKey, connector.Spec.Host, connector.Spec.Port, connector.Spec.TlsCredentials))
 			_ = tw.Flush()
 		}
 	}
diff --git a/internal/cmd/skupper/connector/nonkube/connector_update.go b/internal/cmd/skupper/connector/nonkube/connector_update.go
index c495dcdd..c291dc04 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_update.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_update.go
@@ -44,7 +44,7 @@ func (cmd *CmdConnectorUpdate) NewClient(cobraCommand *cobra.Command, args []str
 
 func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 	var validationErrors []error
-	opts := fs.GetOptions{RuntimeFirst: false}
+	opts := fs.GetOptions{RuntimeFirst: false, LogWarning: false}
 	resourceStringValidator := validator.NewResourceStringValidator()
 	numberValidator := validator.NewNumberValidator()
 	connectorTypeValidator := validator.NewOptionValidator(common.ConnectorTypes)
@@ -132,7 +132,14 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 			cmd.output = cmd.Flags.Output
 		}
 	}
-
+	if cmd.Flags.TlsCredentials != "" {
+		ok, err := resourceStringValidator.Evaluate(cmd.Flags.TlsCredentials)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("tlsCredentials value is not valid: %s", err))
+		} else {
+			cmd.newSettings.tlsCredentials = cmd.Flags.TlsCredentials
+		}
+	}
 	return validationErrors
 }
 
diff --git a/internal/cmd/skupper/link/kube/link_update.go b/internal/cmd/skupper/link/kube/link_update.go
index 2ba3d753..5da082f6 100644
--- a/internal/cmd/skupper/link/kube/link_update.go
+++ b/internal/cmd/skupper/link/kube/link_update.go
@@ -216,9 +216,9 @@ func (cmd *CmdLinkUpdate) WaitUntil() error {
 	})
 
 	if err != nil && linkCondition == nil {
-		return fmt.Errorf("Link %q is not %s yet, check the status for more information\n", cmd.linkName, cmd.status)
+		return fmt.Errorf("Link %q is not yet %s, check the status for more information\n", cmd.linkName, cmd.status)
 	} else if err != nil && linkCondition.Status == metav1.ConditionFalse {
-		return fmt.Errorf("Link %q is %s with errors, check the status for more information\n", cmd.linkName, cmd.status)
+		return fmt.Errorf("Link %q is not yet %s: %s\n", cmd.linkName, cmd.status, linkCondition.Message)
 	}
 
 	fmt.Printf("Link %q is updated\n", cmd.linkName)
diff --git a/internal/cmd/skupper/listener/kube/listener_create.go b/internal/cmd/skupper/listener/kube/listener_create.go
index 6574f923..c847be93 100644
--- a/internal/cmd/skupper/listener/kube/listener_create.go
+++ b/internal/cmd/skupper/listener/kube/listener_create.go
@@ -239,9 +239,9 @@ func (cmd *CmdListenerCreate) WaitUntil() error {
 	})
 
 	if err != nil && listenerCondition == nil {
-		return fmt.Errorf("Listener %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Listener %q is not yet %s, check the status for more information\n", cmd.name, cmd.status)
 	} else if err != nil && listenerCondition.Status == metav1.ConditionFalse {
-		return fmt.Errorf("Listener %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Listener %q is not yet %s: %s\n", cmd.name, cmd.status, listenerCondition.Message)
 	}
 
 	fmt.Printf("Listener %q is %s.\n", cmd.name, cmd.status)
diff --git a/internal/cmd/skupper/listener/kube/listener_update.go b/internal/cmd/skupper/listener/kube/listener_update.go
index ca829296..d6672644 100644
--- a/internal/cmd/skupper/listener/kube/listener_update.go
+++ b/internal/cmd/skupper/listener/kube/listener_update.go
@@ -232,9 +232,9 @@ func (cmd *CmdListenerUpdate) WaitUntil() error {
 	})
 
 	if err != nil && listenerCondition == nil {
-		return fmt.Errorf("Listener %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Listener %q is not yet %s, check the status for more information\n", cmd.name, cmd.status)
 	} else if err != nil && listenerCondition.Status == metav1.ConditionFalse {
-		return fmt.Errorf("Listener %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
+		return fmt.Errorf("Listener %q is not yet %s: %s\n", cmd.name, cmd.status, listenerCondition.Message)
 	}
 
 	fmt.Printf("Listener %q is updated\n", cmd.name)
diff --git a/internal/cmd/skupper/listener/listener.go b/internal/cmd/skupper/listener/listener.go
index 3215f561..a2b150d4 100644
--- a/internal/cmd/skupper/listener/listener.go
+++ b/internal/cmd/skupper/listener/listener.go
@@ -46,7 +46,7 @@ func CmdListenerCreateFactory(configuredPlatform types.Platform) *cobra.Command
 
 	cmd.Flags().StringVarP(&cmdFlags.RoutingKey, common.FlagNameRoutingKey, "r", "", common.FlagDescRoutingKey)
 	cmd.Flags().StringVar(&cmdFlags.Host, common.FlagNameListenerHost, "", common.FlagDescListenerHost)
-	cmd.Flags().StringVarP(&cmdFlags.TlsCredentials, common.FlagNameTlsCredentials, "t", "", common.FlagDescTlsCredentials)
+	cmd.Flags().StringVar(&cmdFlags.TlsCredentials, common.FlagNameTlsCredentials, "", common.FlagDescTlsCredentials)
 	cmd.Flags().StringVar(&cmdFlags.ListenerType, common.FlagNameListenerType, "tcp", common.FlagDescListenerType)
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
@@ -73,7 +73,7 @@ func CmdListenerUpdateFactory(configuredPlatform types.Platform) *cobra.Command
 		Use:   "update <name>",
 		Short: "update a listener",
 		Long: `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`,
+	The user can change port, host name, TLS credentials, listener type and routing key`,
 		Example: "skupper listener update database --host mysql --port 3306",
 	}
 
diff --git a/internal/cmd/skupper/listener/nonkube/listener_create.go b/internal/cmd/skupper/listener/nonkube/listener_create.go
index 2b53b788..70eec0fc 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_create.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_create.go
@@ -2,15 +2,30 @@ package nonkube
 
 import (
 	"fmt"
+	"net"
+	"strconv"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 type CmdListenerCreate struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandListenerCreateFlags
-	Namespace string
-	siteName  string
+	listenerHandler *fs.ListenerHandler
+	CobraCmd        *cobra.Command
+	Flags           *common.CommandListenerCreateFlags
+	namespace       string
+	listenerName    string
+	port            int
+	host            string
+	tlsCredentials  string
+	listenerType    string
+	routingKey      string
+	output          string
 }
 
 func NewCmdListenerCreate() *CmdListenerCreate {
@@ -18,12 +33,149 @@ func NewCmdListenerCreate() *CmdListenerCreate {
 }
 
 func (cmd *CmdListenerCreate) NewClient(cobraCommand *cobra.Command, args []string) {
-	//TODO
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() != "" {
+		cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String()
+	}
+
+	cmd.listenerHandler = fs.NewListenerHandler(cmd.namespace)
+}
+
+func (cmd *CmdListenerCreate) ValidateInput(args []string) []error {
+	var validationErrors []error
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameContext) != nil && cmd.CobraCmd.Flag(common.FlagNameContext).Value.String() != "" {
+		fmt.Println("Warning: --context flag is not supported on this platform")
+	}
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig) != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig).Value.String() != "" {
+		fmt.Println("Warning: --kubeconfig flag is not supported on this platform")
+	}
+
+	resourceStringValidator := validator.NewResourceStringValidator()
+	numberValidator := validator.NewNumberValidator()
+	listenerTypeValidator := validator.NewOptionValidator(common.ListenerTypes)
+	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
+	hostStringValidator := validator.NewHostStringValidator()
+
+	// 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.listenerName = 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 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))
+		}
+	}
+
+	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))
+		}
+	}
+
+	if cmd.Flags.Host != "" {
+		ip := net.ParseIP(cmd.Flags.Host)
+		ok, _ := hostStringValidator.Evaluate(cmd.Flags.Host)
+		if !ok && ip == nil {
+			validationErrors = append(validationErrors, fmt.Errorf("host is not valid: a valid IP address or hostname is expected"))
+		}
+	}
+
+	if cmd.Flags.TlsCredentials != "" {
+		ok, err := resourceStringValidator.Evaluate(cmd.Flags.TlsCredentials)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("tlsCredentials value is not valid: %s", err))
+		}
+	}
+
+	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))
+		}
+	}
+	return validationErrors
+}
+
+func (cmd *CmdListenerCreate) InputToOptions() {
+	// default routingkey to name of listener
+	if cmd.Flags.RoutingKey == "" {
+		cmd.routingKey = cmd.listenerName
+	} else {
+		cmd.routingKey = cmd.Flags.RoutingKey
+	}
+
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
+
+	if cmd.Flags.Host == "" {
+		cmd.host = "0.0.0.0"
+	} else {
+		cmd.host = cmd.Flags.Host
+	}
+
+	cmd.tlsCredentials = cmd.Flags.TlsCredentials
+	cmd.listenerType = cmd.Flags.ListenerType
+	cmd.output = cmd.Flags.Output
 }
 
-func (cmd *CmdListenerCreate) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdListenerCreate) InputToOptions()                     {}
 func (cmd *CmdListenerCreate) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	listenerResource := v2alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.listenerName,
+			Namespace: cmd.namespace,
+		},
+		Spec: v2alpha1.ListenerSpec{
+			Host:           cmd.host,
+			Port:           cmd.port,
+			RoutingKey:     cmd.routingKey,
+			TlsCredentials: cmd.tlsCredentials,
+			Type:           cmd.listenerType,
+		},
+	}
+
+	if cmd.output != "" {
+		encodedOutput, err := utils.Encode(cmd.output, listenerResource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		err := cmd.listenerHandler.Add(listenerResource)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
+
 func (cmd *CmdListenerCreate) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/listener/nonkube/listener_delete.go b/internal/cmd/skupper/listener/nonkube/listener_delete.go
index f7631da3..52838264 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_delete.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_delete.go
@@ -2,15 +2,19 @@ package nonkube
 
 import (
 	"fmt"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 )
 
 type CmdListenerDelete struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandListenerDeleteFlags
-	Namespace string
-	siteName  string
+	listenerHandler *fs.ListenerHandler
+	CobraCmd        *cobra.Command
+	Flags           *common.CommandListenerDeleteFlags
+	namespace       string
+	listenerName    string
 }
 
 func NewCmdListenerDelete() *CmdListenerDelete {
@@ -18,12 +22,67 @@ func NewCmdListenerDelete() *CmdListenerDelete {
 }
 
 func (cmd *CmdListenerDelete) NewClient(cobraCommand *cobra.Command, args []string) {
-	//TODO
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() != "" {
+		cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String()
+	}
+
+	cmd.listenerHandler = fs.NewListenerHandler(cmd.namespace)
+
+}
+
+func (cmd *CmdListenerDelete) ValidateInput(args []string) []error {
+	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: false, LogWarning: false}
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameContext) != nil && cmd.CobraCmd.Flag(common.FlagNameContext).Value.String() != "" {
+		fmt.Println("Warning: --context flag is not supported on this platform")
+	}
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig) != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig).Value.String() != "" {
+		fmt.Println("Warning: --kubeconfig flag is not supported on this platform")
+	}
+
+	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.listenerName = args[0]
+		}
+	}
+
+	if cmd.listenerName != "" {
+		// Validate that there is already a listener with this name
+		listener, err := cmd.listenerHandler.Get(cmd.listenerName, opts)
+		if listener == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("listener %s does not exist", cmd.listenerName))
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdListenerDelete) InputToOptions() {
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
 }
 
-func (cmd *CmdListenerDelete) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdListenerDelete) InputToOptions()                     {}
 func (cmd *CmdListenerDelete) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	err := cmd.listenerHandler.Delete(cmd.listenerName)
+	if err != nil {
+		return err
+	}
+	return nil
 }
+
 func (cmd *CmdListenerDelete) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/listener/nonkube/listener_status.go b/internal/cmd/skupper/listener/nonkube/listener_status.go
index df2b3ec8..4a9eb6cf 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_status.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_status.go
@@ -2,15 +2,23 @@ package nonkube
 
 import (
 	"fmt"
+	"os"
+	"text/tabwriter"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 )
 
 type CmdListenerStatus struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandListenerStatusFlags
-	Namespace string
-	siteName  string
+	listenerHandler *fs.ListenerHandler
+	CobraCmd        *cobra.Command
+	Flags           *common.CommandListenerStatusFlags
+	namespace       string
+	listenerName    string
+	output          string
 }
 
 func NewCmdListenerStatus() *CmdListenerStatus {
@@ -18,12 +26,109 @@ func NewCmdListenerStatus() *CmdListenerStatus {
 }
 
 func (cmd *CmdListenerStatus) NewClient(cobraCommand *cobra.Command, args []string) {
-	//TODO
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() != "" {
+		cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String()
+	}
+
+	cmd.listenerHandler = fs.NewListenerHandler(cmd.namespace)
+}
+
+func (cmd *CmdListenerStatus) ValidateInput(args []string) []error {
+	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: true, LogWarning: false}
+	resourceStringValidator := validator.NewResourceStringValidator()
+	outputTypeValidator := validator.NewOptionValidator(common.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.listenerName = args[0]
+			}
+		}
+	}
+	// Validate that there is a listener with this name in the namespace
+	if cmd.listenerName != "" {
+		listener, err := cmd.listenerHandler.Get(cmd.listenerName, opts)
+		if listener == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("listener %s does not exist in namespace %s", cmd.listenerName, 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) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdListenerStatus) InputToOptions()                     {}
 func (cmd *CmdListenerStatus) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	opts := fs.GetOptions{RuntimeFirst: true, LogWarning: true}
+	if cmd.listenerName == "" {
+		listeners, err := cmd.listenerHandler.List()
+		if listeners == nil || err != nil {
+			fmt.Println("no listeners found:")
+			return err
+		}
+		if cmd.output != "" {
+			for _, listener := range listeners {
+				encodedOutput, err := utils.Encode(cmd.output, listener)
+				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",
+				"NAME", "STATUS", "ROUTING-KEY", "HOST", "PORT"))
+			for _, listener := range listeners {
+				status := "Not Ready"
+				if listener.IsConfigured() {
+					status = "Ok"
+				}
+				fmt.Fprintln(tw, fmt.Sprintf("%s\t%s\t%s\t%s\t%d",
+					listener.Name, status, listener.Spec.RoutingKey, listener.Spec.Host, listener.Spec.Port))
+			}
+			_ = tw.Flush()
+		}
+	} else {
+		listener, err := cmd.listenerHandler.Get(cmd.listenerName, opts)
+		if listener == nil || err != nil {
+			fmt.Println("No listeners found:", err)
+			return err
+		}
+		if cmd.output != "" {
+			encodedOutput, err := utils.Encode(cmd.output, listener)
+			if err != nil {
+				return err
+			}
+			fmt.Println(encodedOutput)
+		} else {
+			status := "Not Ready"
+			if listener.IsConfigured() {
+				status = "Ok"
+			}
+			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\nTlsCredentials:\t%s\n",
+				listener.Name, status, listener.Spec.RoutingKey, listener.Spec.Host, listener.Spec.Port, listener.Spec.TlsCredentials))
+			_ = tw.Flush()
+		}
+	}
+	return nil
 }
+
+func (cmd *CmdListenerStatus) InputToOptions()  {}
 func (cmd *CmdListenerStatus) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/listener/nonkube/listener_update.go b/internal/cmd/skupper/listener/nonkube/listener_update.go
index 21cc9986..dc75696b 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_update.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_update.go
@@ -2,15 +2,32 @@ package nonkube
 
 import (
 	"fmt"
+	"net"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+type ListenerUpdates struct {
+	routingKey     string
+	host           string
+	tlsCredentials string
+	listenerType   string
+	port           int
+	output         string
+}
 type CmdListenerUpdate struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandListenerUpdateFlags
-	Namespace string
-	siteName  string
+	listenerHandler *fs.ListenerHandler
+	CobraCmd        *cobra.Command
+	Flags           *common.CommandListenerUpdateFlags
+	namespace       string
+	listenerName    string
+	newSettings     ListenerUpdates
 }
 
 func NewCmdListenerUpdate() *CmdListenerUpdate {
@@ -18,12 +35,154 @@ func NewCmdListenerUpdate() *CmdListenerUpdate {
 }
 
 func (cmd *CmdListenerUpdate) NewClient(cobraCommand *cobra.Command, args []string) {
-	//TODO
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() != "" {
+		cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String()
+	}
+
+	cmd.listenerHandler = fs.NewListenerHandler(cmd.namespace)
+}
+
+func (cmd *CmdListenerUpdate) ValidateInput(args []string) []error {
+	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: false, LogWarning: false}
+	resourceStringValidator := validator.NewResourceStringValidator()
+	numberValidator := validator.NewNumberValidator()
+	listenerTypeValidator := validator.NewOptionValidator(common.ListenerTypes)
+	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
+	hostStringValidator := validator.NewHostStringValidator()
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameContext) != nil && cmd.CobraCmd.Flag(common.FlagNameContext).Value.String() != "" {
+		fmt.Println("Warning: --context flag is not supported on this platform")
+	}
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig) != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig).Value.String() != "" {
+		fmt.Println("Warning: --kubeconfig flag is not supported on this platform")
+	}
+
+	// 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.listenerName = args[0]
+		}
+	}
+
+	// Validate that there is already a listener with this name in the namespace
+	if cmd.listenerName != "" {
+		listener, err := cmd.listenerHandler.Get(cmd.listenerName, opts)
+		if listener == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("listener %s must exist in namespace %s to be updated", cmd.listenerName, cmd.namespace))
+		} else {
+			// save existing values
+			cmd.newSettings.host = listener.Spec.Host
+			cmd.newSettings.port = listener.Spec.Port
+			cmd.newSettings.tlsCredentials = listener.Spec.TlsCredentials
+			cmd.newSettings.listenerType = listener.Spec.Type
+			cmd.newSettings.routingKey = listener.Spec.RoutingKey
+		}
+	}
+
+	// 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
+		}
+	}
+	if cmd.Flags.Host != "" {
+		ip := net.ParseIP(cmd.Flags.Host)
+		ok, _ := hostStringValidator.Evaluate(cmd.Flags.Host)
+		if !ok && ip == nil {
+			validationErrors = append(validationErrors, fmt.Errorf("host is not valid: a valid IP address or hostname is expected"))
+		} else {
+			cmd.newSettings.host = cmd.Flags.Host
+		}
+	}
+	if cmd.Flags.TlsCredentials != "" {
+		ok, err := resourceStringValidator.Evaluate(cmd.Flags.TlsCredentials)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("tlsCredentials value is not valid: %s", err))
+		} else {
+			cmd.newSettings.tlsCredentials = cmd.Flags.TlsCredentials
+		}
+	}
+	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
+		}
+	}
+	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) InputToOptions() {
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
+	// user wants to clear TlsCredentials
+	if cmd.CobraCmd.Flags().Changed(common.FlagNameTlsCredentials) && cmd.Flags.TlsCredentials == "" {
+		cmd.newSettings.tlsCredentials = ""
+	}
 }
 
-func (cmd *CmdListenerUpdate) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdListenerUpdate) InputToOptions()                     {}
 func (cmd *CmdListenerUpdate) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	listenerResource := v2alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.listenerName,
+			Namespace: cmd.namespace,
+		},
+		Spec: v2alpha1.ListenerSpec{
+			Host:           cmd.newSettings.host,
+			Port:           cmd.newSettings.port,
+			RoutingKey:     cmd.newSettings.routingKey,
+			TlsCredentials: cmd.newSettings.tlsCredentials,
+			Type:           cmd.newSettings.listenerType,
+		},
+	}
+	if cmd.newSettings.output != "" {
+		encodedOutput, err := utils.Encode(cmd.newSettings.output, listenerResource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		err := cmd.listenerHandler.Add(listenerResource)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
+
 func (cmd *CmdListenerUpdate) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/site/kube/site_create.go b/internal/cmd/skupper/site/kube/site_create.go
index 6e8f4571..4f995d6d 100644
--- a/internal/cmd/skupper/site/kube/site_create.go
+++ b/internal/cmd/skupper/site/kube/site_create.go
@@ -65,10 +65,6 @@ func (cmd *CmdSiteCreate) ValidateInput(args []string) []error {
 		validationErrors = append(validationErrors, fmt.Errorf("there is already a site created for this namespace"))
 	}
 
-	if cmd.Flags != nil && cmd.Flags.BindHost != "" {
-		fmt.Println("Warning: --bind-host flag is not supported on this platform")
-	}
-
 	if cmd.Flags != nil && cmd.Flags.SubjectAlternativeNames != nil && len(cmd.Flags.SubjectAlternativeNames) > 0 {
 		fmt.Println("Warning: --subject-alternative-names flag is not supported on this platform")
 	}
@@ -225,9 +221,9 @@ func (cmd *CmdSiteCreate) WaitUntil() error {
 	})
 
 	if err != nil && siteCondition == nil {
-		return fmt.Errorf("Site %q is not %s yet, check the status for more information\n", cmd.siteName, cmd.status)
+		return fmt.Errorf("Site %q is not yet %s, check the status for more information\n", cmd.siteName, cmd.status)
 	} else if err != nil && siteCondition.Status == metav1.ConditionFalse {
-		return fmt.Errorf("Site %q is %s with errors, check the status for more information\n", cmd.siteName, cmd.status)
+		return fmt.Errorf("Site %q is not yet %s: %s\n", cmd.siteName, cmd.status, siteCondition.Message)
 	}
 
 	fmt.Printf("Site %q is %s.\n", cmd.siteName, cmd.status)
diff --git a/internal/cmd/skupper/site/kube/site_delete.go b/internal/cmd/skupper/site/kube/site_delete.go
index 1779161d..e966541a 100644
--- a/internal/cmd/skupper/site/kube/site_delete.go
+++ b/internal/cmd/skupper/site/kube/site_delete.go
@@ -3,10 +3,11 @@ package kube
 import (
 	"context"
 	"fmt"
+	"time"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"github.com/skupperproject/skupper/pkg/utils/validator"
-	"time"
 
 	"github.com/skupperproject/skupper/internal/kube/client"
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
@@ -22,6 +23,7 @@ type CmdSiteDelete struct {
 	siteName  string
 	timeout   time.Duration
 	wait      bool
+	all       bool
 }
 
 func NewCmdSiteDelete() *CmdSiteDelete {
@@ -87,11 +89,112 @@ func (cmd *CmdSiteDelete) ValidateInput(args []string) []error {
 func (cmd *CmdSiteDelete) InputToOptions() {
 	cmd.timeout = cmd.Flags.Timeout
 	cmd.wait = cmd.Flags.Wait
+	cmd.all = cmd.Flags.All
 }
 
 func (cmd *CmdSiteDelete) Run() error {
+
 	err := cmd.Client.Sites(cmd.Namespace).Delete(context.TODO(), cmd.siteName, metav1.DeleteOptions{})
-	return err
+	if err != nil {
+		return err
+	}
+
+	// if delete all, also remove all the other resources
+	if cmd.all {
+		connectors, err := cmd.Client.Connectors(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && connectors != nil && len(connectors.Items) != 0 {
+			for _, connector := range connectors.Items {
+				err = cmd.Client.Connectors(cmd.Namespace).Delete(context.TODO(), connector.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		listeners, err := cmd.Client.Listeners(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && listeners != nil && len(listeners.Items) != 0 {
+			for _, listener := range listeners.Items {
+				err = cmd.Client.Listeners(cmd.Namespace).Delete(context.TODO(), listener.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		//Links are removed after the removal of the site by the controller
+
+		accessTokens, err := cmd.Client.AccessTokens(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && accessTokens != nil && len(accessTokens.Items) != 0 {
+			for _, accessToken := range accessTokens.Items {
+				err = cmd.Client.AccessTokens(cmd.Namespace).Delete(context.TODO(), accessToken.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		accessGrants, err := cmd.Client.AccessGrants(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && accessGrants != nil && len(accessGrants.Items) != 0 {
+			for _, accessGrant := range accessGrants.Items {
+				err = cmd.Client.AccessGrants(cmd.Namespace).Delete(context.TODO(), accessGrant.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		routerAccesses, err := cmd.Client.RouterAccesses(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && routerAccesses != nil && len(routerAccesses.Items) != 0 {
+			for _, routerAccess := range routerAccesses.Items {
+				err = cmd.Client.RouterAccesses(cmd.Namespace).Delete(context.TODO(), routerAccess.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		securedAccesses, err := cmd.Client.SecuredAccesses(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && securedAccesses != nil && len(securedAccesses.Items) != 0 {
+			for _, securedAccess := range securedAccesses.Items {
+				err = cmd.Client.SecuredAccesses(cmd.Namespace).Delete(context.TODO(), securedAccess.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		certificates, err := cmd.Client.Certificates(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && certificates != nil && len(certificates.Items) != 0 {
+			for _, certificate := range certificates.Items {
+				err = cmd.Client.Certificates(cmd.Namespace).Delete(context.TODO(), certificate.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		attachedConnectors, err := cmd.Client.AttachedConnectors(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && attachedConnectors != nil && len(attachedConnectors.Items) != 0 {
+			for _, attachedConnector := range attachedConnectors.Items {
+				err = cmd.Client.AttachedConnectors(cmd.Namespace).Delete(context.TODO(), attachedConnector.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		attachedConnectorBindings, err := cmd.Client.AttachedConnectorBindings(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && attachedConnectorBindings != nil && len(attachedConnectorBindings.Items) != 0 {
+			for _, attachedConnectorBinding := range attachedConnectorBindings.Items {
+				err = cmd.Client.AttachedConnectorBindings(cmd.Namespace).Delete(context.TODO(), attachedConnectorBinding.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
 }
 func (cmd *CmdSiteDelete) WaitUntil() error {
 
diff --git a/internal/cmd/skupper/site/kube/site_status.go b/internal/cmd/skupper/site/kube/site_status.go
index c4717cca..acc7946b 100644
--- a/internal/cmd/skupper/site/kube/site_status.go
+++ b/internal/cmd/skupper/site/kube/site_status.go
@@ -3,10 +3,12 @@ package kube
 import (
 	"context"
 	"fmt"
-	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"os"
 	"text/tabwriter"
 
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+
 	"github.com/skupperproject/skupper/internal/kube/client"
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
 	"github.com/spf13/cobra"
@@ -16,6 +18,7 @@ import (
 type CmdSiteStatus struct {
 	Client    skupperv2alpha1.SkupperV2alpha1Interface
 	CobraCmd  *cobra.Command
+	Flags     *common.CommandSiteStatusFlags
 	Namespace string
 }
 
diff --git a/internal/cmd/skupper/site/kube/site_update.go b/internal/cmd/skupper/site/kube/site_update.go
index c5a22a2e..ab0a4bd3 100644
--- a/internal/cmd/skupper/site/kube/site_update.go
+++ b/internal/cmd/skupper/site/kube/site_update.go
@@ -244,9 +244,9 @@ func (cmd *CmdSiteUpdate) WaitUntil() error {
 	})
 
 	if err != nil && siteCondition == nil {
-		return fmt.Errorf("Site %q is not %s yet, check the status for more information\n", cmd.siteName, cmd.status)
+		return fmt.Errorf("Site %q is not yet %s, check the status for more information\n", cmd.siteName, cmd.status)
 	} else if err != nil && siteCondition.Status == metav1.ConditionFalse {
-		return fmt.Errorf("Site %q is %s with errors, check the status for more information\n", cmd.siteName, cmd.status)
+		return fmt.Errorf("Site %q is not yet %s: %s\n", cmd.siteName, cmd.status, siteCondition.Message)
 	}
 
 	fmt.Printf("Site %q is updated\n", cmd.siteName)
diff --git a/internal/cmd/skupper/site/nonkube/site_create.go b/internal/cmd/skupper/site/nonkube/site_create.go
index 18b879fb..701e8454 100644
--- a/internal/cmd/skupper/site/nonkube/site_create.go
+++ b/internal/cmd/skupper/site/nonkube/site_create.go
@@ -5,6 +5,8 @@ package nonkube
 
 import (
 	"fmt"
+	"net"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
@@ -14,10 +16,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-const (
-	SiteConfigNameKey string = "name"
-)
-
 type CmdSiteCreate struct {
 	siteHandler             *fs.SiteHandler
 	routerAccessHandler     *fs.RouterAccessHandler
@@ -25,7 +23,7 @@ type CmdSiteCreate struct {
 	Flags                   *common.CommandSiteCreateFlags
 	options                 map[string]string
 	siteName                string
-	linkAccessType          string
+	linkAccessEnabled       bool
 	output                  string
 	namespace               string
 	bindHost                string
@@ -48,8 +46,10 @@ func (cmd *CmdSiteCreate) NewClient(cobraCommand *cobra.Command, args []string)
 }
 
 func (cmd *CmdSiteCreate) ValidateInput(args []string) []error {
-
 	var validationErrors []error
+	hostStringValidator := validator.NewHostStringValidator()
+	resourceStringValidator := validator.NewResourceStringValidator()
+	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
 
 	if cmd.Flags.ServiceAccount != "" {
 		fmt.Println("Warning: --service-account flag is not supported on this platform")
@@ -63,10 +63,6 @@ func (cmd *CmdSiteCreate) ValidateInput(args []string) []error {
 		fmt.Println("Warning: --kubeconfig flag is not supported on this platform")
 	}
 
-	resourceStringValidator := validator.NewResourceStringValidator()
-	linkAccessTypeValidator := validator.NewOptionValidator(common.LinkAccessTypes)
-	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
-
 	if len(args) == 0 || args[0] == "" {
 		validationErrors = append(validationErrors, fmt.Errorf("site name must not be empty"))
 	} else if len(args) > 1 {
@@ -77,29 +73,30 @@ func (cmd *CmdSiteCreate) ValidateInput(args []string) []error {
 			validationErrors = append(validationErrors, fmt.Errorf("site name is not valid: %s", err))
 		}
 		cmd.siteName = args[0]
-
-	}
-
-	if cmd.Flags.LinkAccessType != "" {
-		ok, err := linkAccessTypeValidator.Evaluate(cmd.Flags.LinkAccessType)
-		if !ok {
-			validationErrors = append(validationErrors, fmt.Errorf("link access type is not valid: %s", err))
-		}
-	}
-
-	if !cmd.Flags.EnableLinkAccess && len(cmd.Flags.LinkAccessType) > 0 {
-		validationErrors = append(validationErrors, fmt.Errorf("for the site to work with this type of linkAccess, the --enable-link-access option must be set to true"))
 	}
 
-	if cmd.Flags.Output != "" {
+	if cmd.Flags != nil && cmd.Flags.Output != "" {
 		ok, err := outputTypeValidator.Evaluate(cmd.Flags.Output)
 		if !ok {
 			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
 		}
 	}
 
-	if cmd.Flags.BindHost == "" {
-		validationErrors = append(validationErrors, fmt.Errorf("bind host should not be empty"))
+	if cmd.Flags != nil && cmd.Flags.BindHost != "" {
+		ip := net.ParseIP(cmd.Flags.BindHost)
+		ok, _ := hostStringValidator.Evaluate(cmd.Flags.BindHost)
+		if !ok && ip == nil {
+			validationErrors = append(validationErrors, fmt.Errorf("bindhost is not valid: a valid IP address or hostname is expected"))
+		}
+	}
+	if cmd.Flags != nil && len(cmd.Flags.SubjectAlternativeNames) != 0 {
+		for _, name := range cmd.Flags.SubjectAlternativeNames {
+			ip := net.ParseIP(name)
+			ok, _ := hostStringValidator.Evaluate(name)
+			if !ok && ip == nil {
+				validationErrors = append(validationErrors, fmt.Errorf("SubjectAlternativeNames is not valid: a valid IP address or hostname is expected"))
+			}
+		}
 	}
 
 	return validationErrors
@@ -108,17 +105,13 @@ func (cmd *CmdSiteCreate) ValidateInput(args []string) []error {
 func (cmd *CmdSiteCreate) InputToOptions() {
 
 	if cmd.Flags.EnableLinkAccess {
-		if cmd.Flags.LinkAccessType == "" {
-			cmd.linkAccessType = "default"
-		} else {
-			cmd.linkAccessType = cmd.Flags.LinkAccessType
-		}
-	} else {
-		cmd.linkAccessType = "none"
+		cmd.linkAccessEnabled = true
+		cmd.bindHost = cmd.Flags.BindHost
+		cmd.routerAccessName = "router-access-" + cmd.siteName
+		cmd.subjectAlternativeNames = cmd.Flags.SubjectAlternativeNames
 	}
-
 	options := make(map[string]string)
-	options[SiteConfigNameKey] = cmd.siteName
+	options[common.SiteConfigNameKey] = cmd.siteName
 
 	cmd.options = options
 	cmd.output = cmd.Flags.Output
@@ -126,11 +119,6 @@ func (cmd *CmdSiteCreate) InputToOptions() {
 	if cmd.namespace == "" {
 		cmd.namespace = "default"
 	}
-
-	cmd.bindHost = cmd.Flags.BindHost
-	cmd.routerAccessName = "router-access-" + cmd.siteName
-	cmd.subjectAlternativeNames = cmd.Flags.SubjectAlternativeNames
-
 }
 
 func (cmd *CmdSiteCreate) Run() error {
@@ -145,8 +133,7 @@ func (cmd *CmdSiteCreate) Run() error {
 			Namespace: cmd.namespace,
 		},
 		Spec: v2alpha1.SiteSpec{
-			Settings:   cmd.options,
-			LinkAccess: cmd.linkAccessType,
+			Settings: cmd.options,
 		},
 	}
 
@@ -177,22 +164,29 @@ func (cmd *CmdSiteCreate) Run() error {
 
 	if cmd.output != "" {
 		encodedSiteOutput, err := utils.Encode(cmd.output, siteResource)
+		if err != nil {
+			return err
+		}
 		fmt.Println(encodedSiteOutput)
-		fmt.Println("---")
-		encodedRouterAccessOutput, err := utils.Encode(cmd.output, routerAccessResource)
-		fmt.Println(encodedRouterAccessOutput)
-
-		return err
-
+		if cmd.linkAccessEnabled == true {
+			fmt.Println("---")
+			encodedRouterAccessOutput, err := utils.Encode(cmd.output, routerAccessResource)
+			if err != nil {
+				return err
+			}
+			fmt.Println(encodedRouterAccessOutput)
+		}
 	} else {
 		err := cmd.siteHandler.Add(siteResource)
 		if err != nil {
 			return err
 		}
 
-		err = cmd.routerAccessHandler.Add(routerAccessResource)
-		if err != nil {
-			return err
+		if cmd.linkAccessEnabled == true {
+			err = cmd.routerAccessHandler.Add(routerAccessResource)
+			if err != nil {
+				return err
+			}
 		}
 
 	}
diff --git a/internal/cmd/skupper/site/nonkube/site_delete.go b/internal/cmd/skupper/site/nonkube/site_delete.go
index b174f9d4..7b32f8cb 100644
--- a/internal/cmd/skupper/site/nonkube/site_delete.go
+++ b/internal/cmd/skupper/site/nonkube/site_delete.go
@@ -2,16 +2,20 @@ package nonkube
 
 import (
 	"fmt"
-	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 )
 
 type CmdSiteDelete struct {
-	CobraCmd  *cobra.Command
-	Namespace string
-	siteName  string
-	Flags     *common.CommandSiteDeleteFlags
+	siteHandler         *fs.SiteHandler
+	routerAccessHandler *fs.RouterAccessHandler
+	CobraCmd            *cobra.Command
+	namespace           string
+	siteName            string
+	Flags               *common.CommandSiteDeleteFlags
 }
 
 func NewCmdSiteDelete() *CmdSiteDelete {
@@ -19,12 +23,69 @@ func NewCmdSiteDelete() *CmdSiteDelete {
 }
 
 func (cmd *CmdSiteDelete) NewClient(cobraCommand *cobra.Command, args []string) {
-	//TODO
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() != "" {
+		cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String()
+	}
+
+	cmd.siteHandler = fs.NewSiteHandler(cmd.namespace)
+	cmd.routerAccessHandler = fs.NewRouterAccessHandler(cmd.namespace)
+}
+
+func (cmd *CmdSiteDelete) ValidateInput(args []string) []error {
+	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: false, LogWarning: false}
+	resourceStringValidator := validator.NewResourceStringValidator()
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameContext) != nil && cmd.CobraCmd.Flag(common.FlagNameContext).Value.String() != "" {
+		fmt.Println("Warning: --context flag is not supported on this platform")
+	}
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig) != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig).Value.String() != "" {
+		fmt.Println("Warning: --kubeconfig flag is not supported on this platform")
+	}
+
+	// Validate arguments name
+	if len(args) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("site 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("site name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("site name is not valid: %s", err))
+		} else {
+			cmd.siteName = args[0]
+		}
+	}
+
+	if cmd.siteName != "" {
+		// Validate that there is already a site with this name
+		site, err := cmd.siteHandler.Get(cmd.siteName, opts)
+		if site == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("site %s does not exist", cmd.siteName))
+		}
+	}
+	return validationErrors
+}
+
+func (cmd *CmdSiteDelete) InputToOptions() {
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
 }
 
-func (cmd *CmdSiteDelete) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdSiteDelete) InputToOptions()                     {}
 func (cmd *CmdSiteDelete) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	err := cmd.siteHandler.Delete(cmd.siteName)
+	if err != nil {
+		return err
+	}
+	err = cmd.routerAccessHandler.Delete("router-access-" + cmd.siteName)
+	if err != nil {
+		return err
+	}
+	return nil
 }
+
 func (cmd *CmdSiteDelete) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/site/nonkube/site_status.go b/internal/cmd/skupper/site/nonkube/site_status.go
index 1204c203..d7ec0b0a 100644
--- a/internal/cmd/skupper/site/nonkube/site_status.go
+++ b/internal/cmd/skupper/site/nonkube/site_status.go
@@ -2,12 +2,23 @@ package nonkube
 
 import (
 	"fmt"
+	"os"
+	"text/tabwriter"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 )
 
 type CmdSiteStatus struct {
-	CobraCmd  *cobra.Command
-	Namespace string
+	siteHandler *fs.SiteHandler
+	CobraCmd    *cobra.Command
+	Flags       *common.CommandSiteStatusFlags
+	namespace   string
+	siteName    string
+	output      string
 }
 
 func NewCmdSiteStatus() *CmdSiteStatus {
@@ -15,12 +26,89 @@ func NewCmdSiteStatus() *CmdSiteStatus {
 }
 
 func (cmd *CmdSiteStatus) NewClient(cobraCommand *cobra.Command, args []string) {
-	//TODO
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() != "" {
+		cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String()
+	}
+
+	cmd.siteHandler = fs.NewSiteHandler(cmd.namespace)
+}
+
+func (cmd *CmdSiteStatus) ValidateInput(args []string) []error {
+	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: true, LogWarning: false}
+	resourceStringValidator := validator.NewResourceStringValidator()
+	outputTypeValidator := validator.NewOptionValidator(common.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("site name must not be empty"))
+		} else {
+			ok, err := resourceStringValidator.Evaluate(args[0])
+			if !ok {
+				validationErrors = append(validationErrors, fmt.Errorf("site name is not valid: %s", err))
+			} else {
+				cmd.siteName = args[0]
+			}
+		}
+	}
+	// Validate that there is a site with this name in the namespace
+	if cmd.siteName != "" {
+		site, err := cmd.siteHandler.Get(cmd.siteName, opts)
+		if site == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("site %s does not exist", cmd.siteName))
+		}
+	}
+
+	if cmd.Flags != nil && 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 *CmdSiteStatus) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdSiteStatus) InputToOptions()                     {}
 func (cmd *CmdSiteStatus) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	sites, err := cmd.siteHandler.List()
+	if sites == nil || err != nil {
+		fmt.Println("no site found:")
+		return err
+	}
+
+	if cmd.output != "" {
+		for _, site := range sites {
+			encodedOutput, err := utils.Encode(cmd.output, site)
+			if err != nil {
+				return err
+			}
+			fmt.Println(encodedOutput)
+		}
+	} else {
+		writer := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', tabwriter.AlignRight)
+		fmt.Fprintln(writer, "NAME\tSTATUS\tMESSAGE")
+
+		for _, site := range sites {
+			status := "Not Ready"
+			if site.IsReady() {
+				status = "Ready"
+			} else if site.IsConfigured() {
+				status = "configured"
+			}
+
+			fmt.Fprintf(writer, "%s\t%s\t%s", site.Name, status, site.Status.Message)
+			fmt.Fprintln(writer)
+		}
+		writer.Flush()
+	}
+
+	return nil
 }
+
+func (cmd *CmdSiteStatus) InputToOptions()  {}
 func (cmd *CmdSiteStatus) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/site/nonkube/site_update.go b/internal/cmd/skupper/site/nonkube/site_update.go
index 8853ab38..08800783 100644
--- a/internal/cmd/skupper/site/nonkube/site_update.go
+++ b/internal/cmd/skupper/site/nonkube/site_update.go
@@ -2,33 +2,249 @@ package nonkube
 
 import (
 	"fmt"
+	"net"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+type SiteUpdates struct {
+	subjectAlternativeNames []string
+	bindHost                string
+	options                 map[string]string
+}
+
 type CmdSiteUpdate struct {
-	CobraCmd           *cobra.Command
-	Flags              *common.CommandSiteUpdateFlags
-	options            map[string]string
-	siteName           string
-	serviceAccountName string
-	Namespace          string
-	linkAccessType     string
-	output             string
+	siteHandler             *fs.SiteHandler
+	routerAccessHandler     *fs.RouterAccessHandler
+	CobraCmd                *cobra.Command
+	Flags                   *common.CommandSiteUpdateFlags
+	options                 map[string]string
+	siteName                string
+	namespace               string
+	linkAccessEnabled       bool
+	bindHost                string
+	output                  string
+	routerAccessName        string
+	subjectAlternativeNames []string
+	newSettings             SiteUpdates
 }
 
 func NewCmdSiteUpdate() *CmdSiteUpdate {
-
 	return &CmdSiteUpdate{}
 }
 
 func (cmd *CmdSiteUpdate) NewClient(cobraCommand *cobra.Command, args []string) {
-	//TODO
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace) != nil && cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String() != "" {
+		cmd.namespace = cmd.CobraCmd.Flag(common.FlagNameNamespace).Value.String()
+	}
+
+	cmd.siteHandler = fs.NewSiteHandler(cmd.namespace)
+	cmd.routerAccessHandler = fs.NewRouterAccessHandler(cmd.namespace)
+}
+
+func (cmd *CmdSiteUpdate) ValidateInput(args []string) []error {
+	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: false, LogWarning: false}
+	resourceStringValidator := validator.NewResourceStringValidator()
+	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
+	hostStringValidator := validator.NewHostStringValidator()
+
+	if cmd.Flags.ServiceAccount != "" {
+		fmt.Println("Warning: --service-account flag is not supported on this platform")
+	}
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameContext) != nil && cmd.CobraCmd.Flag(common.FlagNameContext).Value.String() != "" {
+		fmt.Println("Warning: --context flag is not supported on this platform")
+	}
+
+	if cmd.CobraCmd != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig) != nil && cmd.CobraCmd.Flag(common.FlagNameKubeconfig).Value.String() != "" {
+		fmt.Println("Warning: --kubeconfig flag is not supported on this platform")
+	}
+
+	// Validate arguments name
+	if len(args) < 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("site 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("site name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("site name is not valid: %s", err))
+		} else {
+			cmd.siteName = args[0]
+		}
+	}
+
+	// Validate that there is already a site with this name in the namespace
+	if cmd.siteName != "" {
+		site, err := cmd.siteHandler.Get(cmd.siteName, opts)
+		if site == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("site %s must exist to be updated", cmd.siteName))
+		} else {
+			// save existing values
+
+		}
+
+		routerAccessName := "router-access-" + cmd.siteName
+		routerAccess, err := cmd.routerAccessHandler.Update(routerAccessName)
+		if err == nil && routerAccess != nil {
+			// save existing values
+			cmd.bindHost = routerAccess.Spec.BindHost
+			cmd.subjectAlternativeNames = routerAccess.Spec.SubjectAlternativeNames
+			cmd.linkAccessEnabled = true
+		}
+	}
+
+	// Validate flags
+	if cmd.Flags != nil && cmd.Flags.BindHost != "" {
+		ip := net.ParseIP(cmd.Flags.BindHost)
+		ok, _ := hostStringValidator.Evaluate(cmd.Flags.BindHost)
+		if !ok && ip == nil {
+			validationErrors = append(validationErrors, fmt.Errorf("bindhost is not valid: a valid IP address or hostname is expected"))
+		} else {
+			cmd.newSettings.bindHost = cmd.Flags.BindHost
+		}
+	}
+	if cmd.Flags != nil && len(cmd.Flags.SubjectAlternativeNames) != 0 {
+		for _, name := range cmd.Flags.SubjectAlternativeNames {
+			ip := net.ParseIP(name)
+			ok, _ := hostStringValidator.Evaluate(name)
+			if !ok && ip == nil {
+				validationErrors = append(validationErrors, fmt.Errorf("SubjectAlternativeNames are not valid: a valid IP address or hostname is expected"))
+			} else {
+				cmd.newSettings.subjectAlternativeNames = append(cmd.newSettings.subjectAlternativeNames, name)
+			}
+		}
+	}
+	if cmd.Flags != nil && cmd.Flags.Output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.Flags.Output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdSiteUpdate) InputToOptions() {
+	// if EnableLinkAccess flag was explicity set use value otherwise use value from
+	// previous create command
+	if cmd.CobraCmd.Flags().Changed(common.FlagNameEnableLinkAccess) {
+		if cmd.Flags.EnableLinkAccess == true {
+			cmd.linkAccessEnabled = true
+		} else {
+			cmd.linkAccessEnabled = false
+		}
+	}
+
+	if cmd.linkAccessEnabled == true {
+		if cmd.newSettings.bindHost != "" {
+			cmd.bindHost = cmd.newSettings.bindHost
+		}
+		if len(cmd.newSettings.subjectAlternativeNames) != 0 {
+			cmd.subjectAlternativeNames = cmd.newSettings.subjectAlternativeNames
+		}
+	}
+	options := make(map[string]string)
+	options[common.SiteConfigNameKey] = cmd.siteName
+
+	cmd.options = options
+	cmd.output = cmd.Flags.Output
+	cmd.routerAccessName = "router-access-" + cmd.siteName
+
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
 }
 
-func (cmd *CmdSiteUpdate) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdSiteUpdate) InputToOptions()                     {}
 func (cmd *CmdSiteUpdate) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+
+	siteResource := v2alpha1.Site{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Site",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.siteName,
+			Namespace: cmd.namespace,
+		},
+		Spec: v2alpha1.SiteSpec{
+			Settings: cmd.options,
+		},
+	}
+
+	routerAccessResource := v2alpha1.RouterAccess{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "RouterAccess",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.routerAccessName,
+			Namespace: cmd.namespace,
+		},
+		Spec: v2alpha1.RouterAccessSpec{
+			Roles: []v2alpha1.RouterAccessRole{
+				{
+					Name: "inter-router",
+					Port: 55671,
+				},
+				{
+					Name: "edge",
+					Port: 45671,
+				},
+			},
+			BindHost:                cmd.bindHost,
+			SubjectAlternativeNames: cmd.subjectAlternativeNames,
+		},
+	}
+
+	if cmd.output != "" {
+		encodedSiteOutput, err := utils.Encode(cmd.output, siteResource)
+		if err != nil {
+			return err
+		}
+		fmt.Println(encodedSiteOutput)
+		if cmd.linkAccessEnabled == true {
+			fmt.Println("---")
+			encodedRouterAccessOutput, err := utils.Encode(cmd.output, routerAccessResource)
+			if err != nil {
+				return err
+			}
+			fmt.Println(encodedRouterAccessOutput)
+		}
+		return err
+	} else {
+		err := cmd.siteHandler.Add(siteResource)
+		if err != nil {
+			return err
+		}
+
+		if cmd.linkAccessEnabled == true {
+			err = cmd.routerAccessHandler.Add(routerAccessResource)
+			if err != nil {
+				return err
+			}
+		} else {
+			fmt.Println("link access not enabled, router access removed", cmd.routerAccessName)
+			err = cmd.routerAccessHandler.Delete(cmd.routerAccessName)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (cmd *CmdSiteUpdate) WaitUntil() error {
+	//TODO check status of the site
+	return nil
 }
-func (cmd *CmdSiteUpdate) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/site/site.go b/internal/cmd/skupper/site/site.go
index 8b5c2549..67984af8 100644
--- a/internal/cmd/skupper/site/site.go
+++ b/internal/cmd/skupper/site/site.go
@@ -54,7 +54,7 @@ There can be only one site definition per namespace.`,
 	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
 	cmd.Flags().StringVar(&cmdFlags.ServiceAccount, common.FlagNameServiceAccount, "", common.FlagDescServiceAccount)
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 30*time.Second, common.FlagDescTimeout)
-	cmd.Flags().StringVar(&cmdFlags.BindHost, common.FlagNameBindHost, "", common.FlagDescBindHost)
+	cmd.Flags().StringVar(&cmdFlags.BindHost, common.FlagNameBindHost, "0.0.0.0", common.FlagDescBindHost)
 	cmd.Flags().StringSliceVar(&cmdFlags.SubjectAlternativeNames, common.FlagNameSubjectAlternativeNames, []string{}, common.FlagDescSubjectAlternativeNames)
 	cmd.Flags().StringVar(&cmdFlags.Wait, common.FlagNameWait, "ready", common.FlagDescWait)
 
@@ -126,8 +126,13 @@ func CmdSiteStatusFactory(configuredPlatform types.Platform) *cobra.Command {
 
 	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSiteStatusDesc, kubeCommand, nonKubeCommand)
 
+	cmdFlags := common.CommandSiteStatusFlags{}
+	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescConnectorStatusOutput)
+
 	kubeCommand.CobraCmd = cmd
+	kubeCommand.Flags = &cmdFlags
 	nonKubeCommand.CobraCmd = cmd
+	nonKubeCommand.Flags = &cmdFlags
 
 	return cmd
 
@@ -148,6 +153,7 @@ skupper site delete --wait=false`,
 	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSiteDeleteDesc, kubeCommand, nonKubeCommand)
 	cmdFlags := common.CommandSiteDeleteFlags{}
 
+	cmd.Flags().BoolVar(&cmdFlags.All, common.FlagNameAll, false, common.FlagDescDeleteAll)
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().BoolVar(&cmdFlags.Wait, common.FlagNameWait, true, common.FlagDescDeleteWait)
 
diff --git a/internal/cmd/skupper/token/kube/token_issue.go b/internal/cmd/skupper/token/kube/token_issue.go
index d6a0873e..9283cfa1 100644
--- a/internal/cmd/skupper/token/kube/token_issue.go
+++ b/internal/cmd/skupper/token/kube/token_issue.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
@@ -25,6 +26,7 @@ type CmdTokenIssue struct {
 	namespace string
 	grantName string
 	fileName  string
+	cost      int
 }
 
 func NewCmdTokenIssue() *CmdTokenIssue {
@@ -47,6 +49,7 @@ func (cmd *CmdTokenIssue) ValidateInput(args []string) []error {
 	tokenStringValidator := validator.NewFilePathStringValidator()
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
 	expirationValidator := validator.NewExpirationInSecondsValidator()
+	numberValidator := validator.NewNumberValidator()
 
 	// Validate token file name
 	if len(args) < 1 {
@@ -114,6 +117,17 @@ func (cmd *CmdTokenIssue) ValidateInput(args []string) []error {
 		}
 	}
 
+	selectedCost, err := strconv.Atoi(cmd.Flags.Cost)
+	if err != nil {
+		validationErrors = append(validationErrors, fmt.Errorf("link cost is not valid: %s", err))
+	}
+	ok, err := numberValidator.Evaluate(selectedCost)
+	if !ok {
+		validationErrors = append(validationErrors, fmt.Errorf("link cost is not valid: %s", err))
+	} else {
+		cmd.cost = selectedCost
+	}
+
 	return validationErrors
 }
 
@@ -157,9 +171,10 @@ func (cmd *CmdTokenIssue) WaitUntil() error {
 					Name: accessGrant.Name,
 				},
 				Spec: v2alpha1.AccessTokenSpec{
-					Url:  accessGrant.Status.Url,
-					Code: accessGrant.Status.Code,
-					Ca:   accessGrant.Status.Ca,
+					Url:      accessGrant.Status.Url,
+					Code:     accessGrant.Status.Code,
+					Ca:       accessGrant.Status.Ca,
+					LinkCost: cmd.cost,
 				},
 			}
 
diff --git a/internal/cmd/skupper/token/token.go b/internal/cmd/skupper/token/token.go
index 274c3dc3..35956b2c 100644
--- a/internal/cmd/skupper/token/token.go
+++ b/internal/cmd/skupper/token/token.go
@@ -49,6 +49,7 @@ func CmdTokenIssueFactory(configuredPlatform types.Platform) *cobra.Command {
 	cmd.Flags().DurationVarP(&cmdFlags.ExpirationWindow, common.FlagNameExpirationWindow, "e", 15*time.Minute, common.FlagDescExpirationWindow)
 	cmd.Flags().DurationVarP(&cmdFlags.Timeout, common.FlagNameTimeout, "t", 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().StringVar(&cmdFlags.Name, common.FlagNameToken, "", common.FlagDescToken)
+	cmd.Flags().StringVar(&cmdFlags.Cost, common.FlagNameCost, "1", common.FlagDescCost)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
diff --git a/internal/cmd/skupper/version/kube/version.go b/internal/cmd/skupper/version/kube/version.go
index f120f728..4af3f6f0 100644
--- a/internal/cmd/skupper/version/kube/version.go
+++ b/internal/cmd/skupper/version/kube/version.go
@@ -1,7 +1,9 @@
 package kube
 
 import (
+	"context"
 	"fmt"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"os"
 	"text/tabwriter"
 
@@ -38,6 +40,7 @@ func (cmd *CmdVersion) NewClient(cobraCommand *cobra.Command, args []string) {
 
 	if err == nil {
 		cmd.namespace = cli.Namespace
+		cmd.KubeClient = cli.GetKubeClient()
 	}
 }
 
@@ -58,10 +61,27 @@ func (cmd *CmdVersion) ValidateInput(args []string) []error {
 }
 
 func (cmd *CmdVersion) InputToOptions() {
+
+	mapRunningPods := make(map[string]string)
+
+	if cmd.KubeClient != nil {
+		// search for running pods in the current namespace
+		runningPodList, err := cmd.KubeClient.CoreV1().Pods(cmd.namespace).List(context.Background(), metav1.ListOptions{LabelSelector: "app.kubernetes.io/part-of in (skupper, skupper-network-observer)"})
+		if err != nil {
+			return
+		}
+
+		for _, runningPod := range runningPodList.Items {
+			for _, container := range runningPod.Status.ContainerStatuses {
+				mapRunningPods[container.Name] = container.Image
+			}
+		}
+	}
+
 	if cmd.output != "" {
-		cmd.manifest = configs.ManifestManager{Components: images.KubeComponents, EnableSHA: true}
+		cmd.manifest = configs.ManifestManager{Components: images.KubeComponents, EnableSHA: true, RunningPods: mapRunningPods}
 	} else {
-		cmd.manifest = configs.ManifestManager{Components: images.KubeComponents, EnableSHA: false}
+		cmd.manifest = configs.ManifestManager{Components: images.KubeComponents, EnableSHA: false, RunningPods: mapRunningPods}
 	}
 
 }
diff --git a/internal/nonkube/client/fs/connector_handler.go b/internal/nonkube/client/fs/connector_handler.go
index a1e0c50e..7ecd7014 100644
--- a/internal/nonkube/client/fs/connector_handler.go
+++ b/internal/nonkube/client/fs/connector_handler.go
@@ -2,8 +2,8 @@ package fs
 
 import (
 	"errors"
-	"fmt"
 	"io/fs"
+	"os"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
@@ -47,7 +47,9 @@ func (s *ConnectorHandler) Get(name string, opt GetOptions) (*v2alpha1.Connector
 		// has run.  If no runtime connectors try and display configured connectors
 		err, file := s.ReadFile(s.pathProvider.GetRuntimeNamespace(), fileName, common.Connectors)
 		if err != nil {
-			fmt.Println("Site not initialized yet")
+			if opt.LogWarning {
+				os.Stderr.WriteString("Site not initialized yet\n")
+			}
 			err, file = s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Connectors)
 			if err != nil {
 				return nil, err
@@ -90,7 +92,7 @@ func (s *ConnectorHandler) List() ([]*v2alpha1.Connector, error) {
 	path := s.pathProvider.GetRuntimeNamespace()
 	err, files := s.ReadDir(path, common.Connectors)
 	if err != nil {
-		fmt.Println("Site not initialized yet")
+		os.Stderr.WriteString("Site not initialized yet\n")
 		path = s.pathProvider.GetNamespace()
 		err, files = s.ReadDir(path, common.Connectors)
 		if err != nil {
diff --git a/internal/nonkube/client/fs/custom_resource_handler.go b/internal/nonkube/client/fs/custom_resource_handler.go
index 9575d7fa..a156a59b 100644
--- a/internal/nonkube/client/fs/custom_resource_handler.go
+++ b/internal/nonkube/client/fs/custom_resource_handler.go
@@ -13,6 +13,7 @@ import (
 
 type GetOptions struct {
 	RuntimeFirst bool
+	LogWarning   bool
 }
 
 type CustomResourceHandler[T any] interface {
diff --git a/internal/nonkube/client/fs/listener_handler.go b/internal/nonkube/client/fs/listener_handler.go
new file mode 100644
index 00000000..7aa83649
--- /dev/null
+++ b/internal/nonkube/client/fs/listener_handler.go
@@ -0,0 +1,116 @@
+package fs
+
+import (
+	"errors"
+	"io/fs"
+	"os"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+)
+
+type ListenerHandler struct {
+	BaseCustomResourceHandler
+	pathProvider PathProvider
+}
+
+func NewListenerHandler(namespace string) *ListenerHandler {
+	return &ListenerHandler{
+		pathProvider: PathProvider{
+			Namespace: namespace,
+		},
+	}
+}
+
+func (s *ListenerHandler) Add(resource v2alpha1.Listener) error {
+
+	fileName := resource.Name + ".yaml"
+	content, err := s.EncodeToYaml(resource)
+	if err != nil {
+		return err
+	}
+
+	err = s.WriteFile(s.pathProvider.GetNamespace(), fileName, content, "listeners")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *ListenerHandler) Get(name string, opts GetOptions) (*v2alpha1.Listener, error) {
+	var context v2alpha1.Listener
+	fileName := name + ".yaml"
+
+	if opts.RuntimeFirst == true {
+		// First read from runtime directory, where output is found after bootstrap
+		// has run.  If no runtime listeners try and display configured listeners
+		err, file := s.ReadFile(s.pathProvider.GetRuntimeNamespace(), fileName, common.Listeners)
+		if err != nil {
+			if opts.LogWarning {
+				os.Stderr.WriteString("Site not initialized yet\n")
+			}
+			err, file = s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Listeners)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if err = s.DecodeYaml(file, &context); err != nil {
+			return nil, err
+		}
+	} else {
+		// read from input directory to get lastest config
+		err, file := s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Listeners)
+		if err != nil {
+			return nil, err
+		}
+		if err := s.DecodeYaml(file, &context); err != nil {
+			return nil, err
+		}
+	}
+
+	return &context, nil
+}
+
+func (s *ListenerHandler) Delete(name string) error {
+	fileName := name + ".yaml"
+
+	if err := s.DeleteFile(s.pathProvider.GetNamespace(), fileName, "listeners"); err != nil {
+		if !errors.Is(err, fs.ErrNotExist) {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *ListenerHandler) List() ([]*v2alpha1.Listener, error) {
+	var listeners []*v2alpha1.Listener
+
+	// First read from runtime directory, where output is found after bootstrap
+	// has run.  If no runtime listeners try and display configured listeners
+	path := s.pathProvider.GetRuntimeNamespace()
+	err, files := s.ReadDir(path, common.Listeners)
+	if err != nil {
+		os.Stderr.WriteString("Site not initialized yet\n")
+		path = s.pathProvider.GetNamespace()
+		err, files = s.ReadDir(path, common.Listeners)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for _, file := range files {
+		err, listener := s.ReadFile(path, file.Name(), common.Listeners)
+		if err != nil {
+			return nil, err
+		}
+		var context v2alpha1.Listener
+		if err = s.DecodeYaml(listener, &context); err != nil {
+			return nil, err
+		}
+		listeners = append(listeners, &context)
+	}
+	return listeners, nil
+}
diff --git a/internal/nonkube/client/fs/router_access_handler.go b/internal/nonkube/client/fs/router_access_handler.go
index 7876cfbb..59260d8b 100644
--- a/internal/nonkube/client/fs/router_access_handler.go
+++ b/internal/nonkube/client/fs/router_access_handler.go
@@ -1,6 +1,10 @@
 package fs
 
 import (
+	"errors"
+	"fmt"
+	"io/fs"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 )
@@ -33,8 +37,55 @@ func (s *RouterAccessHandler) Add(resource v2alpha1.RouterAccess) error {
 
 	return nil
 }
-func (s *RouterAccessHandler) Get(name string, opts GetOptions) (*v2alpha1.RouterAccess, error) {
-	return nil, nil
+
+func (s *RouterAccessHandler) Get(name string) (*v2alpha1.RouterAccess, error) {
+	var context v2alpha1.RouterAccess
+	fileName := name + ".yaml"
+
+	// First read from runtime directory, where output is found after bootstrap
+	// has run.  If no runtime sites try and display configured sites
+	err, file := s.ReadFile(s.pathProvider.GetRuntimeNamespace(), fileName, common.RouterAccesses)
+	if err != nil {
+		fmt.Println("Site not initialized yet")
+		err, file = s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.RouterAccesses)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if err = s.DecodeYaml(file, &context); err != nil {
+		return nil, err
+	}
+
+	return &context, nil
+}
+
+func (s *RouterAccessHandler) Delete(name string) error {
+	fileName := name + ".yaml"
+
+	if err := s.DeleteFile(s.pathProvider.GetNamespace(), fileName, common.RouterAccesses); err != nil {
+		fmt.Println(err)
+		if !errors.Is(err, fs.ErrNotExist) {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *RouterAccessHandler) Update(name string) (*v2alpha1.RouterAccess, error) {
+	var context v2alpha1.RouterAccess
+	fileName := name + ".yaml"
+
+	// read from input directory to get lastest config
+	err, file := s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.RouterAccesses)
+	if err != nil {
+		return nil, err
+	}
+
+	if err = s.DecodeYaml(file, &context); err != nil {
+		return nil, err
+	}
+
+	return &context, nil
 }
-func (s *RouterAccessHandler) Delete(name string) error                { return nil }
-func (s *RouterAccessHandler) List() ([]*v2alpha1.RouterAccess, error) { return nil, nil }
diff --git a/internal/nonkube/client/fs/site_handler.go b/internal/nonkube/client/fs/site_handler.go
index a7e3c207..37cff27d 100644
--- a/internal/nonkube/client/fs/site_handler.go
+++ b/internal/nonkube/client/fs/site_handler.go
@@ -1,6 +1,10 @@
 package fs
 
 import (
+	"errors"
+	"io/fs"
+	"os"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 )
@@ -34,6 +38,79 @@ func (s *SiteHandler) Add(resource v2alpha1.Site) error {
 	return nil
 }
 
-func (s *SiteHandler) Get(name string, opts GetOptions) (*v2alpha1.Site, error) { return nil, nil }
-func (s *SiteHandler) Delete(name string) error                                 { return nil }
-func (s *SiteHandler) List() ([]*v2alpha1.Site, error)                          { return nil, nil }
+func (s *SiteHandler) Get(name string, opts GetOptions) (*v2alpha1.Site, error) {
+	var context v2alpha1.Site
+	fileName := name + ".yaml"
+
+	if opts.RuntimeFirst == true {
+		// First read from runtime directory, where output is found after bootstrap
+		// has run.  If no runtime sites try and display configured sites
+		err, file := s.ReadFile(s.pathProvider.GetRuntimeNamespace(), fileName, common.Sites)
+		if err != nil {
+			if opts.LogWarning {
+				os.Stderr.WriteString("Site not initialized yet\n")
+			}
+			err, file = s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Sites)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if err = s.DecodeYaml(file, &context); err != nil {
+			return nil, err
+		}
+	} else {
+		// read from input directory to get lastest config
+		err, file := s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Sites)
+		if err != nil {
+			return nil, err
+		}
+		if err := s.DecodeYaml(file, &context); err != nil {
+			return nil, err
+		}
+	}
+
+	return &context, nil
+}
+
+func (s *SiteHandler) Delete(name string) error {
+	fileName := name + ".yaml"
+
+	if err := s.DeleteFile(s.pathProvider.GetNamespace(), fileName, common.Sites); err != nil {
+		if !errors.Is(err, fs.ErrNotExist) {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *SiteHandler) List() ([]*v2alpha1.Site, error) {
+	var sites []*v2alpha1.Site
+
+	// First read from runtime directory, where output is found after bootstrap
+	// has run.  If no runtime sites try and display configured sites
+	path := s.pathProvider.GetRuntimeNamespace()
+	err, files := s.ReadDir(path, common.Sites)
+	if err != nil {
+		os.Stderr.WriteString("Site not initialized yet\n")
+		path = s.pathProvider.GetNamespace()
+		err, files = s.ReadDir(path, common.Sites)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for _, file := range files {
+		err, site := s.ReadFile(path, file.Name(), common.Sites)
+		if err != nil {
+			return nil, err
+		}
+		var context v2alpha1.Site
+		if err = s.DecodeYaml(site, &context); err != nil {
+			return nil, err
+		}
+		sites = append(sites, &context)
+	}
+	return sites, nil
+}
diff --git a/pkg/images/image_utils.go b/pkg/images/image_utils.go
index 7fbcc147..de3ab776 100644
--- a/pkg/images/image_utils.go
+++ b/pkg/images/image_utils.go
@@ -209,9 +209,9 @@ func GetImage(imageNames map[string]string, imageRegistry string, enableSHA bool
 	var image SkupperImage
 	var skupperImage []SkupperImage
 
-	for key, name := range imageNames {
-		imageName := os.Getenv(key)
-		if imageName == "" {
+	for _, name := range imageNames {
+		imageName := name
+		if imageRegistry != "" {
 			imageName = strings.Join([]string{imageRegistry, name}, "/")
 		}
 		image.Name = imageName
@@ -233,15 +233,43 @@ func GetImages(component string, enableSHA bool) []SkupperImage {
 	switch component {
 	case "router":
 		// skupper router has two components
-		names[RouterImageEnvKey] = RouterImageName
-		names[AdaptorImageEnvKey] = AdaptorImageName
-		registry = GetImageRegistry()
+		envImage := os.Getenv(RouterImageEnvKey)
+
+		if envImage != "" {
+			names[RouterImageEnvKey] = envImage
+		} else {
+			names[RouterImageEnvKey] = RouterImageName
+			registry = GetImageRegistry()
+		}
+
+		envImage = os.Getenv(AdaptorImageEnvKey)
+		if envImage != "" {
+			names[AdaptorImageEnvKey] = envImage
+		} else {
+			names[AdaptorImageEnvKey] = AdaptorImageName
+			registry = GetImageRegistry()
+		}
 	case "controller":
-		names[ControllerImageEnvKey] = ControllerImageName
-		registry = GetImageRegistry()
+		envImage := os.Getenv(ControllerImageEnvKey)
+
+		if envImage != "" {
+			names[ControllerImageEnvKey] = envImage
+		} else {
+			names[ControllerImageEnvKey] = ControllerImageName
+			registry = GetImageRegistry()
+		}
+
 	case "network-observer":
-		names[NetworkObserverImageEnvKey] = NetworkObserverImageName
-		registry = GetImageRegistry()
+
+		envImage := os.Getenv(NetworkObserverImageEnvKey)
+
+		if envImage != "" {
+			names[NetworkObserverImageEnvKey] = envImage
+		} else {
+			names[NetworkObserverImageEnvKey] = NetworkObserverImageName
+			registry = GetImageRegistry()
+		}
+
 	case "cli":
 		names[CliImageEnvKey] = CliImageName
 		registry = GetImageRegistry()
@@ -253,7 +281,7 @@ func GetImages(component string, enableSHA bool) []SkupperImage {
 		registry = GetOauthProxyImageRegistry()
 	}
 
-	if names != nil && registry != "" {
+	if names != nil {
 		return GetImage(names, registry, enableSHA)
 	} else {
 		return nil
@@ -265,18 +293,31 @@ func GetImageVersion(component string) string {
 
 	switch component {
 	case "router":
-		image = os.Getenv(RouterImageEnvKey)
-		if image == "" {
+		envImage := os.Getenv(RouterImageEnvKey)
+
+		if envImage != "" {
+			image = envImage
+		} else {
 			image = RouterImageName
 		}
+
 	case "controller":
-		image = os.Getenv(ControllerImageEnvKey)
-		if image == "" {
+
+		envImage := os.Getenv(ControllerImageEnvKey)
+
+		if envImage != "" {
+			image = envImage
+		} else {
 			image = ControllerImageName
 		}
+
 	case "network-observer":
-		image = os.Getenv(ControllerImageEnvKey)
-		if image == "" {
+
+		envImage := os.Getenv(NetworkObserverImageEnvKey)
+
+		if envImage != "" {
+			image = envImage
+		} else {
 			image = NetworkObserverImageName
 		}
 	case "cli":
@@ -296,10 +337,15 @@ func GetImageVersion(component string) string {
 		}
 	}
 	if image != "" {
-		parts := strings.Split(image, ":")
-		if len(parts) == 2 {
-			return parts[1]
-		}
+		return GetVersionFromTag(image)
+	}
+	return ""
+}
+
+func GetVersionFromTag(image string) string {
+	parts := strings.Split(image, ":")
+	if len(parts) == 2 {
+		return parts[1]
 	}
 
 	return ""
diff --git a/pkg/kube/site/extended_bindings.go b/pkg/kube/site/extended_bindings.go
index 7d77c463..f0704499 100644
--- a/pkg/kube/site/extended_bindings.go
+++ b/pkg/kube/site/extended_bindings.go
@@ -33,9 +33,6 @@ func NewExtendedBindings(controller *kube.Controller, profilePath string) *Exten
 			slog.String("component", "kube.site.attached_connector"),
 		),
 	}
-	eb.bindings.SetBindingEventHandler(eb)
-	eb.bindings.SetConnectorConfiguration(eb.updateBridgeConfigForConnector)
-	eb.bindings.SetListenerConfiguration(eb.updateBridgeConfigForListener)
 
 	return eb
 }
@@ -47,6 +44,9 @@ func (a *ExtendedBindings) init(context BindingContext, config *qdr.RouterConfig
 	}
 	a.exposed = ExposedPorts{}
 	a.selectors = map[string]TargetSelection{}
+	a.bindings.SetBindingEventHandler(a)
+	a.bindings.SetConnectorConfiguration(a.updateBridgeConfigForConnector)
+	a.bindings.SetListenerConfiguration(a.updateBridgeConfigForListener)
 }
 
 func (a *ExtendedBindings) cleanup() {
diff --git a/pkg/kube/site/site.go b/pkg/kube/site/site.go
index 6237422a..2852fab6 100644
--- a/pkg/kube/site/site.go
+++ b/pkg/kube/site/site.go
@@ -717,6 +717,9 @@ func (s *Site) updateListenerStatus(listener *skupperv2alpha1.Listener, err erro
 func (s *Site) CheckListener(name string, listener *skupperv2alpha1.Listener) error {
 	update, err1 := s.bindings.UpdateListener(name, listener)
 	if s.site == nil {
+		if listener == nil {
+			return nil
+		}
 		return s.updateListenerStatus(listener, stderrors.New("No active site in namespace"))
 	}
 	if update == nil {
diff --git a/pkg/utils/configs/manifest.go b/pkg/utils/configs/manifest.go
index dd788991..1af6ce55 100644
--- a/pkg/utils/configs/manifest.go
+++ b/pkg/utils/configs/manifest.go
@@ -22,8 +22,9 @@ type Manifest struct {
 }
 
 type ManifestManager struct {
-	EnableSHA  bool
-	Components []string
+	EnableSHA   bool
+	Components  []string
+	RunningPods map[string]string
 }
 
 type ManifestGenerator interface {
@@ -32,7 +33,7 @@ type ManifestGenerator interface {
 }
 
 func (manager *ManifestManager) GetConfiguredManifest() SkupperManifest {
-	return getSkupperConfiguredImages(manager.Components, manager.EnableSHA)
+	return getSkupperImages(manager.Components, manager.EnableSHA, manager.RunningPods)
 }
 
 func (manager *ManifestManager) GetDefaultManifestWithEnv() Manifest {
@@ -42,7 +43,7 @@ func (manager *ManifestManager) GetDefaultManifestWithEnv() Manifest {
 	}
 }
 
-func getSkupperConfiguredImages(components []string, enableSHA bool) SkupperManifest {
+func getSkupperImages(components []string, enableSHA bool, runningPods map[string]string) SkupperManifest {
 	var manifest SkupperManifest
 
 	for _, component := range components {
@@ -50,6 +51,11 @@ func getSkupperConfiguredImages(components []string, enableSHA bool) SkupperMani
 		image.Component = component
 		image.Version = images.GetImageVersion(component)
 		image.Images = images.GetImages(component, enableSHA)
+		if runningPods[component] != "" {
+			image.Version = images.GetVersionFromTag(runningPods[component])
+			image.Images = GetRunningImages(component, enableSHA, runningPods)
+		}
+
 		manifest.Components = append(manifest.Components, image)
 	}
 
@@ -126,3 +132,28 @@ func getEnvironmentVariableMap() *map[string]string {
 
 	return &envVariables
 }
+
+func GetRunningImages(component string, enableSHA bool, runningPods map[string]string) []images.SkupperImage {
+
+	names := make(map[string]string)
+
+	switch component {
+	case "router":
+		// skupper router has two components
+		names[images.RouterImageEnvKey] = runningPods["router"]
+		names[images.AdaptorImageEnvKey] = runningPods["kube-adaptor"]
+
+	case "controller":
+		names[images.ControllerImageEnvKey] = runningPods["controller"]
+
+	case "network-observer":
+
+		names[images.NetworkObserverImageEnvKey] = runningPods["network-observer"]
+	}
+
+	if names != nil {
+		return images.GetImage(names, "", enableSHA)
+	}
+
+	return nil
+}
diff --git a/pkg/vanflow/records.go b/pkg/vanflow/records.go
index 3fc56696..e0f33188 100644
--- a/pkg/vanflow/records.go
+++ b/pkg/vanflow/records.go
@@ -292,11 +292,13 @@ func (r TransportBiflowRecord) GetTypeMeta() TypeMeta {
 
 type AppBiflowRecord struct {
 	BaseRecord
-	Parent   *string `vflow:"2"`
-	Protocol *string `vflow:"16"`
-	Latency  *uint64 `vflow:"24"`
-	Method   *string `vflow:"27"`
-	Result   *string `vflow:"28"`
+	Parent        *string `vflow:"2"`
+	Protocol      *string `vflow:"16"`
+	Latency       *uint64 `vflow:"24"`
+	Method        *string `vflow:"27"`
+	Result        *string `vflow:"28"`
+	Octets        *uint64 `vflow:"23"`
+	OctetsReverse *uint64 `vflow:"58"`
 }
 
 func (r AppBiflowRecord) GetTypeMeta() TypeMeta {


Test patch:
diff --git a/cmd/network-observer/internal/flowlog/logging_test.go b/cmd/network-observer/internal/flowlog/logging_test.go
new file mode 100644
index 00000000..ef898295
--- /dev/null
+++ b/cmd/network-observer/internal/flowlog/logging_test.go
@@ -0,0 +1,240 @@
+package flowlog
+
+import (
+	"context"
+	"log/slog"
+	"testing"
+
+	"github.com/skupperproject/skupper/cmd/network-observer/internal/collector"
+	"github.com/skupperproject/skupper/pkg/vanflow"
+	"gotest.tools/v3/assert"
+)
+
+func TestSampling(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	t.Run("lowest priority allow wins", func(t *testing.T) {
+		var ct int
+		messageCounter := func(string, ...any) {
+			ct++
+		}
+		handler := New(ctx, messageCounter, []Rule{ // Lowest priority wins
+			{
+				Priority: 2,
+				Match:    NewRecordTypeSet(vanflow.SiteRecord{}),
+				Strategy: doNotSample,
+			},
+			{
+				Priority: 1,
+				Match:    NewRecordTypeSet(vanflow.SiteRecord{}),
+				Strategy: Unlimited(),
+			},
+			{
+				Priority: 5,
+				Match:    NewRecordTypeSet(vanflow.SiteRecord{}),
+				Strategy: doNotSample,
+			},
+		})
+
+		for x := 0; x < 1_000; x++ {
+			handler(vanflow.RecordMessage{Records: []vanflow.Record{
+				vanflow.SiteRecord{},
+				vanflow.SiteRecord{},
+				vanflow.ProcessRecord{},
+			}})
+		}
+		assert.Equal(t, ct, 2_000)
+	})
+
+	t.Run("rate limt zero with burst", func(t *testing.T) {
+		ct := 0
+		messageCounter := func(string, ...any) {
+			ct++
+		}
+		handler := New(ctx, messageCounter, []Rule{ // zero rate with bursts
+			{
+				Priority: 1,
+				Match:    NewRecordTypeSet(vanflow.SiteRecord{}),
+				Strategy: RateLimited(0.0, 16),
+			},
+		})
+
+		for x := 0; x < 1_000; x++ {
+			handler(vanflow.RecordMessage{Records: []vanflow.Record{
+				vanflow.SiteRecord{},
+				vanflow.SiteRecord{},
+				vanflow.ProcessRecord{},
+			}})
+		}
+		assert.Equal(t, ct, 16)
+	})
+
+	t.Run("hash based", func(t *testing.T) {
+		var (
+			MagicPercentage  = 0.1
+			MagicMatchingIDs = []string{
+				"test::7",
+				"test::3e2",
+				"test::317",
+				"test::3ad",
+			}
+			MagicSkippedIDs = []string{
+				"test::d",
+				"test::3e3",
+				"test::318",
+				"test::3af",
+			}
+		)
+
+		var (
+			ctTport int
+			ctApp   int
+		)
+		check := func(subject string, _ ...any) {
+			switch subject {
+			case vanflow.TransportBiflowRecord{}.GetTypeMeta().String():
+				ctTport++
+			case vanflow.AppBiflowRecord{}.GetTypeMeta().String():
+				ctApp++
+			default:
+				t.Errorf("unexpected type: %s", subject)
+			}
+		}
+		handler := New(ctx, check, []Rule{
+			{
+				Priority: 1,
+				Match:    NewRecordTypeSetAll(),
+				Strategy: TransportFlowHash(MagicPercentage, Unlimited()),
+			},
+		})
+
+		for _, tportID := range MagicMatchingIDs {
+			handler(vanflow.RecordMessage{Records: []vanflow.Record{
+				vanflow.SiteRecord{BaseRecord: vanflow.NewBase(tportID)},
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase(tportID)},
+				vanflow.TransportBiflowRecord{BaseRecord: vanflow.NewBase(tportID)},
+				vanflow.AppBiflowRecord{BaseRecord: vanflow.NewBase(tportID), Parent: &tportID},
+			}})
+		}
+		for _, tportID := range MagicSkippedIDs {
+			handler(vanflow.RecordMessage{Records: []vanflow.Record{
+				vanflow.SiteRecord{BaseRecord: vanflow.NewBase(tportID)},
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase(tportID)},
+				vanflow.TransportBiflowRecord{BaseRecord: vanflow.NewBase(tportID)},
+				vanflow.AppBiflowRecord{BaseRecord: vanflow.NewBase(tportID), Parent: &tportID},
+			}})
+		}
+		assert.Equal(t, ctTport, ctApp)
+		assert.Equal(t, ctTport, len(MagicMatchingIDs))
+
+		ctTport, ctApp = 0, 0
+		handler = New(ctx, check, []Rule{
+			{
+				Priority: 1,
+				Match:    NewRecordTypeSetAll(),
+				Strategy: TransportFlowHash(MagicPercentage, doNotSample),
+			},
+		})
+		handler(vanflow.RecordMessage{Records: []vanflow.Record{
+			vanflow.TransportBiflowRecord{BaseRecord: vanflow.NewBase(MagicMatchingIDs[0])},
+			vanflow.AppBiflowRecord{BaseRecord: vanflow.NewBase(MagicMatchingIDs[1])},
+		}})
+		assert.Equal(t, ctTport, ctApp)
+		assert.Equal(t, ctTport, 0)
+	})
+
+	t.Run("all types", func(t *testing.T) {
+		ct := 0
+		messageCounter := func(string, ...any) {
+			ct++
+		}
+		handler := New(ctx, messageCounter, []Rule{ // zero rate with bursts
+			{
+				Priority: 1,
+				Match:    NewRecordTypeSetAll(),
+				Strategy: Unlimited(),
+			},
+		})
+
+		for x := 0; x < 10; x++ {
+
+			handler(vanflow.RecordMessage{Records: []vanflow.Record{
+				vanflow.SiteRecord{},
+				vanflow.SiteRecord{},
+				vanflow.FlowRecord{},
+				vanflow.AppBiflowRecord{},
+				collector.AddressRecord{},
+				vanflow.ProcessRecord{},
+			}})
+		}
+		assert.Equal(t, ct, 60)
+	})
+	t.Run("log report empty", func(t *testing.T) {
+		type message struct {
+			Msg  string
+			Args []any
+		}
+		messages := []message{}
+
+		recordMessages := func(msg string, args ...any) {
+			messages = append(messages, message{Msg: msg, Args: args})
+		}
+		handler := &handler{
+			logFn: recordMessages,
+			rules: []Rule{
+				{
+					Priority: 1,
+					Match:    NewRecordTypeSet(vanflow.SiteRecord{}),
+					Strategy: Unlimited(),
+				},
+			},
+		}
+		handler.handle(vanflow.RecordMessage{
+			Records: []vanflow.Record{
+				vanflow.SiteRecord{},
+			},
+		})
+		handler.handle(vanflow.RecordMessage{})
+		assert.Equal(t, len(messages), 1)
+		messages = messages[:0]
+		handler.logReport()
+		assert.Equal(t, len(messages), 0)
+	})
+	t.Run("log report", func(t *testing.T) {
+		type message struct {
+			Msg  string
+			Args []any
+		}
+		messages := []message{}
+
+		recordMessages := func(msg string, args ...any) {
+			messages = append(messages, message{Msg: msg, Args: args})
+		}
+		handler := &handler{
+			logFn: recordMessages,
+			rules: []Rule{
+				{
+					Priority: 1,
+					Match:    NewRecordTypeSet(vanflow.SiteRecord{}),
+					Strategy: RateLimited(0.0, 16),
+				},
+			},
+		}
+		for x := 0; x < 1_000; x++ {
+			handler.handle(vanflow.RecordMessage{Records: []vanflow.Record{
+				vanflow.SiteRecord{},
+				vanflow.SiteRecord{},
+				vanflow.ProcessRecord{},
+			}})
+		}
+		assert.Equal(t, len(messages), 16)
+		messages = messages[:0]
+		handler.logReport()
+		assert.Equal(t, len(messages), 1)
+		assert.DeepEqual(t, messages[0].Args, []any{
+			slog.Int("flow/v1/SiteRecord", 2_000-16),
+		})
+	})
+
+}
diff --git a/internal/cmd/skupper/connector/nonkube/connector_create_test.go b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
index bb095a87..ed977eb0 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_create_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
@@ -78,6 +78,12 @@ func TestNonKubeCmdConnectorCreate_ValidateInput(t *testing.T) {
 			flags:          &common.CommandConnectorCreateFlags{RoutingKey: "not-valid$", Host: "1.2.3.4"},
 			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:           "TlsCredentials is not valid",
+			args:           []string{"my-connector-tls", "8080"},
+			flags:          &common.CommandConnectorCreateFlags{TlsCredentials: "not-valid$", Host: "1.2.3.4"},
+			expectedErrors: []string{"tlsCredentials value 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:           "host is not valid",
 			args:           []string{"my-connector-host", "8080"},
diff --git a/internal/cmd/skupper/connector/nonkube/connector_delete_test.go b/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
index d83ebc8b..45dffefe 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
@@ -1,14 +1,18 @@
 package nonkube
 
 import (
+	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/spf13/cobra"
-
 	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -23,6 +27,10 @@ func TestCmdConnectorDelete_ValidateInput(t *testing.T) {
 		expectedErrors    []string
 	}
 
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.InputSiteStatePath))
+
 	testTable := []test{
 		{
 			name:           "connector name is not specified",
@@ -48,6 +56,12 @@ func TestCmdConnectorDelete_ValidateInput(t *testing.T) {
 			flags:          &common.CommandConnectorDeleteFlags{},
 			expectedErrors: []string{"only one argument is allowed for this command"},
 		},
+		{
+			name:           "connector doesn't exist ",
+			args:           []string{"no-connector"},
+			flags:          &common.CommandConnectorDeleteFlags{},
+			expectedErrors: []string{"connector no-connector does not exist"},
+		},
 		{
 			name:           "kubernetes flags are not valid on this platform",
 			args:           []string{"my-connector"},
@@ -60,11 +74,32 @@ func TestCmdConnectorDelete_ValidateInput(t *testing.T) {
 		},
 	}
 
+	//Add a temp file so connector exists for update tests will pass
+	connectorResource := v2alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-connector",
+			Namespace: "test",
+		},
+	}
+
+	command := &CmdConnectorDelete{Flags: &common.CommandConnectorDeleteFlags{}}
+	command.CobraCmd = &cobra.Command{Use: "test"}
+	command.namespace = "test"
+	command.connectorHandler = fs.NewConnectorHandler(command.namespace)
+
+	defer command.connectorHandler.Delete("my-connector")
+	content, err := command.connectorHandler.EncodeToYaml(connectorResource)
+	assert.Check(t, err == nil)
+	err = command.connectorHandler.WriteFile(path, "my-connector.yaml", content, common.Connectors)
+	assert.Check(t, err == nil)
+
 	for _, test := range testTable {
 		t.Run(test.name, func(t *testing.T) {
-			command := &CmdConnectorDelete{Flags: &common.CommandConnectorDeleteFlags{}}
-			command.CobraCmd = &cobra.Command{Use: "test"}
-
+			command.connectorName = ""
 			if test.flags != nil {
 				command.Flags = test.flags
 			}
diff --git a/internal/cmd/skupper/connector/nonkube/connector_update_test.go b/internal/cmd/skupper/connector/nonkube/connector_update_test.go
index 93f5619e..6d38bc57 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_update_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_update_test.go
@@ -73,6 +73,12 @@ func TestCmdConnectorUpdate_ValidateInput(t *testing.T) {
 			flags:          &common.CommandConnectorUpdateFlags{RoutingKey: "not-valid$"},
 			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:           "tlsCredentials is not valid",
+			args:           []string{"my-connector"},
+			flags:          &common.CommandConnectorUpdateFlags{TlsCredentials: "not-valid$", Host: "1.2.3.4"},
+			expectedErrors: []string{"tlsCredentials value 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:           "host is not valid",
 			args:           []string{"my-connector"},
diff --git a/internal/cmd/skupper/listener/kube/listener_update_test.go b/internal/cmd/skupper/listener/kube/listener_update_test.go
index 2fe680be..998a31a3 100644
--- a/internal/cmd/skupper/listener/kube/listener_update_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_update_test.go
@@ -460,13 +460,13 @@ func TestCmdListenerUpdate_WaitUntil(t *testing.T) {
 				},
 			},
 			expectError:  true,
-			errorMessage: "Listener \"my-listener\" is not ready yet, check the status for more information\n",
+			errorMessage: "Listener \"my-listener\" is not yet ready, check the status for more information\n",
 		},
 		{
 			name:         "listener is not returned",
 			status:       "ready",
 			expectError:  true,
-			errorMessage: "Listener \"my-listener\" is not ready yet, check the status for more information\n",
+			errorMessage: "Listener \"my-listener\" is not yet ready, check the status for more information\n",
 		},
 		{
 			name:   "listener is ready",
@@ -588,7 +588,7 @@ func TestCmdListenerUpdate_WaitUntil(t *testing.T) {
 				},
 			},
 			expectError:  true,
-			errorMessage: "Listener \"my-listener\" is configured with errors, check the status for more information\n",
+			errorMessage: "Listener \"my-listener\" is not yet configured: Error\n",
 		},
 	}
 
diff --git a/internal/cmd/skupper/listener/nonkube/listener_create_test.go b/internal/cmd/skupper/listener/nonkube/listener_create_test.go
index 6072c5b0..3027cdf3 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_create_test.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_create_test.go
@@ -1,1 +1,290 @@
 package nonkube
+
+import (
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/spf13/cobra"
+
+	"gotest.tools/v3/assert"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestNonKubeCmdListenerCreate_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		flags             *common.CommandListenerCreateFlags
+		cobraGenericFlags map[string]string
+		expectedErrors    []string
+	}
+
+	testTable := []test{
+		{
+			name:           "listener name and port are not specified",
+			args:           []string{},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"listener name and port must be configured"},
+		},
+		{
+			name:           "listener name is not valid",
+			args:           []string{"my new Listener", "8080"},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			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 name is empty",
+			args:           []string{"", "1234"},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name:           "listener port empty",
+			args:           []string{"my-name-port-empty", ""},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"listener port must not be empty"},
+		},
+		{
+			name:           "port is not valid",
+			args:           []string{"my-listener-port", "abcd"},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"listener port is not valid: strconv.Atoi: parsing \"abcd\": invalid syntax"},
+		},
+		{
+			name:           "listener port not positive",
+			args:           []string{"my-port-positive", "-45"},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"listener port is not valid: value is not positive"},
+		},
+		{
+			name:           "more than two arguments was specified",
+			args:           []string{"my", "listener", "test"},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"only two arguments are allowed for this command"},
+		},
+		{
+			name:           "type is not valid",
+			args:           []string{"my-listener", "8080"},
+			flags:          &common.CommandListenerCreateFlags{ListenerType: "not-valid", Host: "1.2.3.4"},
+			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"},
+			flags:          &common.CommandListenerCreateFlags{RoutingKey: "not-valid$", Host: "1.2.3.4"},
+			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:           "TlsCredentials key is not valid",
+			args:           []string{"my-listener-tls", "8080"},
+			flags:          &common.CommandListenerCreateFlags{TlsCredentials: "not-valid$", Host: "1.2.3.4"},
+			expectedErrors: []string{"tlsCredentials value 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:           "host is not valid",
+			args:           []string{"my-listener-host", "8080"},
+			flags:          &common.CommandListenerCreateFlags{Host: "not-valid$"},
+			expectedErrors: []string{"host is not valid: a valid IP address or hostname is expected"},
+		},
+		{
+			name:           "output format is not valid",
+			args:           []string{"my-listener", "8080"},
+			flags:          &common.CommandListenerCreateFlags{Output: "not-valid", Host: "1.2.3.4"},
+			expectedErrors: []string{"output type is not valid: value not-valid not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name:           "kubernetes flags are not valid on this platform",
+			args:           []string{"my-listener", "8080"},
+			flags:          &common.CommandListenerCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-listener-flags", "8080"},
+			flags: &common.CommandListenerCreateFlags{
+				RoutingKey:     "routingkeyname",
+				TlsCredentials: "secretname",
+				ListenerType:   "tcp",
+				Output:         "json",
+				Host:           "1.2.3.4",
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command := &CmdListenerCreate{Flags: &common.CommandListenerCreateFlags{}}
+			command.CobraCmd = &cobra.Command{Use: "test"}
+
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+
+			if test.cobraGenericFlags != nil && len(test.cobraGenericFlags) > 0 {
+				for name, value := range test.cobraGenericFlags {
+					command.CobraCmd.Flags().String(name, value, "")
+				}
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestNonKubeCmdListenerCreate_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name                   string
+		args                   []string
+		namespace              string
+		flags                  common.CommandListenerCreateFlags
+		expectedNamespace      string
+		listenerName           string
+		expectedTlsCredentials string
+		expectedHost           string
+		expectedRoutingKey     string
+		expectedListenerType   string
+		expectedOutput         string
+	}
+
+	testTable := []test{
+		{
+			name:                   "test1",
+			flags:                  common.CommandListenerCreateFlags{"backend", "", "secret", "tcp", 0, "json", "none"},
+			expectedTlsCredentials: "secret",
+			expectedHost:           "0.0.0.0",
+			expectedRoutingKey:     "backend",
+			expectedListenerType:   "tcp",
+			expectedOutput:         "json",
+			expectedNamespace:      "default",
+		},
+		{
+			name:                   "test2",
+			namespace:              "test",
+			flags:                  common.CommandListenerCreateFlags{"backend", "1.2.3.4", "secret", "tcp", 0, "json", "configured"},
+			expectedTlsCredentials: "secret",
+			expectedHost:           "1.2.3.4",
+			expectedRoutingKey:     "backend",
+			expectedListenerType:   "tcp",
+			expectedOutput:         "json",
+			expectedNamespace:      "test",
+		},
+		{
+			name:                   "test3",
+			namespace:              "default",
+			flags:                  common.CommandListenerCreateFlags{"", "", "secret", "tcp", 0, "yaml", "ready"},
+			expectedTlsCredentials: "secret",
+			expectedHost:           "0.0.0.0",
+			expectedRoutingKey:     "my-listener",
+			expectedListenerType:   "tcp",
+			expectedOutput:         "yaml",
+			expectedNamespace:      "default",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			cmd := CmdListenerCreate{}
+			cmd.Flags = &test.flags
+			cmd.listenerName = "my-listener"
+			cmd.namespace = test.namespace
+			cmd.listenerHandler = fs.NewListenerHandler(cmd.namespace)
+
+			cmd.InputToOptions()
+
+			assert.Check(t, cmd.routingKey == test.expectedRoutingKey)
+			assert.Check(t, cmd.tlsCredentials == test.expectedTlsCredentials)
+			assert.Check(t, cmd.host == test.expectedHost)
+			assert.Check(t, cmd.listenerType == test.expectedListenerType)
+			assert.Check(t, cmd.output == test.expectedOutput)
+			assert.Check(t, cmd.namespace == test.expectedNamespace)
+		})
+	}
+}
+
+func TestNonKubeCmdListenerCreate_Run(t *testing.T) {
+	type test struct {
+		name           string
+		k8sObjects     []runtime.Object
+		skupperObjects []runtime.Object
+		skupperError   string
+		listenerName   string
+		host           string
+		output         string
+		errorMessage   string
+		routingKey     string
+		tlsCredentials string
+		listenerType   string
+		listenerPort   int
+	}
+
+	testTable := []test{
+		{
+			name:           "runs ok",
+			k8sObjects:     nil,
+			skupperObjects: nil,
+			listenerName:   "test1",
+			listenerPort:   8080,
+			listenerType:   "tcp",
+			routingKey:     "keyname",
+			tlsCredentials: "secretname",
+			host:           "1.2.3.4",
+		},
+		{
+			name:           "runs ok yaml",
+			k8sObjects:     nil,
+			skupperObjects: nil,
+			listenerName:   "test2",
+			listenerPort:   8080,
+			listenerType:   "tcp",
+			host:           "2.2.2.2",
+			output:         "yaml",
+		},
+		{
+			name:           "runs fails because the output format is not supported",
+			k8sObjects:     nil,
+			skupperObjects: nil,
+			listenerName:   "test3",
+			listenerPort:   8080,
+			host:           "3.3.3.3",
+			output:         "unsupported",
+			errorMessage:   "format unsupported not supported",
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdListenerCreate{}
+
+		command.listenerName = test.listenerName
+		command.output = test.output
+		command.port = test.listenerPort
+		command.host = test.host
+		command.listenerType = test.listenerType
+		command.namespace = "test"
+		command.listenerHandler = fs.NewListenerHandler(command.namespace)
+		defer command.listenerHandler.Delete("test1")
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error(), err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
diff --git a/internal/cmd/skupper/listener/nonkube/listener_delete_test.go b/internal/cmd/skupper/listener/nonkube/listener_delete_test.go
index 6072c5b0..6405fc0f 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_delete_test.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_delete_test.go
@@ -1,1 +1,168 @@
 package nonkube
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/spf13/cobra"
+	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdListenerDelete_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandListenerDeleteFlags
+		cobraGenericFlags map[string]string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		expectedErrors    []string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.InputSiteStatePath))
+
+	testTable := []test{
+		{
+			name:           "listener name is not specified",
+			args:           []string{},
+			flags:          &common.CommandListenerDeleteFlags{},
+			expectedErrors: []string{"listener name must be specified"},
+		},
+		{
+			name:           "listener name is nil",
+			args:           []string{""},
+			flags:          &common.CommandListenerDeleteFlags{},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name:           "listener name is not valid",
+			args:           []string{"my name"},
+			flags:          &common.CommandListenerDeleteFlags{},
+			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"},
+			flags:          &common.CommandListenerDeleteFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "listener doesn't exist",
+			args:           []string{"no-listener"},
+			flags:          &common.CommandListenerDeleteFlags{},
+			expectedErrors: []string{"listener no-listener does not exist"},
+		},
+		{
+			name:           "kubernetes flags are not valid on this platform",
+			args:           []string{"my-listener"},
+			flags:          &common.CommandListenerDeleteFlags{},
+			expectedErrors: []string{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+		},
+	}
+
+	// Add a temp file so listener exists for delete tests to pass
+	listenerResource := v2alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-listener",
+			Namespace: "test",
+		},
+	}
+
+	command := &CmdListenerDelete{Flags: &common.CommandListenerDeleteFlags{}}
+	command.CobraCmd = &cobra.Command{Use: "test"}
+	command.namespace = "test"
+	command.listenerHandler = fs.NewListenerHandler(command.namespace)
+
+	defer command.listenerHandler.Delete("my-listener")
+	content, err := command.listenerHandler.EncodeToYaml(listenerResource)
+	assert.Check(t, err == nil)
+	err = command.listenerHandler.WriteFile(path, "my-listener.yaml", content, common.Listeners)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command.listenerName = ""
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+
+			if test.cobraGenericFlags != nil && len(test.cobraGenericFlags) > 0 {
+				for name, value := range test.cobraGenericFlags {
+					command.CobraCmd.Flags().String(name, value, "")
+				}
+			}
+
+			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
+		namespace           string
+		deleteName          string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+	}
+
+	testTable := []test{
+		{
+			name:                "run fails default",
+			deleteName:          "my-listener",
+			skupperErrorMessage: "error",
+			errorMessage:        "error",
+		},
+		{
+			name:                "run fails",
+			namespace:           "test",
+			deleteName:          "my-listener",
+			skupperErrorMessage: "error",
+			errorMessage:        "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := &CmdListenerDelete{}
+
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd.listenerName = test.deleteName
+			cmd.namespace = test.namespace
+			cmd.listenerHandler = fs.NewListenerHandler(cmd.namespace)
+			cmd.InputToOptions()
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
diff --git a/internal/cmd/skupper/listener/nonkube/listener_status_test.go b/internal/cmd/skupper/listener/nonkube/listener_status_test.go
index 6072c5b0..a9d92ede 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_status_test.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_status_test.go
@@ -1,1 +1,298 @@
 package nonkube
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdListenerStatus_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandListenerStatusFlags
+		cobraGenericFlags map[string]string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		expectedErrors    []string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.RuntimeSiteStatePath))
+
+	testTable := []test{
+		{
+			name:           "listener is not shown because listener does not exist in the namespace",
+			args:           []string{"no-listener"},
+			flags:          &common.CommandListenerStatusFlags{},
+			expectedErrors: []string{"listener no-listener does not exist in namespace test"},
+		},
+		{
+			name:           "listener name is nil",
+			args:           []string{""},
+			flags:          &common.CommandListenerStatusFlags{},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name:           "more than one argument is specified",
+			args:           []string{"my", "listener"},
+			flags:          &common.CommandListenerStatusFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "listener name is not valid.",
+			args:           []string{"my new listener"},
+			flags:          &common.CommandListenerStatusFlags{},
+			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",
+			flags:          &common.CommandListenerStatusFlags{},
+			expectedErrors: []string{},
+		},
+		{
+			name:           "bad output status",
+			args:           []string{"my-listener"},
+			flags:          &common.CommandListenerStatusFlags{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{"my-listener"},
+			flags:          &common.CommandListenerStatusFlags{Output: "json"},
+			expectedErrors: []string{},
+		},
+	}
+
+	//Add a temp file so listener exists for status tests
+	listenerResource := v2alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-listener",
+			Namespace: "test",
+		},
+	}
+
+	command := &CmdListenerStatus{}
+	command.namespace = "test"
+	command.listenerHandler = fs.NewListenerHandler(command.namespace)
+
+	defer command.listenerHandler.Delete("my-listener")
+	content, err := command.listenerHandler.EncodeToYaml(listenerResource)
+	assert.Check(t, err == nil)
+	err = command.listenerHandler.WriteFile(path, "my-listener.yaml", content, common.Listeners)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command.listenerName = ""
+
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+
+			if test.cobraGenericFlags != nil && len(test.cobraGenericFlags) > 0 {
+				for name, value := range test.cobraGenericFlags {
+					command.CobraCmd.Flags().String(name, value, "")
+				}
+			}
+
+			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
+		listenerName        string
+		flags               common.CommandListenerStatusFlags
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.InputSiteStatePath))
+
+	testTable := []test{
+		{
+			name:         "run fails listener doesn't exist",
+			listenerName: "no-listener",
+			errorMessage: "failed to read file: open " + path + "/listeners/no-listener.yaml: no such file or directory",
+		},
+		{
+			name:         "runs ok, returns 1 listeners",
+			listenerName: "my-listener",
+		},
+		{
+			name:         "runs ok, returns 1 listeners yaml",
+			listenerName: "my-listener",
+			flags:        common.CommandListenerStatusFlags{Output: "yaml"},
+		},
+		{
+			name: "runs ok, returns all listeners",
+		},
+		{
+			name:  "runs ok, returns all listeners json",
+			flags: common.CommandListenerStatusFlags{Output: "json"},
+		},
+		{
+			name:         "runs ok, returns all listeners output bad",
+			flags:        common.CommandListenerStatusFlags{Output: "bad-value"},
+			errorMessage: "format bad-value not supported",
+		},
+		{
+			name:         "runs ok, returns 1 listeners bad output",
+			listenerName: "my-listener",
+			flags:        common.CommandListenerStatusFlags{Output: "bad-value"},
+			errorMessage: "format bad-value not supported",
+		},
+	}
+
+	//Add a temp file so listener exists for status tests
+	listenerResource1 := v2alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-listener",
+			Namespace: "test",
+		},
+		Spec: v2alpha1.ListenerSpec{
+			Host:       "1.2.3.4",
+			Port:       8080,
+			RoutingKey: "backend-8080",
+		},
+		Status: v2alpha1.ListenerStatus{
+			Status: v2alpha1.Status{
+				Conditions: []metav1.Condition{
+					{
+						Type:   "Configured",
+						Status: "True",
+					},
+				},
+			},
+		},
+	}
+	listenerResource2 := v2alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-listener2",
+			Namespace: "test",
+		},
+		Spec: v2alpha1.ListenerSpec{
+			Host:       "1.1.1.1",
+			Port:       9999,
+			RoutingKey: "test-9999",
+		},
+		Status: v2alpha1.ListenerStatus{
+			Status: v2alpha1.Status{
+				Conditions: []metav1.Condition{
+					{
+						Type:   "Configured",
+						Status: "True",
+					},
+				},
+			},
+		},
+	}
+
+	// add two listeners in runtime directory
+	command := &CmdListenerStatus{}
+	command.namespace = "test"
+	command.listenerHandler = fs.NewListenerHandler(command.namespace)
+
+	defer command.listenerHandler.Delete("my-listener")
+	defer command.listenerHandler.Delete("my-listener2")
+
+	path = filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.RuntimeSiteStatePath))
+	content, err := command.listenerHandler.EncodeToYaml(listenerResource1)
+	assert.Check(t, err == nil)
+	err = command.listenerHandler.WriteFile(path, "my-listener.yaml", content, common.Listeners)
+	assert.Check(t, err == nil)
+
+	content, err = command.listenerHandler.EncodeToYaml(listenerResource2)
+	assert.Check(t, err == nil)
+	err = command.listenerHandler.WriteFile(path, "my-listener2.yaml", content, common.Listeners)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		command.listenerName = test.listenerName
+		command.Flags = &test.flags
+		command.output = command.Flags.Output
+
+		t.Run(test.name, func(t *testing.T) {
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdListenerStatus_RunNoDirectory(t *testing.T) {
+	type test struct {
+		name                string
+		flags               common.CommandListenerStatusFlags
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test1/", string(api.InputSiteStatePath))
+
+	testTable := []test{
+		{
+			name:         "runs fails no directory",
+			errorMessage: "failed to read file: open " + path + "/listeners/my-listener.yaml: no such file or directory",
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdListenerStatus{}
+		command.namespace = "test1"
+		command.listenerHandler = fs.NewListenerHandler(command.namespace)
+		command.listenerName = "my-listener"
+		command.Flags = &test.flags
+		command.output = command.Flags.Output
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
diff --git a/internal/cmd/skupper/listener/nonkube/listener_update_test.go b/internal/cmd/skupper/listener/nonkube/listener_update_test.go
index 6072c5b0..91d08ca2 100644
--- a/internal/cmd/skupper/listener/nonkube/listener_update_test.go
+++ b/internal/cmd/skupper/listener/nonkube/listener_update_test.go
@@ -1,1 +1,235 @@
 package nonkube
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/spf13/cobra"
+	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdListenerUpdate_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandListenerUpdateFlags
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		cobraGenericFlags map[string]string
+		expectedErrors    []string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.InputSiteStatePath))
+
+	testTable := []test{
+		{
+			name:           "Listener is not updated because get listener returned error",
+			args:           []string{"no-listener"},
+			flags:          &common.CommandListenerUpdateFlags{},
+			expectedErrors: []string{"listener no-listener must exist in namespace test to be updated"},
+		},
+		{
+			name:           "listener name is not specified",
+			args:           []string{},
+			flags:          &common.CommandListenerUpdateFlags{},
+			expectedErrors: []string{"listener name must be configured"},
+		},
+		{
+			name:           "listener name is nil",
+			args:           []string{""},
+			flags:          &common.CommandListenerUpdateFlags{},
+			expectedErrors: []string{"listener name must not be empty"},
+		},
+		{
+			name:           "more than one argument is specified",
+			args:           []string{"my", "listener"},
+			flags:          &common.CommandListenerUpdateFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "listener name is not valid.",
+			args:           []string{"my new listener"},
+			flags:          &common.CommandListenerUpdateFlags{},
+			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"},
+			flags:          &common.CommandListenerUpdateFlags{ListenerType: "not-valid"},
+			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"},
+			flags:          &common.CommandListenerUpdateFlags{RoutingKey: "not-valid$"},
+			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:           "TlsCredentials key is not valid",
+			args:           []string{"my-listener"},
+			flags:          &common.CommandListenerUpdateFlags{TlsCredentials: "not-valid$", Host: "1.2.3.4"},
+			expectedErrors: []string{"tlsCredentials value 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"},
+			flags:          &common.CommandListenerUpdateFlags{Port: -1},
+			expectedErrors: []string{"listener port is not valid: value is not positive"},
+		},
+		{
+			name:           "host is not valid",
+			args:           []string{"my-listener"},
+			flags:          &common.CommandListenerUpdateFlags{Host: "not-valid$"},
+			expectedErrors: []string{"host is not valid: a valid IP address or hostname is expected"},
+		},
+		{
+			name:           "output is not valid",
+			args:           []string{"my-listener"},
+			flags:          &common.CommandListenerUpdateFlags{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:  "kubernetes flags are not valid on this platform",
+			args:  []string{"my-listener"},
+			flags: &common.CommandListenerUpdateFlags{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+			expectedErrors: []string{},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-listener"},
+			flags: &common.CommandListenerUpdateFlags{
+				RoutingKey:     "routingkeyname",
+				TlsCredentials: "secretname",
+				Port:           1234,
+				ListenerType:   "tcp",
+				Output:         "json",
+				Host:           "1.2.3.4",
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	// Add a temp file so listener exists for update tests to pass
+	listenerResource := v2alpha1.Listener{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Listener",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-listener",
+			Namespace: "test",
+		},
+	}
+
+	command := &CmdListenerUpdate{Flags: &common.CommandListenerUpdateFlags{}}
+	command.CobraCmd = &cobra.Command{Use: "test"}
+	command.namespace = "test"
+	command.listenerHandler = fs.NewListenerHandler(command.namespace)
+
+	defer command.listenerHandler.Delete("my-listener")
+	content, err := command.listenerHandler.EncodeToYaml(listenerResource)
+	assert.Check(t, err == nil)
+	err = command.listenerHandler.WriteFile(path, "my-listener.yaml", content, common.Listeners)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command.listenerName = ""
+
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+
+			if test.cobraGenericFlags != nil && len(test.cobraGenericFlags) > 0 {
+				for name, value := range test.cobraGenericFlags {
+					command.CobraCmd.Flags().String(name, value, "")
+				}
+			}
+
+			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
+		namespace           string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+		listenerName        string
+		host                string
+		output              string
+		routingKey          string
+		tlsCredentials      string
+		listenerType        string
+		port                int
+	}
+
+	testTable := []test{
+		{
+			name:           "runs ok",
+			namespace:      "test",
+			listenerName:   "my-listener",
+			port:           8080,
+			listenerType:   "tcp",
+			host:           "hostname",
+			routingKey:     "keyname",
+			tlsCredentials: "secretname",
+		},
+		{
+			name:           "run output json",
+			listenerName:   "my-listener",
+			port:           8181,
+			listenerType:   "tcp",
+			host:           "hostname",
+			routingKey:     "keyname",
+			tlsCredentials: "secretname",
+			output:         "json",
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdListenerUpdate{}
+		command.CobraCmd = &cobra.Command{Use: "test"}
+		command.listenerName = test.listenerName
+		command.newSettings.output = test.output
+		command.newSettings.port = test.port
+		command.newSettings.host = test.host
+		command.newSettings.routingKey = test.routingKey
+		command.newSettings.tlsCredentials = test.tlsCredentials
+		command.namespace = test.namespace
+		command.listenerHandler = fs.NewListenerHandler(command.namespace)
+		defer command.listenerHandler.Delete("my-listener")
+		t.Run(test.name, func(t *testing.T) {
+			command.InputToOptions()
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error(), err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
diff --git a/internal/cmd/skupper/site/kube/site_delete_test.go b/internal/cmd/skupper/site/kube/site_delete_test.go
index 6006e550..fd00ca15 100644
--- a/internal/cmd/skupper/site/kube/site_delete_test.go
+++ b/internal/cmd/skupper/site/kube/site_delete_test.go
@@ -1,11 +1,12 @@
 package kube
 
 import (
-	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
-	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"testing"
 	"time"
 
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
@@ -251,6 +252,7 @@ func TestCmdSiteDelete_InputToOptions(t *testing.T) {
 		flags           *common.CommandSiteDeleteFlags
 		expectedTimeout time.Duration
 		expectedWait    bool
+		expectedAll     bool
 	}
 
 	testTable := []test{
@@ -264,6 +266,11 @@ func TestCmdSiteDelete_InputToOptions(t *testing.T) {
 			flags:        &common.CommandSiteDeleteFlags{Wait: true},
 			expectedWait: true,
 		},
+		{
+			name:        "all site resources",
+			flags:       &common.CommandSiteDeleteFlags{All: true},
+			expectedAll: true,
+		},
 	}
 
 	for _, test := range testTable {
@@ -276,6 +283,7 @@ func TestCmdSiteDelete_InputToOptions(t *testing.T) {
 
 			assert.Check(t, command.timeout == test.expectedTimeout)
 			assert.Check(t, command.wait == test.expectedWait)
+			assert.Check(t, command.all == test.expectedAll)
 		})
 	}
 }
@@ -288,6 +296,7 @@ func TestCmdSiteDelete_Run(t *testing.T) {
 		skupperError   string
 		siteName       string
 		errorMessage   string
+		all            bool
 	}
 
 	testTable := []test{
@@ -327,6 +336,95 @@ func TestCmdSiteDelete_Run(t *testing.T) {
 			siteName:       "my-site",
 			errorMessage:   "sites.skupper.io \"my-site\" not found",
 		},
+		{
+			name:       "deletes all",
+			all:        true,
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-site",
+						Namespace: "test",
+					},
+					Status: v2alpha1.SiteStatus{
+						Status: v2alpha1.Status{
+							Message: "OK",
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+				&v2alpha1.AccessGrant{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-token",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.AccessToken{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-token",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "the-site",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Certificate{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link-test",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.AttachedConnectorBinding{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-attachedConnectorBinding",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.AttachedConnector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-attachedConnector",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.RouterAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-routerAccess",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.SecuredAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-securedAccess",
+						Namespace: "test",
+					},
+				},
+			},
+			skupperError: "",
+			siteName:     "my-site",
+		},
 	}
 
 	for _, test := range testTable {
@@ -337,7 +435,7 @@ func TestCmdSiteDelete_Run(t *testing.T) {
 		fakeSkupperClient, err := fakeclient.NewFakeClient(command.Namespace, test.k8sObjects, test.skupperObjects, test.skupperError)
 		assert.Assert(t, err)
 		command.Client = fakeSkupperClient.GetSkupperClient().SkupperV2alpha1()
-
+		command.all = test.all
 		command.siteName = test.siteName
 
 		t.Run(test.name, func(t *testing.T) {
diff --git a/internal/cmd/skupper/site/kube/site_update_test.go b/internal/cmd/skupper/site/kube/site_update_test.go
index a85c6dba..a96da0ad 100644
--- a/internal/cmd/skupper/site/kube/site_update_test.go
+++ b/internal/cmd/skupper/site/kube/site_update_test.go
@@ -574,7 +574,7 @@ func TestCmdSiteUpdate_WaitUntil(t *testing.T) {
 			},
 			siteName:     "my-site",
 			skupperError: "",
-			errorMessage: "Site \"my-site\" is not ready yet, check the status for more information\n",
+			errorMessage: "Site \"my-site\" is not yet ready, check the status for more information\n",
 			expectError:  true,
 		},
 		{
@@ -662,7 +662,7 @@ func TestCmdSiteUpdate_WaitUntil(t *testing.T) {
 			siteName:     "my-site",
 			skupperError: "",
 			expectError:  true,
-			errorMessage: "Site \"my-site\" is configured with errors, check the status for more information\n",
+			errorMessage: "Site \"my-site\" is not yet configured: Error\n",
 		},
 	}
 
diff --git a/internal/cmd/skupper/site/nonkube/site_create_test.go b/internal/cmd/skupper/site/nonkube/site_create_test.go
index 0d5ed353..bfda5347 100644
--- a/internal/cmd/skupper/site/nonkube/site_create_test.go
+++ b/internal/cmd/skupper/site/nonkube/site_create_test.go
@@ -1,12 +1,12 @@
 package nonkube
 
 import (
+	"testing"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
-	fs2 "github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
 	"github.com/spf13/cobra"
-	"testing"
-
 	"gotest.tools/v3/assert"
 	"k8s.io/apimachinery/pkg/runtime"
 )
@@ -26,68 +26,75 @@ func TestNonKubeCmdSiteCreate_ValidateInput(t *testing.T) {
 		{
 			name:           "site name is not valid.",
 			args:           []string{"my new site"},
-			flags:          &common.CommandSiteCreateFlags{BindHost: "bindHost"},
+			flags:          &common.CommandSiteCreateFlags{BindHost: "bindhost", EnableLinkAccess: true},
 			expectedErrors: []string{"site 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:           "site name is not specified.",
 			args:           []string{},
-			flags:          &common.CommandSiteCreateFlags{BindHost: "bindHost"},
+			flags:          &common.CommandSiteCreateFlags{BindHost: "bindhost"},
 			expectedErrors: []string{"site name must not be empty"},
 		},
 		{
 			name:           "more than one argument was specified",
 			args:           []string{"my", "site"},
-			flags:          &common.CommandSiteCreateFlags{BindHost: "bindHost"},
+			flags:          &common.CommandSiteCreateFlags{BindHost: "bindhost"},
 			expectedErrors: []string{"only one argument is allowed for this command"},
 		},
-		{
-			name:  "link access type is not valid",
-			args:  []string{"my-site"},
-			flags: &common.CommandSiteCreateFlags{BindHost: "bindHost", LinkAccessType: "not-valid"},
-			expectedErrors: []string{
-				"link access type is not valid: value not-valid not allowed. It should be one of this options: [route loadbalancer default]",
-				"for the site to work with this type of linkAccess, the --enable-link-access option must be set to true",
-			},
-		},
 		{
 			name:  "output format is not valid",
 			args:  []string{"my-site"},
-			flags: &common.CommandSiteCreateFlags{BindHost: "bindHost", Output: "not-valid"},
+			flags: &common.CommandSiteCreateFlags{BindHost: "127.0.0.1", Output: "not-valid"},
 			expectedErrors: []string{
 				"output type is not valid: value not-valid not allowed. It should be one of this options: [json yaml]",
 			},
 		},
 		{
-			name:  "bindHost was not specified",
-			args:  []string{"my-site"},
-			flags: &common.CommandSiteCreateFlags{},
-			expectedErrors: []string{
-				"bind host should not be empty",
-			},
+			name:           "bindHost was not specified ok",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteCreateFlags{EnableLinkAccess: true},
+			expectedErrors: []string{},
 		},
 		{
-			name:           "service-account is not valid on this platform",
+			name:           "bindHost was not valid",
 			args:           []string{"my-site"},
-			flags:          &common.CommandSiteCreateFlags{ServiceAccount: "service-account", BindHost: "bindHost"},
-			expectedErrors: []string{},
+			flags:          &common.CommandSiteCreateFlags{EnableLinkAccess: true, BindHost: "not-valid$"},
+			expectedErrors: []string{"bindhost is not valid: a valid IP address or hostname is expected"},
+		},
+		{
+			name:           "subjectAlternativeNames was not valid",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteCreateFlags{EnableLinkAccess: true, BindHost: "not-valid", SubjectAlternativeNames: []string{"not-valid$"}},
+			expectedErrors: []string{"SubjectAlternativeNames is not valid: a valid IP address or hostname is expected"},
 		},
 		{
 			name:           "kubernetes flags are not valid on this platform",
 			args:           []string{"my-site"},
-			flags:          &common.CommandSiteCreateFlags{BindHost: "bindHost"},
+			flags:          &common.CommandSiteCreateFlags{BindHost: "bindhost"},
 			expectedErrors: []string{},
 			cobraGenericFlags: map[string]string{
 				common.FlagNameContext:    "test",
 				common.FlagNameKubeconfig: "test",
 			},
 		},
+		{
+			name: "flags all valid",
+			args: []string{"my-site"},
+			flags: &common.CommandSiteCreateFlags{
+				Output:                  "json",
+				BindHost:                "1.2.3.4",
+				EnableLinkAccess:        true,
+				SubjectAlternativeNames: []string{"3.3.3.3"},
+			},
+			expectedErrors: []string{},
+		},
 	}
 
 	for _, test := range testTable {
 		t.Run(test.name, func(t *testing.T) {
 			command := &CmdSiteCreate{Flags: &common.CommandSiteCreateFlags{}}
 			command.CobraCmd = &cobra.Command{Use: "test"}
+			command.siteHandler = fs.NewSiteHandler(command.namespace)
 
 			if test.flags != nil {
 				command.Flags = test.flags
@@ -117,88 +124,84 @@ func TestNonKubeCmdSiteCreate_InputToOptions(t *testing.T) {
 		namespace                       string
 		flags                           common.CommandSiteCreateFlags
 		expectedSettings                map[string]string
-		expectedLinkAccess              string
+		expectedLinkAccess              bool
 		expectedOutput                  string
 		expectedNamespace               string
 		expectedSubjectAlternativeNames []string
+		expectedBindHost                string
+		expectedRouterAccessName        string
 	}
 
 	testTable := []test{
 		{
-			name:  "options without link access enabled",
+			name:  "options without link access disabled",
 			args:  []string{"my-site"},
-			flags: common.CommandSiteCreateFlags{},
+			flags: common.CommandSiteCreateFlags{BindHost: "test"},
 			expectedSettings: map[string]string{
 				"name": "my-site",
 			},
-			expectedLinkAccess: "none",
-			expectedOutput:     "",
-			expectedNamespace:  "default",
+			expectedLinkAccess:              false,
+			expectedOutput:                  "",
+			expectedNamespace:               "default",
+			expectedBindHost:                "",
+			expectedRouterAccessName:        "",
+			expectedSubjectAlternativeNames: nil,
 		},
 		{
-			name:  "options with link access enabled but using a type by default and link access bindHost specified",
+			name:  "options with link access enabled",
 			args:  []string{"my-site"},
-			flags: common.CommandSiteCreateFlags{EnableLinkAccess: true},
+			flags: common.CommandSiteCreateFlags{EnableLinkAccess: true, BindHost: "test"},
 			expectedSettings: map[string]string{
 				"name": "my-site",
 			},
-			expectedLinkAccess: "loadbalancer",
-			expectedOutput:     "",
-			expectedNamespace:  "default",
-		},
-		{
-			name:  "options with link access enabled using the nodeport type",
-			args:  []string{"my-site"},
-			flags: common.CommandSiteCreateFlags{EnableLinkAccess: true, LinkAccessType: "nodeport"},
-			expectedSettings: map[string]string{
-				"name": "my-site",
-			},
-			expectedLinkAccess: "nodeport",
-			expectedOutput:     "",
-			expectedNamespace:  "default",
-		},
-		{
-			name:  "options with link access options not well specified",
-			args:  []string{"my-site"},
-			flags: common.CommandSiteCreateFlags{EnableLinkAccess: false, LinkAccessType: "nodeport"},
-			expectedSettings: map[string]string{
-				"name": "my-site",
-			},
-			expectedLinkAccess: "none",
-			expectedOutput:     "",
-			expectedNamespace:  "default",
+			expectedLinkAccess:              true,
+			expectedOutput:                  "",
+			expectedNamespace:               "default",
+			expectedBindHost:                "test",
+			expectedRouterAccessName:        "router-access-my-site",
+			expectedSubjectAlternativeNames: nil,
 		},
 		{
 			name:  "options output type",
 			args:  []string{"my-site"},
-			flags: common.CommandSiteCreateFlags{EnableLinkAccess: false, LinkAccessType: "nodeport", Output: "yaml"},
+			flags: common.CommandSiteCreateFlags{EnableLinkAccess: false, BindHost: "test", Output: "yaml"},
 			expectedSettings: map[string]string{
 				"name": "my-site",
 			},
-			expectedLinkAccess: "none",
-			expectedOutput:     "yaml",
-			expectedNamespace:  "default",
+			expectedLinkAccess:              false,
+			expectedOutput:                  "yaml",
+			expectedNamespace:               "default",
+			expectedBindHost:                "",
+			expectedSubjectAlternativeNames: nil,
+			expectedRouterAccessName:        "",
 		},
 		{
-			name:      "options with specific namespace",
+			name:      "options with subject alternative names",
 			args:      []string{"my-site"},
 			namespace: "test",
-			flags:     common.CommandSiteCreateFlags{},
+			flags:     common.CommandSiteCreateFlags{BindHost: "1.2.3.4", SubjectAlternativeNames: []string{"test"}},
 			expectedSettings: map[string]string{
 				"name": "my-site",
 			},
-			expectedNamespace: "test",
+			expectedLinkAccess:              false,
+			expectedNamespace:               "test",
+			expectedBindHost:                "",
+			expectedSubjectAlternativeNames: nil,
+			expectedRouterAccessName:        "",
 		},
 		{
-			name:      "options with subject alternative names",
+			name:      "options with enable link access and subject alternative names",
 			args:      []string{"my-site"},
 			namespace: "test",
-			flags:     common.CommandSiteCreateFlags{SubjectAlternativeNames: []string{"test", "test2"}},
+			flags:     common.CommandSiteCreateFlags{EnableLinkAccess: true, BindHost: "1.2.3.4", SubjectAlternativeNames: []string{"test"}},
 			expectedSettings: map[string]string{
 				"name": "my-site",
 			},
+			expectedLinkAccess:              true,
 			expectedNamespace:               "test",
-			expectedSubjectAlternativeNames: []string{"test", "test2"},
+			expectedSubjectAlternativeNames: []string{"test"},
+			expectedBindHost:                "1.2.3.4",
+			expectedRouterAccessName:        "router-access-my-site",
 		},
 	}
 
@@ -208,15 +211,17 @@ func TestNonKubeCmdSiteCreate_InputToOptions(t *testing.T) {
 			cmd.Flags = &test.flags
 			cmd.siteName = "my-site"
 			cmd.namespace = test.namespace
-			cmd.siteHandler = fs2.NewSiteHandler(cmd.namespace)
-			cmd.routerAccessHandler = fs2.NewRouterAccessHandler(cmd.namespace)
+			cmd.siteHandler = fs.NewSiteHandler(cmd.namespace)
+			cmd.routerAccessHandler = fs.NewRouterAccessHandler(cmd.namespace)
 
 			cmd.InputToOptions()
 
 			assert.DeepEqual(t, cmd.options, test.expectedSettings)
-
 			assert.Check(t, cmd.output == test.expectedOutput)
 			assert.Check(t, cmd.namespace == test.expectedNamespace)
+			assert.Check(t, cmd.bindHost == test.expectedBindHost)
+			assert.Check(t, cmd.linkAccessEnabled == test.expectedLinkAccess)
+			assert.Check(t, cmd.routerAccessName == test.expectedRouterAccessName)
 			assert.DeepEqual(t, cmd.subjectAlternativeNames, test.expectedSubjectAlternativeNames)
 		})
 	}
@@ -224,43 +229,47 @@ func TestNonKubeCmdSiteCreate_InputToOptions(t *testing.T) {
 
 func TestNonKubeCmdSiteCreate_Run(t *testing.T) {
 	type test struct {
-		name             string
-		k8sObjects       []runtime.Object
-		skupperObjects   []runtime.Object
-		skupperError     string
-		siteName         string
-		host             string
-		options          map[string]string
-		output           string
-		errorMessage     string
-		routerAccessName string
+		name              string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		skupperError      string
+		siteName          string
+		host              string
+		options           map[string]string
+		output            string
+		errorMessage      string
+		routerAccessName  string
+		linkAccessEnabled bool
 	}
 
 	testTable := []test{
 		{
-			name:             "runs ok",
-			k8sObjects:       nil,
-			skupperObjects:   nil,
-			siteName:         "my-site",
-			host:             "bindHost",
-			routerAccessName: "ra-test",
-			options:          map[string]string{"name": "my-site"},
+			name:              "runs ok",
+			k8sObjects:        nil,
+			skupperObjects:    nil,
+			siteName:          "my-site",
+			host:              "bindhost",
+			routerAccessName:  "ra-test",
+			linkAccessEnabled: true,
+			options:           map[string]string{"name": "my-site"},
 		},
 		{
-			name:           "runs ok without create site",
-			k8sObjects:     nil,
-			skupperObjects: nil,
-			siteName:       "test",
-			host:           "bindHost",
-			options:        map[string]string{"name": "my-site"},
-			output:         "yaml",
-			skupperError:   "",
+			name:              "runs ok without create site",
+			k8sObjects:        nil,
+			skupperObjects:    nil,
+			siteName:          "my-site",
+			host:              "bindhost",
+			routerAccessName:  "ra-test",
+			options:           map[string]string{"name": "my-site"},
+			output:            "yaml",
+			linkAccessEnabled: true,
+			skupperError:      "",
 		},
 		{
 			name:           "runs fails because the output format is not supported",
 			k8sObjects:     nil,
 			skupperObjects: nil,
-			siteName:       "test",
+			siteName:       "my-site",
 			host:           "bindHost",
 			options:        map[string]string{"name": "my-site"},
 			output:         "unsupported",
@@ -276,9 +285,11 @@ func TestNonKubeCmdSiteCreate_Run(t *testing.T) {
 		command.options = test.options
 		command.output = test.output
 		command.routerAccessName = test.routerAccessName
-		command.siteHandler = fs2.NewSiteHandler(command.namespace)
-		command.routerAccessHandler = fs2.NewRouterAccessHandler(command.namespace)
-
+		command.linkAccessEnabled = test.linkAccessEnabled
+		command.siteHandler = fs.NewSiteHandler(command.namespace)
+		command.routerAccessHandler = fs.NewRouterAccessHandler(command.namespace)
+		defer command.siteHandler.Delete("my-site")
+		defer command.routerAccessHandler.Delete("my-site")
 		t.Run(test.name, func(t *testing.T) {
 
 			err := command.Run()
diff --git a/internal/cmd/skupper/site/nonkube/site_delete_test.go b/internal/cmd/skupper/site/nonkube/site_delete_test.go
index 6072c5b0..ae2a437f 100644
--- a/internal/cmd/skupper/site/nonkube/site_delete_test.go
+++ b/internal/cmd/skupper/site/nonkube/site_delete_test.go
@@ -1,1 +1,167 @@
 package nonkube
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/spf13/cobra"
+	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdSiteDelete_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandSiteDeleteFlags
+		cobraGenericFlags map[string]string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		expectedErrors    []string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.InputSiteStatePath))
+
+	testTable := []test{
+		{
+			name:           "site name is not specified",
+			args:           []string{},
+			flags:          &common.CommandSiteDeleteFlags{},
+			expectedErrors: []string{"site name must be specified"},
+		},
+		{
+			name:           "site name is nil",
+			args:           []string{""},
+			flags:          &common.CommandSiteDeleteFlags{},
+			expectedErrors: []string{"site name must not be empty"},
+		},
+		{
+			name:           "site name is not valid",
+			args:           []string{"my name"},
+			flags:          &common.CommandSiteDeleteFlags{},
+			expectedErrors: []string{"site 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", "site"},
+			flags:          &common.CommandSiteDeleteFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "site doesn't exist",
+			args:           []string{"no-site"},
+			flags:          &common.CommandSiteDeleteFlags{},
+			expectedErrors: []string{"site no-site does not exist"},
+		},
+		{
+			name:           "kubernetes flags are not valid on this platform",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteDeleteFlags{},
+			expectedErrors: []string{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+		},
+	}
+
+	// Add temp files so site exists for update tests
+	siteResource := v2alpha1.Site{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Site",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-site",
+			Namespace: "test",
+		},
+	}
+	command := &CmdSiteDelete{Flags: &common.CommandSiteDeleteFlags{}}
+	command.CobraCmd = &cobra.Command{Use: "test"}
+	command.namespace = "test"
+	command.siteHandler = fs.NewSiteHandler(command.namespace)
+	command.routerAccessHandler = fs.NewRouterAccessHandler(command.namespace)
+
+	defer command.siteHandler.Delete("my-site")
+	content, err := command.siteHandler.EncodeToYaml(siteResource)
+	assert.Check(t, err == nil)
+	err = command.siteHandler.WriteFile(path, "my-site.yaml", content, common.Sites)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command.siteName = ""
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+
+			if test.cobraGenericFlags != nil && len(test.cobraGenericFlags) > 0 {
+				for name, value := range test.cobraGenericFlags {
+					command.CobraCmd.Flags().String(name, value, "")
+				}
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdSiteDelete_Run(t *testing.T) {
+	type test struct {
+		name                string
+		namespace           string
+		deleteName          string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+	}
+
+	testTable := []test{
+		{
+			name:         "run fails default",
+			deleteName:   "my-site",
+			errorMessage: "error",
+		},
+		{
+			name:         "run fails",
+			namespace:    "test",
+			deleteName:   "my-site",
+			errorMessage: "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := &CmdSiteDelete{}
+
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd.siteName = test.deleteName
+			cmd.namespace = test.namespace
+			cmd.siteHandler = fs.NewSiteHandler(cmd.namespace)
+			cmd.routerAccessHandler = fs.NewRouterAccessHandler(cmd.namespace)
+			cmd.InputToOptions()
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
diff --git a/internal/cmd/skupper/site/nonkube/site_status_test.go b/internal/cmd/skupper/site/nonkube/site_status_test.go
index 6072c5b0..42bec6e9 100644
--- a/internal/cmd/skupper/site/nonkube/site_status_test.go
+++ b/internal/cmd/skupper/site/nonkube/site_status_test.go
@@ -1,1 +1,241 @@
 package nonkube
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdSiteStatus_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandSiteStatusFlags
+		cobraGenericFlags map[string]string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		expectedErrors    []string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.RuntimeSiteStatePath))
+
+	testTable := []test{
+		{
+			name:           "site does not exist in the namespace",
+			args:           []string{"no-site"},
+			expectedErrors: []string{"site no-site does not exist"},
+		},
+		{
+			name:           "site name is nil",
+			args:           []string{""},
+			expectedErrors: []string{"site name must not be empty"},
+		},
+		{
+			name:           "more than one argument is specified",
+			args:           []string{"my", "site"},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "site name is not valid.",
+			args:           []string{"my new site"},
+			expectedErrors: []string{"site 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",
+			expectedErrors: []string{},
+		},
+		{
+			name:           "bad output",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteStatusFlags{Output: "yaml$"},
+			expectedErrors: []string{"output type is not valid: value yaml$ not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name:           "good flags",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteStatusFlags{Output: "yaml"},
+			expectedErrors: []string{},
+		},
+	}
+
+	//Add a temp file so site exists for status tests
+	siteResource := v2alpha1.Site{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Site",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-site",
+			Namespace: "test",
+		},
+	}
+
+	command := &CmdSiteStatus{}
+	command.namespace = "test"
+	command.siteHandler = fs.NewSiteHandler(command.namespace)
+
+	defer command.siteHandler.Delete("my-site")
+	content, err := command.siteHandler.EncodeToYaml(siteResource)
+	assert.Check(t, err == nil)
+	err = command.siteHandler.WriteFile(path, "my-site.yaml", content, common.Sites)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command.siteName = ""
+
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+			if test.cobraGenericFlags != nil && len(test.cobraGenericFlags) > 0 {
+				for name, value := range test.cobraGenericFlags {
+					command.CobraCmd.Flags().String(name, value, "")
+				}
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdSiteStatus_Run(t *testing.T) {
+	type test struct {
+		name                string
+		siteName            string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+		output              string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.InputSiteStatePath))
+	testTable := []test{
+		{
+			name:         "run fails site doesn't exist",
+			siteName:     "no-site",
+			errorMessage: "failed to read file: open " + path + "/sites/no-site.yaml: no such file or directory",
+		},
+		{
+			name:     "runs ok, returns 1 site",
+			siteName: "my-site",
+		},
+		{
+			name: "runs ok, returns 1 site",
+		},
+		{
+			name:     "runs ok, returns 1 site yaml",
+			siteName: "my-site",
+			output:   "yaml",
+		},
+	}
+
+	//Add a temp file so site exists for status tests
+	siteResource1 := v2alpha1.Site{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Site",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-site",
+			Namespace: "test",
+		},
+		Spec: v2alpha1.SiteSpec{
+			LinkAccess: "route",
+			Settings: map[string]string{
+				"name": "my-site",
+			},
+		},
+		Status: v2alpha1.SiteStatus{
+			Status: v2alpha1.Status{
+				Conditions: []metav1.Condition{
+					{
+						Type:   "Configured",
+						Status: "True",
+					},
+				},
+			},
+		},
+	}
+
+	// add site in runtime directory
+	command := &CmdSiteStatus{}
+	command.namespace = "test"
+	command.siteHandler = fs.NewSiteHandler(command.namespace)
+	content, err := command.siteHandler.EncodeToYaml(siteResource1)
+	assert.Check(t, err == nil)
+	path = filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.RuntimeSiteStatePath))
+	err = command.siteHandler.WriteFile(path, "my-site.yaml", content, common.Sites)
+	assert.Check(t, err == nil)
+	defer command.siteHandler.Delete("my-site")
+
+	for _, test := range testTable {
+		command.siteName = test.siteName
+		command.output = test.output
+
+		t.Run(test.name, func(t *testing.T) {
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+func TestCmdSiteStatus_RunNoDirectory(t *testing.T) {
+	type test struct {
+		name                string
+		siteName            string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test1/", string(api.InputSiteStatePath), "/sites")
+
+	testTable := []test{
+		{
+			name:         "runs fails no directory",
+			errorMessage: "failed to read directory: open " + path + ": no such file or directory",
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdSiteStatus{}
+		command.namespace = "test1"
+		command.siteHandler = fs.NewSiteHandler(command.namespace)
+		command.siteName = test.siteName
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
diff --git a/internal/cmd/skupper/site/nonkube/site_update_test.go b/internal/cmd/skupper/site/nonkube/site_update_test.go
index 6072c5b0..05ef5d5c 100644
--- a/internal/cmd/skupper/site/nonkube/site_update_test.go
+++ b/internal/cmd/skupper/site/nonkube/site_update_test.go
@@ -1,1 +1,352 @@
 package nonkube
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/spf13/cobra"
+	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdSiteUpdate_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandSiteUpdateFlags
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		cobraGenericFlags map[string]string
+		expectedErrors    []string
+	}
+
+	homeDir, err := os.UserHomeDir()
+	assert.Check(t, err == nil)
+	path := filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.InputSiteStatePath))
+
+	testTable := []test{
+		{
+			name:           "site is not updated because get site returned error",
+			args:           []string{"no-site"},
+			flags:          &common.CommandSiteUpdateFlags{},
+			expectedErrors: []string{"site no-site must exist to be updated"},
+		},
+		{
+			name:           "site name is not specified",
+			args:           []string{},
+			flags:          &common.CommandSiteUpdateFlags{},
+			expectedErrors: []string{"site name must be configured"},
+		},
+		{
+			name:           "site name is nil",
+			args:           []string{""},
+			flags:          &common.CommandSiteUpdateFlags{},
+			expectedErrors: []string{"site name must not be empty"},
+		},
+		{
+			name:           "more than one argument is specified",
+			args:           []string{"my", "site"},
+			flags:          &common.CommandSiteUpdateFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "site name is not valid.",
+			args:           []string{"my new site"},
+			flags:          &common.CommandSiteUpdateFlags{},
+			expectedErrors: []string{"site 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:           "bind-host is not valid",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteUpdateFlags{BindHost: "not-valid$"},
+			expectedErrors: []string{"bindhost is not valid: a valid IP address or hostname is expected"},
+		},
+		{
+			name:           "subjectAlternativeNames are not valid",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteUpdateFlags{SubjectAlternativeNames: []string{"not-valid$"}},
+			expectedErrors: []string{"SubjectAlternativeNames are not valid: a valid IP address or hostname is expected"},
+		},
+		{
+			name:           "output is not valid",
+			args:           []string{"my-site"},
+			flags:          &common.CommandSiteUpdateFlags{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:  "kubernetes flags are not valid on this platform",
+			args:  []string{"my-site"},
+			flags: &common.CommandSiteUpdateFlags{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+			expectedErrors: []string{},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-site"},
+			flags: &common.CommandSiteUpdateFlags{
+				Output:                  "json",
+				BindHost:                "1.2.3.4",
+				EnableLinkAccess:        true,
+				SubjectAlternativeNames: []string{"3.3.3.3"},
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	// Add temp files so site exists for update tests
+	siteResource := v2alpha1.Site{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Site",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-site",
+			Namespace: "test",
+		},
+	}
+
+	routerAccessResource := v2alpha1.RouterAccess{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "RouterAccess",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "router-access-mysite",
+			Namespace: "test",
+		},
+		Spec: v2alpha1.RouterAccessSpec{
+			Roles: []v2alpha1.RouterAccessRole{
+				{
+					Name: "inter-router",
+					Port: 55671,
+				},
+				{
+					Name: "edge",
+					Port: 45671,
+				},
+			},
+			BindHost:                "1.2.3.4",
+			SubjectAlternativeNames: []string{"test", "2.2.2.2"},
+		},
+	}
+	command := &CmdSiteUpdate{Flags: &common.CommandSiteUpdateFlags{}}
+	command.CobraCmd = &cobra.Command{Use: "test"}
+	command.namespace = "test"
+	command.siteHandler = fs.NewSiteHandler(command.namespace)
+	command.routerAccessHandler = fs.NewRouterAccessHandler(command.namespace)
+
+	defer command.siteHandler.Delete("my-site")
+	defer command.routerAccessHandler.Delete("my-site")
+
+	content, err := command.siteHandler.EncodeToYaml(siteResource)
+	assert.Check(t, err == nil)
+	err = command.siteHandler.WriteFile(path, "my-site.yaml", content, common.Sites)
+	assert.Check(t, err == nil)
+	content, err = command.routerAccessHandler.EncodeToYaml(routerAccessResource)
+	assert.Check(t, err == nil)
+	err = command.routerAccessHandler.WriteFile(path, "router-access-my-site.yaml", content, common.RouterAccesses)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command.siteName = ""
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+
+			if test.cobraGenericFlags != nil && len(test.cobraGenericFlags) > 0 {
+				for name, value := range test.cobraGenericFlags {
+					command.CobraCmd.Flags().String(name, value, "")
+				}
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestNonKubeCmdSiteUpdate_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name                            string
+		args                            []string
+		namespace                       string
+		flags                           common.CommandSiteUpdateFlags
+		expectedSettings                map[string]string
+		expectedLinkAccess              bool
+		expectedNamespace               string
+		expectedSubjectAlternativeNames []string
+		expectedBindHost                string
+		expectedRouterAccessName        string
+	}
+
+	testTable := []test{
+		{
+			name:  "options without link access disabled",
+			args:  []string{"my-site"},
+			flags: common.CommandSiteUpdateFlags{BindHost: "test"},
+			expectedSettings: map[string]string{
+				"name": "my-site",
+			},
+			expectedLinkAccess:              false,
+			expectedNamespace:               "default",
+			expectedBindHost:                "",
+			expectedRouterAccessName:        "router-access-my-site",
+			expectedSubjectAlternativeNames: nil,
+		},
+		{
+			name:  "options with link access enabled",
+			args:  []string{"my-site"},
+			flags: common.CommandSiteUpdateFlags{EnableLinkAccess: true, BindHost: "test"},
+			expectedSettings: map[string]string{
+				"name": "my-site",
+			},
+			expectedLinkAccess:              true,
+			expectedNamespace:               "default",
+			expectedBindHost:                "test",
+			expectedRouterAccessName:        "router-access-my-site",
+			expectedSubjectAlternativeNames: nil,
+		},
+		{
+			name:      "options without enable link access and subject alternative names",
+			args:      []string{"my-site"},
+			namespace: "test",
+			flags:     common.CommandSiteUpdateFlags{EnableLinkAccess: false, BindHost: "1.2.3.4", SubjectAlternativeNames: []string{"test"}},
+			expectedSettings: map[string]string{
+				"name": "my-site",
+			},
+			expectedLinkAccess:              false,
+			expectedNamespace:               "test",
+			expectedBindHost:                "",
+			expectedSubjectAlternativeNames: nil,
+			expectedRouterAccessName:        "router-access-my-site",
+		},
+		{
+			name:      "options with enable link access and subject alternative names",
+			args:      []string{"my-site"},
+			namespace: "test",
+			flags:     common.CommandSiteUpdateFlags{EnableLinkAccess: true, BindHost: "1.2.3.4", SubjectAlternativeNames: []string{"test"}},
+			expectedSettings: map[string]string{
+				"name": "my-site",
+			},
+			expectedLinkAccess:              true,
+			expectedNamespace:               "test",
+			expectedSubjectAlternativeNames: []string{"test"},
+			expectedBindHost:                "1.2.3.4",
+			expectedRouterAccessName:        "router-access-my-site",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			cmd := &CmdSiteUpdate{Flags: &common.CommandSiteUpdateFlags{EnableLinkAccess: test.flags.EnableLinkAccess}}
+			cmd.CobraCmd = &cobra.Command{Use: "test"}
+			cmd.Flags = &test.flags
+			cmd.newSettings.bindHost = test.flags.BindHost
+			cmd.newSettings.subjectAlternativeNames = test.flags.SubjectAlternativeNames
+			cmd.siteName = "my-site"
+			cmd.namespace = test.namespace
+			cmd.linkAccessEnabled = test.flags.EnableLinkAccess
+
+			cmd.InputToOptions()
+
+			assert.DeepEqual(t, cmd.options, test.expectedSettings)
+			assert.Check(t, cmd.namespace == test.expectedNamespace)
+			assert.Check(t, cmd.bindHost == test.expectedBindHost)
+			assert.Check(t, cmd.linkAccessEnabled == test.expectedLinkAccess)
+			assert.Check(t, cmd.routerAccessName == test.expectedRouterAccessName)
+			assert.DeepEqual(t, cmd.subjectAlternativeNames, test.expectedSubjectAlternativeNames)
+		})
+	}
+}
+
+func TestCmdSiteUpdate_Run(t *testing.T) {
+	type test struct {
+		name                string
+		namespace           string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+		siteName            string
+		flags               common.CommandSiteUpdateFlags
+		linkAccessEnabled   bool
+	}
+
+	testTable := []test{
+		{
+			name:      "runs ok link enable",
+			namespace: "test",
+			siteName:  "my-site",
+			flags: common.CommandSiteUpdateFlags{
+				BindHost:                "1.2.3.4",
+				EnableLinkAccess:        true,
+				SubjectAlternativeNames: []string{"2.2.2.2", "test"},
+			},
+			linkAccessEnabled: true,
+		},
+		{
+			name:      "runs ok",
+			namespace: "test",
+			siteName:  "my-site",
+			flags:     common.CommandSiteUpdateFlags{},
+		},
+		{
+			name:     "run ok output json",
+			siteName: "my-site",
+			flags: common.CommandSiteUpdateFlags{
+				BindHost:                "1.2.3.4",
+				EnableLinkAccess:        true,
+				SubjectAlternativeNames: []string{"2.2.2.2", "test", "5.6.7.8"},
+				Output:                  "json",
+			},
+			linkAccessEnabled: true,
+		},
+		{
+			name:     "run ok output yaml",
+			siteName: "my-site",
+			flags: common.CommandSiteUpdateFlags{
+				EnableLinkAccess: false,
+				Output:           "yaml",
+			},
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdSiteUpdate{}
+		command.CobraCmd = &cobra.Command{Use: "test"}
+		command.Flags = &test.flags
+		command.siteName = test.siteName
+		command.siteHandler = fs.NewSiteHandler(command.namespace)
+		command.routerAccessHandler = fs.NewRouterAccessHandler(command.namespace)
+		command.newSettings.bindHost = test.flags.BindHost
+		command.newSettings.subjectAlternativeNames = test.flags.SubjectAlternativeNames
+		command.linkAccessEnabled = test.linkAccessEnabled
+		defer command.siteHandler.Delete("my-site")
+		defer command.routerAccessHandler.Delete("my-site")
+		t.Run(test.name, func(t *testing.T) {
+			command.InputToOptions()
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error(), err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
diff --git a/internal/cmd/skupper/site/site_test.go b/internal/cmd/skupper/site/site_test.go
index 53339e1a..f3b9f93a 100644
--- a/internal/cmd/skupper/site/site_test.go
+++ b/internal/cmd/skupper/site/site_test.go
@@ -1,12 +1,13 @@
 package site
 
 import (
+	"testing"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"gotest.tools/v3/assert"
-	"testing"
 )
 
 func TestCmdSiteFactory(t *testing.T) {
@@ -26,7 +27,7 @@ func TestCmdSiteFactory(t *testing.T) {
 				common.FlagNameServiceAccount:          "",
 				common.FlagNameOutput:                  "",
 				common.FlagNameTimeout:                 "30s",
-				common.FlagNameBindHost:                "",
+				common.FlagNameBindHost:                "0.0.0.0",
 				common.FlagNameSubjectAlternativeNames: "[]",
 				common.FlagNameWait:                    "ready",
 			},
@@ -51,13 +52,16 @@ func TestCmdSiteFactory(t *testing.T) {
 			expectedFlagsWithDefaultValue: map[string]interface{}{
 				common.FlagNameTimeout: "1m0s",
 				common.FlagNameWait:    "true",
+				common.FlagNameAll:     "false",
 			},
 			command: CmdSiteDeleteFactory(types.PlatformKubernetes),
 		},
 		{
-			name:                          "CmdSiteStatusFactory",
-			expectedFlagsWithDefaultValue: map[string]interface{}{},
-			command:                       CmdSiteStatusFactory(types.PlatformKubernetes),
+			name: "CmdSiteStatusFactory",
+			expectedFlagsWithDefaultValue: map[string]interface{}{
+				common.FlagNameOutput: "",
+			},
+			command: CmdSiteStatusFactory(types.PlatformKubernetes),
 		},
 	}
 
diff --git a/internal/cmd/skupper/token/kube/token_issue_test.go b/internal/cmd/skupper/token/kube/token_issue_test.go
index f9a538b2..0502d760 100644
--- a/internal/cmd/skupper/token/kube/token_issue_test.go
+++ b/internal/cmd/skupper/token/kube/token_issue_test.go
@@ -34,6 +34,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
 				Name:               "my-token",
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -94,6 +95,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.AccessGrant{
@@ -122,6 +124,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -153,6 +156,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -193,6 +197,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -233,6 +238,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -274,6 +280,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
 				Name:               "my new token",
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -314,6 +321,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -354,6 +362,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 0,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -395,6 +404,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   10 * time.Second,
 				RedemptionsAllowed: 1,
 				Timeout:            10 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -435,6 +445,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            0 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -468,6 +479,48 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 			},
 			expectedErrors: []string{"timeout is not valid: duration must not be less than 10s; got 0s"},
 		},
+		{
+			name: "cost is not valid",
+			args: []string{"~/token.yaml"},
+			flags: common.CommandTokenIssueFlags{
+				ExpirationWindow:   15 * time.Minute,
+				RedemptionsAllowed: 1,
+				Timeout:            60 * time.Second,
+				Cost:               "Not-valid",
+			},
+			skupperObjects: []runtime.Object{
+				&v2alpha1.SiteList{
+					Items: []v2alpha1.Site{
+						{
+							ObjectMeta: v1.ObjectMeta{
+								Name:      "site1",
+								Namespace: "test",
+							},
+							Status: v2alpha1.SiteStatus{
+								Status: v2alpha1.Status{
+									Conditions: []v1.Condition{
+										{
+											Type:   "Configured",
+											Status: "True",
+										},
+										{
+											Type:   "Running",
+											Status: "True",
+										},
+										{
+											Type:   "Ready",
+											Status: "True",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				`link cost is not valid: strconv.Atoi: parsing "Not-valid": invalid syntax`},
+		},
 		{
 			name: "flags all valid",
 			args: []string{"~/token.yaml"},
@@ -475,6 +528,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "5",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -569,6 +623,7 @@ func TestCmdTokenIssue_Run(t *testing.T) {
 				ExpirationWindow:   20 * time.Minute,
 				RedemptionsAllowed: 2,
 				Timeout:            60 * time.Second,
+				Cost:               "5",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.AccessGrant{
diff --git a/internal/cmd/skupper/token/token_test.go b/internal/cmd/skupper/token/token_test.go
index c3608f6c..8adbd880 100644
--- a/internal/cmd/skupper/token/token_test.go
+++ b/internal/cmd/skupper/token/token_test.go
@@ -27,6 +27,7 @@ func TestCmdTokenFactory(t *testing.T) {
 				common.FlagNameExpirationWindow:   "15m0s",
 				common.FlagNameRedemptionsAllowed: "1",
 				common.FlagNameToken:              "",
+				common.FlagNameCost:               "1",
 			},
 			command: CmdTokenIssueFactory(types.PlatformKubernetes),
 		},