Repository: skupperproject/skupper
Pull request #1631 (2024-09-03 17:00:36)

[v2] ci updates


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0f1f2463..8069471b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -99,6 +99,8 @@ commands:
             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
@@ -135,6 +137,8 @@ commands:
             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
             gunzip -c /tmp/images/test-image.gz | docker import - 0.0.0.0:5000/skupper-tests
             gunzip -c /tmp/images/test-image.gz | docker load
       - run:
@@ -143,6 +147,7 @@ commands:
             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
             docker push 0.0.0.0:5000/skupper-tests
 
   system_monitor:
@@ -160,6 +165,7 @@ commands:
         name: journalctl monitoring
         command: journalctl -p 7 -b -f
         background: true
+
   system_status:
     description: shows some point-in-time system status
     parameters:
@@ -344,6 +350,12 @@ yaml-templates:
         ignore: /.*/
         only: /main/
 
+  v2_branch_filters: &run_for_v2_branch
+    filters:
+      branches:
+        ignore: /.*/
+        only: /v2/
+
   release_filters: &run_for_numeric_tags
     filters:
       branches:
@@ -359,6 +371,12 @@ yaml-templates:
       - main_tests_minikube_local_cluster
       - policy_tests_minikube_local_cluster
 
+  v2_release_requires: &v2_release_requires
+    requires:
+      - build-all
+      - test-skupper-binary
+      - test
+
 workflows:
   version: 2.1
   build-workflow:
@@ -378,8 +396,8 @@ workflows:
             - build-all
       - test:
           <<: *run_for_all_branches_and_numeric_tags
-      # - build_and_save_test_images:
-      #     <<: *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
@@ -398,10 +416,10 @@ workflows:
       #       - build_and_save_test_images
 
       # - publish-github-release-images:
-      #     <<: *run_for_numeric_tags
-      #     <<: *release_requires
-      #     context:
-      #       - skupper-org
+      #    <<: *run_for_numeric_tags
+      #    <<: *release_requires
+      #    context:
+      #      - skupper-org
 
       # - generate-manifest:
       #     <<: *run_for_numeric_tags
@@ -433,6 +451,19 @@ workflows:
       #     context:
       #       - skupper-org
 
+      - publish-github-v2-artifacts:
+          <<: *run_for_v2_branch
+          requires:
+            - publish-github-v2-images
+          context:
+            - skupper-org
+
+      - publish-github-v2-latest-images:
+          <<: *run_for_v2_branch
+          <<: *v2_release_requires
+          context:
+            - skupper-org
+
 jobs:
   test:
     executor:
@@ -692,3 +723,45 @@ jobs:
             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-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:
+    docker:
+      - image: cibuilds/github:0.10
+    steps:
+      - attach_workspace:
+          at: .
+      - run:
+          name: "Create a V2 Pre-Release on GitHub"
+          command: |
+            VERSION="v2-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"
diff --git a/Makefile b/Makefile
index bb843702..9019e2a7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,8 @@
 VERSION := $(shell git describe --tags --dirty=-modified --always)
-CONTROLLER_IMAGE := quay.io/skupper/controller
-BOOTSTRAP_IMAGE := quay.io/skupper/bootstrap
-CONFIG_SYNC_IMAGE := quay.io/skupper/config-sync
-NETWORK_CONSOLE_COLLECTOR_IMAGE := quay.io/skupper/network-console-collector
+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
 TEST_BINARIES_FOLDER := ${PWD}/test/integration/bin
 DOCKER := docker
@@ -11,7 +11,7 @@ PLATFORMS ?= linux/amd64,linux/arm64
 GOOS ?= linux
 GOARCH ?= amd64
 
-all: build-cmd build-config-sync build-controller build-tests build-manifest
+all: build-cmd build-config-sync build-controller build-bootstrap build-tests build-manifest build-network-console-collector
 
 build-tests:
 	mkdir -p ${TEST_BINARIES_FOLDER}
@@ -96,7 +96,7 @@ test:
 	go test -v -count=1 ./pkg/... ./internal/... ./cmd/...
 
 clean:
-	rm -rf skupper controller release config-sync manifest bootstrap ${TEST_BINARIES_FOLDER}
+	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
 
diff --git a/cmd/bootstrap/bootstrap.sh b/cmd/bootstrap/bootstrap.sh
index c6922225..ea973ab7 100755
--- a/cmd/bootstrap/bootstrap.sh
+++ b/cmd/bootstrap/bootstrap.sh
@@ -2,7 +2,7 @@
 
 set -Ceu
 
-IMAGE="quay.io/skupper/bootstrap"
+IMAGE="quay.io/skupper/bootstrap:v2-latest"
 INPUT_PATH="${1:-${PWD}}"
 SKUPPER_OUTPUT_PATH="${XDG_DATA_HOME:-${HOME}/.local/share}/skupper"
 SERVICE_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
diff --git a/cmd/network-console-collector/resources/developer/native-complete.yaml b/cmd/network-console-collector/resources/developer/native-complete.yaml
index be4e4c18..13acf5d2 100644
--- a/cmd/network-console-collector/resources/developer/native-complete.yaml
+++ b/cmd/network-console-collector/resources/developer/native-complete.yaml
@@ -242,7 +242,7 @@ spec:
         app.kubernetes.io/component: server
     spec:
       containers:
-      - image: quay.io/ckruse/network-console-collector:latest
+      - image: quay.io/skupper/network-console-collector:v2-latest
         imagePullPolicy: Always
         name: network-console
         args:
diff --git a/cmd/network-console-collector/resources/developer/openshift-complete.yaml b/cmd/network-console-collector/resources/developer/openshift-complete.yaml
index 0b66d529..9f07dab7 100644
--- a/cmd/network-console-collector/resources/developer/openshift-complete.yaml
+++ b/cmd/network-console-collector/resources/developer/openshift-complete.yaml
@@ -238,7 +238,7 @@ spec:
         app.kubernetes.io/component: server
     spec:
       containers:
-      - image: quay.io/ckruse/network-console-collector:latest
+      - image: quay.io/skupper/network-console-collector:v2-latest
         imagePullPolicy: Always
         name: network-console
         args:
diff --git a/cmd/network-console-collector/resources/native/deployment.yaml b/cmd/network-console-collector/resources/native/deployment.yaml
index 86a962c8..cd38c877 100644
--- a/cmd/network-console-collector/resources/native/deployment.yaml
+++ b/cmd/network-console-collector/resources/native/deployment.yaml
@@ -58,7 +58,7 @@ spec:
         app.kubernetes.io/component: server
     spec:
       containers:
-      - image: quay.io/ckruse/network-console-collector:latest
+      - image: quay.io/skupper/network-console-collector:v2-latest
         imagePullPolicy: Always
         name: network-console
         args:
diff --git a/cmd/network-console-collector/resources/openshift/deployment.yaml b/cmd/network-console-collector/resources/openshift/deployment.yaml
index 76de3868..45d9b0af 100644
--- a/cmd/network-console-collector/resources/openshift/deployment.yaml
+++ b/cmd/network-console-collector/resources/openshift/deployment.yaml
@@ -43,7 +43,7 @@ spec:
       serviceAccountName: network-console-collector
       containers:
       - name: network-console
-        image: quay.io/ckruse/network-console-collector:latest
+        image: quay.io/skupper/network-console-collector:v2-latest
         imagePullPolicy: Always
         args:
           - -listen=127.0.0.1:8080
diff --git a/pkg/site/bindings.go b/pkg/site/bindings.go
index 02fbb083..600b110b 100644
--- a/pkg/site/bindings.go
+++ b/pkg/site/bindings.go
@@ -87,6 +87,13 @@ func (b *Bindings) GetConnector(name string) *skupperv1alpha1.Connector {
 	return nil
 }
 
+func (b *Bindings) GetListener(name string) *skupperv1alpha1.Listener {
+	if existing, ok := b.listeners[name]; ok {
+		return existing
+	}
+	return nil
+}
+
 func (b *Bindings) UpdateConnector(name string, connector *skupperv1alpha1.Connector) qdr.ConfigUpdate {
 	if connector == nil {
 		return b.deleteConnector(name)


Test patch:
diff --git a/pkg/kube/securedaccess/access_test.go b/pkg/kube/securedaccess/access_test.go
new file mode 100644
index 00000000..1b129588
--- /dev/null
+++ b/pkg/kube/securedaccess/access_test.go
@@ -0,0 +1,766 @@
+package securedaccess
+
+import (
+	"reflect"
+	"testing"
+
+	routev1 "github.com/openshift/api/route/v1"
+	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"gotest.tools/assert"
+	corev1 "k8s.io/api/core/v1"
+	networkingv1 "k8s.io/api/networking/v1beta1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func TestSecuredAccessManagerEnsure(t *testing.T) {
+	type args struct {
+		namespace   string
+		name        string
+		spec        *skupperv1alpha1.SecuredAccessSpec
+		annotations *map[string]string
+		refs        *[]metav1.OwnerReference
+	}
+
+	testTable := []struct {
+		name                string
+		args                args
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no existing secureAccess",
+			args: args{
+				namespace: "test",
+				name:      "skupper",
+				spec: &skupperv1alpha1.SecuredAccessSpec{
+					AccessType: ACCESS_TYPE_LOADBALANCER,
+					Ports: []skupperv1alpha1.SecuredAccessPort{
+						{
+							Name:       "port1",
+							Port:       8080,
+							TargetPort: 8081,
+							Protocol:   "TCP",
+						},
+					},
+					Certificate: "skupper",
+					Issuer:      "skupper-site-ca",
+				},
+				annotations: &map[string]string{
+					"internal.skupper.io/controlled":   "true",
+					"internal.skupper.io/routeraccess": "name",
+				},
+				refs: &[]metav1.OwnerReference{
+					{
+						Kind:       "SecuredAccess",
+						APIVersion: "skupper.io/v1alpha1",
+						Name:       "ownerRef",
+						UID:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range testTable {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+
+			if len(m.definitions) != 0 {
+				t.Errorf("SecuredAccessManager.Ensure() expected no definitons")
+			}
+			if err := m.Ensure(tt.args.namespace, tt.args.name, *tt.args.spec, *tt.args.annotations, *tt.args.refs); err != nil {
+				t.Errorf("SecuredAccessManager.Ensure() error = %v", err)
+			}
+			if m.definitions["test/skupper"].Annotations == nil {
+				t.Errorf("Annotations not as expected")
+			}
+			if m.definitions["test/skupper"].OwnerReferences == nil {
+				t.Errorf("OwnerReferences not as expected")
+			}
+			// issue with same values expect return with no changes
+			if err := m.Ensure(tt.args.namespace, tt.args.name, *tt.args.spec, *tt.args.annotations, *tt.args.refs); err != nil {
+				t.Errorf("SecuredAccessManager.Ensure() issue same, definiton error = %v", err)
+			}
+			// change key expect two definitions
+			if err := m.Ensure(tt.args.namespace, "skupper2", *tt.args.spec, *tt.args.annotations, *tt.args.refs); err != nil {
+				t.Errorf("SecuredAccessManager.Ensure() changing key, error = %v", err)
+			}
+			if len(m.definitions) != 2 {
+				t.Errorf("SecuredAccessManager.Ensure() expected two definitions")
+			}
+			// change existing entry expect two definitions
+			tt.args.spec.Ports[0].Name = "Port2"
+			if err := m.Ensure(tt.args.namespace, tt.args.name, *tt.args.spec, *tt.args.annotations, *tt.args.refs); err != nil {
+				t.Errorf("SecuredAccessManager.Ensure() modifying existing, error = %v", err)
+			}
+			// still two definitons
+			if len(m.definitions) != 2 {
+				t.Errorf("SecuredAccessManager.Ensure() expected two entries")
+			}
+		})
+	}
+}
+
+func TestSecuredAccessManagerChangeDelete(t *testing.T) {
+	type args struct {
+		namespace   string
+		name        string
+		spec        *skupperv1alpha1.SecuredAccessSpec
+		annotations *map[string]string
+		refs        *[]metav1.OwnerReference
+	}
+
+	testTable := []struct {
+		name                string
+		args                args
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		current             *skupperv1alpha1.SecuredAccess
+	}{
+		{
+			name: "no existing secureAccess",
+			args: args{
+				namespace: "test",
+				name:      "skupper",
+				spec: &skupperv1alpha1.SecuredAccessSpec{
+					AccessType: ACCESS_TYPE_LOADBALANCER,
+					Ports: []skupperv1alpha1.SecuredAccessPort{
+						{
+							Name:       "port1",
+							Port:       8080,
+							TargetPort: 8081,
+							Protocol:   "TCP",
+						},
+					},
+					Certificate: "skupper",
+					Issuer:      "skupper-site-ca",
+				},
+				annotations: &map[string]string{
+					"internal.skupper.io/controlled":   "true",
+					"internal.skupper.io/routeraccess": "name",
+				},
+				refs: &[]metav1.OwnerReference{
+					{
+						Kind:       "SecuredAccess",
+						APIVersion: "skupper.io/v1alpha1",
+						Name:       "ownerRef",
+						UID:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+				},
+			},
+			current: &skupperv1alpha1.SecuredAccess{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "skupper.io/v1alpha1",
+					Kind:       "SecuredAccess",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "skupper",
+					Namespace: "test",
+					OwnerReferences: []metav1.OwnerReference{
+						{
+							Kind:       "SecuredAccess",
+							APIVersion: "skupper.io/v1alpha1",
+							Name:       "ownerRef",
+							UID:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+						},
+					},
+					Annotations: map[string]string{
+						"internal.skupper.io/controlled":   "true",
+						"internal.skupper.io/routeraccess": "name",
+					},
+				},
+				Spec: skupperv1alpha1.SecuredAccessSpec{
+					AccessType: ACCESS_TYPE_LOCAL,
+					Ports: []skupperv1alpha1.SecuredAccessPort{
+						{
+							Name:       "port1",
+							Port:       8080,
+							TargetPort: 8081,
+							Protocol:   "TCP",
+						},
+					},
+					Certificate: "skupper",
+					Issuer:      "",
+				},
+			},
+		},
+	}
+
+	for _, tt := range testTable {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+
+			if err := m.Ensure(tt.args.namespace, tt.args.name, *tt.args.spec, *tt.args.annotations, *tt.args.refs); err != nil {
+				t.Errorf("SecuredAccessManager.Ensure() error = %v", err)
+			}
+			// change Access Type
+			if err = m.SecuredAccessChanged("test/skupper", tt.current); err != nil {
+				t.Errorf("SecuredAccessManager.SecuredAccessChanged error = %v", err)
+			}
+			// expect one service and one definition
+			if len(m.definitions) != 1 {
+				t.Errorf("SecuredAccessManager.Ensure() expected one definition")
+			}
+			if len(m.services) != 1 {
+				t.Errorf("SecuredAccessManager.Ensure() expected one service")
+			}
+			// delete secured access
+			if err = m.SecuredAccessDeleted("test/skupper"); err != nil {
+				t.Errorf("SecuredAccessManager.SecuredAccessDeleted() failed delete error= %v", err)
+			}
+			// expect no services or definitions
+			if len(m.definitions) != 0 {
+				t.Errorf("SecuredAccessManager.SecuredAccessDeleted() expected no definition")
+			}
+			if len(m.services) != 0 {
+				t.Errorf("SecuredAccessManager.SecuredAccessDeleted() expected no service")
+			}
+			hosts := getHosts(tt.current)
+			if len(hosts) != 2 {
+				t.Errorf("SecuredAccessManager.getHosts() expected %d", len(hosts))
+			}
+		})
+	}
+}
+
+func TestSecuredAccessManagerCheckService(t *testing.T) {
+	type args struct {
+		namespace   string
+		name        string
+		spec        *skupperv1alpha1.SecuredAccessSpec
+		annotations *map[string]string
+		refs        *[]metav1.OwnerReference
+	}
+
+	testTable := []struct {
+		name                string
+		args                args
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		svc                 *corev1.Service
+		expectErr           bool
+	}{
+		{
+			name: "no existing service",
+			args: args{
+				namespace: "test",
+				name:      "skupper",
+				spec: &skupperv1alpha1.SecuredAccessSpec{
+					AccessType: ACCESS_TYPE_LOADBALANCER,
+					Ports: []skupperv1alpha1.SecuredAccessPort{
+						{
+							Name:       "port1",
+							Port:       8080,
+							TargetPort: 8081,
+							Protocol:   "TCP",
+						},
+					},
+					Certificate: "skupper",
+					Issuer:      "skupper-site-ca",
+				},
+				annotations: &map[string]string{
+					"internal.skupper.io/controlled":   "true",
+					"internal.skupper.io/routeraccess": "name",
+				},
+				refs: &[]metav1.OwnerReference{
+					{
+						Kind:       "SecuredAccess",
+						APIVersion: "skupper.io/v1alpha1",
+						Name:       "ownerRef",
+						UID:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+				},
+			},
+			expectErr: false,
+		},
+		{
+			name: "existing service",
+			args: args{
+				namespace: "test",
+				name:      "skupper",
+				spec: &skupperv1alpha1.SecuredAccessSpec{
+					AccessType: ACCESS_TYPE_LOADBALANCER,
+					Selector: map[string]string{
+						"skupper.io/component": "router",
+					},
+					Ports: []skupperv1alpha1.SecuredAccessPort{
+						{
+							Name:       "port1",
+							Port:       8080,
+							TargetPort: 8081,
+							Protocol:   "TCP",
+						},
+					},
+					Certificate: "skupper",
+					Issuer:      "skupper-site-ca",
+				},
+				annotations: &map[string]string{
+					"internal.skupper.io/controlled": "true",
+				},
+				refs: &[]metav1.OwnerReference{
+					{
+						Kind:       "SecuredAccess",
+						APIVersion: "skupper.io/v1alpha1",
+						Name:       "ownerRef",
+						UID:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+				},
+			},
+			svc: &corev1.Service{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "v1",
+					Kind:       "Service",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "skupper",
+					Namespace: "test",
+					Labels: map[string]string{
+						"internal.skupper.io/secured-access": "true",
+					},
+					Annotations: map[string]string{
+						"internal.skupper.io/controlled": "true",
+					},
+				},
+				Spec: corev1.ServiceSpec{
+					Selector: map[string]string{
+						"skupper.io/component": "router",
+					},
+					Type: corev1.ServiceTypeLoadBalancer, //ACCESS_TYPE_ROUTE,
+					Ports: []corev1.ServicePort{
+						{
+							Name:       "port1",
+							Port:       8080,
+							TargetPort: intstr.IntOrString{IntVal: int32(8081)},
+							Protocol:   "TCP",
+						},
+					},
+				},
+			},
+			expectErr: false,
+		},
+		{
+			name: "service parameter change",
+			args: args{
+				namespace: "test",
+				name:      "skupper",
+				spec: &skupperv1alpha1.SecuredAccessSpec{
+					AccessType: ACCESS_TYPE_LOADBALANCER,
+					Selector: map[string]string{
+						"skupper.io/component": "router",
+					},
+					Ports: []skupperv1alpha1.SecuredAccessPort{
+						{
+							Name:       "port1",
+							Port:       8080,
+							TargetPort: 8081,
+							Protocol:   "TCP",
+						},
+					},
+					Certificate: "skupper",
+					Issuer:      "skupper-site-ca",
+				},
+				annotations: &map[string]string{
+					"internal.skupper.io/controlled": "true",
+				},
+				refs: &[]metav1.OwnerReference{
+					{
+						Kind:       "SecuredAccess",
+						APIVersion: "skupper.io/v1alpha1",
+						Name:       "ownerRef",
+						UID:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+				},
+			},
+			svc: &corev1.Service{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "v1",
+					Kind:       "Service",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "skupper",
+					Namespace: "test",
+					Labels: map[string]string{
+						"internal.skupper.io/secured-access": "true",
+					},
+					Annotations: map[string]string{
+						"internal.skupper.io/controlled": "true",
+					},
+				},
+				Spec: corev1.ServiceSpec{
+					Selector: map[string]string{
+						"skupper.io/component": "controller",
+					},
+					Type: corev1.ServiceTypeClusterIP,
+					Ports: []corev1.ServicePort{
+						{
+							Name:       "port1",
+							Port:       8880,
+							TargetPort: intstr.IntOrString{IntVal: int32(8881)},
+							Protocol:   "TCP",
+						},
+					},
+				},
+			},
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range testTable {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+
+			if err := m.Ensure(tt.args.namespace, tt.args.name, *tt.args.spec, *tt.args.annotations, *tt.args.refs); err != nil {
+				t.Errorf("SecuredAccessManager.CheckServic() error = %v", err)
+			}
+
+			// check service
+			if err = m.CheckService("test/skupper", tt.svc); err != nil {
+				if tt.expectErr {
+					// expected error since service doesn't exist so create one
+					m.RecoverService(tt.svc)
+				} else {
+					t.Errorf("SecuredAccessManager.CheckService error = %v", err)
+				}
+			}
+			// expect one service and one definition
+			if len(m.definitions) != 1 {
+				t.Errorf("SecuredAccessManager.CheckService() expected one definition")
+			}
+			if len(m.services) != 1 {
+				t.Errorf("SecuredAccessManager.CheckService() expected one service")
+			}
+			// check values were modified
+			if tt.svc != nil {
+				skey := serviceKey(tt.svc)
+				if !reflect.DeepEqual(tt.svc.Spec.Selector, m.services[skey].Spec.Selector) {
+					t.Errorf("SecuredAccessManager.updateSelector() expected selector to be updated")
+				}
+				if tt.svc.Spec.Type != m.services[skey].Spec.Type {
+					t.Errorf("SecuredAccessManager.updateType() expected Type to be updated")
+				}
+			}
+		})
+	}
+}
+
+func TestSecuredAccessManagerRecoverRoute(t *testing.T) {
+	type args struct {
+		route *routev1.Route
+	}
+	testTable := []struct {
+		name                string
+		args                args
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "good route",
+			args: args{
+				route: &routev1.Route{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "route",
+						Namespace: "test",
+					},
+					Spec: routev1.RouteSpec{
+						Path: "",
+						Host: "test/host",
+						Port: &routev1.RoutePort{
+							TargetPort: intstr.FromString("8080"),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range testTable {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+
+			m.RecoverRoute(tt.args.route)
+			if len(m.routes) != 1 {
+				t.Errorf("SecuredAccessManager.RecoverRoute() expect one route")
+			}
+		})
+	}
+}
+
+func TestSecuredAccessManagerRecoverIngress(t *testing.T) {
+	type args struct {
+		ingress *networkingv1.Ingress
+	}
+	testTable := []struct {
+		name                string
+		args                args
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "ingress",
+			args: args{
+				ingress: &networkingv1.Ingress{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Ingress",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "route1",
+						Namespace: "test",
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range testTable {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+
+			m.RecoverIngress(tt.args.ingress)
+			if len(m.ingresses) != 1 {
+				t.Errorf("SecuredAccessManager.RecoverIngress() expect one route")
+			}
+		})
+	}
+}
+
+func TestSecuredAccessManagerCheckRoute(t *testing.T) {
+	type args struct {
+		routeKey string
+		route    *routev1.Route
+	}
+	testTable := []struct {
+		name                string
+		args                args
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		accessSpec          *skupperv1alpha1.SecuredAccessSpec
+		expectRoutes        int
+		expectErr           bool
+	}{
+		{
+			name: "no route",
+			args: args{
+				routeKey: "test",
+				route:    nil,
+			},
+			expectRoutes: 0,
+			expectErr:    false,
+		},
+		{
+			name: "maliformed route",
+			args: args{
+				routeKey: "test",
+				route: &routev1.Route{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "route",
+					},
+					Spec: routev1.RouteSpec{
+						Path: "",
+						Host: "1.2.3.4-8080.test.host",
+						Port: &routev1.RoutePort{
+							TargetPort: intstr.FromString("8080"),
+						},
+					},
+				},
+			},
+			expectRoutes: 1,
+			expectErr:    false,
+		},
+		{
+			name: "no ServiceAccess found",
+			args: args{
+				routeKey: "test/1.2.3.4-8080",
+				route: &routev1.Route{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "route",
+					},
+					Spec: routev1.RouteSpec{
+						Port: &routev1.RoutePort{
+							TargetPort: intstr.FromString("8080"),
+						},
+					},
+				},
+			},
+			expectRoutes: 1,
+			expectErr:    true,
+		},
+		{
+			name: "good route",
+			args: args{
+				routeKey: "test/1.2.3.4-8080",
+				route: &routev1.Route{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "1.2.3.4",
+						Labels: map[string]string{
+							"internal.skupper.io/secured-access": "true",
+						},
+						Annotations: map[string]string{
+							"internal.skupper.io/controlled": "true",
+						},
+						OwnerReferences: []metav1.OwnerReference{
+							{
+								Kind:       "SecuredAccess",
+								APIVersion: "skupper.io/v1alpha1",
+								Name:       "ownerRef",
+								UID:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+							},
+						},
+					},
+					Spec: routev1.RouteSpec{
+						Path: "",
+						Host: "1.2.3.4-8080.test.domain",
+						Port: &routev1.RoutePort{
+							TargetPort: intstr.FromString("8080"),
+						},
+						To: routev1.RouteTargetReference{
+							Kind: "Service",
+							Name: "4.4.4.4",
+						},
+						TLS: &routev1.TLSConfig{
+							Termination:                   routev1.TLSTerminationPassthrough,
+							InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyNone,
+						},
+					},
+				},
+			},
+			accessSpec: &skupperv1alpha1.SecuredAccessSpec{
+				AccessType: ACCESS_TYPE_ROUTE,
+				Ports: []skupperv1alpha1.SecuredAccessPort{
+					{
+						Name:       "8080",
+						Port:       8080,
+						TargetPort: 8081,
+						Protocol:   "TCP",
+					},
+				},
+				Certificate: "skupper",
+				Issuer:      "skupper-site-ca",
+				Options: map[string]string{
+					"domain": "domain",
+				},
+			},
+			expectRoutes: 2,
+			expectErr:    false,
+		},
+	}
+
+	for _, tt := range testTable {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+
+			if tt.accessSpec != nil {
+				// setup so can test update route
+				if err := m.Ensure("test", "1.2.3.4", *tt.accessSpec, nil, nil); err != nil {
+					t.Errorf("SecuredAccessManager.Ensure() error = %v", err)
+				}
+				// add route
+				if err, _ := m.ensureRoute("test", tt.args.route); err != nil {
+					t.Errorf("SecuredAccessManager.ensureRoute() error = %v", err)
+				}
+				// change parameter in route
+				tt.args.route.Spec.To.Name = "4.4.4.5"
+				if err, _ := m.ensureRoute("test", tt.args.route); err != nil {
+					t.Errorf("SecuredAccessManager.ensureRoute() error = %v", err)
+				}
+			}
+			if err := m.CheckRoute(tt.args.routeKey, tt.args.route); err != nil && !tt.expectErr {
+				t.Errorf("SecuredAccessManager.CheckRoute() error = %v", err)
+			}
+
+			numRoutes := len(m.routes)
+			if numRoutes != tt.expectRoutes {
+				t.Errorf("SecuredAccessManager.CheckRoute() incorrect number of routes installed expected %d found %d",
+					tt.expectRoutes, numRoutes)
+			}
+		})
+	}
+}
+
+func TestSecuredAccessTypes(t *testing.T) {
+	var test struct {
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}
+
+	m, err := newSecureAccessManagerMocks("test", test.k8sObjects, test.skupperObjects, test.skupperErrorMessage)
+	assert.Assert(t, err)
+
+	accessType := DefaultAccessType(m.clients)
+	if accessType != ACCESS_TYPE_ROUTE {
+		t.Errorf("SecuredAccessType.DefaultAccessType() error")
+	}
+
+	envAccessType := GetAccessTypeFromEnv()
+	if envAccessType != "" {
+		t.Errorf("SecuredAccessType.GetAccessTypeFromEnv() error")
+	}
+
+	accessType = getAccessType("", m.clients)
+	if accessType != ACCESS_TYPE_ROUTE {
+		t.Errorf("SecuredAccessType.getAccessType() error")
+	}
+
+	accessType = getAccessType(ACCESS_TYPE_LOADBALANCER, m.clients)
+	if accessType != ACCESS_TYPE_LOADBALANCER {
+		t.Errorf("SecuredAccessType.getAccessType() error")
+	}
+
+	serviceType := serviceType(ACCESS_TYPE_NODEPORT)
+	if serviceType != corev1.ServiceTypeNodePort {
+		t.Errorf("SecuredAccessType.serviceType() error")
+	}
+
+}
+
+// --- helper methods
+func newSecureAccessManagerMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string) (*SecuredAccessManager, error) {
+
+	client, err := fakeclient.NewFakeClient(namespace, k8sObjects, skupperObjects, fakeSkupperError)
+	if err != nil {
+		return nil, err
+	}
+
+	securedAccessManager := &SecuredAccessManager{
+		clients:     client,
+		definitions: make(map[string]*skupperv1alpha1.SecuredAccess),
+		services:    make(map[string]*corev1.Service),
+		routes:      make(map[string]*routev1.Route),
+		ingresses:   make(map[string]*networkingv1.Ingress),
+		//httpProxies: make(map[string]*unstructured.Unstructured),
+		//certMgr:     certificates.CertificateManager,
+	}
+
+	return securedAccessManager, nil
+}
diff --git a/pkg/kube/securedaccess/loadbalancer_test.go b/pkg/kube/securedaccess/loadbalancer_test.go
new file mode 100644
index 00000000..0130f3ed
--- /dev/null
+++ b/pkg/kube/securedaccess/loadbalancer_test.go
@@ -0,0 +1,112 @@
+package securedaccess
+
+import (
+	"testing"
+
+	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"gotest.tools/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestLoadbalancerAccessTypeResolve(t *testing.T) {
+	type args struct {
+		access *skupperv1alpha1.SecuredAccess
+	}
+	tests := []struct {
+		name                string
+		args                args
+		svc                 *corev1.Service
+		want                bool
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no service",
+			args: args{
+				access: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Service",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "skupper",
+						Namespace: "test",
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "no endpoints",
+			args: args{
+				access: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Service",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "skupper",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.SecuredAccessSpec{
+						AccessType: ACCESS_TYPE_LOADBALANCER,
+						Selector: map[string]string{
+							"skupper.io/component": "router",
+						},
+						Ports: []skupperv1alpha1.SecuredAccessPort{
+							{
+								Name:       "8080",
+								Port:       8080,
+								TargetPort: 8081,
+								Protocol:   "TCP",
+							},
+						},
+					},
+				},
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newLoadBalanceSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+			o := &LoadbalancerAccessType{manager: m}
+
+			if tt.name != "no service" {
+				if err = m.createService(tt.args.access); err != nil {
+					t.Errorf("LoadbalancerAccessType.Resolve() failed to create service %s", err)
+				}
+			}
+			if got := o.Resolve(tt.args.access); got != tt.want {
+				t.Errorf("LoadbalancerAccessType.Resolve() = %v, want %v", got, tt.want)
+			}
+
+			if result := o.Realise(tt.args.access); result != true ||
+				tt.args.access.Status.StatusMessage != skupperv1alpha1.STATUS_OK {
+				t.Errorf("LoadbalancerAccessType.Realise(), error setting status")
+			}
+		})
+	}
+}
+
+// --- helper methods
+func newLoadBalanceSecureAccessManagerMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string) (*SecuredAccessManager, error) {
+
+	client, err := fakeclient.NewFakeClient(namespace, k8sObjects, skupperObjects, fakeSkupperError)
+	if err != nil {
+		return nil, err
+	}
+
+	securedAccessManager := &SecuredAccessManager{
+		clients:  client,
+		services: make(map[string]*corev1.Service),
+	}
+
+	return securedAccessManager, nil
+}
diff --git a/pkg/kube/securedaccess/route_test.go b/pkg/kube/securedaccess/route_test.go
new file mode 100644
index 00000000..0caa7ac1
--- /dev/null
+++ b/pkg/kube/securedaccess/route_test.go
@@ -0,0 +1,290 @@
+package securedaccess
+
+import (
+	"testing"
+
+	routev1 "github.com/openshift/api/route/v1"
+	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"gotest.tools/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func TestRouteAccessTypeRealise(t *testing.T) {
+	type args struct {
+		access *skupperv1alpha1.SecuredAccess
+	}
+	tests := []struct {
+		name                string
+		args                args
+		route               *routev1.Route
+		want                bool
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		numRoutes           int
+	}{
+		{
+			name: "existing route",
+			args: args{
+				access: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "1.2.3.4",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.SecuredAccessSpec{
+						AccessType: ACCESS_TYPE_ROUTE,
+						Selector: map[string]string{
+							"skupper.io/component": "router",
+						},
+						Ports: []skupperv1alpha1.SecuredAccessPort{
+							{
+								Name:       "8080",
+								Port:       8080,
+								TargetPort: 8081,
+								Protocol:   "TCP",
+							},
+						},
+					},
+				},
+			},
+			route: &routev1.Route{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "v1",
+					Kind:       "Route",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "1.2.3.4-8080",
+				},
+				Spec: routev1.RouteSpec{
+					Port: &routev1.RoutePort{
+						TargetPort: intstr.FromString("8080"),
+					},
+				},
+			},
+			numRoutes: 1,
+			want:      false,
+		},
+		{
+			name: "new route",
+			args: args{
+				access: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "1.2.3.4",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.SecuredAccessSpec{
+						AccessType: ACCESS_TYPE_ROUTE,
+						Selector: map[string]string{
+							"skupper.io/component": "router",
+						},
+						Ports: []skupperv1alpha1.SecuredAccessPort{
+							{
+								Name:       "8080",
+								Port:       8080,
+								TargetPort: 8081,
+								Protocol:   "TCP",
+							},
+						},
+					},
+				},
+			},
+			route: &routev1.Route{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "v1",
+					Kind:       "Route",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "1.2.3.4-9999",
+				},
+				Spec: routev1.RouteSpec{
+					Port: &routev1.RoutePort{
+						TargetPort: intstr.FromString("9999"),
+					},
+				},
+			},
+			want:      false,
+			numRoutes: 2,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newRouteSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+			o := &RouteAccessType{manager: m}
+
+			// add route
+			if err, _ := o.manager.ensureRoute("test", tt.route); err != nil {
+				t.Errorf("SecuredAccessManager.ensureRoute() error = %v", err)
+			}
+			if got := o.Realise(tt.args.access); got != tt.want {
+				t.Errorf("RouteAccessType.Realise() = %v, want %v", got, tt.want)
+			}
+			numRoutes := len(m.routes)
+			if numRoutes != tt.numRoutes {
+				t.Errorf("RouteAccessType incorrect number of routes installed expected %d found %d",
+					tt.numRoutes, numRoutes)
+			}
+		})
+	}
+}
+
+func TestRouteAccessTypeResolve(t *testing.T) {
+	type args struct {
+		access *skupperv1alpha1.SecuredAccess
+	}
+	tests := []struct {
+		name                string
+		args                args
+		route               *routev1.Route
+		want                bool
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		numRoutes           int
+	}{
+		{
+			name: "no route",
+			args: args{
+				access: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "1.2.3.4",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.SecuredAccessSpec{
+						AccessType: ACCESS_TYPE_LOADBALANCER,
+						Selector: map[string]string{
+							"skupper.io/component": "router",
+						},
+						Ports: []skupperv1alpha1.SecuredAccessPort{
+							{
+								Name:       "8080",
+								Port:       8080,
+								TargetPort: 8081,
+								Protocol:   "TCP",
+							},
+						},
+						Certificate: "skupper",
+						Issuer:      "skupper-site-ca",
+					},
+				},
+			},
+			numRoutes: 0,
+			want:      false,
+		},
+		{
+			name: "one route",
+			args: args{
+				access: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Route",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "1.2.3.4",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.SecuredAccessSpec{
+						AccessType: ACCESS_TYPE_LOADBALANCER,
+						Selector: map[string]string{
+							"skupper.io/component": "router",
+						},
+						Ports: []skupperv1alpha1.SecuredAccessPort{
+							{
+								Name:       "8080",
+								Port:       8080,
+								TargetPort: 8081,
+								Protocol:   "TCP",
+							},
+						},
+						Certificate: "skupper",
+						Issuer:      "skupper-site-ca",
+					},
+				},
+			},
+			route: &routev1.Route{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "v1",
+					Kind:       "Route",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "1.2.3.4-8080",
+				},
+				Spec: routev1.RouteSpec{
+					Host: "1.2.3.4-8080.test.host",
+					Port: &routev1.RoutePort{
+						TargetPort: intstr.FromString("9999"),
+					},
+				},
+			},
+			want:      true,
+			numRoutes: 1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m, err := newRouteSecureAccessManagerMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage)
+			assert.Assert(t, err)
+			o := &RouteAccessType{manager: m}
+
+			if tt.want == true {
+				// add route
+				if err, _ := o.manager.ensureRoute("test", tt.route); err != nil {
+					t.Errorf("SecuredAccessManager.ensureRoute() error = %v", err)
+				}
+			}
+			if got := o.Resolve(tt.args.access); got != tt.want {
+				t.Errorf("RouteAccessType.Realise() = %v, want %v", got, tt.want)
+			}
+			numRoutes := len(m.routes)
+			if numRoutes != tt.numRoutes {
+				t.Errorf("RouteAccessType.Realise() incorrect number of routes installed expected %d found %d",
+					tt.numRoutes, numRoutes)
+			}
+			// check endpoints
+			if tt.want == true {
+				if len(tt.args.access.Status.Endpoints) != 1 &&
+					tt.args.access.Status.Endpoints[0].Host != "1.2.3.4-8080.test.host" {
+					t.Errorf("RouteAccessType.Realise() failure initializing endpoints")
+				}
+			} else {
+				if len(tt.args.access.Status.Endpoints) != 0 {
+					t.Errorf("RouteAccessType.Realise() unexpected endpoints")
+				}
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newRouteSecureAccessManagerMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string) (*SecuredAccessManager, error) {
+
+	client, err := fakeclient.NewFakeClient(namespace, k8sObjects, skupperObjects, fakeSkupperError)
+	if err != nil {
+		return nil, err
+	}
+
+	securedAccessManager := &SecuredAccessManager{
+		clients: client,
+		routes:  make(map[string]*routev1.Route),
+	}
+
+	return securedAccessManager, nil
+}
diff --git a/pkg/kube/site/site_test.go b/pkg/kube/site/site_test.go
new file mode 100644
index 00000000..72ceab3d
--- /dev/null
+++ b/pkg/kube/site/site_test.go
@@ -0,0 +1,1076 @@
+package site
+
+import (
+	"context"
+	"testing"
+
+	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
+	"github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	skupperv1alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v1alpha1"
+	"github.com/skupperproject/skupper/pkg/kube"
+	"github.com/skupperproject/skupper/pkg/kube/certificates"
+	"github.com/skupperproject/skupper/pkg/kube/securedaccess"
+	"github.com/skupperproject/skupper/pkg/qdr"
+	site1 "github.com/skupperproject/skupper/pkg/site"
+	"github.com/skupperproject/skupper/pkg/version"
+	"gotest.tools/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestSite_Recover(t *testing.T) {
+	type args struct {
+		site *skupperv1alpha1.Site
+	}
+	tests := []struct {
+		name                string
+		args                args
+		wantErr             bool
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "site inactive",
+			args: args{
+				site: &skupperv1alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "siteInactive",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+				},
+			},
+			skupperErrorMessage: "NotFound",
+			wantErr:             true,
+		},
+		{
+			name: "site fail CA",
+			args: args{
+				site: &skupperv1alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "site1",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+					Spec: skupperv1alpha1.SiteSpec{
+						DefaultIssuer: "skupper-spec-issuer-ca",
+						LinkAccess:    "loadbalancer",
+					},
+					Status: skupperv1alpha1.SiteStatus{
+						DefaultIssuer: "skupper-status-issuer-ca",
+					},
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.RouterAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "Router1",
+						Namespace: "test",
+					},
+				},
+			},
+			skupperErrorMessage: "NotFound",
+			wantErr:             true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			if err := s.Recover(tt.args.site); (err != nil) != tt.wantErr {
+				t.Errorf("Site.Reconcile() error = %v", err)
+			}
+		})
+	}
+}
+
+func TestSite_checkDefaultRouterAccess(t *testing.T) {
+	type args struct {
+		site *skupperv1alpha1.Site
+	}
+	tests := []struct {
+		name                string
+		args                args
+		rtr                 *skupperv1alpha1.RouterAccess
+		wantErr             bool
+		wantLinkAccess      int
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no link access config",
+			args: args{
+				site: &skupperv1alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "site1",
+						Namespace: "test",
+					},
+				},
+			},
+			wantErr:        false,
+			wantLinkAccess: 0,
+		},
+		{
+			name: "default router config",
+			args: args{
+				site: &skupperv1alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "site1",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.SiteSpec{
+						LinkAccess: "loadbalancer",
+					},
+				},
+			},
+			wantErr:        false,
+			wantLinkAccess: 1,
+		},
+		{
+			name: "default router config exists",
+			args: args{
+				site: &skupperv1alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "site1",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.SiteSpec{
+						LinkAccess: "loadbalancer",
+					},
+				},
+			},
+			rtr: &skupperv1alpha1.RouterAccess{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "skupper.io/v1alpha1",
+					Kind:       "RouterAccess",
+				},
+				ObjectMeta: v1.ObjectMeta{
+					Name:      "skupper-router",
+					Namespace: "test",
+				},
+				Spec: skupperv1alpha1.RouterAccessSpec{
+					AccessType: "nodeport",
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.RouterAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "skupper-router",
+						Namespace: "test",
+					},
+				},
+			},
+			wantErr:        false,
+			wantLinkAccess: 1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			if tt.rtr != nil {
+				// test case when there is already a router linkAccess defined
+				s.linkAccess["skupper-router"] = tt.rtr
+			}
+
+			if err = s.checkDefaultRouterAccess(context.TODO(), tt.args.site); (err != nil) != tt.wantErr {
+				t.Errorf("Site.checkDefaultRouterAccess() error = %v, wantErr %v", err, tt.wantErr)
+			}
+
+			numLinkAccess := len(s.linkAccess)
+			if tt.wantLinkAccess != numLinkAccess {
+				t.Errorf("Site.checkDefaultRouterAccess() expected link access not found expected %d, found %d", tt.wantLinkAccess, numLinkAccess)
+			} else if tt.wantLinkAccess != 0 {
+				if s.linkAccess["skupper-router"].Spec.Issuer != "skupper-site-ca" {
+					t.Errorf("Site.checkDefaultRouterAccess() incorrect default values found")
+				}
+			}
+		})
+	}
+}
+
+func TestSite_ExposeUnexpose(t *testing.T) {
+	type args struct {
+		exposed  *ExposedPortSet
+		exposed2 *ExposedPortSet
+	}
+	tests := []struct {
+		name                string
+		args                args
+		wantErr             bool
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no existing exposed ports",
+			args: args{
+				exposed: &ExposedPortSet{
+					Host: "backend",
+					Ports: map[string]Port{
+						"port1": {
+							Name:       "port1",
+							Port:       1234,
+							TargetPort: 7890,
+							Protocol:   "TCP",
+						},
+					},
+				},
+				exposed2: &ExposedPortSet{
+					Host: "backend",
+					Ports: map[string]Port{
+						"port1": {
+							Name:       "port1",
+							Port:       2222,
+							TargetPort: 7890,
+							Protocol:   "TCP",
+						},
+					},
+				},
+			},
+			k8sObjects: []runtime.Object{
+				&corev1.Service{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "v1",
+						Kind:       "Service",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "backend",
+					},
+					Status: corev1.ServiceStatus{
+						Conditions: []v1.Condition{
+							{
+								Type:   "Configured",
+								Status: "True",
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			s.Expose(tt.args.exposed)
+
+			// modify ports exposed
+			s.Expose(tt.args.exposed2)
+
+			s.Unexpose(tt.args.exposed.Host)
+
+			//TBD errors are not returned and nothing stored in Site
+			//if (err != nil) != tt.wantErr {
+			//	t.Errorf("Site.ExposeUnexpose() error = %v, wantErr %v", err, tt.wantErr)
+			//}
+		})
+	}
+}
+func TestSite_CheckListener(t *testing.T) {
+	type args struct {
+		name     string
+		listener *skupperv1alpha1.Listener
+	}
+	tests := []struct {
+		name                string
+		args                args
+		wantErr             bool
+		want                string
+		wantListeners       uint
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no site",
+			want: "not initialized",
+			// code just silently ignores this failure
+			wantErr:       false,
+			wantListeners: 0,
+		},
+		{
+			name: "one listener added",
+			args: args{
+				name: "listener1",
+				listener: &skupperv1alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "listener1",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+					Spec: skupperv1alpha1.ListenerSpec{
+						RoutingKey: "backend",
+						Port:       8080,
+						Type:       "tcp",
+						Host:       "1.2.3.4",
+					},
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "listener1",
+						Namespace: "test",
+					},
+				},
+			},
+			want:          "initialized",
+			wantErr:       false,
+			wantListeners: 1,
+		},
+		/* TBD updateListenerStatus if kube command err == nil it just
+		   returns and doesn't update s.bindings.UpdateListener ???
+			{
+				name: "listener error",
+				args: args{
+					name: "listener1",
+					listener: &skupperv1alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+							UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+						},
+						Spec: skupperv1alpha1.ListenerSpec{
+							RoutingKey: "backend",
+							Port:       8080,
+							Type:       "tcp",
+							Host:       "1.2.3.4",
+						},
+					},
+				},
+				want:                "initialized",
+				wantErr:             false,
+				wantListeners:       1,
+				skupperErrorMessage: "NotFound",
+			},
+		*/
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			if tt.want == "initialized" {
+				s.initialised = true
+				err = createRouterConfigMock(s)
+				assert.Assert(t, err)
+			}
+
+			if err := s.CheckListener(tt.args.name, tt.args.listener); (err != nil) != tt.wantErr {
+				t.Errorf("Site.CheckListener() error = %v", err)
+			}
+
+			// check if listener is expected and has correct values
+			listener := s.bindings.bindings.GetListener(tt.args.name)
+			if tt.wantListeners != 0 {
+				listenerConfigured := false
+				if listener == nil {
+					t.Errorf("Site.CheckListener() expected listener doesn't exist")
+				} else {
+					if listener.Spec.Port != tt.args.listener.Spec.Port {
+						t.Errorf("Site.CheckListener() expected listener doesn't have correct values")
+					}
+					for _, condition := range listener.Status.Conditions {
+						if condition.Type == "Configured" && condition.Reason == "OK" {
+							listenerConfigured = true
+						}
+					}
+					if listenerConfigured == false {
+						t.Errorf("Site.CheckListener() link not in expected configured state")
+					}
+				}
+			} else if listener != nil {
+				t.Errorf("Site.CheckListener() unexpected listener exists")
+			}
+		})
+	}
+}
+
+func TestSite_CheckConnector(t *testing.T) {
+	type args struct {
+		name      string
+		connector *skupperv1alpha1.Connector
+	}
+	tests := []struct {
+		name                string
+		args                args
+		wantErr             bool
+		want                string
+		wantConnectors      uint
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no connector",
+			// code just silently ignores this failure
+			wantErr:        false,
+			wantConnectors: 0,
+		},
+		{
+			name: "one connector added",
+			args: args{
+				name: "connector1",
+				connector: &skupperv1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "connector1",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+					Spec: skupperv1alpha1.ConnectorSpec{
+						RoutingKey: "backend",
+						Port:       8080,
+						Type:       "tcp",
+						Host:       "1.2.3.4",
+					},
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "connector1",
+						Namespace: "test",
+					},
+				},
+			},
+			want:           "initialized",
+			wantErr:        false,
+			wantConnectors: 1,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			if tt.want == "initialized" {
+				s.initialised = true
+				err = createRouterConfigMock(s)
+				assert.Assert(t, err)
+			}
+
+			if err := s.CheckConnector(tt.args.name, tt.args.connector); (err != nil) != tt.wantErr {
+				t.Errorf("Site.Checkconnector() error = %v", err)
+			}
+
+			// check if connector is expected and has correct values
+			connector := s.bindings.bindings.GetConnector(tt.args.name)
+			if tt.wantConnectors != 0 {
+				connectorConfigured := false
+				if connector == nil {
+					t.Errorf("Site.Checkconnector() expected connector doesn't exist")
+				} else {
+					if connector.Spec.Port != tt.args.connector.Spec.Port {
+						t.Errorf("Site.Checkconnector() expected connector doesn't have correct values")
+					}
+					for _, condition := range connector.Status.Conditions {
+						if condition.Type == "Configured" && condition.Reason == "OK" {
+							connectorConfigured = true
+						}
+					}
+					if connectorConfigured == false {
+						t.Errorf("Site.CheckConnector() link not in expected configured state")
+					}
+				}
+			} else if connector != nil {
+				t.Errorf("Site.Checkconnector() unexpected connector exists")
+			}
+		})
+	}
+}
+
+func TestSite_CheckLink(t *testing.T) {
+	type args struct {
+		name       string
+		linkconfig *skupperv1alpha1.Link
+	}
+	tests := []struct {
+		name                string
+		args                args
+		wantErr             bool
+		want                string
+		wantLinks           int
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no link",
+			args: args{
+				name: "link1",
+			},
+			wantErr:   false,
+			wantLinks: 0,
+		},
+		{
+			name: "link - site not initialized",
+			args: args{
+				name: "link1",
+				linkconfig: &skupperv1alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link1",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+					Spec: skupperv1alpha1.LinkSpec{
+						Cost: 1,
+						Endpoints: []skupperv1alpha1.Endpoint{
+							{
+								Name: string(qdr.RoleInterRouter),
+								Host: "10.10.10.1",
+								Port: "55671",
+							},
+						},
+					},
+				},
+			},
+			wantErr:   false,
+			wantLinks: 1,
+		},
+		{
+			name: "link - not found",
+			args: args{
+				name: "link1",
+				linkconfig: &skupperv1alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link1",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+					Spec: skupperv1alpha1.LinkSpec{
+						Cost: 1,
+						Endpoints: []skupperv1alpha1.Endpoint{
+							{
+								Name: string(qdr.RoleInterRouter),
+								Host: "10.10.10.1",
+								Port: "55671",
+							},
+						},
+					},
+				},
+			},
+			want:                "initialized",
+			wantErr:             true,
+			wantLinks:           1,
+			skupperErrorMessage: "NotFound",
+		},
+		{
+			name: "link - ok",
+			args: args{
+				name: "link1",
+				linkconfig: &skupperv1alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link1",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+					Spec: skupperv1alpha1.LinkSpec{
+						Cost: 2,
+						Endpoints: []skupperv1alpha1.Endpoint{
+							{
+								Name: string(qdr.RoleInterRouter),
+								Host: "1.1.1.1",
+								Port: "55671",
+							},
+						},
+					},
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link1",
+						Namespace: "test",
+					},
+				},
+			},
+			want:      "initialized",
+			wantErr:   false,
+			wantLinks: 1,
+		},
+		{
+			name: "link - error",
+			args: args{
+				name: "link1",
+				linkconfig: &skupperv1alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link1",
+						Namespace: "test",
+						UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+					},
+					Spec: skupperv1alpha1.LinkSpec{
+						Cost: 2,
+						Endpoints: []skupperv1alpha1.Endpoint{
+							{
+								Name: string(qdr.RoleInterRouter),
+								Host: "1.1.1.1",
+								Port: "55671",
+							},
+						},
+					},
+				},
+			},
+			want:                "initialized",
+			wantErr:             true,
+			wantLinks:           1,
+			skupperErrorMessage: "NotFound",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			if tt.want == "initialized" {
+				s.initialised = true
+				err = createRouterConfigMock(s)
+				assert.Assert(t, err)
+			}
+
+			if err := s.CheckLink(tt.args.name, tt.args.linkconfig); (err != nil) != tt.wantErr {
+				t.Errorf("Site.CheckLink() error = %v, wantErr %v", err, tt.skupperErrorMessage)
+			}
+
+			if tt.want == "initialized" {
+				linkConfigured := false
+				if len(s.links) != tt.wantLinks {
+					t.Errorf("Site.CheckLink() link not added")
+				}
+				link := s.links[tt.args.name].Definition()
+				if link.Spec.Cost != tt.args.linkconfig.Spec.Cost {
+					t.Errorf("Site.CheckLink() link not configured correctly")
+				}
+
+				for _, condition := range link.Status.Conditions {
+					if condition.Type == "Configured" && condition.Reason == "OK" {
+						linkConfigured = true
+					}
+				}
+				if linkConfigured == false {
+					t.Errorf("Site.CheckLink() link not in configured state")
+				}
+				// test unlinking
+				if err := s.unlink(tt.args.name); err != nil {
+					t.Errorf("Site.unlink() link remove failure")
+				}
+				if len(s.links) != 0 {
+					t.Errorf("Site.CheckLink() link not removed")
+				}
+			} else {
+				// router not initialized
+				numLinks := len(s.links)
+				if numLinks != tt.wantLinks {
+					t.Errorf("Site.CheckLink() incorrect links expected: want %d found %d", numLinks, tt.wantLinks)
+				}
+				// expect link status not configured
+				if numLinks != 0 {
+					link := s.links[tt.args.name].Definition()
+					for _, condition := range link.Status.Conditions {
+						if condition.Type == "Configured" && condition.Reason == "OK" {
+							t.Errorf("Site.CheckLink() link in configured state but should not be")
+						}
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestSite_CheckRouterAccess(t *testing.T) {
+	type args struct {
+		name string
+		la   *skupperv1alpha1.RouterAccess
+	}
+	tests := []struct {
+		name                string
+		args                args
+		wantErr             bool
+		wantLinkAccess      int
+		want                string
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no router access config",
+			args: args{
+				name: "skupper-router",
+			},
+			// code silently ignores this failure
+			wantErr:        false,
+			wantLinkAccess: 0,
+			want:           "initialized",
+		},
+		{
+			name: "router access config",
+			args: args{
+				name: "skupper-router",
+				la: &skupperv1alpha1.RouterAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "skupper.io/v1alpha1",
+						Kind:       "RouterAccess",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "skupper-router",
+						Namespace: "test",
+					},
+					Spec: skupperv1alpha1.RouterAccessSpec{
+						AccessType: "nodeport",
+					},
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.RouterAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "skupper-router",
+						Namespace: "test",
+					},
+				},
+			},
+			wantErr:        false,
+			wantLinkAccess: 1,
+			want:           "initialized",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, true)
+			assert.Assert(t, err)
+
+			if tt.want == "initialized" {
+				s.initialised = true
+				err = createRouterConfigMock(s)
+				assert.Assert(t, err)
+			}
+
+			if err = s.CheckRouterAccess(tt.args.name, tt.args.la); (err != nil) != tt.wantErr {
+				t.Errorf("Site.CheckRouterAccess() error = %v, wantErr %v", err, tt.wantErr)
+			}
+
+			numLinkAccess := len(s.linkAccess)
+			if tt.wantLinkAccess != numLinkAccess {
+				t.Errorf("Site.CheckRouterAccess() expected link access not found expected %d, found %d", tt.wantLinkAccess, numLinkAccess)
+			} else if tt.wantLinkAccess != 0 {
+				if s.linkAccess["skupper-router"].Spec.AccessType != "nodeport" {
+					t.Errorf("Site.CheckRouterAccess() incorrect values found")
+				}
+			}
+		})
+	}
+}
+
+func Test_NetworkStatusUpdate(t *testing.T) {
+	type args struct {
+		siteRecord []skupperv1alpha1.SiteRecord
+	}
+	tests := []struct {
+		name                string
+		args                args
+		linkconfig          *skupperv1alpha1.Link
+		wantErr             bool
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no site",
+			args: args{
+				siteRecord: []skupperv1alpha1.SiteRecord{
+					{
+						Id:        "",
+						Name:      "site1",
+						Namespace: "test",
+						Platform:  "kubernetes",
+						Version:   "1.8.0",
+					},
+				},
+			},
+			wantErr:             true,
+			skupperErrorMessage: "NotFound",
+		},
+		{
+			name: "site1",
+			args: args{
+				siteRecord: []skupperv1alpha1.SiteRecord{
+					{
+						Id:        "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+						Name:      "site1",
+						Namespace: "test",
+						Platform:  "podman",
+						Version:   "1.8.0",
+						Links: []skupperv1alpha1.LinkRecord{
+							{
+								Name:           "link1",
+								RemoteSiteName: "east",
+							},
+						},
+					},
+				},
+			},
+			linkconfig: &skupperv1alpha1.Link{
+				ObjectMeta: v1.ObjectMeta{
+					Name:      "link1",
+					Namespace: "test",
+					UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+				},
+				Spec: skupperv1alpha1.LinkSpec{
+					Cost: 2,
+					Endpoints: []skupperv1alpha1.Endpoint{
+						{
+							Name: string(qdr.RoleInterRouter),
+							Host: "1.1.1.1",
+							Port: "55671",
+						},
+					},
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.Site{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "skupper.io/v1alpha1",
+						Kind:       "Site",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "site1",
+						Namespace: "test",
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			// add link
+			if tt.linkconfig != nil {
+				link := s.newLink(tt.linkconfig)
+				s.links[tt.linkconfig.ObjectMeta.Name] = link
+			}
+
+			if err := s.NetworkStatusUpdated(tt.args.siteRecord); (err != nil) != tt.wantErr {
+				t.Errorf("Site.NetworkStatusUpdated() error = %v, wantErr %v", err, tt.wantErr)
+			}
+
+			if tt.wantErr == false {
+				// verify network was updated
+				foundConfig := false
+				for _, network := range s.site.Status.Network {
+					if network.Platform == "podman" && network.Version == "1.8.0" {
+						foundConfig = true
+					}
+				}
+				if foundConfig == false {
+					t.Errorf("Site.NetworkStatusUpdated() network not updated")
+				}
+			}
+		})
+	}
+}
+
+func Test_CheckSecuredAccess(t *testing.T) {
+	type args struct {
+		sa *skupperv1alpha1.SecuredAccess
+	}
+	tests := []struct {
+		name                string
+		args                args
+		rtr                 *skupperv1alpha1.RouterAccess
+		wantErr             bool
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+	}{
+		{
+			name: "no linkAccess name",
+			args: args{
+				sa: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "skupper.io/v1alpha1",
+						Kind:       "SecuredAccess",
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "no existing linkAccess",
+			args: args{
+				sa: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "skupper.io/v1alpha1",
+						Kind:       "SecuredAccess",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "skupper-router",
+						Namespace: "test",
+						Annotations: map[string]string{
+							"internal.skupper.io/controlled":   "true",
+							"internal.skupper.io/routeraccess": "skupper-router",
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "linkAccess modified",
+			args: args{
+				sa: &skupperv1alpha1.SecuredAccess{
+					TypeMeta: metav1.TypeMeta{
+						APIVersion: "skupper.io/v1alpha1",
+						Kind:       "SecuredAccess",
+					},
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "skupper-router",
+						Namespace: "test",
+						Annotations: map[string]string{
+							"internal.skupper.io/controlled":   "true",
+							"internal.skupper.io/routeraccess": "skupper-router",
+						},
+					},
+					Spec: v1alpha1.SecuredAccessSpec{
+						AccessType: "loadbalancer",
+					},
+					Status: v1alpha1.SecuredAccessStatus{
+						Endpoints: []skupperv1alpha1.Endpoint{
+							{
+								Name: string(qdr.RoleInterRouter),
+								Host: "10.10.10.1",
+								Port: "55671",
+							},
+						},
+					},
+				},
+			},
+			rtr: &skupperv1alpha1.RouterAccess{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "skupper.io/v1alpha1",
+					Kind:       "RouterAccess",
+				},
+				ObjectMeta: v1.ObjectMeta{
+					Name:      "skupper-router",
+					Namespace: "test",
+				},
+				Spec: skupperv1alpha1.RouterAccessSpec{
+					AccessType: "nodeport",
+				},
+			},
+			skupperObjects: []runtime.Object{
+				&skupperv1alpha1.RouterAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "skupper-router",
+						Namespace: "test",
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
+			assert.Assert(t, err)
+
+			if tt.rtr != nil {
+				// test case when there is already a router linkAccess defined
+				s.linkAccess["skupper-router"] = tt.rtr
+			}
+			s.CheckSecuredAccess(tt.args.sa)
+
+			if tt.wantErr == true {
+				// verify no link access
+				if len(s.linkAccess) != 0 {
+					t.Errorf("Site.CheckSecuredAccess() unexpected linkAccess found")
+				}
+			} else {
+				// expected updated linkAccess
+				if len(s.linkAccess) != 1 {
+					t.Errorf("Site.CheckSecuredAccess() linkAccess not found")
+				}
+				if s.linkAccess["skupper-router"].Spec.AccessType == "loadbalancer" {
+					t.Errorf("Site.CheckSecuredAccess() linkAccess not updated")
+				}
+				// TBD adds to code coverge but doesn't return error if failed
+				if err = s.checkSecuredAccess(); err != nil {
+					t.Errorf("Site.CheckSecuredAccess() linkAccess not updated")
+				}
+			}
+		})
+	}
+}
+
+// --- helper
+
+func newSiteMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string, accessMgr bool) (*Site, error) {
+
+	site := &skupperv1alpha1.Site{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "site1",
+			Namespace: "test",
+			UID:       "8a96ffdf-403b-4e4a-83a8-97d3d459adb6",
+		},
+		Spec: skupperv1alpha1.SiteSpec{
+			DefaultIssuer: "skupper-spec-issuer-ca",
+		},
+		Status: skupperv1alpha1.SiteStatus{
+			DefaultIssuer: "skupper-status-issuer-ca",
+		},
+	}
+
+	client, err := fakeclient.NewFakeClient(namespace, k8sObjects, skupperObjects, fakeSkupperError)
+	if err != nil {
+		return nil, err
+	}
+
+	controller := kube.NewController("test", client)
+	newSite := &Site{
+		controller: controller,
+		bindings:   NewExtendedBindings(controller),
+		links:      make(map[string]*site1.Link),
+		errors:     make(map[string]string),
+		linkAccess: make(map[string]*skupperv1alpha1.RouterAccess),
+		certs:      certificates.NewCertificateManager(controller),
+		access:     securedaccess.NewSecuredAccessManager(client, nil, "loadbalancer"),
+		adaptor:    BindingAdaptor{},
+		routerPods: make(map[string]*corev1.Pod),
+	}
+
+	newSite.site = site
+	newSite.name = site.ObjectMeta.Name
+	newSite.namespace = site.ObjectMeta.Namespace
+
+	return newSite, nil
+}
+
+func createRouterConfigMock(s *Site) error {
+	rc := qdr.InitialConfig(s.name+"-${HOSTNAME}", s.site.GetSiteId(), version.Version, s.isEdge(), 3)
+	rc.AddAddress(qdr.Address{
+		Prefix:       "mc",
+		Distribution: "multicast",
+	})
+	rc.SetNormalListeners(SSL_PROFILE_PATH)
+
+	err := s.createRouterConfig(&rc)
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/pkg/qdr/qdr_test.go b/pkg/qdr/qdr_test.go
index 026b0c5e..d790501b 100644
--- a/pkg/qdr/qdr_test.go
+++ b/pkg/qdr/qdr_test.go
@@ -7,6 +7,8 @@ import (
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/utils"
 	"gotest.tools/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestInitialConfig(t *testing.T) {
@@ -44,7 +46,36 @@ func TestInitialConfig(t *testing.T) {
 	}
 }
 
-func TestAddListener(t *testing.T) {
+func TestInitialConfigSkupperRouter(t *testing.T) {
+	routerOptions := types.RouterOptions{
+		MaxFrameSize:     1518,
+		LoadBalancerIp:   "1.2.3.4",
+		DisableMutualTLS: true,
+		Logging: []types.RouterLogConfig{
+			{
+				Module: "PROTOCOL",
+				Level:  "trace",
+			},
+		},
+	}
+	path := "/tmp/skupper-router"
+	routerConfig := InitialConfigSkupperRouter("foo", "bar", "1.2.3", false, 10, routerOptions, path)
+
+	if routerConfig.Metadata.Id != "foo" {
+		t.Errorf("Invalid id, expected 'foo' got %q", routerConfig.Metadata.Id)
+	}
+	if len(routerConfig.LogConfig) != 1 {
+		t.Errorf("Expect one log entry")
+	}
+	if routerConfig.Addresses["mc"].Distribution != "multicast" {
+		t.Errorf("Expect address to configured as multicast")
+	}
+	if routerConfig.IsEdge() {
+		t.Errorf("Expected interior mode")
+	}
+}
+
+func TestAddRemoveListener(t *testing.T) {
 	config := InitialConfig("foo", "bar", "undefined", true, 3)
 	config.AddListener(Listener{
 		Name: "l1",
@@ -63,6 +94,72 @@ func TestAddListener(t *testing.T) {
 	if config.Listeners["127.0.0.1@8888"].Name != "127.0.0.1@8888" {
 		t.Errorf("Expected name '127.0.0.1@8888' but got %q", config.Listeners["127.0.0.1@8888"].Name)
 	}
+	result := config.AddListener(Listener{
+		Name: "l1",
+		Port: 5672,
+	})
+	if result == true {
+		t.Errorf("Expect add to fail, duplicate listener")
+	}
+	if len(config.Listeners) != 2 {
+		t.Errorf("Expected two listeners but found %d", len(config.Listeners))
+	}
+	result, _ = config.RemoveListener("l1")
+	if result == false {
+		t.Errorf("Expected to remove existing listener")
+	}
+	result, _ = config.RemoveListener("bad")
+	if result == true {
+		t.Errorf("Expected to not find listener")
+	}
+	if len(config.Listeners) != 1 {
+		t.Errorf("Expected 1 listener but found %d", len(config.Listeners))
+	}
+}
+
+func TestAddRemoveConnector(t *testing.T) {
+	config := InitialConfig("foo", "bar", "undefined", true, 3)
+	config.AddConnector(Connector{
+		Name: "c1",
+		Role: RoleInterRouter,
+		Port: "5672",
+		Cost: 1,
+	})
+	if config.Connectors["c1"].Cost != 1 {
+		t.Errorf("Expected cost 1 but got %d", config.Connectors["c1"].Cost)
+	}
+	config.AddConnector(Connector{
+		Name: "c2",
+		Host: "127.0.0.1",
+		Port: "8888",
+		Role: RoleEdge,
+	})
+	if config.Connectors["c2"].Port != "8888" {
+		t.Errorf("Expected port 8888 but got %s", config.Connectors["c2"].Port)
+	}
+	result := config.AddConnector(Connector{
+		Name: "c1",
+		Role: RoleInterRouter,
+		Port: "5672",
+		Cost: 1,
+	})
+	if result == true {
+		t.Errorf("Expect add to fail, duplicate Connector")
+	}
+	if len(config.Connectors) != 2 {
+		t.Errorf("Expected two Connectors but found %d", len(config.Connectors))
+	}
+	result, _ = config.RemoveConnector("c1")
+	if result == false {
+		t.Errorf("Expected to remove existing Connector")
+	}
+	result, _ = config.RemoveConnector("bad")
+	if result == true {
+		t.Errorf("Expected to not find Connector")
+	}
+	if len(config.Connectors) != 1 {
+		t.Errorf("Expected 1 Connector but found %d", len(config.Connectors))
+	}
 }
 
 func TestAddSslProfile(t *testing.T) {
@@ -420,6 +517,31 @@ func TestMarshalUnmarshalRouterConfigWithLogging(t *testing.T) {
 	checkLevel(t, &input, "DEFAULT", "debug+")
 }
 
+func TestUpdateConfigMap(t *testing.T) {
+	routerConfig := RouterConfig{}
+	data, _ := routerConfig.AsConfigMapData()
+
+	siteConfig := &corev1.ConfigMap{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "ConfigMap",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: types.SiteConfigMapName,
+		},
+		Data: data,
+	}
+
+	updated, err := routerConfig.UpdateConfigMap(siteConfig)
+	if !updated {
+		t.Errorf("Expect routerconfig to be updated")
+	}
+
+	if err != nil {
+		t.Errorf("Failed updating routerconfig")
+	}
+}
+
 func TestFailedConvert(t *testing.T) {
 	a := []string{"random"}
 	b := SslProfile{}