diff --git a/Jenkinsfile b/Jenkinsfile
index 2319a3d6d0722fa67ed3a5e5865d53a689b6a715..9a56d69ab399a94a61a9b6e5917695248a9203c2 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -6,17 +6,19 @@ pipeline {
     }
 
     environment {
-        BLUE_OCEAN_URL = "https://jenkins.infra.ozg-cloud.systems/job/ozgcloud-keycloak-operator/job/${env.BRANCH_NAME}/${env.BUILD_NUMBER}/"
         RELEASE_REGEX = /\d+.\d+.\d+/
         SNAPSHOT_REGEX = /\d+.\d+.\d+-SNAPSHOT/
         FAILED_STAGE = ""
         SH_SUCCESS_STATUS_CODE = 0
+
+        KEYCLOAK_OPERATOR_NAME = 'ozgcloud-keycloak-operator'
+        ELASTICSEARCH_OPERATOR_NAME = 'ozgcloud-elasticsearch-operator'
     }
 
     options {
         timeout(time: 1, unit: 'HOURS')
         disableConcurrentBuilds()
-        buildDiscarder(logRotator(numToKeepStr: '5'))
+        buildDiscarder(logRotator(numToKeepStr: '10'))
     }
 
     stages {
@@ -27,8 +29,7 @@ pipeline {
                     def rootPom = readMavenPom file: 'pom.xml'
                     def rootVersion = rootPom.version
 
-
-                    if(env.BRANCH_NAME == 'release'){
+                    if(isReleaseBranch()){
                         if ( !isReleaseVersion([rootVersion])) {
                             error("Keine Release Version für Branch ${env.BRANCH_NAME}.")
                         }
@@ -40,7 +41,7 @@ pipeline {
                 }
             }
         }
-        stage('Build OzgCloud Keycloak Operator') {
+        stage('Build OzgCloud Operator') {
             steps {
                 script {
                     FAILED_STAGE=env.STAGE_NAME
@@ -93,45 +94,50 @@ pipeline {
                 }
 
                 configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) {
-                        sh 'mvn -s $MAVEN_SETTINGS spring-boot:build-image -DskipTests -Dmaven.wagon.http.retryHandler.count=3'
+                    sh 'mvn -s $MAVEN_SETTINGS spring-boot:build-image -DskipTests -Dmaven.wagon.http.retryHandler.count=3'
                 }
-           }
+            }
         }
 
-
         stage('Tag and Push Docker image') {
             steps {
                 script {
                     FAILED_STAGE=env.STAGE_NAME
                     IMAGE_TAG = generateImageTag()
 
-                    tagAndPushDockerImage('ozgcloud-keycloak-operator', IMAGE_TAG)
+                    tagAndPushDockerImage(KEYCLOAK_OPERATOR_NAME, IMAGE_TAG)
+                    tagAndPushDockerImage(ELASTICSEARCH_OPERATOR_NAME, IMAGE_TAG)
                 
-                    if (env.BRANCH_NAME == 'master') {
-                        tagAndPushDockerImage('ozgcloud-keycloak-operator', 'snapshot-latest')
+                    if (isMasterBranch()) {
+                        tagAndPushDockerImage(KEYCLOAK_OPERATOR_NAME, 'snapshot-latest')
+                        tagAndPushDockerImage(ELASTICSEARCH_OPERATOR_NAME, 'snapshot-latest')
                     }
-                    else if (env.BRANCH_NAME == 'release') {
-                        tagAndPushDockerImage('ozgcloud-keycloak-operator', 'latest')
+                    else if (isReleaseBranch()) {
+                        tagAndPushDockerImage(KEYCLOAK_OPERATOR_NAME, 'latest')
+                        tagAndPushDockerImage(ELASTICSEARCH_OPERATOR_NAME, 'latest')
                     }
                 }
             }
         }
 
-        stage('Test, build and deploy Helm Chart') {
+        stage('Test, build and deploy Keycloak-Operator Helm Chart') {
             steps {
                 script {
                     FAILED_STAGE=env.STAGE_NAME
                     HELM_CHART_VERSION = generateHelmChartVersion()
 
-                    dir('src/main/helm') {
-                        sh "helm lint -f ../../test/helm/linter_values.yaml"
-
-                        sh "helm unittest --helm3 -f '../../test/helm/*.yaml' -f '../../test/helm/*/*.yaml' ."
-
-                        sh "helm package --version=${HELM_CHART_VERSION} ."
+                    testAndDeployKeycloakHelmChart(HELM_CHART_VERSION)
+                }
+            }
+        }
+        
+        stage('Test, build and deploy Elasticsearch-Operator Helm Chart') {
+            steps {
+                script {
+                    FAILED_STAGE=env.STAGE_NAME
+                    HELM_CHART_VERSION = generateHelmChartVersion()
 
-                        deployHelmChart("ozgcloud-keycloak-operator", HELM_CHART_VERSION)
-                    }
+                    testAndDeployElasticsearchHelmChart(HELM_CHART_VERSION)
                 }
             }
         }
@@ -144,11 +150,7 @@ pipeline {
                 script {
                     FAILED_STAGE = env.STAGE_NAME
 
-                    cloneGitopsRepo()
-
-                    setNewDevVersion()
-
-                    pushNewDevVersion()
+                    doDevRollout()
                 }
             }
         }
@@ -161,68 +163,58 @@ pipeline {
                 script {
                     FAILED_STAGE = env.STAGE_NAME
 
-                    cloneGitopsRepo()
-
-                    setNewTestVersion()
-
-                    pushNewTestVersion()
+                    doTestRollout()
                 }
             }
         }
 
         stage ('OWASP Dependency-Check Vulnerabilities') {
             steps {
-                    dependencyCheck additionalArguments: ''' 
-                        -o "./" 
-                        -s "./"
-                        -f "ALL" 
-                        -d /dependency-check-data
-                        --suppression dependency-check-supressions.xml
-                        --disableKnownExploited
-                        --noupdate
-                        --disableArchive
-                        --prettyPrint''', odcInstallation: 'dependency-check-owasp'
-
-                    dependencyCheckPublisher( 
-                        pattern: 'dependency-check-report.xml' ,
-                        //unstableNewCritical: 999,
-                        //unstableNewHigh: 999,
-                        //unstableNewMedium: 999,
-                        //unstableNewLow: 999,
-                        //unstableTotalCritical: 999,
-                        //unstableTotalHigh: 999,
-                        //unstableTotalMedium: 999,
-                        //unstableTotalLow: 999,
-                        //failedNewCritical: 999,
-                        //failedNewHigh: 999,
-                        //failedNewMedium: 999,
-                        //failedNewLow: 999,
-                        //failedTotalCritical: 999,
-                        //failedTotalHigh: 999,
-                        //failedTotalMedium: 999,
-                        //failedTotalLow: 999
-                    )            }
-        }
-    }
-    post {
-        failure {
-            script {
-                if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'release') {
-                    //sendFailureMessage()
-                }
+                dependencyCheck additionalArguments: ''' 
+                    -o "./" 
+                    -s "./"
+                    -f "ALL" 
+                    -d /dependency-check-data
+                    --suppression dependency-check-supressions.xml
+                    --disableKnownExploited
+                    --noupdate
+                    --disableArchive
+                    --prettyPrint''', odcInstallation: 'dependency-check-owasp'
+
+                dependencyCheckPublisher( 
+                    pattern: 'dependency-check-report.xml'
+                )           
             }
         }
     }
 }
 
+
+Void testAndDeployKeycloakHelmChart(String helmChartVersion){  
+    dir("${KEYCLOAK_OPERATOR_NAME}/src/main/helm") {
+        runHelmTests()
+        deployHelmChart(KEYCLOAK_OPERATOR_NAME, helmChartVersion)
+    }
+}
+
+Void testAndDeployElasticsearchHelmChart(String helmChartVersion){  
+    dir("${ELASTICSEARCH_OPERATOR_NAME}/src/main/helm") {
+        runHelmTests()
+        deployHelmChart(ELASTICSEARCH_OPERATOR_NAME, helmChartVersion)
+    }
+}
+
+Void runHelmTests(){
+    sh 'helm lint -f ../../test/helm/linter_values.yaml'
+    sh "helm unittest --helm3 -f '../../test/helm/*.yaml' -f '../../test/helm/*/*.yaml' ."
+    sh "helm package --version=${HELM_CHART_VERSION} ."
+}
+
 Void deployHelmChart(String helmChartName, String helmChartVersion) {       
     withCredentials([usernamePassword(credentialsId: 'jenkins-nexus-login', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]){
-        if (env.BRANCH_NAME == 'release') {
-            result = sh script: '''curl -u $USERNAME:$PASSWORD https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps -F file=@'''+helmChartName+'''-'''+helmChartVersion+'''.tgz''', returnStdout: true
-        }
-        else {
-            result = sh script: '''curl -u $USERNAME:$PASSWORD https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps-snapshot -F file=@'''+helmChartName+'''-'''+helmChartVersion+'''.tgz''', returnStdout: true
-        }
+    	def url = getHelmRepoUrl()
+    	echo "Url: ${url}"
+        def result = sh script: '''curl -u $USERNAME:$PASSWORD ''' + url + ''' -F file=@'''+helmChartName+'''-'''+helmChartVersion+'''.tgz''', returnStdout: true
 
         if (result != '') {
             error(result)
@@ -230,34 +222,32 @@ Void deployHelmChart(String helmChartName, String helmChartVersion) {
     }
 }
 
+String getHelmRepoUrl(){
+    if (isReleaseBranch()) {
+        return "https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps"
+    }
+    return "https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps-snapshot"
+}
+
 String generateHelmChartVersion() {
     def chartVersion = getPomVersion('pom.xml')
 
-    if (env.BRANCH_NAME == 'master') {
+    if (isMasterBranch()) {
         chartVersion += "-${env.GIT_COMMIT.take(7)}"
     }
-    else if (env.BRANCH_NAME != 'release') {
+    else if (!isReleaseBranch()) {
         chartVersion += "-${env.BRANCH_NAME}"
     }
 
-    return chartVersion.replaceAll("_", "-")
+    return chartVersion.replaceAll('_', '-')
 }
 
-Void sendFailureMessage() {
-    def room = ''
-    def data = """{"msgtype":"m.text", \
-                    "body":"OzgCloud-Keycloak-Operator: Build Failed. Stage: ${FAILED_STAGE} Build-ID: ${env.BUILD_NUMBER} Link: ${BLUE_OCEAN_URL}", \
-                    "format": "org.matrix.custom.html", \
-                    "formatted_body":"OzgCloud-Keycloak-Operator: Build Failed. Stage: ${FAILED_STAGE} Build-ID: <a href='${BLUE_OCEAN_URL}'>${env.BUILD_NUMBER}</a>"}"""
-       
-    if (env.BRANCH_NAME == 'master') {
-        room = "!iQPAvQIiRwRpNOszjw:matrix.ozg-sh.de"
-    }
-    else if (env.BRANCH_NAME == 'release') {
-        room = "!oWZpUGTFsxkJIYNfYg:matrix.ozg-sh.de"
-    }
+Boolean isMasterBranch() {
+    return env.BRANCH_NAME == 'master'
+}
 
-    sh "curl -XPOST -H 'authorization: Bearer ${getElementAccessToken()}' -d '${data}' https://matrix.ozg-sh.de/_matrix/client/v3/rooms/$room/send/m.room.message"
+Boolean isReleaseBranch() {
+    return env.BRANCH_NAME == 'release'
 }
 
 String getElementAccessToken() {
@@ -266,32 +256,47 @@ String getElementAccessToken() {
     }
 }
 
-Void setNewDevVersion() {
-    setNewOzgOperatorVersion('dev')
+Void doDevRollout() {
+    cloneGitopsRepo()
+    setNewOperatorVersion('dev')
+    pushNewGitopsVersion('dev')
 }
 
-Void setNewTestVersion() {
-    setNewOzgOperatorVersion('test')
+Void doTestRollout() {
+    cloneGitopsRepo()
+    setNewOperatorVersion('test')
+    pushNewGitopsVersion('test')
 }
 
-Void setNewOzgOperatorVersion(String environment) {
-    dir("gitops") {
-        def envFile = "${environment}/application/values/ozgcloud-keycloak-operator-values.yaml"
-        def envVersions = readYaml file: envFile
+Void setNewOperatorVersion(String environment) {
+    dir('gitops') {
+        updateKeycloakOperatorVersions()
+        updateElasticsearchOperatorVersions()
+    }
+}
 
-        envVersions.ozgcloud_keycloak_operator.image.tag = IMAGE_TAG
-        envVersions.ozgcloud_keycloak_operator.helm.version = HELM_CHART_VERSION
+Void updateKeycloakOperatorVersions(String environment){
+    def valuesFile = getApplicationValues(environment, KEYCLOAK_OPERATOR_NAME)
+    def envVersions = readYaml file: envFile
 
-        writeYaml file: envFile, data: envVersions, overwrite: true
-    }
+    envVersions.ozgcloud_keycloak_operator.image.tag = IMAGE_TAG
+    envVersions.ozgcloud_keycloak_operator.helm.version = HELM_CHART_VERSION
+
+    writeYaml file: envFile, data: envVersions, overwrite: true
 }
 
-Void pushNewDevVersion() {
-    pushNewGitopsVersion('dev')
+Void updateElasticsearchOperatorVersions(String environment){
+    def valuesFile = getApplicationValues(environment, ELASTICSEARCH_OPERATOR_NAME)
+    def envVersions = readYaml file: envFile
+
+    envVersions.ozgcloud_elasticsearch_operator.image.tag = IMAGE_TAG
+    envVersions.ozgcloud_elasticsearch_operator.helm.version = HELM_CHART_VERSION
+
+    writeYaml file: envFile, data: envVersions, overwrite: true
 }
 
-Void pushNewTestVersion() {
-    pushNewGitopsVersion('test')
+String getApplicationValues(String environment, String valuesFileName) {
+    return "${environment}/application/values/${valuesFileName}-values.yaml"
 }
 
 Void pushNewGitopsVersion(String environment) {
diff --git a/build.sh b/build.sh
deleted file mode 100755
index 73364f7380a8fc8d130f175a93484d592b87ef0e..0000000000000000000000000000000000000000
--- a/build.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-mvn -Pnative spring-boot:build-image
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 0000000000000000000000000000000000000000..1176e6c790024968252f068bcf3c71091da20f91
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1,7 @@
+
+lombok.log.fieldName=LOG
+lombok.log.slf4j.flagUsage = ERROR
+lombok.log.log4j.flagUsage = ERROR
+lombok.data.flagUsage = ERROR
+lombok.nonNull.exceptionType = IllegalArgumentException
+lombok.addLombokGeneratedAnnotation = true
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/pom.xml b/ozgcloud-elasticsearch-operator/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1814ae1d749e70488a71c85bb8a16eda4fc88c2f
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/pom.xml
@@ -0,0 +1,79 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>de.ozgcloud</groupId>
+		<artifactId>ozgcloud-operator-parent</artifactId>
+		<version>2.1.0-SNAPSHOT</version>
+		<relativePath>../</relativePath>
+	</parent>
+
+	<artifactId>ozgcloud-elasticsearch-operator</artifactId>
+	<packaging>jar</packaging>
+
+	<name>OZG-Cloud Elasticsearch Operator</name>
+	<description>OZG-Cloud Elasticsearch Operator</description>
+
+	<properties>
+		<spring-boot.build-image.imageName>docker.ozg-sh.de/ozgcloud-elasticsearch-operator:build-latest</spring-boot.build-image.imageName>
+	</properties>
+	
+	<dependencies>
+		<dependency>
+		    <groupId>co.elastic.clients</groupId>
+		    <artifactId>elasticsearch-java</artifactId>
+		</dependency>
+		<dependency>
+		    <groupId>com.fasterxml.jackson.core</groupId>
+		    <artifactId>jackson-databind</artifactId>
+		</dependency>
+		<dependency>
+		  <groupId>org.springframework.boot</groupId>
+		  <artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		
+		<!-- test -->
+		<dependency>
+	    	<groupId>org.testcontainers</groupId>
+		    <artifactId>elasticsearch</artifactId>
+		    <scope>test</scope>
+		</dependency>
+		<dependency>
+		    <groupId>io.javaoperatorsdk</groupId>
+		    <artifactId>jenvtest-fabric8-client-support</artifactId>
+		    <scope>test</scope>
+		</dependency>
+		<dependency>
+		    <groupId>io.javaoperatorsdk</groupId>
+		    <artifactId>jenvtest</artifactId>
+		    <scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<fork>true</fork>
+					<annotationProcessorPaths>
+						<path>
+							<groupId>org.projectlombok</groupId>
+							<artifactId>lombok</artifactId>
+							<version>${lombok.version}</version>
+						</path>
+					</annotationProcessorPaths>
+					<showWarnings>true</showWarnings>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
\ No newline at end of file
diff --git a/run_helm_test.sh b/ozgcloud-elasticsearch-operator/run_helm_test.sh
similarity index 100%
rename from run_helm_test.sh
rename to ozgcloud-elasticsearch-operator/run_helm_test.sh
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/Chart.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ae8cad725089582a3f9dd8f21d8514b1f26b2e2f
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/Chart.yaml
@@ -0,0 +1,7 @@
+apiVersion: v2
+name: ozgcloud-elasticsearch-operator
+description: OZG-Cloud Elasticsearch Operator
+type: application
+version: 0.0.0-MANAGED-BY-JENKINS
+appVersion: "0.0.0-MANAGED-BY-JENKINS"
+icon: https://simpleicons.org/icons/helm.svg
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/_helpers.tpl b/ozgcloud-elasticsearch-operator/src/main/helm/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..5b08fa932ccbeb01edc1aea23eb199f904d2c8c9
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/_helpers.tpl
@@ -0,0 +1,7 @@
+
+
+
+{{- define "app.matchLabels" }}
+app.kubernetes.io/name: {{ .Release.Name }}
+app.kubernetes.io/namespace: {{ .Release.Namespace }}
+{{- end -}}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/configmap_bindings_type.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/configmap_bindings_type.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d2ec843e542a4c8f2f215d9bd9d222522122bc44
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/configmap_bindings_type.yaml
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: bindings-type
+  namespace: {{ .Release.Namespace }}
+data:
+  type: |
+    ca-certificates
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/crd/operator.ozgcloud.de_OzgCloudElasticsearch.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/crd/operator.ozgcloud.de_OzgCloudElasticsearch.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bbb564af71b8d78cf0d1400354d10026cc40fddb
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/crd/operator.ozgcloud.de_OzgCloudElasticsearch.yaml
@@ -0,0 +1,43 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: ozgcloudelasticsearchs.operator.ozgcloud.de
+spec:
+  group: operator.ozgcloud.de
+  names:
+    kind: OzgCloudElasticsearch
+    listKind: OzgCloudElasticsearchList
+    plural: ozgcloudelasticsearchs
+    singular: ozgcloudelasticsearch
+  scope: Namespaced
+  versions:
+  - name: v1
+    schema:
+      openAPIV3Schema:
+        type: object
+        description: OzgCloudElasticsearch is the Schema for the elasticsearch API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            type: string
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+          metadata:
+            type: object
+          spec:
+            description: Spec defines the desired state of Elasticsearch
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            description: Status defines the observed state of Elasticsearch
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+    served: true
+    storage: true
+    subresources:
+      status: {}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/deployment.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..549b8979f9eb3d62c6008b29c468926edd1b2cad
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/deployment.yaml
@@ -0,0 +1,112 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Release.Name }}
+  namespace: {{ .Release.Namespace }}
+  labels:
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+    app.kubernetes.io/name: {{ .Release.Name }}
+    app.kubernetes.io/namespace: {{ .Release.Namespace }}
+    app.kubernetes.io/part-of: ozg
+    app.kubernetes.io/version: {{ .Chart.AppVersion }}
+    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+spec:
+  selector:
+    matchLabels:
+    {{- include "app.matchLabels" . | indent 6 }}
+  template:
+    metadata:
+      labels:
+      {{- include "app.matchLabels" . | indent 8 }}
+    spec:
+      serviceAccountName: ozgcloud-elasticsearch-operator-serviceaccount
+      containers:
+      - name: ozgcloud-elasticsearch-operator
+        image: "{{ required "image.repo must be set" (.Values.image).repo }}/{{ required "image.name must be set" (.Values.image).name }}:{{ required "image.tag must be set" (.Values.image).tag }}"
+        env:
+        {{- with (.Values.env).customList }}
+{{ toYaml . | indent 8 }}
+        {{- end }}
+        - name: SERVICE_BINDING_ROOT
+          value: "/bindings"
+        imagePullPolicy: Always
+        readinessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /actuator/health/readiness
+            port: 8081
+            scheme: HTTP
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 3
+        startupProbe:
+          failureThreshold: 10
+          httpGet:
+            path: /actuator/health/readiness
+            port: 8081
+            scheme: HTTP
+          initialDelaySeconds: 30
+          periodSeconds: 5
+          successThreshold: 1
+          timeoutSeconds: 5
+        resources:
+        {{- with .Values.resources }}
+{{ toYaml . | indent 10 }}
+        {{- end }}
+        securityContext:
+          allowPrivilegeEscalation: false
+          privileged: false
+          readOnlyRootFilesystem: false
+          runAsNonRoot: true
+        stdin: true
+        terminationMessagePath: /dev/termination-log
+        terminationMessagePolicy: File
+        tty: true
+        volumeMounts:
+        - name: bindings
+          mountPath: "/bindings/ca-certificates/type"
+          subPath: type
+          readOnly: true
+        - name: elasticsearch-certificate
+          mountPath: "/bindings/ca-certificates/es-root-ca.pem"
+          subPath: ca.crt
+          readOnly: true
+      volumes:
+        - name: bindings
+          configMap:
+            name: bindings-type
+        - name: elasticsearch-certificate
+          secret:
+            secretName: {{ required "elasticsearch.certificateSecretName must be set" (.Values.elasticsearch).certificateSecretName }}
+            optional: false
+      dnsConfig: {}
+      dnsPolicy: ClusterFirst
+      imagePullSecrets:
+      - name: {{ required "imagePullSecret must be set" .Values.imagePullSecret }}
+      restartPolicy: Always
+
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_role.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_role.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7b92028c82b9571f7018ac6e5ee99646649b319e
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_role.yaml
@@ -0,0 +1,39 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: ozgcloud-elasticsearch-operator-admin-secret-view-role
+  namespace: {{ required "elasticsearch.namespace must be set" (.Values.elasticsearch).namespace }}
+rules:
+  - apiGroups:
+      - ""
+    resourceNames:
+      - {{ required "elasticsearch.adminSecretName must be set" (.Values.elasticsearch).adminSecretName }}
+      - {{ required "elasticsearch.certificateSecretName must be set" (.Values.elasticsearch).certificateSecretName }}
+    resources:
+      - secrets
+    verbs:
+      - get
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_rolebinding.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_rolebinding.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f17704d5bd024797b643bcbf6e06d095ded6fa77
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_rolebinding.yaml
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: ozgcloud-elasticsearch-operator-admin-secret-view-role-binding
+  namespace: {{ required "elasticsearch.namespace must be set" (.Values.elasticsearch).namespace }}
+subjects:
+  - kind: ServiceAccount
+    name: ozgcloud-elasticsearch-operator-serviceaccount
+    namespace: {{ .Release.Namespace }}
+roleRef:
+  kind: Role
+  name: ozgcloud-elasticsearch-operator-admin-secret-view-role
+  apiGroup: rbac.authorization.k8s.io
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_edit_role.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_edit_role.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6d1374db245847f4616041d850d657f9b3f7bca0
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_edit_role.yaml
@@ -0,0 +1,18 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: ozgcloud-elasticsearch-operator-edit-role
+  labels:
+    app.kubernetes.io/name: {{ .Release.Name }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/component: ozgcloud-elasticsearch-operator
+rules:
+- apiGroups:
+  - operator.ozgcloud.de
+  resources:
+  - ozgcloudelasticsearchs
+  - ozgcloudelasticsearchs/status
+  - ozgcloudelasticsearchs/finalizers
+  verbs:
+  - patch
+  - update
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_edit_rolebinding.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_edit_rolebinding.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..856aa637f4164e537213ebe1415dbee0e1137e61
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_edit_rolebinding.yaml
@@ -0,0 +1,13 @@
+
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: ozgcloud-elasticsearch-operator-edit-role-binding
+subjects:
+  - kind: ServiceAccount
+    name: ozgcloud-elasticsearch-operator-serviceaccount
+    namespace: {{ .Release.Namespace }}
+roleRef:
+  kind: ClusterRole
+  name: ozgcloud-elasticsearch-operator-edit-role
+  apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_serviceaccount.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_serviceaccount.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a1441499d95af95ada1da6ab84ec0e3584abc095
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_serviceaccount.yaml
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: ozgcloud-elasticsearch-operator-serviceaccount
+  namespace: {{ .Release.Namespace }}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_view_role.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_view_role.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9e0698765648fa7e4d9e15c1256a8b1853833c68
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_view_role.yaml
@@ -0,0 +1,20 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: ozgcloud-elasticsearch-operator-view-role
+  labels:
+    app.kubernetes.io/name: {{ .Release.Name }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/component: ozgcloud-elasticsearch-operator
+rules:
+- apiGroups:
+  - operator.ozgcloud.de
+  resources:
+  - secrets
+  - ozgcloudelasticsearchs
+  - ozgcloudelasticsearchs/status
+  - ozgcloudelasticsearchs/finalizers
+  verbs:
+  - get
+  - list
+  - watch
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_view_rolebinding.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_view_rolebinding.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..150d7509b3921c1e5815e3a83e9eaf44d4f2258f
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/templates/rbac/ozgcloud_elasticsearch_operator_view_rolebinding.yaml
@@ -0,0 +1,13 @@
+
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: ozgcloud-elasticsearch-operator-view-role-binding
+subjects:
+  - kind: ServiceAccount
+    name: ozgcloud-elasticsearch-operator-serviceaccount
+    namespace: {{ .Release.Namespace }}
+roleRef:
+  kind: ClusterRole
+  name: ozgcloud-elasticsearch-operator-view-role
+  apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/helm/values.yaml b/ozgcloud-elasticsearch-operator/src/main/helm/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e4b3820a25467fb8faeec61a5b75be8d5ef06c99
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/helm/values.yaml
@@ -0,0 +1,8 @@
+
+image:
+  repo: docker.ozg-sh.de
+
+elasticsearch:
+    namespace: elastic-system
+    adminSecretName: ozg-search-cluster-es-elastic-user
+    certificateSecretName: ozg-search-cluster-es-http-ca-internal
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/CustomResourceStatus.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/CustomResourceStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ef66d5c501cc50d1d8dbf0e3ac323f0df118e99
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/CustomResourceStatus.java
@@ -0,0 +1,5 @@
+package de.ozgcloud.operator;
+
+public enum CustomResourceStatus {
+	OK, IN_PROGRESS, ERROR;
+}
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/ElasticsearchOperatorApplication.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/ElasticsearchOperatorApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..dde6f8261a674f6afd0d21ce3b395116dfa5acdd
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/ElasticsearchOperatorApplication.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.operator;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ElasticsearchOperatorApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(ElasticsearchOperatorApplication.class, args);
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/ElasticsearchReconciler.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/ElasticsearchReconciler.java
new file mode 100644
index 0000000000000000000000000000000000000000..24899d8fd8ce95377de40d38e4d0c676b7e1226b
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/ElasticsearchReconciler.java
@@ -0,0 +1,80 @@
+package de.ozgcloud.operator;
+
+import java.util.Base64;
+
+import org.apache.commons.collections.MapUtils;
+import org.springframework.stereotype.Component;
+
+import io.fabric8.kubernetes.api.model.Secret;
+import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+@RequiredArgsConstructor
+@ControllerConfiguration
+@Component
+public class ElasticsearchReconciler implements Reconciler<OzgCloudElasticsearchCustomResource>, Cleaner<OzgCloudElasticsearchCustomResource> {
+
+	private final OzgCloudElasticsearchService service;
+
+	@Override
+	public UpdateControl<OzgCloudElasticsearchCustomResource> reconcile(OzgCloudElasticsearchCustomResource resource,
+			Context<OzgCloudElasticsearchCustomResource> context) {
+		try {
+			var namespace = resource.getMetadata().getNamespace();
+			LOG.info("{}: Reconcile user", namespace);
+			var secret = service.getOrCreateCredentialSecret(resource, context);
+			service.createIndexIfMissing(namespace);
+			service.createSecurityRoleIfMissing(namespace);
+			service.createSecurityUserIfMissing(namespace, getPassword(secret));
+			service.createCertificateIfMissing(namespace);
+			LOG.info("{}: Reconcile user successful.", namespace);
+			return OzgCloudElasticsearchUpdateControlBuilder.fromResource(resource).withStatus(CustomResourceStatus.OK).build();
+		} catch (Exception exception) {
+			LOG.warn(resource.getMetadata().getNamespace() + ": Reconcile user failed.", exception);
+			return buildExceptionUpdateControl(resource, exception);
+		}
+	}
+
+	String getPassword(Secret secret) {
+		return decode(MapUtils.getString(secret.getData(), OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_PASSWORD_FIELD));
+	}
+
+	private String decode(String encodedPassword) {
+		try {
+			return new String(Base64.getDecoder().decode(encodedPassword));
+		} catch (Exception e) {
+			throw new RuntimeException("Could not decode password from secret.");
+		}
+	}
+
+	UpdateControl<OzgCloudElasticsearchCustomResource> buildExceptionUpdateControl(OzgCloudElasticsearchCustomResource resource,
+			Exception exception) {
+		return OzgCloudElasticsearchUpdateControlBuilder
+				.fromResource(resource)
+				.withStatus(CustomResourceStatus.ERROR)
+				.withReschedule(OperatorConfig.RECONCILER_RETRY_SECONDS_ON_ERROR)
+				.withMessage(exception.getMessage())
+				.build();
+	}
+
+	@Override
+	public DeleteControl cleanup(OzgCloudElasticsearchCustomResource resource, Context<OzgCloudElasticsearchCustomResource> context) {
+		var namespace = resource.getMetadata().getNamespace();
+		try {
+			service.deleteSecurityUserIfExists(namespace);
+			service.deleteSecurityRoleIfExists(namespace);
+			service.deleteIndexIfExists(namespace);
+			return DeleteControl.defaultDelete();
+		} catch (Exception e) {
+			LOG.warn(resource.getMetadata().getNamespace() + ": Could not cleanup elasticsearch resource.", e);
+			return DeleteControl.defaultDelete();
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/IndicesPrivilege.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/IndicesPrivilege.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7312ee608e05021f9892da996f20fb4e4fe1f8c
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/IndicesPrivilege.java
@@ -0,0 +1,14 @@
+package de.ozgcloud.operator;
+
+import lombok.Getter;
+
+public enum IndicesPrivilege {
+	ALL("all");
+	
+	@Getter
+	private String value;
+	
+	private IndicesPrivilege(String value) {
+		this.value = value;
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OperatorConfig.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OperatorConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..02d81e473448151ee2cbbb9cd0e6050107042dbf
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OperatorConfig.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.operator;
+
+import java.time.Duration;
+import java.util.List;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import io.javaoperatorsdk.operator.Operator;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+
+@Configuration
+public class OperatorConfig {
+
+	public static final Duration RECONCILER_RETRY_SECONDS = Duration.ofSeconds(20);
+	public static final Duration RECONCILER_RETRY_SECONDS_ON_ERROR = Duration.ofSeconds(60);
+
+	@Bean(initMethod = "start", destroyMethod = "stop")
+	@SuppressWarnings("rawtypes")
+	Operator operator(List<Reconciler> controllers) {
+		var operator = new Operator();
+		controllers.forEach(operator::register);
+		return operator;
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchCustomResource.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchCustomResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..933d76e0263fc49587c1a0a12b903eb2d54978e5
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchCustomResource.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.operator;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Kind;
+import io.fabric8.kubernetes.model.annotation.Plural;
+import io.fabric8.kubernetes.model.annotation.Singular;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Kind("OzgCloudElasticsearch")
+@Group("operator.ozgcloud.de")
+@Version("v1")
+@Singular("ozgcloudelasticsearch")
+@Plural("ozgcloudelasticsearchs")
+@SuppressWarnings("serial")
+class OzgCloudElasticsearchCustomResource extends CustomResource<OzgCloudElasticsearchSpec, OzgCloudElasticsearchCustomResourceStatus> implements Namespaced {
+}
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchCustomResourceStatus.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchCustomResourceStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..39b7f8f38d0e608870556f3f9f4e7aaeadba7c42
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchCustomResourceStatus.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.operator;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class OzgCloudElasticsearchCustomResourceStatus extends ObservedGenerationAwareStatus {
+
+	private CustomResourceStatus status;
+
+	private String message;
+}
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchProperties.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..6cb1e7326a03d09eb88758d8da976c064599eac0
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchProperties.java
@@ -0,0 +1,35 @@
+package de.ozgcloud.operator;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@ConfigurationProperties("ozgcloud.elasticsearch")
+@Configuration
+public class OzgCloudElasticsearchProperties {
+
+	private String secretCredentialsName;
+	private String certificateSecretName;
+
+	private OzgCloudElasticsearchServerProperties server;
+
+	@Getter
+	@Setter
+	public static class OzgCloudElasticsearchServerProperties {
+
+		private String namespace;
+		private String secretName;
+		private String secretDataKey;
+		private String secretCredentialsName;
+		private String host;
+		private int port;
+		private String scheme;
+		private String certificateNamespace;
+		private String certificateSecretName;
+		private String certificateSecretDataKey;
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchSecretHelper.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchSecretHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..18e2301050842f9615feac1ee7541ce52b7811b4
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchSecretHelper.java
@@ -0,0 +1,62 @@
+package de.ozgcloud.operator;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.stereotype.Component;
+
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Component
+class OzgCloudElasticsearchSecretHelper {
+
+	static final String SECRET_TYPE = "Opaque";
+
+	static final String CREDENTIAL_SECRET_ADDRESS_FIELD = "address";
+	static final String CREDENTIAL_SECRET_INDEX_FIELD = "index";
+	static final String CREDENTIAL_SECRET_PASSWORD_FIELD = "password";
+	static final String CREDENTIAL_SECRET_USERNAME_FIELD = "username";
+
+	static final String CERTIFICATE_SECRET_DATA_KEY = "ca.crt";
+
+	static final int PASSWORD_LENGTH = 15;
+
+	private final OzgCloudElasticsearchProperties properties;
+
+	public Secret buildCredentialSecret(String namespace, String name) {
+		return new SecretBuilder()
+				.withType(SECRET_TYPE)
+				.withMetadata(createMetaData(name, namespace))
+				.addToStringData(CREDENTIAL_SECRET_ADDRESS_FIELD, buildSecretAddress())
+				.addToStringData(CREDENTIAL_SECRET_INDEX_FIELD, namespace)
+				.addToStringData(CREDENTIAL_SECRET_PASSWORD_FIELD, generatePassword())
+				.addToStringData(CREDENTIAL_SECRET_USERNAME_FIELD, namespace)
+				.build();
+	}
+
+	private String buildSecretAddress() {
+		return String.format("%s:%s", properties.getServer().getHost(), properties.getServer().getPort());
+	}
+
+	private String generatePassword() {
+		return RandomStringUtils.randomAlphabetic(PASSWORD_LENGTH);
+	}
+
+	public Secret buildCertificateSecret(String namespace, String data) {
+		return new SecretBuilder()
+				.withType(SECRET_TYPE)
+				.withMetadata(createMetaData(properties.getCertificateSecretName(), namespace))
+				.addToData(CERTIFICATE_SECRET_DATA_KEY, data)
+				.build();
+	}
+
+	private ObjectMeta createMetaData(String name, String namespace) {
+		var metadata = new ObjectMeta();
+		metadata.setName(name);
+		metadata.setNamespace(namespace);
+
+		return metadata;
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchService.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7207e433215e4bbdbf7a5254b0aa9d16dfbee09
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchService.java
@@ -0,0 +1,138 @@
+package de.ozgcloud.operator;
+
+import java.util.Objects;
+
+import org.apache.commons.collections.MapUtils;
+import org.springframework.stereotype.Component;
+
+import de.ozgcloud.operator.PutRoleRequestData.IndicesPrivilegesData;
+import de.ozgcloud.operator.common.elasticsearch.ElasticsearchRemoteService;
+import de.ozgcloud.operator.common.kubernetes.KubernetesRemoteService;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.dsl.Resource;
+import io.fabric8.kubernetes.client.extension.ResourceAdapter;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+@RequiredArgsConstructor
+@Component
+public class OzgCloudElasticsearchService {
+
+	private final OzgCloudElasticsearchSecretHelper secretHelper;
+	private final OzgCloudElasticsearchProperties properties;
+
+	private final ElasticsearchRemoteService remoteService;
+
+	private final KubernetesRemoteService kubernetesService;
+
+	public Secret getOrCreateCredentialSecret(OzgCloudElasticsearchCustomResource resource, Context<OzgCloudElasticsearchCustomResource> context) {
+		try {
+			LOG.debug("{}: Get or create secret.", resource.getMetadata().getNamespace());
+			var namespace = resource.getMetadata().getNamespace();
+			var secretResource = getCredentialsSecretResource(namespace);
+
+			if (Objects.isNull(secretResource.get())) {
+				LOG.info("{}: Secret not exists, create one ...", resource.getMetadata().getNamespace());
+				createCredentialSecret(secretResource, namespace);
+				LOG.info("{}: Secret creation successful.", resource.getMetadata().getNamespace());
+			}
+			return secretResource.get();
+		} catch (Exception e) {
+			LOG.warn(resource.getMetadata().getNamespace() + ": Secret creation failed: ", e);
+			throw e;
+		}
+	}
+
+	private Resource<Secret> getCredentialsSecretResource(String namespace) {
+		return kubernetesService.getSecretResource(namespace, properties.getSecretCredentialsName());
+	}
+
+	private void createCredentialSecret(Resource<Secret> resource, String namespace) {
+		createAdapter(resource).create(secretHelper.buildCredentialSecret(namespace, properties.getSecretCredentialsName()));
+	}
+
+	public void createIndexIfMissing(String name) throws Exception {
+		LOG.debug("{}: Check elasticsearch index...", name);
+		if (!remoteService.existsIndex(name)) {
+			remoteService.createIndex(name);
+		}
+	}
+
+	public void createSecurityRoleIfMissing(String roleName) throws Exception {
+		LOG.debug("{}: Check elasticsearch role...", roleName);
+		if (!remoteService.existsSecurityRole(roleName)) {
+			remoteService.createSecurityRole(buildPutRoleRequestData(roleName));
+		}
+	}
+
+	PutRoleRequestData buildPutRoleRequestData(String roleName) {
+		return PutRoleRequestData.builder().name(roleName).indivesPrivilegesData(buildIndicesPrivilegesData(roleName)).build();
+	}
+
+	private IndicesPrivilegesData buildIndicesPrivilegesData(String roleName) {
+		return IndicesPrivilegesData.builder().names(roleName).privileges(IndicesPrivilege.ALL.getValue()).build();
+	}
+
+	public void createSecurityUserIfMissing(String namespace, String password) throws Exception {
+		LOG.debug("{}: Check elasticsearch user...", namespace);
+		if (!remoteService.existsSecurityUser(namespace)) {
+			remoteService.createSecurityUser(buildPutUserRequestData(namespace, password));
+		}
+	}
+
+	PutUserRequestData buildPutUserRequestData(String namespace, String password) {
+		return PutUserRequestData.builder().username(namespace).roles(namespace).password(password).build();
+	}
+
+	public void deleteSecurityUserIfExists(String userName) throws Exception {
+		LOG.debug("{}: Check delete elasticsearch user...", userName);
+		if (remoteService.existsSecurityUser(userName)) {
+			remoteService.deleteSecurityUser(userName);
+		}
+	}
+
+	public void deleteSecurityRoleIfExists(String roleName) throws Exception {
+		LOG.debug("{}: Check delete elasticsearch role...", roleName);
+		if (remoteService.existsSecurityRole(roleName)) {
+			remoteService.deleteSecurityRole(roleName);
+		}
+	}
+
+	public void deleteIndexIfExists(String indexName) throws Exception {
+		LOG.debug("{}: Check delete elasticsearch index ...", indexName);
+		if (remoteService.existsIndex(indexName)) {
+			remoteService.deleteIndex(indexName);
+		}
+	}
+
+	public void createCertificateIfMissing(String namespace) {
+		try {
+			LOG.debug("{}: Create certificate secret if missing...", namespace);
+			var secretResource = kubernetesService.getSecretResource(namespace, properties.getCertificateSecretName());
+
+			if (Objects.isNull(secretResource.get())) {
+				LOG.info("{}: Create certificate secret", namespace);
+				createCredentialSecret(namespace, secretResource);
+			}
+		} catch (Exception e) {
+			throw new RuntimeException("Certificate secret creation failed " + namespace);
+		}
+	}
+
+	void createCredentialSecret(String namespace, Resource<Secret> secretResource) {
+		var serverSecretResource = kubernetesService.getSecretResource(properties.getServer().getCertificateNamespace(),
+				properties.getServer().getCertificateSecretName());
+
+		createAdapter(secretResource).create(secretHelper.buildCertificateSecret(namespace, getSecretData(serverSecretResource.get())));
+	}
+
+	private String getSecretData(Secret secret) {
+		return MapUtils.getString(secret.getData(), properties.getServer().getCertificateSecretDataKey());
+	}
+
+	ResourceAdapter<Secret> createAdapter(Resource<Secret> resource) {
+		return new ResourceAdapter<>(resource);
+	}
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/KeycloakClientTest.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchSpec.java
similarity index 73%
rename from src/test/java/de/ozgcloud/operator/keycloak/KeycloakClientTest.java
rename to ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchSpec.java
index 9ac65f4fc0d630c18cf5c33d2040da38b78e3984..dd5d0ac27dc8922e049a9a2f469969a003e56840 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/KeycloakClientTest.java
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchSpec.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
  * Ministerpräsidenten des Landes Schleswig-Holstein
  * Staatskanzlei
  * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
@@ -21,14 +21,18 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-package de.ozgcloud.operator.keycloak;
+package de.ozgcloud.operator;
 
-import org.junit.jupiter.api.Test;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
-class KeycloakClientTest {
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Builder
+@JsonIgnoreProperties(ignoreUnknown = true)
+class OzgCloudElasticsearchSpec {
 
-	@Test
-	void shouldInitKeycloakClient() {
-		new KeycloakClient();
-	}
 }
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchUpdateControlBuilder.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchUpdateControlBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..22702728bc06914e35e1ba171b8194cbc1aedc78
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/OzgCloudElasticsearchUpdateControlBuilder.java
@@ -0,0 +1,61 @@
+package de.ozgcloud.operator;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+
+class OzgCloudElasticsearchUpdateControlBuilder {
+
+	private OzgCloudElasticsearchCustomResource resource;
+
+	private CustomResourceStatus status;
+	private Optional<String> message = Optional.empty();
+
+	private boolean reschedule = false;
+	private Duration scheduleDuration;
+
+	public OzgCloudElasticsearchUpdateControlBuilder(OzgCloudElasticsearchCustomResource resource) {
+		this.resource = resource;
+	}
+
+	public static OzgCloudElasticsearchUpdateControlBuilder fromResource(OzgCloudElasticsearchCustomResource resource) {
+		return new OzgCloudElasticsearchUpdateControlBuilder(resource);
+	}
+
+	public OzgCloudElasticsearchUpdateControlBuilder withStatus(CustomResourceStatus status) {
+		this.status = status;
+		return this;
+	}
+
+	public OzgCloudElasticsearchUpdateControlBuilder withMessage(String message) {
+		this.message = Optional.ofNullable(message);
+		return this;
+	}
+
+	public OzgCloudElasticsearchUpdateControlBuilder withReschedule(Duration duration) {
+		this.reschedule = true;
+		this.scheduleDuration = duration;
+		return this;
+	}
+
+	public UpdateControl<OzgCloudElasticsearchCustomResource> build() {
+		resource.setStatus(buildElasticCustomResourceStatus());
+
+		return buildUpdateControl();
+	}
+
+	private OzgCloudElasticsearchCustomResourceStatus buildElasticCustomResourceStatus() {
+		var userStatus = OzgCloudElasticsearchCustomResourceStatus.builder().status(status);
+		message.ifPresent(userStatus::message);
+
+		return userStatus.build();
+	}
+
+	private UpdateControl<OzgCloudElasticsearchCustomResource> buildUpdateControl() {
+		if (reschedule) {
+			return UpdateControl.updateStatus(resource).rescheduleAfter(scheduleDuration);
+		}
+		return UpdateControl.updateStatus(resource);
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/PutRoleRequestData.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/PutRoleRequestData.java
new file mode 100644
index 0000000000000000000000000000000000000000..276a2c0ba05d7fe17b450f9fe98e157b08045559
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/PutRoleRequestData.java
@@ -0,0 +1,23 @@
+package de.ozgcloud.operator;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@Builder
+@Getter
+@Setter
+public class PutRoleRequestData {
+	
+	private String name;
+	private IndicesPrivilegesData indivesPrivilegesData;
+	
+	@Builder
+	@Getter
+	@Setter
+	public static class IndicesPrivilegesData {
+		
+		private String names;
+		private String privileges;
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/PutUserRequestData.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/PutUserRequestData.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ca925bd250d71679f88da96e565e5667cec9061
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/PutUserRequestData.java
@@ -0,0 +1,15 @@
+package de.ozgcloud.operator;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@Builder
+@Getter
+@Setter
+public class PutUserRequestData {
+	
+	private String username;
+	private String roles;
+	private String password;
+}
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchClientConfiguration.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchClientConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..aa25d5e3454e672c0d6f7fee59472a714f546d0d
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchClientConfiguration.java
@@ -0,0 +1,88 @@
+package de.ozgcloud.operator.common.elasticsearch;
+
+import java.util.Base64;
+
+import org.apache.commons.collections.MapUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.elasticsearch.client.RestClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.json.jackson.JacksonJsonpMapper;
+import co.elastic.clients.transport.rest_client.RestClientTransport;
+import de.ozgcloud.operator.OzgCloudElasticsearchProperties;
+import de.ozgcloud.operator.common.kubernetes.KubernetesRemoteService;
+import io.fabric8.kubernetes.api.model.Secret;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+@Configuration
+public class ElasticsearchClientConfiguration {
+
+	@Autowired
+	private KubernetesRemoteService kubernetesService;
+	@Autowired
+	private OzgCloudElasticsearchProperties elasticSearchProperties;
+
+	@Bean
+	@Scope("singleton")
+	ElasticsearchClient createElasticsearchClient() {
+		LOG.info("Create elasticsearch client...");
+		var credentialsProvider = createCredentialsProvider(elasticSearchProperties.getServer().getSecretDataKey(), getPassword());
+		var restClient = buildRestClient(credentialsProvider);
+		var transport = createRestClientTransport(restClient);
+		return new ElasticsearchClient(transport);
+	}
+
+	private BasicCredentialsProvider createCredentialsProvider(String userName, String password) {
+		var credentialsProvider = new BasicCredentialsProvider();
+		credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password));
+		return credentialsProvider;
+	}
+
+	private RestClient buildRestClient(BasicCredentialsProvider credentialsProvider) {
+		return RestClient.builder(createHttpHost())
+				.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider))
+				.build();
+	}
+
+	private HttpHost createHttpHost() {
+		LOG.info(String.format("ElasticSearch config: host: %s with port: %s and scheme: %s", elasticSearchProperties.getServer().getHost(),
+				elasticSearchProperties.getServer().getPort(), elasticSearchProperties.getServer().getScheme()));
+		return new HttpHost(elasticSearchProperties.getServer().getHost(), elasticSearchProperties.getServer().getPort(),
+				elasticSearchProperties.getServer().getScheme());
+	}
+
+	private RestClientTransport createRestClientTransport(RestClient restClient) {
+		return new RestClientTransport(restClient, new JacksonJsonpMapper());
+	}
+
+	String getPassword() {
+		LOG.debug(String.format("get password from secret: %s in namespace %s", elasticSearchProperties.getServer().getSecretName(),
+				elasticSearchProperties.getServer().getNamespace()));
+		var secret = getCredentialsSecret();
+		var password = getPasswordFromSecret(secret);
+		return password;
+	}
+
+	private Secret getCredentialsSecret() {
+		return kubernetesService
+				.getSecretResource(elasticSearchProperties.getServer().getNamespace(), elasticSearchProperties.getServer().getSecretName()).get();
+	}
+
+	private String getPasswordFromSecret(Secret secret) {
+		var encodedPassword = MapUtils.getString(secret.getData(), elasticSearchProperties.getServer().getSecretDataKey());
+		return decode(encodedPassword, secret);
+	}
+
+	private String decode(String encodedPassword, Secret secret) {
+		return new String(Base64.getDecoder().decode(encodedPassword));
+	}
+
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteService.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteService.java
new file mode 100644
index 0000000000000000000000000000000000000000..12c4c5db85dff6deeb1d815386f13b54823d6b38
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteService.java
@@ -0,0 +1,132 @@
+package de.ozgcloud.operator.common.elasticsearch;
+
+import java.io.IOException;
+
+import org.springframework.stereotype.Component;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.elasticsearch._types.ElasticsearchException;
+import co.elastic.clients.elasticsearch.security.IndicesPrivileges;
+import co.elastic.clients.elasticsearch.security.PutRoleRequest;
+import co.elastic.clients.elasticsearch.security.PutUserRequest;
+import de.ozgcloud.operator.PutRoleRequestData;
+import de.ozgcloud.operator.PutUserRequestData;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+@RequiredArgsConstructor
+@Component
+public class ElasticsearchRemoteService {
+
+	private final ElasticsearchClient client;
+
+	public boolean existsIndex(String index) throws Exception {
+		try {
+			LOG.debug("{}: Test if elasticsearch index exits.", index);
+			var exists = client.indices().exists(builder -> builder.index(index)).value();
+			LOG.debug("{}: Elasticsearch index exists: {}", index, exists);
+			return exists;
+		} catch (ElasticsearchException | IOException e) {
+			throw new RuntimeException("Error checking index '" + index, e);
+		}
+	}
+
+	public void createIndex(String indexName) throws Exception {
+		try {
+			LOG.info("{}: Create elasticsearch index", indexName);
+			client.indices().create(builder -> builder.index(indexName));
+			LOG.info("{}: Create elasticsearch index successful", indexName);
+		} catch (Exception e) {
+			throw new RuntimeException("Create elasticsearch index " + indexName + "failed.", e);
+		}
+	}
+
+	public boolean existsSecurityRole(String roleName) throws Exception {
+		return !client.security().getRole(builder -> builder.name(roleName)).result().isEmpty();
+	}
+
+	public void createSecurityRole(PutRoleRequestData requestData) throws Exception {
+		try {
+			LOG.info("{}: Create elasticsearch role ", requestData.getName());
+			client.security().putRole(createPutRoleRequest(requestData));
+			LOG.info("{}: Create elasticsearch role successful", requestData.getName());
+		} catch (Exception e) {
+			throw new RuntimeException("Create elasticsearch role " + requestData.getName() + "failed.", e);
+		}
+	}
+
+	PutRoleRequest createPutRoleRequest(PutRoleRequestData requestData) {
+		return PutRoleRequest.of(requestBuilder -> buildRequest(requestBuilder, requestData));
+	}
+
+	private PutRoleRequest.Builder buildRequest(PutRoleRequest.Builder requestBuilder, PutRoleRequestData requestData) {
+		requestBuilder.name(requestData.getName());
+		requestBuilder.indices(builder -> buildIndicesPrivilegesRequest(builder, requestData));
+
+		return requestBuilder;
+	}
+
+	private IndicesPrivileges.Builder buildIndicesPrivilegesRequest(IndicesPrivileges.Builder builder, PutRoleRequestData requestData) {
+		builder.names(requestData.getIndivesPrivilegesData().getNames());
+		builder.privileges(requestData.getIndivesPrivilegesData().getPrivileges());
+
+		return builder;
+	}
+
+	public boolean existsSecurityUser(String userName) throws Exception {
+		return !client.security().getUser(builder -> builder.username(userName)).result().isEmpty();
+	}
+
+	public void createSecurityUser(PutUserRequestData requestData) throws Exception {
+		try {
+			LOG.info("{}: Create elasticsearch user", requestData.getUsername());
+			client.security().putUser(createPutUserRequest(requestData));
+			LOG.info("{}: Create elasticsearch user successful", requestData.getUsername());
+		} catch (Exception e) {
+			throw new RuntimeException("Create elasticsearch user " + requestData.getUsername() + "failed.", e);
+		}
+	}
+
+	PutUserRequest createPutUserRequest(PutUserRequestData requestData) {
+		return PutUserRequest.of(requestBuilder -> buildPutUserRequest(requestBuilder, requestData));
+	}
+
+	private PutUserRequest.Builder buildPutUserRequest(PutUserRequest.Builder builder, PutUserRequestData requestData) {
+		builder.username(requestData.getUsername());
+		builder.roles(requestData.getRoles());
+		builder.password(requestData.getPassword());
+
+		return builder;
+	}
+
+	public void deleteIndex(String indexName) throws Exception {
+		try {
+			LOG.info("{}: Delete elasticsearch index", indexName);
+			client.indices().delete(builder -> builder.index(indexName));
+			LOG.info("{}: Delete elasticsearch index successful", indexName);
+		} catch (Exception e) {
+			throw new RuntimeException("Delete elasticsearch index " + indexName + "failed.", e);
+		}
+	}
+
+	public void deleteSecurityRole(String roleName) throws Exception {
+		try {
+			LOG.info("{}: Delete elasticsearch role", roleName);
+			client.security().deleteRole(builder -> builder.name(roleName));
+			LOG.info("{}: Delete elasticsearch role successful", roleName);
+		} catch (Exception e) {
+			throw new RuntimeException("Delete elasticsearch role " + roleName + "failed.", e);
+		}
+	}
+
+	public void deleteSecurityUser(String userName) throws Exception {
+		try {
+			LOG.info("{}: Delete elasticsearch user", userName);
+			client.security().deleteUser(builder -> builder.username(userName));
+			LOG.info("{}: Delete elasticsearch user successful", userName);
+		} catch (Exception e) {
+			throw new RuntimeException("Delete elasticsearch user " + userName + "failed.", e);
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteService.java b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteService.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8d4bd2e85fc7f380f650bc974775984809b9b80
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteService.java
@@ -0,0 +1,19 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import org.springframework.stereotype.Component;
+
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.dsl.Resource;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Component
+public class KubernetesRemoteService {
+
+	private final KubernetesClient client;
+
+	public Resource<Secret> getSecretResource(String namespace, String name) {
+		return client.secrets().inNamespace(namespace).withName(name);
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/main/resources/application.yml b/ozgcloud-elasticsearch-operator/src/main/resources/application.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8fe6660834d7b720fc93b6aad55cbdc3f8b120f6
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/main/resources/application.yml
@@ -0,0 +1,37 @@
+ozgcloud:
+  elasticsearch:
+    secretCredentialsName: elasticsearch-credentials
+    certificateSecretName: elasticsearch-certificate
+    server:
+      namespace: elastic-system
+      secretName: ozg-search-cluster-es-elastic-user
+      secretDataKey: elastic
+      host: ozg-search-cluster-es-http.${ozgcloud.elasticsearch.server.namespace}
+      port: 9200
+      scheme: https
+      certificateNamespace: ozgcloud-elasticsearch-operator
+      certificateSecretName: ozg-search-cluster-es-http-ca-internal
+      certificateSecretDataKey: ca.crt
+
+management:
+  server:
+    port: 8081
+  health:
+    livenessState:
+      enabled: true
+    readinessState:
+      enabled: true
+  endpoint:
+    health:
+      group:
+        exploratory:
+          include: livenessState,readinessState,ping
+          show-details: always
+      probes:
+        enabled: true
+    prometheus:
+      enabled: true
+  endpoints:
+    web:
+      exposure:
+        include: "*"
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/configmap_bindings_type_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/configmap_bindings_type_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..83ece1c08bd44bc06afaa631215dcf699fd33239
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/configmap_bindings_type_test.yaml
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: Certificate ConfigMap Binding
+release:
+  namespace: sh-helm-test
+templates:
+  - templates/configmap_bindings_type.yaml
+tests:
+  - it: validate configMap values
+    asserts:
+      - isKind:
+          of: ConfigMap
+      - equal:
+          path: metadata.name
+          value: bindings-type
+      - equal:
+          path: metadata.namespace
+          value: sh-helm-test
+      - equal:
+          path: data
+          value:
+            type: | 
+              ca-certificates
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/crd/operator.ozgcloud.de_OzgCloudElasticsearch_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/crd/operator.ozgcloud.de_OzgCloudElasticsearch_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6efab0f76873510049e19e5804665d807cf26965
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/crd/operator.ozgcloud.de_OzgCloudElasticsearch_test.yaml
@@ -0,0 +1,51 @@
+suite: operator.ozgcloud.de_OzgCloudElasticsearch test
+templates:
+  - templates/crd/operator.ozgcloud.de_OzgCloudElasticsearch.yaml
+tests:
+  - it: should have apiVersion
+    asserts: 
+      - equal:
+          path: apiVersion
+          value: apiextensions.k8s.io/v1
+  - it: should have isKind of
+    asserts:
+      - isKind:
+          of: CustomResourceDefinition
+
+  - it: should have metadata name
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloudelasticsearchs.operator.ozgcloud.de
+
+  - it: should have spec group
+    asserts:
+      - equal:
+          path: spec.group
+          value: operator.ozgcloud.de
+  - it: should have spec names kind
+    asserts: 
+      - equal:
+          path: spec.names.kind
+          value: OzgCloudElasticsearch
+  - it: should have spec names listKind
+    asserts: 
+      - equal:
+          path: spec.names.listKind
+          value: OzgCloudElasticsearchList
+  - it: should have spec names plural
+    asserts: 
+      - equal:
+          path: spec.names.plural
+          value: ozgcloudelasticsearchs
+  - it: should have spec names singular
+    asserts: 
+      - equal:
+          path: spec.names.singular
+          value: ozgcloudelasticsearch
+
+  - it: should have spec scope
+    asserts: 
+      - equal:
+          path: spec.scope
+          value: Namespaced
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/crd/operator.ozgcloud.de_OzgCloudElasticsearch_versions_v1_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/crd/operator.ozgcloud.de_OzgCloudElasticsearch_versions_v1_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8e66bce6f29772a0e7051e326aa106a396d182b6
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/crd/operator.ozgcloud.de_OzgCloudElasticsearch_versions_v1_test.yaml
@@ -0,0 +1,99 @@
+suite: operator.ozgcloud.de_OzgCloudElasticsearch versions v1 test
+templates:
+  - templates/crd/operator.ozgcloud.de_OzgCloudElasticsearch.yaml
+tests:
+  - it: should have versions name
+    asserts: 
+      - equal:
+          path: spec.versions[0].name
+          value: v1
+  - it: should have versions schema description
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.description
+          value: OzgCloudElasticsearch is the Schema for the elasticsearch API
+  - it: should have versions schema type
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.type
+          value: object
+          
+  - it: should have versions schema apiVersion property type
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.apiVersion.type
+          value: string
+  - it: should have versions schema apiVersion property description
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.apiVersion.description
+          value: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+              
+  - it: should have versions schema kind property type
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.kind.type
+          value: string
+  - it: should have versions schema kind property description
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.kind.description
+          value: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+              
+  - it: should have versions schema metadata
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.metadata.type
+          value: object
+          
+  - it: should have versions schema spec description
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.spec.description
+          value: Spec defines the desired state of Elasticsearch
+  - it: should have versions schema spec type
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.spec.type
+          value: object
+  - it: should have versions schema spec preserve unknown fields
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.spec.x-kubernetes-preserve-unknown-fields
+          value: true
+          
+  - it: should have versions schema status description
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.status.description
+          value: Status defines the observed state of Elasticsearch
+  - it: should have versions schema status type
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.status.type
+          value: object
+  - it: should have versions schema status preserve unknown fields
+    asserts: 
+      - equal:
+          path: spec.versions[0].schema.openAPIV3Schema.properties.status.x-kubernetes-preserve-unknown-fields
+          value: true
+          
+  - it: should have versions served
+    asserts: 
+      - equal:
+          path: spec.versions[0].served
+          value: true
+  - it: should have versions storage
+    asserts: 
+      - equal:
+          path: spec.versions[0].storage
+          value: true
+  - it: should have versions subresources statis
+    asserts: 
+      - equal:
+          path: spec.versions[0].subresources.status
+          value: {}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_container_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_container_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..462e66f312dc001e037e1684545c168375caead4
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_container_test.yaml
@@ -0,0 +1,93 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment container test
+release:
+  name: elastic-test-operator
+  namespace: sh-helm-test
+templates:
+  - deployment.yaml
+tests:
+  - it: validate image type and container image
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.containers[0].name
+          value: ozgcloud-elasticsearch-operator
+      - equal:
+          path: spec.template.spec.containers[0].image
+          value: docker.ozg-sh.de/hase:latest
+      - equal:
+          path: spec.template.spec.containers[0].imagePullPolicy
+          value: Always
+
+  - it: validate health checks
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.containers[0].readinessProbe
+          value: 
+            failureThreshold: 3
+            httpGet:
+              path: /actuator/health/readiness
+              port: 8081
+              scheme: HTTP
+            periodSeconds: 10
+            successThreshold: 1
+            timeoutSeconds: 3
+      - equal:
+          path: spec.template.spec.containers[0].startupProbe
+          value: 
+            failureThreshold: 10
+            httpGet:
+              path: /actuator/health/readiness
+              port: 8081
+              scheme: HTTP
+            initialDelaySeconds: 30
+            periodSeconds: 5
+            successThreshold: 1
+            timeoutSeconds: 5
+
+  - it: validate security context
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.containers[0].securityContext
+          value: 
+            allowPrivilegeEscalation: false
+            privileged: false
+            readOnlyRootFilesystem: false
+            runAsNonRoot: true
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_env_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_env_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0179c95b140f6f2cbdf14fb0ebb7a0576da5f807
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_env_test.yaml
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: test environments
+templates:
+  - templates/deployment.yaml
+tests:
+  - it: check customList
+    template: deployment.yaml
+    set:
+      env.customList:
+        - name: my_test_environment_name
+          value: "A test value"
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - contains:
+          path: spec.template.spec.containers[0].env
+          content:
+            name: my_test_environment_name
+            value: "A test value"
+
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_matchlabels_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_matchlabels_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6b20b3d2f94de47d71794c8110acfd824b48ff57
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_matchlabels_test.yaml
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment matchlabels
+release:
+  name: ozgcloud-elasticsearch-operator
+  namespace: sh-helm-test
+templates:
+  - templates/deployment.yaml
+tests:
+  - it: check matchlabels
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.selector.matchLabels.[app.kubernetes.io/name]
+          value: ozgcloud-elasticsearch-operator
+      - equal:
+          path: spec.selector.matchLabels.[app.kubernetes.io/namespace]
+          value: sh-helm-test
+
+      - equal:
+          path: spec.template.metadata.labels.[app.kubernetes.io/name]
+          value: ozgcloud-elasticsearch-operator
+      - equal:
+          path: spec.template.metadata.labels.[app.kubernetes.io/namespace]
+          value: sh-helm-test
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_metadata_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_metadata_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9679c3b4b5b17e6fbd6b920f0fc14bcf3b9c9464
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_metadata_test.yaml
@@ -0,0 +1,70 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment test metadata
+release:
+  name: ozgcloud-elasticsearch-operator
+  namespace: sh-helm-test
+templates:
+  - templates/deployment.yaml
+tests:
+  - it: check metadata labels
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/instance]
+          value: ozgcloud-elasticsearch-operator
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/name]
+          value: ozgcloud-elasticsearch-operator
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/part-of]
+          value: ozg
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/namespace]
+          value: sh-helm-test
+  - it: check metadata name
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator
+  - it: check metadata namespace
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: metadata.namespace
+          value: sh-helm-test
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_pull_secret_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_pull_secret_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..983d2758b0eb06d149ad1b11264cf07890566517
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_pull_secret_test.yaml
@@ -0,0 +1,42 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment pull secret
+release:
+  name: ozgcloud-elasticsearch-operator
+  namespace: sh-helm-test
+templates:
+  - deployment.yaml
+tests:
+  - it: validate image pull secret resource name
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.imagePullSecrets[0].name
+          value: imagePullSecret
+
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_resources_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_resources_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5e871d26719f7d886e29729a2134f75419fd4aae
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_resources_test.yaml
@@ -0,0 +1,67 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment resources test
+release:
+  name: afm-adapter
+templates:
+  - templates/deployment.yaml
+tests:
+  - it: test resources
+    set:
+      resources:
+        limits:
+          cpu: "11m"
+          memory: "22Mi"
+        requests:
+          cpu: "33m"
+          memory: "44Mi"
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.containers[0].resources.limits.cpu
+          value: 11m
+      - equal:
+          path: spec.template.spec.containers[0].resources.limits.memory
+          value: 22Mi
+      - equal:
+          path: spec.template.spec.containers[0].resources.requests.cpu
+          value: 33m
+      - equal:
+          path: spec.template.spec.containers[0].resources.requests.memory
+          value: 44Mi
+
+  - it: test empty resources
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - isEmpty:
+          path: spec.template.spec.containers[0].resources
+
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_template_spec_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_template_spec_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8ec9dbacd2267f9b2579ca4c5e2e57330edf10f6
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_template_spec_test.yaml
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment templace spec test
+release:
+  name: elastic-test-operator
+  namespace: sh-helm-test
+templates:
+  - deployment.yaml
+tests:
+  - it: validate serviceaccount name
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.serviceAccountName
+          value: ozgcloud-elasticsearch-operator-serviceaccount
+    
+  - it: validate restartPolicy
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.restartPolicy
+          value: Always
+
+  - it: validate dns config
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - equal:
+          path: spec.template.spec.dnsConfig
+          value: {}
+      - equal:
+          path: spec.template.spec.dnsPolicy
+          value: ClusterFirst
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_type_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_type_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e992e29f94ea59c82a59b616a7db948864061c56
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_type_test.yaml
@@ -0,0 +1,43 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment type test
+release:
+  name: elastic-test-operator
+  namespace: sh-helm-test
+templates:
+  - deployment.yaml
+tests:
+  - it: validate template type and api Version
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - isKind:
+          of: Deployment
+      - isAPIVersion:
+          of: apps/v1
+
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/deployment_volumes.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_volumes.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..15ce39bf9a4c2bafc6f15c13bb54a9e86c3e4c37
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/deployment_volumes.yaml
@@ -0,0 +1,86 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: deployment type test
+release:
+  name: elastic-test-operator
+  namespace: sh-helm-test
+templates:
+  - deployment.yaml
+tests:
+  - it: should create SERVICE_BINDING_ROOT
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - contains:
+          path: spec.template.spec.containers[0].env
+          content:
+            name: SERVICE_BINDING_ROOT
+            value: "/bindings"
+
+  - it: should create volumes
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - contains:
+          path: spec.template.spec.volumes
+          content:  
+              name: bindings
+              configMap:
+                name: bindings-type
+      - contains:
+          path: spec.template.spec.volumes
+          content:  
+              name: elasticsearch-certificate
+              secret:
+                secretName: ozg-search-cluster-es-http-ca-internal
+                optional: false
+
+  - it: should create volumeMounts
+    set:
+      image:
+        name: hase
+        tag: latest
+      imagePullSecret: imagePullSecret
+    asserts:
+      - contains:
+          path: spec.template.spec.containers[0].volumeMounts
+          content:  
+              name: bindings
+              mountPath: "/bindings/ca-certificates/type"
+              subPath: type
+              readOnly: true
+      - contains:
+          path: spec.template.spec.containers[0].volumeMounts
+          content:  
+              name: elasticsearch-certificate
+              mountPath: "/bindings/ca-certificates/es-root-ca.pem"
+              subPath: ca.crt
+              readOnly: true
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/linter_values.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/linter_values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..aab55aca7ad1c21dbc0730aa82a28e0b3d8d872b
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/linter_values.yaml
@@ -0,0 +1,11 @@
+
+image:
+  name: test
+  tag: latest
+
+imagePullSecret: "docker-secret"
+
+env:
+  customList:
+    - name: my_test_environment_name
+      value: "A test value"
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_role_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_role_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9604c57257fe009fdb987fb9ab9ca673da1c1344
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_role_test.yaml
@@ -0,0 +1,69 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: Elasticsearch admin secret view role test
+release:
+  name: ozgcloud-elasticsearch-operator
+  namespace: test-namespace
+templates:
+  - templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_role.yaml
+tests:
+  - it: should have apiVersion
+    asserts: 
+      - equal:
+          path: apiVersion
+          value: rbac.authorization.k8s.io/v1
+  - it: should have isKind of
+    asserts:
+      - isKind:
+          of: Role
+
+  - it: should have metadata name
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator-admin-secret-view-role
+  - it: should have metadata namespace
+    asserts:
+      - equal:
+          path: metadata.namespace
+          value: elastic-system
+  
+
+  - it: should have rules for ozgcloudelasticsearches resource
+    asserts:
+      - equal:
+          path: rules
+          value:
+            - apiGroups:
+                - ""
+              resourceNames:
+                - ozg-search-cluster-es-elastic-user
+                - ozg-search-cluster-es-http-ca-internal
+              resources:
+                - secrets
+              verbs:
+                - get
+      
+      
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_rolebinding_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_rolebinding_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9cde6cc508e89f8f45d7f5afa55737d94783c826
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_rolebinding_test.yaml
@@ -0,0 +1,64 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: Elasticsearch admin secret view rolebinding test
+release:
+  name: ozgcloud-elasticsearch-operator
+  namespace: sh-helm-test
+templates:
+  - templates/rbac/ozgcloud_elasticsearch_operator_admin_secret_view_rolebinding.yaml
+tests:
+  - it: should have apiVersion
+    asserts: 
+      - equal:
+          path: apiVersion
+          value: rbac.authorization.k8s.io/v1
+  - it: should have isKind of
+    asserts:
+      - isKind:
+          of: RoleBinding
+
+  - it: should have metadata name
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator-admin-secret-view-role-binding
+
+  - it: should have subjects
+    asserts:
+      - equal:
+          path: subjects
+          value:
+            - kind: ServiceAccount
+              name: ozgcloud-elasticsearch-operator-serviceaccount
+              namespace: sh-helm-test
+
+  - it: should have roleRef
+    asserts:
+      - equal:
+          path: roleRef
+          value:
+            kind: Role
+            name: ozgcloud-elasticsearch-operator-admin-secret-view-role
+            apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_edit_role_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_edit_role_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..078cf0a0cf2fd7498271d63ca3fb6e686b85e71a
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_edit_role_test.yaml
@@ -0,0 +1,53 @@
+suite: elasticsearch_edit_role test
+release:
+  name: release-name
+templates:
+  - templates/rbac/ozgcloud_elasticsearch_operator_edit_role.yaml
+tests:
+  - it: should have apiVersion
+    asserts: 
+      - equal:
+          path: apiVersion
+          value: rbac.authorization.k8s.io/v1
+  - it: should have isKind of
+    asserts:
+      - isKind:
+          of: ClusterRole
+
+  - it: should have metadata name
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator-edit-role
+  - it: should have metadata labels name
+    asserts: 
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/name]
+          value: release-name
+  - it: should have metadata labels instance
+    asserts: 
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/instance]
+          value: release-name
+  - it: should have metadata labels component
+    asserts: 
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/component]
+          value: ozgcloud-elasticsearch-operator
+
+  - it: should have rules for ozgcloudelasticsearchs resource
+    asserts:
+      - contains:
+          path: rules
+          content:
+            apiGroups:
+              - operator.ozgcloud.de
+            resources:
+              - ozgcloudelasticsearchs
+              - ozgcloudelasticsearchs/status
+              - ozgcloudelasticsearchs/finalizers
+            verbs:
+              - patch
+              - update
+      
+      
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_edit_rolebinding_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_edit_rolebinding_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..25e7418ed1558fa4c2d0ca7274c05442477a70d0
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_edit_rolebinding_test.yaml
@@ -0,0 +1,41 @@
+
+
+suite: elasticsearch_view_rolebinding test
+release:
+  namespace: sh-helm-test
+templates:
+  - templates/rbac/ozgcloud_elasticsearch_operator_view_rolebinding.yaml
+tests:
+  - it: should have apiVersion
+    asserts: 
+      - equal:
+          path: apiVersion
+          value: rbac.authorization.k8s.io/v1
+  - it: should have isKind of
+    asserts:
+      - isKind:
+          of: ClusterRoleBinding
+
+  - it: should have metadata name
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator-view-role-binding
+
+  - it: should have subjects
+    asserts:
+      - equal:
+          path: subjects
+          value:
+            - kind: ServiceAccount
+              name: ozgcloud-elasticsearch-operator-serviceaccount
+              namespace: sh-helm-test
+
+  - it: should have roleRef
+    asserts:
+      - equal:
+          path: roleRef
+          value:
+            kind: ClusterRole
+            name: ozgcloud-elasticsearch-operator-view-role
+            apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_serviceaccount_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_serviceaccount_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c2a93ca53960bf57424ff9d3f153e5cfbecb9a43
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_serviceaccount_test.yaml
@@ -0,0 +1,41 @@
+#
+# Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den
+# Ministerpräsidenten des Landes Schleswig-Holstein
+# Staatskanzlei
+# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+#
+# Lizenziert unter der EUPL, Version 1.2 oder - sobald
+# diese von der Europäischen Kommission genehmigt wurden -
+# Folgeversionen der EUPL ("Lizenz");
+# Sie dürfen dieses Werk ausschließlich gemäß
+# dieser Lizenz nutzen.
+# Eine Kopie der Lizenz finden Sie hier:
+#
+# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+#
+# Sofern nicht durch anwendbare Rechtsvorschriften
+# gefordert oder in schriftlicher Form vereinbart, wird
+# die unter der Lizenz verbreitete Software "so wie sie
+# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+# ausdrücklich oder stillschweigend - verbreitet.
+# Die sprachspezifischen Genehmigungen und Beschränkungen
+# unter der Lizenz sind dem Lizenztext zu entnehmen.
+#
+
+suite: ServiceAccount test
+release:
+  name: ozgcloud-elasticsearch-operator
+  namespace: test-namespace
+templates:
+  - templates/rbac/ozgcloud_elasticsearch_operator_serviceaccount.yaml
+tests:
+  - it: test metadata
+    asserts:
+      - isKind:
+          of: ServiceAccount
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator-serviceaccount
+      - equal:
+          path: metadata.namespace
+          value: test-namespace
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_view_role_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_view_role_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5112d901aaeeaa0ca71735fcc76417364cbaafac
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_view_role_test.yaml
@@ -0,0 +1,53 @@
+suite: elasticsearch_view_role test
+release:
+  name: release-name
+templates:
+  - templates/rbac/ozgcloud_elasticsearch_operator_view_role.yaml
+tests:
+  - it: should have apiVersion
+    asserts: 
+      - equal:
+          path: apiVersion
+          value: rbac.authorization.k8s.io/v1
+  - it: should have isKind of
+    asserts:
+      - isKind:
+          of: ClusterRole
+
+  - it: should have metadata name
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator-view-role
+  - it: should have metadata labels name
+    asserts: 
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/name]
+          value: release-name
+  - it: should have metadata labels instance
+    asserts: 
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/instance]
+          value: release-name
+  - it: should have metadata labels component
+    asserts: 
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/component]
+          value: ozgcloud-elasticsearch-operator
+
+  - it: should have rules for ozgcloudelasticsearchs resource
+    asserts:
+      - contains:
+          path: rules
+          content:
+              apiGroups:
+                - operator.ozgcloud.de
+              resources:
+                - secrets
+                - ozgcloudelasticsearchs
+                - ozgcloudelasticsearchs/status
+                - ozgcloudelasticsearchs/finalizers
+              verbs:
+                - get
+                - list
+                - watch
diff --git a/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_view_rolebinding_test.yaml b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_view_rolebinding_test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e5191b1292c29fbe58af8360d27f0f954487980d
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/helm/rbac/ozgcloud_elasticsearch_operator_view_rolebinding_test.yaml
@@ -0,0 +1,41 @@
+
+
+suite: elasticsearch_edit_rolebinding test
+release:
+  namespace: sh-helm-test
+templates:
+  - templates/rbac/ozgcloud_elasticsearch_operator_edit_rolebinding.yaml
+tests:
+  - it: should have apiVersion
+    asserts: 
+      - equal:
+          path: apiVersion
+          value: rbac.authorization.k8s.io/v1
+  - it: should have isKind of
+    asserts:
+      - isKind:
+          of: ClusterRoleBinding
+
+  - it: should have metadata name
+    asserts:
+      - equal:
+          path: metadata.name
+          value: ozgcloud-elasticsearch-operator-edit-role-binding
+
+  - it: should have subjects
+    asserts:
+      - equal:
+          path: subjects
+          value:
+            - kind: ServiceAccount
+              name: ozgcloud-elasticsearch-operator-serviceaccount
+              namespace: sh-helm-test
+
+  - it: should have roleRef
+    asserts:
+      - equal:
+          path: roleRef
+          value:
+            kind: ClusterRole
+            name: ozgcloud-elasticsearch-operator-edit-role
+            apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchCustomResourceStatusTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchCustomResourceStatusTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3e4eb3f132a2914615670b7948c2cef29648a9f
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchCustomResourceStatusTestFactory.java
@@ -0,0 +1,15 @@
+package de.ozgcloud.operator;
+
+public class ElasticsearchCustomResourceStatusTestFactory {
+	
+	public final static CustomResourceStatus STATUS = CustomResourceStatus.OK;
+	
+	public static OzgCloudElasticsearchCustomResourceStatus create() {
+		return createBuilder().build();
+	}
+	
+	public static OzgCloudElasticsearchCustomResourceStatus.OzgCloudElasticsearchCustomResourceStatusBuilder createBuilder() {
+		return OzgCloudElasticsearchCustomResourceStatus.builder()
+				.status(STATUS);
+	}	
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchCustomResourceTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchCustomResourceTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f4a5402b2a8dcce256cb3ca50906ef830986aff
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchCustomResourceTestFactory.java
@@ -0,0 +1,11 @@
+package de.ozgcloud.operator;
+
+public class ElasticsearchCustomResourceTestFactory {
+
+	public static OzgCloudElasticsearchCustomResource create() {
+		var resource = new OzgCloudElasticsearchCustomResource();
+		resource.setStatus(ElasticsearchCustomResourceStatusTestFactory.create());
+		resource.setMetadata(ObjectMetaTestFactory.create());
+		return resource;
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchReconcilerITCase.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchReconcilerITCase.java
new file mode 100644
index 0000000000000000000000000000000000000000..66f872dd9f706bd3052598a5a6797cc73659f3e0
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchReconcilerITCase.java
@@ -0,0 +1,64 @@
+package de.ozgcloud.operator;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.net.HttpURLConnection;
+
+import org.junit.Rule;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import io.fabric8.kubernetes.api.model.PodBuilder;
+import io.fabric8.kubernetes.api.model.PodList;
+import io.fabric8.kubernetes.api.model.PodListBuilder;
+import io.fabric8.kubernetes.client.NamespacedKubernetesClient;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+
+class ElasticsearchReconcilerITCase {
+
+	@Rule
+	private final KubernetesServer server = new KubernetesServer();
+	private NamespacedKubernetesClient client;
+
+	@BeforeEach
+	void init() {
+		server.before();
+		client = server.getClient();
+	}
+
+	@DisplayName("Reconcile")
+	@Nested
+	class TestReconcile {
+
+		private final PodList podList = new PodListBuilder().withItems(
+				new PodBuilder().withNewMetadata().withName("pod1").endMetadata()
+						.build(),
+				new PodBuilder().withNewMetadata().withName("pod2").endMetadata()
+						.build())
+				.build();
+
+		@Test
+		public void shouldGetMockedPods() {
+			server.expect().get().withPath("/api/v1/pods").andReturn(HttpURLConnection.HTTP_OK, podList).once();
+
+			var podList = client.pods().inAnyNamespace().list();
+
+			assertThat(podList).isNotNull();
+			assertThat(podList.getItems()).hasSize(2);
+		}
+
+		@Test
+		public void shouldGetMockedPodsInNamespace() {
+			var namespace = "default";
+			server.expect().get().withPath("/api/v1/namespaces/" + namespace + "/pods")
+					.andReturn(HttpURLConnection.HTTP_OK, new PodListBuilder().build())
+					.once();
+
+			var podList = client.pods().inNamespace(namespace).list();
+
+			assertThat(podList.getItems()).isEmpty();
+		}
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchReconcilerTest.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchReconcilerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..163b26d782bb6b73c2174d5b8f51d79a2b257098
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ElasticsearchReconcilerTest.java
@@ -0,0 +1,232 @@
+package de.ozgcloud.operator;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Base64;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.operator.common.kubernetes.NamespaceTestFactory;
+import de.ozgcloud.operator.common.kubernetes.SecretTestFactory;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import lombok.SneakyThrows;
+
+class ElasticsearchReconcilerTest {
+
+	@Spy
+	@InjectMocks
+	private ElasticsearchReconciler reconciler;
+	@Mock
+	private OzgCloudElasticsearchService service;
+
+	@DisplayName("Reconcile")
+	@Nested
+	class TestReconcile {
+
+		@Mock
+		private Context<OzgCloudElasticsearchCustomResource> context;
+
+		private final OzgCloudElasticsearchCustomResource resource = ElasticsearchCustomResourceTestFactory.create();
+
+		private final static String PASSWORD = new String(Base64.getEncoder().encode("dummyPassword".getBytes()));
+		private final static String DECODED_PASSWORD = new String(Base64.getDecoder().decode(PASSWORD));
+		private final Secret secret = SecretTestFactory.createBuilder()
+				.addToData(OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_PASSWORD_FIELD, PASSWORD).build();
+
+		@DisplayName("process flow")
+		@Nested
+		class TestProcessFlow {
+
+			@BeforeEach
+			void mockCredentialSecret() {
+				when(service.getOrCreateCredentialSecret(any(), any())).thenReturn(secret);
+			}
+
+			@Test
+			void shouldGetCredentialSecret() {
+				reconcile();
+
+				verify(service).getOrCreateCredentialSecret(resource, context);
+			}
+
+			@SneakyThrows
+			@Test
+			void shouldCreateIndexIfMissing() {
+				reconcile();
+
+				verify(service).createIndexIfMissing(NamespaceTestFactory.NAMESPACE);
+			}
+
+			@SneakyThrows
+			@Test
+			void shouldCreateecurityRoleIfMissing() {
+				reconcile();
+
+				verify(service).createSecurityRoleIfMissing(NamespaceTestFactory.NAMESPACE);
+			}
+
+			@SneakyThrows
+			@Test
+			void shouldCallCreateSecurityUserIfMissing() {
+				reconcile();
+
+				verify(service).createSecurityUserIfMissing(NamespaceTestFactory.NAMESPACE, DECODED_PASSWORD);
+			}
+
+			@SneakyThrows
+			@Test
+			void shouldCallCreateCertificateIfMissing() {
+				reconcile();
+
+				verify(service).createCertificateIfMissing(NamespaceTestFactory.NAMESPACE);
+			}
+		}
+
+		@DisplayName("on exception")
+		@Nested
+		class TestOnException {
+
+			private final Exception exception = new RuntimeException();
+
+			@BeforeEach
+			void mock() {
+				when(service.getOrCreateCredentialSecret(any(), any())).thenThrow(exception);
+			}
+
+			@Test
+			void shouldBuildExceptionUpdateControl() {
+				reconcile();
+
+				verify(reconciler).buildExceptionUpdateControl(resource, exception);
+			}
+		}
+
+		@SneakyThrows
+		private UpdateControl<OzgCloudElasticsearchCustomResource> reconcile() {
+			return reconciler.reconcile(resource, context);
+		}
+
+		@DisplayName("build exception update control")
+		@Nested
+		class TestBuildExceptionUpdateControl {
+
+			private final OzgCloudElasticsearchCustomResource resource = ElasticsearchCustomResourceTestFactory.create();
+			private final static String EXCEPTION_MESSAGE = "ExeptionMessage";
+			private final Exception exception = new RuntimeException(EXCEPTION_MESSAGE);
+
+			@Test
+			void shouldContainResource() {
+				var updateControl = buildExceptionUpdateControl();
+
+				assertThat(updateControl.getResource()).isEqualTo(resource);
+			}
+
+			@Test
+			void shouldContainUpdateStatus() {
+				var updateControl = buildExceptionUpdateControl();
+
+				assertThat(updateControl.getResource().getStatus().getStatus()).isEqualTo(CustomResourceStatus.ERROR);
+			}
+
+			@Test
+			void shouldContainReschedule() {
+				var updateControl = buildExceptionUpdateControl();
+
+				assertThat(updateControl.getScheduleDelay()).hasValue(60000L);
+			}
+
+			@Test
+			void shouldContainMessage() {
+				var updateControl = buildExceptionUpdateControl();
+
+				assertThat(updateControl.getResource().getStatus().getMessage()).isEqualTo(EXCEPTION_MESSAGE);
+			}
+
+			private UpdateControl<OzgCloudElasticsearchCustomResource> buildExceptionUpdateControl() {
+				return reconciler.buildExceptionUpdateControl(resource, exception);
+			}
+		}
+
+		@DisplayName("get password")
+		@Nested
+		class TestGetPassword {
+
+			private static final String SECRET_PASSWORD = LoremIpsum.getInstance().getWords(1);
+
+			@Test
+			void shouldReturnPasssowrd() {
+				var secret = buildSecret();
+
+				var password = reconciler.getPassword(secret);
+
+				assertThat(password).isEqualTo(SECRET_PASSWORD);
+			}
+
+			private Secret buildSecret() {
+				return new SecretBuilder()
+						.addToData(Map.of(OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_PASSWORD_FIELD,
+								encodeStringBase64(SECRET_PASSWORD)))
+						.build();
+			}
+
+			private String encodeStringBase64(String string) {
+				return Base64.getEncoder().encodeToString(string.getBytes());
+			}
+		}
+	}
+
+	@DisplayName("Cleanup")
+	@Nested
+	class TestCleanup {
+
+		@Mock
+		private Context<OzgCloudElasticsearchCustomResource> context;
+
+		private final OzgCloudElasticsearchCustomResource resource = ElasticsearchCustomResourceTestFactory.create();
+
+		@SneakyThrows
+		@Test
+		void shouldDeleteSecurityUser() {
+			reconciler.cleanup(resource, context);
+
+			verify(service).deleteSecurityUserIfExists(NamespaceTestFactory.NAMESPACE);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldDeleteSecurityRole() {
+			reconciler.cleanup(resource, context);
+
+			verify(service).deleteSecurityRoleIfExists(NamespaceTestFactory.NAMESPACE);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldDeleteIndex() {
+			reconciler.cleanup(resource, context);
+
+			verify(service).deleteIndexIfExists(NamespaceTestFactory.NAMESPACE);
+		}
+
+		@Test
+		void shouldReturnDeleteControl() {
+			var deleteControl = reconciler.cleanup(resource, context);
+
+			assertThat(deleteControl).isNotNull();
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/IndicesPrivilegesDataTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/IndicesPrivilegesDataTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..9333775ebdc8fc9b1ffcb510e09b78781519ae90
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/IndicesPrivilegesDataTestFactory.java
@@ -0,0 +1,21 @@
+package de.ozgcloud.operator;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.operator.PutRoleRequestData.IndicesPrivilegesData;
+
+public class IndicesPrivilegesDataTestFactory {
+
+	public static final String NAME = LoremIpsum.getInstance().getFirstName();
+	public static final String PRIVILEGES = IndicesPrivilege.ALL.getValue();
+	
+	public static IndicesPrivilegesData  create() {
+		return createBuilder().build();
+	}
+	
+	public static IndicesPrivilegesData.IndicesPrivilegesDataBuilder createBuilder(){
+		return IndicesPrivilegesData.builder()
+				.names(NAME)
+				.privileges(PRIVILEGES);
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ObjectMetaTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ObjectMetaTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..c86a0813c25cd1cf7c0315db3078f1af8df934d1
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/ObjectMetaTestFactory.java
@@ -0,0 +1,14 @@
+package de.ozgcloud.operator;
+
+import de.ozgcloud.operator.common.kubernetes.NamespaceTestFactory;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+
+public class ObjectMetaTestFactory {
+
+	public static ObjectMeta create() {
+		var objectMeta = new ObjectMeta();
+		objectMeta.setNamespace(NamespaceTestFactory.NAMESPACE);
+		
+		return objectMeta;
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/OzgCloudElasticsearchSecretHelperTest.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/OzgCloudElasticsearchSecretHelperTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5cba01b723db24e94b8f02da258f9a57543ab408
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/OzgCloudElasticsearchSecretHelperTest.java
@@ -0,0 +1,174 @@
+package de.ozgcloud.operator;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.ozgcloud.operator.OzgCloudElasticsearchProperties.OzgCloudElasticsearchServerProperties;
+import de.ozgcloud.operator.common.kubernetes.NamespaceTestFactory;
+import de.ozgcloud.operator.common.kubernetes.SecretTestFactory;
+import io.fabric8.kubernetes.api.model.Secret;
+
+class OzgCloudElasticsearchSecretHelperTest {
+
+	@Spy
+	@InjectMocks
+	private OzgCloudElasticsearchSecretHelper builder;
+	@Mock
+	private OzgCloudElasticsearchProperties properties;
+
+	@DisplayName("Build credential secret")
+	@Nested
+	class TestBuildCredentialsSecret {
+
+		private final static String HOST = "dummyHost";
+		private final static int PORT = 42;
+
+		@Mock
+		private OzgCloudElasticsearchServerProperties serverProperties;
+
+		@BeforeEach
+		void mockProperties() {
+			when(properties.getServer()).thenReturn(serverProperties);
+			when(serverProperties.getHost()).thenReturn(HOST);
+			when(serverProperties.getPort()).thenReturn(PORT);
+		}
+
+		@Test
+		void shouldContainType() {
+			var secret = buildCredentialSecret();
+
+			assertThat(secret.getType()).isEqualTo(OzgCloudElasticsearchSecretHelper.SECRET_TYPE);
+		}
+
+		@DisplayName("metadata")
+		@Nested
+		class TestMetadata {
+
+			@Test
+			void shouldContainName() {
+				var secret = buildCredentialSecret();
+
+				assertThat(secret.getMetadata().getName()).isEqualTo(SecretTestFactory.NAME);
+			}
+
+			@Test
+			void shouldContainNamespace() {
+				var secret = buildCredentialSecret();
+
+				assertThat(secret.getMetadata().getNamespace()).isEqualTo(NamespaceTestFactory.NAMESPACE);
+			}
+		}
+
+		@DisplayName("address")
+		@Nested
+		class TestAddress {
+
+			@Test
+			void shouldBeSet() {
+				var secret = buildCredentialSecret();
+
+				assertThat(secret.getStringData()).containsEntry(OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_ADDRESS_FIELD,
+						String.format("%s:%s", HOST, PORT));
+			}
+
+			@Test
+			void shouldGetHostFromProperties() {
+				buildCredentialSecret();
+
+				verify(serverProperties).getHost();
+			}
+
+			@Test
+			void shouldGetPortFromPorperties() {
+				buildCredentialSecret();
+
+				verify(serverProperties).getPort();
+			}
+		}
+
+		@Test
+		void shouldContainIndex() {
+			var secret = buildCredentialSecret();
+
+			assertThat(secret.getStringData()).containsEntry(OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_INDEX_FIELD,
+					NamespaceTestFactory.NAMESPACE);
+		}
+
+		@Test
+		void shouldContainPassword() {
+			var secret = buildCredentialSecret();
+
+			assertThat(secret.getStringData()).containsKey(OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_PASSWORD_FIELD);
+			assertThat(secret.getStringData().get(OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_PASSWORD_FIELD)).isNotNull();
+		}
+
+		@Test
+		void shouldContainUsername() {
+			var secret = buildCredentialSecret();
+
+			assertThat(secret.getStringData()).containsEntry(OzgCloudElasticsearchSecretHelper.CREDENTIAL_SECRET_USERNAME_FIELD,
+					NamespaceTestFactory.NAMESPACE);
+		}
+
+		private Secret buildCredentialSecret() {
+			return builder.buildCredentialSecret(NamespaceTestFactory.NAMESPACE, SecretTestFactory.NAME);
+		}
+	}
+
+	@DisplayName("Build certificate secret")
+	@Nested
+	class TestBuildCertificatSecret {
+
+		private static final String DATA = "fgdrgsgreg";
+
+		@Test
+		void shouldHaveType() {
+			var secret = builder.buildCertificateSecret(NamespaceTestFactory.NAMESPACE, DATA);
+
+			assertThat(secret.getType()).isEqualTo(OzgCloudElasticsearchSecretHelper.SECRET_TYPE);
+		}
+
+		@DisplayName("metadata")
+		@Nested
+		class TestMetadata {
+
+			private static final String CERTIFICATE_SECRET_NAME = "rfgsgrgsr";
+
+			@BeforeEach
+			void mock() {
+				when(properties.getCertificateSecretName()).thenReturn(CERTIFICATE_SECRET_NAME);
+			}
+
+			@Test
+			void shouldContainName() {
+				var secret = builder.buildCertificateSecret(NamespaceTestFactory.NAMESPACE, DATA);
+
+				assertThat(secret.getMetadata().getName()).isEqualTo(CERTIFICATE_SECRET_NAME);
+			}
+
+			@Test
+			void shouldContainNamespace() {
+				var secret = builder.buildCertificateSecret(NamespaceTestFactory.NAMESPACE, DATA);
+
+				assertThat(secret.getMetadata().getNamespace()).isEqualTo(NamespaceTestFactory.NAMESPACE);
+			}
+		}
+
+		@Test
+		void shouldHaveCaCrtData() {
+			var secret = builder.buildCertificateSecret(NamespaceTestFactory.NAMESPACE, DATA);
+
+			assertThat(secret.getData()).containsExactly(Map.entry(OzgCloudElasticsearchSecretHelper.CERTIFICATE_SECRET_DATA_KEY, DATA));
+		}
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/OzgCloudElasticsearchServiceTest.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/OzgCloudElasticsearchServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d0c3734879f08925a6dce3fa0fcc6ee97dc711d
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/OzgCloudElasticsearchServiceTest.java
@@ -0,0 +1,418 @@
+package de.ozgcloud.operator;
+
+import static de.ozgcloud.operator.common.kubernetes.NamespaceTestFactory.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.operator.OzgCloudElasticsearchProperties.OzgCloudElasticsearchServerProperties;
+import de.ozgcloud.operator.common.elasticsearch.ElasticsearchRemoteService;
+import de.ozgcloud.operator.common.kubernetes.KubernetesRemoteService;
+import de.ozgcloud.operator.common.kubernetes.NamespaceTestFactory;
+import de.ozgcloud.operator.common.kubernetes.SecretTestFactory;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.dsl.Resource;
+import io.fabric8.kubernetes.client.extension.ResourceAdapter;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import lombok.SneakyThrows;
+
+class OzgCloudElasticsearchServiceTest {
+
+	@Spy
+	@InjectMocks
+	private OzgCloudElasticsearchService service;
+	@Mock
+	private ElasticsearchRemoteService remoteService;
+	@Mock
+	private OzgCloudElasticsearchSecretHelper secretHelper;
+	@Mock
+	private OzgCloudElasticsearchProperties properties;
+	@Mock
+	private KubernetesRemoteService kubernetesService;
+
+	@DisplayName("Get or create secret")
+	@Nested
+	class TestGetOrCreateSecret {
+
+		@Mock
+		private Context<OzgCloudElasticsearchCustomResource> context;
+		@Mock
+		private ResourceAdapter<Secret> resourceAdapter;
+		@Mock
+		private Resource<Secret> secretResource;
+
+		private final Secret secret = SecretTestFactory.create();
+
+		private final OzgCloudElasticsearchCustomResource resource = ElasticsearchCustomResourceTestFactory.create();
+
+		@BeforeEach
+		void mock() {
+			when(kubernetesService.getSecretResource(any(), any())).thenReturn(secretResource);
+			when(secretResource.get()).thenReturn(secret);
+			when(properties.getSecretCredentialsName()).thenReturn(IndicesPrivilegesDataTestFactory.PRIVILEGES);
+		}
+
+		@Test
+		void shouldGetSecret() {
+			service.getOrCreateCredentialSecret(resource, context);
+
+			verify(kubernetesService).getSecretResource(NamespaceTestFactory.NAMESPACE, IndicesPrivilegesDataTestFactory.PRIVILEGES);
+		}
+
+		@DisplayName("on existing")
+		@Nested
+		class TestOnExisting {
+
+			@Test
+			void shouldReturnSecret() {
+				var secret = service.getOrCreateCredentialSecret(resource, context);
+
+				assertThat(secret).isNotNull();
+			}
+		}
+
+		@DisplayName("on missing secret")
+		@Nested
+		class TestOnMissingSecret {
+
+			@BeforeEach
+			void mock() {
+				when(secretResource.get()).thenReturn(null);
+
+				doReturn(resourceAdapter).when(service).createAdapter(any());
+			}
+
+			@Test
+			void shouldBuildSecret() {
+				service.getOrCreateCredentialSecret(resource, context);
+
+				verify(secretHelper).buildCredentialSecret(NamespaceTestFactory.NAMESPACE, IndicesPrivilegesDataTestFactory.PRIVILEGES);
+			}
+
+			@Test
+			void shouldCreateSecret() {
+				when(secretHelper.buildCredentialSecret(any(), any())).thenReturn(secret);
+
+				service.getOrCreateCredentialSecret(resource, context);
+
+				verify(resourceAdapter).create(secret);
+			}
+		}
+	}
+
+	@DisplayName("Create index if missing")
+	@Nested
+	class TestCreateIndexIfMissing {
+
+		@SneakyThrows
+		@Test
+		void shouldCheckIfIndexExists() {
+			service.createIndexIfMissing(NAMESPACE);
+
+			verify(remoteService).existsIndex(NAMESPACE);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldCreateIndexIfMissing() {
+			when(remoteService.existsIndex(any())).thenReturn(false);
+
+			service.createIndexIfMissing(NAMESPACE);
+
+			verify(remoteService).createIndex(NAMESPACE);
+		}
+	}
+
+	@DisplayName("Check security role if missing")
+	@Nested
+	class TestCheckSecurityRoleIfMissing {
+
+		private final PutRoleRequestData putRoleRequest = PutRoleRequestDataTestFactory.create();
+
+		@SneakyThrows
+		@Test
+		void shouldCheckIfSecurityRoleExists() {
+			service.createSecurityRoleIfMissing(NAMESPACE);
+
+			verify(remoteService).existsSecurityRole(NAMESPACE);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldCreateSecurityRoleIfMissing() {
+			when(remoteService.existsSecurityRole(any())).thenReturn(false);
+			doReturn(putRoleRequest).when(service).buildPutRoleRequestData(any());
+
+			service.createSecurityRoleIfMissing(NAMESPACE);
+
+			verify(remoteService).createSecurityRole(putRoleRequest);
+		}
+
+		@DisplayName("create put role request data")
+		@Nested
+		class TestCreatePutRoleRequestData {
+
+			@Test
+			void shouldContainName() {
+				var requestData = buildPutRoleRequestData();
+
+				assertThat(requestData.getName()).isEqualTo(NAMESPACE);
+			}
+
+			@DisplayName("indices privileges data")
+			@Nested
+			class TestIndicesPrivilegesData {
+
+				@Test
+				void shouldContainName() {
+					var requestData = buildPutRoleRequestData();
+
+					assertThat(requestData.getIndivesPrivilegesData().getNames()).isEqualTo(NAMESPACE);
+				}
+
+				@Test
+				void shouldContainPrivileges() {
+					var requestData = buildPutRoleRequestData();
+
+					assertThat(requestData.getIndivesPrivilegesData().getPrivileges()).isEqualTo(IndicesPrivilegesDataTestFactory.PRIVILEGES);
+				}
+			}
+
+			private PutRoleRequestData buildPutRoleRequestData() {
+				return service.buildPutRoleRequestData(NAMESPACE);
+			}
+		}
+	}
+
+	@DisplayName("Create security user if missing")
+	@Nested
+	class TestCheckSecurityUserIfMissing {
+
+		private final PutUserRequestData putUserRequestData = PutUserRequestDataTestFactory.create();
+
+		@SneakyThrows
+		@Test
+		void shouldCheckIfSecurityUserExists() {
+			service.createSecurityUserIfMissing(NAMESPACE, PutUserRequestDataTestFactory.PASSWORD);
+
+			verify(remoteService).existsSecurityUser(NAMESPACE);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldCreateSecurityUserIfMissing() {
+			when(remoteService.existsSecurityUser(any())).thenReturn(false);
+			doReturn(putUserRequestData).when(service).buildPutUserRequestData(any(), any());
+
+			service.createSecurityUserIfMissing(NAMESPACE, PutUserRequestDataTestFactory.PASSWORD);
+
+			verify(remoteService).createSecurityUser(putUserRequestData);
+		}
+
+		@DisplayName("create put user request data")
+		@Nested
+		class TestCreatePutUserRequestData {
+
+			@Test
+			void shouldContainUsername() {
+				var requestData = buildPutUserRequestData();
+
+				assertThat(requestData.getUsername()).isEqualTo(NAMESPACE);
+			}
+
+			@Test
+			void shouldContainRoles() {
+				var requestData = buildPutUserRequestData();
+
+				assertThat(requestData.getRoles()).isEqualTo(NAMESPACE);
+			}
+
+			@Test
+			void shouldContainPassword() {
+				var requestData = buildPutUserRequestData();
+
+				assertThat(requestData.getPassword()).isEqualTo(PutUserRequestDataTestFactory.PASSWORD);
+			}
+
+			private PutUserRequestData buildPutUserRequestData() {
+				return service.buildPutUserRequestData(NAMESPACE, PutUserRequestDataTestFactory.PASSWORD);
+			}
+		}
+	}
+
+	@DisplayName("Delete security user if exists")
+	@Nested
+	class TestDeleteSecurityUserIfExists {
+
+		@SneakyThrows
+		@Test
+		void shouldCheckIfSecurityUserExists() {
+			service.deleteSecurityUserIfExists(PutUserRequestDataTestFactory.USERNAME);
+
+			verify(remoteService).existsSecurityUser(PutUserRequestDataTestFactory.USERNAME);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldDeleteSecurityUserIfExists() {
+			when(remoteService.existsSecurityUser(any())).thenReturn(true);
+
+			service.deleteSecurityUserIfExists(PutUserRequestDataTestFactory.USERNAME);
+
+			verify(remoteService).deleteSecurityUser(PutUserRequestDataTestFactory.USERNAME);
+		}
+	}
+
+	@DisplayName("Delete security role if exists")
+	@Nested
+	class TestDeleteSecurityRoleIfExists {
+
+		@SneakyThrows
+		@Test
+		void shouldCheckIfSecurityRoleExists() {
+			service.deleteSecurityRoleIfExists(PutRoleRequestDataTestFactory.NAME);
+
+			verify(remoteService).existsSecurityRole(PutRoleRequestDataTestFactory.NAME);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldDeleteSecurityRoleIfExists() {
+			when(remoteService.existsSecurityRole(any())).thenReturn(true);
+
+			service.deleteSecurityRoleIfExists(PutRoleRequestDataTestFactory.NAME);
+
+			verify(remoteService).deleteSecurityRole(PutRoleRequestDataTestFactory.NAME);
+		}
+	}
+
+	@DisplayName("Delete index if exists")
+	@Nested
+	class TestDeleteIndexIfExists {
+
+		private static final String INDEX_NAME = NAMESPACE;
+
+		@SneakyThrows
+		@Test
+		void shouldCheckIfIndexExists() {
+			service.deleteIndexIfExists(INDEX_NAME);
+
+			verify(remoteService).existsIndex(INDEX_NAME);
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldDeleteSecurityRoleIfExists() {
+			when(remoteService.existsIndex(any())).thenReturn(true);
+
+			service.deleteIndexIfExists(INDEX_NAME);
+
+			verify(remoteService).deleteIndex(INDEX_NAME);
+		}
+	}
+
+	@DisplayName("Create certificate if missing")
+	@Nested
+	class TestCreateCertificateIfMissing {
+
+		private static final String CERTIFICATE_NAME = "dummySecretName";
+
+		@Mock
+		private Resource<Secret> secretResource;
+
+		@DisplayName("process flow")
+		@Nested
+		class TestProcessFlow {
+
+			@Mock
+			private OzgCloudElasticsearchServerProperties serverProperties;
+
+			@BeforeEach
+			void mock() {
+				when(kubernetesService.getSecretResource(any(), any())).thenReturn(secretResource);
+				when(properties.getCertificateSecretName()).thenReturn(CERTIFICATE_NAME);
+			}
+
+			@Test
+			void shouldGetCertificateSecret() {
+				when(secretResource.get()).thenReturn(SecretTestFactory.create());
+
+				service.createCertificateIfMissing(NAMESPACE);
+
+				verify(kubernetesService).getSecretResource(NAMESPACE, CERTIFICATE_NAME);
+			}
+
+			@Test
+			void shouldCreateIfMissing() {
+				when(secretResource.get()).thenReturn(null);
+				doNothing().when(service).createCredentialSecret(any(), any());
+
+				service.createCertificateIfMissing(NAMESPACE);
+
+				verify(service).createCredentialSecret(NAMESPACE, secretResource);
+			}
+		}
+
+		@DisplayName("create credential secret")
+		@Nested
+		class TestCreateCredentialSecret {
+
+			private static final String SERVER_CERTIFICATE_SECRET_NAME = LoremIpsum.getInstance().getWords(1);
+
+			private static final String SERVER_CERTIFICATE_SECRET_DATA_KEY = LoremIpsum.getInstance().getWords(1);
+			private static final String SERVER_CERTIFICATE_SECRET_DATA = LoremIpsum.getInstance().getWords(1);
+			private static final String SERVER_CERTIFICATE_NAMESPACE = LoremIpsum.getInstance().getWords(1);
+			private static final Secret SERVER_CERTIFICATE_SECRET = SecretTestFactory.createBuilder()
+					.addToData(SERVER_CERTIFICATE_SECRET_DATA_KEY, SERVER_CERTIFICATE_SECRET_DATA)
+					.build();
+			private static final Secret CREDENTIAL_SECRET = SecretTestFactory.create();
+			@Mock
+			private OzgCloudElasticsearchServerProperties serverProperties;
+			@Mock
+			private ResourceAdapter<Secret> resourceAdapter;
+
+			@BeforeEach
+			void mock() {
+				when(properties.getServer()).thenReturn(serverProperties);
+				when(serverProperties.getCertificateSecretName()).thenReturn(SERVER_CERTIFICATE_SECRET_NAME);
+				when(serverProperties.getCertificateSecretDataKey()).thenReturn(SERVER_CERTIFICATE_SECRET_DATA_KEY);
+				when(serverProperties.getCertificateNamespace()).thenReturn(SERVER_CERTIFICATE_NAMESPACE);
+				when(kubernetesService.getSecretResource(any(), any())).thenReturn(secretResource);
+				when(secretResource.get()).thenReturn(SERVER_CERTIFICATE_SECRET);
+				doReturn(resourceAdapter).when(service).createAdapter(any());
+				when(secretHelper.buildCertificateSecret(any(), any())).thenReturn(CREDENTIAL_SECRET);
+			}
+
+			@Test
+			void shouldGetCertificateSecret() {
+				service.createCredentialSecret(NAMESPACE, secretResource);
+
+				verify(kubernetesService).getSecretResource(SERVER_CERTIFICATE_NAMESPACE, SERVER_CERTIFICATE_SECRET_NAME);
+			}
+
+			@Test
+			void shouldBuildSecret() {
+				service.createCredentialSecret(NAMESPACE, secretResource);
+
+				verify(secretHelper).buildCertificateSecret(NAMESPACE, SERVER_CERTIFICATE_SECRET_DATA);
+			}
+
+			@Test
+			void shouldCreate() {
+				service.createCredentialSecret(NAMESPACE, secretResource);
+
+				verify(resourceAdapter).create(CREDENTIAL_SECRET);
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/PutRoleRequestDataTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/PutRoleRequestDataTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..3950d7f545ad1e1aa95cbacc70abfefa0df3059e
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/PutRoleRequestDataTestFactory.java
@@ -0,0 +1,18 @@
+package de.ozgcloud.operator;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+public class PutRoleRequestDataTestFactory {
+
+	public static final String NAME = LoremIpsum.getInstance().getFirstName();
+
+	public static PutRoleRequestData create() {
+		return createBuilder().build();
+	}
+
+	public static PutRoleRequestData.PutRoleRequestDataBuilder createBuilder() {
+		return PutRoleRequestData.builder()
+				.name(NAME)
+				.indivesPrivilegesData(IndicesPrivilegesDataTestFactory.create());
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/PutUserRequestDataTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/PutUserRequestDataTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..0c8fc65a96eac4761f82b126be7af594ca6c58ac
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/PutUserRequestDataTestFactory.java
@@ -0,0 +1,23 @@
+package de.ozgcloud.operator;
+
+import org.apache.commons.lang3.RandomStringUtils;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+public class PutUserRequestDataTestFactory {
+	
+	public static final String USERNAME = LoremIpsum.getInstance().getFirstName();
+	public static final String ROLES = LoremIpsum.getInstance().getWords(1);
+	public static final String PASSWORD = RandomStringUtils.randomAlphanumeric(6);
+
+	public static PutUserRequestData  create() {
+		return createBuilder().build();
+	}
+	
+	public static PutUserRequestData.PutUserRequestDataBuilder createBuilder(){
+		return PutUserRequestData.builder()
+				.username(USERNAME)
+				.roles(ROLES)
+				.password(PASSWORD);
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchClientConfigurationTest.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchClientConfigurationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..627170a48b4cacc7acf7a3b8c838fe1bd5564f59
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchClientConfigurationTest.java
@@ -0,0 +1,67 @@
+package de.ozgcloud.operator.common.elasticsearch;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Base64;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.ozgcloud.operator.OzgCloudElasticsearchProperties;
+import de.ozgcloud.operator.OzgCloudElasticsearchProperties.OzgCloudElasticsearchServerProperties;
+import de.ozgcloud.operator.common.kubernetes.KubernetesRemoteService;
+import de.ozgcloud.operator.common.kubernetes.SecretTestFactory;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.dsl.Resource;
+
+class ElasticsearchClientConfigurationTest {
+	
+	@Spy
+	@InjectMocks
+	private ElasticsearchClientConfiguration configuration;
+	@Mock
+	private OzgCloudElasticsearchProperties properties;
+	@Mock
+	private KubernetesRemoteService kubernetesRemoteService;
+	
+	@DisplayName("Create elasticsearch client")
+	@Nested
+	class TestCreateElasticsearchClient {
+		
+		private static final String SECRET_DATA_KEY = "dsefsfef";
+		private static final String SECRET_DATA_VALUE = "testPassword";
+		private static final String SECRET_DATA_ENCODED_VALUE = encodeStringBase64(SECRET_DATA_VALUE);
+		private static final Secret SECRET = SecretTestFactory.createBuilder().addToData(SECRET_DATA_KEY, SECRET_DATA_ENCODED_VALUE).build(); 
+		
+		@Mock
+		private Resource<Secret> secretResource;
+		@Mock
+		private OzgCloudElasticsearchServerProperties serverProperties;
+		
+		@BeforeEach
+		void mock() {
+			when(properties.getServer()).thenReturn(serverProperties);
+			when(serverProperties.getSecretDataKey()).thenReturn(SECRET_DATA_KEY);
+			when(kubernetesRemoteService.getSecretResource(any(), any())).thenReturn(secretResource);
+			when(secretResource.get()).thenReturn(SECRET);
+		}
+		
+		@Test
+		void shouldReturnPasssowrd() {
+			var password = configuration.getPassword();
+			
+			assertThat(password).isEqualTo(SECRET_DATA_VALUE);
+		}
+		
+		private static String encodeStringBase64(String string) {
+			return Base64.getEncoder().encodeToString(string.getBytes());
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteServiceITCase.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteServiceITCase.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ac49af8471222eff42202a3ec63a15a14cd579a
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteServiceITCase.java
@@ -0,0 +1,352 @@
+package de.ozgcloud.operator.common.elasticsearch;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.elasticsearch.indices.ExistsRequest;
+import co.elastic.clients.elasticsearch.security.IndicesPrivileges;
+import co.elastic.clients.elasticsearch.security.PutRoleRequest;
+import co.elastic.clients.elasticsearch.security.PutUserRequest;
+import de.ozgcloud.operator.IndicesPrivilegesDataTestFactory;
+import de.ozgcloud.operator.PutRoleRequestDataTestFactory;
+import de.ozgcloud.operator.PutUserRequestDataTestFactory;
+import lombok.SneakyThrows;
+
+class ElasticsearchRemoteServiceITCase {
+	
+	private static final String INDEX_NAME = "test_index";
+	
+	private final ElasticsearchClient client = ElasticsearchTestClient.create();
+	private final ElasticsearchRemoteService service = new ElasticsearchRemoteService(client);
+
+	@BeforeAll
+	public static void startContainer() {
+		ElasticsearchTestClient.ELASTICSEARCH_CONTAINER.start();
+	}
+	
+	@AfterAll
+	public static void stopContainer() {
+		ElasticsearchTestClient.ELASTICSEARCH_CONTAINER.stop();
+	}
+
+	@DisplayName("Exists index")
+	@Nested
+	class TestExistsIndex {
+		
+		@DisplayName("on existing")
+		@Nested
+		class TestOnExisting {
+			
+			@SneakyThrows
+			@BeforeEach
+			private void initIndex() {
+				createIndex();
+			}
+			
+			@AfterEach
+			void cleanup() {
+				deleteIndex();
+			}
+			
+			@SneakyThrows
+			@Test
+			void shouldReturnTrue() {
+				var exists = service.existsIndex(INDEX_NAME);
+				
+				assertThat(exists).isTrue();
+			}
+		}
+
+		@SneakyThrows
+		@Test
+		void shouldReturnFalseIfMissing() {
+			var exists = service.existsIndex(INDEX_NAME);
+			
+			assertThat(exists).isFalse();
+		}
+	}
+	
+	@DisplayName("Create index")
+	@Nested
+	class TestCreateIndex {
+		
+		@AfterEach
+		void cleanup() {
+			deleteIndex();
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldCreateIndex() {
+			service.createIndex(INDEX_NAME);
+			
+			assertThat(existsIndex()).isTrue();
+		}
+	}
+	
+	@DisplayName("Exists security role")
+	@Nested
+	class TestExistsSecurityRole {
+		
+		@DisplayName("on existing")
+		@Nested
+		class TestOnExisting {
+			
+			@SneakyThrows
+			@BeforeEach
+			private void initSecurityRole() {
+				createIndex();
+				client.security().putRole(service.createPutRoleRequest(PutRoleRequestDataTestFactory.create()));
+			}
+			
+			@AfterEach
+			void cleanup() {
+				deleteIndex();
+			}
+			
+			@SneakyThrows
+			@Test
+			void shouldReturnTrue() {
+				var exists = service.existsSecurityRole(PutRoleRequestDataTestFactory.NAME);
+				
+				assertThat(exists).isTrue();
+			}
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldReturnFalseIfMissing() {
+			var exists = service.existsSecurityRole(PutRoleRequestDataTestFactory.NAME);
+			
+			assertThat(exists).isFalse();
+		}
+	}
+	
+	@DisplayName("Create security role")
+	@Nested
+	class TestCreateSecurityRole {
+		
+		@AfterEach
+		void cleanup() {
+			deleteSecurityRole();
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldCreateSecurityRole() {
+			service.createSecurityRole(PutRoleRequestDataTestFactory.create());
+			
+			assertThat(existsSecurityRole()).isTrue();
+		}
+		
+		@SneakyThrows
+		private void deleteSecurityRole() {
+			client.security().deleteRole(builder -> builder.name(PutRoleRequestDataTestFactory.NAME));
+		}
+	}
+	
+	@DisplayName("Exists security user")
+	@Nested
+	class TestExistsSecurityUser {
+		
+		@DisplayName("on existing")
+		@Nested
+		class TestOnExisting {
+			
+			@SneakyThrows
+			@BeforeEach
+			private void initSecurityUser() {
+				createIndex();
+				client.security().putUser(service.createPutUserRequest(PutUserRequestDataTestFactory.create()));
+			}
+			
+			@AfterEach
+			void cleanup() {
+				deleteIndex();
+				deleteSecurityRole();
+			}
+			
+			@SneakyThrows
+			@Test
+			void shouldReturnTrue() {
+				var exists = service.existsSecurityUser(PutUserRequestDataTestFactory.USERNAME);
+				
+				assertThat(exists).isTrue();
+			}
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldReturnFalseIfMissing() {
+			var exists = service.existsSecurityUser(PutUserRequestDataTestFactory.USERNAME);
+			
+			assertThat(exists).isFalse();
+		}
+	}
+	
+	@SneakyThrows
+	private void deleteSecurityRole() {
+		client.security().deleteUser(builder -> builder.username(PutUserRequestDataTestFactory.USERNAME));
+	}
+	@SneakyThrows
+	private void deleteIndex() {
+		client.indices().delete(builder -> builder.index(INDEX_NAME));
+	}
+	
+	@DisplayName("Create security user")
+	@Nested
+	class TestCreateSecurityUser {
+		
+		@AfterEach
+		void cleanup() {
+			deleteSecurityUser();
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldCreateSecurityUser() {
+			service.createSecurityUser(PutUserRequestDataTestFactory.create());
+			
+			assertThat(existsSecurityUser()).isTrue();
+		}
+		
+		@SneakyThrows
+		private boolean existsSecurityUser() {
+			return !client.security().getUser(builder -> builder.username(PutUserRequestDataTestFactory.USERNAME)).result().isEmpty();
+		}
+		
+		@SneakyThrows
+		private void deleteSecurityUser() {
+			client.security().deleteUser(builder -> builder.username(PutUserRequestDataTestFactory.USERNAME));
+		}
+	}
+	
+	@DisplayName("Delete index")
+	@Nested
+	class TestDeleteIndex {
+		
+		@BeforeEach
+		void init() {
+			createIndex();
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldDeleteIfExists() {			
+			service.deleteIndex(INDEX_NAME);
+			
+			assertThat(existsIndex()).isFalse();
+		}
+	}
+	
+	@DisplayName("Delete security role")
+	@Nested
+	class TestDeleteSecurityRole {
+		
+		@BeforeEach
+		void init() {
+			createIndex();
+			createSecurityRole();
+		}
+		
+		@AfterEach
+		void cleanup() {
+			deleteIndex();
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldDeleteIfExists() {
+			assertThat(existsSecurityRole()).isTrue();
+			
+			service.deleteSecurityRole(PutRoleRequestDataTestFactory.NAME);
+			
+			assertThat(existsSecurityRole()).isFalse();
+		}
+	}
+	
+	@DisplayName("Delete security user")
+	@Nested
+	class TestDeleteSecurityUser {
+		
+		@BeforeEach
+		void init() {
+			createIndex();
+			createSecurityUser();
+		}
+		
+		@AfterEach
+		void cleanup() {
+			deleteIndex();
+		}
+		
+		@SneakyThrows
+		@Test
+		void shouldDeleteIfExists() {
+			assertThat(existsSecurityUser()).isTrue();
+			
+			service.deleteSecurityUser(PutUserRequestDataTestFactory.USERNAME);
+			
+			assertThat(existsSecurityUser()).isFalse();
+		}
+	}
+	
+	@SneakyThrows
+	private boolean existsIndex() {
+		return client.indices().exists(ExistsRequest.of(builder -> builder.index(INDEX_NAME))).value();
+	}
+	
+	@SneakyThrows
+	private void createIndex() {
+		client.indices().create(builder -> builder.index(INDEX_NAME));
+	}
+	
+	@SneakyThrows
+	private void createSecurityRole() {
+		client.security().putRole(this::buildRequest);
+	}
+	
+	private PutRoleRequest.Builder buildRequest(PutRoleRequest.Builder requestBuilder) {
+		requestBuilder.name(PutRoleRequestDataTestFactory.NAME);
+		requestBuilder.indices(this::buildIndicesPrivilegesRequest);
+		return requestBuilder;
+	}
+	
+	private IndicesPrivileges.Builder buildIndicesPrivilegesRequest(IndicesPrivileges.Builder builder) {
+		builder.names(IndicesPrivilegesDataTestFactory.NAME);
+		builder.privileges(IndicesPrivilegesDataTestFactory.PRIVILEGES);
+		
+		return builder;
+	}
+	
+	@SneakyThrows
+	private boolean existsSecurityRole() {
+		return !client.security().getRole(builder -> builder.name(PutRoleRequestDataTestFactory.NAME)).result().isEmpty();
+	}
+	
+	@SneakyThrows
+	private void createSecurityUser() {
+		client.security().putUser(this::buildPutUserRequest);
+	}
+	
+	private PutUserRequest.Builder buildPutUserRequest(PutUserRequest.Builder builder) {
+		builder.username(PutUserRequestDataTestFactory.USERNAME);
+		builder.roles(PutUserRequestDataTestFactory.ROLES);
+		builder.password(PutUserRequestDataTestFactory.PASSWORD);
+		
+		return builder;
+	}
+	
+	@SneakyThrows
+	private boolean existsSecurityUser() {
+		return !client.security().getUser(builder -> builder.username(PutUserRequestDataTestFactory.USERNAME)).result().isEmpty();
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteServiceTest.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7fdc948bc05d4eb5d147e6e39f257f6acb456e5
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchRemoteServiceTest.java
@@ -0,0 +1,91 @@
+package de.ozgcloud.operator.common.elasticsearch;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import de.ozgcloud.operator.IndicesPrivilegesDataTestFactory;
+import de.ozgcloud.operator.PutRoleRequestDataTestFactory;
+import de.ozgcloud.operator.PutUserRequestDataTestFactory;
+
+class ElasticsearchRemoteServiceTest {
+
+	@Spy
+	@InjectMocks
+	private ElasticsearchRemoteService service;
+	@Mock
+	private ElasticsearchClient client;
+	
+	@DisplayName("Create security role")
+	@Nested
+	class TestCreateSecurityRole {
+		
+		@DisplayName("create put role request")
+		@Nested
+		class TestCreatePutRoleRequest {
+			
+			@Test
+			void shouldHaveName() {
+				var request = service.createPutRoleRequest(PutRoleRequestDataTestFactory.create());
+				
+				assertThat(request.name()).isEqualTo(PutRoleRequestDataTestFactory.NAME);
+			}
+			
+			@DisplayName("indices privileges")
+			@Nested
+			class TestIndicesPrivileges {
+				
+				@Test
+				void shouldHaveName() {
+					var request = service.createPutRoleRequest(PutRoleRequestDataTestFactory.create());
+					
+					assertThat(request.indices().get(0).names()).containsExactly(IndicesPrivilegesDataTestFactory.NAME);
+				}
+				
+				@Test
+				void shouldHavePrivileges() {
+					var request = service.createPutRoleRequest(PutRoleRequestDataTestFactory.create());
+					
+					assertThat(request.indices().get(0).privileges()).containsExactly(IndicesPrivilegesDataTestFactory.PRIVILEGES);
+				}
+			}
+		}
+	}
+	
+	@DisplayName("Create security user")
+	@Nested
+	class TestCreateSecurityUser {
+		
+		@DisplayName("create put user request")
+		@Nested
+		class TestCreatePutUserRequest {
+			
+			@Test
+			void shouldHaveName() {
+				var request = service.createPutUserRequest(PutUserRequestDataTestFactory.create());
+				
+				assertThat(request.username()).isEqualTo(PutUserRequestDataTestFactory.USERNAME);
+			}
+			
+			@Test
+			void shouldHaveRoles() {
+				var request = service.createPutUserRequest(PutUserRequestDataTestFactory.create());
+				
+				assertThat(request.roles()).containsExactly(PutUserRequestDataTestFactory.ROLES);
+			}
+			
+			@Test
+			void shouldHavePassword() {
+				var request = service.createPutUserRequest(PutUserRequestDataTestFactory.create());
+				
+				assertThat(request.password()).isEqualTo(PutUserRequestDataTestFactory.PASSWORD);
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchTestClient.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchTestClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac592cd1c66251c7d8185afaf8496f0fbb314583
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticsearchTestClient.java
@@ -0,0 +1,59 @@
+package de.ozgcloud.operator.common.elasticsearch;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.elasticsearch.client.RestClient;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.json.jackson.JacksonJsonpMapper;
+import co.elastic.clients.transport.rest_client.RestClientTransport;
+
+public class ElasticsearchTestClient {
+	
+	private static final String IMAGE_NAME = "docker.elastic.co/elasticsearch/elasticsearch:8.11.3";
+	private static final String USER = "elastic";
+	private static final String PASSWORD = "s3cret";
+	private static final int PORT = 9200;
+	private static final String SCHEME = "https";
+	private static final Duration STARTUP_TIMEOUT = Duration.of(2, ChronoUnit.MINUTES);
+	 
+	public static final ElasticsearchContainer ELASTICSEARCH_CONTAINER = new ElasticsearchContainer(IMAGE_NAME)
+			.withExposedPorts(PORT)
+			.withPassword(PASSWORD)
+			.withStartupTimeout(STARTUP_TIMEOUT);
+
+	public static ElasticsearchClient create() {
+		var transport = new RestClientTransport(buildRestClient(), new JacksonJsonpMapper());
+		return new ElasticsearchClient(transport);
+	}
+	
+	private static RestClient buildRestClient() {
+		var host = new HttpHost("localhost", ELASTICSEARCH_CONTAINER.getMappedPort(PORT), SCHEME);
+		var credentialsProvider = buildCredentialsProvider();
+		var builder = RestClient.builder(host);
+
+		builder.setHttpClientConfigCallback(clientBuilder -> {
+			clientBuilder.setSSLContext(ELASTICSEARCH_CONTAINER.createSslContextFromCa());
+			clientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+			return clientBuilder;
+		});
+//		builder.setNodeSelector(INGEST_NODE_SELECTOR);
+//        final ObjectMapper mapper = new ObjectMapper();
+//        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
+//        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+		return builder.build();
+	}
+	
+	private static BasicCredentialsProvider buildCredentialsProvider() {
+		var credentialsProvider = new BasicCredentialsProvider();
+		credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(USER, PASSWORD));
+		return credentialsProvider;
+	}
+
+}
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteServiceITCase.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteServiceITCase.java
new file mode 100644
index 0000000000000000000000000000000000000000..847b78880357e5939b4112a7e5131274d697a90e
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteServiceITCase.java
@@ -0,0 +1,52 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.extension.ResourceAdapter;
+import io.javaoperatorsdk.jenvtest.junit.EnableKubeAPIServer;
+
+@EnableKubeAPIServer
+class KubernetesRemoteServiceITCase {
+
+	private static KubernetesClient client;
+	private final KubernetesRemoteService service = new KubernetesRemoteService(client);
+
+	@DisplayName("Get secret resource")
+	@Nested
+	class TestGetSecretResource {
+		
+		@Test
+		void shouldReturnEmptyResourceIfMissing() {
+			var resource = service.getSecretResource(NamespaceTestFactory.NAMESPACE, SecretTestFactory.NAME);
+			
+			assertThat(resource.get()).isNull();
+		}
+		
+		@Test
+		void shouldReturnSecretResourceIfExists() {
+			createNamespace();
+			createSecret();
+			
+			var resource = service.getSecretResource(NamespaceTestFactory.NAMESPACE, SecretTestFactory.NAME);
+			
+			assertThat(resource.get()).isNotNull();
+		}
+		
+		private void createNamespace() {
+			var resource = client.namespaces().withName(NamespaceTestFactory.NAMESPACE);
+			var adapter = new ResourceAdapter<>(resource);
+			adapter.create(NamespaceTestFactory.create());
+		}
+		
+		private void createSecret() {
+			var resource = client.secrets().inNamespace(NamespaceTestFactory.NAMESPACE).withName(SecretTestFactory.NAME);
+			var adapter = new ResourceAdapter<>(resource);
+			adapter.create(SecretTestFactory.create());
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteServiceTest.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d650f5b76e9fe1e0af7a0ec8a6c412cf35efd34
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesRemoteServiceTest.java
@@ -0,0 +1,63 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.Rule;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.extension.ResourceAdapter;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+
+class KubernetesRemoteServiceTest {
+
+	@Rule
+	private KubernetesServer server;
+	private KubernetesClient client;
+	private KubernetesRemoteService service;
+
+	@BeforeEach
+	void init() {
+		server = new KubernetesServer(true, true);
+		server.before();
+		client = server.getClient();
+		service = new KubernetesRemoteService(client);
+	}
+
+	@DisplayName("Get secret")
+	@Nested
+	class TestGetSecret {
+		
+		private Secret secret = SecretTestFactory.create();
+		
+		@Test
+		void shouldReturnExistingResourceIfExists() {
+			createSecret();
+			
+			var secret = getSecret();
+
+			assertThat(secret).isNotNull().isEqualTo(secret);
+		}
+		
+		private void createSecret() {
+			var secretResource = client.secrets().withName(SecretTestFactory.NAME);
+			var adapter = new ResourceAdapter<>(secretResource);
+			adapter.create(secret);
+		}
+
+		@Test
+		void shouldReturnNullNOTExists() {
+			var secret = getSecret();
+
+			assertThat(secret).isNull();
+		}
+		
+		private Secret getSecret() {
+			return service.getSecretResource(NamespaceTestFactory.NAMESPACE, SecretTestFactory.NAME).get();
+		}
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/NamespaceTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/NamespaceTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4982af7f0898f21c29810ecead06822087ba5c1
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/NamespaceTestFactory.java
@@ -0,0 +1,17 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import io.fabric8.kubernetes.api.model.Namespace;
+import io.fabric8.kubernetes.api.model.NamespaceBuilder;
+
+public class NamespaceTestFactory {
+	
+	public static final String NAMESPACE ="test-namespace";
+
+	public static final Namespace create() {
+		return createBuilder().build();
+	}
+	
+	public static NamespaceBuilder createBuilder() {
+		return new NamespaceBuilder().withNewMetadata().withName(NAMESPACE).endMetadata();
+	}
+}
diff --git a/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/SecretTestFactory.java b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/SecretTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..9820f6ec977ce5b615273d13f3487d3b492f4f28
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/SecretTestFactory.java
@@ -0,0 +1,20 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+
+public class SecretTestFactory {
+	
+	public final static String NAME = "secret-name";
+
+	public static final Secret create() {
+		return createBuilder().build();
+	}
+	
+	public static final SecretBuilder createBuilder() {
+		var builder = new SecretBuilder();
+		builder.withNewMetadata().withName(NAME).withNamespace(NamespaceTestFactory.NAMESPACE).endMetadata().build();
+	
+		return builder;
+	}
+}
diff --git a/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/ozgcloud-elasticsearch-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
similarity index 100%
rename from src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
rename to ozgcloud-elasticsearch-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
diff --git a/ozgcloud-elasticsearch-operator/src/test/resources/application.yml b/ozgcloud-elasticsearch-operator/src/test/resources/application.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dee0cbb80474c74bd0fff2abdbc95dab71ddfc34
--- /dev/null
+++ b/ozgcloud-elasticsearch-operator/src/test/resources/application.yml
@@ -0,0 +1,12 @@
+ozgcloud:
+  elasticsearch:
+    secretCredentialsName: elasticsearch-credentials
+    certificateSecretName: elasticsearch-certificate
+    server:
+      namespace: elastic-system
+      secretName: ozg-search-cluster-es-elastic-user
+      secretDataKey: elastic
+      host: ozg-search-cluster-es-http.${ozgcloud.elasticsearch.server.namespace}
+      port: 9200
+      scheme: https
+      certificateSecretName: elasticsearch-certificate
\ No newline at end of file
diff --git a/src/test/resources/junit-platform.properties b/ozgcloud-elasticsearch-operator/src/test/resources/junit-platform.properties
similarity index 100%
rename from src/test/resources/junit-platform.properties
rename to ozgcloud-elasticsearch-operator/src/test/resources/junit-platform.properties
diff --git a/README.md b/ozgcloud-keycloak-operator/README.MD
similarity index 100%
rename from README.md
rename to ozgcloud-keycloak-operator/README.MD
diff --git a/doc/examples/client-alfa.yaml b/ozgcloud-keycloak-operator/doc/examples/client-alfa.yaml
similarity index 100%
rename from doc/examples/client-alfa.yaml
rename to ozgcloud-keycloak-operator/doc/examples/client-alfa.yaml
diff --git a/doc/examples/realm-berlin.yaml b/ozgcloud-keycloak-operator/doc/examples/realm-berlin.yaml
similarity index 100%
rename from doc/examples/realm-berlin.yaml
rename to ozgcloud-keycloak-operator/doc/examples/realm-berlin.yaml
diff --git a/doc/examples/user-helge.yaml b/ozgcloud-keycloak-operator/doc/examples/user-helge.yaml
similarity index 100%
rename from doc/examples/user-helge.yaml
rename to ozgcloud-keycloak-operator/doc/examples/user-helge.yaml
diff --git a/mvnw b/ozgcloud-keycloak-operator/mvnw
similarity index 100%
rename from mvnw
rename to ozgcloud-keycloak-operator/mvnw
diff --git a/mvnw.cmd b/ozgcloud-keycloak-operator/mvnw.cmd
similarity index 100%
rename from mvnw.cmd
rename to ozgcloud-keycloak-operator/mvnw.cmd
diff --git a/ozgcloud-keycloak-operator/pom.xml b/ozgcloud-keycloak-operator/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cb36f4172e4fe6b8e5838866e9c5c36b6b8f9e13
--- /dev/null
+++ b/ozgcloud-keycloak-operator/pom.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	
+	<parent>
+		<groupId>de.ozgcloud</groupId>
+		<artifactId>ozgcloud-operator-parent</artifactId>
+		<version>2.1.0-SNAPSHOT</version>
+		<relativePath>../</relativePath>
+	</parent>
+
+	<artifactId>ozgcloud-keycloak-operator</artifactId>
+	<packaging>jar</packaging>
+	
+	<name>OzgCloud Keycloak Operator</name>
+	<description>OzgCloud Keycloak Operator</description>
+	
+	<properties>
+		<spring-boot.build-image.imageName>docker.ozg-sh.de/ozgcloud-keycloak-operator:build-latest</spring-boot.build-image.imageName>
+	</properties>
+		
+	<dependencies>
+		<!-- spring -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>io.javaoperatorsdk</groupId>
+			<artifactId>operator-framework-spring-boot-starter</artifactId>
+		</dependency>
+		
+		<!-- keycloak -->
+		<dependency>
+		    <groupId>org.keycloak</groupId>
+		    <artifactId>keycloak-admin-client</artifactId>
+		</dependency>
+		
+		<!-- tools -->
+		<dependency>
+		    <groupId>org.mapstruct</groupId>
+		    <artifactId>mapstruct</artifactId>
+		</dependency>
+		
+		<!-- javax -->
+		<dependency>
+	    	<groupId>javax.validation</groupId>
+	    	<artifactId>validation-api</artifactId>
+		</dependency>
+		<dependency>
+		    <groupId>jakarta.xml.bind</groupId>
+		    <artifactId>jakarta.xml.bind-api</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+		</dependency>
+		<dependency>
+		    <groupId>org.reflections</groupId>
+		    <artifactId>reflections</artifactId>
+		</dependency>		
+		<dependency>
+			<groupId>commons-beanutils</groupId>
+			<artifactId>commons-beanutils</artifactId>
+		</dependency>
+		
+		<!-- test -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-engine</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-params</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>			
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<fork>true</fork>
+					<annotationProcessorPaths>
+						<path>
+							<groupId>org.projectlombok</groupId>
+							<artifactId>lombok</artifactId>
+							<version>${lombok.version}</version>
+						</path>
+						<path>
+							<groupId>org.mapstruct</groupId>
+							<artifactId>mapstruct-processor</artifactId>
+							<version>${mapstruct.version}</version>
+						</path>
+					</annotationProcessorPaths>
+					<showWarnings>true</showWarnings>
+					<compilerArgs>
+						<compilerArg>
+							-Amapstruct.defaultComponentModel=spring
+						</compilerArg>
+						<compilerArg>
+							-Amapstruct.unmappedTargetPolicy=WARN
+						</compilerArg>
+					</compilerArgs>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+	<distributionManagement>
+		<repository>
+			<id>ozg-nexus</id>
+			<name>ozg-releases</name>
+			<url>https://nexus.ozg-sh.de/repository/ozg-releases/</url>
+		</repository>
+		<snapshotRepository>
+			<id>ozg-snapshots-nexus</id>
+			<name>ozg-snapshots</name>
+			<url>https://nexus.ozg-sh.de/repository/ozg-snapshots/</url>
+		</snapshotRepository>
+	</distributionManagement>
+	
+</project>
diff --git a/run-local.sh b/ozgcloud-keycloak-operator/run-local.sh
similarity index 100%
rename from run-local.sh
rename to ozgcloud-keycloak-operator/run-local.sh
diff --git a/ozgcloud-keycloak-operator/run_helm_test.sh b/ozgcloud-keycloak-operator/run_helm_test.sh
new file mode 100755
index 0000000000000000000000000000000000000000..8097a39a04ff100d5c9350e205d942100dd37894
--- /dev/null
+++ b/ozgcloud-keycloak-operator/run_helm_test.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -e
+
+helm template  ./src/main/helm/ -f src/test/helm/linter_values.yaml
+helm lint -f src/test/helm/linter_values.yaml ./src/main/helm/
+cd src/main/helm && helm unittest --helm3 -f '../../test/helm/*/*.yaml' -f '../../test/helm/*.yaml' .
diff --git a/samples/crd/KeycloakUser b/ozgcloud-keycloak-operator/samples/crd/KeycloakUser
similarity index 100%
rename from samples/crd/KeycloakUser
rename to ozgcloud-keycloak-operator/samples/crd/KeycloakUser
diff --git a/src/main/helm/Chart.yaml b/ozgcloud-keycloak-operator/src/main/helm/Chart.yaml
similarity index 100%
rename from src/main/helm/Chart.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/Chart.yaml
diff --git a/src/main/helm/templates/_helpers.tpl b/ozgcloud-keycloak-operator/src/main/helm/templates/_helpers.tpl
similarity index 100%
rename from src/main/helm/templates/_helpers.tpl
rename to ozgcloud-keycloak-operator/src/main/helm/templates/_helpers.tpl
diff --git a/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakClient.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakClient.yaml
similarity index 100%
rename from src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakClient.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakClient.yaml
diff --git a/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakGroup.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakGroup.yaml
similarity index 100%
rename from src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakGroup.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakGroup.yaml
diff --git a/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakRealms.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakRealms.yaml
similarity index 100%
rename from src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakRealms.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakRealms.yaml
diff --git a/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakUser.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakUser.yaml
similarity index 100%
rename from src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakUser.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/crds/operator.ozgcloud.de_OzgKeycloakUser.yaml
diff --git a/src/main/helm/templates/deployment.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/deployment.yaml
similarity index 100%
rename from src/main/helm/templates/deployment.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/deployment.yaml
diff --git a/src/main/helm/templates/image_pull_secret.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/image_pull_secret.yaml
similarity index 100%
rename from src/main/helm/templates/image_pull_secret.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/image_pull_secret.yaml
diff --git a/src/main/helm/templates/rbacs/keycloak_admin_secret_read.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/keycloak_admin_secret_read.yaml
similarity index 100%
rename from src/main/helm/templates/rbacs/keycloak_admin_secret_read.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/keycloak_admin_secret_read.yaml
diff --git a/src/main/helm/templates/rbacs/keycloak_read.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/keycloak_read.yaml
similarity index 100%
rename from src/main/helm/templates/rbacs/keycloak_read.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/keycloak_read.yaml
diff --git a/src/main/helm/templates/rbacs/keycloak_write.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/keycloak_write.yaml
similarity index 100%
rename from src/main/helm/templates/rbacs/keycloak_write.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/keycloak_write.yaml
diff --git a/src/main/helm/templates/rbacs/serviceaccount.yaml b/ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/serviceaccount.yaml
similarity index 100%
rename from src/main/helm/templates/rbacs/serviceaccount.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/templates/rbacs/serviceaccount.yaml
diff --git a/src/main/helm/values.yaml b/ozgcloud-keycloak-operator/src/main/helm/values.yaml
similarity index 100%
rename from src/main/helm/values.yaml
rename to ozgcloud-keycloak-operator/src/main/helm/values.yaml
diff --git a/src/main/java/de/ozgcloud/operator/Config.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/Config.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/Config.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/Config.java
diff --git a/src/main/java/de/ozgcloud/operator/OzgOperatorApplication.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/KeycloakOperatorApplication.java
similarity index 91%
rename from src/main/java/de/ozgcloud/operator/OzgOperatorApplication.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/KeycloakOperatorApplication.java
index 454595caa34862daf2388138d8f290ff6bb8ea76..27c3b688cde8c4fcdb5cfe23c058ffc9c2a5f192 100644
--- a/src/main/java/de/ozgcloud/operator/OzgOperatorApplication.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/KeycloakOperatorApplication.java
@@ -27,9 +27,9 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 @SpringBootApplication
-public class OzgOperatorApplication {
+public class KeycloakOperatorApplication {
 
 	public static void main(String[] args) {
-		SpringApplication.run(OzgOperatorApplication.class, args);
+		SpringApplication.run(KeycloakOperatorApplication.class, args);
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/SpringNativeConfiguration.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/SpringNativeConfiguration.java
similarity index 95%
rename from src/main/java/de/ozgcloud/operator/SpringNativeConfiguration.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/SpringNativeConfiguration.java
index 60899d6ae6d55ebbb760f7bfd54f897f56d2df2d..7209006b8ae74602b72a9a1ef450c6191da8484e 100644
--- a/src/main/java/de/ozgcloud/operator/SpringNativeConfiguration.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/SpringNativeConfiguration.java
@@ -36,10 +36,10 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
 
 import io.fabric8.kubernetes.api.model.KubernetesResource;
 import io.fabric8.kubernetes.api.model.NamedCluster;
-import lombok.extern.slf4j.Slf4j;
+import lombok.extern.log4j.Log4j2;
 
-@Slf4j
-public class SpringNativeConfiguration {
+@Log4j2
+class SpringNativeConfiguration {
 
 	static class KuberenetesCLientImplHints implements RuntimeHintsRegistrar {
 
@@ -96,9 +96,7 @@ public class SpringNativeConfiguration {
 		}
 
 		private void register(RuntimeHints hints, Class<?> clazz) {
-			if (log.isDebugEnabled()) {
-				log.debug("trying to register " + clazz.getName() + " for reflection");
-			}
+			LOG.debug("trying to register {} for reflection.", clazz.getName());
 			hints.reflection().registerType(clazz, MemberCategory.values());
 		}
 	}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
similarity index 86%
rename from src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
index 8a92e32ac943a5d05850ff5d0d38a5358c7a0e96..28bba540623182bdf36b18f026d2c0d83eecb6b8 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
@@ -27,7 +27,6 @@ import java.util.Base64;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.admin.client.KeycloakBuilder;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Scope;
@@ -35,21 +34,30 @@ import org.springframework.context.annotation.Scope;
 import io.fabric8.kubernetes.api.model.Secret;
 import io.fabric8.kubernetes.client.KubernetesClient;
 import io.fabric8.kubernetes.client.dsl.Resource;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Configuration
 public class KeycloakClient {
 
-	@Autowired
-	private KubernetesClient kubernetesClient;
+	private final KubernetesClient kubernetesClient;
 
 	@Bean
 	@Scope("singleton")
 	Keycloak createKeycloak() {
+		setProperties();
+		return buildKeycloak();
+	}
+
+	private void setProperties() {
 		System.setProperty("com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump", "true");
 		System.setProperty("com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump", "true");
 		System.setProperty("com.sun.xml.ws.transport.http.HttpAdapter.dump", "true");
 		System.setProperty("com.sun.xml.internal.ws.transport.http.HttpAdapter.dump", "true");
 		System.setProperty("com.sun.xml.internal.ws.transport.http.HttpAdapter.dumpTreshold", "999999");
+	}
+
+	private Keycloak buildKeycloak() {
 		return KeycloakBuilder.builder()
 				.serverUrl("http://keycloak-keycloakx-http.keycloak")
 				.realm("master")
@@ -59,20 +67,20 @@ public class KeycloakClient {
 				.build();
 	}
 
-	String getKeycloakAdminPassword() {
+	private String getKeycloakAdminPassword() {
 		return decodeBase64(getKeycloakRealmAdminSecret()
 				.get()
 				.getData()
 				.get("password"));
 	}
 
-	Resource<Secret> getKeycloakRealmAdminSecret() {
+	private Resource<Secret> getKeycloakRealmAdminSecret() {
 		return kubernetesClient.secrets()
 				.inNamespace("keycloak")
 				.withName("keycloak-admin-secret");
 	}
 
-	String decodeBase64(String base64String) {
+	private String decodeBase64(String base64String) {
 		return new String(Base64.getDecoder().decode(base64String));
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteService.java
similarity index 54%
rename from src/main/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteService.java
index fd1e5ad4a2800edc7a50a8adad0a44976249cba5..53e57ffed8c03c1406bc0ecdd4c877b35873aed9 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteService.java
@@ -1,47 +1,21 @@
 package de.ozgcloud.operator.keycloak;
 
-/*
- * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
 import java.util.Objects;
 import java.util.Optional;
-import java.util.logging.Level;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
-@Log
 public class KeycloakGenericRemoteService {
 
-	@Autowired
-	private Keycloak keycloak;
+	private final Keycloak keycloak;
 
 	public Optional<ClientRepresentation> getByClientId(String clientId, String realm) {
 		return keycloak.realm(realm).clients().findAll().stream()
@@ -61,7 +35,6 @@ public class KeycloakGenericRemoteService {
 	}
 
 	public Optional<RoleRepresentation> getClientRole(String roleName, String realClientId, String realm) {
-		log.log(Level.INFO, "Get role " + roleName + " from client with ID " + realClientId + " in realm " + realm);
 		return Optional.ofNullable(keycloak.realm(realm).clients().get(realClientId))
 				.orElseThrow(() -> new KeycloakException("Client with ID " + realClientId + " for realm " + realm + " not found."))
 				.roles()
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakResultParser.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakResultParser.java
similarity index 92%
rename from src/main/java/de/ozgcloud/operator/keycloak/KeycloakResultParser.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakResultParser.java
index 70320488270f14ee88f82c14168a29561d0b576c..28e044c19a17e877843f687c1bdcfd2a6e212f71 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakResultParser.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/KeycloakResultParser.java
@@ -26,15 +26,14 @@ package de.ozgcloud.operator.keycloak;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.logging.Level;
 
 import javax.ws.rs.core.Response;
 
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
-import lombok.extern.java.Log;
+import lombok.extern.log4j.Log4j2;
 
-@Log
+@Log4j2
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class KeycloakResultParser {
 
@@ -43,7 +42,7 @@ public class KeycloakResultParser {
 			String body = null;
 			if (response.hasEntity() && response.getEntity() instanceof InputStream inputStream) {
 				body = new String(inputStream.readNBytes(256), StandardCharsets.UTF_8);
-				log.info("Entity: " + body);
+				LOG.info("Entity: {}.", body);
 			}
 			if (!response.getStatusInfo().equals(Response.Status.CREATED)) {
 				Response.StatusType statusInfo = response.getStatusInfo();
@@ -52,7 +51,7 @@ public class KeycloakResultParser {
 						"expected status: Created (201) with Body: " + body);
 			}
 		} catch (IOException e) {
-			log.log(Level.SEVERE, "Could not parse Keycloak Response", e);
+			LOG.error("Could not parse Keycloak Response", e);
 		}
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/OzgCloudCustomResourceStatus.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/OzgCloudCustomResourceStatus.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/OzgCloudCustomResourceStatus.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/OzgCloudCustomResourceStatus.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapper.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapper.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapper.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapper.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionService.java
similarity index 86%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionService.java
index 93b4130f0c71831f12c882667e5038c1aebc34e3..7491e77768576a3fb8a39defccc618bc2b91ed68 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionService.java
@@ -25,18 +25,18 @@ package de.ozgcloud.operator.keycloak.client;
 
 import java.util.Optional;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
 class KeycloakClientPreconditionService {
 
-	@Autowired
-	private KeycloakGenericRemoteService keycloakGenericRemoteService;
+	private final KeycloakGenericRemoteService keycloakGenericRemoteService;
 
-	Optional<String> getReconcilePreconditionErrors(OzgCloudKeycloakClient resource) {
+	public Optional<String> getReconcilePreconditionErrors(OzgCloudKeycloakClient resource) {
 
 		String namespace = resource.getMetadata().getNamespace();
 
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconciler.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconciler.java
similarity index 80%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconciler.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconciler.java
index 11f4f71bddbd9bbb8f8b9c181a861dd6bd85a75b..a4c174f7147e0abd32007d20e16662f8e337fac5 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconciler.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconciler.java
@@ -24,9 +24,7 @@
 package de.ozgcloud.operator.keycloak.client;
 
 import java.util.Optional;
-import java.util.logging.Level;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.Config;
@@ -35,26 +33,24 @@ import io.javaoperatorsdk.operator.api.reconciler.Context;
 import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
 import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
 import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
+@RequiredArgsConstructor
 @ControllerConfiguration
 @Component
-@Log
+@Log4j2
 public class KeycloakClientReconciler implements Reconciler<OzgCloudKeycloakClient> {
 
-	@Autowired
-	private KeycloakClientService service;
+	private final KeycloakClientService service;
 
-	@Autowired
-	private KeycloakClientPreconditionService preconditionService;
+	private final KeycloakClientPreconditionService preconditionService;
 
 	@Override
 	public UpdateControl<OzgCloudKeycloakClient> reconcile(OzgCloudKeycloakClient resource, Context<OzgCloudKeycloakClient> context) {
 
 		try {
-			String crdName = resource.getMetadata().getName();
-
-			log.info("Reconcile KeycloakClient " + crdName);
+			LOG.info("{} reconciling...", resource.getMetadata().getName());
 
 			Optional<String> preconditionError = preconditionService.getReconcilePreconditionErrors(resource);
 			if (preconditionError.isPresent()) {
@@ -65,10 +61,7 @@ public class KeycloakClientReconciler implements Reconciler<OzgCloudKeycloakClie
 
 			return buildStatusOk(resource);
 		} catch (Exception e) {
-			log.log(Level.SEVERE,
-					"Could not reconcile client " + resource.getMetadata().getName() + " in namespace " + resource.getMetadata().getNamespace() + ": "
-							+ e.getMessage(),
-					e);
+			LOG.error(resource.getMetadata().getName() + " could not reconcile in namespace " + resource.getMetadata().getNamespace(), e);
 			resource.setStatus(OzgCloudKeycloakClientStatus.builder().status(OzgCloudCustomResourceStatus.ERROR).message(e.getMessage()).build());
 			return UpdateControl.updateStatus(resource).rescheduleAfter(Config.RECONCILER_RETRY_SECONDS);
 		}
@@ -80,9 +73,8 @@ public class KeycloakClientReconciler implements Reconciler<OzgCloudKeycloakClie
 	}
 
 	private UpdateControl<OzgCloudKeycloakClient> buildStatusInProgress(OzgCloudKeycloakClient resource, String errorMessage) {
-		log.log(Level.INFO,
-				"Could not yet reconcile client " + resource.getMetadata().getName() + " in namespace " + resource.getMetadata().getNamespace() + ":"
-						+ errorMessage);
+		LOG.info("{} could not yet reconcile in namespace {}: {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace(),
+				errorMessage);
 		resource.setStatus(OzgCloudKeycloakClientStatus.builder().status(OzgCloudCustomResourceStatus.IN_PROGRESS).message(errorMessage).build());
 		return UpdateControl.updateStatus(resource).rescheduleAfter(Config.RECONCILER_RETRY_SECONDS);
 	}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteService.java
similarity index 89%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteService.java
index 7c3d4c1bf7c6748c85a0df583d46c60faae9356d..c5e0e4f65d07e4dcf89e124ff1198569c861a786 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteService.java
@@ -23,33 +23,31 @@
  */
 package de.ozgcloud.operator.keycloak.client;
 
-import java.util.logging.Level;
-
 import org.keycloak.admin.client.CreatedResponseUtil;
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakResultParser;
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
+@Log4j2
+@RequiredArgsConstructor
 @Component
-@Log
 class KeycloakClientRemoteService {
 
-	@Autowired
-	private Keycloak keycloak;
+	private final Keycloak keycloak;
 
 	public void updateClient(ClientRepresentation client, String realm) {
 		getClientResource(realm, client.getId()).update(client);
 	}
 
 	public String createClient(ClientRepresentation client, String realm) {
-		log.log(Level.FINE, "Creating client {0} in realm {1}", new String[] { client.getId(), realm });
+		LOG.debug("Creating client {} in realm {}", client.getId(), realm);
 		var response = getRealm(realm).clients().create(client);
 		KeycloakResultParser.parseCreatedResponse(response);
 		return CreatedResponseUtil.getCreatedId(response);
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientService.java
similarity index 90%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientService.java
index 90e9671841a6e00f29ebc63ff904e8a0c9cb1956..3182648ff9b5e59ac3176bc87b27389a1e51125d 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/KeycloakClientService.java
@@ -26,25 +26,22 @@ package de.ozgcloud.operator.keycloak.client;
 import java.util.List;
 
 import org.keycloak.representations.idm.ClientRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
 class KeycloakClientService {
 
-	@Autowired
-	private KeycloakClientRemoteService remoteService;
+	private final KeycloakClientRemoteService remoteService;
 
-	@Autowired
-	private KeycloakGenericRemoteService genericRemoteService;
+	private final KeycloakGenericRemoteService genericRemoteService;
 
-	@Autowired
-	private ProtocolMapperRepresentationHelper mapperRepresentationBuilder;
+	private final ProtocolMapperRepresentationHelper mapperRepresentationBuilder;
 
-	@Autowired
-	private KeycloakClientMapper mapper;
+	private final KeycloakClientMapper mapper;
 
 	void createOrUpdateClient(OzgCloudKeycloakClientSpec spec, String namespace) {
 		genericRemoteService.getByClientId(spec.getClientId(), namespace)
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClient.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClient.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClient.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClient.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpec.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpec.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpec.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpec.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatus.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatus.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatus.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatus.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelper.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelper.java
similarity index 94%
rename from src/main/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelper.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelper.java
index 402c3eee8f6957cdecaaab0192dd6b172ff45626..2c38785d4c3f8e3d7dde8fcf6f1e9617f11cefb1 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelper.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelper.java
@@ -2,15 +2,14 @@ package de.ozgcloud.operator.keycloak.client;
 
 import java.util.HashMap;
 import java.util.Map;
-import java.util.logging.Level;
 
 import org.apache.commons.lang3.StringUtils;
 import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 import org.springframework.stereotype.Component;
 
-import lombok.extern.java.Log;
+import lombok.extern.log4j.Log4j2;
 
-@Log
+@Log4j2
 @Component
 class ProtocolMapperRepresentationHelper {
 
@@ -37,7 +36,7 @@ class ProtocolMapperRepresentationHelper {
 	static final String CLIENT_ROLES_MAPPER_NAME = "client roles";
 
 	public ProtocolMapperRepresentation createOrganisationsEinheitIdMapper() {
-		log.log(Level.FINE, "Create createOrganisationsEinheitIdMapper...");
+		LOG.debug("Create createOrganisationsEinheitIdMapper...");
 		return createUserAttributeMapper(ORGANISATIONS_EINHEIT_ID_MAPPER_NAME, buildOrganisationsEinheitIdMapperConfig());
 	}
 
@@ -55,7 +54,7 @@ class ProtocolMapperRepresentationHelper {
 	}
 
 	public ProtocolMapperRepresentation createOzgCloudUserIdMapper() {
-		log.log(Level.FINE, "Create createOzgCloudUserIdMapper...");
+		LOG.debug("Create createOzgCloudUserIdMapper...");
 		return createUserAttributeMapper(OZGCLOUD_USER_ID_MAPPER_NAME, buildOzgCloudUserIdMapperConfig());
 	}
 
@@ -73,7 +72,7 @@ class ProtocolMapperRepresentationHelper {
 	}
 
 	public ProtocolMapperRepresentation createOrganisationeEinheitIdLdapMapper() {
-		log.log(Level.FINE, "Create createOrganisationeEinheitIdLdapMapper...");
+		LOG.debug("Create createOrganisationeEinheitIdLdapMapper...");
 		return createUserAttributeMapper(ORGANISATIONS_EINHEIT_ID_LDAP_MAPPER_NAME, buildOrganisationsEinheitIdLdapMapperConfig());
 	}
 
@@ -101,7 +100,7 @@ class ProtocolMapperRepresentationHelper {
 	}
 
 	public ProtocolMapperRepresentation createClientRolesMapper() {
-		log.log(Level.FINE, "Create createClientRolesMapper...");
+		LOG.debug("Create createOrganisationeEinheitIdLdapMapper...");
 		return createUserClientRoleMapper(CLIENT_ROLES_MAPPER_NAME, buildClientRolesMapperConfig());
 	}
 
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapper.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapper.java
similarity index 89%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapper.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapper.java
index 519f6617a41e3b3a206e37fc3331958579c69adf..7c7b782fd244c677b1a37d4bc0d1848225f74d6a 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapper.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapper.java
@@ -38,10 +38,11 @@ import org.mapstruct.ReportingPolicy;
 interface KeycloakGroupMapper {
 
 	@Mapping(target = "attributes", source = "attributes", qualifiedByName = "mapAttributes")
-	GroupRepresentation map(OzgCloudKeycloakGroupSpec group);
+	public GroupRepresentation map(OzgCloudKeycloakGroupSpec group);
 
 	@Named("mapAttributes")
 	default Map<String, List<String>> mapAttributes(List<OzgCloudKeycloakGroupSpec.Attribute> attributes) {
-		return attributes.stream().collect(Collectors.toMap(OzgCloudKeycloakGroupSpec.Attribute::getName, attribute -> List.of(attribute.getValue())));
+		return attributes.stream()
+				.collect(Collectors.toMap(OzgCloudKeycloakGroupSpec.Attribute::getName, attribute -> List.of(attribute.getValue())));
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionService.java
similarity index 82%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionService.java
index 0f6571bf732fc8156724b2b52c980f4f18d5be05..56477e69bc54f2f90af087697dad20658a913db6 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionService.java
@@ -25,20 +25,19 @@ package de.ozgcloud.operator.keycloak.group;
 
 import java.util.Optional;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
 class KeycloakGroupPreconditionService {
 
-	@Autowired
-	private KeycloakGenericRemoteService keycloakGenericRemoteService;
+	private final KeycloakGenericRemoteService keycloakGenericRemoteService;
 
-	Optional<String> getReconcilePreconditionErrors(OzgCloudKeycloakGroup resource) {
-
-		String namespace = resource.getMetadata().getNamespace();
+	public Optional<String> getReconcilePreconditionErrors(OzgCloudKeycloakGroup resource) {
+		var namespace = resource.getMetadata().getNamespace();
 
 		if (!keycloakGenericRemoteService.realmExists(namespace)) {
 			return Optional.of("Realm " + namespace + " does not yet exist");
@@ -46,4 +45,4 @@ class KeycloakGroupPreconditionService {
 
 		return Optional.empty();
 	}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconciler.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconciler.java
similarity index 74%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconciler.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconciler.java
index 7388466efab7780dab2f49aeec1d8ba70af92f47..3eea1b1ec467aa1e041e906709f1a31e6bd1bb29 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconciler.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconciler.java
@@ -24,9 +24,7 @@
 package de.ozgcloud.operator.keycloak.group;
 
 import java.util.Optional;
-import java.util.logging.Level;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.Config;
@@ -35,26 +33,23 @@ import io.javaoperatorsdk.operator.api.reconciler.Context;
 import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
 import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
 import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
+@Log4j2
+@RequiredArgsConstructor
 @ControllerConfiguration
 @Component
-@Log
 public class KeycloakGroupReconciler implements Reconciler<OzgCloudKeycloakGroup> {
 
-	@Autowired
-	private KeycloakGroupService service;
+	private final KeycloakGroupService service;
 
-	@Autowired
-	private KeycloakGroupPreconditionService preconditionService;
+	private final KeycloakGroupPreconditionService preconditionService;
 
 	@Override
 	public UpdateControl<OzgCloudKeycloakGroup> reconcile(OzgCloudKeycloakGroup resource, Context<OzgCloudKeycloakGroup> context) {
-
 		try {
-			String crdName = resource.getMetadata().getName();
-
-			log.info("Reconcile KeycloakGroup " + crdName);
+			LOG.info("{} reconiling..", resource.getMetadata().getName());
 
 			Optional<String> preconditionError = preconditionService.getReconcilePreconditionErrors(resource);
 			if (preconditionError.isPresent()) {
@@ -65,24 +60,20 @@ public class KeycloakGroupReconciler implements Reconciler<OzgCloudKeycloakGroup
 
 			return buildStatusOk(resource);
 		} catch (Exception e) {
-			log.log(Level.SEVERE,
-					"Could not reconcile group " + resource.getMetadata().getName() + " in namespace " + resource.getMetadata().getNamespace() + ": "
-							+ e.getMessage(),
-					e);
+			LOG.warn(resource.getMetadata().getName() + " could not reconcile in namespace " + resource.getMetadata().getNamespace(), e);
 			resource.setStatus(OzgCloudKeycloakGroupStatus.builder().status(OzgCloudCustomResourceStatus.ERROR).message(e.getMessage()).build());
 			return UpdateControl.updateStatus(resource).rescheduleAfter(Config.RECONCILER_RETRY_SECONDS);
 		}
 	}
 
-	UpdateControl<OzgCloudKeycloakGroup> buildStatusOk(OzgCloudKeycloakGroup resource) {
+	private UpdateControl<OzgCloudKeycloakGroup> buildStatusOk(OzgCloudKeycloakGroup resource) {
 		resource.setStatus(OzgCloudKeycloakGroupStatus.builder().status(OzgCloudCustomResourceStatus.OK).message(null).build());
 		return UpdateControl.updateStatus(resource);
 	}
 
-	UpdateControl<OzgCloudKeycloakGroup> buildStatusInProgress(OzgCloudKeycloakGroup resource, String errorMessage) {
-		log.log(Level.INFO,
-				"Could not yet reconcile group " + resource.getMetadata().getName() + " in namespace " + resource.getMetadata().getNamespace() + ": "
-						+ errorMessage);
+	private UpdateControl<OzgCloudKeycloakGroup> buildStatusInProgress(OzgCloudKeycloakGroup resource, String errorMessage) {
+		LOG.warn("{} could not yet reconcile group in namespace {}: {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace(),
+				errorMessage);
 		resource.setStatus(OzgCloudKeycloakGroupStatus.builder().status(OzgCloudCustomResourceStatus.IN_PROGRESS).message(errorMessage).build());
 		return UpdateControl.updateStatus(resource).rescheduleAfter(Config.RECONCILER_RETRY_SECONDS);
 	}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteService.java
similarity index 74%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteService.java
index af4c33a57d55aebff5a65159c4d9fbe88d2d4b91..353727e163a313c7b9165f9efac0ea96f5f6faa8 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteService.java
@@ -25,34 +25,31 @@ package de.ozgcloud.operator.keycloak.group;
 
 import java.util.Objects;
 import java.util.Optional;
-import java.util.logging.Level;
-
-import javax.ws.rs.core.Response;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.representations.idm.GroupRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakResultParser;
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
+@Log4j2
+@RequiredArgsConstructor
 @Component
-@Log
 class KeycloakGroupRemoteService {
 
-	@Autowired
-	private Keycloak keycloak;
+	private final Keycloak keycloak;
 
-	Optional<GroupRepresentation> getGroupByName(String groupName, String realm) {
+	public Optional<GroupRepresentation> getGroupByName(String groupName, String realm) {
 		return keycloak.realm(realm).groups().groups()
 				.stream().filter(group -> Objects.equals(groupName, group.getName()))
 				.findFirst();
 	}
 
-	void createGroup(GroupRepresentation group, String realm) {
-		log.log(Level.FINE, "Creating group {0} in realm {1}", new Object[] { group.getName(), realm });
-		Response response = keycloak.realm(realm).groups().add(group);
+	public void createGroup(GroupRepresentation group, String realm) {
+		LOG.debug("Creating group {} in realm {}", group.getName(), realm);
+		var response = keycloak.realm(realm).groups().add(group);
 		KeycloakResultParser.parseCreatedResponse(response);
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupService.java
similarity index 84%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupService.java
index 4eaa2eddbee5b0543f7112a953e0ce6979fe86b3..e8783ffe8c0ebeebf3b215f9edabe73f22d6b86d 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupService.java
@@ -25,19 +25,19 @@ package de.ozgcloud.operator.keycloak.group;
 
 import java.util.Optional;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
 @Component
 class KeycloakGroupService {
 
-	@Autowired
-	private KeycloakGroupRemoteService remoteService;
+	private final KeycloakGroupRemoteService remoteService;
 
-	@Autowired
-	private KeycloakGroupMapper mapper;
+	private final KeycloakGroupMapper mapper;
 
-	void createGroup(OzgCloudKeycloakGroupSpec group, String realm) {
+	public void createGroup(OzgCloudKeycloakGroupSpec group, String realm) {
 		Optional.of(group)
 				.map(mapper::map)
 				.filter(groupRepresentation -> remoteService.getGroupByName(group.getName(), realm).isEmpty())
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroup.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroup.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroup.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroup.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpec.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpec.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpec.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpec.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupStatus.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupStatus.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupStatus.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupStatus.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapper.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapper.java
similarity index 96%
rename from src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapper.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapper.java
index 8b5e7bebf74cfe94492a642ad13b98bb22f31bdd..5af876447d55a10bf33180920c1e9d6a9a79fa2a 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapper.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapper.java
@@ -41,7 +41,7 @@ interface KeycloakRealmMapper {
 	@Mapping(target = "defaultLocale", constant = "de")
 	@Mapping(target = "internationalizationEnabled", constant = "true")
 	@Mapping(target = "passwordPolicy", constant = "upperCase(1) and lowerCase(1) and length(8) and notUsername")
-	RealmRepresentation map(OzgCloudKeycloakRealmSpec realm);
+	public RealmRepresentation map(OzgCloudKeycloakRealmSpec realm);
 
 	@Named("supportedLocales")
 	default Set<String> mapPassword(OzgCloudKeycloakRealmSpec spec) {
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconciler.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconciler.java
similarity index 76%
rename from src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconciler.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconciler.java
index 4b62b2b7b131a179f96330657e862b6e6aecf690..dc3e333e5b42269e4cfe2ec8a00418b676ef595b 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconciler.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconciler.java
@@ -23,9 +23,6 @@
  */
 package de.ozgcloud.operator.keycloak.realm;
 
-import java.util.logging.Level;
-
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.Config;
@@ -36,23 +33,22 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
 import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
 import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
 import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
+@Log4j2
+@RequiredArgsConstructor
 @ControllerConfiguration
 @Component
-@Log
 public class KeycloakRealmReconciler implements Reconciler<OzgCloudKeycloakRealm>, Cleaner<OzgCloudKeycloakRealm> {
 
-	@Autowired
-	private KeycloakRealmService service;
+	private final KeycloakRealmService service;
 
 	@Override
 	public UpdateControl<OzgCloudKeycloakRealm> reconcile(OzgCloudKeycloakRealm resource, Context<OzgCloudKeycloakRealm> context) {
-		log.info("Realm reconciler reconcile, keep after delete is set to: " + resource.getSpec().isKeepAfterDelete());
+		LOG.info("{} reconciling...", resource.getMetadata().getName());
 		try {
-			String realmName = resource.getMetadata().getNamespace();
-
-			log.info("Reconcile KeycloakRealm " + realmName + " (crd name " + resource.getMetadata().getName() + ")");
+			var realmName = resource.getMetadata().getNamespace();
 
 			service.createRealm(resource.getSpec(), realmName);
 
@@ -60,10 +56,7 @@ public class KeycloakRealmReconciler implements Reconciler<OzgCloudKeycloakRealm
 			return UpdateControl.updateStatus(resource);
 
 		} catch (Exception e) {
-			log.log(Level.SEVERE,
-					"Could not reconcile realm " + resource.getMetadata().getName() + " in namespace " + resource.getMetadata().getNamespace() + ": "
-							+ e.getMessage(),
-					e);
+			LOG.warn(resource.getMetadata().getName() + " could not reconcile in namespace " + resource.getMetadata().getNamespace(), e);
 			resource.setStatus(OzgCloudKeycloakRealmStatus.builder().status(OzgCloudCustomResourceStatus.ERROR).message(e.getMessage()).build());
 			return UpdateControl.updateStatus(resource).rescheduleAfter(Config.RECONCILER_RETRY_SECONDS);
 		}
@@ -71,8 +64,9 @@ public class KeycloakRealmReconciler implements Reconciler<OzgCloudKeycloakRealm
 
 	@Override
 	public DeleteControl cleanup(OzgCloudKeycloakRealm realm, Context<OzgCloudKeycloakRealm> context) {
-		log.info("Realm reconciler cleanup, keep after delete is set to: " + realm.getSpec().isKeepAfterDelete());
+		LOG.info("{} cleanup...", realm.getMetadata().getName());
 		if (realm.getSpec().isKeepAfterDelete()) {
+			LOG.info("keep data");
 			return DeleteControl.defaultDelete();
 		}
 		return cleanup(realm);
@@ -80,12 +74,12 @@ public class KeycloakRealmReconciler implements Reconciler<OzgCloudKeycloakRealm
 
 	DeleteControl cleanup(OzgCloudKeycloakRealm realm) {
 		var realmName = realm.getMetadata().getNamespace();
-		log.info("Deleting KeycloakRealm " + realmName);
+		LOG.info("{} do cleanup...", realmName);
 		try {
 			service.deleteRealm(realmName);
 			return DeleteControl.defaultDelete();
 		} catch (Exception e) {
-			log.log(Level.SEVERE, "Could not delete KeycloakRealm " + realmName, e);
+			LOG.warn(realmName + " could not delete.", e);
 			return DeleteControl.defaultDelete();
 		}
 	}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
similarity index 86%
rename from src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
index 97801f718bc4915bb5a61dee50eb55533f85f1e8..124f1d5dc65bcf9a39d05c279e8d8f4ac5b48150 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
@@ -25,20 +25,21 @@ package de.ozgcloud.operator.keycloak.realm;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.representations.idm.RealmRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
 @Component
 class KeycloakRealmRemoteService {
 
-	@Autowired
-	Keycloak keycloak;
+	private final Keycloak keycloak;
 
-	void createRealm(RealmRepresentation realm) {
+	public void createRealm(RealmRepresentation realm) {
 		keycloak.realms().create(realm);
 	}
 
-	void deleteRealm(String realmName) {
+	public void deleteRealm(String realmName) {
 		keycloak.realm(realmName).remove();
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
similarity index 82%
rename from src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
index 6911ce510d6ff3187f86de6bf94794abb7c3e05e..279a05573ba927a7b84b4f76166785b1fb2641e1 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
@@ -26,24 +26,22 @@ package de.ozgcloud.operator.keycloak.realm;
 import java.util.Optional;
 
 import org.keycloak.representations.idm.RealmRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
 class KeycloakRealmService {
 
-	@Autowired
-	private KeycloakRealmRemoteService remoteService;
+	private final KeycloakRealmRemoteService remoteService;
 
-	@Autowired
-	private KeycloakRealmMapper mapper;
+	private final KeycloakRealmMapper mapper;
 
-	@Autowired
-	private KeycloakGenericRemoteService keycloakGenericRemoteService;
+	private final KeycloakGenericRemoteService keycloakGenericRemoteService;
 
-	void createRealm(OzgCloudKeycloakRealmSpec realm, String realmName) {
+	public void createRealm(OzgCloudKeycloakRealmSpec realm, String realmName) {
 		Optional.of(realm)
 				.map(mapper::map)
 				.map(realmRepresentation -> addRealmName(realmRepresentation, realmName))
@@ -56,7 +54,7 @@ class KeycloakRealmService {
 		return realm;
 	}
 
-	void deleteRealm(String realmName) {
+	public void deleteRealm(String realmName) {
 		remoteService.deleteRealm(realmName);
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealm.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealm.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealm.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealm.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpec.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpec.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpec.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpec.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatus.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatus.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatus.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatus.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionService.java
similarity index 95%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionService.java
index 6366465994d50b5f7f97acf82baa6c65e6135ba8..a2a19c68883187f986ebc18e7e1b912a5bd563c4 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionService.java
@@ -25,18 +25,18 @@ package de.ozgcloud.operator.keycloak.user;
 
 import java.util.Optional;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService;
 import de.ozgcloud.operator.keycloak.user.OzgCloudKeycloakUserSpec.KeycloakUserSpecClientRole;
 import de.ozgcloud.operator.keycloak.user.OzgCloudKeycloakUserSpec.KeycloakUserSpecUserGroup;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
 class KeycloakUserPreconditionService {
 
-	@Autowired
-	private KeycloakGenericRemoteService keycloakGenericRemoteService;
+	private final KeycloakGenericRemoteService keycloakGenericRemoteService;
 
 	public Optional<String> getPreconditionErrors(OzgCloudKeycloakUser user) {
 		var namespace = user.getMetadata().getNamespace();
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
similarity index 72%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
index 26aab3f7af098cadf48b91ee7a828a852642543d..6470b4e9c9d664629b23c18b2cbea9694f338cf5 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
@@ -23,11 +23,6 @@
  */
 package de.ozgcloud.operator.keycloak.user;
 
-import java.util.logging.Level;
-
-import javax.ws.rs.NotFoundException;
-
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.Config;
@@ -38,32 +33,31 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
 import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
 import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
 import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
-@Log
+@Log4j2
+@RequiredArgsConstructor
 @ControllerConfiguration
 @Component
 public class KeycloakUserReconciler implements Reconciler<OzgCloudKeycloakUser>, Cleaner<OzgCloudKeycloakUser> {
 
-	@Autowired
-	private KeycloakUserService service;
-	@Autowired
-	private KeycloakUserPreconditionService preconditionService;
+	private final KeycloakUserService service;
+	private final KeycloakUserPreconditionService preconditionService;
 
 	@Override
 	public UpdateControl<OzgCloudKeycloakUser> reconcile(OzgCloudKeycloakUser resource, Context<OzgCloudKeycloakUser> context) {
 		var userName = resource.getMetadata().getName();
 		var namespace = resource.getMetadata().getNamespace();
 
-		log.info(String.format("Reconciling user %s...", userName));
-		log.info("User reconciler reconcile, keep after delete is set to: " + resource.getSpec().isKeepAfterDelete());
+		LOG.info("{} reconciling...", userName);
 
 		try {
 			var preconditionError = preconditionService.getPreconditionErrors(resource);
 			if (preconditionError.isPresent()) {
 				var errorMessage = preconditionError.get();
 
-				log.warning(String.format("Could not reconcile user %s in namespace %s: %s", userName, namespace, errorMessage));
+				LOG.warn("{} could not reconcile in namespace {}: {}.", userName, namespace, errorMessage);
 
 				return UserUpdateControlBuilder.fromResource(resource)
 						.withStatus(OzgCloudCustomResourceStatus.IN_PROGRESS)
@@ -77,13 +71,11 @@ public class KeycloakUserReconciler implements Reconciler<OzgCloudKeycloakUser>,
 			return UserUpdateControlBuilder.fromResource(resource).withStatus(OzgCloudCustomResourceStatus.OK).build();
 
 		} catch (Exception e) {
-			var errorMessage = e.getMessage();
-
-			log.log(Level.SEVERE, String.format("Could not reconcile user %s for namespace %s: %s", userName, namespace, errorMessage), e);
+			LOG.warn(userName + " could not reconcile in namespace " + namespace, e);
 
 			return UserUpdateControlBuilder.fromResource(resource)
 					.withStatus(OzgCloudCustomResourceStatus.ERROR)
-					.withMessage(errorMessage)
+					.withMessage(e.getMessage())
 					.withReschedule(Config.RECONCILER_RETRY_SECONDS_ON_ERROR)
 					.build();
 		}
@@ -91,8 +83,9 @@ public class KeycloakUserReconciler implements Reconciler<OzgCloudKeycloakUser>,
 
 	@Override
 	public DeleteControl cleanup(OzgCloudKeycloakUser user, Context<OzgCloudKeycloakUser> context) {
-		log.info("User reconciler cleanup, keep after delete is set to: " + user.getSpec().isKeepAfterDelete());
+		LOG.info("{} cleanup...", user.getMetadata().getName());
 		if (user.getSpec().isKeepAfterDelete()) {
+			LOG.info("keep data");
 			return DeleteControl.defaultDelete();
 		}
 		return cleanup(user);
@@ -101,15 +94,12 @@ public class KeycloakUserReconciler implements Reconciler<OzgCloudKeycloakUser>,
 	DeleteControl cleanup(OzgCloudKeycloakUser user) {
 		var userName = user.getMetadata().getName();
 		var namespace = user.getMetadata().getNamespace();
-		log.info(String.format("Deleting KeycloakUser %s", userName));
+		LOG.info("{} do cleanup...", userName);
 		try {
 			service.deleteUser(user.getSpec(), namespace);
 			return DeleteControl.defaultDelete();
-		} catch (NotFoundException e) {
-			log.log(Level.INFO, String.format("Could not delete user %s in namespace %s, user not found.", userName, namespace));
-			return DeleteControl.defaultDelete();
 		} catch (Exception e) {
-			log.log(Level.SEVERE, String.format("Could not delete user %s in namespace %s", userName, namespace), e);
+			LOG.warn(userName + " could not delete user in namespace " + namespace, e);
 			return DeleteControl.defaultDelete();
 		}
 	}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
similarity index 95%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
index 4e1d2f02fbfb611f550e9682ef5b0dedd7224d66..b38218f6504b5dc1c0d26f17109151f56695f0c5 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
@@ -32,21 +32,20 @@ import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakException;
 import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService;
 import de.ozgcloud.operator.keycloak.KeycloakResultParser;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
 class KeycloakUserRemoteService {
 
-	@Autowired
-	private Keycloak keycloak;
+	private final Keycloak keycloak;
 
-	@Autowired
-	private KeycloakGenericRemoteService keycloakGenericRemoteService;
+	private final KeycloakGenericRemoteService keycloakGenericRemoteService;
 
 	public void createUser(UserRepresentation user, String namespace) {
 		var realmResource = getRealm(namespace);
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
similarity index 90%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
index 13e2cb942453183040d56a1cc07b7d2127d9eae3..c8343f80700a4d031455c212a3e7ce5a6896378f 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
@@ -26,20 +26,19 @@ package de.ozgcloud.operator.keycloak.user;
 import java.util.Optional;
 
 import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
 @Component
 class KeycloakUserService {
 
-	@Autowired
-	private KeycloakUserRemoteService remoteService;
+	private final KeycloakUserRemoteService remoteService;
 
-	@Autowired
-	private UserSecretService userSecretService;
+	private final UserSecretService userSecretService;
 
-	@Autowired
-	private KeycloakUserMapper userMapper;
+	private final KeycloakUserMapper userMapper;
 
 	public void createOrUpdateUser(OzgCloudKeycloakUserSpec userSpec, String namespace) {
 
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java
similarity index 63%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java
index c865115328e08f8c576fe0337b20a8594fdd4a6f..60d18d2ed869d51469ffc39cd32fe19117b691b2 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java
@@ -1,22 +1,22 @@
 package de.ozgcloud.operator.keycloak.user;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import io.fabric8.kubernetes.api.model.Secret;
 import io.fabric8.kubernetes.client.KubernetesClient;
 import io.fabric8.kubernetes.client.dsl.Resource;
-import lombok.extern.java.Log;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
-@Log
+@RequiredArgsConstructor
+@Log4j2
 @Component
 class KubernetesRemoteService {
 
-	@Autowired
-	private KubernetesClient kubernetesClient;
+	private final KubernetesClient kubernetesClient;
 
 	public Resource<Secret> getSecret(String namespace, String name) {
-		log.info(String.format("KubernetesClient: Get %s secret from %s namespace.", name, namespace));
+		LOG.info("KubernetesClient: Get {} secret from {} namespace.", name, namespace);
 		return kubernetesClient.secrets().inNamespace(namespace).withName(name);
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUser.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUser.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUser.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUser.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpec.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpec.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpec.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpec.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatus.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatus.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatus.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatus.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserNameConverter.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserNameConverter.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/UserNameConverter.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserNameConverter.java
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java
similarity index 83%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java
index e72f995476d93e663608c0152cbffb56908a3043..070fdbf3b593c966ddf462c0a7c45e48bf10f7ca 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java
@@ -1,5 +1,7 @@
 package de.ozgcloud.operator.keycloak.user;
 
+import java.util.Base64;
+
 import org.apache.commons.lang3.RandomStringUtils;
 import org.springframework.stereotype.Component;
 
@@ -20,14 +22,18 @@ class UserSecretBuilder {
 				.withType(SECRET_TYPE)
 				.withMetadata(createMetaData(name, namespace))
 				.addToStringData(SECRET_NAME_FIELD, userSpec.getUsername())
-				.addToStringData(SECRET_PASSWORD_FIELD, generatePassword())
+				.addToData(SECRET_PASSWORD_FIELD, generatePassword())
 				.build();
 	}
 
 	String generatePassword() {
 		var upperCaseCharacter = RandomStringUtils.randomAlphabetic(1).toUpperCase();
 		var randomString = RandomStringUtils.randomAlphanumeric(7);
-		return upperCaseCharacter + randomString;
+		return encode(upperCaseCharacter + randomString);
+	}
+
+	String encode(String strValue) {
+		return new String(Base64.getEncoder().encode(strValue.getBytes()));
 	}
 
 	private ObjectMeta createMetaData(String name, String namespace) {
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java
similarity index 96%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java
index f82e19b6e5b3d9854da2f5322cbb1f4d70c1a49e..8e314c3f1cd83b67809c13d59997c64572012b96 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java
@@ -43,7 +43,7 @@ class UserSecretReader {
 		try {
 			return new String(Base64.decode(encodedPassword));
 		} catch (IOException e) {
-			throw new RuntimeException("Could not decode content from secret (base64) for secret " + secret.getFullResourceName());
+			throw new RuntimeException("Could not decode content from secret (base64) for secret " + secret.getFullResourceName(), e);
 		}
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java
similarity index 82%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java
index 23bab6cdf8a741a590a5a17c3485922046efeacf..a82bc2f9dfe1d7d3203ea815a288acb154b2854d 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java
@@ -2,24 +2,21 @@ package de.ozgcloud.operator.keycloak.user;
 
 import java.util.Optional;
 
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import io.fabric8.kubernetes.api.model.Secret;
 import io.fabric8.kubernetes.client.dsl.Resource;
 import io.fabric8.kubernetes.client.extension.ResourceAdapter;
+import lombok.RequiredArgsConstructor;
 
+@RequiredArgsConstructor
 @Component
 class UserSecretService {
 
-	@Autowired
-	private UserNameConverter userNameConverter;
-	@Autowired
-	private UserSecretBuilder secretBuilder;
-	@Autowired
-	private UserSecretReader secretReader;
-	@Autowired
-	private KubernetesRemoteService kubernetesRemoteService;
+	private final UserNameConverter userNameConverter;
+	private final UserSecretBuilder secretBuilder;
+	private final UserSecretReader secretReader;
+	private final KubernetesRemoteService kubernetesRemoteService;
 
 	public Secret create(OzgCloudKeycloakUserSpec userSpec, String namespace) {
 		var secretName = userNameConverter.toSecretName(userSpec.getKeycloakUser());
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java
similarity index 100%
rename from src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java
diff --git a/src/main/resources/application.yml b/ozgcloud-keycloak-operator/src/main/resources/application.yml
similarity index 100%
rename from src/main/resources/application.yml
rename to ozgcloud-keycloak-operator/src/main/resources/application.yml
diff --git a/src/test/helm/deployment_env_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/deployment_env_test.yaml
similarity index 100%
rename from src/test/helm/deployment_env_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/deployment_env_test.yaml
diff --git a/src/test/helm/deployment_matchlabels_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/deployment_matchlabels_test.yaml
similarity index 100%
rename from src/test/helm/deployment_matchlabels_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/deployment_matchlabels_test.yaml
diff --git a/src/test/helm/deployment_metadata_labels_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/deployment_metadata_labels_test.yaml
similarity index 100%
rename from src/test/helm/deployment_metadata_labels_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/deployment_metadata_labels_test.yaml
diff --git a/src/test/helm/deployment_pull_secret_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/deployment_pull_secret_test.yaml
similarity index 100%
rename from src/test/helm/deployment_pull_secret_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/deployment_pull_secret_test.yaml
diff --git a/src/test/helm/deployment_resources_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/deployment_resources_test.yaml
similarity index 100%
rename from src/test/helm/deployment_resources_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/deployment_resources_test.yaml
diff --git a/src/test/helm/deployment_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/deployment_test.yaml
similarity index 100%
rename from src/test/helm/deployment_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/deployment_test.yaml
diff --git a/src/test/helm/image_pull_secret_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/image_pull_secret_test.yaml
similarity index 100%
rename from src/test/helm/image_pull_secret_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/image_pull_secret_test.yaml
diff --git a/src/test/helm/linter_values.yaml b/ozgcloud-keycloak-operator/src/test/helm/linter_values.yaml
similarity index 100%
rename from src/test/helm/linter_values.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/linter_values.yaml
diff --git a/src/test/helm/rbacs/keycloak_admin_secret_read.yaml b/ozgcloud-keycloak-operator/src/test/helm/rbacs/keycloak_admin_secret_read_test.yaml
similarity index 100%
rename from src/test/helm/rbacs/keycloak_admin_secret_read.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/rbacs/keycloak_admin_secret_read_test.yaml
diff --git a/src/test/helm/rbacs/keycloak_read_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/rbacs/keycloak_read_test.yaml
similarity index 100%
rename from src/test/helm/rbacs/keycloak_read_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/rbacs/keycloak_read_test.yaml
diff --git a/src/test/helm/rbacs/keycloak_write_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/rbacs/keycloak_write_test.yaml
similarity index 100%
rename from src/test/helm/rbacs/keycloak_write_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/rbacs/keycloak_write_test.yaml
diff --git a/src/test/helm/rbacs/serviceaccount_test.yaml b/ozgcloud-keycloak-operator/src/test/helm/rbacs/serviceaccount_test.yaml
similarity index 100%
rename from src/test/helm/rbacs/serviceaccount_test.yaml
rename to ozgcloud-keycloak-operator/src/test/helm/rbacs/serviceaccount_test.yaml
diff --git a/src/test/java/de/ozgcloud/operator/OzgOperatorApplicationTests.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/KeycloakOperatorApplicationTest.java
similarity index 96%
rename from src/test/java/de/ozgcloud/operator/OzgOperatorApplicationTests.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/KeycloakOperatorApplicationTest.java
index c1f44350b15d1699808938451c32e7ae06a8af6a..52a4ee18784f2e3cd66467e8323b4fbe66ac3fe3 100644
--- a/src/test/java/de/ozgcloud/operator/OzgOperatorApplicationTests.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/KeycloakOperatorApplicationTest.java
@@ -27,7 +27,7 @@ import org.junit.jupiter.api.Test;
 
 //@SpringBootTest
 //@EnableMockOperator
-class OzgOperatorApplicationTests {
+class KeycloakOperatorApplicationTest {
 
 	@Test
 	void contextLoads() {
diff --git a/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/KeycloakClientTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/KeycloakClientTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..64d1be69f681da74c8fc3ac60114478421832042
--- /dev/null
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/KeycloakClientTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.operator.keycloak;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import io.fabric8.kubernetes.client.KubernetesClient;
+
+class KeycloakClientTest {
+
+	@DisplayName("Create keycloak")
+	@Nested
+	class TestCreateKeycloak {
+
+		@Mock
+		private KubernetesClient kubernetsClient;
+
+		@Test
+		void shouldInitKeycloakClient() {
+			var keycloak = new KeycloakClient(kubernetsClient);
+
+			assertThat(keycloak).isNotNull();
+		}
+	}
+}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/KeycloakGenericRemoteServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/KeycloakLivelTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/KeycloakLivelTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/KeycloakLivelTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/KeycloakLivelTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/ClientRepresentationTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/ClientRepresentationTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/ClientRepresentationTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/ClientRepresentationTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapperTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapperTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapperTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientMapperTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientPreconditionServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconcilerTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconcilerTest.java
similarity index 97%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconcilerTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconcilerTest.java
index c8f41130a340729d0bc529d78fed419219798e88..01b9a7c05e509a97e5c9ce7f9736f2788a137fb5 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconcilerTest.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientReconcilerTest.java
@@ -39,8 +39,6 @@ import de.ozgcloud.operator.keycloak.OzgCloudCustomResourceStatus;
 
 class KeycloakClientReconcilerTest {
 
-	public static final String ERROR_MESSAGE = "ErrorMessage";
-
 	@Spy
 	@InjectMocks
 	private KeycloakClientReconciler reconciler;
@@ -54,6 +52,8 @@ class KeycloakClientReconcilerTest {
 	@Nested
 	class TestReconcile {
 
+		private static final String ERROR_MESSAGE = "ErrorMessage";
+
 		@Test
 		void shouldCallServiceAddClient() {
 			OzgCloudKeycloakClient client = OzgCloudKeycloakClientTestFactory.create();
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientRemoteServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakClientServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakLivelTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakLivelTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakLivelTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/KeycloakLivelTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecProtocolMapperTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecProtocolMapperTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecProtocolMapperTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecProtocolMapperTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientSpecTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatusTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatusTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatusTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientStatusTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/OzgCloudKeycloakClientTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelperTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelperTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelperTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationHelperTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/ProtocolMapperRepresentationTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/client/RoleRepresentationTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/RoleRepresentationTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/client/RoleRepresentationTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/client/RoleRepresentationTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/GroupRepresentationTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/GroupRepresentationTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/GroupRepresentationTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/GroupRepresentationTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapperTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapperTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapperTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupMapperTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupPreconditionServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconcilerTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconcilerTest.java
similarity index 80%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconcilerTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconcilerTest.java
index c4eca85fbecda61936da1891720e9ebf3fdb9863..5e02bd50768c8afa95d5bb2a66acd798d0d8d308 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconcilerTest.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupReconcilerTest.java
@@ -39,8 +39,6 @@ import de.ozgcloud.operator.keycloak.OzgCloudCustomResourceStatus;
 
 class KeycloakGroupReconcilerTest {
 
-	public static final String ERROR_MESSAGE = "ErrorMessage";
-
 	@Spy
 	@InjectMocks
 	private KeycloakGroupReconciler reconciler;
@@ -54,25 +52,26 @@ class KeycloakGroupReconcilerTest {
 	@Nested
 	class TestReconcile {
 
+		private static final String ERROR_MESSAGE = "ErrorMessage";
+		private static final OzgCloudKeycloakGroup KEYCLOAK_GROUP = OzgCloudKeycloakGroupTestFactory.create();
+
 		@Test
 		void shouldCallServiceAddGroup() {
-			OzgCloudKeycloakGroup group = OzgCloudKeycloakGroupTestFactory.create();
-
-			reconciler.reconcile(group, null);
+			reconciler.reconcile(KEYCLOAK_GROUP, null);
 
-			verify(service).createGroup(group.getSpec(), OzgCloudKeycloakGroupTestFactory.METADATA_NAMESPACE);
+			verify(service).createGroup(KEYCLOAK_GROUP.getSpec(), OzgCloudKeycloakGroupTestFactory.METADATA_NAMESPACE);
 		}
 
 		@Test
 		void shouldReturnUpdateStatus() {
-			var response = reconciler.reconcile(OzgCloudKeycloakGroupTestFactory.create(), null);
+			var response = reconciler.reconcile(KEYCLOAK_GROUP, null);
 
 			assertThat(response.getResource()).isNotNull();
 		}
 
 		@Test
 		void shouldSetStatusOk() {
-			var response = reconciler.reconcile(OzgCloudKeycloakGroupTestFactory.create(), null);
+			var response = reconciler.reconcile(KEYCLOAK_GROUP, null);
 
 			assertThat(response.getResource().getStatus().getStatus()).isEqualTo(OzgCloudCustomResourceStatus.OK);
 		}
@@ -81,7 +80,7 @@ class KeycloakGroupReconcilerTest {
 		void shouldReturnInProgressStatusIfPreconditionsNotMet() {
 			when(preconditionService.getReconcilePreconditionErrors(any())).thenReturn(Optional.of(ERROR_MESSAGE));
 
-			var response = reconciler.reconcile(OzgCloudKeycloakGroupTestFactory.create(), null);
+			var response = reconciler.reconcile(KEYCLOAK_GROUP, null);
 
 			assertThat(response.getResource().getStatus().getStatus()).isEqualTo(OzgCloudCustomResourceStatus.IN_PROGRESS);
 		}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupRemoteServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupServiceTest.java
similarity index 81%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupServiceTest.java
index 1bc024a25ae520702044b8efa4a72640845a541a..b78cb0f74366340a21151c211e792a9ad8cc38fe 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupServiceTest.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakGroupServiceTest.java
@@ -52,43 +52,43 @@ class KeycloakGroupServiceTest {
 	@Nested
 	class TestCreateGroup {
 
+		private final static OzgCloudKeycloakGroupSpec KEYCLOAK_GROUP = OzgCloudKeycloakGroupSpecTestFactory.create();
+
 		@Test
 		void shouldCallGetGroupRemoteService() {
 			var groupRepresentation = GroupRepresentationTestFactory.create();
 			when(mapper.map(any())).thenReturn(groupRepresentation);
 
-			service.createGroup(OzgCloudKeycloakGroupSpecTestFactory.create(), REALM);
+			service.createGroup(KEYCLOAK_GROUP, REALM);
 
 			verify(remoteService).getGroupByName(OzgCloudKeycloakGroupSpecTestFactory.NAME, REALM);
 		}
 
 		@Test
 		void shouldCallMapper() {
-			var group = OzgCloudKeycloakGroupSpecTestFactory.create();
-			service.createGroup(group, REALM);
 
-			verify(mapper).map(group);
+			service.createGroup(KEYCLOAK_GROUP, REALM);
+
+			verify(mapper).map(KEYCLOAK_GROUP);
 		}
 
 		@Test
 		void shouldCreateGroupIfNotExists() {
-			var ozgGroup = OzgCloudKeycloakGroupSpecTestFactory.create();
 			var groupRepresentation = GroupRepresentationTestFactory.create();
 			when(remoteService.getGroupByName(OzgCloudKeycloakGroupSpecTestFactory.NAME, REALM)).thenReturn(Optional.empty());
-			when(mapper.map(ozgGroup)).thenReturn(groupRepresentation);
+			when(mapper.map(KEYCLOAK_GROUP)).thenReturn(groupRepresentation);
 
-			service.createGroup(ozgGroup, REALM);
+			service.createGroup(KEYCLOAK_GROUP, REALM);
 
 			verify(remoteService).createGroup(groupRepresentation, REALM);
 		}
 
 		@Test
 		void shouldNotCreateGroupIfAlreadyExists() {
-			var ozgGroup = OzgCloudKeycloakGroupSpecTestFactory.create();
 			when(remoteService.getGroupByName(OzgCloudKeycloakGroupSpecTestFactory.NAME, REALM)).thenReturn(Optional.of(mock(GroupRepresentation.class)));
-			when(mapper.map(ozgGroup)).thenReturn(GroupRepresentationTestFactory.create());
+			when(mapper.map(KEYCLOAK_GROUP)).thenReturn(GroupRepresentationTestFactory.create());
 
-			service.createGroup(ozgGroup, REALM);
+			service.createGroup(KEYCLOAK_GROUP, REALM);
 
 			verify(remoteService, never()).createGroup(any(), anyString());
 		}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakLivelTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakLivelTest.java
similarity index 92%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakLivelTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakLivelTest.java
index 91ccd1f7ba8e7805522aeaca4bce060169221114..0da2e64f31296badb50a8339acdc5484231880d3 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakLivelTest.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/KeycloakLivelTest.java
@@ -35,13 +35,13 @@ import org.mockito.InjectMocks;
 import org.mockito.Spy;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
-import lombok.extern.java.Log;
+import lombok.extern.log4j.Log4j2;
 
 @Disabled("Should only be used manually")
-@Log
+@Log4j2
 @ExtendWith(SpringExtension.class)
 class KeycloakLivelTest {
-	
+
 	private static final String NAMESPACE = "by-torsten-ozgcloud-keycloak-operator-dev";
 
 	@Spy
@@ -68,8 +68,8 @@ class KeycloakLivelTest {
 		FieldUtils.writeField(remoteService, "keycloak", kc, true);
 
 		remoteService.createGroup(createGroup(), NAMESPACE);
-		log.info("Hase: " + remoteService.getGroupByName("hase", NAMESPACE));
-		log.info("Maus: " + remoteService.getGroupByName("maus", NAMESPACE));
+		LOG.info("Hase: {}.", remoteService.getGroupByName("hase", NAMESPACE));
+		LOG.info("Maus: {}.", remoteService.getGroupByName("maus", NAMESPACE));
 
 		service.createGroup(OzgCloudKeycloakGroupSpec.builder().name("Fuchs").build(), NAMESPACE);
 	}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecAttributeTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecAttributeTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecAttributeTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecAttributeTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupSpecTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/group/OzgCloudKeycloakGroupTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakLivelTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakLivelTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakLivelTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakLivelTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapperTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapperTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapperTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmMapperTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconcilerTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconcilerTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconcilerTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmReconcilerTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpecTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpecTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpecTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmSpecTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatusTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatusTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatusTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmStatusTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/OzgCloudKeycloakRealmTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/realm/RealmRepresentationTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/RealmRepresentationTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/realm/RealmRepresentationTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/RealmRepresentationTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakLivelTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakLivelTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakLivelTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakLivelTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserPreconditionServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteServiceTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpecTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpecTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpecTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserSpecTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatusTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatusTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatusTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserStatusTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/OzgCloudKeycloakUserTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/SecretTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/SecretTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/SecretTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/SecretTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserNameConverterTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserNameConverterTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/UserNameConverterTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserNameConverterTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java
similarity index 78%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java
index 7cfbbf8e13b7d2e1d89104616c6aa28a6550e372..0cc1d435be4aa4ae3b05cc6fe6f69abafd994087 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java
@@ -1,8 +1,11 @@
 package de.ozgcloud.operator.keycloak.user;
 
 import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
+import java.util.Base64;
+
 import org.apache.commons.lang3.StringUtils;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -11,7 +14,7 @@ import org.mockito.Spy;
 
 import de.ozgcloud.operator.keycloak.user.OzgCloudKeycloakUserSpec.KeycloakUserSpecUser;
 
-public class UserSecretBuilderTest {
+class UserSecretBuilderTest {
 
 	private final static String NAME = "dummyName";
 	private final static String NAMESPACE = "dummyNamespace";
@@ -52,7 +55,7 @@ public class UserSecretBuilderTest {
 
 			var secret = builder.build(NAME, userSpec, NAMESPACE);
 
-			assertThat(secret.getStringData()).containsEntry(UserSecretBuilder.SECRET_PASSWORD_FIELD, SecretTestFactory.PASSWORD);
+			assertThat(secret.getData()).containsEntry(UserSecretBuilder.SECRET_PASSWORD_FIELD, SecretTestFactory.PASSWORD);
 		}
 
 		@DisplayName("metadata")
@@ -80,24 +83,35 @@ public class UserSecretBuilderTest {
 
 			@Test
 			void shouldHaveSize() {
-				var password = builder.generatePassword();
+				var password = decode(builder.generatePassword());
 
 				assertThat(password).hasSize(8);
 			}
 
 			@Test
 			void shouldHaveUpperCaseLetterAtFirst() {
-				var password = builder.generatePassword();
+				var password = decode(builder.generatePassword());
 
 				assertThat(StringUtils.substring(password, 0, 1)).isUpperCase();
 			}
 
 			@Test
 			void shouldContainsAlphanumericOnly() {
-				var password = builder.generatePassword();
+				var password = decode(builder.generatePassword());
 
 				assertThat(password).isAlphanumeric();
 			}
+
+			@Test
+			void shouldEncode() {
+				builder.generatePassword();
+
+				verify(builder).encode(any());
+			}
+
+			private String decode(String strValue) {
+				return new String(Base64.getDecoder().decode(strValue.getBytes()));
+			}
 		}
 	}
 }
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretReaderTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretReaderTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretReaderTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretReaderTest.java
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java
similarity index 100%
rename from src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java
diff --git a/src/test/resources/KeycloakUserTest.yaml b/ozgcloud-keycloak-operator/src/test/resources/KeycloakUserTest.yaml
similarity index 100%
rename from src/test/resources/KeycloakUserTest.yaml
rename to ozgcloud-keycloak-operator/src/test/resources/KeycloakUserTest.yaml
diff --git a/ozgcloud-keycloak-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/ozgcloud-keycloak-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 0000000000000000000000000000000000000000..79b126e6cdb86bec1f4f08c205de8961bde1934a
--- /dev/null
+++ b/ozgcloud-keycloak-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1 @@
+org.mockito.junit.jupiter.MockitoExtension
\ No newline at end of file
diff --git a/src/test/resources/application.yaml b/ozgcloud-keycloak-operator/src/test/resources/application.yaml
similarity index 100%
rename from src/test/resources/application.yaml
rename to ozgcloud-keycloak-operator/src/test/resources/application.yaml
diff --git a/ozgcloud-keycloak-operator/src/test/resources/junit-platform.properties b/ozgcloud-keycloak-operator/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000000000000000000000000000000000000..b059a65dc46792ea5494de02f888ab2ad08cd420
--- /dev/null
+++ b/ozgcloud-keycloak-operator/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.extensions.autodetection.enabled=true
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 176165c6c7d73e6412cd0c46fdf63a390c6034d8..fbd13b21d2fbfebd615c1c634643b77410110777 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,79 +1,85 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 	<modelVersion>4.0.0</modelVersion>
-	
+
 	<parent>
 		<groupId>org.springframework.boot</groupId>
 		<artifactId>spring-boot-starter-parent</artifactId>
 		<version>3.1.3</version>
-		<relativePath/>
+		<relativePath />
 	</parent>
 
 	<groupId>de.ozgcloud</groupId>
-	<artifactId>ozgcloud-keycloak-operator</artifactId>
+	<artifactId>ozgcloud-operator-parent</artifactId>
 	<version>2.1.0-SNAPSHOT</version>
-	<name>OzgCloud Keycloak Operator</name>
-	<description>OzgCloud Keycloak Operator</description>
+	<packaging>pom</packaging>
+
+	<name>OzgCloud Operator Parent</name>
+	<description>OzgCloud Operator Parent</description>
 	
+	<modules>
+		<module>ozgcloud-keycloak-operator</module>
+		<module>ozgcloud-elasticsearch-operator</module>
+	</modules>
+
 	<properties>
-		<spring-boot.build-image.imageName>docker.ozg-sh.de/ozgcloud-keycloak-operator:build-latest</spring-boot.build-image.imageName>
-		
-		<operator-sdk.version>5.2.0</operator-sdk.version>
+		<spring-boot.version>3.1.3</spring-boot.version>
+		<operator-sdk.version>5.4.1</operator-sdk.version>
+
+		<!-- tools -->
+		<commons-beanutils.version>1.9.4</commons-beanutils.version>
+		<lombok.version>1.18.28</lombok.version>
 		<mapstruct.version>1.5.5.Final</mapstruct.version>
 		<keycloak-adapter.version>20.0.5</keycloak-adapter.version>
-		<commons-beanutils.version>1.9.4</commons-beanutils.version>
 		<reflections.version>0.10.2</reflections.version>
 		<validation-api.version>2.0.1.Final</validation-api.version>
-	</properties>
+		<lorem.version>2.2</lorem.version>
+
+		<!-- test -->
+		<junit-jupiter.version>5.9.3</junit-jupiter.version>
+		<kubernetes-server-mock.version>6.9.2</kubernetes-server-mock.version>
+		<io.javaoperatorsdk.version>0.9.5</io.javaoperatorsdk.version>
 		
+		<!-- plugin -->
+		<license-maven-plugin.version>4.1</license-maven-plugin.version>
+		<ozgcloud-license.version>1.6.0</ozgcloud-license.version>
+	</properties>
+
 	<dependencies>
+		<!-- spring -->
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter</artifactId>
 		</dependency>
-		<dependency>
-		    <groupId>org.keycloak</groupId>
-		    <artifactId>keycloak-admin-client</artifactId>
-		    <version>${keycloak-adapter.version}</version>
-		</dependency>
-		<dependency>
-		    <groupId>org.mapstruct</groupId>
-		    <artifactId>mapstruct</artifactId>
-		    <version>${mapstruct.version}</version>
-		</dependency>
 		<dependency>
 			<groupId>io.javaoperatorsdk</groupId>
 			<artifactId>operator-framework-spring-boot-starter</artifactId>
-			<version>${operator-sdk.version}</version>
-		</dependency>
-		<dependency>
-	    	<groupId>javax.validation</groupId>
-	    	<artifactId>validation-api</artifactId>
-	    	<version>${validation-api.version}</version>
 		</dependency>
 		<dependency>
-		    <groupId>jakarta.xml.bind</groupId>
-		    <artifactId>jakarta.xml.bind-api</artifactId>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
 		</dependency>
+
+		<!-- tools -->
 		<dependency>
 			<groupId>org.projectlombok</groupId>
 			<artifactId>lombok</artifactId>
 		</dependency>
-		<dependency>
-		    <groupId>org.reflections</groupId>
-		    <artifactId>reflections</artifactId>
-		    <version>${reflections.version}</version>
-		</dependency>		
 		<dependency>
 			<groupId>commons-beanutils</groupId>
 			<artifactId>commons-beanutils</artifactId>
-			<version>${commons-beanutils.version}</version>
+		</dependency>
+		<dependency>
+		    <groupId>com.thedeanda</groupId>
+		    <artifactId>lorem</artifactId>
 		</dependency>
 		
+
+		<!-- test -->
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-test</artifactId>
-			<scope>test</scope>
 			<exclusions>
 				<exclusion>
 					<groupId>org.junit.vintage</groupId>
@@ -91,44 +97,141 @@
 			<artifactId>junit-jupiter-params</artifactId>
 			<scope>test</scope>
 		</dependency>
-
+		<dependency>
+			<groupId>io.fabric8</groupId>
+			<artifactId>kubernetes-server-mock</artifactId>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 
-	<build>
-		<plugins>
-			<plugin>
+	<dependencyManagement>
+		<dependencies>
+			<!-- own projects -->
+			<dependency>
+				<groupId>de.ozgcloud.common</groupId>
+				<artifactId>ozgcloud-common-license</artifactId>
+				<version>${ozgcloud-license.version}</version>
+			</dependency>
+
+			<!-- spring -->
+			<dependency>
 				<groupId>org.springframework.boot</groupId>
-				<artifactId>spring-boot-maven-plugin</artifactId>
-			</plugin>			
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-compiler-plugin</artifactId>
-				<configuration>
-					<fork>true</fork>
-					<annotationProcessorPaths>
-						<path>
-							<groupId>org.projectlombok</groupId>
-							<artifactId>lombok</artifactId>
-							<version>${lombok.version}</version>
-						</path>
-						<path>
-							<groupId>org.mapstruct</groupId>
-							<artifactId>mapstruct-processor</artifactId>
-							<version>${mapstruct.version}</version>
-						</path>
-					</annotationProcessorPaths>
-					<showWarnings>true</showWarnings>
-					<compilerArgs>
-						<compilerArg>
-							-Amapstruct.defaultComponentModel=spring
-						</compilerArg>
-						<compilerArg>
-							-Amapstruct.unmappedTargetPolicy=WARN
-						</compilerArg>
-					</compilerArgs>
-				</configuration>
-			</plugin>
-		</plugins>
+				<artifactId>spring-boot-starter</artifactId>
+				<version>${spring-boot.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>io.javaoperatorsdk</groupId>
+				<artifactId>operator-framework-spring-boot-starter</artifactId>
+				<version>${operator-sdk.version}</version>
+			</dependency>
+
+			<!-- keycloak -->
+			<dependency>
+				<groupId>org.keycloak</groupId>
+				<artifactId>keycloak-admin-client</artifactId>
+				<version>${keycloak-adapter.version}</version>
+			</dependency>
+
+			<!-- tools -->
+			<dependency>
+				<groupId>org.projectlombok</groupId>
+				<artifactId>lombok</artifactId>
+				<version>${lombok.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.mapstruct</groupId>
+				<artifactId>mapstruct</artifactId>
+				<version>${mapstruct.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.reflections</groupId>
+				<artifactId>reflections</artifactId>
+				<version>${reflections.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>commons-beanutils</groupId>
+				<artifactId>commons-beanutils</artifactId>
+				<version>${commons-beanutils.version}</version>
+			</dependency>
+			<dependency>
+			    <groupId>com.thedeanda</groupId>
+			    <artifactId>lorem</artifactId>
+			    <version>${lorem.version}</version>
+			</dependency>
+
+			<!-- javax -->
+			<dependency>
+				<groupId>javax.validation</groupId>
+				<artifactId>validation-api</artifactId>
+				<version>${validation-api.version}</version>
+			</dependency>
+
+			<!-- test -->
+			<dependency>
+				<groupId>org.junit.jupiter</groupId>
+				<artifactId>junit-jupiter-engine</artifactId>
+				<version>${junit-jupiter.version}</version>
+				<scope>test</scope>
+			</dependency>
+			<dependency>
+				<groupId>org.junit.jupiter</groupId>
+				<artifactId>junit-jupiter-params</artifactId>
+				<version>${junit-jupiter.version}</version>
+				<scope>test</scope>
+			</dependency>
+			<dependency>
+				<groupId>io.fabric8</groupId>
+				<artifactId>kubernetes-server-mock</artifactId>
+				<version>${kubernetes-server-mock.version}</version>
+			</dependency>
+			<dependency>
+			    <groupId>io.javaoperatorsdk</groupId>
+			    <artifactId>jenvtest-fabric8-client-support</artifactId>
+			    <version>${io.javaoperatorsdk.version}</version>
+			    <scope>test</scope>
+			</dependency>
+			<dependency>
+			    <groupId>io.javaoperatorsdk</groupId>
+			    <artifactId>jenvtest</artifactId>
+			    <version>${io.javaoperatorsdk.version}</version>
+			    <scope>test</scope>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<build>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>com.mycila</groupId>
+					<artifactId>license-maven-plugin</artifactId>
+					<version>${license-maven-plugin.version}</version>
+					<configuration>
+						<mapping>
+							<ts>SLASHSTAR_STYLE</ts>
+							<config>SCRIPT_STYLE</config>
+						</mapping>
+						<licenseSets>
+							<licenseSet>
+								<header>license/eupl_v1_2_de/header.txt</header>
+								<excludes>
+									<exclude>**/README</exclude>
+									<exclude>src/test/resources/**</exclude>
+									<exclude>src/main/resources/**</exclude>
+								</excludes>
+							</licenseSet>
+						</licenseSets>
+					</configuration>
+					<dependencies>
+						<dependency>
+							<groupId>de.ozgcloud.common</groupId>
+							<artifactId>ozgcloud-common-license</artifactId>
+							<version>${ozgcloud-license.version}</version>
+						</dependency>
+					</dependencies>
+				</plugin>
+			</plugins>
+		</pluginManagement>
 	</build>
 
 	<distributionManagement>
@@ -143,5 +246,5 @@
 			<url>https://nexus.ozg-sh.de/repository/ozg-snapshots/</url>
 		</snapshotRepository>
 	</distributionManagement>
-	
-</project>
+
+</project>
\ No newline at end of file
diff --git a/release-erstellen.sh b/release-erstellen.sh
index dfe2a755312486a606fc94a2187bd1e52c2e3271..fc5a1f481ba0e2d029766c45d2551321d93495f4 100755
--- a/release-erstellen.sh
+++ b/release-erstellen.sh
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 if [ "$#" -ne 1 ]; then
-    echo "Aufruf: ozg-release-erstellen.sh JA"
+    echo "Aufruf: release-erstellen.sh JA"
     echo "Als Parameter bitte 'JA' eintragen zur Sicherheit"
     exit 1
 fi
diff --git a/release-startdev.sh b/release-startdev.sh
index f02c90d2bb6c5136860c392415b827a92637bb35..4fc6aa98500e450dfdd07d026a46772accdb514e 100755
--- a/release-startdev.sh
+++ b/release-startdev.sh
@@ -3,7 +3,7 @@
 #set -x
 
 if [ "$#" -ne 1 ]; then
-    echo "Aufruf: ozg-release-startdev.sh NEWVERSION"
+    echo "Aufruf: release-startdev.sh NEWVERSION"
     exit 1
 fi