Repository: skupperproject/skupper
Pull request #1761 (2024-10-31 17:31:16)

support for tls credentials in connectors and listeners
(plus some improved test coverage)

lynnemorrison: Will these changes require changes to the kube connector/listener commands?
grs: > Will these changes require changes to the kube connector/listener commands?

Yes, probably having options in the CLI eventually for these new fields will be useful. However I think the naming is still in flux so I would not rush to add anything yet.


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9fa6d185..b442e31f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -14,258 +14,13 @@ executors:
       image: ubuntu-2204:2022.10.2
     resource_class: large
 
-  local_cluster_policy_test_executor:
-    machine:
-      image: ubuntu-2204:2022.10.2
-    # Policy tests run all in sequence.  So, there is no benefit in adding more
-    # CPU or memory.  Within the tests, there is paralellism, but on goroutines,
-    # so they'd not use the additional CPUs.  The only thing a running policy
-    # test competes with is the other system processes
-    # resource_class: medium (the default)
-
 commands:
-  minikube-install:
-    description: Installs the minikube executable onto the system.
-    steps:
-      - run:
-          command: >-
-            curl -Lo minikube
-            https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 &&
-            chmod +x minikube && sudo
-            mv minikube /usr/local/bin/
-          name: Install Minikube Executable
-
-  minikube-start-medium:
-    description: Starts the minikube service, with 2 CPU and 2 GiB
-    steps:
-      - run:
-          command: >-
-            minikube start --vm-driver=docker --cpus 2 --memory 2048 --insecure-registry ${LOCAL_IP}:5000
-          name: Start Minikube Cluster
-
-  # We're using only 6 GiB out of the available 15 GiB of the linux/large
-  # CircleCI executor.  This allows us some leeway to grow within this
-  # resource class when memory issues happen, so we can prepare for the
-  # next resource class with plenty of time, when required.
-  minikube-start-large:
-    description: Starts the minikube service, with 4 CPU and 6GiB
-    steps:
-      - run:
-          command: >-
-            minikube start --vm-driver=docker --cpus 4 --memory 6144  --insecure-registry ${LOCAL_IP}:5000
-          name: Start Minikube Cluster
-
-  minikube-start-load-balancer:
-    description: Starts the minikube tunnel
-    steps:
-      - run:
-          command: minikube tunnel
-          name: Start Minikube Tunnel
-          background: true
-
-  # By default, we grep for out of memory messages.  To get the full output,
-  # just set the argument 're' to '.'
-  minikube-logs:
-    description: Tail minikube logs, grepping for something (by default, OOM)
-    parameters:
-      re:
-        # This is for egrep, so you can do things like "(oom|error)"
-        default: "(out of memory|oom.?kill)"
-        type: string
-    steps:
-    - run:
-        name: Tail and grep minikube logs
-        command: minikube logs -f | egrep -i "<<parameters.re>>"
-        background: true
-
-  prepare_for_local_cluster_tests:
-    description: install right versions of go, docker, kubectl, and also build
-    steps:
-      - run:
-          name: Saving local ip
-          command: |
-            LOCAL_IP=$(ip addr show | grep inet\ | grep -vE 'lo$|docker' | awk '{print $2}' | awk -F '/' '{print $1}')
-            echo "export LOCAL_IP=${LOCAL_IP}" >> ${BASH_ENV}
-            echo "Local IP: ${LOCAL_IP}"
-            source $BASH_ENV
-      - run:
-          name: Export environment variables persistent in execution shell
-          command: |
-            echo 'export KUBECONFIG=/home/circleci/.kube/config' >> $BASH_ENV
-            echo 'export GOROOT=/usr/local/go' >> $BASH_ENV
-            echo 'export GOPATH=$HOME/go' >> $BASH_ENV
-            echo 'export PATH=$GOPATH/bin:$GOROOT/bin:$PATH' >> $BASH_ENV
-            echo "export SKUPPER_CONTROLLER_IMAGE=${LOCAL_IP}:5000/controller" >> $BASH_ENV
-            echo "export SKUPPER_CONFIG_SYNC_IMAGE=${LOCAL_IP}:5000/config-sync" >> $BASH_ENV
-            echo "export SKUPPER_NETWORK_CONSOLE_COLLECTOR_IMAGE=${LOCAL_IP}:5000/network-console-collector" >> $BASH_ENV
-            echo "export SKUPPER_BOOTSTRAP_IMAGE=${LOCAL_IP}:5000/bootstrap" >> $BASH_ENV
-            echo "export TEST_IMAGE=${LOCAL_IP}:5000/skupper-tests" >> $BASH_ENV
-            source $BASH_ENV
-      - checkout
-      - run:
-          name: cleanup previous go installation
-          command: sudo rm -rf /usr/local/go
-      - docker/install-docker
-      - go/install:
-          version: "1.22.8"
-      - kube-orb/install-kubectl
-      - run: make
-
-  local_registry_start:
-    description: prepare a local registry using pre-built images
-    steps:
-      - run:
-          name: defining insecure registry location
-          command: |
-            echo "{\"insecure-registries\": [\"0.0.0.0:5000\", \"${LOCAL_IP}:5000\"]}" | jq > /tmp/daemon.json
-            sudo cp /tmp/daemon.json /etc/docker/
-            sudo systemctl restart docker
-      - run:
-          name: create registry container
-          command: |
-            docker run --name registry -d -p 5000:5000 registry
-      - attach_workspace:
-          at: /tmp/
-      - run:
-          name: load docker images
-          command: |
-            gunzip -c /tmp/images/controller.gz | docker import - 0.0.0.0:5000/controller
-            gunzip -c /tmp/images/controller.gz | docker load
-            gunzip -c /tmp/images/config-sync.gz | docker import - 0.0.0.0:5000/config-sync
-            gunzip -c /tmp/images/config-sync.gz | docker load
-            gunzip -c /tmp/images/network-console-collector.gz | docker import - 0.0.0.0:5000/network-console-collector
-            gunzip -c /tmp/images/network-console-collector.gz | docker load
-            gunzip -c /tmp/images/bootstrap.gz | docker import - 0.0.0.0:5000/bootstrap
-            gunzip -c /tmp/images/bootstrap.gz | docker load
-      - run:
-          name: push to local registry
-          command: |
-            docker push 0.0.0.0:5000/controller
-            docker push 0.0.0.0:5000/config-sync
-            docker push 0.0.0.0:5000/network-console-collector
-            docker push 0.0.0.0:5000/bootstrap
-
-  system_monitor:
-    description: shows continuous system state
-    steps:
-    - run:
-        name: sar info
-        command: sar -h -q -r -u 60
-        background: true
-    - run:
-        name: vmstat info
-        command: vmstat -w -t 10
-        background: true
-    - run:
-        name: journalctl monitoring
-        command: journalctl -p 7 -b -f
-        background: true
-
-  system_status:
-    description: shows some point-in-time system status
-    parameters:
-      cluster_dump_re:
-        default: "(oom|error)"
-        type: string
-    steps:
-    - run:
-        name: point-in-time system status
-        command: |
-          set +e
-          cat /sys/fs/cgroup/memory/memory.max_usage_in_bytes
-          free -m
-          uptime
-          minikube kubectl cluster-info dump | grep -i "<<parameters.cluster_dump_re>>"
-          true
-        when: always
-
-  run_cluster_tests:
-    description: run all e2e tests inside the current KUBECONFIG configured cluster
-    parameters:
-      args:
-        default: ""
-        type: string
-    steps:
-      - system_status # before the tests
-      - run:
-          name: Creating artifacts directory
-          command: mkdir /tmp/artifacts
-      - run:
-          name: Creating results directory
-          command: mkdir /tmp/results
-      - run:
-          name: Run Integration Tests
-          no_output_timeout: 60m
-          command: |
-            make build-cmd && sudo install skupper /usr/local/bin
-            export PUBLIC_1_INGRESS_HOST=`minikube ip`
-            export SKUPPER_TEST_CLI_MAX_STATUS_ATTEMPTS=20
-            go test -tags=integration,podman -timeout=60m -v ./test/integration/... | tee /tmp/results/test-integration.out
-      - run:
-          name: Run client tests in real cluster
-          command: |
-            go test -v -count=1 -timeout=20m ./client -use-cluster | tee /tmp/results/test-client.out
-      - run:
-          name: Run skupper cli tests in real cluster
-          command: |
-            go test -v -count=1 ./cmd/skupper -use-cluster | tee /tmp/results/test-cmd-skupper.out
-      - run:
-          name: Run site-controller tests in real cluster
-          command: |
-            go test -v -count=1 ./cmd/site-controller -use-cluster | tee /tmp/results/test-cmd-site-controller.out
-      - system_status
-      - run:
-          name: Archiving test artifacts
-          command: |
-            find ./ -name "*.tar.gz" -o -name "*.log" -o -name "*.json" | tar -c -T - | tar -C /tmp/artifacts -xv
-          when: always
-      - run:
-          name: Generate junit reports
-          command: |
-            (cd /tmp/results && for f in *.out; do [[ -f $f ]] && cat $f | ~/go/bin/go-junit-report > ${f/.out/.xml}; done)
-          when: always
-      - store_artifacts:
-          path: /tmp/artifacts
-          destination: test-artifacts
-      - store_test_results:
-          path: /tmp/results
-
-  run_cluster_policy_tests:
-    description: run all e2e policy tests inside the current KUBECONFIG configured cluster
-    parameters:
-      args:
-        default: ""
-        type: string
+  skopeo-install:
+    description: Install Skopeo
     steps:
-      - system_status
-      - run:
-          name: Creating policy artifacts directory
-          command: mkdir /tmp/policy-artifacts
-      - run:
-          name: Creating policy results directory
-          command: mkdir /tmp/policy-results
-      - run:
-          name: Run Policy Integration Tests
-          no_output_timeout: 60m
-          command: |
-            make build-cmd && sudo install skupper /usr/local/bin
-            go test -tags=policy -timeout=60m -v ./test/integration/... | tee /tmp/policy-results/test-policy.out
-      - system_status
-      - run:
-          name: Archiving policy test artifacts
-          command: |
-            find ./ -name "*.tar.gz" -o -name "*.log" -o -name "*.json" | tar -c -T - | tar -C /tmp/policy-artifacts -xv
-          when: always
       - run:
-          name: Generate policy junit reports
-          command: |
-            (cd /tmp/policy-results && for f in *.out; do [[ -f $f ]] && cat $f | ~/go/bin/go-junit-report > ${f/.out/.xml}; done)
-          when: always
-      - store_artifacts:
-          path: /tmp/policy-artifacts
-          destination: policy-test-artifacts
-      - store_test_results:
-          path: /tmp/policy-results
+          name: Apt Install Skopeo
+          command: sudo apt-get update -y && sudo apt-get install -y skopeo
 
   compile_go_program:
     description: Compile specified platform.
@@ -296,54 +51,11 @@ commands:
             file dist/<< parameters.platform >>/skupper<< parameters.exesuffix >>;
             go version -m dist/<< parameters.platform >>/skupper<< parameters.exesuffix >>;
 
-  podman-latest:
-    description: "Install latest podman v4 or higher"
-    steps:
-      - run:
-          name: install podman
-          command: |
-            sudo mkdir -p /etc/apt/keyrings
-            curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_$(lsb_release -rs)/Release.key \
-              | gpg --dearmor \
-              | sudo tee /etc/apt/keyrings/devel_kubic_libcontainers_unstable.gpg > /dev/null
-            echo \
-              "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/devel_kubic_libcontainers_unstable.gpg]\
-                https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_$(lsb_release -rs)/ /" \
-              | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list > /dev/null
-            sudo apt-get update -qq
-            sudo apt-get -qq -y install podman
-            podman version
-            
-            # temporary fix for https://github.com/containers/podman/issues/21024
-            wget https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_22.04/amd64/conmon_2.1.2~0_amd64.deb -O /tmp/conmon_2.1.2.deb
-            sudo apt install /tmp/conmon_2.1.2.deb
-
-            # Bypassing CircleCI issue with user session (see: https://github.com/containers/podman/issues/16529)
-            mkdir -p ~/.config/containers/containers.conf.d
-            ( echo '[containers]' ; echo 'cgroups = "disabled"' ) > ~/.config/containers/containers.conf.d/podman-circleci-issue.conf
-
-            # Starting systemd user service
-            systemctl --user start podman.socket
-
-  go-junit:
-    description: "Install go-junit-reporter"
-    steps:
-      - run:
-          name: go junit reporter
-          command: |
-            go install github.com/jstemmer/go-junit-report@v1.0.0
-
 yaml-templates:
-  branch_filters: &run_for_all_branches_and_numeric_tags
+  branch_filters: &run_for_all_branches
     filters:
       tags:
-        only: /[0-9].*/
-
-  main_branch_filters: &run_for_main_branch
-    filters:
-      branches:
         ignore: /.*/
-        only: /main/
 
   v2_branch_filters: &run_for_v2_branch
     filters:
@@ -358,104 +70,91 @@ yaml-templates:
       tags:
         only: /[0-9].*/
 
-  release_requires: &release_requires
-    requires:
-      - build-all
-      - test-skupper-binary
-      - test
-      - main_tests_minikube_local_cluster
-      - policy_tests_minikube_local_cluster
-
   v2_release_requires: &v2_release_requires
     requires:
       - build-all
       - test-skupper-binary
       - test
+      - build-oci-images
 
 workflows:
   version: 2.1
-  build-workflow:
+  release:
     jobs:
       - build-all:
-          <<: *run_for_all_branches_and_numeric_tags
+         <<: *run_for_numeric_tags
       - test-skupper-binary:
-          <<: *run_for_all_branches_and_numeric_tags
+          <<: *run_for_numeric_tags
           matrix:
             parameters:
               image:
                 - quay.io/centos/centos:stream8
                 - quay.io/centos/centos:stream9
                 - quay.io/fedora/fedora:38
-                - quay.io/fedora/fedora:39
+                - quay.io/fedora/fedora:41
+                - docker.io/debian:bookworm
+                - docker.io/alpine:latest
           requires:
             - build-all
       - test:
-          <<: *run_for_all_branches_and_numeric_tags
-      - build_and_save_test_images:
-          <<: *run_for_all_branches_and_numeric_tags
-
-      # - main_tests_minikube_local_cluster:
-      #     <<: *run_for_all_branches_and_numeric_tags
-      #     pre-steps:
-      #       - prepare_for_local_cluster_tests
-      #     requires:
-      #       - test
-      #       - build_and_save_test_images
-
-      # - policy_tests_minikube_local_cluster:
-      #     <<: *run_for_all_branches_and_numeric_tags
-      #     pre-steps:
-      #       - prepare_for_local_cluster_tests
-      #     requires:
-      #       - test
-      #       - build_and_save_test_images
-
-      - publish-github-release-images:
-         <<: *run_for_numeric_tags
-         <<: *v2_release_requires
-         context:
-           - skupper-org
-
-      - generate-manifest:
+          <<: *run_for_numeric_tags
+      - build-oci-images:
+          <<: *run_for_numeric_tags
+          image_tag: << pipeline.git.tag >>
+      - publish-oci-images:
           <<: *run_for_numeric_tags
           <<: *v2_release_requires
-          requires:
-            - publish-github-release-images
+          image_tag: << pipeline.git.tag >>
           context:
             - skupper-org
-
+      - generate-manifest:
+          <<: *run_for_numeric_tags
+          requires:
+            - publish-oci-images
       - publish-github-release-artifacts:
           <<: *run_for_numeric_tags
-          <<: *v2_release_requires
           requires:
+            - publish-oci-images
             - generate-manifest
-            - publish-github-release-images
           context:
             - skupper-org
-
-      # - publish-github-main-artifacts:
-      #     <<: *run_for_main_branch
-      #     requires:
-      #       - publish-github-main-images
-      #     context:
-      #       - skupper-org
-
-      # - publish-github-main-images:
-      #     <<: *run_for_main_branch
-      #     <<: *release_requires
-      #     context:
-      #       - skupper-org
-
-      - publish-github-v2-artifacts:
-          <<: *run_for_v2_branch
+  build:
+    jobs:
+      - build-all:
+          <<: *run_for_all_branches
+      - test-skupper-binary:
+          <<: *run_for_all_branches
+          matrix:
+            parameters:
+              image:
+                - quay.io/centos/centos:stream8
+                - quay.io/centos/centos:stream9
+                - quay.io/fedora/fedora:38
+                - quay.io/fedora/fedora:41
+                - docker.io/debian:bookworm
+                - docker.io/alpine:latest
           requires:
-            - publish-github-v2-latest-images
+            - build-all
+      - test:
+          <<: *run_for_all_branches
+      - build-oci-images:
+          image_tag: v2-latest
+          <<: *run_for_all_branches
+      - publish-oci-images:
+          <<: *run_for_v2_branch
+          <<: *v2_release_requires
+          image_tag: v2-latest
           context:
             - skupper-org
-
-      - publish-github-v2-latest-images:
+      - generate-manifest:
+          requires:
+            - publish-oci-images
+      - publish-github-prerelease-artifacts:
           <<: *run_for_v2_branch
-          <<: *v2_release_requires
+          version: v2-release
+          requires:
+            - publish-oci-images
+            - generate-manifest
           context:
             - skupper-org
 
@@ -489,29 +188,73 @@ jobs:
           path: /tmp/artifacts
           destination: test-artifacts
 
-  build_and_save_test_images:
+  generate-manifest:
+    executor:
+      name: go_cimg
+    steps:
+      - checkout
+      - go/mod-download-cached
+      - setup_remote_docker
+      - run: make generate-manifest
+      - run: mkdir skupper-manifest
+      - run: cp ./manifest.json skupper-manifest
+      - persist_to_workspace:
+          root: .
+          paths:
+            - skupper-manifest
+
+  build-oci-images:
     executor: local_cluster_test_executor
+    parameters:
+      image_tag:
+        type: string
+      registry:
+        type: string
+        default: "quay.io/skupper"
     steps:
       - docker/install-docker
+      - skopeo-install
       - checkout
       - run: docker buildx create --use --name skupper-buildx
-      - run: make -e docker-build
+      - run: make multiarch-oci IMAGE_TAG=<< parameters.image_tag >> REGISTRY=<< parameters.registry >>
       - run:
-          name: persisting images to workspace
+          name: Inspect images
           command: |
-            mkdir /tmp/images
-            docker tag quay.io/skupper/controller:v2-latest 0.0.0.0:5000/controller
-            docker save 0.0.0.0:5000/controller | gzip > /tmp/images/controller.gz
-            docker tag quay.io/skupper/config-sync:v2-latest 0.0.0.0:5000/config-sync
-            docker save 0.0.0.0:5000/config-sync | gzip > /tmp/images/config-sync.gz
-            docker tag quay.io/skupper/network-console-collector:v2-latest 0.0.0.0:5000/network-console-collector
-            docker save 0.0.0.0:5000/network-console-collector | gzip > /tmp/images/network-console-collector.gz
-            docker tag quay.io/skupper/bootstrap:v2-latest 0.0.0.0:5000/bootstrap
-            docker save 0.0.0.0:5000/bootstrap | gzip > /tmp/images/bootstrap.gz
+            mkdir -p oci-digests
+            make describe-multiarch-oci \
+              IMAGE_TAG=<< parameters.image_tag >> \
+              REGISTRY=<< parameters.registry >> \
+              | tee ./oci-digests/digests.yaml
       - persist_to_workspace:
-          root: /tmp
+          root: ./
           paths:
-            - images/
+            - oci-archives/
+            - oci-digests/
+
+  publish-oci-images:
+    executor:
+      name: go_cimg
+    environment:
+      REGISTRY_AUTH_FILE: ~/.skopeoauth.json
+    parameters:
+      image_tag:
+        type: string
+      registry:
+        type: string
+        default: "quay.io/skupper"
+    steps:
+      - skopeo-install
+      - checkout
+      - attach_workspace:
+          at: .
+      - run:
+          name: skopeo login
+          command: |
+            skopeo login quay.io -u ${QUAY_LOGIN} -p ${QUAY_PASSWORD}
+      - run:
+          name: publish images
+          command: |
+            make describe-multiarch-oci push-multiarch-oci REGISTRY="<< parameters.registry >>" IMAGE_TAG="<< parameters.image_tag >>"
 
   build-all:
     executor:
@@ -528,17 +271,6 @@ jobs:
           goarch: amd64
           platform: linux-amd64
 
-      - compile_go_program:
-          goos: linux
-          goarch: "386"
-          platform: linux-i386
-
-      - compile_go_program:
-          goos: windows
-          goarch: "386"
-          platform: windows-i386
-          exesuffix: ".exe"
-
       - compile_go_program:
           goos: windows
           goarch: amd64
@@ -555,11 +287,6 @@ jobs:
           goarch: arm64
           platform: mac-arm64
 
-      - compile_go_program:
-          goos: linux
-          goarch: arm
-          platform: linux-arm32
-
       - compile_go_program:
           goos: linux
           goarch: arm64
@@ -591,50 +318,6 @@ jobs:
             cd dist/linux-amd64
             ./skupper site
 
-  main_tests_minikube_local_cluster:
-    executor: local_cluster_test_executor
-    steps:
-      - run: echo "skupper_image = ${SKUPPER_CONTROLLER_IMAGE}"
-      - local_registry_start
-      - minikube-install
-      - minikube-start-large
-      - minikube-start-load-balancer
-      - podman-latest
-      - go-junit
-      - minikube-logs
-      - system_monitor
-      - run: kubectl cluster-info
-      - run_cluster_tests
-
-  policy_tests_minikube_local_cluster:
-    executor: local_cluster_policy_test_executor
-    steps:
-      - run: echo "skupper_image = ${SKUPPER_CONTROLLER_IMAGE}"
-      - local_registry_start
-      - minikube-install
-      - minikube-start-medium
-      - minikube-start-load-balancer
-      - minikube-logs
-      - go-junit
-      - system_monitor
-      - run: kubectl cluster-info
-      - run_cluster_policy_tests
-
-  generate-manifest:
-    executor:
-      name: go_cimg
-    steps:
-      - checkout
-      - go/mod-download-cached
-      - setup_remote_docker
-      - run: make generate-manifest
-      - run: mkdir skupper-manifest
-      - run: cp ./manifest.json skupper-manifest
-      - persist_to_workspace:
-          root: .
-          paths:
-            - skupper-manifest
-
   publish-github-release-artifacts:
     docker:
       - image: cibuilds/github:0.10
@@ -647,6 +330,7 @@ jobs:
             VERSION="${CIRCLE_TAG}"
             BASEDIR=`pwd`
             mkdir "${BASEDIR}/archives"
+            cp ./oci-digests/* "${BASEDIR}/archives"
             for p in `ls dist` ; do
               cd "$BASEDIR/dist/$p"
               if [[ $p == windows* ]] ; then
@@ -659,98 +343,22 @@ jobs:
             cp "${BASEDIR}/skupper-manifest/manifest.json" "${BASEDIR}/archives"
             ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace -prerelease -draft ${VERSION} "${BASEDIR}/archives"
 
-  publish-github-release-images:
-    executor:
-      name: go_cimg
-    steps:
-      - checkout
-      - setup_remote_docker
-      - run: docker login quay.io -u ${QUAY_LOGIN} -p ${QUAY_PASSWORD}
-      - run:
-          name:
-          command: |
-            echo 'export CONTROLLER_IMAGE=quay.io/skupper/controller:${CIRCLE_TAG}' >> $BASH_ENV
-            echo 'export CONFIG_SYNC_IMAGE=quay.io/skupper/config-sync:${CIRCLE_TAG}' >> $BASH_ENV
-            echo 'export NETWORK_CONSOLE_COLLECTOR_IMAGE=quay.io/skupper/network-console-collector:${CIRCLE_TAG}' >> $BASH_ENV
-            echo 'export BOOTSTRAP_IMAGE=quay.io/skupper/bootstrap:${CIRCLE_TAG}' >> $BASH_ENV
-            source $BASH_ENV
-            docker buildx create --use --name skupper-buildx
-            make -e docker-build
-            make -e docker-push
-
-  publish-github-main-images:
-    executor:
-      name: go_cimg
-    steps:
-      - checkout
-      - setup_remote_docker
-      - run: docker login quay.io -u ${QUAY_LOGIN} -p ${QUAY_PASSWORD}
-      - run:
-          name: "Publishing main images"
-          command: |
-            echo 'export CONTROLLER_IMAGE=quay.io/skupper/controller:main' >> $BASH_ENV
-            echo 'export CONFIG_SYNC_IMAGE=quay.io/skupper/config-sync:main' >> $BASH_ENV
-            echo 'export NETWORK_CONSOLE_COLLECTOR_IMAGE=quay.io/skupper/network-console-collector:main' >> $BASH_ENV
-            echo 'export BOOTSTRAP_IMAGE=quay.io/skupper/bootstrap:main' >> $BASH_ENV
-            source $BASH_ENV
-            docker buildx create --use --name skupper-buildx
-            make -e docker-build
-            make -e docker-push
-
-  publish-github-main-artifacts:
-    docker:
-      - image: cibuilds/github:0.10
-    steps:
-      - attach_workspace:
-          at: .
-      - run:
-          name: "Create a Pre-Release on GitHub"
-          command: |
-            VERSION="main-release"
-            BASEDIR=`pwd`
-            mkdir "${BASEDIR}/archives"
-            for p in `ls dist` ; do
-              cd "$BASEDIR/dist/$p"
-              if [[ $p == windows* ]] ; then
-                zip -q "${BASEDIR}/archives/skupper-cli-${VERSION}-$p.zip" *
-              else
-                tar -zcf "${BASEDIR}/archives/skupper-cli-${VERSION}-$p.tgz" *
-              fi
-            done
-            cd ${BASEDIR}
-            ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace -prerelease ${VERSION} "${BASEDIR}/archives"
-
-  publish-github-v2-latest-images:
-    executor:
-      name: go_cimg
-    steps:
-      - checkout
-      - setup_remote_docker
-      - run: docker login quay.io -u ${QUAY_LOGIN} -p ${QUAY_PASSWORD}
-      - run:
-          name: "Publishing v2 images"
-          command: |
-            echo 'export CONTROLLER_IMAGE=quay.io/skupper/controller:v2-latest' >> $BASH_ENV
-            echo 'export CONFIG_SYNC_IMAGE=quay.io/skupper/config-sync:v2-latest' >> $BASH_ENV
-            echo 'export NETWORK_CONSOLE_COLLECTOR_IMAGE=quay.io/skupper/network-console-collector:v2-latest' >> $BASH_ENV
-            echo 'export BOOTSTRAP_IMAGE=quay.io/skupper/bootstrap:v2-latest' >> $BASH_ENV
-            source $BASH_ENV
-            docker buildx create --use --name skupper-buildx
-            make -e docker-build
-            make -e docker-push
-
-  publish-github-v2-artifacts:
+  publish-github-prerelease-artifacts:
     docker:
       - image: cibuilds/github:0.10
+    parameters:
+      version:
+        type: string
     steps:
       - attach_workspace:
           at: .
       - run:
           name: "Create a V2 Pre-Release on GitHub"
           command: |
-            VERSION="v2-release"
+            VERSION="<< parameters.version >>"
             BASEDIR=`pwd`
             mkdir "${BASEDIR}/archives"
+            cp ./oci-digests/* "${BASEDIR}/archives"
             for p in `ls dist` ; do
               cd "$BASEDIR/dist/$p"
               if [[ $p == windows* ]] ; then
@@ -760,4 +368,5 @@ jobs:
               fi
             done
             cd ${BASEDIR}
+            cp "${BASEDIR}/skupper-manifest/manifest.json" "${BASEDIR}/archives"
             ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace -prerelease ${VERSION} "${BASEDIR}/archives"
diff --git a/.gitignore b/.gitignore
index dc689ba5..8bb8a660 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
 /generate-doc
 /bootstrap
 /controller
+/network-console-collector
 
 # Test binary, built with `go test -c`
 *.test
@@ -28,3 +29,4 @@
 
 # Generated files
 manifest.json
+oci-archives/
diff --git a/Dockerfile.bootstrap b/Dockerfile.bootstrap
index f1e1f547..0887295f 100644
--- a/Dockerfile.bootstrap
+++ b/Dockerfile.bootstrap
@@ -1,6 +1,5 @@
 FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
 
-ARG TARGETOS
 ARG TARGETARCH
 
 WORKDIR /go/src/app
@@ -9,9 +8,13 @@ COPY go.sum .
 RUN go mod download
 COPY . .
 
-RUN make build-bootstrap GOOS=$TARGETOS GOARCH=$TARGETARCH
+ENV CGO_ENABLED=0
+RUN make build-bootstrap GOARCH=$TARGETARCH
 
 FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9-minimal
+LABEL \
+  org.opencontainers.image.title="Skupper Bootstrap" \
+  org.opencontainers.image.description="Produces and manges static configurations for non-kube installations of Skupper"
 
 # Create user and group and switch to user's context
 RUN microdnf -y install shadow-utils \
diff --git a/Dockerfile.config-sync b/Dockerfile.config-sync
index ae863a7e..f678aa6e 100644
--- a/Dockerfile.config-sync
+++ b/Dockerfile.config-sync
@@ -1,6 +1,5 @@
 FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
 
-ARG TARGETOS
 ARG TARGETARCH
 
 WORKDIR /go/src/app
@@ -10,9 +9,13 @@ COPY go.sum .
 RUN go mod download
 
 COPY . .
-RUN make GOOS=$TARGETOS GOARCH=$TARGETARCH
+ENV CGO_ENABLED=0
+RUN make GOARCH=$TARGETARCH build-config-sync
 
 FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9-minimal
+LABEL \
+  org.opencontainers.image.title="Skupper Router Adaptor" \
+  org.opencontainers.image.description="Kubernetes aware interface for the skupper-router"
 
 # Create user and group and switch to user's context
 RUN microdnf -y install shadow-utils \
diff --git a/Dockerfile.controller b/Dockerfile.controller
index 1d41b421..e5929f19 100644
--- a/Dockerfile.controller
+++ b/Dockerfile.controller
@@ -1,6 +1,5 @@
 FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
 
-ARG TARGETOS
 ARG TARGETARCH
 
 WORKDIR /go/src/app
@@ -9,9 +8,13 @@ COPY go.sum .
 RUN go mod download
 COPY . .
 
-RUN make build-controller GOOS=$TARGETOS GOARCH=$TARGETARCH
+ENV CGO_ENABLED=0
+RUN make build-controller GOARCH=$TARGETARCH
 
 FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9-minimal
+LABEL \
+  org.opencontainers.image.title="Skupper Controller" \
+  org.opencontainers.image.description="Kubernetes controller for operating skupper networks"
 
 # Create user and group and switch to user's context
 RUN microdnf -y install shadow-utils \
diff --git a/Dockerfile.network-console-collector b/Dockerfile.network-console-collector
index 7b4a0c95..1ed9c64a 100644
--- a/Dockerfile.network-console-collector
+++ b/Dockerfile.network-console-collector
@@ -1,6 +1,5 @@
 FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
 
-ARG TARGETOS
 ARG TARGETARCH
 
 WORKDIR /go/src/app
@@ -11,9 +10,13 @@ RUN go mod download
 
 COPY . .
 
-RUN make GOOS=$TARGETOS GOARCH=$TARGETARCH build-network-console-collector
+ENV CGO_ENABLED=0
+RUN make GOARCH=$TARGETARCH build-network-console-collector
 
 FROM --platform=$BUILDPLATFORM node:18.18.2 AS console-builder
+LABEL \
+  org.opencontainers.image.title="Skupper Network Console Collector" \
+  org.opencontainers.image.description="Exposes Skupper network details thorugh an API, metrics and a web console"
 
 WORKDIR /skupper-console/
 ADD https://github.com/skupperproject/skupper-console/archive/v2.tar.gz .
diff --git a/Makefile b/Makefile
index b15498a7..87911363 100644
--- a/Makefile
+++ b/Makefile
@@ -1,27 +1,31 @@
 VERSION := $(shell git describe --tags --dirty=-modified --always)
-CONTROLLER_IMAGE := quay.io/skupper/controller:v2-latest
-BOOTSTRAP_IMAGE := quay.io/skupper/bootstrap:v2-latest
-CONFIG_SYNC_IMAGE := quay.io/skupper/config-sync:v2-latest
-NETWORK_CONSOLE_COLLECTOR_IMAGE := quay.io/skupper/network-console-collector:v2-latest
-TEST_IMAGE := quay.io/skupper/skupper-tests:v2-latest
-TEST_BINARIES_FOLDER := ${PWD}/test/integration/bin
-DOCKER := docker
-LDFLAGS := -X github.com/skupperproject/skupper/pkg/version.Version=${VERSION}
+REVISION := $(shell git rev-parse HEAD)
+
+LDFLAGS_EXTRA ?= -s -w # default to building stripped executables
+LDFLAGS := ${LDFLAGS_EXTRA} -X github.com/skupperproject/skupper/pkg/version.Version=${VERSION}
 TESTFLAGS := -v -race -short
-PLATFORMS ?= linux/amd64,linux/arm64
 GOOS ?= linux
 GOARCH ?= amd64
 
-all: build-cmd build-config-sync build-controller build-bootstrap build-tests build-manifest build-network-console-collector update-helm-crd
+REGISTRY := quay.io/skupper
+IMAGE_TAG := v2-latest
+PLATFORMS ?= linux/amd64,linux/arm64
+CONTAINERFILES := Dockerfile.bootstrap Dockerfile.config-sync Dockerfile.controller Dockerfile.network-console-collector
+SHARED_IMAGE_LABELS = \
+    --label "org.opencontainers.image.created=$(shell TZ=GMT date --iso-8601=seconds)" \
+	--label "org.opencontainers.image.url=https://skupper.io/" \
+	--label "org.opencontainers.image.documentation=https://skupper.io/" \
+	--label "org.opencontainers.image.source=https://github.com/skupperproject/skupper" \
+	--label "org.opencontainers.image.version=${VERSION}" \
+	--label "org.opencontainers.image.revision=${REVISION}" \
+	--label "org.opencontainers.image.licenses=Apache-2.0"
+
+
+DOCKER := docker
+SKOPEO := skopeo
+PODMAN := podman
 
-build-tests:
-	mkdir -p ${TEST_BINARIES_FOLDER}
-#	GOOS=${GOOS} GOARCH=${GOARCH} go test -c -tags=job -v ./test/integration/examples/tcp_echo/job -o ${TEST_BINARIES_FOLDER}/tcp_echo_test
-#	GOOS=${GOOS} GOARCH=${GOARCH} go test -c -tags=job -v ./test/integration/examples/http/job -o ${TEST_BINARIES_FOLDER}/http_test
-#	GOOS=${GOOS} GOARCH=${GOARCH} go test -c -tags=job -v ./test/integration/examples/bookinfo/job -o ${TEST_BINARIES_FOLDER}/bookinfo_test
-#	GOOS=${GOOS} GOARCH=${GOARCH} go test -c -tags=job -v ./test/integration/examples/mongodb/job -o ${TEST_BINARIES_FOLDER}/mongo_test
-#	GOOS=${GOOS} GOARCH=${GOARCH} go test -c -tags=job -v ./test/integration/examples/custom/hipstershop/job -o ${TEST_BINARIES_FOLDER}/grpcclient_test
-#	GOOS=${GOOS} GOARCH=${GOARCH} go test -c -tags=job -v ./test/integration/examples/tls_t/job -o ${TEST_BINARIES_FOLDER}/tls_test
+all: build-cmd build-config-sync build-controller build-bootstrap build-manifest build-network-console-collector update-helm-crd
 
 build-cmd:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o skupper ./cmd/skupper
@@ -30,10 +34,10 @@ build-bootstrap:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o bootstrap ./cmd/bootstrap
 
 build-controller:
-	go build -ldflags="${LDFLAGS}"  -o controller cmd/controller/main.go cmd/controller/controller.go
+	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o controller ./cmd/controller
 
 build-config-sync:
-	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o config-sync cmd/config-sync/main.go cmd/config-sync/config_sync.go cmd/config-sync/collector.go
+	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o config-sync ./cmd/config-sync
 
 build-network-console-collector:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o network-console-collector ./cmd/network-console-collector
@@ -44,36 +48,50 @@ build-manifest:
 build-doc-generator:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o generate-doc ./internal/cmd/generate-doc
 
-docker-build-test-image:
-	${DOCKER} buildx build --platform ${PLATFORMS} -t ${TEST_IMAGE} -f Dockerfile.ci-test .
-	${DOCKER} buildx build --load -t ${TEST_IMAGE} -f Dockerfile.ci-test .
-
-docker-build: docker-build-test-image docker-build-bootstrap docker-build-network-console-collector
-	${DOCKER} buildx build --platform ${PLATFORMS} -t ${CONTROLLER_IMAGE} -f Dockerfile.controller .
-	${DOCKER} buildx build --load  -t ${CONTROLLER_IMAGE} -f Dockerfile.controller .
-	${DOCKER} buildx build --platform ${PLATFORMS} -t ${CONFIG_SYNC_IMAGE} -f Dockerfile.config-sync .
-	${DOCKER} buildx build --load  -t ${CONFIG_SYNC_IMAGE} -f Dockerfile.config-sync .
-
-docker-build-bootstrap:
-	${DOCKER} buildx build --platform ${PLATFORMS} -t ${BOOTSTRAP_IMAGE} -f Dockerfile.bootstrap .
-	${DOCKER} buildx build --load  -t ${BOOTSTRAP_IMAGE} -f Dockerfile.bootstrap .
-
-docker-push-bootstrap:
-	${DOCKER} buildx build --push --platform ${PLATFORMS} -t ${BOOTSTRAP_IMAGE} -f Dockerfile.bootstrap .
-
-docker-build-network-console-collector:
-	${DOCKER} buildx build --platform ${PLATFORMS} -t ${NETWORK_CONSOLE_COLLECTOR_IMAGE} -f Dockerfile.network-console-collector .
-	${DOCKER} buildx build --load  -t ${NETWORK_CONSOLE_COLLECTOR_IMAGE} -f Dockerfile.network-console-collector .
-
-docker-push-network-console-collector:
-	${DOCKER} buildx build --push --platform ${PLATFORMS} -t ${NETWORK_CONSOLE_COLLECTOR_IMAGE} -f Dockerfile.network-console-collector .
-
-docker-push-test-image:
-	${DOCKER} buildx build --push --platform ${PLATFORMS} -t ${TEST_IMAGE} -f Dockerfile.ci-test .
-
-docker-push: docker-push-test-image docker-push-bootstrap docker-push-network-console-collector
-	${DOCKER} buildx build --push --platform ${PLATFORMS} -t ${CONFIG_SYNC_IMAGE} -f Dockerfile.config-sync .
-	${DOCKER} buildx build --push --platform ${PLATFORMS} -t ${CONTROLLER_IMAGE} -f Dockerfile.controller .
+## native/default container image builds
+docker-build: $(patsubst Dockerfile.%,docker-build-%,$(CONTAINERFILES))
+docker-build-%: Dockerfile.%
+	${DOCKER} build $(SHARED_IMAGE_LABELS) -t "${REGISTRY}/$*:${IMAGE_TAG}" -f $< .
+
+podman-build: $(patsubst Dockerfile.%,podman-build-%,$(CONTAINERFILES))
+podman-build-%: Dockerfile.%
+	${PODMAN} build $(SHARED_IMAGE_LABELS) -t "${REGISTRY}/$*:${IMAGE_TAG}" -f $< .
+
+
+## multi-platform container images built in docker buildkit builder and
+# exported to oci archive format.
+multiarch-oci: $(patsubst Dockerfile.%,multiarch-oci-%,$(CONTAINERFILES))
+multiarch-oci-%: Dockerfile.% oci-archives
+	${DOCKER} buildx build \
+		"--output=type=oci,dest=$(shell pwd)/oci-archives/$*.tar" \
+		-t "${REGISTRY}/$*:${IMAGE_TAG}" \
+		$(SHARED_IMAGE_LABELS) \
+		--platform ${PLATFORMS} \
+		-f $< .
+
+## push multiarch-oci images to a registry using skopeo
+push-multiarch-oci: $(patsubst Dockerfile.%,push-multiarch-oci-%,$(CONTAINERFILES))
+push-multiarch-oci-%: ./oci-archives/%.tar
+	${SKOPEO} copy --all \
+		oci-archive:$< \
+		"docker://${REGISTRY}/$*:${IMAGE_TAG}"
+
+## Load images from oci-archive into local image storage
+podman-load-oci:
+	for archive in ./oci-archives/*.tar; do ${PODMAN} load < "$$archive"; done
+## Has unfortunate podman dependency; docker image load does not load OCI archives, while podman does.
+docker-load-oci:
+	for archive in ./oci-archives/*.tar; do \
+		img=$$(${PODMAN} load -q < "$$archive" | awk -F": " '{print $$2}') \
+		&& ${PODMAN} image save "$$img" | ${DOCKER} load; \
+	done
+
+## Print fully qualified image names by arch
+describe-multiarch-oci:
+	@scripts/oci-index-archive-info.sh amd64 arm64
+
+oci-archives:
+	mkdir -p oci-archives
 
 format:
 	go fmt ./...
@@ -98,46 +116,16 @@ cover:
 		-coverprofile cover.out \
 		./...
 
-clean:
-	rm -rf skupper controller release config-sync manifest bootstrap network-console-collector ${TEST_BINARIES_FOLDER}
-
-package: release/windows.zip release/darwin.zip release/linux.tgz release/s390x.tgz release/arm64.tgz
-
-release/linux.tgz: release/linux/skupper
-	tar -czf release/linux.tgz -C release/linux/ skupper
-
-release/linux/skupper: cmd/skupper/skupper.go
-	GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o release/linux/skupper ./cmd/skupper
-
-release/windows/skupper: cmd/skupper/skupper.go
-	GOOS=windows GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o release/windows/skupper ./cmd/skupper
-
-release/windows.zip: release/windows/skupper
-	zip -j release/windows.zip release/windows/skupper
-
-release/darwin/skupper: cmd/skupper/skupper.go
-	GOOS=darwin GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o release/darwin/skupper ./cmd/skupper
-
-release/darwin.zip: release/darwin/skupper
-	zip -j release/darwin.zip release/darwin/skupper
-
 generate-manifest: build-manifest
 	./manifest
 
 generate-doc: build-doc-generator
 	./generate-doc ./doc/cli
 
-release/s390x/skupper: cmd/skupper/skupper.go
-	GOOS=linux GOARCH=s390x go build -ldflags="${LDFLAGS}" -o release/s390x/skupper ./cmd/skupper
-
-release/s390x.tgz: release/s390x/skupper
-	tar -czf release/s390x.tgz release/s390x/skupper
-
-release/arm64/skupper: cmd/skupper/skupper.go
-	GOOS=linux GOARCH=arm64 go build -ldflags="${LDFLAGS}" -o release/arm64/skupper ./cmd/skupper
-
-release/arm64.tgz: release/arm64/skupper
-	tar -czf release/arm64.tgz release/arm64/skupper
-
 update-helm-crd:
 	./scripts/update-helm-crds.sh
+
+clean:
+	rm -rf skupper controller config-sync manifest \
+		bootstrap network-console-collector generate-doc \
+		cover.out oci-archives
diff --git a/api/types/crds/skupper_connector_crd.yaml b/api/types/crds/skupper_connector_crd.yaml
index d7dd190d..0e95beff 100644
--- a/api/types/crds/skupper_connector_crd.yaml
+++ b/api/types/crds/skupper_connector_crd.yaml
@@ -25,6 +25,10 @@ spec:
                   type: string
                 tlsCredentials:
                   type: string
+                noClientAuth:
+                  type: boolean
+                verifyHostname:
+                  type: boolean
                 type:
                   type: string
                 includeNotReady:
diff --git a/cmd/bootstrap/PROVIDED_CERTIFICATES.md b/cmd/bootstrap/PROVIDED_CERTIFICATES.md
index 77f2a262..60a0c35e 100644
--- a/cmd/bootstrap/PROVIDED_CERTIFICATES.md
+++ b/cmd/bootstrap/PROVIDED_CERTIFICATES.md
@@ -13,32 +13,32 @@ The certificates generated by Skupper are stored under the namespace home direct
 file system, using the following tree:
 
 ```shell
-certificates/
+runtime/certs/
 ├── ca
 ├── client
 └── server
 ```
 
-As the CA `skupper-site-ca` is generated, it will be stored under the `certificates/ca` path
+As the CA `skupper-site-ca` is generated, it will be stored under the `runtime/certs/ca` path
 as a directory named `skupper-site-ca`. Inside a certificate folder, the following files are
 expected: `tls.key`, `tls.crt` and `ca.crt` (when dealing with a CA, `ca.crt` and `tls.crt`
 have the same content).
 
 If a `RouterAccess` is defined as part of the non kube site definition, Skupper will also
 generate a server certificate that is valid for the provided `RouterAccess.spec.bindHost` and
-for each entry in the  `RouterAccess.spec.subjectAlternativeName` list. The respective certificate
+for each entry in the `RouterAccess.spec.subjectAlternativeName` list. The respective certificate
 is signed by the CA mentioned earlier (skupper-site-ca) and it will be named according to the
 provided `spec.tlsCredentials` field, or when it is omitted, the certificate will be named as the
-RouterAccess and stored as a directory under `certificates/server`.
+RouterAccess and stored as a directory under `runtime/certs/server`.
 
 A set of static links are also created for the provided `RouterAccess.spec.bindHost` and for each
-entry in the  `RouterAccess.spec.subjectAlternativeName` list as separate YAML files.
+entry in the `RouterAccess.spec.subjectAlternativeName` list as separate YAML files.
 
 These static links are composed by a `Link` and a `Secret`, which is basically the client certificate
 signed by `skupper-site-ca`, that will be used by other sites to establish Skupper links. The client
 certificate is also named based on the RouterAccess `spec.tlsCredentials` field or, when it is omitted,
 the certificate will be named as the RouterAccess, prefixed with `client-` and stored under
-`certificates/client`.
+`runtime/certs/client`.
 
 After a site bundle has been produced, it contains the whole site definition that can be installed
 at a remote location. Thus, all certificates and static links are already part of the bundle and no
@@ -49,7 +49,7 @@ new certificate is signed at the moment a bundle is installed.
 You can provide your own certificates to be used by Skupper for site linking.
 
 The provided certificates must be placed in a similar directory tree, as shown before,
-but its path under the namespace home is located at `input/certificates` instead.
+but its path under the namespace home is located at `input/certs` instead.
 
 Certificates placed under this path will be used by Skupper primarily and
 all the other certificates expected by Skupper, will be generated when not provided. 
@@ -86,15 +86,15 @@ provide them at any time, for example:
 
 During "Local site initialization time" or "Site bundle preparation time", Skupper will detect
 that a server certificate has been provided and will inspect it to determine its subject
-alternative names and based on that, a set of static links will be created, allowing those
-links to be distributed to target sites accordingly. For the static links to remain valid,
-the expected client certificate must also be provided.
+alternative names (SAN). A static link file will be created for each SAN, allowing those links
+to be distributed to target sites accordingly. For the static links to remain valid, the expected
+client certificate must also be provided.
 
 If a server certificate is provided at "Site bundle installation time", Skupper will also try to
 determine its subject alternative names using the `openssl` binary (only if available) and it will
-use it to generate the static links for the bundle installation. This way, the set of static links
-available for an installed bundle will be valid for all expected target hostnames and IP addresses
-defined through the server certificate.
+use it to generate the static link files during the bundle installation, or when extracting static links
+from the site bundle. This way, the set of static links available for an installed bundle will be valid for
+all expected target hostnames and IP addresses defined through the server certificate.
 
 Again, if a server certificate is provided, the respective client certificate is also expected
 so that the static links have valid client credentials.
@@ -108,7 +108,7 @@ certificates used for site linking, you can simply create the following structur
 the namespace home of your choice, for example:
 
 ```shell
-${HOME}/.local/share/skupper/namespaces/default/input/certificates/
+${HOME}/.local/share/skupper/namespaces/default/input/certs/
 └── ca
     └── skupper-site-ca
         ├── ca.crt
@@ -137,7 +137,7 @@ field is omitted) is `my-router-access`, then the following structure, for serve
 must be provided under the namespace home of your choice, for example:
 
 ```shell
-${HOME}/.local/share/skupper/namespaces/default/input/certificates/
+${HOME}/.local/share/skupper/namespaces/default/input/certs/
 ├── client
 │   └── client-my-router-access
 │       ├── ca.crt
@@ -166,7 +166,7 @@ If the following domain name is not defined as being the `spec.bindHost` or as p
 link that uses `my.local.server.com` as the target endpoint at:
 
 ```shell
-$HOME/.local/share/skupper/namespaces/default/runtime/link/link-my-router-access-my.local.server.com.yaml
+$HOME/.local/share/skupper/namespaces/default/runtime/links/link-my-router-access-my.local.server.com.yaml
 ```
 
 If the respective server certificates are defined at bundle installation time, Skupper will also inspect
diff --git a/cmd/bootstrap/README.md b/cmd/bootstrap/README.md
index 33244c3e..9c300249 100644
--- a/cmd/bootstrap/README.md
+++ b/cmd/bootstrap/README.md
@@ -2,7 +2,7 @@
 
 In this current phase of the Skupper V2 implementation, non-kubernetes sites
 can be bootstrapped using a locally built binary, that can be produced by running
-`make build-bootstrap`, or using the quay.io/skupper/bootstrap:v2-latest
+`make build-bootstrap`, using the quay.io/skupper/bootstrap:v2-latest
 container image by calling `./cmd/bootstrap/bootstrap.sh` with the appropriate
 flags.
 
@@ -71,7 +71,7 @@ ${HOME}/.local/share/skupper/namespaces when it is not set.
 As the root user, namespaces are stored under: /var/lib/skupper/namespaces.
 
 In case the path (-p) flag is omitted, Skupper will try to process
-custom resources stored at the sources directory of the default namespace,
+custom resources stored at the input/resources directory of the default namespace,
 or from the namespace provided through the namespace (-n) flag.
 
 If the respective namespace already exists and you want to bootstrap it
@@ -158,7 +158,7 @@ information like:
 
 * Location where static links have been defined (when a `RouterAccess` is provided)
 * Location where the site bundle has been saved (if bundle strategy flag `-b` set)
-* Namespace, Site name, Platform, Version and path to the sources
+* Namespace, Site name, Platform, Version and path to the resources
 
 #### Site bundles
 
@@ -257,7 +257,7 @@ The `west` site will be defined using the following CRs:
 
 ##### Site
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Site
 metadata:
   name: west
@@ -266,7 +266,7 @@ metadata:
 ##### Listener
 
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Listener
 metadata:
   name: backend
@@ -279,7 +279,7 @@ spec:
 ##### RouterAccess
 
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: RouterAccess
 metadata:
   name: go-west
@@ -308,7 +308,7 @@ Now that the `west` site has been created, copy the generated link into
 the `east` site local directory. Example:
 
 ```shell
-cp ${HOME}/.local/share/skupper/namespaces/west/runtime/link/link-go-west.yaml ./east/link-go-west.yaml
+cp ${HOME}/.local/share/skupper/namespaces/west/runtime/links/link-go-west-127.0.0.1.yaml ./east/link-go-west.yaml
 ```
 
 #### East
@@ -317,7 +317,7 @@ The `east` site will be defined using the following CRs:
 
 **Site**
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Site
 metadata:
   name: east
@@ -326,7 +326,7 @@ metadata:
 **Connector**
 ```yaml
 ---
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Connector
 metadata:
   name: backend
@@ -349,7 +349,7 @@ kind: Secret
 metadata:
   name: link-go-west
 ---
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Link
 metadata:
   name: link-go-west
@@ -381,7 +381,7 @@ in your browser and it should work.
 ### Updating an existing installation
 
 Suppose modifications have been made to the `west` site CRs, directly at the
-namespace directory (i.e: ${HOME}/.local/share/skupper/namespaces/west/input/sources).
+namespace directory (i.e: ${HOME}/.local/share/skupper/namespaces/west/input/resources).
 
 To re-initialize the west site, run:
 
@@ -393,13 +393,6 @@ The command above will reprocess all source CRs from the namespace path and
 restart the related components. The Certificate Authorities (CAs) are preserved,
 therefore eventual incoming links must still work.
 
-In case the CRs are located somewhere else, then a path must also be specified, as
-in the example below:
-
-```shell
-bootstrap -n west -p <path> -f
-```
-
 ### Cleanup
 
 To remove both namespaces, run:
@@ -421,22 +414,22 @@ Skupper nonkube bootstrap (version: main-release-161-g7c5100a2-modified)
 Site "west" has been created (as a distributable bundle)
 Installation bundle available at: /home/user/.local/share/skupper/bundles/skupper-install-west.sh
 Default namespace: default
-Default platform: docker
+Default platform: podman
 ```
 
-#### Extracting the static link
+#### Extracting the static links
 
-To extract the link, run:
+To extract the links, run:
 
 ```shell
 /home/user/.local/share/skupper/bundles/skupper-install-west.sh -d /tmp
 Static links for site "west" have been saved into /tmp/west
 ```
 
-Copy the static Link to the `east` site definition.
+Copy the static link to the `east` site definition.
 
 ```shell
-cp /tmp/west/link-go-west.yaml ./east/link-go-west.yaml
+cp /tmp/west/link-go-west-127.0.0.1.yaml ./east/link-go-west.yaml
 ```
 
 #### Creating the east bundle
diff --git a/cmd/bootstrap/bootstrap.go b/cmd/bootstrap/bootstrap.go
index 3116f9c1..a0b2b803 100644
--- a/cmd/bootstrap/bootstrap.go
+++ b/cmd/bootstrap/bootstrap.go
@@ -49,7 +49,7 @@ ${HOME}/.local/share/skupper/namespaces when it is not set.
 As the root user, namespaces are stored under: /var/lib/skupper/namespaces.
 
 In case the path (-p) flag is omitted, Skupper will try to process
-custom resources stored at the sources directory of the default namespace,
+custom resources stored at the input/resources directory of the default namespace,
 or from the namespace provided through the namespace (-n) flag.
 
 If the respective namespace already exists and you want to bootstrap it
@@ -161,20 +161,20 @@ func main() {
 		// when input path is empty, but a namespace is provided, try to reload an existing site definition
 		if inputSourcesDefined {
 			inputPath = existingPath
-			fmt.Printf("Sources will consumed from namespace %q\n", namespace)
+			fmt.Printf("Resources will consumed from namespace %q\n", namespace)
 		} else {
 			fmt.Printf("Input path has not been provided and namespace %s does not exist\n", namespace)
-			fmt.Printf("No sources found at: %s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
+			fmt.Printf("No resources found at: %s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
 			os.Exit(1)
 		}
 	} else if inputSourcesDefined {
-		fmt.Printf("Input path has been provided, but namespace %s has input sources defined at:\n", namespace)
+		fmt.Printf("Input path has been provided, but namespace %s has input resources defined at:\n", namespace)
 		fmt.Printf("%s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
 		os.Exit(1)
 	}
 
 	// if namespace already exists, fail if force is not set
-	_, err := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimeSiteStatePath))
+	_, err := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimePath))
 	if !isBundle && err == nil && !*force {
 		fmt.Printf("Namespace already exists: %s\n", namespace)
 		os.Exit(1)
@@ -239,7 +239,7 @@ func bootstrap(inputPath string, namespace string, bundleStrategy string) (*api.
 		nsPlatformLoader := &common.NamespacePlatformLoader{}
 		nsPlatform, err := nsPlatformLoader.Load(namespace)
 		if err != nil {
-			_, runtimeStateErr := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimeSiteStatePath))
+			_, runtimeStateErr := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimePath))
 			if runtimeStateErr == nil {
 				return nil, fmt.Errorf("unable to determine current platform used in namespace %q", namespace)
 			}
@@ -268,7 +268,7 @@ func bootstrap(inputPath string, namespace string, bundleStrategy string) (*api.
 	targetNamespace := utils.DefaultStr(namespace, "default")
 	if inputPath == sourcesPath {
 		if crNamespace != targetNamespace {
-			return nil, fmt.Errorf("namespace must be %q, but sources are defined using %q", targetNamespace, crNamespace)
+			return nil, fmt.Errorf("namespace must be %q, but resources are defined using %q", targetNamespace, crNamespace)
 		}
 	} else if namespace != "" {
 		siteState.SetNamespace(namespace)
diff --git a/cmd/bootstrap/bootstrap.sh b/cmd/bootstrap/bootstrap.sh
index 07d7aa6e..4f90a11b 100755
--- a/cmd/bootstrap/bootstrap.sh
+++ b/cmd/bootstrap/bootstrap.sh
@@ -107,7 +107,7 @@ create_service() {
         return
     fi
     service_name="skupper-${namespace}.service"
-    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${namespace}/runtime/scripts/${service_name}"
+    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${namespace}/internal/scripts/${service_name}"
     if [ ! -f "${service_file}" ]; then
         echo "SystemD service has not been defined"
         return
diff --git a/cmd/bootstrap/remove.sh b/cmd/bootstrap/remove.sh
index 0874deea..f40e9ef7 100755
--- a/cmd/bootstrap/remove.sh
+++ b/cmd/bootstrap/remove.sh
@@ -26,7 +26,7 @@ usage() {
 }
 
 remove_definition() {
-    platform_file="${namespaces_path}/${namespace}/runtime/state/platform.yaml"
+    platform_file="${namespaces_path}/${namespace}/runtime/platform.yaml"
     if [ -f "${platform_file}" ]; then
         SKUPPER_PLATFORM=$(grep '^platform: ' "${platform_file}" | sed -e 's/.*: //g')
         if [ "${SKUPPER_PLATFORM}" = "podman" ] || [ "${SKUPPER_PLATFORM}" = "docker" ]; then
diff --git a/deployments/helm/crds/skupper_connector_crd.yaml b/deployments/helm/crds/skupper_connector_crd.yaml
index d7dd190d..0e95beff 100644
--- a/deployments/helm/crds/skupper_connector_crd.yaml
+++ b/deployments/helm/crds/skupper_connector_crd.yaml
@@ -25,6 +25,10 @@ spec:
                   type: string
                 tlsCredentials:
                   type: string
+                noClientAuth:
+                  type: boolean
+                verifyHostname:
+                  type: boolean
                 type:
                   type: string
                 includeNotReady:
diff --git a/internal/cmd/skupper/common/constants.go b/internal/cmd/skupper/common/constants.go
index 4e9e31e7..06f77244 100644
--- a/internal/cmd/skupper/common/constants.go
+++ b/internal/cmd/skupper/common/constants.go
@@ -7,3 +7,10 @@ var (
 	ConnectorTypes  = []string{"tcp"}
 	WorkloadTypes   = []string{"deployment", "service", "daemonset", "statefulset"}
 )
+
+const (
+	Connectors     string = "connectors"
+	Listeners      string = "listeners"
+	Sites          string = "sites"
+	RouterAccesses string = "routerAccesses"
+)
diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go
index 33befd22..97e8cfe8 100644
--- a/internal/cmd/skupper/common/flags.go
+++ b/internal/cmd/skupper/common/flags.go
@@ -189,3 +189,10 @@ type CommandListenerStatusFlags struct {
 type CommandListenerDeleteFlags struct {
 	Timeout time.Duration
 }
+
+type CommandVersionFlags struct {
+	Output string
+}
+
+type CommandDebugFlags struct {
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_update.go b/internal/cmd/skupper/connector/kube/connector_update.go
index 7eff05f4..62b6147b 100644
--- a/internal/cmd/skupper/connector/kube/connector_update.go
+++ b/internal/cmd/skupper/connector/kube/connector_update.go
@@ -99,7 +99,6 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 			cmd.newSettings.connectorType = connector.Spec.Type
 			cmd.newSettings.includeNotReady = connector.Spec.IncludeNotReady
 			cmd.newSettings.routingKey = connector.Spec.RoutingKey
-
 			cmd.existingHost = connector.Spec.Host
 			cmd.existingSelector = connector.Spec.Selector
 		}
diff --git a/internal/cmd/skupper/connector/nonkube/connector_create.go b/internal/cmd/skupper/connector/nonkube/connector_create.go
index 774d3c33..f3dcd4bc 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_create.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_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 CmdConnectorCreate struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandConnectorCreateFlags
-	Namespace string
-	siteName  string
+	connectorHandler *fs.ConnectorHandler
+	CobraCmd         *cobra.Command
+	Flags            *common.CommandConnectorCreateFlags
+	namespace        string
+	connectorName    string
+	port             int
+	output           string
+	host             string
+	routingKey       string
+	connectorType    string
+	tlsSecret        string
 }
 
 func NewCmdConnectorCreate() *CmdConnectorCreate {
@@ -18,12 +33,139 @@ func NewCmdConnectorCreate() *CmdConnectorCreate {
 }
 
 func (cmd *CmdConnectorCreate) 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.connectorHandler = fs.NewConnectorHandler(cmd.namespace)
+}
+
+func (cmd *CmdConnectorCreate) 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()
+	connectorTypeValidator := validator.NewOptionValidator(common.ConnectorTypes)
+	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
+	hostStringValidator := validator.NewHostStringValidator()
+
+	// Validate arguments name and port
+	if len(args) < 2 {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name and port must be configured"))
+	} else if len(args) > 2 {
+		validationErrors = append(validationErrors, fmt.Errorf("only two arguments are allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must not be empty"))
+	} else if args[1] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector port must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+		} else {
+			cmd.connectorName = args[0]
+		}
+		cmd.port, err = strconv.Atoi(args[1])
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("connector port is not valid: %s", err))
+		}
+		ok, err = numberValidator.Evaluate(cmd.port)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector port is not valid: %s", err))
+		}
+	}
+
+	// Validate 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.ConnectorType != "" {
+		ok, err := connectorTypeValidator.Evaluate(cmd.Flags.ConnectorType)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector type is not valid: %s", err))
+		}
+	}
+	if cmd.Flags.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.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 {
+		validationErrors = append(validationErrors, fmt.Errorf("host name must be configured: an IP address or hostname is expected"))
+	}
+	if cmd.Flags.TlsSecret != "" {
+		// TBD what is valid TlsSecret
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdConnectorCreate) InputToOptions() {
+	// default routingkey to name of connector
+	if cmd.Flags.RoutingKey == "" {
+		cmd.routingKey = cmd.connectorName
+	} else {
+		cmd.routingKey = cmd.Flags.RoutingKey
+	}
+
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
+
+	cmd.host = cmd.Flags.Host
+	cmd.connectorType = cmd.Flags.ConnectorType
+	cmd.output = cmd.Flags.Output
+	cmd.tlsSecret = cmd.Flags.TlsSecret
 }
 
-func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdConnectorCreate) InputToOptions()                     {}
 func (cmd *CmdConnectorCreate) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	connectorResource := v2alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.connectorName,
+			Namespace: cmd.namespace,
+		},
+		Spec: v2alpha1.ConnectorSpec{
+			Host:           cmd.host,
+			Port:           cmd.port,
+			RoutingKey:     cmd.routingKey,
+			TlsCredentials: cmd.tlsSecret,
+			Type:           cmd.connectorType,
+		},
+	}
+
+	if cmd.output != "" {
+		encodedOutput, err := utils.Encode(cmd.output, connectorResource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		err := cmd.connectorHandler.Add(connectorResource)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
+
 func (cmd *CmdConnectorCreate) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/connector/nonkube/connector_delete.go b/internal/cmd/skupper/connector/nonkube/connector_delete.go
index 339b3d9f..eea74970 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_delete.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_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 CmdConnectorDelete struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandConnectorDeleteFlags
-	Namespace string
-	siteName  string
+	connectorHandler *fs.ConnectorHandler
+	CobraCmd         *cobra.Command
+	Flags            *common.CommandConnectorDeleteFlags
+	namespace        string
+	connectorName    string
 }
 
 func NewCmdConnectorDelete() *CmdConnectorDelete {
@@ -18,12 +22,56 @@ func NewCmdConnectorDelete() *CmdConnectorDelete {
 }
 
 func (cmd *CmdConnectorDelete) 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.connectorHandler = fs.NewConnectorHandler(cmd.namespace)
+}
+
+func (cmd *CmdConnectorDelete) ValidateInput(args []string) []error {
+	var validationErrors []error
+	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("connector name must be configured"))
+	} else if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+		} else {
+			cmd.connectorName = args[0]
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdConnectorDelete) InputToOptions() {
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
 }
 
-func (cmd *CmdConnectorDelete) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdConnectorDelete) InputToOptions()                     {}
 func (cmd *CmdConnectorDelete) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	err := cmd.connectorHandler.Delete(cmd.connectorName)
+	if err != nil {
+		return err
+	}
+	return nil
 }
+
 func (cmd *CmdConnectorDelete) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/connector/nonkube/connector_status.go b/internal/cmd/skupper/connector/nonkube/connector_status.go
index cc391e8b..7ea8f487 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_status.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_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 CmdConnectorStatus struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandConnectorStatusFlags
-	Namespace string
-	siteName  string
+	connectorHandler *fs.ConnectorHandler
+	CobraCmd         *cobra.Command
+	Flags            *common.CommandConnectorStatusFlags
+	namespace        string
+	connectorName    string
+	output           string
 }
 
 func NewCmdConnectorStatus() *CmdConnectorStatus {
@@ -18,12 +26,107 @@ func NewCmdConnectorStatus() *CmdConnectorStatus {
 }
 
 func (cmd *CmdConnectorStatus) 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.connectorHandler = fs.NewConnectorHandler(cmd.namespace)
+}
+
+func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error {
+	var validationErrors []error
+	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("connector name must not be empty"))
+		} else {
+			ok, err := resourceStringValidator.Evaluate(args[0])
+			if !ok {
+				validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+			} else {
+				cmd.connectorName = args[0]
+			}
+		}
+	}
+	// Validate that there is a connector with this name in the namespace
+	if cmd.connectorName != "" {
+		connector, err := cmd.connectorHandler.Get(cmd.connectorName)
+		if connector == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("connector %s does not exist in namespace %s", cmd.connectorName, cmd.namespace))
+		}
+	}
+
+	if cmd.Flags.Output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.Flags.Output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.output = cmd.Flags.Output
+		}
+	}
+
+	return validationErrors
 }
 
-func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdConnectorStatus) InputToOptions()                     {}
 func (cmd *CmdConnectorStatus) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	if cmd.connectorName == "" {
+		connectors, err := cmd.connectorHandler.List()
+		if connectors == nil || err != nil {
+			fmt.Println("No connectors found:")
+			return err
+		}
+		if cmd.output != "" {
+			for _, connector := range connectors {
+				encodedOutput, err := utils.Encode(cmd.output, connector)
+				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 _, connector := range connectors {
+				status := "Not Ready"
+				if connector.IsConfigured() {
+					status = "Ok"
+				}
+				fmt.Fprintln(tw, fmt.Sprintf("%s\t%s\t%s\t%s\t%d",
+					connector.Name, status, connector.Spec.RoutingKey, connector.Spec.Host, connector.Spec.Port))
+			}
+			_ = tw.Flush()
+		}
+	} else {
+		connector, err := cmd.connectorHandler.Get(cmd.connectorName)
+		if connector == nil || err != nil {
+			fmt.Println("No connectors found:")
+			return err
+		}
+		if cmd.output != "" {
+			encodedOutput, err := utils.Encode(cmd.output, connector)
+			if err != nil {
+				return err
+			}
+			fmt.Println(encodedOutput)
+		} else {
+			status := "Not Ready"
+			if connector.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\n",
+				connector.Name, status, connector.Spec.RoutingKey, connector.Spec.Host, connector.Spec.Port))
+			_ = tw.Flush()
+		}
+	}
+	return nil
 }
+
+func (cmd *CmdConnectorStatus) InputToOptions()  {}
 func (cmd *CmdConnectorStatus) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/connector/nonkube/connector_update.go b/internal/cmd/skupper/connector/nonkube/connector_update.go
index 6e695067..e6044b63 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_update.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_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 ConnectorUpdates struct {
+	routingKey    string
+	host          string
+	connectorType string
+	port          int
+	tlsSecret     string
+}
 type CmdConnectorUpdate struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandConnectorUpdateFlags
-	Namespace string
-	siteName  string
+	connectorHandler *fs.ConnectorHandler
+	CobraCmd         *cobra.Command
+	Flags            *common.CommandConnectorUpdateFlags
+	namespace        string
+	connectorName    string
+	newSettings      ConnectorUpdates
+	output           string
 }
 
 func NewCmdConnectorUpdate() *CmdConnectorUpdate {
@@ -18,12 +35,142 @@ func NewCmdConnectorUpdate() *CmdConnectorUpdate {
 }
 
 func (cmd *CmdConnectorUpdate) 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.connectorHandler = fs.NewConnectorHandler(cmd.namespace)
+}
+
+func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
+	var validationErrors []error
+	resourceStringValidator := validator.NewResourceStringValidator()
+	numberValidator := validator.NewNumberValidator()
+	connectorTypeValidator := validator.NewOptionValidator(common.ConnectorTypes)
+	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("connector name must be configured"))
+	} else if len(args) > 1 {
+		validationErrors = append(validationErrors, fmt.Errorf("only one argument is allowed for this command"))
+	} else if args[0] == "" {
+		validationErrors = append(validationErrors, fmt.Errorf("connector name must not be empty"))
+	} else {
+		ok, err := resourceStringValidator.Evaluate(args[0])
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector name is not valid: %s", err))
+		} else {
+			cmd.connectorName = args[0]
+		}
+	}
+
+	// Validate that there is already a connector with this name in the namespace
+	if cmd.connectorName != "" {
+		connector, err := cmd.connectorHandler.Update(cmd.connectorName)
+		if connector == nil || err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("connector %s must exist in namespace %s to be updated", cmd.connectorName, cmd.namespace))
+		} else {
+			// save existing values
+			cmd.newSettings.host = connector.Spec.Host
+			cmd.newSettings.port = connector.Spec.Port
+			cmd.newSettings.connectorType = connector.Spec.Type
+			cmd.newSettings.tlsSecret = connector.Spec.TlsCredentials
+			cmd.newSettings.routingKey = connector.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.ConnectorType != "" {
+		ok, err := connectorTypeValidator.Evaluate(cmd.Flags.ConnectorType)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector type is not valid: %s", err))
+		} else {
+			cmd.newSettings.connectorType = cmd.Flags.ConnectorType
+		}
+	}
+	if cmd.Flags.Port != 0 {
+		ok, err := numberValidator.Evaluate(cmd.Flags.Port)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("connector port is not valid: %s", err))
+		} else {
+			cmd.newSettings.port = cmd.Flags.Port
+		}
+	}
+	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.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 *CmdConnectorUpdate) InputToOptions() {
+	if cmd.namespace == "" {
+		cmd.namespace = "default"
+	}
 }
 
-func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error { return nil }
-func (cmd *CmdConnectorUpdate) InputToOptions()                     {}
 func (cmd *CmdConnectorUpdate) Run() error {
-	return fmt.Errorf("command not supported by the selected platform")
+	connectorResource := v2alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      cmd.connectorName,
+			Namespace: cmd.namespace,
+		},
+		Spec: v2alpha1.ConnectorSpec{
+			Host:           cmd.newSettings.host,
+			Port:           cmd.newSettings.port,
+			RoutingKey:     cmd.newSettings.routingKey,
+			TlsCredentials: cmd.newSettings.tlsSecret,
+			Type:           cmd.newSettings.connectorType,
+		},
+	}
+
+	if cmd.output != "" {
+		encodedOutput, err := utils.Encode(cmd.output, connectorResource)
+		fmt.Println(encodedOutput)
+		return err
+	} else {
+		err := cmd.connectorHandler.Add(connectorResource)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
+
 func (cmd *CmdConnectorUpdate) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/debug/debug.go b/internal/cmd/skupper/debug/debug.go
new file mode 100644
index 00000000..c5273690
--- /dev/null
+++ b/internal/cmd/skupper/debug/debug.go
@@ -0,0 +1,43 @@
+package debug
+
+import (
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/debug/kube"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/debug/nonkube"
+
+	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/spf13/cobra"
+)
+
+func NewCmdDebug() *cobra.Command {
+
+	cmd := CmdDebugFactory(config.GetPlatform())
+
+	return cmd
+}
+
+func CmdDebugFactory(configuredPlatform types.Platform) *cobra.Command {
+	kubeCommand := kube.NewCmdDebug()
+	nonKubeCommand := nonkube.NewCmdDebug()
+
+	cmdDebugDesc := common.SkupperCmdDescription{
+		Use:   "debug",
+		Short: "",
+		Long:  "",
+	}
+
+	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDebugDesc, kubeCommand, nonKubeCommand)
+
+	cmdFlags := common.CommandDebugFlags{}
+
+	//Add flags if necessary
+
+	kubeCommand.CobraCmd = cmd
+	kubeCommand.Flags = &cmdFlags
+	nonKubeCommand.CobraCmd = cmd
+	nonKubeCommand.Flags = &cmdFlags
+
+	return cmd
+
+}
diff --git a/internal/cmd/skupper/debug/kube/debug.go b/internal/cmd/skupper/debug/kube/debug.go
new file mode 100644
index 00000000..136050c1
--- /dev/null
+++ b/internal/cmd/skupper/debug/kube/debug.go
@@ -0,0 +1,42 @@
+package kube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
+)
+
+type CmdDebug struct {
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Flags      *common.CommandDebugFlags
+	Namespace  string
+}
+
+func NewCmdDebug() *CmdDebug {
+
+	skupperCmd := CmdDebug{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdDebug) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+	if err == nil {
+		cmd.Client = cli.GetSkupperClient().SkupperV2alpha1()
+		cmd.KubeClient = cli.GetKubeClient()
+		cmd.Namespace = cli.Namespace
+	}
+}
+
+func (cmd *CmdDebug) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdDebug) InputToOptions() {}
+
+func (cmd *CmdDebug) Run() error { fmt.Println("not yet implemented"); return nil }
+
+func (cmd *CmdDebug) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/debug/nonkube/debug.go b/internal/cmd/skupper/debug/nonkube/debug.go
new file mode 100644
index 00000000..18cf4dc3
--- /dev/null
+++ b/internal/cmd/skupper/debug/nonkube/debug.go
@@ -0,0 +1,32 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/spf13/cobra"
+)
+
+type CmdDebug struct {
+	CobraCmd  *cobra.Command
+	Flags     *common.CommandDebugFlags
+	Namespace string
+}
+
+func NewCmdDebug() *CmdDebug {
+
+	skupperCmd := CmdDebug{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdDebug) NewClient(cobraCommand *cobra.Command, args []string) {
+
+}
+
+func (cmd *CmdDebug) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdDebug) InputToOptions() {}
+
+func (cmd *CmdDebug) Run() error { fmt.Println("not yet implemented"); return nil }
+
+func (cmd *CmdDebug) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/root/root.go b/internal/cmd/skupper/root/root.go
index 29b6bd0d..add52cda 100644
--- a/internal/cmd/skupper/root/root.go
+++ b/internal/cmd/skupper/root/root.go
@@ -4,10 +4,12 @@ import (
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/connector"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/debug"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/link"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/listener"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/site"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/token"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/version"
 	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/spf13/cobra"
 )
@@ -30,6 +32,8 @@ func NewSkupperRootCommand() *cobra.Command {
 	rootCmd.AddCommand(listener.NewCmdListener())
 	rootCmd.AddCommand(link.NewCmdLink())
 	rootCmd.AddCommand(connector.NewCmdConnector())
+	rootCmd.AddCommand(version.NewCmdVersion())
+	rootCmd.AddCommand(debug.NewCmdDebug())
 
 	return rootCmd
 }
diff --git a/internal/cmd/skupper/site/nonkube/site_create.go b/internal/cmd/skupper/site/nonkube/site_create.go
index a53438bf..18b879fb 100644
--- a/internal/cmd/skupper/site/nonkube/site_create.go
+++ b/internal/cmd/skupper/site/nonkube/site_create.go
@@ -9,12 +9,15 @@ import (
 	"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/site"
 	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+const (
+	SiteConfigNameKey string = "name"
+)
+
 type CmdSiteCreate struct {
 	siteHandler             *fs.SiteHandler
 	routerAccessHandler     *fs.RouterAccessHandler
@@ -115,7 +118,7 @@ func (cmd *CmdSiteCreate) InputToOptions() {
 	}
 
 	options := make(map[string]string)
-	options[site.SiteConfigNameKey] = cmd.siteName
+	options[SiteConfigNameKey] = cmd.siteName
 
 	cmd.options = options
 	cmd.output = cmd.Flags.Output
diff --git a/internal/cmd/skupper/site/site.go b/internal/cmd/skupper/site/site.go
index 56dcd3d5..28810cb6 100644
--- a/internal/cmd/skupper/site/site.go
+++ b/internal/cmd/skupper/site/site.go
@@ -4,13 +4,14 @@ Copyright © 2024 Skupper Team <skupper@googlegroups.com>
 package site
 
 import (
+	"time"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/site/kube"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/site/nonkube"
 	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/spf13/cobra"
-	"time"
 )
 
 func NewCmdSite() *cobra.Command {
diff --git a/internal/cmd/skupper/version/kube/version.go b/internal/cmd/skupper/version/kube/version.go
new file mode 100644
index 00000000..f4059276
--- /dev/null
+++ b/internal/cmd/skupper/version/kube/version.go
@@ -0,0 +1,43 @@
+package kube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
+)
+
+type CmdVersion struct {
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Flags      *common.CommandVersionFlags
+	Namespace  string
+}
+
+func NewCmdVersion() *CmdVersion {
+
+	skupperCmd := CmdVersion{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdVersion) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+
+	if err == nil {
+		cmd.Client = cli.GetSkupperClient().SkupperV2alpha1()
+		cmd.KubeClient = cli.GetKubeClient()
+		cmd.Namespace = cli.Namespace
+	}
+}
+
+func (cmd *CmdVersion) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdVersion) InputToOptions() {}
+
+func (cmd *CmdVersion) Run() error { fmt.Println("not yet implemented"); return nil }
+
+func (cmd *CmdVersion) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/version/nonkube/version.go b/internal/cmd/skupper/version/nonkube/version.go
new file mode 100644
index 00000000..5811a908
--- /dev/null
+++ b/internal/cmd/skupper/version/nonkube/version.go
@@ -0,0 +1,32 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/spf13/cobra"
+)
+
+type CmdVersion struct {
+	CobraCmd  *cobra.Command
+	Flags     *common.CommandVersionFlags
+	Namespace string
+}
+
+func NewCmdVersion() *CmdVersion {
+
+	skupperCmd := CmdVersion{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdVersion) NewClient(cobraCommand *cobra.Command, args []string) {
+
+}
+
+func (cmd *CmdVersion) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdVersion) InputToOptions() {}
+
+func (cmd *CmdVersion) Run() error { fmt.Println("not yet implemented"); return nil }
+
+func (cmd *CmdVersion) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/version/version.go b/internal/cmd/skupper/version/version.go
new file mode 100644
index 00000000..b99b5d70
--- /dev/null
+++ b/internal/cmd/skupper/version/version.go
@@ -0,0 +1,44 @@
+package version
+
+import (
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/version/nonkube"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/version/kube"
+
+	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/spf13/cobra"
+)
+
+func NewCmdVersion() *cobra.Command {
+
+	cmd := CmdVersionFactory(config.GetPlatform())
+
+	return cmd
+}
+
+func CmdVersionFactory(configuredPlatform types.Platform) *cobra.Command {
+	kubeCommand := kube.NewCmdVersion()
+	nonKubeCommand := nonkube.NewCmdVersion()
+
+	cmdVersionDesc := common.SkupperCmdDescription{
+		Use:   "version",
+		Short: "",
+		Long:  "",
+	}
+
+	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdVersionDesc, kubeCommand, nonKubeCommand)
+
+	cmdFlags := common.CommandVersionFlags{}
+
+	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
+
+	kubeCommand.CobraCmd = cmd
+	kubeCommand.Flags = &cmdFlags
+	nonKubeCommand.CobraCmd = cmd
+	nonKubeCommand.Flags = &cmdFlags
+
+	return cmd
+
+}
diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template
index 833d7abe..f3f12cc1 100644
--- a/internal/nonkube/bundle/install.sh.template
+++ b/internal/nonkube/bundle/install.sh.template
@@ -38,7 +38,7 @@ if [ "${UID}" -eq 0 ]; then
     export USERNS="''"
 fi
 export NAMESPACES_PATH="${SKUPPER_OUTPUT_PATH}/namespaces"
-export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/platform.yaml"
+export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/platform.yaml"
 export USER="${USER:-$(id -un)}"
 SITE_ID="$(hostname -s)-${USER}-$(date +%s)"
 export SITE_ID
@@ -79,7 +79,7 @@ parse_opts() {
                 if [ -n "${OPTARG}" ]; then
                     export NAMESPACE="${OPTARG}"
                 fi
-                export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/platform.yaml"
+                export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/platform.yaml"
                 ;;
             d)
                 export DUMP_TOKENS=true
@@ -125,7 +125,7 @@ create_service() {
     service_name="skupper-${NAMESPACE}.service"
     service_file_suffix="container"
     [ "${SKUPPER_PLATFORM}" = "systemd" ] && service_file_suffix="systemd"
-    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${NAMESPACE}/runtime/scripts/skupper.service.${service_file_suffix}"
+    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${NAMESPACE}/internal/scripts/skupper.service.${service_file_suffix}"
 
     if [ ! -f "${service_file}" ]; then
         echo "SystemD service has not been defined"
@@ -164,7 +164,7 @@ remove_site() {
         exit 0
     fi
 
-    SITE_PATH="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/site/"
+    SITE_PATH="${NAMESPACES_PATH}/${NAMESPACE}/runtime/resources/site/"
     if ! grep -qr "name: ${SITE_NAME}\$" "${SITE_PATH}"; then
         echo "Site \"${SITE_NAME}\" is not defined on namespace \"${NAMESPACE}\""
         exit 0
@@ -181,20 +181,20 @@ remove_site() {
 }
 
 dump_tokens() {
-    tokens="$(ls "./${SOURCE_NAMESPACE}/runtime/link/")"
+    tokens="$(ls "./${SOURCE_NAMESPACE}/runtime/links/")"
     if [ -z "${tokens}" ]; then
         echo "No static links defined in this site bundle"
         return
     fi
     token_out_dir="${SKUPPER_TOKEN_OUTPUT_PATH:?}/${SITE_NAME:?}"
     mkdir -p "${token_out_dir}"
-    cp "./${SOURCE_NAMESPACE}/runtime/link/"*.yaml "${token_out_dir}"
+    cp "./${SOURCE_NAMESPACE}/runtime/links/"*.yaml "${token_out_dir}"
     echo "Static links for site \"${SITE_NAME}\" have been saved into ${token_out_dir}"
 }
 
 set_router_access_port() {
-    config_file="${NAMESPACES_PATH}/${NAMESPACE}/config/router/skrouterd.json"
-    python "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/scripts/router_free_port.py" "${config_file}"
+    config_file="${NAMESPACES_PATH}/${NAMESPACE}/runtime/router/skrouterd.json"
+    python "${NAMESPACES_PATH:?}/${NAMESPACE:?}/internal/scripts/router_free_port.py" "${config_file}"
 }
 
 create_sed_script() {
@@ -215,8 +215,8 @@ create_sed_script() {
         echo "s#{{"{{"}}.SiteId{{"}}"}}#${SITE_ID}#g"
         echo "s#{{"{{"}}.NamespacesPath{{"}}"}}#${NAMESPACES_PATH}#g"
         echo "s#{{"{{"}}.RuntimeDir{{"}}"}}#${RUNTIME_DIR}#g"
-        echo "s#{{"{{"}}.SiteScriptPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/runtime/scripts#g"
-        echo "s#{{"{{"}}.SiteConfigPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/config/router#g"
+        echo "s#{{"{{"}}.SiteScriptPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/internal/scripts#g"
+        echo "s#{{"{{"}}.SiteConfigPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/runtime/router#g"
         echo "s#{{"{{"}}.RunAs{{"}}"}}#${RUNAS}#g"
         echo "s#{{"{{"}}.UserNamespace{{"}}"}}#${USERNS}#g"
     } >> script.sed
@@ -228,7 +228,7 @@ render_sed_templates() {
 }
 
 show_token_info() {
-    token_path="${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/link"
+    token_path="${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/links"
     [ ! -d "${token_path}" ] && return
     tokens_found=$(find "${token_path}" -type f | wc -l)
     if [ "${tokens_found}" -gt 0 ]; then
@@ -238,27 +238,15 @@ show_token_info() {
 
 create_containers() {
     [ "${SKUPPER_PLATFORM}" = "systemd" ] && return
-    "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/scripts/containers_create.sh"
+    "${NAMESPACES_PATH:?}/${NAMESPACE:?}/internal/scripts/containers_create.sh"
 }
 
 create_site() {
-    if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime" ]; then
-        echo "Failed: namespace \"${NAMESPACE}\" is already defined"
-        echo "Location: ${NAMESPACES_PATH}/${NAMESPACE}"
-        exit 1
-    fi
-
-    if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources" ]; then
-        echo "Failed: namespace \"${NAMESPACE}\" already contains input sources"
-        echo "Location: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources"
-        exit 1
-    fi
-
     echo "Skupper site bundle installation"
     echo "Namespace : ${NAMESPACE}"
     echo "Site name : ${SITE_NAME}"
     echo "Platform  : ${SKUPPER_PLATFORM}"
-    echo "Definition: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources"
+    echo "Definition: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/resources"
     echo "Version   : ${VERSION}"
 
     # Create base directory tree
@@ -301,18 +289,32 @@ sanity_check() {
             exit_error "A required command could not be found: ${cmd}"
         fi
     done
+
+    if ! ${REMOVE} && ! ${DUMP_TOKENS}; then
+        if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime" ]; then
+            echo "Failed: namespace \"${NAMESPACE}\" is already defined"
+            echo "Location: ${NAMESPACES_PATH}/${NAMESPACE}"
+            exit 1
+        fi
+
+        if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/resources" ]; then
+            echo "Failed: namespace \"${NAMESPACE}\" already contains input resources"
+            echo "Location: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/resources"
+            exit 1
+        fi
+    fi
 }
 
 list_valid_certificates() {
     base_path="${1}"
     cd "${base_path}"
-    for dir in $(find ./* -type d | cut -c 3-); do
+    for dir in $(find ./* -type d 2> /dev/null | cut -c 3-); do
         [ -f "${dir}/tls.key" ] && [ -f "${dir}/tls.crt" ] && [ -f "${dir}/ca.crt" ] && echo "$dir"
     done
 }
 
 handle_provided_certificates() {
-    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates"
+    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certs"
     [ ! -d "${USER_CERTIFICATES_PATH}" ] && return
     cas=""
     clients=""
@@ -348,15 +350,15 @@ handle_provided_certificates() {
             echo "  - ${server}"
         done
     fi
-    cp -r "${USER_CERTIFICATES_PATH}"/* "./${SOURCE_NAMESPACE}/certificates"
+    cp -r "${USER_CERTIFICATES_PATH}"/* "./${SOURCE_NAMESPACE}/runtime/certs"
     update_static_links
     generate_static_links_for_sans
 }
 
 update_static_links() {
     DIR="./${SOURCE_NAMESPACE}"
-    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates"
-    router_access_files=$(find "${DIR:?}/sources/routerAccesses" -type f)
+    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certs"
+    router_access_files=$(find "${DIR:?}/internal/snapshot/routerAccesses" -type f)
 
     for ra in ${router_access_files}; do
         ra_name=$(grep name: "${ra}" | grep -v '\- name:' | awk '{print $NF}' | sed 's/"//g')
@@ -381,7 +383,7 @@ update_static_links() {
         tls_crt=$(base64 -w0 "${client_cert_path}/tls.crt")
         tls_key=$(base64 -w0 "${client_cert_path}/tls.key")
         # find all static links generated for the given RouterAccess
-        static_link_files=$(find "${DIR:?}/runtime/link" -type f -name "link-${ra_name}-*")
+        static_link_files=$(find "${DIR:?}/runtime/links" -type f -name "link-${ra_name}-*")
         for link in ${static_link_files}; do
             echo "- Updating client credentials on static link: ${link}"
             sed -i "s/tls.key: .*/tls.key: ${tls_key}/g" "${link}"
@@ -399,7 +401,7 @@ generate_static_links_for_sans() {
         return
     fi
 
-    router_access_files=$(find "${DIR:?}/sources/routerAccesses" -type f)
+    router_access_files=$(find "${DIR:?}/internal/snapshot/routerAccesses" -type f)
 
     for ra in ${router_access_files}; do
         ra_name=$(grep name: "${ra}" | grep -v '\- name:' | awk '{print $NF}' | sed 's/"//g')
@@ -407,17 +409,17 @@ generate_static_links_for_sans() {
         bind_host=$(grep bindHost: "${ra}" | awk '{print $NF}' | sed 's/"//g')
 
         # discovering SANS
-        server_cert="${DIR}/certificates/server/${tls_credential:-${ra_name}}/tls.crt"
+        server_cert="${DIR}/runtime/certs/server/${tls_credential:-${ra_name}}/tls.crt"
         [ ! -f "${server_cert}" ] && continue
         subjects="$(openssl x509 -noout -ext subjectAltName -in "${server_cert}" |grep -v 'Subject Alternative Name:' | \
                     sed 's/ //g;s/,/\n/g' |  awk -F: '{if ($1 == "DNS" || $1 == "IPAddress") print $2}')"
         [ -z "${subjects}" ] && continue
         for subject in ${subjects}; do
-            link_file="${DIR}/runtime/link/link-${ra_name}-${subject}.yaml"
+            link_file="${DIR}/runtime/links/link-${ra_name}-${subject}.yaml"
             [ -f "${link_file}" ] && continue
             echo "- Creating static link for new server subject found: ${subject}"
-            cp "${DIR}/runtime/link/link-${ra_name}-${bind_host:-127.0.0.1}.yaml" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml"
-            sed -i "s/host: .*/host: ${subject}/g" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml"
+            cp "${DIR}/runtime/links/link-${ra_name}-${bind_host:-127.0.0.1}.yaml" "${DIR}/runtime/links/link-${ra_name}-${subject}.yaml"
+            sed -i "s/host: .*/host: ${subject}/g" "${DIR}/runtime/links/link-${ra_name}-${subject}.yaml"
         done
     done
 }
diff --git a/internal/nonkube/client/fs/connector_handler.go b/internal/nonkube/client/fs/connector_handler.go
new file mode 100644
index 00000000..bceaec6c
--- /dev/null
+++ b/internal/nonkube/client/fs/connector_handler.go
@@ -0,0 +1,120 @@
+package fs
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+)
+
+type ConnectorHandler struct {
+	BaseCustomResourceHandler
+	pathProvider PathProvider
+}
+
+func NewConnectorHandler(namespace string) *ConnectorHandler {
+	return &ConnectorHandler{
+		pathProvider: PathProvider{
+			Namespace: namespace,
+		},
+	}
+}
+
+func (s *ConnectorHandler) Add(resource v2alpha1.Connector) error {
+
+	fileName := resource.Name + ".yaml"
+	content, err := s.EncodeToYaml(resource)
+	if err != nil {
+		return err
+	}
+
+	err = s.WriteFile(s.pathProvider.GetNamespace(), fileName, content, common.Connectors)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *ConnectorHandler) Get(name string) (*v2alpha1.Connector, error) {
+	var context v2alpha1.Connector
+	fileName := name + ".yaml"
+
+	// First read from runtime directory, where output is found after bootstrap
+	// 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")
+		err, file = s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Connectors)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if err = s.DecodeYaml(file, &context); err != nil {
+		return nil, err
+	}
+
+	return &context, nil
+}
+
+func (s *ConnectorHandler) Delete(name string) error {
+	fileName := name + ".yaml"
+
+	if err := s.DeleteFile(s.pathProvider.GetNamespace(), fileName, common.Connectors); err != nil {
+		if !errors.Is(err, fs.ErrNotExist) {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *ConnectorHandler) List() ([]*v2alpha1.Connector, error) {
+	var connectors []*v2alpha1.Connector
+
+	// First read from runtime directory, where output is found after bootstrap
+	// has run.  If no runtime connectors try and display configured connectors
+	path := s.pathProvider.GetRuntimeNamespace()
+	err, files := s.ReadDir(path, common.Connectors)
+	if err != nil {
+		fmt.Println("Site not initialized yet")
+		path = s.pathProvider.GetNamespace()
+		err, files = s.ReadDir(path, common.Connectors)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for _, file := range files {
+		err, connector := s.ReadFile(path, file.Name(), common.Connectors)
+		if err != nil {
+			return nil, err
+		}
+		var context v2alpha1.Connector
+		if err = s.DecodeYaml(connector, &context); err != nil {
+			return nil, err
+		}
+		connectors = append(connectors, &context)
+	}
+	return connectors, nil
+}
+
+func (s *ConnectorHandler) Update(name string) (*v2alpha1.Connector, error) {
+	var context v2alpha1.Connector
+	fileName := name + ".yaml"
+
+	// read from input directory to get lastest config
+	err, file := s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Connectors)
+	if err != nil {
+		return nil, err
+	}
+
+	if err = s.DecodeYaml(file, &context); err != nil {
+		return nil, err
+	}
+
+	return &context, nil
+}
diff --git a/internal/nonkube/client/fs/custom_resource_handler.go b/internal/nonkube/client/fs/custom_resource_handler.go
index 630cc768..22d76d1b 100644
--- a/internal/nonkube/client/fs/custom_resource_handler.go
+++ b/internal/nonkube/client/fs/custom_resource_handler.go
@@ -2,20 +2,29 @@ package fs
 
 import (
 	"fmt"
-	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"io/fs"
 	"os"
 	"path/filepath"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+
+	"sigs.k8s.io/yaml"
 )
 
 type CustomResourceHandler[T any] interface {
 	Add(T) error
 	Update(T) error
-	Get(name string) T
+	Get(name string) (T, error)
+	List() ([]T, error)
 	Delete(name string) error
 
 	//Common methods
 	EncodeToYaml(resource interface{}) (string, error)
 	WriteFile(path string, name string, content string, kind string) error
+	DecodeYaml(content []byte, resource interface{}) (interface{}, error)
+	ReadFile(path string, name string, kind string) (error, []byte)
+	DeleteFile(path string, name string, kind string) error
+	ReadDir(path string, kind string) (error, []fs.DirEntry)
 }
 
 type BaseCustomResourceHandler struct{}
@@ -25,19 +34,21 @@ func (b *BaseCustomResourceHandler) EncodeToYaml(resource interface{}) (string,
 	return utils.Encode("yaml", resource)
 }
 
-func (b *BaseCustomResourceHandler) WriteFile(path string, name string, content string, kind string) error {
+func (b *BaseCustomResourceHandler) DecodeYaml(content []byte, resource interface{}) error {
 
-	// Resolve the home directory
-	homeDir, err := os.UserHomeDir()
-	if err != nil {
+	if err := yaml.Unmarshal(content, &resource); err != nil {
 		return err
 	}
+	return nil
+}
 
-	fullPath := filepath.Join(homeDir, path, kind)
+func (b *BaseCustomResourceHandler) WriteFile(path string, name string, content string, kind string) error {
+
+	fullPath := filepath.Join(path, kind)
 	completeFilePath := filepath.Join(fullPath, name)
 
 	// Create the directories recursively
-	err = os.MkdirAll(fullPath, 0775)
+	err := os.MkdirAll(fullPath, 0775)
 	if err != nil {
 		return fmt.Errorf("failed to create directories: %s", err)
 	}
@@ -59,3 +70,41 @@ func (b *BaseCustomResourceHandler) WriteFile(path string, name string, content
 
 	return nil
 }
+
+func (b *BaseCustomResourceHandler) ReadFile(path string, name string, kind string) (error, []byte) {
+
+	fullPath := filepath.Join(path, kind)
+	completeFilePath := filepath.Join(fullPath, name)
+
+	file, err := os.ReadFile(completeFilePath)
+	if err != nil {
+		return fmt.Errorf("failed to read file: %s", err), nil
+	}
+
+	return nil, file
+}
+
+func (b *BaseCustomResourceHandler) DeleteFile(path string, name string, kind string) error {
+	var completeFilePath string
+
+	fullPath := filepath.Join(path, kind)
+	completeFilePath = filepath.Join(fullPath, name)
+
+	if err := os.RemoveAll(completeFilePath); err != nil {
+		return fmt.Errorf("failed to delete file: %s", err)
+	}
+
+	return nil
+}
+
+func (b *BaseCustomResourceHandler) ReadDir(path string, kind string) (error, []fs.DirEntry) {
+
+	fullPath := filepath.Join(path, kind)
+
+	files, err := os.ReadDir(fullPath)
+	if err != nil {
+		return fmt.Errorf("failed to read directory: %s", err), nil
+	}
+
+	return nil, files
+}
diff --git a/internal/nonkube/client/fs/path_provider.go b/internal/nonkube/client/fs/path_provider.go
index 5851fdf0..a2fdff72 100644
--- a/internal/nonkube/client/fs/path_provider.go
+++ b/internal/nonkube/client/fs/path_provider.go
@@ -1,19 +1,17 @@
 package fs
 
-import "fmt"
+import (
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+)
 
 type PathProvider struct {
 	Namespace string
 }
 
-func (p *PathProvider) getDefaultNamespace() string {
-	return ".local/share/skupper/namespaces/default/input/sources"
-}
-
 func (p *PathProvider) GetNamespace() string {
+	return api.GetHostNamespaceHome(p.Namespace) + "/" + string(api.InputSiteStatePath)
+}
 
-	if p.Namespace == "" {
-		return p.getDefaultNamespace()
-	}
-	return fmt.Sprintf(".local/share/skupper/namespaces/%s/input/sources", p.Namespace)
+func (p *PathProvider) GetRuntimeNamespace() string {
+	return api.GetHostNamespaceHome(p.Namespace) + "/" + string(api.RuntimeSiteStatePath)
 }
diff --git a/internal/nonkube/client/fs/router_access_handler.go b/internal/nonkube/client/fs/router_access_handler.go
index fcbdd329..8877da45 100644
--- a/internal/nonkube/client/fs/router_access_handler.go
+++ b/internal/nonkube/client/fs/router_access_handler.go
@@ -1,6 +1,9 @@
 package fs
 
-import "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+import (
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+)
 
 type RouterAccessHandler struct {
 	BaseCustomResourceHandler
@@ -23,13 +26,14 @@ func (s *RouterAccessHandler) Add(resource v2alpha1.RouterAccess) error {
 		return err
 	}
 
-	err = s.WriteFile(s.pathProvider.GetNamespace(), fileName, content, "routerAccesses")
+	err = s.WriteFile(s.pathProvider.GetNamespace(), fileName, content, common.RouterAccesses)
 	if err != nil {
 		return err
 	}
 
 	return nil
 }
-func (s *RouterAccessHandler) Update(resource v2alpha1.RouterAccess) error { return nil }
-func (s *RouterAccessHandler) Get(name string) *v2alpha1.Site              { return nil }
-func (s *RouterAccessHandler) Delete(name string) error                    { return nil }
+func (s *RouterAccessHandler) Update(name string) (*v2alpha1.RouterAccess, error) { return nil, nil }
+func (s *RouterAccessHandler) Get(name string) (*v2alpha1.RouterAccess, error)    { return nil, 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 b396fc7a..b465081a 100644
--- a/internal/nonkube/client/fs/site_handler.go
+++ b/internal/nonkube/client/fs/site_handler.go
@@ -1,6 +1,9 @@
 package fs
 
-import "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+import (
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+)
 
 type SiteHandler struct {
 	BaseCustomResourceHandler
@@ -23,13 +26,15 @@ func (s *SiteHandler) Add(resource v2alpha1.Site) error {
 		return err
 	}
 
-	err = s.WriteFile(s.pathProvider.GetNamespace(), fileName, content, "sites")
+	err = s.WriteFile(s.pathProvider.GetNamespace(), fileName, content, common.Sites)
 	if err != nil {
 		return err
 	}
 
 	return nil
 }
-func (s *SiteHandler) Update(resource v2alpha1.Site) error { return nil }
-func (s *SiteHandler) Get(name string) *v2alpha1.Site      { return nil }
-func (s *SiteHandler) Delete(name string) error            { return nil }
+
+func (s *SiteHandler) Update(name string) (*v2alpha1.Site, error) { return nil, nil }
+func (s *SiteHandler) Get(name string) (*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 }
diff --git a/pkg/apis/skupper/v2alpha1/types.go b/pkg/apis/skupper/v2alpha1/types.go
index f476abdf..05147a37 100644
--- a/pkg/apis/skupper/v2alpha1/types.go
+++ b/pkg/apis/skupper/v2alpha1/types.go
@@ -486,6 +486,8 @@ type ConnectorSpec struct {
 	Selector        string            `json:"selector,omitempty"`
 	Port            int               `json:"port"`
 	TlsCredentials  string            `json:"tlsCredentials,omitempty"`
+	NoClientAuth    bool              `json:"noClientAuth,omitempty"`
+	VerifyHostname  bool              `json:"verifyHostname,omitempty"`
 	Type            string            `json:"type,omitempty"`
 	IncludeNotReady bool              `json:"includeNotReady,omitempty"`
 	Settings        map[string]string `json:"settings,omitempty"`
diff --git a/pkg/kube/site/attached_connector.go b/pkg/kube/site/attached_connector.go
index 13adec40..d47a2189 100644
--- a/pkg/kube/site/attached_connector.go
+++ b/pkg/kube/site/attached_connector.go
@@ -22,9 +22,9 @@ type ExtendedBindings struct {
 	logger     *slog.Logger
 }
 
-func NewExtendedBindings(controller *kube.Controller) *ExtendedBindings {
+func NewExtendedBindings(controller *kube.Controller, profilePath string) *ExtendedBindings {
 	return &ExtendedBindings{
-		bindings:   site.NewBindings(),
+		bindings:   site.NewBindings(profilePath),
 		connectors: map[string]*AttachedConnector{},
 		controller: controller,
 		logger: slog.New(slog.Default().Handler()).With(
@@ -74,8 +74,9 @@ func (b *ExtendedBindings) Apply(config *qdr.RouterConfig) bool {
 	for _, connector := range b.connectors {
 		connector.updateBridgeConfig(b.bindings.SiteId, &desired)
 	}
-	//TODO: add/remove SslProfiles as necessary
+	b.bindings.AddSslProfiles(config)
 	config.UpdateBridgeConfig(desired)
+	config.RemoveUnreferencedSslProfiles()
 	return true //TODO: can optimise by indicating if no change was required
 }
 
diff --git a/pkg/kube/site/site.go b/pkg/kube/site/site.go
index 9d2e4952..d1a209ff 100644
--- a/pkg/kube/site/site.go
+++ b/pkg/kube/site/site.go
@@ -47,7 +47,7 @@ type Site struct {
 
 func NewSite(namespace string, controller *kube.Controller, certs certificates.CertificateManager, access SecuredAccessFactory) *Site {
 	return &Site{
-		bindings:   NewExtendedBindings(controller),
+		bindings:   NewExtendedBindings(controller, SSL_PROFILE_PATH),
 		namespace:  namespace,
 		controller: controller,
 		links:      map[string]*site.Link{},
diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go
index 50620d4f..66951036 100644
--- a/pkg/nonkube/api/environment.go
+++ b/pkg/nonkube/api/environment.go
@@ -12,19 +12,22 @@ type InternalPath string
 type InternalPathProvider func(namespace string, internalPath InternalPath) string
 
 const (
-	ConfigRouterPath            InternalPath = "config/router"
-	CertificatesCaPath          InternalPath = "certificates/ca"
-	CertificatesClientPath      InternalPath = "certificates/client"
-	CertificatesServerPath      InternalPath = "certificates/server"
-	CertificatesLinkPath        InternalPath = "certificates/link"
-	InputCertificatesCaPath     InternalPath = "input/certificates/ca"
-	InputCertificatesClientPath InternalPath = "input/certificates/client"
-	InputCertificatesServerPath InternalPath = "input/certificates/server"
-	InputSiteStatePath          InternalPath = "input/sources"
-	LoadedSiteStatePath         InternalPath = "sources"
-	RuntimeSiteStatePath        InternalPath = "runtime/state"
-	RuntimeTokenPath            InternalPath = "runtime/link"
-	RuntimeScriptsPath          InternalPath = "runtime/scripts"
+	InputCertificatesBasePath   InternalPath = "input/certs"
+	InputCertificatesCaPath     InternalPath = "input/certs/ca"
+	InputCertificatesClientPath InternalPath = "input/certs/client"
+	InputCertificatesServerPath InternalPath = "input/certs/server"
+	InputSiteStatePath          InternalPath = "input/resources"
+	RouterConfigPath            InternalPath = "runtime/router"
+	CertificatesBasePath        InternalPath = "runtime/certs"
+	CertificatesCaPath          InternalPath = "runtime/certs/ca"
+	CertificatesClientPath      InternalPath = "runtime/certs/client"
+	CertificatesServerPath      InternalPath = "runtime/certs/server"
+	CertificatesLinkPath        InternalPath = "runtime/certs/link"
+	RuntimePath                 InternalPath = "runtime"
+	RuntimeSiteStatePath        InternalPath = "runtime/resources"
+	RuntimeTokenPath            InternalPath = "runtime/links"
+	LoadedSiteStatePath         InternalPath = "internal/snapshot"
+	ScriptsPath                 InternalPath = "internal/scripts"
 )
 
 type IdGetter func() int
diff --git a/pkg/nonkube/api/site_state.go b/pkg/nonkube/api/site_state.go
index 80c11af0..4d26b847 100644
--- a/pkg/nonkube/api/site_state.go
+++ b/pkg/nonkube/api/site_state.go
@@ -239,7 +239,7 @@ func (s *SiteState) linkAccessMap() site.RouterAccessMap {
 func (s *SiteState) linkMap(sslProfileBasePath string) site.LinkMap {
 	linkMap := site.LinkMap{}
 	for name, link := range s.Links {
-		siteLink := site.NewLink(name, path.Join(sslProfileBasePath, "certificates/link"))
+		siteLink := site.NewLink(name, path.Join(sslProfileBasePath, string(CertificatesLinkPath)))
 		link.SetConfigured(nil)
 		siteLink.Update(link)
 		linkMap[name] = siteLink
@@ -247,8 +247,8 @@ func (s *SiteState) linkMap(sslProfileBasePath string) site.LinkMap {
 	return linkMap
 }
 
-func (s *SiteState) bindings() *site.Bindings {
-	b := site.NewBindings()
+func (s *SiteState) bindings(sslProfileBasePath string) *site.Bindings {
+	b := site.NewBindings(path.Join(sslProfileBasePath, "certificates/bindings"))
 	for name, connector := range s.Connectors {
 		connector.SetConfigured(nil)
 		_ = b.UpdateConnector(name, connector)
@@ -289,11 +289,11 @@ func (s *SiteState) ToRouterConfig(sslProfileBasePath string, platform string) q
 		routerConfig.SiteConfig.Platform = "{{.Platform}}"
 	}
 	// LinkAccess
-	s.linkAccessMap().DesiredConfig(nil, path.Join(sslProfileBasePath, "certificates/server")).Apply(&routerConfig)
+	s.linkAccessMap().DesiredConfig(nil, path.Join(sslProfileBasePath, string(CertificatesServerPath))).Apply(&routerConfig)
 	// Link
 	s.linkMap(sslProfileBasePath).Apply(&routerConfig)
 	// Bindings
-	s.bindings().Apply(&routerConfig)
+	s.bindings(sslProfileBasePath).Apply(&routerConfig)
 	// Log (static for now) TODO use site specific options to configure logging
 	routerConfig.SetLogLevel("ROUTER_CORE", "error+")
 
diff --git a/pkg/nonkube/bundle/site_state_renderer.go b/pkg/nonkube/bundle/site_state_renderer.go
index 5cca695b..37efe815 100644
--- a/pkg/nonkube/bundle/site_state_renderer.go
+++ b/pkg/nonkube/bundle/site_state_renderer.go
@@ -2,7 +2,9 @@ package bundle
 
 import (
 	_ "embed"
+	"encoding/json"
 	"fmt"
+	"log/slog"
 	"os"
 	"path"
 
@@ -110,13 +112,13 @@ func (s *SiteStateRenderer) prepareContainers() error {
 		},
 		FileMounts: []container.FileMount{
 			{
-				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", "config/router"),
+				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", string(api.RouterConfigPath)),
 				Destination: "/etc/skupper-router/config",
 				Options:     []string{"z"},
 			},
 			{
-				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", "certificates"),
-				Destination: "/etc/skupper-router/certificates",
+				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", string(api.CertificatesBasePath)),
+				Destination: "/etc/skupper-router/runtime/certs",
 				Options:     []string{"z"},
 			},
 		},
@@ -126,12 +128,24 @@ func (s *SiteStateRenderer) prepareContainers() error {
 		//      validate whether CPU and memory thresholds can be
 		//      set to the container
 	}
+	logger := common.NewLogger()
+	if logger.Enabled(nil, slog.LevelDebug) {
+		for name, newContainer := range s.containers {
+			containerJson, _ := json.Marshal(newContainer)
+			logger.Debug("container prepared:",
+				slog.String("name", name),
+				slog.String("container", string(containerJson)),
+			)
+		}
+	}
 	return nil
 }
 
 func (s *SiteStateRenderer) createContainerScript() error {
-	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.RuntimeScriptsPath)
+	logger := common.NewLogger()
+	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.ScriptsPath)
 	scriptContent := containersToShell(s.containers)
+	logger.Debug("writing containers_create.sh", slog.String("path", scriptsPath))
 	err := os.WriteFile(path.Join(scriptsPath, "containers_create.sh"), scriptContent, 0755)
 	if err != nil {
 		return fmt.Errorf("failed to create containers script: %v", err)
@@ -140,6 +154,7 @@ func (s *SiteStateRenderer) createContainerScript() error {
 }
 
 func (s *SiteStateRenderer) createBundle() error {
+	var logger = common.NewLogger()
 	bundlesHomeDir := api.GetDefaultOutputBundlesPath()
 	siteHomeDir := api.GetDefaultBundleOutputPath(s.siteState.Site.Namespace)
 	tarball := utils.NewTarball()
@@ -162,6 +177,7 @@ func (s *SiteStateRenderer) createBundle() error {
 			OutputPath: bundlesHomeDir,
 		}
 	}
+	logger.Debug("generating bundle:", slog.String("path", bundlesHomeDir), slog.String("site", s.siteState.Site.Name))
 	err = generator.Generate(tarball, string(s.Platform))
 	if err != nil {
 		return fmt.Errorf("failed to generate site bundle (%q): %v", s.siteState.Site.Name, err)
@@ -170,7 +186,9 @@ func (s *SiteStateRenderer) createBundle() error {
 }
 
 func (s *SiteStateRenderer) removeSiteFiles() error {
+	logger := common.NewLogger()
 	siteHomeDir := api.GetDefaultBundleOutputPath(s.siteState.Site.Namespace)
+	logger.Debug("removing temporary bundle home", slog.String("path", siteHomeDir))
 	err := os.RemoveAll(siteHomeDir)
 	if err != nil {
 		return fmt.Errorf("file to remove temporary site directory %q: %v", siteHomeDir, err)
@@ -179,7 +197,9 @@ func (s *SiteStateRenderer) removeSiteFiles() error {
 }
 
 func (s *SiteStateRenderer) createFreePortScript() error {
-	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.RuntimeScriptsPath)
+	logger := common.NewLogger()
+	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.ScriptsPath)
+	logger.Debug("writing freeport.sh", slog.String("path", scriptsPath))
 	err := os.WriteFile(path.Join(scriptsPath, "router_free_port.py"), []byte(FreePortScript), 0755)
 	if err != nil {
 		return fmt.Errorf("failed to create router_free_port.py script: %v", err)
diff --git a/pkg/nonkube/bundle/systemd.go b/pkg/nonkube/bundle/systemd.go
index 1a072616..9b2200f1 100644
--- a/pkg/nonkube/bundle/systemd.go
+++ b/pkg/nonkube/bundle/systemd.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	_ "embed"
 	"fmt"
+	"log/slog"
 	"os"
 	"path"
 	"text/template"
@@ -15,11 +16,12 @@ import (
 
 func CreateSystemdServices(siteState *api.SiteState) error {
 	var err error
+	var logger = common.NewLogger()
 	serviceTemplates := map[string]string{
 		"systemd":   common.SystemdServiceTemplate,
 		"container": common.SystemdContainerServiceTemplate,
 	}
-	scriptsPath := api.GetInternalBundleOutputPath(siteState.Site.Namespace, api.RuntimeScriptsPath)
+	scriptsPath := api.GetInternalBundleOutputPath(siteState.Site.Namespace, api.ScriptsPath)
 	for platform, serviceTemplate := range serviceTemplates {
 		var buf = new(bytes.Buffer)
 		parsedTemplate := template.Must(template.New("service").Parse(serviceTemplate))
@@ -36,6 +38,7 @@ func CreateSystemdServices(siteState *api.SiteState) error {
 			return fmt.Errorf("failed to execute %s service template: %w", platform, err)
 		}
 		serviceFile := path.Join(scriptsPath, fmt.Sprintf("skupper.service.%s", platform))
+		logger.Debug("writing systemd service file", slog.String("path", serviceFile))
 		err = os.WriteFile(serviceFile, buf.Bytes(), 0644)
 		if err != nil {
 			return fmt.Errorf("failed to write %s service file: %w", platform, err)
diff --git a/pkg/nonkube/common/claim_redeemer.go b/pkg/nonkube/common/claim_redeemer.go
index c1225661..e7de0d09 100644
--- a/pkg/nonkube/common/claim_redeemer.go
+++ b/pkg/nonkube/common/claim_redeemer.go
@@ -6,6 +6,7 @@ import (
 	"crypto/x509"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strings"
 	"time"
@@ -21,11 +22,15 @@ import (
 func RedeemClaims(siteState *api.SiteState) error {
 	var errs []error
 
+	logger := NewLogger()
 	for name, claim := range siteState.Claims {
 		err := redeemAccessToken(claim, siteState)
 		if err != nil {
 			errs = append(errs, fmt.Errorf("failed to redeem claim %s: %w", name, err))
-			fmt.Printf("RedeemClaims: failed to redeem claim: %s: %s\n", name, err)
+			logger.Error("RedeemClaims: failed to redeem claim",
+				slog.String("name", name),
+				slog.String("error", err.Error()),
+			)
 		}
 	}
 
diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go
index bd609d42..d0745792 100644
--- a/pkg/nonkube/common/fs_config_renderer.go
+++ b/pkg/nonkube/common/fs_config_renderer.go
@@ -3,7 +3,7 @@ package common
 import (
 	"errors"
 	"fmt"
-	"log"
+	"log/slog"
 	"os"
 	"path"
 	"strconv"
@@ -21,7 +21,7 @@ import (
 
 var (
 	configurationDirectories = []api.InternalPath{
-		api.ConfigRouterPath,
+		api.RouterConfigPath,
 		api.CertificatesCaPath,
 		api.CertificatesClientPath,
 		api.CertificatesServerPath,
@@ -33,20 +33,27 @@ var (
 		api.LoadedSiteStatePath,
 		api.RuntimeSiteStatePath,
 		api.RuntimeTokenPath,
-		api.RuntimeScriptsPath,
+		api.ScriptsPath,
 	}
 	reloadDirectories = []api.InternalPath{
-		api.ConfigRouterPath,
+		api.RouterConfigPath,
 		api.CertificatesClientPath,
 		api.CertificatesServerPath,
 		api.CertificatesLinkPath,
 		api.RuntimeSiteStatePath,
 		api.RuntimeTokenPath,
-		api.RuntimeScriptsPath,
+		api.ScriptsPath,
 		api.LoadedSiteStatePath,
 	}
 )
 
+type InputPathType string
+
+const (
+	InputPathResources InputPathType = "resources"
+	InputPathCerts     InputPathType = "certs"
+)
+
 const (
 	DefaultSslProfileBasePath = "${SSL_PROFILE_BASE_PATH}"
 )
@@ -67,6 +74,7 @@ func NewFileSystemConfigurationRenderer(outputPath string) *FileSystemConfigurat
 // Render simply renders the given site state as configuration files.
 func (c *FileSystemConfigurationRenderer) Render(siteState *api.SiteState) error {
 	var err error
+	logger := NewLogger()
 	outputPath := c.GetOutputPath(siteState)
 	if c.SslProfileBasePath == "" {
 		c.SslProfileBasePath = DefaultSslProfileBasePath
@@ -117,7 +125,9 @@ func (c *FileSystemConfigurationRenderer) Render(siteState *api.SiteState) error
 	// Saving runtime platform
 	if !c.Bundle {
 		content := fmt.Sprintf("platform: %s\n", c.Platform)
-		err = os.WriteFile(path.Join(outputPath, string(api.RuntimeSiteStatePath), "platform.yaml"), []byte(content), 0644)
+		platformPath := path.Join(outputPath, string(api.RuntimePath), "platform.yaml")
+		logger.Debug("writing platform", slog.String("platform", c.Platform), slog.String("path", platformPath))
+		err = os.WriteFile(platformPath, []byte(content), 0644)
 		if err != nil {
 			return fmt.Errorf("failed to write runtime platform: %w", err)
 		}
@@ -127,6 +137,11 @@ func (c *FileSystemConfigurationRenderer) Render(siteState *api.SiteState) error
 	for raName, ra := range siteState.RouterAccesses {
 		for _, role := range ra.Spec.Roles {
 			if role.Name != "normal" {
+				logger.Debug("site endpoint configured:",
+					slog.String("host", ra.Spec.BindHost),
+					slog.String("role", role.Name),
+					slog.Int("port", role.Port),
+				)
 				endpoints = append(endpoints, v2alpha1.Endpoint{
 					Name: fmt.Sprintf("%s-%s", raName, role.Name),
 					Host: ra.Spec.BindHost,
@@ -152,33 +167,37 @@ func (c *FileSystemConfigurationRenderer) GetOutputPath(siteState *api.SiteState
 	return defaultOutputPathProvider(siteState.Site.Namespace)
 }
 
-func (c *FileSystemConfigurationRenderer) GetInputPath(siteState *api.SiteState) string {
+func (c *FileSystemConfigurationRenderer) GetInputPath(siteState *api.SiteState, dir InputPathType) string {
 	var customSiteHomeProvider = api.GetCustomSiteHome
 	var defaultOutputPathProvider = api.GetDefaultOutputPath
 	if c.customOutputPath != "" {
-		return path.Join(customSiteHomeProvider(siteState.Site, c.customOutputPath), "input")
+		return path.Join(customSiteHomeProvider(siteState.Site, c.customOutputPath), "input", string(dir))
 	}
-	return path.Join(defaultOutputPathProvider(siteState.Site.Namespace), "input")
+	return path.Join(defaultOutputPathProvider(siteState.Site.Namespace), "input", string(dir))
 }
 
 func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, runtimeSiteState *api.SiteState) error {
+	logger := NewLogger()
 	if loadedSiteState != nil {
 		outputPath := c.GetOutputPath(loadedSiteState)
 		sourcesPath := path.Join(outputPath, string(api.LoadedSiteStatePath))
 		inputSourcesPath := path.Join(outputPath, string(api.InputSiteStatePath))
-		existingSources, _ := new(utils.DirectoryReader).ReadDir(inputSourcesPath, nil)
+		existingLoadedSources, _ := new(utils.DirectoryReader).ReadDir(sourcesPath, nil)
+		existingInputSources, _ := new(utils.DirectoryReader).ReadDir(inputSourcesPath, nil)
 		// when sources are already defined, we back them up
-		if len(existingSources) > 0 {
+		if len(existingLoadedSources) > 0 {
 			tb := utils.NewTarball()
-			err := tb.AddFiles(inputSourcesPath)
+			err := tb.AddFiles(sourcesPath)
 			if err != nil {
 				return fmt.Errorf("unable to backup existing sources: %s", err)
 			}
-			tbFile := path.Join(outputPath, "input", "sources.backup.tar.gz")
+			tbFile := path.Join(outputPath, "sources.backup.tar.gz")
+			logger.Debug("saving loaded state sources", slog.String("path", tbFile))
 			err = tb.Save(tbFile)
 			if err != nil {
 				return fmt.Errorf("unable to backup sources.backup.tar.gz: %s", err)
 			}
+			logger.Debug("clean up previous loaded state", slog.String("path", sourcesPath))
 			err = os.RemoveAll(sourcesPath)
 			if err != nil {
 				return fmt.Errorf("unable to remove former loaded state: %s", err)
@@ -186,17 +205,21 @@ func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, run
 			if err = os.Mkdir(sourcesPath, 0755); err != nil {
 				return fmt.Errorf("unable to recreate sources directory %s: %s", sourcesPath, err)
 			}
-		} else {
+		}
+		if len(existingInputSources) == 0 {
+			logger.Debug("creating input sources", slog.String("path", inputSourcesPath))
 			if err := api.MarshalSiteState(*loadedSiteState, inputSourcesPath); err != nil {
 				return err
 			}
 		}
+		logger.Debug("saving loaded state", slog.String("path", sourcesPath))
 		if err := api.MarshalSiteState(*loadedSiteState, sourcesPath); err != nil {
 			return err
 		}
 	}
 	outputPath := c.GetOutputPath(runtimeSiteState)
 	runtimeStatePath := path.Join(outputPath, string(api.RuntimeSiteStatePath))
+	logger.Debug("saving runtime state", slog.String("path", runtimeStatePath))
 	if err := api.MarshalSiteState(*runtimeSiteState, runtimeStatePath); err != nil {
 		return err
 	}
@@ -204,9 +227,12 @@ func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, run
 }
 
 func CleanupNamespaceForReload(namespace string) error {
+	logger := NewLogger()
+	logger.Debug("cleaning up namespace for reload")
 	// Re-create internal configuration directories
 	for _, dir := range reloadDirectories {
 		outputPath := api.GetInternalOutputPath(namespace, dir)
+		logger.Debug("recreating directory:", slog.String("path", outputPath))
 		if err := os.RemoveAll(outputPath); err != nil {
 			return fmt.Errorf("failed to remove directory %s: %w", outputPath, err)
 		}
@@ -238,6 +264,7 @@ func RestoreNamespaceData(data []byte) error {
 }
 
 func (c *FileSystemConfigurationRenderer) createTokens(siteState *api.SiteState) error {
+	logger := NewLogger()
 	tokens := make([]*api.Token, 0)
 	for name, linkAccess := range siteState.RouterAccesses {
 		noInterRouterRole := linkAccess.FindRole("inter-router") == nil
@@ -270,10 +297,11 @@ func (c *FileSystemConfigurationRenderer) createTokens(siteState *api.SiteState)
 		tokenPath := path.Join(outputPath, string(api.RuntimeTokenPath), tokenFileName)
 		tokenYaml, err := token.Marshal()
 		if err != nil {
-			return fmt.Errorf("unable to marshal token: %v", err)
+			return fmt.Errorf("unable to marshal static link: %v", err)
 		}
+		logger.Debug("writing static link", slog.String("path", tokenPath))
 		if err := os.WriteFile(tokenPath, tokenYaml, 0644); err != nil {
-			return fmt.Errorf("unable to create token file %s: %v", tokenPath, err)
+			return fmt.Errorf("unable to create static link file %s: %v", tokenPath, err)
 		}
 	}
 	return nil
@@ -288,7 +316,8 @@ func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *api.Site
 		return fmt.Errorf("unable to marshal router config: %v", err)
 	}
 	outputPath := c.GetOutputPath(siteState)
-	routerConfigFileName := path.Join(outputPath, string(api.ConfigRouterPath), "skrouterd.json")
+	routerConfigFileName := path.Join(outputPath, string(api.RouterConfigPath), "skrouterd.json")
+	NewLogger().Debug("Writing router configuration", slog.String("path", routerConfigFileName))
 	err = os.WriteFile(routerConfigFileName, []byte(routerConfigJson), 0644)
 	if err != nil {
 		return fmt.Errorf("unable to write router config file: %v", err)
@@ -298,6 +327,7 @@ func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *api.Site
 
 func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.SiteState) error {
 	var err error
+	var logger = NewLogger()
 	writeSecretFilesIgnore := func(basePath string, secret *corev1.Secret, ignoreExisting bool) error {
 		baseDir, err := os.Open(basePath)
 		if err != nil {
@@ -323,10 +353,11 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.S
 				// ignoring existing certificate
 				_ = certFile.Close()
 				if ignoreExisting {
-					log.Printf("warning: %s will not be overwritten", certFileName)
+					logger.Warn("certificate will not be overwritten", slog.String("path", certFileName))
 					continue
 				}
 			}
+			logger.Debug("writing certificate", slog.String("path", certFileName))
 			err = os.WriteFile(certFileName, data, 0640)
 			if err != nil {
 				return fmt.Errorf("error writing %s: %v", certFileName, err)
@@ -389,7 +420,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.S
 			secret = *userSecret
 			fmt.Printf("-> User provided %s certificate found: %s\n", purpose, name)
 		}
-		certPath := path.Join(outputPath, "certificates", purpose, name)
+		certPath := path.Join(outputPath, string(api.CertificatesBasePath), purpose, name)
 		err = writeSecretFiles(certPath, &secret)
 		if err != nil {
 			return err
@@ -402,7 +433,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.S
 		if !ok {
 			return fmt.Errorf("secret %s not found", secretName)
 		}
-		certPath := path.Join(outputPath, "certificates", "link", secretName+"-profile")
+		certPath := path.Join(outputPath, string(api.CertificatesLinkPath), secretName+"-profile")
 		err = writeSecretFiles(certPath, secret)
 		if err != nil {
 			return err
@@ -453,12 +484,12 @@ func (c *FileSystemConfigurationRenderer) loadClientSecret(siteState *api.SiteSt
 }
 
 func (c *FileSystemConfigurationRenderer) loadCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) {
-	outputPath := c.GetOutputPath(siteState)
+	outputPath := path.Join(c.GetOutputPath(siteState), string(api.CertificatesBasePath))
 	return c.loadCertAsSecretFrom(outputPath, siteState, purpose, name)
 }
 
 func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) {
-	userInputPath := c.GetInputPath(siteState)
+	userInputPath := path.Join(c.GetInputPath(siteState, InputPathCerts))
 	secret, err := c.loadCertAsSecretFrom(userInputPath, siteState, purpose, name)
 	if err != nil {
 		if os.IsNotExist(err) {
@@ -483,7 +514,7 @@ func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.Si
 }
 
 func (c *FileSystemConfigurationRenderer) loadCertAsSecretFrom(basePath string, siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) {
-	certPath := path.Join(basePath, "certificates", purpose, name)
+	certPath := path.Join(basePath, purpose, name)
 	var secret *corev1.Secret
 	certDir, err := os.Open(certPath)
 	if err != nil {
diff --git a/pkg/nonkube/common/log.go b/pkg/nonkube/common/log.go
new file mode 100644
index 00000000..66357a89
--- /dev/null
+++ b/pkg/nonkube/common/log.go
@@ -0,0 +1,10 @@
+package common
+
+import (
+	"log/slog"
+)
+
+func NewLogger() *slog.Logger {
+	slog.SetLogLoggerLevel(slog.LevelInfo)
+	return slog.New(slog.Default().Handler())
+}
diff --git a/pkg/nonkube/common/platform.go b/pkg/nonkube/common/platform.go
index e998016f..778f40b4 100644
--- a/pkg/nonkube/common/platform.go
+++ b/pkg/nonkube/common/platform.go
@@ -26,7 +26,7 @@ func (s *NamespacePlatformLoader) Load(namespace string) (string, error) {
 		namespace = "default"
 	}
 	pathProvider := s.GetPathProvider()
-	runtimePath := pathProvider(namespace, api.RuntimeSiteStatePath)
+	runtimePath := pathProvider(namespace, api.RuntimePath)
 	platformFile, err := os.ReadFile(path.Join(runtimePath, "platform.yaml"))
 	if err != nil {
 		return "", fmt.Errorf("failed to read platform config file for namespace %s: %w", namespace, err)
diff --git a/pkg/nonkube/common/site_state_loader.go b/pkg/nonkube/common/site_state_loader.go
index a7064322..e2c658d0 100644
--- a/pkg/nonkube/common/site_state_loader.go
+++ b/pkg/nonkube/common/site_state_loader.go
@@ -172,7 +172,7 @@ func LoadIntoSiteState(reader *bufio.Reader, siteState *api.SiteState) error {
 }
 
 func LoadRouterConfig(namespace string) (*qdr.RouterConfig, error) {
-	routerConfigPath := api.GetInternalOutputPath(namespace, api.ConfigRouterPath)
+	routerConfigPath := api.GetInternalOutputPath(namespace, api.RouterConfigPath)
 	routerConfigFile := path.Join(routerConfigPath, "skrouterd.json")
 	routerConfigData, err := os.ReadFile(routerConfigFile)
 	if err != nil {
diff --git a/pkg/nonkube/common/site_state_renderer_common.go b/pkg/nonkube/common/site_state_renderer_common.go
index f549f6f6..543c407e 100644
--- a/pkg/nonkube/common/site_state_renderer_common.go
+++ b/pkg/nonkube/common/site_state_renderer_common.go
@@ -64,6 +64,8 @@ func copySiteStateMap[T any](m map[string]T) map[string]T {
 
 func CreateRouterAccess(siteState *api.SiteState) error {
 	if !siteState.HasRouterAccess() {
+		logger := NewLogger()
+		logger.Debug("Creating skupper-local RouterAccess")
 		name := fmt.Sprintf("skupper-local")
 		var port = 5671
 		var err error
@@ -72,6 +74,7 @@ func CreateRouterAccess(siteState *api.SiteState) error {
 			if err != nil {
 				return err
 			}
+			logger.Debug("Free TCP port discovered", "port", port)
 		}
 		siteState.CreateRouterAccess(name, port)
 	}
diff --git a/pkg/nonkube/common/site_state_validator.go b/pkg/nonkube/common/site_state_validator.go
index 4ee60996..92980b31 100644
--- a/pkg/nonkube/common/site_state_validator.go
+++ b/pkg/nonkube/common/site_state_validator.go
@@ -79,7 +79,7 @@ func (s *SiteStateValidator) validateRouterAccesses(routerAccesses map[string]*v
 			}
 		}
 		if len(routerAccess.Spec.Roles) == 0 {
-			return fmt.Errorf("invalid router access: roles are required")
+			return fmt.Errorf("invalid router access: %s - roles are required", routerAccess.Name)
 		}
 		for _, role := range routerAccess.Spec.Roles {
 			if !utils.StringSliceContains(validLinkAccessRoles, role.Name) {
@@ -135,12 +135,12 @@ func (s *SiteStateValidator) validateListeners(listeners map[string]*v2alpha1.Li
 			return fmt.Errorf("invalid listener name: %w", err)
 		}
 		if listener.Spec.Host == "" || listener.Spec.Port == 0 {
-			return fmt.Errorf("host and port are required")
+			return fmt.Errorf("invalid listener: %s - host and port are required", listener.Name)
 		}
 		ip := net.ParseIP(listener.Spec.Host)
 		validHostname := hostnameRfc1123Regex.MatchString(listener.Spec.Host)
 		if ip == nil && !validHostname {
-			return fmt.Errorf("invalid listener host: %s - a valid IP address or hostname is expected", listener.Spec.Host)
+			return fmt.Errorf("invalid listener host: %s - a valid IP address or hostname is expected (listener: %q)", listener.Spec.Host, name)
 		}
 		if utils.IntSliceContains(hostPorts[listener.Spec.Host], listener.Spec.Port) {
 			return fmt.Errorf("port %d is already mapped for host %q (listener: %q)", listener.Spec.Port, listener.Spec.Host, name)
@@ -159,12 +159,12 @@ func (s *SiteStateValidator) validateConnectors(connectors map[string]*v2alpha1.
 			return fmt.Errorf("invalid connector name: %w", err)
 		}
 		if connector.Spec.Host == "" || connector.Spec.Port == 0 {
-			return fmt.Errorf("connector host and port are required")
+			return fmt.Errorf("connector host and port are required (connector: %q)", connector.Name)
 		}
 		ip := net.ParseIP(connector.Spec.Host)
 		validHostname := hostnameRfc1123Regex.MatchString(connector.Spec.Host)
 		if ip == nil && !validHostname {
-			return fmt.Errorf("invalid connector host: %s - a valid IP address or hostname is expected", connector.Spec.Host)
+			return fmt.Errorf("invalid connector host: %s - a valid IP address or hostname is expected (connector: %q)", connector.Spec.Host, connector.Name)
 		}
 		if connector.Spec.RoutingKey == "" {
 			return fmt.Errorf("routingKey is missing for connector: %s", connector.Name)
diff --git a/pkg/nonkube/common/startup.go b/pkg/nonkube/common/startup.go
index 8ba6293a..92404034 100644
--- a/pkg/nonkube/common/startup.go
+++ b/pkg/nonkube/common/startup.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	_ "embed"
 	"fmt"
+	"log/slog"
 	"os"
 	"path"
 	"text/template"
@@ -65,19 +66,22 @@ func GetStartupScripts(args StartupScriptsArgs, pathProvider api.InternalPathPro
 	if pathProvider == nil {
 		pathProvider = api.GetInternalOutputPath
 	}
-	scripts.path = pathProvider(args.Namespace, api.RuntimeScriptsPath)
+	scripts.path = pathProvider(args.Namespace, api.ScriptsPath)
 	return scripts, nil
 }
 
 func (s *startupScripts) Create() error {
 	var startBuf bytes.Buffer
 	var stopBuf bytes.Buffer
+	logger := NewLogger()
+	logger.Debug("creating startup scripts")
 
 	startTemplate := template.Must(template.New("start").Parse(s.StartScript))
 	if err := startTemplate.Execute(&startBuf, s); err != nil {
 		return fmt.Errorf("failed to create start script: %w", err)
 	}
 	startFileName := path.Join(s.path, s.GetStartFileName())
+	logger.Debug("writing start script", slog.String("path", startFileName))
 	err := os.WriteFile(startFileName, startBuf.Bytes(), 0755)
 	if err != nil {
 		return err
@@ -87,6 +91,7 @@ func (s *startupScripts) Create() error {
 		return fmt.Errorf("failed to create stop script: %w", err)
 	}
 	stopFileName := path.Join(s.path, s.GetStopFileName())
+	logger.Debug("writing stop script", slog.String("path", stopFileName))
 	err = os.WriteFile(stopFileName, stopBuf.Bytes(), 0755)
 	if err != nil {
 		return err
diff --git a/pkg/nonkube/common/systemd.go b/pkg/nonkube/common/systemd.go
index 2d6d0bd4..b50647c8 100644
--- a/pkg/nonkube/common/systemd.go
+++ b/pkg/nonkube/common/systemd.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	_ "embed"
 	"fmt"
+	"log/slog"
 	"os"
 	"os/exec"
 	"path"
@@ -51,8 +52,8 @@ type systemdServiceInfo struct {
 func NewSystemdServiceInfo(siteState *api.SiteState, platform string) (SystemdService, error) {
 	site := siteState.Site
 	siteHomePath := api.GetHostSiteHome(site)
-	siteScriptPath := path.Join(siteHomePath, string(api.RuntimeScriptsPath))
-	siteConfigPath := path.Join(siteHomePath, string(api.ConfigRouterPath))
+	siteScriptPath := path.Join(siteHomePath, string(api.ScriptsPath))
+	siteConfigPath := path.Join(siteHomePath, string(api.RouterConfigPath))
 	namespace := site.Namespace
 	if namespace == "" {
 		namespace = "default"
@@ -83,9 +84,11 @@ func (s *systemdServiceInfo) Create() error {
 		}
 		return fmt.Errorf(msg)
 	}
-
+	var logger = NewLogger()
+	logger.Debug("creating systemd service")
 	var buf = new(bytes.Buffer)
 	var service *template.Template
+	logger.Debug("using service template for:", slog.String("platform", s.platform))
 	if s.platform == string(types.PlatformSystemd) {
 		service = template.Must(template.New(s.GetServiceName()).Parse(SystemdServiceTemplate))
 	} else {
@@ -106,6 +109,7 @@ func (s *systemdServiceInfo) Create() error {
 
 	// Saving systemd user service
 	serviceName := s.GetServiceName()
+	logger.Debug("writing service file", slog.String("path", s.getServiceFile()))
 	err = os.WriteFile(s.getServiceFile(), buf.Bytes(), 0644)
 	if err != nil {
 		return fmt.Errorf("unable to write unit file (%s): %w", s.getServiceFile(), err)
@@ -113,6 +117,7 @@ func (s *systemdServiceInfo) Create() error {
 
 	// Only enable when running locally
 	if !api.IsRunningInContainer() {
+		logger.Debug("enabling systemd service", slog.String("name", serviceName))
 		return s.enableService(serviceName)
 	}
 
@@ -121,7 +126,7 @@ func (s *systemdServiceInfo) Create() error {
 
 func (s *systemdServiceInfo) getServiceFile() string {
 	if api.IsRunningInContainer() {
-		return path.Join(api.GetInternalOutputPath(s.Site.Namespace, api.RuntimeScriptsPath), s.GetServiceName())
+		return path.Join(api.GetInternalOutputPath(s.Site.Namespace, api.ScriptsPath), s.GetServiceName())
 	}
 	if s.getUid() == 0 {
 		return path.Join(s.rootSystemdBasePath, s.GetServiceName())
@@ -134,25 +139,32 @@ func (s *systemdServiceInfo) Remove() error {
 		return fmt.Errorf("SystemD is not enabled at user level")
 	}
 
+	logger := NewLogger()
+
 	// Stopping systemd user service
 	if !api.IsRunningInContainer() {
+		logger.Debug("stopping service", slog.String("name", s.GetServiceName()))
 		cmd := s.getCmdStopSystemdService(s.GetServiceName())
 		_ = cmd.Run()
 
 		// Disabling systemd user service
+		logger.Debug("disabling service", slog.String("name", s.GetServiceName()))
 		cmd = s.getCmdDisableSystemdService(s.GetServiceName())
 		_ = cmd.Run()
 	}
 
 	// Removing the .service file
+	logger.Debug("removing service", slog.String("path", s.getServiceFile()))
 	_ = os.Remove(s.getServiceFile())
 
 	// Reloading systemd user daemon
 	if !api.IsRunningInContainer() {
+		logger.Debug("reloading systemd daemon")
 		cmd := s.getCmdReloadSystemdDaemon()
 		_ = cmd.Run()
 
 		// Resetting failed status
+		logger.Debug("resetting failed systemd service", slog.String("name", s.GetServiceName()))
 		cmd = s.getCmdResetFailedSystemService(s.GetServiceName())
 		_ = cmd.Run()
 	}
diff --git a/pkg/nonkube/compat/site_state_renderer.go b/pkg/nonkube/compat/site_state_renderer.go
index 9b250bb3..cab9b754 100644
--- a/pkg/nonkube/compat/site_state_renderer.go
+++ b/pkg/nonkube/compat/site_state_renderer.go
@@ -2,7 +2,9 @@ package compat
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"log/slog"
 	"os"
 	"path"
 	"time"
@@ -28,6 +30,7 @@ type SiteStateRenderer struct {
 
 func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) error {
 	var err error
+	var logger = common.NewLogger()
 	var validator api.SiteStateValidator = &common.SiteStateValidator{}
 	err = validator.Validate(loadedSiteState)
 	if err != nil {
@@ -47,29 +50,43 @@ func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool)
 		// when reload is successful, backupData must be nil
 		if backupData == nil {
 			for _, temporaryName := range s.stoppedContainers {
+				logger.Debug("Removing temporary container ", slog.String("name", temporaryName))
 				err = s.cli.ContainerRemove(temporaryName)
 				if err != nil {
-					fmt.Printf("Failed to remove temporary container %s: %v\n", temporaryName, err)
+					logger.Error("Failed to remove temporary container",
+						slog.String("name", temporaryName),
+						slog.String("error", err.Error()),
+					)
 				}
 			}
 			return
 		}
-		fmt.Println("Bootstrap failed, restoring previous state")
+		logger.Error("Bootstrap failed, restoring namespace")
 		err := common.RestoreNamespaceData(backupData)
 		if err != nil {
-			fmt.Printf("Error restoring namespace data for %q - %s\n", loadedSiteState.GetNamespace(), err)
+			logger.Error("Error restoring namespace data:",
+				slog.String("namespace", loadedSiteState.GetNamespace()),
+				slog.String("error", err.Error()),
+			)
 			return
 		}
 		for originalName, temporaryName := range s.stoppedContainers {
 			if temporaryName != originalName {
 				err = s.cli.ContainerRename(temporaryName, originalName)
 				if err != nil {
-					fmt.Printf("Error restoring container name from %q to %q - %s\n", temporaryName, originalName, err)
+					logger.Error("Error restoring container:",
+						slog.String("temporary", temporaryName),
+						slog.String("name", originalName),
+						slog.String("error", err.Error()),
+					)
 				}
 			}
 			err = s.cli.ContainerStart(originalName)
 			if err != nil {
-				fmt.Printf("Error starting container %q - %s\n", originalName, err)
+				logger.Error("Error starting container",
+					slog.String("name", originalName),
+					slog.String("error", err.Error()),
+				)
 			}
 		}
 	}()
@@ -195,13 +212,13 @@ func (s *SiteStateRenderer) prepareContainers() error {
 		},
 		FileMounts: []container.FileMount{
 			{
-				Source:      path.Join(siteConfigPath, "config/router"),
+				Source:      path.Join(siteConfigPath, string(api.RouterConfigPath)),
 				Destination: "/etc/skupper-router/config",
 				Options:     []string{"z"},
 			},
 			{
-				Source:      path.Join(siteConfigPath, "certificates"),
-				Destination: "/etc/skupper-router/certificates",
+				Source:      path.Join(siteConfigPath, string(api.CertificatesBasePath)),
+				Destination: "/etc/skupper-router/runtime/certs",
 				Options:     []string{"z"},
 			},
 		},
@@ -211,12 +228,27 @@ func (s *SiteStateRenderer) prepareContainers() error {
 		//      validate whether CPU and memory thresholds can be
 		//      set to the container
 	}
+	logger := common.NewLogger()
+	if logger.Enabled(nil, slog.LevelDebug) {
+		for name, newContainer := range s.containers {
+			containerJson, _ := json.Marshal(newContainer)
+			logger.Debug("container prepared:",
+				slog.String("name", name),
+				slog.String("container", string(containerJson)),
+			)
+		}
+	}
 	return nil
 }
 
 func (s *SiteStateRenderer) pullImages(ctx context.Context) error {
 	var err error
+	var logger = common.NewLogger()
 	for component, skupperContainer := range s.containers {
+		logger.Debug("pulling:",
+			slog.String("image", skupperContainer.Image),
+			slog.String("component", component),
+		)
 		err = s.cli.ImagePull(ctx, skupperContainer.Image)
 		if err != nil {
 			return fmt.Errorf("failed to pull %s image %s: %w", component, skupperContainer.Image, err)
@@ -245,7 +277,12 @@ func (s *SiteStateRenderer) createContainers() error {
 			return fmt.Errorf("container %s already exists (component: %s)", skupperContainer.Name, component)
 		}
 	}
+	logger := common.NewLogger()
 	for component, skupperContainer := range s.containers {
+		logger.Debug("creating container",
+			slog.String("component", component),
+			slog.String("name", skupperContainer.Name),
+		)
 		err = s.cli.ContainerCreate(&skupperContainer)
 		if err != nil {
 			return fmt.Errorf("failed to create %q container (%s): %w", component, skupperContainer.Name, err)
@@ -256,8 +293,13 @@ func (s *SiteStateRenderer) createContainers() error {
 
 func (s *SiteStateRenderer) startContainers() error {
 	var err error
+	var logger = common.NewLogger()
 	defer s.cleanupContainers(err)
 	for component, skupperContainer := range s.containers {
+		logger.Debug("starting container",
+			slog.String("component", component),
+			slog.String("name", skupperContainer.Name),
+		)
 		err = s.cli.ContainerStart(skupperContainer.Name)
 		if err != nil {
 			return fmt.Errorf("failed to start %s container %q: %w", component, skupperContainer.Name, err)
diff --git a/pkg/nonkube/systemd/site_state_renderer.go b/pkg/nonkube/systemd/site_state_renderer.go
index 6e0af91a..6f1d42f9 100644
--- a/pkg/nonkube/systemd/site_state_renderer.go
+++ b/pkg/nonkube/systemd/site_state_renderer.go
@@ -2,6 +2,7 @@ package systemd
 
 import (
 	"fmt"
+	"log/slog"
 	"os"
 
 	"github.com/skupperproject/skupper/api/types"
@@ -18,6 +19,7 @@ type SiteStateRenderer struct {
 
 func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) error {
 	var err error
+	var logger = common.NewLogger()
 	var validator api.SiteStateValidator = &common.SiteStateValidator{}
 	err = validator.Validate(loadedSiteState)
 	if err != nil {
@@ -34,15 +36,20 @@ func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool)
 		if backupData == nil {
 			return
 		}
-		fmt.Println("Bootstrap failed, restoring previous state")
+		logger.Error("Bootstrap failed, restoring namespace")
 		err := common.RestoreNamespaceData(backupData)
 		if err != nil {
-			fmt.Printf("Error restoring namespace data for %q - %s\n", loadedSiteState.GetNamespace(), err)
+			logger.Error("Error restoring namespace data:",
+				slog.String("namespace", loadedSiteState.GetNamespace()),
+				slog.String("error", err.Error()),
+			)
 			return
 		}
 		err = s.createSystemdService()
 		if err != nil {
-			fmt.Printf("Error recovering systemd service info - %s\n", err)
+			logger.Error("Error recovering systemd service info:",
+				slog.String("error", err.Error()),
+			)
 		}
 	}()
 
diff --git a/pkg/qdr/amqp_mgmt.go b/pkg/qdr/amqp_mgmt.go
index 647dd666..4811d9f2 100644
--- a/pkg/qdr/amqp_mgmt.go
+++ b/pkg/qdr/amqp_mgmt.go
@@ -119,7 +119,7 @@ func (r Record) AsRecord(field string) Record {
 }
 
 func asTcpEndpoint(record Record) TcpEndpoint {
-	return TcpEndpoint{
+	endpoint := TcpEndpoint{
 		Name:       record.AsString("name"),
 		Host:       record.AsString("host"),
 		Port:       record.AsString("port"),
@@ -128,6 +128,12 @@ func asTcpEndpoint(record Record) TcpEndpoint {
 		SslProfile: record.AsString("sslProfile"),
 		ProcessID:  record.AsString("processId"),
 	}
+	if value, ok := record["verifyHostname"]; ok {
+		if verify, ok := value.(bool); ok {
+			endpoint.VerifyHostname = &verify
+		}
+	}
+	return endpoint
 }
 
 func asConnection(record Record) Connection {
diff --git a/pkg/qdr/qdr.go b/pkg/qdr/qdr.go
index e762399e..c3785d3b 100644
--- a/pkg/qdr/qdr.go
+++ b/pkg/qdr/qdr.go
@@ -210,6 +210,17 @@ func (r *RouterConfig) RemoveSslProfile(name string) bool {
 	}
 }
 
+func (r *RouterConfig) RemoveUnreferencedSslProfiles() bool {
+	unreferenced := r.UnreferencedSslProfiles()
+	changed := false
+	for _, profile := range unreferenced {
+		if r.RemoveSslProfile(profile.Name) {
+			changed = true
+		}
+	}
+	return changed
+}
+
 func (r *RouterConfig) UnreferencedSslProfiles() map[string]SslProfile {
 	results := map[string]SslProfile{}
 	for _, profile := range r.SslProfiles {
@@ -824,9 +835,19 @@ func equivalentHost(a string, b string) bool {
 	}
 }
 
+func (a TcpEndpoint) equivalentVerifyHostname(b TcpEndpoint) bool {
+	if a.VerifyHostname == nil {
+		return b.VerifyHostname == nil || *b.VerifyHostname == true
+	}
+	if b.VerifyHostname == nil {
+		return a.VerifyHostname == nil || *a.VerifyHostname == true
+	}
+	return *a.VerifyHostname == *b.VerifyHostname
+}
+
 func (a TcpEndpoint) Equivalent(b TcpEndpoint) bool {
 	if !equivalentHost(a.Host, b.Host) || a.Port != b.Port || a.Address != b.Address ||
-		a.SiteId != b.SiteId || a.ProcessID != b.ProcessID {
+		a.SiteId != b.SiteId || a.ProcessID != b.ProcessID || !a.equivalentVerifyHostname(b) {
 		return false
 	}
 	return true
diff --git a/pkg/site/bindings.go b/pkg/site/bindings.go
index 046c5986..99dcd722 100644
--- a/pkg/site/bindings.go
+++ b/pkg/site/bindings.go
@@ -22,20 +22,22 @@ type ConnectorFunction func(*skupperv2alpha1.Connector) *skupperv2alpha1.Connect
 type ListenerFunction func(*skupperv2alpha1.Listener) *skupperv2alpha1.Listener
 
 type Bindings struct {
-	SiteId     string
-	connectors map[string]*skupperv2alpha1.Connector
-	listeners  map[string]*skupperv2alpha1.Listener
-	handler    BindingEventHandler
-	configure  struct {
+	SiteId      string
+	profilePath string
+	connectors  map[string]*skupperv2alpha1.Connector
+	listeners   map[string]*skupperv2alpha1.Listener
+	handler     BindingEventHandler
+	configure   struct {
 		listener  ListenerConfiguration
 		connector ConnectorConfiguration
 	}
 }
 
-func NewBindings() *Bindings {
+func NewBindings(profilePath string) *Bindings {
 	bindings := &Bindings{
-		connectors: map[string]*skupperv2alpha1.Connector{},
-		listeners:  map[string]*skupperv2alpha1.Listener{},
+		profilePath: profilePath,
+		connectors:  map[string]*skupperv2alpha1.Connector{},
+		listeners:   map[string]*skupperv2alpha1.Listener{},
 	}
 	bindings.configure.listener = UpdateBridgeConfigForListener
 	bindings.configure.connector = UpdateBridgeConfigForConnector
@@ -138,11 +140,11 @@ func (b *Bindings) updateListener(latest *skupperv2alpha1.Listener) qdr.ConfigUp
 	name := latest.ObjectMeta.Name
 	existing, ok := b.listeners[name]
 	b.listeners[name] = latest
-	if b.handler != nil {
-		b.handler.ListenerUpdated(latest)
-	}
 
 	if !ok || !reflect.DeepEqual(existing.Spec, latest.Spec) {
+		if b.handler != nil {
+			b.handler.ListenerUpdated(latest)
+		}
 		return b
 	}
 	return nil
@@ -174,8 +176,41 @@ func (b *Bindings) ToBridgeConfig() qdr.BridgeConfig {
 	return config
 }
 
+func (b *Bindings) AddSslProfiles(config *qdr.RouterConfig) bool {
+	profiles := map[string]qdr.SslProfile{}
+	for _, c := range b.connectors {
+		if c.Spec.TlsCredentials != "" {
+			if c.Spec.NoClientAuth {
+				//if only ca is used, need to qualify the profile to ensure that it does not collide with
+				// use of the same secret where client auth *is* required
+				name := getSslProfileName(c)
+				if _, ok := profiles[name]; !ok {
+					profiles[name] = qdr.ConfigureSslProfile(name, b.profilePath, false)
+				}
+			} else {
+				if _, ok := profiles[c.Spec.TlsCredentials]; !ok {
+					profiles[c.Spec.TlsCredentials] = qdr.ConfigureSslProfile(c.Spec.TlsCredentials, b.profilePath, true)
+				}
+			}
+		}
+	}
+	for _, l := range b.listeners {
+		if _, ok := profiles[l.Spec.TlsCredentials]; l.Spec.TlsCredentials != "" && !ok {
+			profiles[l.Spec.TlsCredentials] = qdr.ConfigureSslProfile(l.Spec.TlsCredentials, b.profilePath, true)
+		}
+	}
+	changed := false
+	for _, profile := range profiles {
+		if config.AddSslProfile(profile) {
+			changed = true
+		}
+	}
+	return changed
+}
+
 func (b *Bindings) Apply(config *qdr.RouterConfig) bool {
-	//TODO: add/remove SslProfiles as necessary
+	b.AddSslProfiles(config)
 	config.UpdateBridgeConfig(b.ToBridgeConfig())
+	config.RemoveUnreferencedSslProfiles()
 	return true //TODO: can optimise by indicating if no change was required
 }
diff --git a/pkg/site/connector.go b/pkg/site/connector.go
index 32e77c24..7c90a7a8 100644
--- a/pkg/site/connector.go
+++ b/pkg/site/connector.go
@@ -21,15 +21,32 @@ func updateBridgeConfigForConnector(siteId string, connector *skupperv2alpha1.Co
 	name := connector.Name + "-" + host
 	if connector.Spec.Type == "tcp" || connector.Spec.Type == "" {
 		config.AddTcpConnector(qdr.TcpEndpoint{
-			Name:       name,
-			SiteId:     siteId,
-			Host:       host,
-			Port:       strconv.Itoa(connector.Spec.Port),
-			Address:    connector.Spec.RoutingKey,
-			SslProfile: connector.Spec.TlsCredentials,
-			ProcessID:  processID,
-			//TODO:
-			//VerifyHostname
+			Name:           name,
+			SiteId:         siteId,
+			Host:           host,
+			Port:           strconv.Itoa(connector.Spec.Port),
+			Address:        connector.Spec.RoutingKey,
+			SslProfile:     getSslProfileName(connector),
+			ProcessID:      processID,
+			VerifyHostname: getVerifyHostname(connector),
 		})
 	}
 }
+
+func getSslProfileName(connector *skupperv2alpha1.Connector) string {
+	if connector.Spec.TlsCredentials == "" {
+		return ""
+	}
+	if connector.Spec.NoClientAuth {
+		return connector.Spec.TlsCredentials + "-profile"
+	}
+	return connector.Spec.TlsCredentials
+}
+
+func getVerifyHostname(connector *skupperv2alpha1.Connector) *bool {
+	if connector.Spec.TlsCredentials == "" {
+		return nil
+	}
+	value := connector.Spec.VerifyHostname
+	return &value
+}
diff --git a/pkg/site/listener.go b/pkg/site/listener.go
index 7f211c61..1a6ea656 100644
--- a/pkg/site/listener.go
+++ b/pkg/site/listener.go
@@ -21,8 +21,6 @@ func UpdateBridgeConfigForListenerWithHostAndPort(siteId string, listener *skupp
 			Port:       strconv.Itoa(port),
 			Address:    listener.Spec.RoutingKey,
 			SslProfile: listener.Spec.TlsCredentials,
-			//TODO:
-			//VerifyHostname
 		})
 	}
 }
diff --git a/pkg/site/siteconfig.go b/pkg/site/siteconfig.go
deleted file mode 100644
index fa53e48e..00000000
--- a/pkg/site/siteconfig.go
+++ /dev/null
@@ -1,861 +0,0 @@
-package site
-
-import (
-	"fmt"
-	"regexp"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/skupperproject/skupper/pkg/qdr"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	"github.com/skupperproject/skupper/api/types"
-)
-
-const (
-	// core options
-	SiteConfigNameKey                string = "name"
-	SiteConfigRouterModeKey          string = "router-mode"
-	SiteConfigIngressKey             string = "ingress"
-	SiteConfigIngressAnnotationsKey  string = "ingress-annotations"
-	SiteConfigIngressHostKey         string = "ingress-host"
-	SiteConfigCreateNetworkPolicyKey string = "create-network-policy"
-	SiteConfigRoutersKey             string = "routers"
-	SiteConfigRunAsUserKey           string = "run-as-user"
-	SiteConfigRunAsGroupKey          string = "run-as-group"
-	SiteConfigClusterPermissionsKey  string = "cluster-permissions"
-	SiteConfigServiceAccountKey      string = "service-account"
-
-	// console options
-	SiteConfigConsoleKey               string = "console"
-	SiteConfigConsoleAuthenticationKey string = "console-authentication"
-	SiteConfigConsoleUserKey           string = "console-user"
-	SiteConfigConsolePasswordKey       string = "console-password"
-	SiteConfigConsoleIngressKey        string = "console-ingress"
-	SiteConfigRestAPIKey               string = "rest-api"
-
-	// flow collector options
-	SiteConfigFlowCollectorKey            string = "flow-collector"
-	SiteConfigFlowCollectorRecordTtlKey   string = "flow-collector-record-ttl"
-	SiteConfigFlowCollectorCpuKey         string = "flow-collector-cpu"
-	SiteConfigFlowCollectorMemoryKey      string = "flow-collector-memory"
-	SiteConfigFlowCollectorCpuLimitKey    string = "flow-collector-cpu-limit"
-	SiteConfigFlowCollectorMemoryLimitKey string = "flow-collector-memory-limit"
-
-	// prometheus server options
-	SiteConfigPrometheusExternalServerKey       string = "prometheus-external-server"
-	SiteConfigPrometheusServerAuthenticationKey string = "prometheus-server-authentication"
-	SiteConfigPrometheusServerUserKey           string = "prometheus-server-user"
-	SiteConfigPrometheusServerPasswordKey       string = "prometheus-server-password"
-	SiteConfigPrometheusServerCpuKey            string = "prometheus-server-cpu"
-	SiteConfigPrometheusServerMemoryKey         string = "prometheus-server-memory"
-	SiteConfigPrometheusServerCpuLimitKey       string = "prometheus-server-cpu-limit"
-	SiteConfigPrometheusServerMemoryLimitKey    string = "prometheus-server-memory-limit"
-	SiteConfigPrometheusServerPodAnnotationsKey string = "prometheus-server-pod-annotations"
-
-	// router options
-	SiteConfigRouterConsoleKey             string = "router-console"
-	SiteConfigRouterLoggingKey             string = "router-logging"
-	SiteConfigRouterCpuKey                 string = "router-cpu"
-	SiteConfigRouterMemoryKey              string = "router-memory"
-	SiteConfigRouterCpuLimitKey            string = "router-cpu-limit"
-	SiteConfigRouterMemoryLimitKey         string = "router-memory-limit"
-	SiteConfigRouterAffinityKey            string = "router-pod-affinity"
-	SiteConfigRouterAntiAffinityKey        string = "router-pod-antiaffinity"
-	SiteConfigRouterNodeSelectorKey        string = "router-node-selector"
-	SiteConfigRouterMaxFrameSizeKey        string = "xp-router-max-frame-size"
-	SiteConfigRouterMaxSessionFramesKey    string = "xp-router-max-session-frames"
-	SiteConfigRouterDataConnectionCountKey string = "router-data-connection-count"
-	SiteConfigRouterIngressHostKey         string = "router-ingress-host"
-	SiteConfigRouterServiceAnnotationsKey  string = "router-service-annotations"
-	SiteConfigRouterPodAnnotationsKey      string = "router-pod-annotations"
-	SiteConfigRouterLoadBalancerIp         string = "router-load-balancer-ip"
-	SiteConfigRouterDisableMutualTLS       string = "router-disable-mutual-tls"
-
-	// controller options
-	SiteConfigServiceControllerKey            string = "service-controller"
-	SiteConfigServiceSyncKey                  string = "service-sync"
-	SiteConfigServiceSyncSiteTtlKey           string = "service-sync-site-ttl"
-	SiteConfigControllerCpuKey                string = "controller-cpu"
-	SiteConfigControllerMemoryKey             string = "controller-memory"
-	SiteConfigControllerCpuLimitKey           string = "controller-cpu-limit"
-	SiteConfigControllerMemoryLimitKey        string = "controller-memory-limit"
-	SiteConfigControllerAffinityKey           string = "controller-pod-affinity"
-	SiteConfigControllerAntiAffinityKey       string = "controller-pod-antiaffinity"
-	SiteConfigControllerNodeSelectorKey       string = "controller-node-selector"
-	SiteConfigControllerIngressHostKey        string = "controller-ingress-host"
-	SiteConfigControllerServiceAnnotationsKey string = "controller-service-annotations"
-	SiteConfigControllerPodAnnotationsKey     string = "controller-pod-annotations"
-	SiteConfigControllerLoadBalancerIp        string = "controller-load-balancer-ip"
-
-	// config-sync options
-	SiteConfigConfigSyncCpuKey         string = "config-sync-cpu"
-	SiteConfigConfigSyncMemoryKey      string = "config-sync-memory"
-	SiteConfigConfigSyncCpuLimitKey    string = "config-sync-cpu-limit"
-	SiteConfigConfigSyncMemoryLimitKey string = "config-sync-memory-limit"
-
-	SiteConfigEnableSkupperEventsKey string = "enable-skupper-events"
-
-	//labels:
-	ValidRfc1123Label                = `^(` + ValidRfc1123LabelKey + `)+=(` + ValidRfc1123LabelValue + `)+(,(` + ValidRfc1123LabelKey + `)+=(` + ValidRfc1123LabelValue + `)+)*$`
-	ValidRfc1123LabelKey             = "[a-z0-9]([-._a-z0-9]*[a-z0-9])*"
-	ValidRfc1123LabelValue           = "[a-zA-Z0-9]([-._a-zA-Z0-9]*[a-zA-Z0-9])*"
-	DefaultSkupperExtraLabels string = ""
-)
-
-func WriteSiteConfig(spec types.SiteConfigSpec, namespace string) (*corev1.ConfigMap, error) {
-	var errs []string
-	siteConfig := &corev1.ConfigMap{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "v1",
-			Kind:       "ConfigMap",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:        types.SiteConfigMapName,
-			Annotations: spec.Annotations,
-			Labels:      spec.Labels,
-		},
-		Data: map[string]string{
-			SiteConfigNameKey:                  namespace,
-			SiteConfigRouterModeKey:            string(types.TransportModeInterior),
-			SiteConfigServiceControllerKey:     "true",
-			SiteConfigServiceSyncKey:           "true",
-			SiteConfigConsoleKey:               "false",
-			SiteConfigFlowCollectorKey:         "false",
-			SiteConfigClusterPermissionsKey:    "false",
-			SiteConfigRouterConsoleKey:         "false",
-			SiteConfigRouterLoggingKey:         "",
-			SiteConfigConsoleAuthenticationKey: string(types.ConsoleAuthModeInternal),
-			SiteConfigConsoleUserKey:           "",
-			SiteConfigConsolePasswordKey:       "",
-			SiteConfigIngressKey:               types.IngressLoadBalancerString,
-		},
-	}
-	if spec.SkupperName != "" {
-		siteConfig.Data[SiteConfigNameKey] = spec.SkupperName
-	}
-	if spec.RouterMode != "" {
-		siteConfig.Data[SiteConfigRouterModeKey] = spec.RouterMode
-	}
-	if spec.Routers != 0 {
-		siteConfig.Data[SiteConfigRoutersKey] = strconv.Itoa(spec.Routers)
-	}
-	if !spec.EnableController {
-		siteConfig.Data[SiteConfigServiceControllerKey] = "false"
-	}
-	if !spec.EnableServiceSync {
-		siteConfig.Data[SiteConfigServiceSyncKey] = "false"
-	}
-	if spec.SiteTtl != 0 {
-		siteConfig.Data[SiteConfigServiceSyncSiteTtlKey] = spec.SiteTtl.String()
-	}
-	if spec.EnableConsole {
-		siteConfig.Data[SiteConfigConsoleKey] = "true"
-	}
-	if spec.EnableRestAPI {
-		siteConfig.Data[SiteConfigRestAPIKey] = "true"
-	}
-	if spec.EnableFlowCollector {
-		siteConfig.Data[SiteConfigFlowCollectorKey] = "true"
-	}
-	if spec.AuthMode != "" {
-		siteConfig.Data[SiteConfigConsoleAuthenticationKey] = spec.AuthMode
-	}
-	if spec.User != "" {
-		siteConfig.Data[SiteConfigConsoleUserKey] = spec.User
-	}
-	if spec.Password != "" {
-		siteConfig.Data[SiteConfigConsolePasswordKey] = spec.Password
-	}
-	if spec.Ingress != "" {
-		siteConfig.Data[SiteConfigIngressKey] = spec.Ingress
-	}
-	if len(spec.IngressAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.IngressAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigIngressAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if spec.ConsoleIngress != "" {
-		siteConfig.Data[SiteConfigConsoleIngressKey] = spec.ConsoleIngress
-	}
-	if spec.IngressHost != "" {
-		siteConfig.Data[SiteConfigIngressHostKey] = spec.IngressHost
-	}
-	if spec.CreateNetworkPolicy {
-		siteConfig.Data[SiteConfigCreateNetworkPolicyKey] = "true"
-	}
-	if spec.RunAsUser != 0 {
-		siteConfig.Data[SiteConfigRunAsUserKey] = strconv.FormatInt(spec.RunAsUser, 10)
-	}
-	if spec.RunAsGroup != 0 {
-		siteConfig.Data[SiteConfigRunAsGroupKey] = strconv.FormatInt(spec.RunAsGroup, 10)
-	}
-	if spec.EnableClusterPermissions {
-		siteConfig.Data[SiteConfigClusterPermissionsKey] = "true"
-	}
-	if spec.Router.Logging != nil {
-		siteConfig.Data[SiteConfigRouterLoggingKey] = qdr.RouterLogConfigToString(spec.Router.Logging)
-	}
-	if spec.Router.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.Router.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterCpuKey, spec.Router.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterCpuKey] = spec.Router.Cpu
-		}
-	}
-	if spec.Router.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.Router.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterMemoryKey, spec.Router.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterMemoryKey] = spec.Router.Memory
-		}
-	}
-	if spec.Router.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Router.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterCpuLimitKey, spec.Router.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterCpuLimitKey] = spec.Router.CpuLimit
-		}
-	}
-	if spec.Router.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Router.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterMemoryLimitKey, spec.Router.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterMemoryLimitKey] = spec.Router.MemoryLimit
-		}
-	}
-	if spec.Router.Affinity != "" {
-		siteConfig.Data[SiteConfigRouterAffinityKey] = spec.Router.Affinity
-	}
-	if spec.Router.AntiAffinity != "" {
-		siteConfig.Data[SiteConfigRouterAntiAffinityKey] = spec.Router.AntiAffinity
-	}
-	if spec.Router.NodeSelector != "" {
-		siteConfig.Data[SiteConfigRouterNodeSelectorKey] = spec.Router.NodeSelector
-	}
-	if spec.Router.IngressHost != "" {
-		siteConfig.Data[SiteConfigRouterIngressHostKey] = spec.Router.IngressHost
-	}
-	if spec.Router.MaxFrameSize != types.RouterMaxFrameSizeDefault {
-		siteConfig.Data[SiteConfigRouterMaxFrameSizeKey] = strconv.Itoa(spec.Router.MaxFrameSize)
-	}
-	if spec.Router.MaxSessionFrames != types.RouterMaxSessionFramesDefault {
-		siteConfig.Data[SiteConfigRouterMaxSessionFramesKey] = strconv.Itoa(spec.Router.MaxSessionFrames)
-	}
-	if spec.Router.DataConnectionCount != "" {
-		siteConfig.Data[SiteConfigRouterDataConnectionCountKey] = spec.Router.DataConnectionCount
-	}
-	if len(spec.Router.ServiceAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Router.ServiceAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigRouterServiceAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if len(spec.Router.PodAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Router.PodAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigRouterPodAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if spec.Router.LoadBalancerIp != "" {
-		siteConfig.Data[SiteConfigRouterLoadBalancerIp] = spec.Router.LoadBalancerIp
-	}
-	if spec.Router.DisableMutualTLS {
-		siteConfig.Data[SiteConfigRouterDisableMutualTLS] = "true"
-	}
-	if spec.Controller.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerCpuKey, spec.Controller.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerCpuKey] = spec.Controller.Cpu
-		}
-	}
-	if spec.Controller.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerMemoryKey, spec.Controller.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerMemoryKey] = spec.Controller.Memory
-		}
-	}
-	if spec.Controller.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerCpuLimitKey, spec.Controller.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerCpuLimitKey] = spec.Controller.CpuLimit
-		}
-	}
-	if spec.Controller.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerMemoryLimitKey, spec.Controller.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerMemoryLimitKey] = spec.Controller.MemoryLimit
-		}
-	}
-	if spec.Controller.Affinity != "" {
-		siteConfig.Data[SiteConfigControllerAffinityKey] = spec.Controller.Affinity
-	}
-	if spec.Controller.AntiAffinity != "" {
-		siteConfig.Data[SiteConfigControllerAntiAffinityKey] = spec.Controller.AntiAffinity
-	}
-	if spec.Controller.NodeSelector != "" {
-		siteConfig.Data[SiteConfigControllerNodeSelectorKey] = spec.Controller.NodeSelector
-	}
-	if spec.Controller.IngressHost != "" {
-		siteConfig.Data[SiteConfigControllerIngressHostKey] = spec.Controller.IngressHost
-	}
-	if len(spec.Controller.ServiceAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Controller.ServiceAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigControllerServiceAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if len(spec.Controller.PodAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Controller.PodAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigControllerPodAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if spec.Controller.LoadBalancerIp != "" {
-		siteConfig.Data[SiteConfigControllerLoadBalancerIp] = spec.Controller.LoadBalancerIp
-	}
-
-	if spec.ConfigSync.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncCpuKey, spec.ConfigSync.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncCpuKey] = spec.ConfigSync.Cpu
-		}
-	}
-	if spec.ConfigSync.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncMemoryKey, spec.ConfigSync.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncMemoryKey] = spec.ConfigSync.Memory
-		}
-	}
-	if spec.ConfigSync.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncCpuLimitKey, spec.ConfigSync.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncCpuLimitKey] = spec.ConfigSync.CpuLimit
-		}
-	}
-	if spec.ConfigSync.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncMemoryLimitKey, spec.ConfigSync.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncMemoryLimitKey] = spec.ConfigSync.MemoryLimit
-		}
-	}
-
-	if spec.FlowCollector.FlowRecordTtl != 0 {
-		siteConfig.Data[SiteConfigFlowCollectorRecordTtlKey] = spec.FlowCollector.FlowRecordTtl.String()
-	}
-	if spec.FlowCollector.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorCpuKey, spec.FlowCollector.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorCpuKey] = spec.FlowCollector.Cpu
-		}
-	}
-	if spec.FlowCollector.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorMemoryKey, spec.FlowCollector.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorMemoryKey] = spec.FlowCollector.Memory
-		}
-	}
-	if spec.FlowCollector.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorCpuLimitKey, spec.FlowCollector.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorCpuLimitKey] = spec.FlowCollector.CpuLimit
-		}
-	}
-	if spec.FlowCollector.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorMemoryLimitKey, spec.FlowCollector.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorMemoryLimitKey] = spec.FlowCollector.MemoryLimit
-		}
-	}
-
-	if spec.EnableSkupperEvents {
-		siteConfig.Data[SiteConfigEnableSkupperEventsKey] = "true"
-	} else {
-		siteConfig.Data[SiteConfigEnableSkupperEventsKey] = "false"
-	}
-
-	if spec.PrometheusServer.ExternalServer != "" {
-		siteConfig.Data[SiteConfigPrometheusExternalServerKey] = spec.PrometheusServer.ExternalServer
-	}
-	if spec.PrometheusServer.AuthMode != "" {
-		siteConfig.Data[SiteConfigPrometheusServerAuthenticationKey] = spec.PrometheusServer.AuthMode
-	}
-	if spec.PrometheusServer.User != "" {
-		siteConfig.Data[SiteConfigPrometheusServerUserKey] = spec.PrometheusServer.User
-	}
-	if spec.PrometheusServer.Password != "" {
-		siteConfig.Data[SiteConfigPrometheusServerPasswordKey] = spec.PrometheusServer.Password
-	}
-	if spec.PrometheusServer.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerCpuKey, spec.PrometheusServer.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerCpuKey] = spec.PrometheusServer.Cpu
-		}
-	}
-	if spec.PrometheusServer.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerMemoryKey, spec.PrometheusServer.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerMemoryKey] = spec.PrometheusServer.Memory
-		}
-	}
-	if spec.PrometheusServer.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerCpuLimitKey, spec.PrometheusServer.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerCpuLimitKey] = spec.PrometheusServer.CpuLimit
-		}
-	}
-	if spec.PrometheusServer.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerMemoryLimitKey, spec.PrometheusServer.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerMemoryLimitKey] = spec.PrometheusServer.MemoryLimit
-		}
-	}
-	if len(spec.PrometheusServer.PodAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.PrometheusServer.PodAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigPrometheusServerPodAnnotationsKey] = strings.Join(annotations, ",")
-	}
-
-	// TODO: allow Replicas to be set through skupper-site configmap?
-	if !spec.SiteControlled {
-		if siteConfig.ObjectMeta.Labels == nil {
-			siteConfig.ObjectMeta.Labels = map[string]string{}
-		}
-		siteConfig.ObjectMeta.Labels[types.SiteControllerIgnore] = "true"
-	}
-	if DefaultSkupperExtraLabels != "" {
-		labelRegex := regexp.MustCompile(ValidRfc1123Label)
-		if labelRegex.MatchString(DefaultSkupperExtraLabels) {
-			s := strings.Split(DefaultSkupperExtraLabels, ",")
-			for _, kv := range s {
-				parts := strings.Split(kv, "=")
-				if len(parts) > 1 {
-					siteConfig.ObjectMeta.Labels[parts[0]] = parts[1]
-				}
-			}
-		}
-	}
-	if len(errs) > 0 {
-		return siteConfig, fmt.Errorf(strings.Join(errs, ", "))
-	}
-	return siteConfig, nil
-}
-
-func ReadSiteConfig(siteConfig *corev1.ConfigMap, defaultIngress string) (*types.SiteConfig, error) {
-	return ReadSiteConfigFrom(&siteConfig.ObjectMeta, &siteConfig.TypeMeta, siteConfig.Data, defaultIngress)
-}
-
-func ReadSiteConfigFrom(siteConfig *metav1.ObjectMeta, typeMeta *metav1.TypeMeta, data map[string]string, defaultIngress string) (*types.SiteConfig, error) {
-	var errs []string
-	var result types.SiteConfig
-	result.Spec.SkupperNamespace = siteConfig.Namespace
-	// TODO: what should the defaults be for name, namespace
-	if skupperName, ok := data[SiteConfigNameKey]; ok {
-		result.Spec.SkupperName = skupperName
-	} else {
-		result.Spec.SkupperName = siteConfig.Namespace
-	}
-	if routerMode, ok := data[SiteConfigRouterModeKey]; ok {
-		result.Spec.RouterMode = routerMode
-	} else {
-		// check for deprecated key
-		if isEdge, ok := data["edge"]; ok {
-			if isEdge == "true" {
-				result.Spec.RouterMode = string(types.TransportModeEdge)
-			} else {
-				result.Spec.RouterMode = string(types.TransportModeInterior)
-			}
-		} else {
-			result.Spec.RouterMode = string(types.TransportModeInterior)
-		}
-	}
-	if routers, ok := data[SiteConfigRoutersKey]; ok {
-		result.Spec.Routers, _ = strconv.Atoi(routers)
-	}
-	if enableController, ok := data[SiteConfigServiceControllerKey]; ok {
-		result.Spec.EnableController, _ = strconv.ParseBool(enableController)
-	} else {
-		result.Spec.EnableController = true
-	}
-	if enableServiceSync, ok := data[SiteConfigServiceSyncKey]; ok {
-		result.Spec.EnableServiceSync, _ = strconv.ParseBool(enableServiceSync)
-	} else {
-		result.Spec.EnableServiceSync = true
-	}
-	if value, ok := data[SiteConfigServiceSyncSiteTtlKey]; ok {
-		ttl, err := time.ParseDuration(value)
-		if err == nil {
-			result.Spec.SiteTtl = ttl
-		}
-	}
-	if enableConsole, ok := data[SiteConfigConsoleKey]; ok {
-		result.Spec.EnableConsole, _ = strconv.ParseBool(enableConsole)
-	} else {
-		result.Spec.EnableConsole = false
-	}
-	if enableFlowCollector, ok := data[SiteConfigFlowCollectorKey]; ok {
-		result.Spec.EnableFlowCollector, _ = strconv.ParseBool(enableFlowCollector)
-	} else {
-		result.Spec.EnableFlowCollector = false
-	}
-	if value, ok := data[SiteConfigFlowCollectorRecordTtlKey]; ok {
-		ttl, err := time.ParseDuration(value)
-		if err == nil {
-			result.Spec.FlowCollector.FlowRecordTtl = ttl
-		}
-	} else {
-		result.Spec.FlowCollector.FlowRecordTtl = types.DefaultFlowTimeoutDuration
-	}
-	if value, ok := data[SiteConfigRestAPIKey]; ok {
-		result.Spec.EnableRestAPI, _ = strconv.ParseBool(value)
-	} else {
-		result.Spec.EnableRestAPI = result.Spec.EnableConsole
-	}
-	if enableClusterPermissions, ok := data[SiteConfigClusterPermissionsKey]; ok {
-		result.Spec.EnableClusterPermissions, _ = strconv.ParseBool(enableClusterPermissions)
-	} else {
-		result.Spec.EnableClusterPermissions = false
-	}
-	if createNetworkPolicy, ok := data[SiteConfigCreateNetworkPolicyKey]; ok {
-		result.Spec.CreateNetworkPolicy, _ = strconv.ParseBool(createNetworkPolicy)
-	} else {
-		result.Spec.CreateNetworkPolicy = false
-	}
-	if authMode, ok := data[SiteConfigConsoleAuthenticationKey]; ok {
-		result.Spec.AuthMode = authMode
-	} else {
-		result.Spec.AuthMode = string(types.ConsoleAuthModeInternal)
-	}
-	if user, ok := data[SiteConfigConsoleUserKey]; ok {
-		result.Spec.User = user
-	} else {
-		result.Spec.User = ""
-	}
-	if password, ok := data[SiteConfigConsolePasswordKey]; ok {
-		result.Spec.Password = password
-	} else {
-		result.Spec.Password = ""
-	}
-	if ingress, ok := data[SiteConfigIngressKey]; ok {
-		result.Spec.Ingress = ingress
-	} else {
-		// check for deprecated key
-		if clusterLocal, ok := data["cluster-local"]; ok && clusterLocal == "true" {
-			result.Spec.Ingress = types.IngressNoneString
-		} else {
-			result.Spec.Ingress = defaultIngress
-		}
-	}
-	if ingressAnnotations, ok := data[SiteConfigIngressAnnotationsKey]; ok {
-		result.Spec.IngressAnnotations = asMap(splitWithEscaping(ingressAnnotations, ',', '\\'))
-	}
-	if consoleIngress, ok := data[SiteConfigConsoleIngressKey]; ok {
-		result.Spec.ConsoleIngress = consoleIngress
-	}
-	if ingressHost, ok := data[SiteConfigIngressHostKey]; ok {
-		result.Spec.IngressHost = ingressHost
-	}
-	if runAsUser, ok := data[SiteConfigRunAsUserKey]; ok {
-		result.Spec.RunAsUser, _ = strconv.ParseInt(runAsUser, 10, 64)
-	}
-	if runAsGroup, ok := data[SiteConfigRunAsGroupKey]; ok {
-		result.Spec.RunAsGroup, _ = strconv.ParseInt(runAsGroup, 10, 64)
-	}
-	// TODO: allow Replicas to be set through skupper-site configmap?
-	if siteConfig.Labels == nil {
-		result.Spec.SiteControlled = true
-	} else if ignore, ok := siteConfig.Labels[types.SiteControllerIgnore]; ok {
-		siteIgnore, _ := strconv.ParseBool(ignore)
-		result.Spec.SiteControlled = !siteIgnore
-	} else {
-		result.Spec.SiteControlled = true
-	}
-	result.Reference.UID = string(siteConfig.UID)
-	result.Reference.Name = siteConfig.Name
-	result.Reference.Kind = typeMeta.Kind
-	result.Reference.APIVersion = typeMeta.APIVersion
-	if routerLogging, ok := data[SiteConfigRouterLoggingKey]; ok && routerLogging != "" {
-		logConf, err := qdr.ParseRouterLogConfig(routerLogging)
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.Logging = logConf
-		}
-	} else {
-		logConf, err := qdr.ParseRouterLogConfig("ROUTER_CORE:error+")
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.Logging = logConf
-		}
-	}
-	if routerCpu, ok := data[SiteConfigRouterCpuKey]; ok && routerCpu != "" {
-		result.Spec.Router.Cpu = routerCpu
-	}
-	if routerMemory, ok := data[SiteConfigRouterMemoryKey]; ok && routerMemory != "" {
-		result.Spec.Router.Memory = routerMemory
-	}
-	if routerCpuLimit, ok := data[SiteConfigRouterCpuLimitKey]; ok && routerCpuLimit != "" {
-		result.Spec.Router.CpuLimit = routerCpuLimit
-	}
-	if routerMemoryLimit, ok := data[SiteConfigRouterMemoryLimitKey]; ok && routerMemoryLimit != "" {
-		result.Spec.Router.MemoryLimit = routerMemoryLimit
-	}
-	if routerNodeSelector, ok := data[SiteConfigRouterNodeSelectorKey]; ok && routerNodeSelector != "" {
-		result.Spec.Router.NodeSelector = routerNodeSelector
-	}
-	if routerAffinity, ok := data[SiteConfigRouterAffinityKey]; ok && routerAffinity != "" {
-		result.Spec.Router.Affinity = routerAffinity
-	}
-	if routerAntiAffinity, ok := data[SiteConfigRouterAntiAffinityKey]; ok && routerAntiAffinity != "" {
-		result.Spec.Router.AntiAffinity = routerAntiAffinity
-	}
-	if routerIngressHost, ok := data[SiteConfigRouterIngressHostKey]; ok && routerIngressHost != "" {
-		result.Spec.Router.IngressHost = routerIngressHost
-	}
-
-	if routerMaxFrameSize, ok := data[SiteConfigRouterMaxFrameSizeKey]; ok && routerMaxFrameSize != "" {
-		val, err := strconv.Atoi(routerMaxFrameSize)
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.MaxFrameSize = val
-		}
-	} else {
-		result.Spec.Router.MaxFrameSize = types.RouterMaxFrameSizeDefault
-	}
-	if routerMaxSessionFrames, ok := data[SiteConfigRouterMaxSessionFramesKey]; ok && routerMaxSessionFrames != "" {
-		val, err := strconv.Atoi(routerMaxSessionFrames)
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.MaxSessionFrames = val
-		}
-	} else {
-		result.Spec.Router.MaxSessionFrames = types.RouterMaxSessionFramesDefault
-	}
-	if routerDataConnectionCount, ok := data[SiteConfigRouterDataConnectionCountKey]; ok && routerDataConnectionCount != "" {
-		result.Spec.Router.DataConnectionCount = routerDataConnectionCount
-	}
-
-	if routerServiceAnnotations, ok := data[SiteConfigRouterServiceAnnotationsKey]; ok {
-		result.Spec.Router.ServiceAnnotations = asMap(splitWithEscaping(routerServiceAnnotations, ',', '\\'))
-	}
-	if routerPodAnnotations, ok := data[SiteConfigRouterPodAnnotationsKey]; ok {
-		result.Spec.Router.PodAnnotations = asMap(splitWithEscaping(routerPodAnnotations, ',', '\\'))
-	}
-	if routerServiceLoadBalancerIp, ok := data[SiteConfigRouterLoadBalancerIp]; ok {
-		result.Spec.Router.LoadBalancerIp = routerServiceLoadBalancerIp
-	}
-	if value, ok := data[SiteConfigRouterDisableMutualTLS]; ok {
-		result.Spec.Router.DisableMutualTLS, _ = strconv.ParseBool(value)
-	}
-
-	if controllerCpu, ok := data[SiteConfigControllerCpuKey]; ok && controllerCpu != "" {
-		result.Spec.Controller.Cpu = controllerCpu
-	}
-	if controllerMemory, ok := data[SiteConfigControllerMemoryKey]; ok && controllerMemory != "" {
-		result.Spec.Controller.Memory = controllerMemory
-	}
-	if controllerCpuLimit, ok := data[SiteConfigControllerCpuLimitKey]; ok && controllerCpuLimit != "" {
-		result.Spec.Controller.CpuLimit = controllerCpuLimit
-	}
-	if controllerMemoryLimit, ok := data[SiteConfigControllerMemoryLimitKey]; ok && controllerMemoryLimit != "" {
-		result.Spec.Controller.MemoryLimit = controllerMemoryLimit
-	}
-	if controllerNodeSelector, ok := data[SiteConfigControllerNodeSelectorKey]; ok && controllerNodeSelector != "" {
-		result.Spec.Controller.NodeSelector = controllerNodeSelector
-	}
-	if controllerAffinity, ok := data[SiteConfigControllerAffinityKey]; ok && controllerAffinity != "" {
-		result.Spec.Controller.Affinity = controllerAffinity
-	}
-	if controllerAntiAffinity, ok := data[SiteConfigControllerAntiAffinityKey]; ok && controllerAntiAffinity != "" {
-		result.Spec.Controller.AntiAffinity = controllerAntiAffinity
-	}
-	if controllerIngressHost, ok := data[SiteConfigControllerIngressHostKey]; ok && controllerIngressHost != "" {
-		result.Spec.Controller.IngressHost = controllerIngressHost
-	}
-	if controllerServiceAnnotations, ok := data[SiteConfigControllerServiceAnnotationsKey]; ok {
-		result.Spec.Controller.ServiceAnnotations = asMap(splitWithEscaping(controllerServiceAnnotations, ',', '\\'))
-	}
-	if controllerPodAnnotations, ok := data[SiteConfigControllerPodAnnotationsKey]; ok {
-		result.Spec.Controller.PodAnnotations = asMap(splitWithEscaping(controllerPodAnnotations, ',', '\\'))
-	}
-	if controllerServiceLoadBalancerIp, ok := data[SiteConfigControllerLoadBalancerIp]; ok {
-		result.Spec.Controller.LoadBalancerIp = controllerServiceLoadBalancerIp
-	}
-
-	if configSyncCpu, ok := data[SiteConfigConfigSyncCpuKey]; ok && configSyncCpu != "" {
-		result.Spec.ConfigSync.Cpu = configSyncCpu
-	}
-	if configSyncMemory, ok := data[SiteConfigConfigSyncMemoryKey]; ok && configSyncMemory != "" {
-		result.Spec.ConfigSync.Memory = configSyncMemory
-	}
-	if configSyncCpuLimit, ok := data[SiteConfigConfigSyncCpuLimitKey]; ok && configSyncCpuLimit != "" {
-		result.Spec.ConfigSync.CpuLimit = configSyncCpuLimit
-	}
-	if configSyncMemoryLimit, ok := data[SiteConfigConfigSyncMemoryLimitKey]; ok && configSyncMemoryLimit != "" {
-		result.Spec.ConfigSync.MemoryLimit = configSyncMemoryLimit
-	}
-
-	if value, ok := data[SiteConfigEnableSkupperEventsKey]; ok {
-		result.Spec.EnableSkupperEvents, _ = strconv.ParseBool(value)
-	}
-
-	if flowCollectorCpu, ok := data[SiteConfigFlowCollectorCpuKey]; ok && flowCollectorCpu != "" {
-		result.Spec.FlowCollector.Cpu = flowCollectorCpu
-	}
-	if flowCollectorMemory, ok := data[SiteConfigFlowCollectorMemoryKey]; ok && flowCollectorMemory != "" {
-		result.Spec.FlowCollector.Memory = flowCollectorMemory
-	}
-	if flowCollectorCpuLimit, ok := data[SiteConfigFlowCollectorCpuLimitKey]; ok && flowCollectorCpuLimit != "" {
-		result.Spec.FlowCollector.CpuLimit = flowCollectorCpuLimit
-	}
-	if flowCollectorMemoryLimit, ok := data[SiteConfigFlowCollectorMemoryLimitKey]; ok && flowCollectorMemoryLimit != "" {
-		result.Spec.FlowCollector.MemoryLimit = flowCollectorMemoryLimit
-	}
-
-	if externalServer, ok := data[SiteConfigPrometheusExternalServerKey]; ok {
-		result.Spec.PrometheusServer.ExternalServer = externalServer
-	} else {
-		result.Spec.PrometheusServer.ExternalServer = ""
-	}
-	if authMode, ok := data[SiteConfigPrometheusServerAuthenticationKey]; ok {
-		result.Spec.PrometheusServer.AuthMode = authMode
-	} else {
-		result.Spec.PrometheusServer.AuthMode = string(types.PrometheusAuthModeTls)
-	}
-	if user, ok := data[SiteConfigPrometheusServerUserKey]; ok {
-		result.Spec.PrometheusServer.User = user
-	} else {
-		result.Spec.PrometheusServer.User = ""
-	}
-	if password, ok := data[SiteConfigPrometheusServerPasswordKey]; ok {
-		result.Spec.PrometheusServer.Password = password
-	} else {
-		result.Spec.PrometheusServer.Password = ""
-	}
-	if prometheusCpu, ok := data[SiteConfigPrometheusServerCpuKey]; ok && prometheusCpu != "" {
-		result.Spec.PrometheusServer.Cpu = prometheusCpu
-	}
-	if prometheusMemory, ok := data[SiteConfigPrometheusServerMemoryKey]; ok && prometheusMemory != "" {
-		result.Spec.PrometheusServer.Memory = prometheusMemory
-	}
-	if prometheusCpuLimit, ok := data[SiteConfigPrometheusServerCpuLimitKey]; ok && prometheusCpuLimit != "" {
-		result.Spec.PrometheusServer.CpuLimit = prometheusCpuLimit
-	}
-	if prometheusMemoryLimit, ok := data[SiteConfigPrometheusServerMemoryLimitKey]; ok && prometheusMemoryLimit != "" {
-		result.Spec.PrometheusServer.MemoryLimit = prometheusMemoryLimit
-	}
-	if prometheusPodAnnotations, ok := data[SiteConfigPrometheusServerPodAnnotationsKey]; ok {
-		result.Spec.PrometheusServer.PodAnnotations = asMap(splitWithEscaping(prometheusPodAnnotations, ',', '\\'))
-	}
-
-	annotations, labels := GetSiteAnnotationsAndLabels(siteConfig)
-	result.Spec.Annotations = annotations
-	result.Spec.Labels = labels
-	if len(errs) > 0 {
-		return &result, fmt.Errorf(strings.Join(errs, ", "))
-	}
-	return &result, nil
-}
-
-func GetSiteAnnotationsAndLabels(siteConfig *metav1.ObjectMeta) (map[string]string, map[string]string) {
-	annotationExclusions := []string{}
-	labelExclusions := []string{}
-	annotations := map[string]string{}
-	for key, value := range siteConfig.Annotations {
-		if key == types.AnnotationExcludes {
-			annotationExclusions = strings.Split(value, ",")
-		} else if key == types.LabelExcludes {
-			labelExclusions = strings.Split(value, ",")
-		} else {
-			annotations[key] = value
-		}
-	}
-	for _, key := range annotationExclusions {
-		delete(annotations, key)
-	}
-	labels := map[string]string{}
-	for key, value := range siteConfig.Labels {
-		if key != types.SiteControllerIgnore {
-			labels[key] = value
-		}
-	}
-	for _, key := range labelExclusions {
-		delete(labels, key)
-	}
-	return annotations, labels
-}
-
-func UpdateLogging(config types.SiteConfigSpec, configmap *corev1.ConfigMap) bool {
-	latestLogging := qdr.RouterLogConfigToString(config.Router.Logging)
-	if configmap.Data[SiteConfigRouterLoggingKey] != latestLogging {
-		configmap.Data[SiteConfigRouterLoggingKey] = latestLogging
-		return true
-	}
-	return false
-}
-
-func UpdateForCollectorEnabled(configmap *corev1.ConfigMap) {
-	configmap.Data[SiteConfigConsoleKey] = "true"
-	configmap.Data[SiteConfigFlowCollectorKey] = "true"
-}
-
-func splitWithEscaping(s string, separator, escape byte) []string {
-	var token []byte
-	var tokens []string
-	for i := 0; i < len(s); i++ {
-		if s[i] == separator {
-			tokens = append(tokens, strings.TrimSpace(string(token)))
-			token = token[:0]
-		} else if s[i] == escape && i+1 < len(s) {
-			i++
-			token = append(token, s[i])
-		} else {
-			token = append(token, s[i])
-		}
-	}
-	tokens = append(tokens, strings.TrimSpace(string(token)))
-	return tokens
-}
-
-func asMap(entries []string) map[string]string {
-	result := map[string]string{}
-	for _, entry := range entries {
-		parts := strings.Split(entry, "=")
-		if len(parts) > 1 {
-			result[parts[0]] = parts[1]
-		} else {
-			result[parts[0]] = ""
-		}
-	}
-	return result
-}
diff --git a/pkg/utils/validator/simple_validator.go b/pkg/utils/validator/simple_validator.go
index 427eaddd..1230e57f 100644
--- a/pkg/utils/validator/simple_validator.go
+++ b/pkg/utils/validator/simple_validator.go
@@ -28,6 +28,17 @@ func NewStringValidator() *StringValidator {
 	}
 }
 
+func NewHostStringValidator() *StringValidator {
+	re, err := regexp.Compile(`^[a-z0-9]+([-.]{1}[a-z0-9]+)*$`)
+	if err != nil {
+		fmt.Printf("Error compiling regex: %v", err)
+		return nil
+	}
+	return &StringValidator{
+		Expression: re,
+	}
+}
+
 func NewResourceStringValidator() *StringValidator {
 	re, err := regexp.Compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$")
 	if err != nil {
diff --git a/scripts/oci-index-archive-info.sh b/scripts/oci-index-archive-info.sh
new file mode 100755
index 00000000..66843a16
--- /dev/null
+++ b/scripts/oci-index-archive-info.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+
+REGISTRY=${REGISTRY:-quay.io/skupper}
+IMAGE_TAG=${IMAGE_TAG:-"v2-latest"}
+ARCHIVES_PATH=${ARCHIVES_PATH:-./oci-archives}
+
+skopeo::digest (){
+  skopeo inspect oci-archive:"$1" \
+    | jq -r ".Digest"
+}
+
+skopeo::digest::for::arch (){
+  skopeo inspect --raw oci-archive:"$1" \
+    | jq -r \
+	".manifests[] | select(.platform.architecture == \"$2\" and .platform.os == \"linux\") | .digest"
+}
+
+echo "index:"
+for archive in "${ARCHIVES_PATH}"/*; do
+  if [ -f "$archive" ]; then
+    fn=$(basename -- "$archive")
+    imagename="${fn%.*}"
+    digest=$(skopeo::digest "$archive" "$x")
+    if [ -n "$digest" ]; then
+      printf "  - %s/%s:%s@%s\n" "$REGISTRY" "$imagename" "$IMAGE_TAG" "$digest"
+    fi
+  fi
+done
+for x in "$@"; do
+  echo "$x:"
+  for archive in "${ARCHIVES_PATH}"/*; do
+    if [ -f "$archive" ]; then
+      fn=$(basename -- "$archive")
+      imagename="${fn%.*}"
+	  digest=$(skopeo::digest::for::arch "$archive" "$x")
+	  if [ -n "$digest" ]; then
+        printf "  - %s/%s:%s@%s\n" "$REGISTRY" "$imagename" "$IMAGE_TAG" "$digest"
+      fi
+    fi
+  done
+done
+


Test patch:
diff --git a/internal/cmd/skupper/connector/nonkube/connector_create_test.go b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
index 6072c5b0..7b407551 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_create_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
@@ -1,1 +1,293 @@
 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/assert"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestNonKubeCmdConnectorCreate_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		flags             *common.CommandConnectorCreateFlags
+		cobraGenericFlags map[string]string
+		expectedErrors    []string
+	}
+
+	testTable := []test{
+		{
+			name:           "Connector name and port are not specified",
+			args:           []string{},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"connector name and port must be configured"},
+		},
+		{
+			name:           "Connector name is not valid",
+			args:           []string{"my new Connector", "8080"},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name:           "Connector name is empty",
+			args:           []string{"", "1234"},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name:           "connector port empty",
+			args:           []string{"my-name-port-empty", ""},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"connector port must not be empty"},
+		},
+		{
+			name:           "port is not valid",
+			args:           []string{"my-connector-port", "abcd"},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"connector port is not valid: strconv.Atoi: parsing \"abcd\": invalid syntax"},
+		},
+		{
+			name:           "port not positive",
+			args:           []string{"my-port-positive", "-45"},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"connector port is not valid: value is not positive"},
+		},
+		{
+			name:           "more than two arguments was specified",
+			args:           []string{"my", "Connector", "test"},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{"only two arguments are allowed for this command"},
+		},
+		{
+			name:           "type is not valid",
+			args:           []string{"my-connector", "8080"},
+			flags:          &common.CommandConnectorCreateFlags{ConnectorType: "not-valid", Host: "1.2.3.4"},
+			expectedErrors: []string{"connector type is not valid: value not-valid not allowed. It should be one of this options: [tcp]"},
+		},
+		{
+			name:           "routing key is not valid",
+			args:           []string{"my-connector-rk", "8080"},
+			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:           "host is not valid",
+			args:           []string{"my-connector-host", "8080"},
+			flags:          &common.CommandConnectorCreateFlags{Host: "not-valid$"},
+			expectedErrors: []string{"host is not valid: a valid IP address or hostname is expected"},
+		},
+		{
+			name:           "host is not configued",
+			args:           []string{"my-connector-host", "8080"},
+			flags:          &common.CommandConnectorCreateFlags{},
+			expectedErrors: []string{"host name must be configured: an IP address or hostname is expected"},
+		},
+		{
+			name:           "output format is not valid",
+			args:           []string{"my-connector", "8080"},
+			flags:          &common.CommandConnectorCreateFlags{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-connector", "8080"},
+			flags:          &common.CommandConnectorCreateFlags{Host: "1.2.3.4"},
+			expectedErrors: []string{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-connector-flags", "8080"},
+			flags: &common.CommandConnectorCreateFlags{
+				RoutingKey:    "routingkeyname",
+				TlsSecret:     "secretname",
+				ConnectorType: "tcp",
+				Output:        "json",
+				Host:          "1.2.3.4",
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command := &CmdConnectorCreate{Flags: &common.CommandConnectorCreateFlags{}}
+			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 TestNonKubeCmdConnectorCreate_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name                  string
+		args                  []string
+		namespace             string
+		flags                 common.CommandConnectorCreateFlags
+		expectedNamespace     string
+		Connectorname         string
+		expectedTlsSecret     string
+		expectedHost          string
+		expectedRoutingKey    string
+		expectedConnectorType string
+		expectedOutput        string
+	}
+
+	testTable := []test{
+		{
+			name:                  "test1",
+			flags:                 common.CommandConnectorCreateFlags{"backend", "", "", "secret", "tcp", false, "", 0, "json"},
+			expectedTlsSecret:     "secret",
+			expectedHost:          "",
+			expectedRoutingKey:    "backend",
+			expectedConnectorType: "tcp",
+			expectedOutput:        "json",
+			expectedNamespace:     "default",
+		},
+		{
+			name:                  "test2",
+			namespace:             "test",
+			flags:                 common.CommandConnectorCreateFlags{"backend", "1.2.3.4", "", "secret", "tcp", false, "", 0, "json"},
+			expectedTlsSecret:     "secret",
+			expectedHost:          "1.2.3.4",
+			expectedRoutingKey:    "backend",
+			expectedConnectorType: "tcp",
+			expectedOutput:        "json",
+			expectedNamespace:     "test",
+		},
+		{
+			name:                  "test3",
+			namespace:             "test",
+			flags:                 common.CommandConnectorCreateFlags{"", "", "", "secret", "tcp", false, "", 0, "yaml"},
+			expectedTlsSecret:     "secret",
+			expectedHost:          "",
+			expectedRoutingKey:    "my-Connector",
+			expectedConnectorType: "tcp",
+			expectedOutput:        "yaml",
+			expectedNamespace:     "test",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			cmd := CmdConnectorCreate{}
+			cmd.Flags = &test.flags
+			cmd.connectorName = "my-Connector"
+			cmd.namespace = test.namespace
+			cmd.connectorHandler = fs.NewConnectorHandler(cmd.namespace)
+
+			cmd.InputToOptions()
+
+			assert.Check(t, cmd.routingKey == test.expectedRoutingKey)
+			assert.Check(t, cmd.tlsSecret == test.expectedTlsSecret)
+			assert.Check(t, cmd.host == test.expectedHost)
+			assert.Check(t, cmd.connectorType == test.expectedConnectorType)
+			assert.Check(t, cmd.output == test.expectedOutput)
+			assert.Check(t, cmd.namespace == test.expectedNamespace)
+		})
+	}
+}
+
+func TestNonKubeCmdConnectorCreate_Run(t *testing.T) {
+	type test struct {
+		name           string
+		namespace      string
+		k8sObjects     []runtime.Object
+		skupperObjects []runtime.Object
+		skupperError   string
+		connectorName  string
+		host           string
+		output         string
+		errorMessage   string
+		routingKey     string
+		tlsSecret      string
+		connectorType  string
+		connectorPort  int
+	}
+
+	testTable := []test{
+		{
+			name:           "runs ok",
+			namespace:      "test",
+			k8sObjects:     nil,
+			skupperObjects: nil,
+			connectorName:  "test1",
+			connectorPort:  8080,
+			connectorType:  "tcp",
+			routingKey:     "keyname",
+			tlsSecret:      "secretname",
+			host:           "1.2.3.4",
+		},
+		{
+			name:           "runs ok yaml",
+			k8sObjects:     nil,
+			skupperObjects: nil,
+			connectorName:  "test2",
+			connectorPort:  8080,
+			connectorType:  "tcp",
+			host:           "2.2.2.2",
+			output:         "yaml",
+		},
+		{
+			name:           "runs fails because the output format is not supported",
+			namespace:      "default",
+			k8sObjects:     nil,
+			skupperObjects: nil,
+			connectorName:  "test3",
+			connectorPort:  8080,
+			host:           "3.3.3.3",
+			output:         "unsupported",
+			errorMessage:   "format unsupported not supported",
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdConnectorCreate{}
+
+		command.connectorName = test.connectorName
+		command.output = test.output
+		command.port = test.connectorPort
+		command.host = test.host
+		command.connectorType = test.connectorType
+		command.namespace = test.namespace
+		command.connectorHandler = fs.NewConnectorHandler(command.namespace)
+		defer command.connectorHandler.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/connector/nonkube/connector_delete_test.go b/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
index 6072c5b0..521ef67e 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
@@ -1,1 +1,133 @@
 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/assert"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdConnectorDelete_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandConnectorDeleteFlags
+		cobraGenericFlags map[string]string
+		k8sObjects        []runtime.Object
+		skupperObjects    []runtime.Object
+		expectedErrors    []string
+	}
+
+	testTable := []test{
+		{
+			name:           "connector name is not specified",
+			args:           []string{},
+			flags:          &common.CommandConnectorDeleteFlags{},
+			expectedErrors: []string{"connector name must be configured"},
+		},
+		{
+			name:           "connector name is nil",
+			args:           []string{""},
+			flags:          &common.CommandConnectorDeleteFlags{},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name:           "connector name is not valid",
+			args:           []string{"my name"},
+			flags:          &common.CommandConnectorDeleteFlags{},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name:           "more than one argument is specified",
+			args:           []string{"my", "connector"},
+			flags:          &common.CommandConnectorDeleteFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "kubernetes flags are not valid on this platform",
+			args:           []string{"my-connector"},
+			flags:          &common.CommandConnectorDeleteFlags{},
+			expectedErrors: []string{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command := &CmdConnectorDelete{Flags: &common.CommandConnectorDeleteFlags{}}
+			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 TestCmdConnectorDelete_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-connector",
+			skupperErrorMessage: "error",
+			errorMessage:        "error",
+		},
+		{
+			name:                "run fails",
+			namespace:           "test",
+			deleteName:          "my-connector",
+			skupperErrorMessage: "error",
+			errorMessage:        "error",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd := &CmdConnectorDelete{}
+
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd.connectorName = test.deleteName
+			cmd.namespace = test.namespace
+			cmd.connectorHandler = fs.NewConnectorHandler(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/connector/nonkube/connector_status_test.go b/internal/cmd/skupper/connector/nonkube/connector_status_test.go
index 6072c5b0..8608284b 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_status_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_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/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdConnectorStatus_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandConnectorStatusFlags
+		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:           "connector is not shown because connector does not exist in the namespace",
+			args:           []string{"no-connector"},
+			flags:          &common.CommandConnectorStatusFlags{},
+			expectedErrors: []string{"connector no-connector does not exist in namespace test"},
+		},
+		{
+			name:           "connector name is nil",
+			args:           []string{""},
+			flags:          &common.CommandConnectorStatusFlags{},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name:           "more than one argument is specified",
+			args:           []string{"my", "connector"},
+			flags:          &common.CommandConnectorStatusFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "connector name is not valid.",
+			args:           []string{"my new connector"},
+			flags:          &common.CommandConnectorStatusFlags{},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name:           "no args",
+			flags:          &common.CommandConnectorStatusFlags{},
+			expectedErrors: []string{},
+		},
+		{
+			name:           "bad output status",
+			args:           []string{"my-connector"},
+			flags:          &common.CommandConnectorStatusFlags{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-connector"},
+			flags:          &common.CommandConnectorStatusFlags{Output: "json"},
+			expectedErrors: []string{},
+		},
+	}
+
+	//Add a temp file so connector exists for status tests
+	connectorResource := v2alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-connector",
+			Namespace: "test",
+		},
+	}
+
+	command := &CmdConnectorStatus{}
+	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.connectorName = ""
+
+			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 TestCmdConnectorStatus_Run(t *testing.T) {
+	type test struct {
+		name                string
+		connectorName       string
+		flags               common.CommandConnectorStatusFlags
+		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 connector doesn't exist",
+			connectorName: "no-connector",
+			errorMessage:  "failed to read file: open " + path + "/connectors/no-connector.yaml: no such file or directory",
+		},
+		{
+			name:          "runs ok, returns 1 connectors",
+			connectorName: "my-connector",
+		},
+		{
+			name:          "runs ok, returns 1 connectors yaml",
+			connectorName: "my-connector",
+			flags:         common.CommandConnectorStatusFlags{Output: "yaml"},
+		},
+		{
+			name: "runs ok, returns all connectors",
+		},
+		{
+			name:  "runs ok, returns all connectors json",
+			flags: common.CommandConnectorStatusFlags{Output: "json"},
+		},
+		{
+			name:         "runs ok, returns all connectors output bad",
+			flags:        common.CommandConnectorStatusFlags{Output: "bad-value"},
+			errorMessage: "format bad-value not supported",
+		},
+		{
+			name:          "runs ok, returns 1 connectors bad output",
+			connectorName: "my-connector",
+			flags:         common.CommandConnectorStatusFlags{Output: "bad-value"},
+			errorMessage:  "format bad-value not supported",
+		},
+	}
+
+	//Add a temp file so connector exists for status tests
+	connectorResource1 := v2alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-connector",
+			Namespace: "test",
+		},
+		Spec: v2alpha1.ConnectorSpec{
+			Host:       "1.2.3.4",
+			Port:       8080,
+			RoutingKey: "backend-8080",
+		},
+		Status: v2alpha1.ConnectorStatus{
+			Status: v2alpha1.Status{
+				Conditions: []metav1.Condition{
+					{
+						Type:   "Configured",
+						Status: "True",
+					},
+				},
+			},
+		},
+	}
+	connectorResource2 := v2alpha1.Connector{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "skupper.io/v2alpha1",
+			Kind:       "Connector",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "my-connector2",
+			Namespace: "test",
+		},
+		Spec: v2alpha1.ConnectorSpec{
+			Host:       "1.1.1.1",
+			Port:       9999,
+			RoutingKey: "test-9999",
+		},
+		Status: v2alpha1.ConnectorStatus{
+			Status: v2alpha1.Status{
+				Conditions: []metav1.Condition{
+					{
+						Type:   "Configured",
+						Status: "True",
+					},
+				},
+			},
+		},
+	}
+
+	// add two connectors in runtime directory
+	command := &CmdConnectorStatus{}
+	command.namespace = "test"
+	command.connectorHandler = fs.NewConnectorHandler(command.namespace)
+
+	defer command.connectorHandler.Delete("my-connector")
+	defer command.connectorHandler.Delete("my-connector2")
+
+	path = filepath.Join(homeDir, "/.local/share/skupper/namespaces/test/", string(api.RuntimeSiteStatePath))
+	content, err := command.connectorHandler.EncodeToYaml(connectorResource1)
+	assert.Check(t, err == nil)
+	err = command.connectorHandler.WriteFile(path, "my-connector.yaml", content, common.Connectors)
+	assert.Check(t, err == nil)
+
+	content, err = command.connectorHandler.EncodeToYaml(connectorResource2)
+	assert.Check(t, err == nil)
+	err = command.connectorHandler.WriteFile(path, "my-connector2.yaml", content, common.Connectors)
+	assert.Check(t, err == nil)
+
+	for _, test := range testTable {
+		command.connectorName = test.connectorName
+		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 TestCmdConnectorStatus_RunNoDirectory(t *testing.T) {
+	type test struct {
+		name                string
+		flags               common.CommandConnectorStatusFlags
+		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 + "/connectors/my-connector.yaml: no such file or directory",
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdConnectorStatus{}
+		command.namespace = "test1"
+		command.connectorHandler = fs.NewConnectorHandler(command.namespace)
+		command.connectorName = "my-connector"
+		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/connector/nonkube/connector_update_test.go b/internal/cmd/skupper/connector/nonkube/connector_update_test.go
index 6072c5b0..8f0be13a 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_update_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_update_test.go
@@ -1,1 +1,227 @@
 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/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestCmdConnectorUpdate_ValidateInput(t *testing.T) {
+	type test struct {
+		name              string
+		args              []string
+		flags             *common.CommandConnectorUpdateFlags
+		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:           "connector is not updated because get connector returned error",
+			args:           []string{"no-connector"},
+			flags:          &common.CommandConnectorUpdateFlags{},
+			expectedErrors: []string{"connector no-connector must exist in namespace test to be updated"},
+		},
+		{
+			name:           "connector name is not specified",
+			args:           []string{},
+			flags:          &common.CommandConnectorUpdateFlags{},
+			expectedErrors: []string{"connector name must be configured"},
+		},
+		{
+			name:           "connector name is nil",
+			args:           []string{""},
+			flags:          &common.CommandConnectorUpdateFlags{},
+			expectedErrors: []string{"connector name must not be empty"},
+		},
+		{
+			name:           "more than one argument is specified",
+			args:           []string{"my", "connector"},
+			flags:          &common.CommandConnectorUpdateFlags{},
+			expectedErrors: []string{"only one argument is allowed for this command"},
+		},
+		{
+			name:           "connector name is not valid.",
+			args:           []string{"my new connector"},
+			flags:          &common.CommandConnectorUpdateFlags{},
+			expectedErrors: []string{"connector name is not valid: value does not match this regular expression: ^[a-z0-9]([-a-z0-9]*[a-z0-9])*(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])*)*$"},
+		},
+		{
+			name:           "connector type is not valid",
+			args:           []string{"my-connector"},
+			flags:          &common.CommandConnectorUpdateFlags{ConnectorType: "not-valid"},
+			expectedErrors: []string{"connector type is not valid: value not-valid not allowed. It should be one of this options: [tcp]"},
+		},
+		{
+			name:           "routing key is not valid",
+			args:           []string{"my-connector"},
+			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:           "host is not valid",
+			args:           []string{"my-connector"},
+			flags:          &common.CommandConnectorUpdateFlags{Host: "not-valid$"},
+			expectedErrors: []string{"host is not valid: a valid IP address or hostname is expected"},
+		},
+		{
+			name:           "port is not valid",
+			args:           []string{"my-connector"},
+			flags:          &common.CommandConnectorUpdateFlags{Port: -1},
+			expectedErrors: []string{"connector port is not valid: value is not positive"},
+		},
+		{
+			name:           "output is not valid",
+			args:           []string{"my-connector"},
+			flags:          &common.CommandConnectorUpdateFlags{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-connector"},
+			flags: &common.CommandConnectorUpdateFlags{},
+			cobraGenericFlags: map[string]string{
+				common.FlagNameContext:    "test",
+				common.FlagNameKubeconfig: "test",
+			},
+			expectedErrors: []string{},
+		},
+		{
+			name: "flags all valid",
+			args: []string{"my-connector"},
+			flags: &common.CommandConnectorUpdateFlags{
+				RoutingKey:    "routingkeyname",
+				TlsSecret:     "secretname",
+				Port:          1234,
+				ConnectorType: "tcp",
+				Output:        "json",
+				Host:          "1.2.3.4",
+			},
+			expectedErrors: []string{},
+		},
+	}
+
+	//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 := &CmdConnectorUpdate{Flags: &common.CommandConnectorUpdateFlags{}}
+	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.connectorName = ""
+			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 TestCmdConnectorUpdate_Run(t *testing.T) {
+	type test struct {
+		name                string
+		namespace           string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+		connectorName       string
+		host                string
+		output              string
+		routingKey          string
+		tlsSecret           string
+		connectorType       string
+		port                int
+	}
+
+	testTable := []test{
+		{
+			name:          "runs ok",
+			namespace:     "test",
+			connectorName: "my-connector",
+			port:          8080,
+			connectorType: "tcp",
+			host:          "hostname",
+			routingKey:    "keyname",
+			tlsSecret:     "secretname",
+		},
+		{
+			name:          "run output json",
+			connectorName: "my-connector",
+			port:          8181,
+			connectorType: "tcp",
+			host:          "hostname",
+			routingKey:    "keyname",
+			tlsSecret:     "secretname",
+			output:        "json",
+		},
+	}
+
+	for _, test := range testTable {
+		command := &CmdConnectorUpdate{}
+
+		command.connectorName = test.connectorName
+		command.output = test.output
+		command.newSettings.port = test.port
+		command.newSettings.host = test.host
+		command.newSettings.routingKey = test.routingKey
+		command.newSettings.tlsSecret = test.tlsSecret
+		command.namespace = test.namespace
+		command.connectorHandler = fs.NewConnectorHandler(command.namespace)
+		defer command.connectorHandler.Delete("my-connector")
+		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/debug/debug_test.go b/internal/cmd/skupper/debug/debug_test.go
new file mode 100644
index 00000000..8dc17c68
--- /dev/null
+++ b/internal/cmd/skupper/debug/debug_test.go
@@ -0,0 +1,1 @@
+package debug
diff --git a/internal/cmd/skupper/debug/kube/debug_test.go b/internal/cmd/skupper/debug/kube/debug_test.go
new file mode 100644
index 00000000..f84e1e4e
--- /dev/null
+++ b/internal/cmd/skupper/debug/kube/debug_test.go
@@ -0,0 +1,1 @@
+package kube
diff --git a/internal/cmd/skupper/debug/nonkube/debug_test.go b/internal/cmd/skupper/debug/nonkube/debug_test.go
new file mode 100644
index 00000000..6072c5b0
--- /dev/null
+++ b/internal/cmd/skupper/debug/nonkube/debug_test.go
@@ -0,0 +1,1 @@
+package nonkube
diff --git a/internal/cmd/skupper/version/kube/version_test.go b/internal/cmd/skupper/version/kube/version_test.go
new file mode 100644
index 00000000..f84e1e4e
--- /dev/null
+++ b/internal/cmd/skupper/version/kube/version_test.go
@@ -0,0 +1,1 @@
+package kube
diff --git a/internal/cmd/skupper/version/nonkube/version_test.go b/internal/cmd/skupper/version/nonkube/version_test.go
new file mode 100644
index 00000000..6072c5b0
--- /dev/null
+++ b/internal/cmd/skupper/version/nonkube/version_test.go
@@ -0,0 +1,1 @@
+package nonkube
diff --git a/internal/cmd/skupper/version/version_test.go b/internal/cmd/skupper/version/version_test.go
new file mode 100644
index 00000000..f37d99d0
--- /dev/null
+++ b/internal/cmd/skupper/version/version_test.go
@@ -0,0 +1,1 @@
+package version
diff --git a/pkg/kube/site/site_test.go b/pkg/kube/site/site_test.go
index 1d2a3c7f..b60bbe59 100644
--- a/pkg/kube/site/site_test.go
+++ b/pkg/kube/site/site_test.go
@@ -1032,7 +1032,7 @@ func newSiteMocks(namespace string, k8sObjects []runtime.Object, skupperObjects
 	controller := kube.NewController("test", client)
 	newSite := &Site{
 		controller: controller,
-		bindings:   NewExtendedBindings(controller),
+		bindings:   NewExtendedBindings(controller, ""),
 		links:      make(map[string]*site1.Link),
 		errors:     make(map[string]string),
 		linkAccess: make(map[string]*skupperv2alpha1.RouterAccess),
diff --git a/pkg/nonkube/api/site_state_test.go b/pkg/nonkube/api/site_state_test.go
index 5a187d63..9c194af5 100644
--- a/pkg/nonkube/api/site_state_test.go
+++ b/pkg/nonkube/api/site_state_test.go
@@ -110,7 +110,7 @@ func TestSiteState_ToRouterConfig(t *testing.T) {
 			}
 			assert.Equal(t, len(rolesFound), 3, "expecting normal, inter-router and edge, found: %s", rolesFound)
 			assert.Equal(t, len(routerConfig.Connectors), 1)
-			assert.Equal(t, len(routerConfig.SslProfiles), 3)
+			assert.Equal(t, len(routerConfig.SslProfiles), 6)
 			assert.Assert(t, strings.HasPrefix(routerConfig.SslProfiles["link-access-one"].CaCertFile, sslProfileBasePath))
 			assert.Assert(t, strings.HasPrefix(routerConfig.SslProfiles["link-one-profile"].CaCertFile, sslProfileBasePath))
 			assert.Assert(t, strings.HasPrefix(routerConfig.SslProfiles["local-access-one"].CaCertFile, sslProfileBasePath))
diff --git a/pkg/nonkube/common/fs_config_renderer_test.go b/pkg/nonkube/common/fs_config_renderer_test.go
index 54d1f2e3..519d3ae4 100644
--- a/pkg/nonkube/common/fs_config_renderer_test.go
+++ b/pkg/nonkube/common/fs_config_renderer_test.go
@@ -41,7 +41,7 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate
 	fsConfigRenderer.customOutputPath = customOutputPath
 	assert.Assert(t, fsConfigRenderer.Render(ss))
 	customOutputPath = fsConfigRenderer.GetOutputPath(ss)
-	for _, dirName := range []string{"certificates", "config", "sources", "runtime"} {
+	for _, dirName := range []string{"input", "runtime", "internal"} {
 		file, err := os.Stat(path.Join(customOutputPath, dirName))
 		assert.Assert(t, err)
 		assert.Assert(t, file.IsDir())
@@ -50,38 +50,38 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate
 		t.Skipf("The %s environment variable is set to: %s", types.ENV_PLATFORM, envPlatform)
 	}
 	expectedFiles := []string{
-		"config/router/skrouterd.json",
-		"certificates/ca/skupper-site-ca/tls.crt",
-		"certificates/ca/skupper-site-ca/tls.key",
-		"certificates/ca/skupper-service-ca/tls.crt",
-		"certificates/ca/skupper-service-ca/tls.key",
-		"certificates/client/client-link-access-one/tls.crt",
-		"certificates/client/client-link-access-one/tls.key",
-		"certificates/client/client-link-access-one/ca.crt",
-		"certificates/server/link-access-one/tls.crt",
-		"certificates/server/link-access-one/tls.key",
-		"certificates/server/link-access-one/ca.crt",
-		"certificates/server/listener-one-credentials/tls.crt",
-		"certificates/server/listener-one-credentials/tls.key",
-		"certificates/server/listener-one-credentials/ca.crt",
-		"certificates/server/listener-two-credentials/tls.crt",
-		"certificates/server/listener-two-credentials/tls.key",
-		"certificates/server/listener-two-credentials/ca.crt",
-		"certificates/server/connector-one-credentials/tls.key",
-		"certificates/server/connector-one-credentials/ca.crt",
-		"certificates/server/connector-one-credentials/tls.crt",
-		"certificates/link/link-one-profile/ca.crt",
-		"certificates/link/link-one-profile/tls.crt",
-		"certificates/link/link-one-profile/tls.key",
-		"runtime/state/platform.yaml",
-		"runtime/link/link-link-access-one-127.0.0.1.yaml",
+		string(api.RouterConfigPath) + "/skrouterd.json",
+		string(api.CertificatesCaPath) + "/skupper-site-ca/tls.crt",
+		string(api.CertificatesCaPath) + "/skupper-site-ca/tls.key",
+		string(api.CertificatesCaPath) + "/skupper-service-ca/tls.crt",
+		string(api.CertificatesCaPath) + "/skupper-service-ca/tls.key",
+		string(api.CertificatesClientPath) + "/client-link-access-one/tls.crt",
+		string(api.CertificatesClientPath) + "/client-link-access-one/tls.key",
+		string(api.CertificatesClientPath) + "/client-link-access-one/ca.crt",
+		string(api.CertificatesServerPath) + "/link-access-one/tls.crt",
+		string(api.CertificatesServerPath) + "/link-access-one/tls.key",
+		string(api.CertificatesServerPath) + "/link-access-one/ca.crt",
+		string(api.CertificatesServerPath) + "/listener-one-credentials/tls.crt",
+		string(api.CertificatesServerPath) + "/listener-one-credentials/tls.key",
+		string(api.CertificatesServerPath) + "/listener-one-credentials/ca.crt",
+		string(api.CertificatesServerPath) + "/listener-two-credentials/tls.crt",
+		string(api.CertificatesServerPath) + "/listener-two-credentials/tls.key",
+		string(api.CertificatesServerPath) + "/listener-two-credentials/ca.crt",
+		string(api.CertificatesServerPath) + "/connector-one-credentials/tls.key",
+		string(api.CertificatesServerPath) + "/connector-one-credentials/ca.crt",
+		string(api.CertificatesServerPath) + "/connector-one-credentials/tls.crt",
+		string(api.CertificatesLinkPath) + "/link-one-profile/ca.crt",
+		string(api.CertificatesLinkPath) + "/link-one-profile/tls.crt",
+		string(api.CertificatesLinkPath) + "/link-one-profile/tls.key",
+		string(api.RuntimePath) + "/platform.yaml",
+		string(api.RuntimeTokenPath) + "/link-link-access-one-127.0.0.1.yaml",
 	}
 	if !addInputCertificates {
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-localhost.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-localhost.yaml")
 	} else {
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-10.0.0.1.yaml")
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-10.0.0.2.yaml")
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-fake.domain.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-10.0.0.1.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-10.0.0.2.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-fake.domain.yaml")
 	}
 	for _, fileName := range expectedFiles {
 		fs, err := os.Stat(path.Join(customOutputPath, fileName))
@@ -95,12 +95,12 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate
 }
 
 func compareCertificates(t *testing.T, customOutputPath string) {
-	caPath := path.Join(customOutputPath, "certificates/ca/skupper-site-ca")
-	serverPath := path.Join(customOutputPath, "certificates/server/link-access-one")
-	clientPath := path.Join(customOutputPath, "certificates/client/client-link-access-one")
-	inputCaPath := path.Join(customOutputPath, "input/certificates/ca/skupper-site-ca")
-	inputServerPath := path.Join(customOutputPath, "input/certificates/server/link-access-one")
-	inputClientPath := path.Join(customOutputPath, "input/certificates/client/client-link-access-one")
+	caPath := path.Join(customOutputPath, string(api.CertificatesCaPath), "skupper-site-ca")
+	serverPath := path.Join(customOutputPath, string(api.CertificatesServerPath), "link-access-one")
+	clientPath := path.Join(customOutputPath, string(api.CertificatesClientPath), "client-link-access-one")
+	inputCaPath := path.Join(customOutputPath, string(api.InputCertificatesCaPath), "skupper-site-ca")
+	inputServerPath := path.Join(customOutputPath, string(api.InputCertificatesServerPath), "link-access-one")
+	inputClientPath := path.Join(customOutputPath, string(api.InputCertificatesClientPath), "client-link-access-one")
 	pathsToCompare := map[string]string{
 		caPath:     inputCaPath,
 		serverPath: inputServerPath,
@@ -128,9 +128,9 @@ func createInputCertificates(t *testing.T, customOutputPath string) {
 	client := certs.GenerateSecret("fake-client-cert", "fake-client-cert", "", &ca)
 
 	// paths for each provided certificate
-	caPath := path.Join(customOutputPath, "namespaces/default/input/certificates/ca/skupper-site-ca")
-	serverPath := path.Join(customOutputPath, "namespaces/default/input/certificates/server/link-access-one")
-	clientPath := path.Join(customOutputPath, "namespaces/default/input/certificates/client/client-link-access-one")
+	caPath := path.Join(customOutputPath, "namespaces/default", string(api.InputCertificatesCaPath), "skupper-site-ca")
+	serverPath := path.Join(customOutputPath, "namespaces/default", string(api.InputCertificatesServerPath), "link-access-one")
+	clientPath := path.Join(customOutputPath, "namespaces/default", string(api.InputCertificatesClientPath), "client-link-access-one")
 	certsMap := map[string]corev1.Secret{
 		caPath:     ca,
 		serverPath: server,
diff --git a/pkg/nonkube/common/site_state_validator_test.go b/pkg/nonkube/common/site_state_validator_test.go
index 492b8ab4..bec91013 100644
--- a/pkg/nonkube/common/site_state_validator_test.go
+++ b/pkg/nonkube/common/site_state_validator_test.go
@@ -66,7 +66,7 @@ func TestSiteStateValidator_Validate(t *testing.T) {
 				}
 			}),
 			valid:         false,
-			errorContains: "invalid router access: roles are required",
+			errorContains: "invalid router access: link-access-one - roles are required",
 		},
 		{
 			info: "invalid-link-access-roles-invalid-role",
diff --git a/pkg/nonkube/common/systemd_test.go b/pkg/nonkube/common/systemd_test.go
index c3387f8b..8252aac0 100644
--- a/pkg/nonkube/common/systemd_test.go
+++ b/pkg/nonkube/common/systemd_test.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/utils"
 	"gotest.tools/assert"
 )
@@ -35,8 +36,8 @@ func TestSystemdService(t *testing.T) {
 			assert.Assert(t, err)
 			assert.Equal(t, systemdService.GetServiceName(), "skupper-default.service")
 			systemdServiceImpl := systemdService.(*systemdServiceInfo)
-			assert.Equal(t, systemdServiceImpl.SiteScriptPath, path.Join(outputPath, "namespaces/default/runtime/scripts"))
-			assert.Equal(t, systemdServiceImpl.SiteConfigPath, path.Join(outputPath, "namespaces/default/config/router"))
+			assert.Equal(t, systemdServiceImpl.SiteScriptPath, path.Join(outputPath, "namespaces/default", string(api.ScriptsPath)))
+			assert.Equal(t, systemdServiceImpl.SiteConfigPath, path.Join(outputPath, "namespaces/default", string(api.RouterConfigPath)))
 			systemdServiceImpl.command = func(name string, arg ...string) *exec.Cmd {
 				assert.Assert(t, utils.StringSliceContains(arg, "--user") == (uid != 0))
 				t.Logf("mocking command: %s %s", name, strings.Join(arg, " "))
diff --git a/pkg/site/bindings_test.go b/pkg/site/bindings_test.go
index a7d6c572..5e8476fa 100644
--- a/pkg/site/bindings_test.go
+++ b/pkg/site/bindings_test.go
@@ -10,29 +10,34 @@ import (
 )
 
 func TestBindings_Apply(t *testing.T) {
+	vFalse := false
 	type fields struct {
-		SiteId     string
-		connectors map[string]*skupperv2alpha1.Connector
-		listeners  map[string]*skupperv2alpha1.Listener
+		path                   string
+		SiteId                 string
+		connectors             []*skupperv2alpha1.Connector
+		listeners              []*skupperv2alpha1.Listener
+		connectorConfiguration ConnectorConfiguration
+		listenerConfiguration  ListenerConfiguration
+		connectorFunction      ConnectorFunction
+		listenerFunction       ListenerFunction
 	}
-	type args struct {
-		tcpListeners  map[string]qdr.TcpEndpoint
-		tcpConnectors map[string]qdr.TcpEndpoint
+	type expected struct {
+		tcpListeners  qdr.TcpEndpointMap
+		tcpConnectors qdr.TcpEndpointMap
 		sslProfiles   map[string]qdr.SslProfile
 	}
 	tests := []struct {
-		name   string
-		fields fields
-		args   args
-		want   bool
+		name     string
+		fields   fields
+		config   *qdr.RouterConfig
+		expected expected
 	}{
 		{
 			name: "bind a tcp listener",
 			fields: fields{
-				SiteId:     "site-1",
-				connectors: make(map[string]*skupperv2alpha1.Connector),
-				listeners: map[string]*skupperv2alpha1.Listener{
-					"listener1": &skupperv2alpha1.Listener{
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
 						ObjectMeta: v1.ObjectMeta{
 							Name:      "listener1",
 							Namespace: "test",
@@ -46,21 +51,35 @@ func TestBindings_Apply(t *testing.T) {
 					},
 				},
 			},
-			args: args{
-				tcpListeners:  map[string]qdr.TcpEndpoint{},
-				tcpConnectors: map[string]qdr.TcpEndpoint{},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:    "listener1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "echo:9090",
+						SiteId:  "site-1",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
 				sslProfiles:   map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 		{
 			name: "bind a tcp connector",
 			fields: fields{
 				SiteId: "site-1",
-				connectors: map[string]*skupperv2alpha1.Connector{
-					"connector1": &skupperv2alpha1.Connector{
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
 						ObjectMeta: v1.ObjectMeta{
-							Name:      "listener1",
+							Name:      "connector1",
 							Namespace: "test",
 						},
 						Spec: skupperv2alpha1.ConnectorSpec{
@@ -71,99 +90,486 @@ func TestBindings_Apply(t *testing.T) {
 						},
 					},
 				},
-				listeners: make(map[string]*skupperv2alpha1.Listener),
 			},
-			args: args{
-				tcpListeners:  map[string]qdr.TcpEndpoint{},
-				tcpConnectors: map[string]qdr.TcpEndpoint{},
-				sslProfiles:   map[string]qdr.SslProfile{},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:    "connector1-10.10.10.1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "echo:9090",
+						SiteId:  "site-1",
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 		{
 			name: "unbind a tcp listener",
 			fields: fields{
-				SiteId:     "site-1",
-				connectors: make(map[string]*skupperv2alpha1.Connector),
-				listeners:  make(map[string]*skupperv2alpha1.Listener),
+				SiteId: "site-1",
 			},
-			args: args{
-				tcpListeners: map[string]qdr.TcpEndpoint{
-					"listener1": qdr.TcpEndpoint{
-						Name:   "listener1",
-						Host:   "10.10.10.1",
-						Port:   "9090",
-						SiteId: "site-1",
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners: map[string]qdr.TcpEndpoint{
+						"listener1": qdr.TcpEndpoint{
+							Name:   "listener1",
+							Host:   "10.10.10.1",
+							Port:   "9090",
+							SiteId: "site-1",
+						},
 					},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
 				},
-				tcpConnectors: map[string]qdr.TcpEndpoint{},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners:  qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{},
 				sslProfiles:   map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 		{
 			name: "unbind a tcp connector",
 			fields: fields{
-				SiteId:     "site-1",
-				connectors: make(map[string]*skupperv2alpha1.Connector),
-				listeners:  make(map[string]*skupperv2alpha1.Listener),
+				SiteId: "site-1",
 			},
-			args: args{
-				tcpListeners: map[string]qdr.TcpEndpoint{},
-				tcpConnectors: map[string]qdr.TcpEndpoint{
-					"connector1": qdr.TcpEndpoint{
-						Name:   "connector1",
-						Host:   "10.10.10.1",
-						Port:   "9090",
-						SiteId: "site-1",
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners: map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{
+						"connector1": qdr.TcpEndpoint{
+							Name:   "connector1",
+							Host:   "10.10.10.1",
+							Port:   "9090",
+							SiteId: "site-1",
+						},
+					},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners:  qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles:   map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "bind a tcp listener with ssl enabled",
+			fields: fields{
+				path:   "/etc/foo/",
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey:     "echo:9090",
+							Host:           "10.10.10.1",
+							Port:           9090,
+							Type:           "tcp",
+							TlsCredentials: "my-credentials",
+						},
+					},
+				},
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:       "listener1",
+						Host:       "10.10.10.1",
+						Port:       "9090",
+						Address:    "echo:9090",
+						SiteId:     "site-1",
+						SslProfile: "my-credentials",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles: map[string]qdr.SslProfile{
+					"my-credentials": {
+						Name:           "my-credentials",
+						CertFile:       "/etc/foo/my-credentials/tls.crt",
+						PrivateKeyFile: "/etc/foo/my-credentials/tls.key",
+						CaCertFile:     "/etc/foo/my-credentials/ca.crt",
+					},
+				},
+			},
+		},
+		{
+			name: "bind a tcp connector with ssl enabled",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey:     "echo:9090",
+							Host:           "10.10.10.1",
+							Port:           9090,
+							Type:           "tcp",
+							TlsCredentials: "a-secret",
+						},
+					},
+				},
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:           "connector1-10.10.10.1",
+						Host:           "10.10.10.1",
+						Port:           "9090",
+						Address:        "echo:9090",
+						SiteId:         "site-1",
+						SslProfile:     "a-secret",
+						VerifyHostname: &vFalse,
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{
+					"a-secret": {
+						Name:           "a-secret",
+						CertFile:       "a-secret/tls.crt",
+						PrivateKeyFile: "a-secret/tls.key",
+						CaCertFile:     "a-secret/ca.crt",
+					},
+				},
+			},
+		},
+		{
+			name: "bind a tcp connector with ssl enabled but no client auth",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey:     "echo:9090",
+							Host:           "10.10.10.1",
+							Port:           9090,
+							Type:           "tcp",
+							TlsCredentials: "a-secret",
+							NoClientAuth:   true,
+						},
+					},
+				},
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:           "connector1-10.10.10.1",
+						Host:           "10.10.10.1",
+						Port:           "9090",
+						Address:        "echo:9090",
+						SiteId:         "site-1",
+						SslProfile:     "a-secret-profile",
+						VerifyHostname: &vFalse,
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{
+					"a-secret-profile": {
+						Name:       "a-secret-profile",
+						CaCertFile: "a-secret-profile/ca.crt",
+					},
+				},
+			},
+		},
+		{
+			name: "configure connector for pods",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				connectorConfiguration: getPodConnectorConfiguration([]skupperv2alpha1.PodDetails{
+					{
+						UID: "pod1",
+						IP:  "11.5.6.21",
+					},
+					{
+						UID: "pod2",
+						IP:  "11.5.6.22",
+					},
+				}),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-11.5.6.21": {
+						Name:      "connector1-11.5.6.21",
+						Host:      "11.5.6.21",
+						Port:      "9090",
+						Address:   "echo:9090",
+						SiteId:    "site-1",
+						ProcessID: "pod1",
+					},
+					"connector1-11.5.6.22": {
+						Name:      "connector1-11.5.6.22",
+						Host:      "11.5.6.22",
+						Port:      "9090",
+						Address:   "echo:9090",
+						SiteId:    "site-1",
+						ProcessID: "pod2",
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "listener host and port override",
+			fields: fields{
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listenerConfiguration: getListenerHostPortOverride("my-host", 5678),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:    "listener1",
+						Host:    "my-host",
+						Port:    "5678",
+						Address: "echo:9090",
+						SiteId:  "site-1",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles:   map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "mapped listener",
+			fields: fields{
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listenerFunction: changeListenerRoutingKey("foo"),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:    "listener1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "foo",
+						SiteId:  "site-1",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles:   map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "mapped connector",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				connectorFunction: changeConnectorRoutingKey("foo"),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:    "connector1-10.10.10.1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "foo",
+						SiteId:  "site-1",
 					},
 				},
 				sslProfiles: map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			b := NewBindings()
-			b.SiteId = tt.fields.SiteId
-			b.listeners = tt.fields.listeners
-			b.connectors = tt.fields.connectors
-			// redundant for coverage
-			b.SetListenerConfiguration(b.configure.listener)
-			b.SetConnectorConfiguration(b.configure.connector)
-
-			argsConfig := qdr.InitialConfig("some-id", tt.fields.SiteId, "v2.0", false, 10)
-			argsConfig.Bridges.TcpListeners = tt.args.tcpListeners
-			argsConfig.Bridges.TcpConnectors = tt.args.tcpConnectors
-			argsConfig.SslProfiles = tt.args.sslProfiles
-
-			if got := b.Apply(&argsConfig); got != tt.want {
-				t.Errorf("Bindings.Apply() = %v, want %v", got, tt.want)
+			b := NewBindings(tt.fields.path)
+			b.SetSiteId(tt.fields.SiteId)
+			if tt.fields.listenerConfiguration != nil {
+				b.SetListenerConfiguration(tt.fields.listenerConfiguration)
+			}
+			if tt.fields.connectorConfiguration != nil {
+				b.SetConnectorConfiguration(tt.fields.connectorConfiguration)
+			}
+			for _, listener := range tt.fields.listeners {
+				b.UpdateListener(listener.Name, listener)
+			}
+			for _, connector := range tt.fields.connectors {
+				b.UpdateConnector(connector.Name, connector)
+			}
+			if tt.fields.connectorFunction != nil || tt.fields.listenerFunction != nil {
+				b.Map(tt.fields.connectorFunction, tt.fields.listenerFunction)
 			}
-			assert.Assert(t, len(argsConfig.Bridges.TcpListeners) == len(b.listeners))
-			assert.Assert(t, len(argsConfig.Bridges.TcpConnectors) == len(b.connectors))
+
+			b.Apply(tt.config)
+			assert.DeepEqual(t, tt.config.Bridges.TcpListeners, tt.expected.tcpListeners)
+			assert.DeepEqual(t, tt.config.Bridges.TcpConnectors, tt.expected.tcpConnectors)
+			assert.DeepEqual(t, tt.config.SslProfiles, tt.expected.sslProfiles)
 		})
 	}
 }
 
+func changeConnectorRoutingKey(key string) ConnectorFunction {
+	return func(input *skupperv2alpha1.Connector) *skupperv2alpha1.Connector {
+		copy := *input
+		copy.Spec.RoutingKey = key
+		return &copy
+	}
+}
+
+func changeListenerRoutingKey(key string) ListenerFunction {
+	return func(input *skupperv2alpha1.Listener) *skupperv2alpha1.Listener {
+		copy := *input
+		copy.Spec.RoutingKey = key
+		return &copy
+	}
+}
+
+func getListenerHostPortOverride(host string, port int) ListenerConfiguration {
+	return func(siteId string, listener *skupperv2alpha1.Listener, config *qdr.BridgeConfig) {
+		UpdateBridgeConfigForListenerWithHostAndPort(siteId, listener, host, port, config)
+	}
+}
+
+func getPodConnectorConfiguration(pods []skupperv2alpha1.PodDetails) ConnectorConfiguration {
+	return func(siteId string, connector *skupperv2alpha1.Connector, config *qdr.BridgeConfig) {
+		for _, pod := range pods {
+			UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, config)
+		}
+	}
+}
+
 func TestBindings_UpdateListener(t *testing.T) {
 	type fields struct {
 		SiteId     string
 		connectors map[string]*skupperv2alpha1.Connector
 		listeners  map[string]*skupperv2alpha1.Listener
-		handler    BindingEventHandler
 	}
 	type args struct {
 		name     string
 		listener *skupperv2alpha1.Listener
 	}
+	type expected struct {
+		listener  events
+		connector events
+	}
 	tests := []struct {
-		name    string
-		fields  fields
-		args    args
-		want    qdr.ConfigUpdate
-		wantErr bool
+		name     string
+		fields   fields
+		args     args
+		expected expected
 	}{
 		{
 			name: "add listener",
@@ -187,7 +593,11 @@ func TestBindings_UpdateListener(t *testing.T) {
 					},
 				},
 			},
-			wantErr: false,
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1"},
+				},
+			},
 		},
 		{
 			name: "delete listener",
@@ -213,17 +623,125 @@ func TestBindings_UpdateListener(t *testing.T) {
 				name:     "listener1",
 				listener: nil,
 			},
-			wantErr: false,
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1"},
+					Deleted: []string{"listener1"},
+				},
+			},
+		},
+		{
+			name: "delete listener that is not there",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners:  make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name:     "listener1",
+				listener: nil,
+			},
+		},
+		{
+			name: "update listener",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners: map[string]*skupperv2alpha1.Listener{
+					"listener1": &skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+			},
+			args: args{
+				name: "listener1",
+				listener: &skupperv2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "listener1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ListenerSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9999,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1", "listener1"},
+				},
+			},
+		},
+		{
+			name: "unchanged listener",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners: map[string]*skupperv2alpha1.Listener{
+					"listener1": &skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+			},
+			args: args{
+				name: "listener1",
+				listener: &skupperv2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "listener1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ListenerSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9090,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1"},
+				},
+			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			b := NewBindings()
-			b.SiteId = tt.fields.SiteId
-			b.listeners = tt.fields.listeners
-			b.connectors = tt.fields.connectors
+			b := NewBindings("")
+			b.SetSiteId(tt.fields.SiteId)
+			for _, listener := range tt.fields.listeners {
+				b.UpdateListener(listener.Name, listener)
+			}
+			for _, connector := range tt.fields.connectors {
+				b.UpdateConnector(connector.Name, connector)
+			}
+			handler := &TestBindingEventHandler{}
+			b.SetBindingEventHandler(handler)
 
 			b.UpdateListener(tt.args.name, tt.args.listener)
+			assert.Equal(t, b.GetListener(tt.args.name), tt.args.listener)
+			assert.DeepEqual(t, handler.listener, tt.expected.listener)
+			assert.DeepEqual(t, handler.connector, tt.expected.connector)
 		})
 	}
 }
@@ -233,7 +751,6 @@ func TestBindings_UpdateConnector(t *testing.T) {
 		SiteId     string
 		connectors map[string]*skupperv2alpha1.Connector
 		listeners  map[string]*skupperv2alpha1.Listener
-		handler    BindingEventHandler
 		configure  struct {
 			listener  ListenerConfiguration
 			connector ConnectorConfiguration
@@ -243,12 +760,15 @@ func TestBindings_UpdateConnector(t *testing.T) {
 		name      string
 		connector *skupperv2alpha1.Connector
 	}
+	type expected struct {
+		listener  events
+		connector events
+	}
 	tests := []struct {
-		name    string
-		fields  fields
-		args    args
-		want    qdr.ConfigUpdate
-		wantErr bool
+		name     string
+		fields   fields
+		args     args
+		expected expected
 	}{
 		{
 			name: "add connector",
@@ -272,7 +792,11 @@ func TestBindings_UpdateConnector(t *testing.T) {
 					},
 				},
 			},
-			wantErr: false,
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1"},
+				},
+			},
 		},
 		{
 			name: "delete connector",
@@ -298,17 +822,160 @@ func TestBindings_UpdateConnector(t *testing.T) {
 				name:      "connector1",
 				connector: nil,
 			},
-			wantErr: false,
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1"},
+					Deleted: []string{"connector1"},
+				},
+			},
+		},
+		{
+			name: "delete connector that is not there",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners:  make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name:      "connector1",
+				connector: nil,
+			},
+		},
+		{
+			name: "update connector",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: map[string]*skupperv2alpha1.Connector{
+					"connector1": &skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listeners: make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name: "connector1",
+				connector: &skupperv2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "connector1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ConnectorSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9999,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1", "connector1"},
+				},
+			},
+		},
+		{
+			name: "add connector that is already there",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: map[string]*skupperv2alpha1.Connector{
+					"connector1": &skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listeners: make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name: "connector1",
+				connector: &skupperv2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "connector1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ConnectorSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9090,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1"},
+				},
+			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			b := NewBindings()
-			b.SiteId = tt.fields.SiteId
-			b.listeners = tt.fields.listeners
-			b.connectors = tt.fields.connectors
+			b := NewBindings("")
+			b.SetSiteId(tt.fields.SiteId)
+			for _, listener := range tt.fields.listeners {
+				b.UpdateListener(listener.Name, listener)
+			}
+			for _, connector := range tt.fields.connectors {
+				b.UpdateConnector(connector.Name, connector)
+			}
+			handler := &TestBindingEventHandler{}
+			b.SetBindingEventHandler(handler)
 
 			b.UpdateConnector(tt.args.name, tt.args.connector)
+			assert.Equal(t, b.GetConnector(tt.args.name), tt.args.connector)
+			assert.DeepEqual(t, handler.listener, tt.expected.listener)
+			assert.DeepEqual(t, handler.connector, tt.expected.connector)
 		})
 	}
 }
+
+type events struct {
+	Updated []string
+	Deleted []string
+}
+
+func (e *events) updated(name string) {
+	e.Updated = append(e.Updated, name)
+}
+
+func (e *events) deleted(name string) {
+	e.Deleted = append(e.Deleted, name)
+}
+
+type TestBindingEventHandler struct {
+	listener  events
+	connector events
+}
+
+func (h *TestBindingEventHandler) ListenerUpdated(listener *skupperv2alpha1.Listener) {
+	h.listener.updated(listener.Name)
+}
+
+func (h *TestBindingEventHandler) ListenerDeleted(listener *skupperv2alpha1.Listener) {
+	h.listener.deleted(listener.Name)
+}
+
+func (h *TestBindingEventHandler) ConnectorUpdated(connector *skupperv2alpha1.Connector) bool {
+	h.connector.updated(connector.Name)
+	return false
+}
+
+func (h *TestBindingEventHandler) ConnectorDeleted(connector *skupperv2alpha1.Connector) {
+	h.connector.deleted(connector.Name)
+}
diff --git a/pkg/site/link_test.go b/pkg/site/link_test.go
index 56f88cbd..cebf750d 100644
--- a/pkg/site/link_test.go
+++ b/pkg/site/link_test.go
@@ -426,6 +426,63 @@ func TestLink_Update(t *testing.T) {
 			if got := link.Update(tt.args.definition); got != tt.want {
 				t.Errorf("Link.Update() = %v, want %v", got, tt.want)
 			}
+			assert.Equal(t, link.Definition(), tt.args.definition)
+			assert.DeepEqual(t, link.Definition(), tt.args.definition)
+		})
+	}
+}
+
+func TestRemoveConnector_Apply(t *testing.T) {
+	type fields struct {
+		name string
+	}
+	type args struct {
+		current qdr.RouterConfig
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		want   bool
+	}{
+		{
+			name: "remove connector",
+			fields: fields{
+				name: "link1",
+			},
+			args: args{
+				current: qdr.RouterConfig{
+					Connectors: map[string]qdr.Connector{
+						"link1": {
+							Name:       "link1",
+							SslProfile: "link1-profile",
+						},
+					},
+					SslProfiles: map[string]qdr.SslProfile{
+						"link1-profile": {
+							Name: "link1-profile",
+						},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "connector does not exist",
+			fields: fields{
+				name: "link1",
+			},
+			args: args{
+				current: qdr.RouterConfig{},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := NewRemoveConnector(tt.fields.name).Apply(&tt.args.current); got != tt.want {
+				t.Errorf("Link.Apply() = %v, want %v", got, tt.want)
+			}
 		})
 	}
 }