From 83246cbf6d87ffc689aa0ee2f82ea388c37ea229 Mon Sep 17 00:00:00 2001
From: Neil-Jocelyn Schark <neil.schark@h-da.de>
Date: Fri, 1 Dec 2023 13:44:47 +0000
Subject: [PATCH] Create new integration-test setup

See merge request danet/gosdn!574

Co-authored-by: Malte Bauch <malte.bauch@stud.h-da.de>
Co-authored-by: Fabian Seidl <fabian.seidl@h-da.de>
---
 .devcontainer/Dockerfile                      |  11 +-
 .devcontainer/devcontainer.json               |   6 +-
 .dockerignore                                 |   1 -
 .gitignore                                    |   6 +-
 .gitlab-ci.yml                                |  19 +-
 .gitlab/ci/.build-container-images.yml        |  71 ++-
 .gitlab/ci/.code-quality-ci.yml               |   2 +-
 .gitlab/ci/.integration-test.yml              |  33 --
 .gitlab/ci/.test.yml                          |  63 +-
 .gitlab/ci/gnmi-cert-integration.Dockerfile   |   8 +
 .gitlab/ci/gosdn-integration.Dockerfile       |  10 +
 .../.integration-test-containerlab.yml        |  15 +-
 .gitlab/ci/legacy/.integration-test.yml       |  32 +
 .gitlab/ci/legacy/.test.yml                   |  33 ++
 Makefile                                      |  30 +-
 README.md                                     |  24 +-
 .../arista-routing-engine.Dockerfile          |   2 +-
 .../venv-manager/venv-manager.Dockerfile      |   2 +-
 .../venv-manager/venv-manager/venv-manager.go |   2 +-
 applications/ws-events/ws-events.Dockerfile   |   2 +-
 controller/Makefile                           |  22 +-
 controller/api/apiIntegration_test.go         | 146 -----
 controller/api/initialise_test.go             |   2 -
 controller/cmd/root.go                        |   4 +-
 .../configs/integration-test-gosdn.toml       |  21 +
 controller/controller.Dockerfile              |  10 +-
 controller/controller.Dockerfile.dockerignore |   4 +-
 .../server/configurationmanagement.go         |   5 +-
 .../integration/nucleusIntegration_test.go    | 556 ------------------
 dev_env_data/clab/basic_two_gnmi_targets.yaml |  24 +
 dev_env_data/clab/demo.clab.yaml              |   6 +-
 dev_env_data/clab/gosdn.clab.yaml             |   2 +-
 .../integration-test_docker-compose.yml       |  53 ++
 .../example_tests/controller_test.go          |  19 +
 .../integrationTestUtils.go                   |  72 +++
 mkdocs.yml                                    |   4 +-
 plugin-registry/plugin-registry.Dockerfile    |   2 +-
 .../plugin-registry.Dockerfile.dockerignore   |   3 +-
 38 files changed, 453 insertions(+), 874 deletions(-)
 delete mode 100644 .gitlab/ci/.integration-test.yml
 create mode 100644 .gitlab/ci/gnmi-cert-integration.Dockerfile
 create mode 100644 .gitlab/ci/gosdn-integration.Dockerfile
 rename .gitlab/ci/{ => legacy}/.integration-test-containerlab.yml (92%)
 create mode 100644 .gitlab/ci/legacy/.integration-test.yml
 create mode 100644 .gitlab/ci/legacy/.test.yml
 delete mode 100644 controller/api/apiIntegration_test.go
 create mode 100644 controller/configs/integration-test-gosdn.toml
 delete mode 100644 controller/test/integration/nucleusIntegration_test.go
 create mode 100644 dev_env_data/clab/basic_two_gnmi_targets.yaml
 create mode 100644 dev_env_data/docker-compose/integration-test_docker-compose.yml
 create mode 100644 integration-tests/example_tests/controller_test.go
 create mode 100644 integration-tests/integrationTestUtils/integrationTestUtils.go

diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 74cbf443a..811bc1b83 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,4 +1,4 @@
-# The devcontainer will be based on Python
+# The devcontainer will be based on debian
 # The base container already has entrypoint, vscode user account, etc. out of the box
 FROM mcr.microsoft.com/vscode/devcontainers/base:bullseye
 
@@ -6,17 +6,17 @@ FROM mcr.microsoft.com/vscode/devcontainers/base:bullseye
 ARG _CLAB_VERSION
 
 # Set permissions for mounts in devcontainer.json
-RUN mkdir /home/vscode/.vscode-server/
-RUN mkdir /home/vscode/.vscode-server/bin
+RUN mkdir -p /home/vscode/.vscode-server/bin
 RUN chown -R vscode:vscode /home/vscode/.vscode-server
 
 # Update and install some basic tools inside the container
 # Adjust this list based on your demands
 RUN apt-get update \
     && apt-get upgrade -y \
-    && apt-get install -y --no-install-recommends \
+    && apt-get install -y \
     sshpass \
     curl \
+    wget \
     iputils-ping \
     htop \
     zsh \
@@ -29,7 +29,7 @@ RUN apt-get update \
     && rm -Rf /usr/share/doc && rm -Rf /usr/share/man \
     && apt-get clean
 
-# Install oh-my-zsh for more terminal features and set is as primary shell
+# Install oh-my-zsh for more terminal features and set it as primary shell
 ENV SHELL /bin/zsh
 RUN wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true
 
@@ -39,4 +39,3 @@ RUN echo "export EDITOR='nano'" >> /home/vscode/.zshrc
 
 # Install preferred version of the containerlab
 RUN bash -c "$(curl -sL https://get.containerlab.dev)" -- -v ${_CLAB_VERSION}
-    #&& pip3 install --user yamllint
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 7c93425e6..887b52a5d 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,11 +1,11 @@
 // For format details, see https://aka.ms/devcontainer.json. For config options, see the
-    // README at: https://github.com/devcontainers/templates/tree/main/src/python
+// README at: https://github.com/devcontainers/templates/tree/main/src/python
     {
         "name": "goSDN-develop-clab-go",
         "build": {
             "dockerfile": "Dockerfile",
             "args": {
-                "_CLAB_VERSION": "0.47.2"
+                "_CLAB_VERSION": "0.48.6"
             }
         },
         "features": {
@@ -16,7 +16,7 @@
             },
             "ghcr.io/devcontainers/features/go:1": {
                     "version": "1.21"
-                }
+            }
         },
         // Add any required extensions that must be pre-installed in the devcontainer
         "customizations": {
diff --git a/.dockerignore b/.dockerignore
index a6d07de06..0b61b6ba7 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,4 @@
 .git
-.gitlab
 build
 documentation
 mocks
diff --git a/.gitignore b/.gitignore
index ea0772adf..bdaaaa30d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,8 @@ artifacts/
 
 # containerlab
 clab-gosdn*/
-*clab.yml.bak
-dev_env_data/clab/*clab.yaml.bak
+*.yml.bak
+dev_env_data/clab/*.yaml.bak
 
 # non vimmers
 .vscode/
@@ -74,4 +74,4 @@ plugin-registry/plugins/
 
 # lab-vm
 lab-vm/vm-with-packer/VM/
-lab-vm/vm-with-vagrant/.vagrant/
\ No newline at end of file
+lab-vm/vm-with-vagrant/.vagrant/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2ea8eb57a..7d116ad58 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,8 +1,13 @@
 variables:
-    GOSDN_IMAGE: "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}"
-    GOSDN_TESTING_IMAGE: "${CI_REGISTRY_IMAGE}:testing_${CI_COMMIT_SHA}"
+    DOCKER_TAG: $CI_COMMIT_SHA
+    GOSDN_IMAGE: "${CI_REGISTRY_IMAGE}:${DOCKER_TAG}"
+    GOSDN_TESTING_IMAGE: "${CI_REGISTRY_IMAGE}:testing_${DOCKER_TAG}"
+    GOSDN_INTEGRATION_TEST_IMAGE: "${CI_REGISTRY_IMAGE}/integration-tests:${DOCKER_TAG}"
     CEOS_IMAGE: "${CI_PCONTAINERS_REGISTRY_IMAGE}/ceos:4.28.2F"
+    CLAB_VERSION: "0.48.6"
     GOLANG_VERSION: "1.21"
+    GNMI_TARGET_IMAGE: registry.code.fbi.h-da.de/danet/gnmi-target/debian:master
+    GNMI_TARGET_INTEGRATION_IMAGE: "registry.code.fbi.h-da.de/danet/gosdn/gnmi-target-integration-test:${CI_COMMIT_SHA}"
 
 workflow:
   rules:
@@ -12,10 +17,11 @@ workflow:
 stages:
     - tools
     - build
+    - build-testing
+    - build-release
     - test
     - analyze
-    - integration-test
-    - build-release
+    - test-legacy
     - deploy
     - .post
 
@@ -25,8 +31,9 @@ include:
     - local: "/.gitlab/ci/.code-quality-ci.yml"
     - local: "/.gitlab/ci/.security-and-compliance-ci.yml"
     - local: "/.gitlab/ci/.test.yml"
-    - local: "/.gitlab/ci/.integration-test-containerlab.yml"
-    - local: "/.gitlab/ci/.integration-test.yml"
     - local: "/.gitlab/ci/.uml-autogen-ci.yml"
     - local: "/.gitlab/ci/.renovate.yml"
     - local: "/.gitlab/ci/.mk-docs-deploy.yml"
+    - local: "/.gitlab/ci/legacy/.test.yml"
+    #- local: "/.gitlab/ci/legacy/.integration-test-containerlab.yml"
+    #- local: "/.gitlab/ci/legacy/.integration-test.yml"
diff --git a/.gitlab/ci/.build-container-images.yml b/.gitlab/ci/.build-container-images.yml
index ea3ee5a32..512dce387 100644
--- a/.gitlab/ci/.build-container-images.yml
+++ b/.gitlab/ci/.build-container-images.yml
@@ -1,53 +1,76 @@
 .build: &build
     stage: build
-    tags:
-      - shell-builder
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest
+    services:
+        - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest
+          alias: docker
     variables:
-        TAG: $CI_COMMIT_SHA
+      DOCKER_TLS_CERTDIR: "/certs"
+    before_script:
+      - apk add git
+      - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+      - docker login -u $CI_DEPENDENCY_PROXY_USER -p $CI_DEPENDENCY_PROXY_PASSWORD $CI_DEPENDENCY_PROXY_SERVER
     needs: []
 
-build-controller-testing-image:
-    script:
-        - docker buildx build -t "$GOSDN_TESTING_IMAGE" -f "${CI_PROJECT_DIR}/controller/controller.Dockerfile" --target "builder" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "BUILDARGS=$BUILDARGS" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
-        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-        - docker push "$GOSDN_TESTING_IMAGE"
-    <<: *build
+
+.build-testing: &build-testing
+    stage: build-testing
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest
+    services:
+        - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest
+          alias: docker
+    variables:
+      DOCKER_TLS_CERTDIR: "/certs"
+    before_script:
+      - apk add git
+      - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+      - docker login -u $CI_DEPENDENCY_PROXY_USER -p $CI_DEPENDENCY_PROXY_PASSWORD $CI_DEPENDENCY_PROXY_SERVER
 
 build-controller-image:
     script:
-        - docker buildx build -t "$CI_REGISTRY_IMAGE:$TAG" -f "${CI_PROJECT_DIR}/controller/controller.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "BUILDARGS=$BUILDARGS" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
-        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-        - docker push "$CI_REGISTRY_IMAGE:$TAG"
+        - docker buildx build -t "$CI_REGISTRY_IMAGE:$DOCKER_TAG" -f "${CI_PROJECT_DIR}/controller/controller.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "BUILDARGS=$BUILDARGS" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
+        - docker push "$CI_REGISTRY_IMAGE:$DOCKER_TAG"
     <<: *build
 
 build-cli-image:
     script:
         - CLI_IMAGE_NAME="${CI_REGISTRY_IMAGE}/gosdnc"
-        - docker buildx build -t "$CLI_IMAGE_NAME:$TAG" -f "${CI_PROJECT_DIR}/cli/cli.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "BUILDARGS=$BUILDARGS" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
-        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-        - docker push "$CLI_IMAGE_NAME:$TAG"
+        - docker buildx build -t "$CLI_IMAGE_NAME:$DOCKER_TAG" -f "${CI_PROJECT_DIR}/cli/cli.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "BUILDARGS=$BUILDARGS" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
+        - docker push "$CLI_IMAGE_NAME:$DOCKER_TAG"
     <<: *build
 
 build-venv-manager-image:
     script:
         - VENV_MANAGER_IMAGE_NAME="${CI_REGISTRY_IMAGE}/venv-manager"
-        - docker buildx build -t "$VENV_MANAGER_IMAGE_NAME:$TAG" -f "${CI_PROJECT_DIR}/applications/venv-manager/venv-manager.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
-        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-        - docker push "$VENV_MANAGER_IMAGE_NAME:$TAG"
+        - docker buildx build -t "$VENV_MANAGER_IMAGE_NAME:$DOCKER_TAG" -f "${CI_PROJECT_DIR}/applications/venv-manager/venv-manager.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
+        - docker push "$VENV_MANAGER_IMAGE_NAME:$DOCKER_TAG"
     <<: *build
 
 build-arista-routing-engine-image:
     script:
         - ARISTA_ROUTING_ENGINE_IMAGE_NAME="${CI_REGISTRY_IMAGE}/arista-routing-engine"
-        - docker buildx build -t "$ARISTA_ROUTING_ENGINE_IMAGE_NAME:$TAG" -f "${CI_PROJECT_DIR}/applications/arista-routing-engine/arista-routing-engine.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
-        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-        - docker push "$ARISTA_ROUTING_ENGINE_IMAGE_NAME:$TAG"
+        - docker buildx build -t "$ARISTA_ROUTING_ENGINE_IMAGE_NAME:$DOCKER_TAG" -f "${CI_PROJECT_DIR}/applications/arista-routing-engine/arista-routing-engine.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
+        - docker push "$ARISTA_ROUTING_ENGINE_IMAGE_NAME:$DOCKER_TAG"
     <<: *build
 
 build-plugin-registry-image:
     script:
         - PLUGIN_REGISTRY_IMAGE_NAME="${CI_REGISTRY_IMAGE}/plugin-registry"
-        - docker buildx build -t "$PLUGIN_REGISTRY_IMAGE_NAME:$TAG" -f "${CI_PROJECT_DIR}/plugin-registry/plugin-registry.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
-        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-        - docker push "$PLUGIN_REGISTRY_IMAGE_NAME:$TAG"
+        - docker buildx build -t "$PLUGIN_REGISTRY_IMAGE_NAME:$DOCKER_TAG" -f "${CI_PROJECT_DIR}/plugin-registry/plugin-registry.Dockerfile" --build-arg "GOLANG_VERSION=$GOLANG_VERSION" --build-arg "GITLAB_PROXY=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/" .
+        - docker push "$PLUGIN_REGISTRY_IMAGE_NAME:$DOCKER_TAG"
     <<: *build
+
+build-integration-test-images:
+    needs: ["build-controller-image"]
+    script:
+        # Build certs
+        - apk add openssl make
+        - make generate-certs
+        # Build gosdn with ssl certificates
+        - docker buildx build -t "$CI_REGISTRY_IMAGE:${DOCKER_TAG}_integration-test" -f "${CI_PROJECT_DIR}/.gitlab/ci/gosdn-integration.Dockerfile" --build-arg "GOSDN_IMAGE=$CI_REGISTRY_IMAGE:${DOCKER_TAG}" .
+        # Update gnmi-target with ssl certificates
+        - docker buildx build -t $GNMI_TARGET_INTEGRATION_IMAGE -f "${CI_PROJECT_DIR}/.gitlab/ci/gnmi-cert-integration.Dockerfile" --build-arg "GNMI_TARGET_IMAGE=${GNMI_TARGET_IMAGE}" .
+        # Push images
+        - docker push "$CI_REGISTRY_IMAGE:${DOCKER_TAG}_integration-test"
+        - docker push $GNMI_TARGET_INTEGRATION_IMAGE
+    <<: *build-testing
diff --git a/.gitlab/ci/.code-quality-ci.yml b/.gitlab/ci/.code-quality-ci.yml
index 85db5c48d..ecfcef2cd 100644
--- a/.gitlab/ci/.code-quality-ci.yml
+++ b/.gitlab/ci/.code-quality-ci.yml
@@ -1,7 +1,7 @@
 variables:
     GOLANG_VERSION: "1.21"
 code-quality:
-    image: golangci/golangci-lint:v1.55.2-alpine
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.55.2-alpine
     stage: analyze
     script:
         # writes golangci-lint output to gl-code-quality-report.json
diff --git a/.gitlab/ci/.integration-test.yml b/.gitlab/ci/.integration-test.yml
deleted file mode 100644
index 2825458dd..000000000
--- a/.gitlab/ci/.integration-test.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-.integration-test: &integration-test
-    image: $GOSDN_TESTING_IMAGE
-    stage: integration-test
-    needs:
-        ["containerlab-deploy"]
-    variables:
-        GOSDN_LOG: "nolog"
-        GOSDN_TEST_API_ENDPOINT: "10.254.254.105:${GOSDN_GRPC_PORT}"
-        CEOS_TEST_ENDPOINT: "10.254.254.105:${CEOS1_PORT}"
-        GOSDN_TEST_USER: "admin"
-        GOSDN_TEST_PASSWORD: "admin"
-
-            # NOTE: Current test setup and runners are changed, therefore this test is not possible to run at the moment
-            #integration-test:nucleus:
-            #    <<: *integration-test
-            #    script:
-            #        - ${CI_PROJECT_DIR}/.gitlab/ci/scripts/wait-for-it.sh ${CEOS_TEST_ENDPOINT} -s -t 180 -- echo "CEOS is up"
-            #        - cd controller/
-            #        - make integration-test-nucleus
-
-integration-test:api:
-    <<: *integration-test
-    variables:
-        K8S_OP: "getenv"
-    script:
-        - cd controller/
-        - make integration-test-api
-
-integration-test:cli:
-    <<: *integration-test
-    script:
-        - cd cli/
-        - echo "Your tests here"
diff --git a/.gitlab/ci/.test.yml b/.gitlab/ci/.test.yml
index 4edcd3c59..9a20eb0c0 100644
--- a/.gitlab/ci/.test.yml
+++ b/.gitlab/ci/.test.yml
@@ -1,34 +1,39 @@
 .test: &test
-    image: $GOSDN_TESTING_IMAGE
     stage: test
-    rules:
-        - when: on_success
-    variables:
-        GOSDN_LOG: "nolog"
-        GOSDN_CHANGE_TIMEOUT: "5000ms"
-    coverage: '/total:\s+\(statements\)\s+(\d+.\d+\%)/'
-    artifacts:
-        when: always
-        reports:
-            junit: report.xml
-            coverage_report:
-                coverage_format: cobertura
-                path: ./controller/coverage.xml
-    needs:
-        - build-controller-testing-image
 
-unit-test:
-    script:
-        - cd controller
-        - make ci-unit-test
-    after_script:
-        - cd controller
-        - go tool cover -func=coverage.out
-        - gocover-cobertura < coverage.out > coverage.xml
-    <<: *test
-
-controller-test:
+integration-test-controller:
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golang:$GOLANG_VERSION-bookworm
+    needs:
+        - ["build-integration-test-images"]
+    #image: debian:latest
+    variables:
+        MONGO_INITDB_ROOT_USERNAME: root
+        MONGO_INITDB_ROOT_PASSWORD: example
+        FF_NETWORK_PER_BUILD: 1
+    services:
+      - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/mongo:5
+        alias: mongo
+      - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rabbitmq:3-management
+        alias: rabbitmq
+        variables:
+          HEALTHCHECK_TCP_PORT: "5672"
+      - name: $PLUGIN_REGISTRY_IMAGE_NAME:$DOCKER_TAG
+      - name: $GNMI_TARGET_INTEGRATION_IMAGE
+        alias: gnmi-target_1
+        command: ["start", "--cert", "/etc/gnmi-target/ssl/certs/gnmi-target-selfsigned.crt", "--key", "/etc/gnmi-target/ssl/private/gnmi-target-selfsigned.key", "--ca_file", "/etc/gnmi-target/ssl/ca.crt"]
+      - name: $GNMI_TARGET_INTEGRATION_IMAGE
+        alias: gnmi-target_2
+        command: ["start", "--cert", "/etc/gnmi-target/ssl/certs/gnmi-target-selfsigned.crt", "--key", "/etc/gnmi-target/ssl/private/gnmi-target-selfsigned.key", "--ca_file", "/etc/gnmi-target/ssl/ca.crt"]
+      # Use gosdn docker image from this branch
+      # Uncomment later when fixed. See: https://code.fbi.h-da.de/danet/gosdn/-/issues/335
+      #- name: "$CI_REGISTRY_IMAGE:${DOCKER_TAG}_integration-test"
+      #  alias: gosdn
+      #  variables:
+      #    GOSDN_ADMIN_PASSWORD: TestPassword
+      #  command: ["--config", "/app/configs/integration-test-gosdn.toml", "--security", "insecure"]
     script:
-        - cd controller
-        - make ci-controller-test
+        # Remove later when fixed. See: https://code.fbi.h-da.de/danet/gosdn/-/issues/335
+        - make build-gosdn
+        - cd artifacts && GOSDN_ADMIN_PASSWORD=TestPassword ./gosdn --config ../controller/configs/integration-test-gosdn.toml --security insecure &
+        - INTEGRATION_TEST_CONTROLLER_URL="localhost:55055" go test ./integration-tests/*
     <<: *test
diff --git a/.gitlab/ci/gnmi-cert-integration.Dockerfile b/.gitlab/ci/gnmi-cert-integration.Dockerfile
new file mode 100644
index 000000000..1aa3d0c16
--- /dev/null
+++ b/.gitlab/ci/gnmi-cert-integration.Dockerfile
@@ -0,0 +1,8 @@
+ARG GNMI_TARGET_IMAGE=registry.code.fbi.h-da.de/danet/gnmi-target/debian:latest
+
+FROM ${GNMI_TARGET_IMAGE}
+
+RUN mkdir -p /etc/gnmi-target/ssl/certs/
+RUN mkdir -p /etc/gnmi-target/ssl/private/
+
+COPY ./artifacts/ssl/gnmi-target /etc/gnmi-target/ssl
diff --git a/.gitlab/ci/gosdn-integration.Dockerfile b/.gitlab/ci/gosdn-integration.Dockerfile
new file mode 100644
index 000000000..7629a5218
--- /dev/null
+++ b/.gitlab/ci/gosdn-integration.Dockerfile
@@ -0,0 +1,10 @@
+ARG GOSDN_IMAGE
+
+FROM ${GOSDN_IMAGE}
+
+RUN mkdir /app/ssl
+COPY ./artifacts/ssl/gosdn /app/ssl
+
+#COPY .gitlab/ci/test-start.sh test-start.sh
+#RUN chmod +x test-start.sh
+#ENTRYPOINT ["./test-start.sh"]
diff --git a/.gitlab/ci/.integration-test-containerlab.yml b/.gitlab/ci/legacy/.integration-test-containerlab.yml
similarity index 92%
rename from .gitlab/ci/.integration-test-containerlab.yml
rename to .gitlab/ci/legacy/.integration-test-containerlab.yml
index 467fce876..4cbc4d9a1 100644
--- a/.gitlab/ci/.integration-test-containerlab.yml
+++ b/.gitlab/ci/legacy/.integration-test-containerlab.yml
@@ -3,8 +3,8 @@ variables:
     CLAB_DIR: "/home/gitlab-runner/clab"
     CLAB_NAME: "clab${CI_PIPELINE_IID}"
 
-containerlab-deploy:
-    stage: integration-test
+legacy-containerlab-deploy:
+    stage: test-legacy
     tags:
         - shell-containerlab
     needs: ["build-controller-image", "build-controller-testing-image"]
@@ -38,16 +38,15 @@ containerlab-deploy:
             dotenv: ${CI_PROJECT_DIR}/build.env
 
 
-containerlab-destroy:
-    stage: integration-test
+legacy-containerlab-destroy:
+    stage: test-legacy
     tags:
         - shell-containerlab
     needs:
         [
-            "containerlab-deploy",
-            #"integration-test:nucleus",
-            "integration-test:api"
-
+            "legacy-containerlab-deploy",
+            "legacy-integration-test-legacy:nucleus",
+            "legacy-integration-test-legacy:api"
         ]
     before_script:
         - cd ${CLAB_DIR}
diff --git a/.gitlab/ci/legacy/.integration-test.yml b/.gitlab/ci/legacy/.integration-test.yml
new file mode 100644
index 000000000..c999735bc
--- /dev/null
+++ b/.gitlab/ci/legacy/.integration-test.yml
@@ -0,0 +1,32 @@
+.legacy-integration-test: &legacy-integration-test
+    image: $GOSDN_TESTING_IMAGE
+    stage: test-legacy
+    needs:
+        ["legacy-containerlab-deploy"]
+    variables:
+        GOSDN_LOG: "nolog"
+        GOSDN_TEST_API_ENDPOINT: "10.254.254.105:${GOSDN_GRPC_PORT}"
+        CEOS_TEST_ENDPOINT: "10.254.254.105:${CEOS1_PORT}"
+        GOSDN_TEST_USER: "admin"
+        GOSDN_TEST_PASSWORD: "admin"
+
+legacy-integration-test-legacy:nucleus:
+    <<: *legacy-integration-test
+    script:
+        - ${CI_PROJECT_DIR}/.gitlab/ci/scripts/wait-for-it.sh ${CEOS_TEST_ENDPOINT} -s -t 180 -- echo "CEOS is up"
+        - cd controller/
+        - make integration-test-nucleus
+
+legacy-integration-test-legacy:api:
+    <<: *legacy-integration-test
+    variables:
+        K8S_OP: "getenv"
+    script:
+        - cd controller/
+        - make integration-test-api
+
+legacy-integration-test:cli:
+    <<: *legacy-integration-test
+    script:
+        - cd cli/
+        - echo "Your tests here"
diff --git a/.gitlab/ci/legacy/.test.yml b/.gitlab/ci/legacy/.test.yml
new file mode 100644
index 000000000..bc17769e6
--- /dev/null
+++ b/.gitlab/ci/legacy/.test.yml
@@ -0,0 +1,33 @@
+.legacy-test: &legacy-test
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golang:$GOLANG_VERSION-bookworm
+    stage: test-legacy
+    rules:
+        - when: on_success
+    variables:
+        GOSDN_LOG: "nolog"
+        GOSDN_CHANGE_TIMEOUT: "5000ms"
+    coverage: '/total:\s+\(statements\)\s+(\d+.\d+\%)/'
+    artifacts:
+        when: always
+        reports:
+            junit: report.xml
+            coverage_report:
+                coverage_format: cobertura
+                path: ./controller/coverage.xml
+    needs: []
+
+legacy-unit-test:
+    script:
+        - cd controller
+        - make ci-unit-test
+    after_script:
+        - cd controller
+        - go tool cover -func=coverage.out
+        - gocover-cobertura < coverage.out > coverage.xml
+    <<: *legacy-test
+
+#legacy-controller-test:
+#    script:
+#        - cd controller
+#        - make ci-controller-test
+#    <<: *legacy-test
diff --git a/Makefile b/Makefile
index 0605bfb10..d81179029 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ MAKEFILE_SUBDIR := ./makefiles
 
 GOCMD=go
 GOBUILD=$(GOCMD) build
-GOCLEAN=$(GOCMD) clean
+GOCLEAN=$(GOCMD) clean -cache -fuzzcache -testcache -modcache
 BUILD_ARTIFACTS_PATH=artifacts
 
 PLUGIN_NAME= bundled_plugin.zip
@@ -73,20 +73,34 @@ shell-orchestrator:
 start: build-gosdn
 	./$(BUILD_ARTIFACTS_PATH)/gosdn -l debug --config ./controller/configs/gosdn.toml
 
-start-virt-manager-example: build containerize-plugin-registry
+virt-manager-example-start: build containerize-plugin-registry
 	./scripts/manage_virt_env.sh --mode start --topology dev_env_data/clab/basic_two_aristas.yaml --sdnconfig dev_env_data/sdn/basic_two_aristas.json
 
-stop-virt-manager-example:
+virt-manager-example-stop:
 	./scripts/manage_virt_env.sh --mode stop --topology dev_env_data/clab/basic_two_aristas.yaml
 
-start-dev-env: containerize-gosdn containerize-plugin-registry
-	./scripts/simple-dev-setup.sh --mode start --topology dev_env_data/clab/basic_two_aristas.yaml
+dev-env-start: containerize-gosdn containerize-plugin-registry
+	./scripts/simple-dev-setup.sh --mode start --topology dev_env_data/clab/basic_two_gnmi_targets.yaml
 
-stop-dev-env:
-	./scripts/simple-dev-setup.sh --mode stop --topology dev_env_data/clab/basic_two_aristas.yaml
+dev-env-stop:
+	./scripts/simple-dev-setup.sh --mode stop --topology dev_env_data/clab/basic_two_gnmi_targets.yaml
 
 generate-certs: generate-gnmi-target-certs generate-gosdn-certs
 
+controller-integration-tests: generate-certs containerize-gosdn containerize-plugin-registry
+	docker-compose -f dev_env_data/docker-compose/integration-test_docker-compose.yml down
+	docker-compose -f dev_env_data/docker-compose/integration-test_docker-compose.yml up -d
+	go test ./integration-tests/*
+	docker-compose -f dev_env_data/docker-compose/integration-test_docker-compose.yml down
+
+controller-integration-tests-debug-up: generate-certs containerize-gosdn containerize-plugin-registry
+	docker-compose -f dev_env_data/docker-compose/integration-test_docker-compose.yml up -d
+
+controller-integration-tests-debug-down:
+	docker-compose -f dev_env_data/docker-compose/integration-test_docker-compose.yml down
+
+# Warning: Depending on you go configuration might also clean caches, modules and docker containers from your other projects.
 clean:
 	$(GOCLEAN)
-	rm -rf $(BUILD_ARTIFACTS_PATH)
+	rm -rf $(BUILD_ARTIFACTS_PATH) $(TOOLS_DIR)
+	docker system prune -af
diff --git a/README.md b/README.md
index 9afdfb484..f560b2c23 100644
--- a/README.md
+++ b/README.md
@@ -34,10 +34,10 @@ There you can also find some tutorials to get to know the SDN controller:
   - [Additional services](#additional-services)
   - [Configuration file](#configuration-file)
 - [Development-Tutorial](#development-tutorial)
+- [Integration tests](#integration-tests)
 - [Contributing](CONTRIBUTING.md)
 - [License](LICENSE)
 - [CI Status](#ci-status)
-- [Development Tutorial](#development-tutorial)
 
 ---
 
@@ -56,7 +56,7 @@ network using one unified controller.
 
 ## Example
 
-A simple showcase how the controller can be adressed after
+A simple showcase how the controller can be addressed after
 `make containerlab-start` is shown below:
 
 ![](gosdn-cli-showcase.webm)
@@ -68,7 +68,7 @@ A simple showcase how the controller can be adressed after
 - The `cli` is the CLI to manage the `controller`.
 - `csbi` is the implementation of Containerised-Southbound-Interfaces (based on
   the idea and the proof of concept of Manuel Kieweg). Allowing to request
-  capabilities of MNEs and generate a containerised Southbound-Interface based
+  capabilities of MNEs and generate a containerized Southbound-Interface based
   on them. They are currently unsupported.
 - `controller` represents the `goSDN-controller`.
 
@@ -96,7 +96,7 @@ Any network element directly configured by `goSDN`
 ## Launch goSDN Controller local
 In this chapter, you learn how to launch the goSDN controller.
 
-Firstly, make sure that you're located in the root directory of gosdn.
+Firstly, make sure that you're located in the root directory of goSDN.
 `goSDN` provides a `Makefile` for all common use cases.
 
 ```sh
@@ -125,7 +125,7 @@ running `./gosdn` from the shell:
 
 ## Getting Started
 
-If you want to use the the [playground](#playground) you have to make sure you
+If you want to use the [playground](#playground) you have to make sure you
 have [containerlab](https://containerlab.dev/install/) installed on your
 system.
 
@@ -201,9 +201,9 @@ For the storage system:
 For the event system (RabbitMQ broker):
 
 - `amqpprefix`: the URI prefix that is needed to connect to the RabbitMQ broker.
-- `amqphost`: ip adress of the broker
+- `amqphost`: IP address of the broker
 - `amqpport`: port of the broker
-- `amqpuser`: user name of credentials to access the broker, default value is `guest`
+- `amqpuser`: username of credentials to access the broker, default value is `guest`
 - `amqppassword`: user password of credentials to access the broker, default is `guest`
 
 For the plugin registry:
@@ -300,3 +300,13 @@ To develop applications, we provide a framework that can be found [here](https:/
 The framework provides some basic code to easily set up the subscription to the event system of the controller, so that your application can get notified via the events published by the RabbitMQ server about information you would like to receive. This includes functions to register and subscribe to the used server. The information you receive includes changes on network elements like if a value of a YANG path changes or other changes like additions or deletions of entities.
 
 Examples where the application framework is used can be found [here](https://code.fbi.h-da.de/danet/gosdn/-/tree/master/applications).
+
+## Integration tests
+
+The integration tests are currently in its own folder named `integration-tests`, as they use a complete black box design.
+The idea is to test as many of our code with as little tests as possible to get a "catch all" approach where a test may fail and hint for a problem, without really telling us what the problem is.
+As of the nature of this project many people contribute only a small part and often don't have time to write good unit-tests.
+Therefore, we simply want to make sure that a change doesn't break anything important.
+
+There exists a `example_tests` folder where examples of using the `integrationTestUtils` are shown. Each test should use the appropriate set-up and tear-down/clean-up functions, so that each test operates independent of each other.
+
diff --git a/applications/arista-routing-engine/arista-routing-engine.Dockerfile b/applications/arista-routing-engine/arista-routing-engine.Dockerfile
index 62e45a37c..6df44a04b 100644
--- a/applications/arista-routing-engine/arista-routing-engine.Dockerfile
+++ b/applications/arista-routing-engine/arista-routing-engine.Dockerfile
@@ -1,6 +1,6 @@
 ARG GOLANG_VERSION=1.21
 ARG BUILDARGS
-ARG $GITLAB_PROXY
+ARG GITLAB_PROXY
 
 FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-alpine as builder
 WORKDIR /gosdn/
diff --git a/applications/venv-manager/venv-manager.Dockerfile b/applications/venv-manager/venv-manager.Dockerfile
index a4f8cb9b0..4eece74d6 100644
--- a/applications/venv-manager/venv-manager.Dockerfile
+++ b/applications/venv-manager/venv-manager.Dockerfile
@@ -1,6 +1,6 @@
 ARG GOLANG_VERSION=1.21
 ARG BUILDARGS
-ARG $GITLAB_PROXY=code.fbi.h-da.de:443/danet/dependency_proxy/containers
+ARG GITLAB_PROXY=code.fbi.h-da.de:443/danet/dependency_proxy/containers
 
 FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-bookworm AS builder
 
diff --git a/applications/venv-manager/venv-manager/venv-manager.go b/applications/venv-manager/venv-manager/venv-manager.go
index ddb0df5f2..15de1ede0 100644
--- a/applications/venv-manager/venv-manager/venv-manager.go
+++ b/applications/venv-manager/venv-manager/venv-manager.go
@@ -137,7 +137,7 @@ func (v *VenvManager) CreateSDNConfigFile() error {
 	return nil
 }
 
-// getSDNConfigData gets the sDN configuration data.
+// getSDNConfigData gets the SDN configuration data.
 func (v *VenvManager) getSDNConfigData() (*string, error) {
 	conn, err := v.createConnection()
 	if err != nil {
diff --git a/applications/ws-events/ws-events.Dockerfile b/applications/ws-events/ws-events.Dockerfile
index 442629743..04223f4a4 100644
--- a/applications/ws-events/ws-events.Dockerfile
+++ b/applications/ws-events/ws-events.Dockerfile
@@ -1,6 +1,6 @@
 ARG GOLANG_VERSION=1.21
 ARG BUILDARGS
-ARG $GITLAB_PROXY
+ARG GITLAB_PROXY
 
 FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-alpine as builder
 WORKDIR /app/
diff --git a/controller/Makefile b/controller/Makefile
index d558d5c61..14fb43fef 100644
--- a/controller/Makefile
+++ b/controller/Makefile
@@ -9,7 +9,7 @@ GOBUILD=$(GOCMD) build
 GOCLEAN=$(GOCMD) clean
 BINARY_NAME=gosdn
 
-# Tool Vesions
+# Tool Versions
 GOTESTSUM_VERSION=v1.8.1
 MOCKERY_VERSION=v2.20.0
 GOCOVER_COBERTURA=v1.2.0
@@ -43,27 +43,11 @@ start-insecure: clean build
 unit-test: install-tools
 	ENVIRONMENT=testing ./$(TOOLS_DIR)/gotestsum --junitfile report.xml --format testname -- -short -race $$( go list ./... | grep -v /forks/ | grep -v /mocks ) -v -trimpath -coverprofile=coverage.out
 
-controller-test: install-tools
-	ENVIRONMENT=testing ./$(TOOLS_DIR)/gotestsum --junitfile report.xml --format testname -- -race -v -run TestRun
+show-unit-test-coverage: unit-test
+	go tool cover -html=coverage.out
 
 ci-unit-test: ci-install-tools
 	ENVIRONMENT=testing gotestsum --junitfile report.xml --format testname -- -short -race $$( go list ./... | grep -v /forks/ | grep -v /mocks ) -v -trimpath -coverprofile=coverage.out -covermode atomic -timeout 30m
 
-ci-controller-test: ci-install-tools
-	ENVIRONMENT=testing gotestsum --junitfile report.xml --format testname -- -race -v -run TestRun -trimpath -coverprofile=coverage.out
-
-integration-test-nucleus:
-	ENVIRONMENT=testing  &&\
-	cd ./test/integration &&\
-	go test -race -v -run TestGnmi_SetIntegration &&\
-	go test -race -v -run TestGnmi_GetIntegration &&\
-	go test -race -v -run TestGnmi_SubscribeIntegration &&\
-	go test -race -v -run TestGnmi_CapabilitiesIntegration
-
-integration-test-api:
-	ENVIRONMENT=testing  &&\
-	cd ./api &&\
-	go test -race -v -run TestApiIntegration
-
 generate-mocks: install-tools
 	./$(TOOLS_DIR)/mockery --all --dir "./interfaces/"
diff --git a/controller/api/apiIntegration_test.go b/controller/api/apiIntegration_test.go
deleted file mode 100644
index 99e5d4b62..000000000
--- a/controller/api/apiIntegration_test.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package api
-
-import (
-	"context"
-	"errors"
-	"testing"
-
-	mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
-	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
-	"github.com/google/uuid"
-	guuid "github.com/google/uuid"
-	"github.com/openconfig/gnmi/proto/gnmi"
-	"github.com/openconfig/ygot/ygot"
-	"github.com/spf13/viper"
-)
-
-// This functions complexity is too high, but this will be addresses once the
-// TestSetup is refactored.
-//
-//nolint:gocyclo
-func TestApiIntegration(t *testing.T) {
-	// TDOO: Remove once openshift grpc support is available
-	t.Skip("skipped due to openshift limitations")
-	if testing.Short() {
-		t.Skip("skipping integration test")
-	}
-
-	path, err := ygot.StringToStructuredPath(testPath)
-	if err != nil {
-		t.Error(err)
-	}
-
-	tests := []struct {
-		name    string
-		wantErr bool
-	}{
-		{
-			name:    "default",
-			wantErr: false,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			defer viper.Reset()
-			if err := Init(context.TODO(), testAPIEndpoint); (err != nil) != tt.wantErr {
-				if errors.As(err, &viper.ConfigFileNotFoundError{}) {
-					t.Errorf("gosdn cli init error = %v, wantErr %v", err, tt.wantErr)
-					return
-				}
-			}
-			cliPnd := viper.GetString("CLI_PND")
-			cliSbi := viper.GetString("CLI_SBI")
-
-			suid, err := uuid.Parse(cliSbi)
-			if err != nil {
-				t.Error(err)
-			}
-			puid, err := uuid.Parse(cliPnd)
-			if err != nil {
-				t.Error(err)
-			}
-
-			opt := &tpb.TransportOption{
-				Address:  testAddress,
-				Username: testUsername,
-				Password: testPassword,
-				TransportOption: &tpb.TransportOption_GnmiTransportOption{
-					GnmiTransportOption: &tpb.GnmiTransportOption{},
-				},
-			}
-			if _, err := AddNetworkElement(
-				context.TODO(),
-				testAPIEndpoint,
-				"test-networkElement",
-				opt,
-				suid,
-				puid,
-				[]string{},
-			); (err != nil) != tt.wantErr {
-				t.Errorf("gosdn cli add-networkElement error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			mneid := viper.GetString("LAST_NETWORK_ELEMENT_UUID")
-
-			_, err = GetNetworkElement(
-				context.TODO(),
-				testAPIEndpoint,
-				cliPnd,
-				mneid,
-			)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("gosdn cli request error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-
-			_, err = GetNetworkElement(
-				context.TODO(),
-				testAPIEndpoint,
-				cliPnd,
-				mneid,
-			)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("gosdn cli get-networkElement error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-
-			hostname := guuid.New().String()
-			hostnameAsTypedValue := &gnmi.TypedValue{
-				Value: &gnmi.TypedValue_StringVal{
-					StringVal: hostname,
-				},
-			}
-			_, err = ChangeRequest(
-				context.TODO(),
-				testAPIEndpoint,
-				mneid,
-				cliPnd,
-				path,
-				hostnameAsTypedValue,
-				mnepb.ApiOperation_API_OPERATION_UPDATE,
-			)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("gosdn cli set error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-
-			resp, err := GetNetworkElement(context.TODO(), testAddress, testUsername, testPassword)
-			if err != nil {
-				if !tt.wantErr {
-					t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
-				}
-				return
-			}
-			var got string
-			if resp != nil {
-				got = resp.Mne.Name
-			} else {
-				t.Errorf("integration test failed got cannot be nil")
-			}
-			if got != hostname {
-				t.Errorf("integration test failed = got: %v, want: %v", got, hostname)
-			}
-		})
-	}
-}
diff --git a/controller/api/initialise_test.go b/controller/api/initialise_test.go
index 4fa24cb91..c05da0204 100644
--- a/controller/api/initialise_test.go
+++ b/controller/api/initialise_test.go
@@ -257,8 +257,6 @@ func bufDialer(context.Context, string) (net.Conn, error) {
 	return lis.Dial()
 }
 
-const testPath = "/system/config/hostname"
-
 var testAddress = "10.254.254.105:6030"
 var testAPIEndpoint = "gosdn-latest.apps.ocp.fbi.h-da.de"
 var testUsername = "admin"
diff --git a/controller/cmd/root.go b/controller/cmd/root.go
index e3c8ee91f..2fc51cfa2 100644
--- a/controller/cmd/root.go
+++ b/controller/cmd/root.go
@@ -149,7 +149,7 @@ func initConfig() {
 	log.WithFields(viper.AllSettings()).Debug("current viper config")
 }
 
-func ensureFileSystemStoreExists(pathToFile string) error {
+func ensureFileSystemPathExists(pathToFile string) error {
 	emptyString := []byte("")
 	// create folder if it does not exist
 	if err := os.MkdirAll(configHome, 0777); err != nil {
@@ -172,7 +172,7 @@ func ensureViperConfigFileExists() {
 	configPath := filepath.Join(configHome, configName+"."+configType)
 
 	if _, err := os.Stat(configPath); os.IsNotExist(err) {
-		err := ensureFileSystemStoreExists(configPath)
+		err := ensureFileSystemPathExists(configPath)
 		if err != nil {
 			panic(err)
 		}
diff --git a/controller/configs/integration-test-gosdn.toml b/controller/configs/integration-test-gosdn.toml
new file mode 100644
index 000000000..2708de32e
--- /dev/null
+++ b/controller/configs/integration-test-gosdn.toml
@@ -0,0 +1,21 @@
+amqphost = 'rabbitmq'
+amqppassword = 'guest'
+amqpport = '5672'
+amqpprefix = 'amqp://'
+amqpuser = 'guest'
+basepnduuid = '5f20f34b-cbd0-4511-9ddc-c50cf6a3b49d'
+config = './gosdn/controller/configs/integration-test-gosdn.toml'
+csbi-orchestrator = 'localhost:55056'
+databaseconnection = 'mongodb://root:example@mongo:27017'
+defaultjwtduration = 24
+filesystempathtostores = 'stores'
+gnmisubscriptionspath = 'configs/gNMISubscriptions.txt'
+help = false
+log-level = 'debug'
+plugin-folder = 'plugins'
+plugin-registry = 'plugin-registry:55057'
+security = 'insecure'
+socket = ':55055'
+tlscacertfile = '/ssl/ca.crt'
+tlscertfile = '/ssl/certs/gosdn-selfsigned.crt'
+tlskeyfile = '/ssl/private/gosdn-selfsigned.key'
diff --git a/controller/controller.Dockerfile b/controller/controller.Dockerfile
index 65c04f4e7..56a9bbecc 100644
--- a/controller/controller.Dockerfile
+++ b/controller/controller.Dockerfile
@@ -1,6 +1,6 @@
 ARG GOLANG_VERSION=1.21
 ARG BUILDARGS
-ARG $GITLAB_PROXY
+ARG GITLAB_PROXY
 
 FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-bookworm as builder
 WORKDIR /gosdn/
@@ -11,8 +11,14 @@ RUN --mount=type=cache,target=/root/go/pkg/mod \
 
 FROM ${GITLAB_PROXY}debian:bookworm-slim
 WORKDIR /app/
+RUN mkdir -p controller/configs/ && touch controller/configs/gNMISubscriptions.txt
+
+COPY --from=builder /gosdn/artifacts/gosdn ./gosdn
+
 COPY --from=builder /gosdn/controller/configs/development-gosdn.toml.example ./configs/development-gosdn.toml
+COPY --from=builder /gosdn/controller/configs/integration-test-gosdn.toml ./configs/integration-test-gosdn.toml
 COPY --from=builder /gosdn/controller/configs/containerlab-gosdn.toml.example ./configs/containerlab-gosdn.toml
 COPY --from=builder /gosdn/controller/configs/gNMISubscriptions.txt.example ./configs/gNMISubscriptions.txt
-COPY --from=builder /gosdn/artifacts/gosdn ./gosdn
+
+EXPOSE 55055 8080 40000
 ENTRYPOINT ["./gosdn"]
diff --git a/controller/controller.Dockerfile.dockerignore b/controller/controller.Dockerfile.dockerignore
index 1fbdb8fde..3b9460bba 100644
--- a/controller/controller.Dockerfile.dockerignore
+++ b/controller/controller.Dockerfile.dockerignore
@@ -4,8 +4,7 @@ build
 documentation
 mocks
 test
-clab-gosdn_csbi_arista_base
-clab-gosdn_sts_demo_basic
+clab-gosdn*
 .cobra.yaml
 .dockerignore
 .gitlab-ci.yaml
@@ -16,3 +15,4 @@ artifacts
 build-tools
 models/YangModels
 models/arista
+integration-tests
diff --git a/controller/northbound/server/configurationmanagement.go b/controller/northbound/server/configurationmanagement.go
index 539a567ac..0dabea7d5 100644
--- a/controller/northbound/server/configurationmanagement.go
+++ b/controller/northbound/server/configurationmanagement.go
@@ -85,15 +85,14 @@ func (c ConfigurationManagementServer) ExportSDNConfig(ctx context.Context, requ
 	}
 
 	var sdnConfig = sdnConfig{}
+	var err error
 	sdnConfig.PndID = request.Pid
 
-	networkElements, err := c.mneService.GetAll()
+	sdnConfig.NetworkElements, err = c.mneService.GetAll()
 	if err != nil {
 		return nil, err
 	}
 
-	sdnConfig.NetworkElements = networkElements
-
 	sdnConfig.Nodes, err = c.nodeService.GetAll()
 	if err != nil {
 		return nil, err
diff --git a/controller/test/integration/nucleusIntegration_test.go b/controller/test/integration/nucleusIntegration_test.go
deleted file mode 100644
index 8fdcb85f1..000000000
--- a/controller/test/integration/nucleusIntegration_test.go
+++ /dev/null
@@ -1,556 +0,0 @@
-package integration
-
-// import (
-// 	"context"
-// 	"os"
-// 	"reflect"
-// 	"sort"
-// 	"testing"
-// 	"time"
-
-// 	"github.com/google/uuid"
-
-// 	mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
-// 	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
-// 	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
-
-// 	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/change"
-// 	"code.fbi.h-da.de/danet/gosdn/controller/mocks"
-// 	"code.fbi.h-da.de/danet/gosdn/models/generated/openconfig"
-// 	"code.fbi.h-da.de/danet/gosdn/plugins/sdk"
-
-// 	"code.fbi.h-da.de/danet/gosdn/controller/customerrs"
-// 	"code.fbi.h-da.de/danet/gosdn/controller/nucleus"
-// 	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/types"
-// 	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/util/proto"
-// 	"code.fbi.h-da.de/danet/gosdn/forks/goarista/gnmi"
-// 	"github.com/google/uuid"
-// 	gpb "github.com/openconfig/gnmi/proto/gnmi"
-// 	log "github.com/sirupsen/logrus"
-// 	pb "google.golang.org/protobuf/proto"
-// )
-
-// const unreachable = "203.0.113.10:6030"
-// const testPath = "/system/config/hostname"
-
-// var modifiedHostname = "ceos3000"
-// var testAddress = "10.254.254.105:6030"
-// var testUsername = "admin"
-// var testPassword = "arista"
-// var opt *tpb.TransportOption
-// var gnmiMessages map[string]pb.Message
-
-// func TestMain(m *testing.M) {
-// 	testSetupIntegration()
-// 	os.Exit(m.Run())
-// }
-
-// func testSetupIntegration() {
-// 	if os.Getenv("GOSDN_LOG") == "nolog" {
-// 		log.SetLevel(log.PanicLevel)
-// 	}
-
-// 	addr := os.Getenv("CEOS_TEST_ENDPOINT")
-// 	if addr != "" {
-// 		testAddress = addr
-// 		log.Infof("CEOS_TEST_ENDPOINT set to %v", testAddress)
-// 	}
-// 	u := os.Getenv("GOSDN_TEST_USER")
-// 	if u != "" {
-// 		testUsername = u
-// 		log.Infof("GOSDN_TEST_USER set to %v", testUsername)
-// 	}
-// 	p := os.Getenv("GOSDN_TEST_PASSWORD")
-// 	if p != "" {
-// 		testPassword = p
-// 		log.Infof("GOSDN_TEST_PASSWORD set to %v", testPassword)
-// 	}
-
-// 	gnmiMessages = map[string]pb.Message{
-// 		"../proto/cap-resp-arista-ceos":                  &gpb.CapabilityResponse{},
-// 		"../proto/req-full-node":                         &gpb.GetRequest{},
-// 		"../proto/req-full-node-arista-ceos":             &gpb.GetRequest{},
-// 		"../proto/req-interfaces-arista-ceos":            &gpb.GetRequest{},
-// 		"../proto/req-interfaces-interface-arista-ceos":  &gpb.GetRequest{},
-// 		"../proto/req-interfaces-wildcard":               &gpb.GetRequest{},
-// 		"../proto/resp-full-node":                        &gpb.GetResponse{},
-// 		"../proto/resp-full-node-arista-ceos":            &gpb.GetResponse{},
-// 		"../proto/resp-interfaces-arista-ceos":           &gpb.GetResponse{},
-// 		"../proto/resp-interfaces-interface-arista-ceos": &gpb.GetResponse{},
-// 		"../proto/resp-interfaces-wildcard":              &gpb.GetResponse{},
-// 		"../proto/resp-set-system-config-hostname":       &gpb.SetResponse{},
-// 	}
-// 	for k, v := range gnmiMessages {
-// 		if err := proto.Read(k, v); err != nil {
-// 			log.Fatalf("error parsing %v: %v", k, err)
-// 		}
-// 	}
-
-// 	opt = &tpb.TransportOption{
-// 		Address:  testAddress,
-// 		Username: testUsername,
-// 		Password: testPassword,
-// 		TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 			GnmiTransportOption: &tpb.GnmiTransportOption{},
-// 		},
-// 	}
-// }
-
-// func TestGnmi_SetInvalidIntegration(t *testing.T) {
-// 	if testing.Short() {
-// 		t.Skip("skipping integration test")
-// 	}
-// 	type fields struct {
-// 		opt *tpb.TransportOption
-// 	}
-// 	type args struct {
-// 		ctx     context.Context
-// 		payload change.Payload
-// 		path    string
-// 	}
-// 	tests := []struct {
-// 		name    string
-// 		fields  fields
-// 		args    args
-// 		wantErr bool
-// 	}{
-// 		{
-// 			name: "destination unreachable",
-// 			fields: fields{
-// 				opt: &tpb.TransportOption{
-// 					Address: unreachable,
-// 					TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 						GnmiTransportOption: &tpb.GnmiTransportOption{}},
-// 				},
-// 			},
-// 			args: args{
-// 				ctx:     context.Background(),
-// 				payload: change.Payload{},
-// 				path:    "/",
-// 			},
-// 			wantErr: true,
-// 		},
-// 		{
-// 			name:   "invalid update",
-// 			fields: fields{opt: opt},
-// 			args: args{
-// 				ctx:     context.Background(),
-// 				payload: change.Payload{},
-// 				path:    "/",
-// 			},
-// 			wantErr: true,
-// 		},
-// 	}
-// 	for _, tt := range tests {
-// 		t.Run(tt.name, func(t *testing.T) {
-// 			deviceModel, err := sdk.NewDeviceModel(openconfig.Schema, openconfig.Unmarshal, openconfig.SchemaTreeGzip)
-// 			if err != nil {
-// 				t.Errorf("SetInvalidIntegration() error = %v", err)
-// 				return
-// 			}
-// 			g, err := nucleus.NewTransport(tt.fields.opt, deviceModel)
-// 			if (err != nil) != tt.wantErr {
-// 				t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr)
-// 				return
-// 			}
-// 			err = g.Set(tt.args.ctx, tt.args.payload, tt.args.path, &mocks.Plugin{})
-// 			if (err != nil) != tt.wantErr {
-// 				t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr)
-// 				return
-// 			}
-// 		})
-// 	}
-// }
-
-// func TestGnmi_SetValidIntegration(t *testing.T) {
-// 	if testing.Short() {
-// 		t.Skip("skipping integration test")
-// 	}
-
-// 	opt := &tpb.TransportOption{
-// 		Address:  testAddress,
-// 		Username: testUsername,
-// 		Password: testPassword,
-// 		TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 			GnmiTransportOption: &tpb.GnmiTransportOption{},
-// 		},
-// 	}
-// 	pnd, err := nucleus.NewPND("test", "test", uuid.New(), nil, nil, nil, nil)
-// 	if err != nil {
-// 		t.Error(err)
-// 		return
-// 	}
-// 	_, err = pnd.AddNetworkElement("test", opt, nil, uuid.New(), pnd.ID())
-// 	if err != nil {
-// 		t.Error(err)
-// 		return
-// 	}
-// 	mne, err := pnd.GetNetworkElement("test")
-// 	if err != nil {
-// 		t.Error(err)
-// 		return
-// 	}
-
-// 	tests := []struct {
-// 		name  string
-// 		apiOp mnepb.ApiOperation
-// 		path  string
-// 		value string
-// 		want  string
-// 	}{
-// 		{
-// 			name:  "update",
-// 			apiOp: mnepb.ApiOperation_API_OPERATION_UPDATE,
-// 			path:  testPath,
-// 			value: modifiedHostname,
-// 			want:  modifiedHostname,
-// 		},
-// 		{
-// 			name:  "replace",
-// 			apiOp: mnepb.ApiOperation_API_OPERATION_REPLACE,
-// 			path:  "/system/config/domain-name",
-// 			value: modifiedHostname,
-// 			want:  modifiedHostname,
-// 		},
-// 		{
-// 			name:  "delete",
-// 			apiOp: mnepb.ApiOperation_API_OPERATION_DELETE,
-// 			path:  testPath,
-// 		},
-// 	}
-// 	for _, tt := range tests {
-// 		tt := tt
-// 		t.Run(tt.name, func(t *testing.T) {
-// 			t.Parallel()
-
-// 			n := &server.NetworkElementServer{}
-
-// 			cuid, err := n.ChangeMNE(mne.ID(), tt.apiOp, tt.path, tt.value)
-// 			if err != nil {
-// 				t.Error(err)
-// 				return
-// 			}
-// 			if err := n.Commit(cuid); err != nil {
-// 				t.Error(err)
-// 				return
-// 			}
-// 			if err := n.Confirm(cuid); err != nil {
-// 				t.Error(err)
-// 				return
-// 			}
-// 			if tt.name != "delete" {
-// 				resp, err := pnd.Request(mne.ID(), tt.path)
-// 				if err != nil {
-// 					t.Error(err)
-// 					return
-// 				}
-// 				r, ok := resp.(*gpb.GetResponse)
-// 				if !ok {
-// 					t.Error(&customerrs.InvalidTypeAssertionError{
-// 						Value: resp,
-// 						Type:  &gpb.GetResponse{},
-// 					})
-// 					return
-// 				}
-// 				got := r.Notification[0].Update[0].Val.GetStringVal()
-// 				if !reflect.DeepEqual(got, tt.want) {
-// 					t.Errorf("GetNetworkElement() got = %v, want %v", got, tt.want)
-// 				}
-// 			}
-// 		})
-// 	}
-// }
-
-// func TestGnmi_GetIntegration(t *testing.T) {
-// 	if testing.Short() {
-// 		t.Skip("skipping integration test")
-// 	}
-
-// 	paths := []string{
-// 		"/interfaces/interface",
-// 		"system/config/hostname",
-// 	}
-// 	type fields struct {
-// 		opt *tpb.TransportOption
-// 	}
-// 	type args struct {
-// 		ctx    context.Context
-// 		params []string
-// 	}
-// 	tests := []struct {
-// 		name    string
-// 		fields  fields
-// 		args    args
-// 		want    interface{}
-// 		wantErr bool
-// 	}{
-// 		{
-// 			name:   "default",
-// 			fields: fields{opt: opt},
-// 			args: args{
-// 				ctx:    context.Background(),
-// 				params: paths[:1],
-// 			},
-// 			want:    gnmiMessages["../proto/resp-interfaces-arista-ceos"],
-// 			wantErr: false,
-// 		},
-// 		{
-// 			name: "destination unreachable",
-// 			fields: fields{
-// 				opt: &tpb.TransportOption{
-// 					Address: unreachable,
-// 					TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 						GnmiTransportOption: &tpb.GnmiTransportOption{}},
-// 				},
-// 			},
-// 			args: args{
-// 				ctx:    context.Background(),
-// 				params: paths,
-// 			},
-// 			want:    nil,
-// 			wantErr: true,
-// 		},
-// 	}
-// 	for _, tt := range tests {
-// 		t.Run(tt.name, func(t *testing.T) {
-// 			deviceModel, err := sdk.NewDeviceModel(openconfig.Schema, openconfig.Unmarshal, openconfig.SchemaTreeGzip)
-// 			if err != nil {
-// 				t.Errorf("Get() error = %v", err)
-// 				return
-// 			}
-// 			g, err := nucleus.NewTransport(tt.fields.opt, deviceModel)
-// 			if err != nil {
-// 				t.Error(err)
-// 				return
-// 			}
-// 			got, err := g.Get(tt.args.ctx, tt.args.params...)
-// 			if (err != nil) != tt.wantErr {
-// 				t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
-// 				return
-// 			}
-// 			if reflect.TypeOf(got) != reflect.TypeOf(tt.want) {
-// 				t.Errorf("Get() got = %v, want %v", got, tt.want)
-// 			}
-// 		})
-// 	}
-// }
-// func TestGnmi_SubscribeIntegration(t *testing.T) {
-// 	if testing.Short() {
-// 		t.Skip("skipping integration test")
-// 	}
-
-// 	type fields struct {
-// 		opt *tpb.TransportOption
-// 	}
-// 	type args struct {
-// 		ctx  context.Context
-// 		opts *gnmi.SubscribeOptions
-// 	}
-// 	tests := []struct {
-// 		name    string
-// 		fields  fields
-// 		args    args
-// 		wantErr bool
-// 	}{
-// 		{
-// 			name: "default",
-// 			fields: fields{
-// 				opt: &tpb.TransportOption{
-// 					Address:  testAddress,
-// 					Username: testUsername,
-// 					Password: testPassword,
-// 					Tls:      false,
-// 					TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 						GnmiTransportOption: &tpb.GnmiTransportOption{
-// 							Compression:     "",
-// 							GrpcDialOptions: nil,
-// 							Token:           "",
-// 							Encoding:        0,
-// 						},
-// 					},
-// 				},
-// 			},
-// 			args: args{
-// 				ctx: context.Background(),
-// 				opts: &gnmi.SubscribeOptions{
-// 					Mode:              "stream",
-// 					StreamMode:        "sample",
-// 					SampleInterval:    uint64(1 * time.Second),
-// 					HeartbeatInterval: uint64(100 * time.Millisecond),
-// 					Paths: gnmi.SplitPaths([]string{
-// 						"/interfaces/interface/name",
-// 						"/system/config/hostname",
-// 					}),
-// 					Target: testAddress,
-// 				},
-// 			},
-// 			wantErr: false,
-// 		},
-// 		{
-// 			name: "wrong path",
-// 			fields: fields{
-// 				opt: &tpb.TransportOption{
-// 					TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 						GnmiTransportOption: &tpb.GnmiTransportOption{}},
-// 				},
-// 			},
-// 			args: args{
-// 				opts: &gnmi.SubscribeOptions{
-// 					Mode:              "stream",
-// 					StreamMode:        "sample",
-// 					SampleInterval:    uint64(1 * time.Second),
-// 					HeartbeatInterval: uint64(100 * time.Millisecond),
-// 					Paths: gnmi.SplitPaths([]string{
-// 						"interfaces/interface/name",
-// 						"ystem/config/hostname",
-// 					}),
-// 					Target: testAddress,
-// 				},
-// 			},
-// 			wantErr: true,
-// 		},
-// 		{
-// 			name: "destination unreachable",
-// 			fields: fields{
-// 				opt: &tpb.TransportOption{
-// 					Address: "203.0.113.10:6030",
-// 					TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 						GnmiTransportOption: &tpb.GnmiTransportOption{}},
-// 				},
-// 			},
-// 			args: args{
-// 				opts: &gnmi.SubscribeOptions{},
-// 			},
-// 			wantErr: false,
-// 		},
-// 	}
-// 	for _, tt := range tests {
-// 		t.Run(tt.name, func(t *testing.T) {
-// 			var wantErr = tt.wantErr
-// 			deviceModel, err := sdk.NewDeviceModel(openconfig.Schema, openconfig.Unmarshal, openconfig.SchemaTreeGzip)
-// 			if err != nil {
-// 				t.Errorf("Subscribe() error = %v", err)
-// 				return
-// 			}
-// 			g, err := nucleus.NewTransport(tt.fields.opt, deviceModel)
-// 			if err != nil {
-// 				t.Error(err)
-// 				return
-// 			}
-// 			ctx := context.WithValue(context.Background(), types.CtxKeyOpts, tt.args.opts) //nolint
-// 			ctx, cancel := context.WithCancel(ctx)
-// 			go func() {
-// 				subErr := g.Subscribe(ctx)
-// 				if (subErr != nil) != wantErr {
-// 					if !wantErr && subErr != nil {
-// 						if subErr.Error() != "rpc error: code = Canceled desc = context canceled" {
-// 							t.Errorf("Subscribe() error = %v, wantErr %v", subErr, tt.wantErr)
-// 						}
-// 					}
-// 				}
-// 			}()
-// 			time.Sleep(time.Second * 3)
-// 			cancel()
-// 			time.Sleep(time.Second * 1)
-// 		})
-// 	}
-// }
-
-// func TestGnmi_CapabilitiesIntegration(t *testing.T) {
-// 	if testing.Short() {
-// 		t.Skip("skipping integration test")
-// 	}
-// 	type fields struct {
-// 		opt *tpb.TransportOption
-// 	}
-// 	type args struct {
-// 		ctx context.Context
-// 	}
-// 	tests := []struct {
-// 		name    string
-// 		fields  fields
-// 		args    args
-// 		want    interface{}
-// 		wantErr bool
-// 	}{
-// 		{
-// 			name:    "supported models",
-// 			fields:  fields{opt: opt},
-// 			args:    args{ctx: context.Background()},
-// 			want:    gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).SupportedModels,
-// 			wantErr: false,
-// 		},
-// 		{
-// 			name:    "supported encodings",
-// 			fields:  fields{opt: opt},
-// 			args:    args{ctx: context.Background()},
-// 			want:    gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).SupportedEncodings,
-// 			wantErr: false,
-// 		},
-// 		{
-// 			name:    "gnmi version",
-// 			fields:  fields{opt: opt},
-// 			args:    args{ctx: context.Background()},
-// 			want:    gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).GNMIVersion,
-// 			wantErr: false,
-// 		},
-// 		{
-// 			name: "destination unreachable",
-// 			fields: fields{opt: &tpb.TransportOption{
-// 				Address: "203.0.113.10:6030",
-// 				TransportOption: &tpb.TransportOption_GnmiTransportOption{
-// 					GnmiTransportOption: &tpb.GnmiTransportOption{}},
-// 			},
-// 			},
-// 			args:    args{ctx: context.Background()},
-// 			want:    nil,
-// 			wantErr: true,
-// 		},
-// 	}
-// 	for _, tt := range tests {
-// 		t.Run(tt.name, func(t *testing.T) {
-// 			deviceModel, err := sdk.NewDeviceModel(openconfig.Schema, openconfig.Unmarshal, openconfig.SchemaTreeGzip)
-// 			if err != nil {
-// 				t.Errorf("Capabilities() error = %v", err)
-// 				return
-// 			}
-// 			tr, err := nucleus.NewTransport(tt.fields.opt, deviceModel)
-// 			if err != nil {
-// 				t.Error(err)
-// 				return
-// 			}
-// 			g, ok := tr.(*nucleus.Gnmi)
-// 			if !ok {
-// 				t.Error(&customerrs.InvalidTypeAssertionError{
-// 					Value: tr,
-// 					Type:  &nucleus.Gnmi{},
-// 				})
-// 			}
-// 			resp, err := g.Capabilities(tt.args.ctx)
-// 			if (err != nil) != tt.wantErr {
-// 				t.Errorf("Capabilities() error = %v, wantErr %v", err, tt.wantErr)
-// 				return
-// 			}
-// 			var got interface{}
-// 			switch tt.name {
-// 			case "supported encodings":
-// 				got = resp.(*gpb.CapabilityResponse).SupportedEncodings
-// 			case "supported models":
-// 				t.Skip("test causes false negative")
-// 				got = resp.(*gpb.CapabilityResponse).SupportedModels
-// 				sort.Slice(got.([]*gpb.ModelData), func(i, j int) bool {
-// 					return got.([]*gpb.ModelData)[i].Name < got.([]*gpb.ModelData)[j].Name
-// 				})
-// 				sort.Slice(tt.want.([]*gpb.ModelData), func(i, j int) bool {
-// 					return tt.want.([]*gpb.ModelData)[i].Name < tt.want.([]*gpb.ModelData)[j].Name
-// 				})
-// 			case "gnmi version":
-// 				got = resp.(*gpb.CapabilityResponse).GNMIVersion
-// 			default:
-// 			}
-// 			if !reflect.DeepEqual(got, tt.want) {
-// 				t.Errorf("Type() = %v, want %v", got, tt.want)
-// 			}
-// 		})
-// 	}
-// }
diff --git a/dev_env_data/clab/basic_two_gnmi_targets.yaml b/dev_env_data/clab/basic_two_gnmi_targets.yaml
new file mode 100644
index 000000000..b582864cc
--- /dev/null
+++ b/dev_env_data/clab/basic_two_gnmi_targets.yaml
@@ -0,0 +1,24 @@
+name: gosdn_two_gnmi_targets
+
+mgmt:
+  network: gosdn-net
+  ipv4-subnet: 172.100.0.0/16
+  ipv6-subnet: 2001:db8::/64
+
+topology:
+  kinds:
+    linux:
+      image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:master
+
+  nodes:
+    gnmi0:
+      kind: linux
+      mgmt-ipv4: 172.100.0.11
+
+    gnmi1:
+      kind: linux
+      mgmt-ipv4: 172.100.0.12
+
+  links:
+    - endpoints: ["gnmi0:eth1","gnmi1:eth1"]
+    - endpoints: ["gnmi0:eth2","gnmi1:eth2"]
diff --git a/dev_env_data/clab/demo.clab.yaml b/dev_env_data/clab/demo.clab.yaml
index 66a91dfd3..9543d6304 100644
--- a/dev_env_data/clab/demo.clab.yaml
+++ b/dev_env_data/clab/demo.clab.yaml
@@ -18,7 +18,7 @@ topology:
 
     client0:
       kind: linux
-      image: registry.code.fbi.h-da.de/danet/gnmi-target/ubuntu:develop
+      image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:develop
       binds:
         - ../../artifacts/ssl:/etc/gnmi-target/ssl
       cmd:
@@ -28,7 +28,7 @@ topology:
 
     client1:
       kind: linux
-      image: registry.code.fbi.h-da.de/danet/gnmi-target/ubuntu:develop
+      image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:develop
       binds:
         - ../../artifacts/ssl:/etc/gnmi-target/ssl
       cmd:
@@ -38,7 +38,7 @@ topology:
 
     client2:
       kind: linux
-      image: registry.code.fbi.h-da.de/danet/gnmi-target/ubuntu:develop
+      image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:develop
       binds:
         - ../../artifacts/ssl:/etc/gnmi-target/ssl
       cmd:
diff --git a/dev_env_data/clab/gosdn.clab.yaml b/dev_env_data/clab/gosdn.clab.yaml
index 0de625cff..596c9d0c2 100644
--- a/dev_env_data/clab/gosdn.clab.yaml
+++ b/dev_env_data/clab/gosdn.clab.yaml
@@ -49,7 +49,7 @@ topology:
 
     gnmi-target:
       kind: linux
-      image: registry.code.fbi.h-da.de/danet/gnmi-target/ubuntu:develop
+      image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:develop
       binds:
         - ../../artifacts/ssl/gnmi-target:/etc/gnmi-target/ssl
       ports:
diff --git a/dev_env_data/docker-compose/integration-test_docker-compose.yml b/dev_env_data/docker-compose/integration-test_docker-compose.yml
new file mode 100644
index 000000000..2d2058dad
--- /dev/null
+++ b/dev_env_data/docker-compose/integration-test_docker-compose.yml
@@ -0,0 +1,53 @@
+version: '3'
+
+# Only use via makefile as otherwise goSDN isn't rebuild!
+
+services:
+  mongo:
+    image: mongo:5
+    environment:
+      MONGO_INITDB_ROOT_USERNAME: root
+      MONGO_INITDB_ROOT_PASSWORD: example
+
+  rabbitmq:
+    image: rabbitmq:3-management
+    healthcheck:
+        test: rabbitmq-diagnostics -q ping
+        interval: 30s
+        timeout: 10s
+        retries: 5
+
+  plugin-registry:
+    image: plugin-registry
+
+  gosdn:
+    image: gosdn
+    ports:
+      - 55055:55055
+      - 8080:8080
+      - 40000:40000
+    depends_on:
+      gnmi-target:
+        condition: service_started
+      mongo:
+        condition: service_started
+      plugin-registry:
+        condition: service_started
+      rabbitmq:
+        condition: service_healthy
+    command:
+      --config /app/configs/integration-test-gosdn.toml --security insecure
+    environment:
+        GOSDN_ADMIN_PASSWORD: TestPassword
+    volumes:
+      - ../../artifacts/ssl/gosdn:/app/ssl
+
+  gnmi-target:
+    image: registry.code.fbi.h-da.de/danet/gnmi-target/debian:master
+    deploy:
+      mode: replicated
+      replicas: 2
+    volumes:
+      - ../../artifacts/ssl/gnmi-target:/etc/gnmi-target/ssl
+    command:
+      start --cert /etc/gnmi-target/ssl/certs/gnmi-target-selfsigned.crt --key /etc/gnmi-target/ssl/private/gnmi-target-selfsigned.key --ca_file /etc/gnmi-target/ssl/ca.crt
diff --git a/integration-tests/example_tests/controller_test.go b/integration-tests/example_tests/controller_test.go
new file mode 100644
index 000000000..14d3833c2
--- /dev/null
+++ b/integration-tests/example_tests/controller_test.go
@@ -0,0 +1,19 @@
+package integration_test
+
+import (
+	"testing"
+
+	integration_test_utils "code.fbi.h-da.de/danet/gosdn/integration-tests/integrationTestUtils"
+)
+
+func TestExample(t *testing.T) {
+	conn, err := integration_test_utils.CreateConnection()
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+	sndConfig, err := integration_test_utils.PrepareEvironment(conn)
+	if err != nil {
+		t.Errorf(err.Error())
+	}
+	defer integration_test_utils.RestoreEnvironment(conn, sndConfig)
+}
diff --git a/integration-tests/integrationTestUtils/integrationTestUtils.go b/integration-tests/integrationTestUtils/integrationTestUtils.go
new file mode 100644
index 000000000..2afa6eb83
--- /dev/null
+++ b/integration-tests/integrationTestUtils/integrationTestUtils.go
@@ -0,0 +1,72 @@
+package integration_test_utils
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	configMgmtPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/configurationmanagement"
+	ppb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+func CreateConnection() (*grpc.ClientConn, error) {
+	controller_url := "localhost:55055"
+	controller_env := os.Getenv("INTEGRATION_TEST_CONTROLLER_URL")
+	if controller_env != "" {
+		controller_url = controller_env
+	}
+	dialOption := grpc.WithTransportCredentials(insecure.NewCredentials())
+	conn, err := grpc.Dial(controller_url, dialOption, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100*1024*1024)))
+	if err != nil {
+		return nil, err
+	}
+
+	return conn, nil
+}
+
+func PrepareEvironment(conn *grpc.ClientConn) (string, error) {
+	ctx := context.Background()
+	pndService := ppb.NewPndServiceClient(conn)
+	pndRes, err := pndService.GetPndList(ctx, &ppb.GetPndListRequest{Timestamp: getTimestamp()})
+	if err != nil {
+		return "", err
+	}
+	pndID := pndRes.Pnd[0].Id
+
+	configMgmtService := configMgmtPb.NewConfigurationManagementServiceClient(conn)
+
+	sdnConfigResponse, err := configMgmtService.ExportSDNConfig(ctx, &configMgmtPb.ExportSDNConfigRequest{Timestamp: getTimestamp(), Pid: pndID})
+	if err != nil {
+		return "", err
+	}
+
+	return sdnConfigResponse.SdnConfigData, nil
+}
+
+func RestoreEnvironment(conn *grpc.ClientConn, sdnConfig string) {
+	ctx := context.Background()
+
+	pndService := ppb.NewPndServiceClient(conn)
+	pndRes, err := pndService.GetPndList(ctx, &ppb.GetPndListRequest{Timestamp: getTimestamp()})
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	// currently only support for default PND
+	pndID := pndRes.Pnd[0].Id
+
+	configMgmtService := configMgmtPb.NewConfigurationManagementServiceClient(conn)
+
+	_, err = configMgmtService.ImportSDNConfig(ctx, &configMgmtPb.ImportSDNConfigRequest{Timestamp: getTimestamp(), Pid: pndID, SdnConfigData: sdnConfig})
+	if err != nil {
+		fmt.Println(err)
+	}
+}
+
+func getTimestamp() int64 {
+	return int64(time.Now().Nanosecond())
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index bb40753ca..16ba4fcad 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -13,7 +13,7 @@ theme:
     palette:
     # Palette toggle for light mode
     - media: "(prefers-color-scheme: light)"
-      scheme: default 
+      scheme: default
       primary: white
       toggle:
         icon: material/brightness-7
@@ -36,4 +36,4 @@ theme:
 site_author: goSDN-Team
 site_description: goSDN is a modern SDN controller based around gnmi, YANG and go
 repo_name: danet/gosdn
-repo_url: https://code.fbi.h-da.de/danet/gosdn
\ No newline at end of file
+repo_url: https://code.fbi.h-da.de/danet/gosdn
diff --git a/plugin-registry/plugin-registry.Dockerfile b/plugin-registry/plugin-registry.Dockerfile
index 6448d49d2..a82ea5647 100644
--- a/plugin-registry/plugin-registry.Dockerfile
+++ b/plugin-registry/plugin-registry.Dockerfile
@@ -1,6 +1,6 @@
 ARG GOLANG_VERSION=1.21
 ARG BUILDARGS
-ARG $GITLAB_PROXY
+ARG GITLAB_PROXY
 
 FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-bookworm as builder
 WORKDIR /plugin-registry/
diff --git a/plugin-registry/plugin-registry.Dockerfile.dockerignore b/plugin-registry/plugin-registry.Dockerfile.dockerignore
index d4fbe10ce..ef3ac2462 100644
--- a/plugin-registry/plugin-registry.Dockerfile.dockerignore
+++ b/plugin-registry/plugin-registry.Dockerfile.dockerignore
@@ -4,8 +4,7 @@ build
 documentation
 mocks
 test
-clab-gosdn_csbi_arista_base
-clab-gosdn_sts_demo_basic
+clab-gosdn*
 .cobra.yaml
 .dockeringore
 .gitlab-ci.yaml
-- 
GitLab