From 38b6f73b6bb00e097e1b9c749a63d6f6730483d1 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Mon, 11 Dec 2023 15:42:02 +0100
Subject: [PATCH] OZG-4453 OZG-4670 create elastic operator; create user secret
 if not exists

---
 ozgcloud-elastic-operator/pom.xml             |  71 +++++++
 .../operator/CustomResourceStatus.java        |   5 +
 .../operator/ElasticCustomResourceStatus.java |  46 +++++
 .../operator/ElasticOperatorApplication.java  |  35 ++++
 .../de/ozgcloud/operator/OperatorConfig.java  |  48 +++++
 .../common/kubernetes/KubernetesService.java  | 184 ++++++++++++++++++
 .../user/ElasticUserCustomResource.java       |  42 ++++
 .../operator/user/ElasticUserReconciler.java  |  26 +++
 .../user/ElasticUserSecretBuilder.java        |  41 ++++
 .../operator/user/ElasticUserService.java     |  68 +++++++
 .../operator/user/ElasticUserSpec.java        |  38 ++++
 .../user/ElasticUserUpdateControlBuilder.java |  63 ++++++
 .../kubernetes/KubernetesServiceTest.java     | 109 +++++++++++
 .../kubernetes/NamespaceTestFactory.java      |  16 ++
 .../common/kubernetes/SecretTestFactory.java  |  21 ++
 ...lasticCustomResourceStatusTestFactory.java |  18 ++
 .../ElasticUserCustomResourceTestFactory.java |  11 ++
 .../user/ElasticUserReconcilerITCase.java     |  64 ++++++
 .../user/ElasticUserReconcilerTest.java       |  48 +++++
 .../user/ElasticUserSecretBuilderTest.java    |  76 ++++++++
 .../operator/user/ElasticUserServiceTest.java | 160 +++++++++++++++
 .../operator/user/ObjectMetaTestFactory.java  |  15 ++
 .../org.junit.jupiter.api.extension.Extension |   1 +
 .../test/resources/junit-platform.properties  |   1 +
 ....java => KeycloakOperatorApplication.java} |   4 +-
 ...a => KeycloakOperatorApplicationTest.java} |   2 +-
 pom.xml                                       |   1 +
 27 files changed, 1211 insertions(+), 3 deletions(-)
 create mode 100644 ozgcloud-elastic-operator/pom.xml
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/CustomResourceStatus.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticCustomResourceStatus.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticOperatorApplication.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/OperatorConfig.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/kubernetes/KubernetesService.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserCustomResource.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserReconciler.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSecretBuilder.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserService.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSpec.java
 create mode 100644 ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserUpdateControlBuilder.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesServiceTest.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/NamespaceTestFactory.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/SecretTestFactory.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticCustomResourceStatusTestFactory.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserCustomResourceTestFactory.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerITCase.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerTest.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserSecretBuilderTest.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserServiceTest.java
 create mode 100644 ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ObjectMetaTestFactory.java
 create mode 100644 ozgcloud-elastic-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
 create mode 100644 ozgcloud-elastic-operator/src/test/resources/junit-platform.properties
 rename ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/{OzgCloudOperatorApplication.java => KeycloakOperatorApplication.java} (91%)
 rename ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/{OzgOperatorApplicationTests.java => KeycloakOperatorApplicationTest.java} (96%)

diff --git a/ozgcloud-elastic-operator/pom.xml b/ozgcloud-elastic-operator/pom.xml
new file mode 100644
index 0000000..6df5d09
--- /dev/null
+++ b/ozgcloud-elastic-operator/pom.xml
@@ -0,0 +1,71 @@
+<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>0.0.1-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>ozgcloud-elastic-operator</artifactId>
+	<name>OzgCloud Elastic Operator</name>
+	<description>OzgCloud Elastic Operator</description>
+
+	<dependencies>
+		<!-- keycloak -->
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-admin-client</artifactId>
+		</dependency>
+	
+		<!-- TOCHECK: werden die gebraucht!? -->
+		<dependency>
+			<groupId>javax.validation</groupId>
+			<artifactId>validation-api</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>jakarta.xml.bind</groupId>
+			<artifactId>jakarta.xml.bind-api</artifactId>
+			<version>4.0.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.reflections</groupId>
+			<artifactId>reflections</artifactId>
+		</dependency>
+		<!-- TOCHECK -->
+
+		<!-- spring -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+		</dependency>
+		
+		<!-- tools -->
+		<dependency>
+			<groupId>org.mapstruct</groupId>
+			<artifactId>mapstruct</artifactId>
+		</dependency>
+		
+		<!-- test -->
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-engine</artifactId>
+			<version>5.9.3</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-params</artifactId>
+			<version>5.9.3</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+		    <groupId>io.fabric8</groupId>
+		    <artifactId>kubernetes-server-mock</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	
+</project>
\ No newline at end of file
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/CustomResourceStatus.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/CustomResourceStatus.java
new file mode 100644
index 0000000..7ef66d5
--- /dev/null
+++ b/ozgcloud-elastic-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-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticCustomResourceStatus.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticCustomResourceStatus.java
new file mode 100644
index 0000000..6cf85a9
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticCustomResourceStatus.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+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 ElasticCustomResourceStatus extends ObservedGenerationAwareStatus {
+
+	private CustomResourceStatus status;
+
+	private String message;
+}
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticOperatorApplication.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticOperatorApplication.java
new file mode 100644
index 0000000..2c35d6d
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticOperatorApplication.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ElasticOperatorApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(ElasticOperatorApplication.class, args);
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/OperatorConfig.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/OperatorConfig.java
new file mode 100644
index 0000000..8804478
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/OperatorConfig.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+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-elastic-operator/src/main/java/de/ozgcloud/operator/common/kubernetes/KubernetesService.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/kubernetes/KubernetesService.java
new file mode 100644
index 0000000..ef03ab1
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/kubernetes/KubernetesService.java
@@ -0,0 +1,184 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import java.util.Objects;
+
+import org.springframework.stereotype.Component;
+
+import io.fabric8.kubernetes.api.model.EnvVar;
+import io.fabric8.kubernetes.api.model.EnvVarBuilder;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.batch.v1.Job;
+import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.dsl.Resource;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Component
+public class KubernetesService {
+
+	private final KubernetesClient client;
+
+	public boolean existNamespace(String namespace) {
+		return Objects.nonNull(client.namespaces().withName(namespace).get());
+	}
+
+	public Resource<Secret> getSecretResource(String namespace, String name) {
+		return client.secrets().inNamespace(namespace).withName(name);
+	}
+	
+	
+	//PoC
+	public Pod doInPod(String namespace, String podName) {
+
+//        client.batch().v1().jobs().inNamespace(namespace).createOrReplace(job);
+		client.batch().v1().raw(podName);
+		
+		
+		return client.pods().inNamespace(namespace).withName(podName).get();
+	}
+	
+	public void executeJob(String namespace) {
+		var ELASTIC_USER_SECRET_NAME = "ozg-search-cluster-es-elastic-user";
+		var ELASTIC_SYSTEM_NAMESPACE = "elastic-system";
+		var ELASTIC_USER_SECRET_ELASTIC = "elastic";
+		var secretResource = getSecretResource(ELASTIC_SYSTEM_NAMESPACE, ELASTIC_USER_SECRET_NAME);
+		
+		if(Objects.isNull(secretResource.get())) {
+			//Error - elastic-system namespace secret not exists
+		}
+			
+		
+		var password = secretResource.get().getStringData().get(ELASTIC_USER_SECRET_ELASTIC);
+		var jobEnvs = buildEnvVars(namespace, password);
+	}
+		
+	private Job buildJob(String jobName) {
+		return new JobBuilder()
+	            .withApiVersion("batch/v1")
+	            .withNewMetadata()
+	            .withName(jobName)
+	            .endMetadata()
+	            .withNewSpec()
+	            .withBackoffLimit(4)//TOCHECK: notwendig? wofür?
+	            .withNewTemplate()
+	            .withNewSpec()
+	            .withRestartPolicy("Never")
+	            //containers START
+	            .addNewContainer()
+	            .withName("es-create-access")
+	            .withImage("manusant/curl-jq")
+	            .withEnv(buildEnvVars("password", "namespace"))
+	            
+	            
+	            
+	            
+	            .withCommand("/bin/sh", "-c")
+	            .withArgs("echo \"Job started\"; i=1; while [ $i -le $MAX_COUNT ]; do echo $i; i=$((i+1)) ; sleep 1;done; echo \"Job Done!\"")
+	            .endContainer()
+	            //containers END
+	            .endSpec()
+	            .endTemplate()
+	            .endSpec().build();
+	}
+	
+	static final String ELASTICSEARCH_NAMESPACE_PASSWORD_FIELD = "ES_NS_PASSWORD";
+	static final String ELASTICSEARCH_NAMESPACE_USER_FIELD = "ES_NS_USER";
+	static final String ELASTICSEARCH_CLUSTER_FIELD = "ES_CLUSTER";
+	static final String ELASTICSEARCH_PASSWORD_FIELD = "ELASTICSEARCH_PASSWORD";
+	
+	private EnvVar buildEnvVars(String namespace, String password) {
+//		 - name: ES_NS_PASSWORD
+//       value: "{{ elasticsearch_user_password }}"
+//     - name: ES_NS_USER
+//       value: "{{ kommune }}"
+//     - name: ES_CLUSTER
+//       value: "{{ kommune }}"
+//     - name: ELASTICSEARCH_PASSWORD
+//       valueFrom:
+//         secretKeyRef:
+//           name: ozg-search-cluster-es-elastic-user
+//           key: elastic
+//   command:
+		return new EnvVarBuilder()
+				.withName(ELASTICSEARCH_NAMESPACE_PASSWORD_FIELD).withValue(password)
+				.withName(ELASTICSEARCH_NAMESPACE_USER_FIELD).withValue(namespace)
+				.withName(ELASTICSEARCH_CLUSTER_FIELD).withValue(namespace)
+				.withName(ELASTICSEARCH_PASSWORD_FIELD).withValue(password)
+				.build();
+	}
+}
+
+
+//apiVersion: batch/v1
+//kind: Job
+//metadata:
+//  name: es-create-access-{{ kommune }}
+//spec:
+//  parallelism: 1
+//  completions: 1
+//  template:
+//    metadata:
+//      name: es-create-access-{{ kommune }}
+//    spec:
+//      restartPolicy: Never
+//      containers:
+//        - name: es-create-access
+//          image: manusant/curl-jq
+//          env:
+//            - name: ES_NS_PASSWORD
+//              value: "{{ elasticsearch_user_password }}"
+//            - name: ES_NS_USER
+//              value: "{{ kommune }}"
+//            - name: ES_CLUSTER
+//              value: "{{ kommune }}"
+//            - name: ELASTICSEARCH_PASSWORD
+//              valueFrom:
+//                secretKeyRef:
+//                  name: ozg-search-cluster-es-elastic-user
+//                  key: elastic
+//          command:
+//            - /bin/sh
+//            - -c
+//            - |
+//              curl -k -X PUT -u elastic:$ELASTICSEARCH_PASSWORD -H 'Content-Type: application/json' 'https://ozg-search-cluster-es-http:9200/'$ES_NS_USER
+//      initContainers:
+//        - name: create-es-role
+//          image: manusant/curl-jq
+//          env:
+//            - name: ES_NS_PASSWORD
+//              value: "{{ elasticsearch_user_password }}"
+//            - name: ES_NS_USER
+//              value: "{{ kommune }}"
+//            - name: ES_CLUSTER
+//              value: "{{ kommune }}"
+//            - name: ELASTICSEARCH_PASSWORD
+//              valueFrom:
+//                secretKeyRef:
+//                  name: ozg-search-cluster-es-elastic-user
+//                  key: elastic
+//          command:
+//            - /bin/sh
+//            - -c
+//            - |
+//              curl -k -X POST -u elastic:$ELASTICSEARCH_PASSWORD -H 'Content-Type: application/json' 'https://ozg-search-cluster-es-http:9200/_security/role/'$ES_NS_USER -d '{ "indices": [ { "names": [ "'$ES_NS_USER'*" ], "privileges": ["all"] } ] }'
+//        - name: create-es-user
+//          image: manusant/curl-jq
+//          env:
+//            - name: ES_NS_PASSWORD
+//              value: "{{ elasticsearch_user_password }}"
+//            - name: ES_NS_USER
+//              value: "{{ kommune }}"
+//            - name: ES_CLUSTER
+//              value: "{{ kommune }}"
+//            - name: ELASTICSEARCH_PASSWORD
+//              valueFrom:
+//                secretKeyRef:
+//                  name: ozg-search-cluster-es-elastic-user
+//                  key: elastic
+//          command:
+//            - /bin/sh
+//            - -c
+//            - |
+//              curl -k -X POST -u elastic:$ELASTICSEARCH_PASSWORD -H 'Content-Type: application/json' 'https://ozg-search-cluster-es-http:9200/_security/user/'$ES_NS_USER -d '{"password" : "'$ES_NS_PASSWORD'" ,"roles" : [ "'$ES_NS_USER'" ]}'
\ No newline at end of file
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserCustomResource.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserCustomResource.java
new file mode 100644
index 0000000..e0f02a3
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserCustomResource.java
@@ -0,0 +1,42 @@
+/*
+ * 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.user;
+
+import de.ozgcloud.operator.ElasticCustomResourceStatus;
+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("OzgCloudElasticUser")
+@Group("operator.ozgcloud.de")
+@Version("v1")
+@Singular("ozgcloudelasticuser")
+@Plural("ozgcloudelasticusers")
+@SuppressWarnings("serial")
+class ElasticUserCustomResource extends CustomResource<ElasticUserSpec, ElasticCustomResourceStatus> implements Namespaced {
+}
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserReconciler.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserReconciler.java
new file mode 100644
index 0000000..9c37257
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserReconciler.java
@@ -0,0 +1,26 @@
+package de.ozgcloud.operator.user;
+
+import org.springframework.stereotype.Component;
+
+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.RequiredArgsConstructor;
+import lombok.extern.java.Log;
+
+@Log
+@RequiredArgsConstructor
+@ControllerConfiguration
+@Component
+public class ElasticUserReconciler implements Reconciler<ElasticUserCustomResource> {
+
+	private final ElasticUserService service;
+
+	@Override
+	public UpdateControl<ElasticUserCustomResource> reconcile(ElasticUserCustomResource resource, Context<ElasticUserCustomResource> context)
+			throws Exception {
+		log.info("Reconcile user: " + resource.getCRDName());
+		return service.checkSecret(resource, context);
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSecretBuilder.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSecretBuilder.java
new file mode 100644
index 0000000..b4f9efe
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSecretBuilder.java
@@ -0,0 +1,41 @@
+package de.ozgcloud.operator.user;
+
+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;
+
+@Component
+class ElasticUserSecretBuilder {
+
+	static final String SECRET_TYPE = "Opaque";
+	static final String SECRET_ADDRESS_FIELD = "address";
+	static final String SECRET_INDEX_FIELD = "index";
+	static final String SECRET_PASSWORD_FIELD = "password";
+	static final String SECRET_USERNAME_FIELD = "username";
+
+	static final String SECRET_ADDRESS_VALUE = "ozg-search-cluster-es-http.elastic-system:9200";
+	static final int PASSWORD_LENGTH = 15;
+
+	public Secret build(ElasticUserCustomResource resource, String name) {
+		var namespace = resource.getMetadata().getNamespace();
+		return new SecretBuilder()
+				.withType(SECRET_TYPE)
+				.withMetadata(createMetaData(name, namespace))
+				.addToStringData(SECRET_ADDRESS_FIELD, SECRET_ADDRESS_VALUE)
+				.addToStringData(SECRET_INDEX_FIELD, namespace)
+				.addToStringData(SECRET_PASSWORD_FIELD, RandomStringUtils.randomAlphabetic(PASSWORD_LENGTH))
+				.addToStringData(SECRET_USERNAME_FIELD, namespace)
+				.build();
+	}
+
+	private ObjectMeta createMetaData(String name, String namespace) {
+		var metadata = new ObjectMeta();
+		metadata.setName(name);
+		metadata.setNamespace(namespace);
+
+		return metadata;
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserService.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserService.java
new file mode 100644
index 0000000..71324cb
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserService.java
@@ -0,0 +1,68 @@
+package de.ozgcloud.operator.user;
+
+import java.util.Objects;
+
+import org.springframework.stereotype.Component;
+
+import de.ozgcloud.operator.CustomResourceStatus;
+import de.ozgcloud.operator.OperatorConfig;
+import de.ozgcloud.operator.common.kubernetes.KubernetesService;
+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 io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.java.Log;
+
+@Log
+@RequiredArgsConstructor
+@Component
+class ElasticUserService {
+
+	static final String ELASTIC_USER_SECRET_NAME = "elasticsearch-credentials";
+
+	private final ElasticUserSecretBuilder secretBuilder;
+
+	private final KubernetesService kubernetesService;
+
+	public UpdateControl<ElasticUserCustomResource> checkSecret(ElasticUserCustomResource resource, Context<ElasticUserCustomResource> context) {
+		try {
+			log.info("Check secret...");
+			var secretResource = getSecretResource(resource);
+			if (Objects.isNull(secretResource.get())) {
+				log.info("Secret not exists, create one...");
+				
+				createAdapter(secretResource).create(secretBuilder.build(resource, ELASTIC_USER_SECRET_NAME));
+				
+				log.info("Secret creation successful.");
+			}
+			return ElasticUserUpdateControlBuilder.fromResource(resource).withStatus(CustomResourceStatus.OK).build();
+		} catch (Exception e) {
+			log.info("Secret creation failed: " + e);
+			return buildExceptionUpdateControl(resource, e);
+		}
+	}
+
+	private Resource<Secret> getSecretResource(ElasticUserCustomResource resource) {
+		return kubernetesService.getSecretResource(resource.getMetadata().getNamespace(), ELASTIC_USER_SECRET_NAME);
+	}
+	
+	ResourceAdapter<Secret> createAdapter(Resource<Secret> resource) {
+		return new ResourceAdapter<Secret>(resource);
+	}
+	
+	void createBySecret(Resource<Secret> resource, Secret secret) {
+		var resourceAdapter = new ResourceAdapter<Secret>(resource);
+		resourceAdapter.create(secret);
+	}
+
+	UpdateControl<ElasticUserCustomResource> buildExceptionUpdateControl(ElasticUserCustomResource resource, Exception e) {
+		return ElasticUserUpdateControlBuilder
+				.fromResource(resource)
+				.withStatus(CustomResourceStatus.ERROR)
+				.withMessage(e.getMessage())
+				.withReschedule(OperatorConfig.RECONCILER_RETRY_SECONDS_ON_ERROR)
+				.build();
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSpec.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSpec.java
new file mode 100644
index 0000000..47b1ba6
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSpec.java
@@ -0,0 +1,38 @@
+/*
+ * 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.user;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Builder
+@JsonIgnoreProperties(ignoreUnknown = true)
+class ElasticUserSpec {
+
+}
diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserUpdateControlBuilder.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserUpdateControlBuilder.java
new file mode 100644
index 0000000..88fa010
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserUpdateControlBuilder.java
@@ -0,0 +1,63 @@
+package de.ozgcloud.operator.user;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import de.ozgcloud.operator.CustomResourceStatus;
+import de.ozgcloud.operator.ElasticCustomResourceStatus;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+
+class ElasticUserUpdateControlBuilder {
+
+	private ElasticUserCustomResource resource;
+
+	private CustomResourceStatus status;
+	private Optional<String> message = Optional.empty();
+
+	private boolean reschedule = false;
+	private Duration scheduleDuration;
+
+	public ElasticUserUpdateControlBuilder(ElasticUserCustomResource resource) {
+		this.resource = resource;
+	}
+
+	public static ElasticUserUpdateControlBuilder fromResource(ElasticUserCustomResource resource) {
+		return new ElasticUserUpdateControlBuilder(resource);
+	}
+
+	public ElasticUserUpdateControlBuilder withStatus(CustomResourceStatus status) {
+		this.status = status;
+		return this;
+	}
+
+	public ElasticUserUpdateControlBuilder withMessage(String message) {
+		this.message = Optional.ofNullable(message);
+		return this;
+	}
+
+	public ElasticUserUpdateControlBuilder withReschedule(Duration duration) {
+		this.reschedule = true;
+		this.scheduleDuration = duration;
+		return this;
+	}
+
+	public UpdateControl<ElasticUserCustomResource> build() {
+		resource.setStatus(buildElasticCustomResourceStatus());
+
+		return buildUpdateControl();
+	}
+
+	private ElasticCustomResourceStatus buildElasticCustomResourceStatus() {
+		var userStatus = ElasticCustomResourceStatus.builder().status(status);
+		message.ifPresent(userStatus::message);
+
+		return userStatus.build();
+	}
+
+	private UpdateControl<ElasticUserCustomResource> buildUpdateControl() {
+		if (reschedule) {
+			return UpdateControl.updateStatus(resource).rescheduleAfter(scheduleDuration);
+		}
+		return UpdateControl.updateStatus(resource);
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesServiceTest.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesServiceTest.java
new file mode 100644
index 0000000..bcbbb65
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/KubernetesServiceTest.java
@@ -0,0 +1,109 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+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 de.ozgcloud.operator.user.ObjectMetaTestFactory;
+import io.fabric8.kubernetes.api.model.Namespace;
+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 KubernetesServiceTest {
+
+	@Rule
+	private KubernetesServer server;
+	private KubernetesClient client;
+	private KubernetesService service;
+
+	@BeforeEach
+	void init() {
+		server = new KubernetesServer(true, true);
+		server.before();
+		client = server.getClient();
+		service = new KubernetesService(client);
+	}
+
+	@DisplayName("Exist namespace")
+	@Nested
+	class TestExistNamespace {
+
+		private final Namespace namespace = NamespaceTestFactory.create();
+		
+		@Test
+		public void shouldReturnTrueIfExistsWithCrud() {
+			var namespaceResource = client.namespaces().withName(ObjectMetaTestFactory.NAMESPACE);
+			var adapter = new ResourceAdapter<>(namespaceResource);
+			adapter.create(namespace);
+			
+			var exists = service.existNamespace(ObjectMetaTestFactory.NAMESPACE);
+
+			assertThat(exists).isTrue();
+		}
+
+		@Test
+		public void shouldReturnTrueIfExists() {
+			server.expect().get().withPath("/api/v1/namespaces/" + ObjectMetaTestFactory.NAMESPACE)
+					.andReturn(HttpURLConnection.HTTP_OK, namespace)
+					.once();
+
+			var exists = service.existNamespace(ObjectMetaTestFactory.NAMESPACE);
+
+			assertThat(exists).isTrue();
+		}
+
+		@Test
+		public void shouldReturnFalseIfMissing() {
+			var exists = service.existNamespace(ObjectMetaTestFactory.NAMESPACE);
+
+			assertThat(exists).isFalse();
+		}
+	}
+
+	@DisplayName("Get secret")
+	@Nested
+	class TestGetSecret {
+		
+		private final Secret secret = SecretTestFactory.create(); 
+
+		@Test
+		void shouldReturnExistingResourceIfExists() {
+			server.expect().get().withPath("/api/v1/namespaces/" + ObjectMetaTestFactory.NAMESPACE + "/secrets/" + SecretTestFactory.NAME)
+					.andReturn(HttpURLConnection.HTTP_OK, secret)
+					.once();
+
+			var secret = getSecret();
+
+			assertThat(secret).isNotNull();
+		}
+
+		@Test
+		void shouldReturnNullNOTExists() {
+			var secret = getSecret();
+
+			assertThat(secret).isNull();
+		}
+		
+		private Secret getSecret() {
+			return service.getSecretResource(ObjectMetaTestFactory.NAMESPACE, SecretTestFactory.NAME).get();
+		}
+	}
+	
+	//PoC
+	@Nested
+	class TestDoInPod {
+		
+		@Test
+		void doSomething() {
+			var pod = service.doInPod("test", "pod");
+		}
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/NamespaceTestFactory.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/NamespaceTestFactory.java
new file mode 100644
index 0000000..6c1e676
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/NamespaceTestFactory.java
@@ -0,0 +1,16 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import de.ozgcloud.operator.user.ObjectMetaTestFactory;
+import io.fabric8.kubernetes.api.model.Namespace;
+import io.fabric8.kubernetes.api.model.NamespaceBuilder;
+
+public class NamespaceTestFactory {
+
+	public static final Namespace create() {
+		return createBuilder().build();
+	}
+	
+	public static NamespaceBuilder createBuilder() {
+		return new NamespaceBuilder().withNewMetadata().withName(ObjectMetaTestFactory.NAMESPACE).endMetadata();
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/SecretTestFactory.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/SecretTestFactory.java
new file mode 100644
index 0000000..debcddf
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/kubernetes/SecretTestFactory.java
@@ -0,0 +1,21 @@
+package de.ozgcloud.operator.common.kubernetes;
+
+import de.ozgcloud.operator.user.ObjectMetaTestFactory;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+
+public class SecretTestFactory {
+	
+	final static String NAME = "secretName";
+
+	public static final Secret create() {
+		return createBuilder().build();
+	}
+	
+	public static final SecretBuilder createBuilder() {
+		var builder = new SecretBuilder();
+		builder.withNewMetadata().withName(NAME).withNamespace(ObjectMetaTestFactory.NAMESPACE).endMetadata().build();
+	
+		return builder;
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticCustomResourceStatusTestFactory.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticCustomResourceStatusTestFactory.java
new file mode 100644
index 0000000..36fd6fd
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticCustomResourceStatusTestFactory.java
@@ -0,0 +1,18 @@
+package de.ozgcloud.operator.user;
+
+import de.ozgcloud.operator.CustomResourceStatus;
+import de.ozgcloud.operator.ElasticCustomResourceStatus;
+
+public class ElasticCustomResourceStatusTestFactory {
+	
+	public final static CustomResourceStatus STATUS = CustomResourceStatus.OK;
+	
+	public static ElasticCustomResourceStatus create() {
+		return createBuilder().build();
+	}
+	
+	public static ElasticCustomResourceStatus.ElasticCustomResourceStatusBuilder createBuilder() {
+		return ElasticCustomResourceStatus.builder()
+				.status(STATUS);
+	}	
+}
\ No newline at end of file
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserCustomResourceTestFactory.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserCustomResourceTestFactory.java
new file mode 100644
index 0000000..47599fb
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserCustomResourceTestFactory.java
@@ -0,0 +1,11 @@
+package de.ozgcloud.operator.user;
+
+public class ElasticUserCustomResourceTestFactory {
+	
+	public static ElasticUserCustomResource create() {
+		var resource = new ElasticUserCustomResource();
+		resource.setStatus(ElasticCustomResourceStatusTestFactory.create());
+		resource.setMetadata(ObjectMetaTestFactory.create());
+		return resource;
+	}
+}
\ No newline at end of file
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerITCase.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerITCase.java
new file mode 100644
index 0000000..f9d9fb0
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerITCase.java
@@ -0,0 +1,64 @@
+package de.ozgcloud.operator.user;
+
+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 ElasticUserReconcilerITCase {
+
+	@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-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerTest.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerTest.java
new file mode 100644
index 0000000..20915e5
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserReconcilerTest.java
@@ -0,0 +1,48 @@
+package de.ozgcloud.operator.user;
+
+import static org.mockito.Mockito.*;
+
+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.common.kubernetes.KubernetesService;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import lombok.SneakyThrows;
+
+class ElasticUserReconcilerTest {
+	
+	@Spy
+	@InjectMocks
+	private ElasticUserReconciler reconciler;
+	@Mock
+	private ElasticUserService service;
+	@Mock
+	private KubernetesService kubernetesService;
+
+	@DisplayName("Reconcile")
+	@Nested
+	class TestReconcile {
+		
+		@Mock
+		private Context<ElasticUserCustomResource> context;
+		
+		private final ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create();
+		
+		@Test
+		void shouldCreateSecret() {
+			reconcile();
+			
+			verify(service).checkSecret(resource, context);
+		}
+		
+		@SneakyThrows
+		private UpdateControl<ElasticUserCustomResource> reconcile() {
+			return reconciler.reconcile(resource, context);
+		}
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserSecretBuilderTest.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserSecretBuilderTest.java
new file mode 100644
index 0000000..7795ab6
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserSecretBuilderTest.java
@@ -0,0 +1,76 @@
+package de.ozgcloud.operator.user;
+
+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;
+
+class ElasticUserSecretBuilderTest {
+
+	private final ElasticUserSecretBuilder builder = new ElasticUserSecretBuilder();
+	
+	@DisplayName("Build")
+	@Nested
+	class TestBuild {
+		
+		private ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create();
+		private final String secretName = "SecretName";
+			
+		@Test
+		void shouldContainType() {
+			var secret = builder.build(resource, secretName);
+			
+			assertThat(secret.getType()).isEqualTo(ElasticUserSecretBuilder.SECRET_TYPE);
+		}
+		
+		
+		@DisplayName("metadata")
+		@Nested
+		class TestMetadata {
+			
+			@Test
+			void shouldContainName() {
+				var secret = builder.build(resource, secretName);
+				
+				assertThat(secret.getMetadata().getName()).isEqualTo(ElasticUserSecretBuilder.SECRET_TYPE);
+			}
+			
+			@Test
+			void shouldContainNamespace() {
+				var secret = builder.build(resource, secretName);
+				
+				assertThat(secret.getMetadata().getNamespace()).isEqualTo(ObjectMetaTestFactory.NAMESPACE);
+			}
+		}
+		
+		@Test
+		void shouldContainAddress() {
+			var secret = builder.build(resource, secretName);
+			
+			assertThat(secret.getStringData()).containsEntry(ElasticUserSecretBuilder.SECRET_ADDRESS_FIELD, ElasticUserSecretBuilder.SECRET_ADDRESS_VALUE);
+		}
+		
+		@Test
+		void shouldContainIndex() {
+			var secret = builder.build(resource, secretName);
+			
+			assertThat(secret.getStringData()).containsEntry(ElasticUserSecretBuilder.SECRET_INDEX_FIELD, ObjectMetaTestFactory.NAMESPACE);
+		}
+		
+		@Test
+		void shouldContainPassword() {
+			var secret = builder.build(resource, secretName);
+			
+			assertThat(secret.getStringData()).containsKey(ElasticUserSecretBuilder.SECRET_PASSWORD_FIELD);
+			assertThat(secret.getStringData().get(ElasticUserSecretBuilder.SECRET_PASSWORD_FIELD)).isNotNull();
+		}
+		
+		@Test
+		void shouldContainUsername() {
+			var secret = builder.build(resource, secretName);
+			
+			assertThat(secret.getStringData()).containsEntry(ElasticUserSecretBuilder.SECRET_USERNAME_FIELD, ObjectMetaTestFactory.NAMESPACE);
+		}
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserServiceTest.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserServiceTest.java
new file mode 100644
index 0000000..828d8b5
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ElasticUserServiceTest.java
@@ -0,0 +1,160 @@
+package de.ozgcloud.operator.user;
+
+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.Disabled;
+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.common.kubernetes.KubernetesService;
+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;
+
+class ElasticUserServiceTest {
+	
+	@Spy
+	@InjectMocks
+	private ElasticUserService service;
+	@Mock
+	private ElasticUserSecretBuilder secretBuilder;
+	@Mock
+	private KubernetesService kubernetesService;
+
+	@DisplayName("Check secret")
+	@Nested
+	class TestCheckSecret {
+		
+		@Mock
+		private Context<ElasticUserCustomResource> context;
+		
+		private final ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create(); 
+		
+		@Test
+		void shouldGetSecret() {
+			service.checkSecret(resource, context);
+			
+			verify(kubernetesService).getSecretResource(ObjectMetaTestFactory.NAMESPACE, ElasticUserService.ELASTIC_USER_SECRET_NAME);
+		}
+		
+		@DisplayName("on missing secret")
+		@Nested
+		class TestOnMissingSecret {
+			
+			@Mock
+			private ResourceAdapter<Secret> resourceAdapter;
+			@Mock
+			private Resource<Secret> secretResource;
+			
+			private final Secret secret = SecretTestFactory.create();
+			
+			@BeforeEach
+			void mockResourceAdapter() {
+				when(kubernetesService.getSecretResource(any(), any())).thenReturn(secretResource);
+				when(secretResource.get()).thenReturn(null);
+				
+				doReturn(resourceAdapter).when(service).createAdapter(any());
+			}
+			
+			@Test
+			void shouldBuildSecret() {
+				service.checkSecret(resource, context);
+				
+				verify(secretBuilder).build(resource, ElasticUserService.ELASTIC_USER_SECRET_NAME);
+			}
+			
+			@Test
+			void shouldCreateSecret() {
+				when(secretBuilder.build(any(), any())).thenReturn(secret);
+				
+				service.checkSecret(resource, context);
+				
+				verify(resourceAdapter).create(secret);
+			}
+			
+			@DisplayName("update control")
+			@Nested
+			class TestUpdateControl {
+				
+				@Test
+				void shouldNotBeUpdateable() {
+					var updateControl = service.checkSecret(resource, context);
+					
+					assertThat(updateControl.isUpdateResourceAndStatus()).isFalse();
+				}
+				
+				@Test
+				void shouldContainResource() {
+					var updateControl = service.checkSecret(resource, context);
+					
+					assertThat(updateControl.getResource()).isEqualTo(resource);
+				}
+			}
+		}
+		
+		@DisplayName("On Exception")
+		@Nested
+		class TestOnException {
+			
+			private final Exception ex = new RuntimeException();
+			
+			@Test
+			void shouldBuildExceptionUpdateControlOnException() {
+				doThrow(ex).when(kubernetesService).getSecretResource(any(), any());
+				
+				service.checkSecret(resource, context);
+				
+				verify(service).buildExceptionUpdateControl(resource, ex);
+			}
+		}
+		
+		@DisplayName("build exception update control")
+		@Nested
+		class TestBuildExceptionUpdateControl {
+			
+			private final ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create(); 
+			private final Exception exception = new RuntimeException();
+			
+			@Test
+			void shouldContainResource() {
+				var updateControl = service.buildExceptionUpdateControl(resource, exception);
+				
+				assertThat(updateControl.getResource()).isEqualTo(resource);
+			}
+			
+			@Disabled("FIXME")
+			@Test
+			void shouldContainUpdateStatus() {
+				var updateControl = service.buildExceptionUpdateControl(resource, exception);
+				
+				assertThat(updateControl.isUpdateResource()).isFalse();
+				assertThat(updateControl.isPatchStatus()).isFalse();
+				assertThat(updateControl.isNoUpdate()).isFalse();
+			}
+			
+			@Test
+			void shouldContainReschedule() {
+				var updateControl = service.buildExceptionUpdateControl(resource, exception);
+				
+				assertThat(updateControl.getScheduleDelay()).hasValue(60000L);
+			}
+			
+			@Disabled("FIXME")
+			@Test
+			void shouldContainMessage() {
+				var updateControl = service.buildExceptionUpdateControl(resource, exception);
+				
+				//TBD?
+			}
+		}
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ObjectMetaTestFactory.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ObjectMetaTestFactory.java
new file mode 100644
index 0000000..817ff94
--- /dev/null
+++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/user/ObjectMetaTestFactory.java
@@ -0,0 +1,15 @@
+package de.ozgcloud.operator.user;
+
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+
+public class ObjectMetaTestFactory {
+	
+	public static final String NAMESPACE ="TestNamespace";
+
+	public static ObjectMeta create() {
+		var objectMeta = new ObjectMeta();
+		objectMeta.setNamespace(NAMESPACE);
+		
+		return objectMeta;
+	}
+}
diff --git a/ozgcloud-elastic-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/ozgcloud-elastic-operator/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 0000000..79b126e
--- /dev/null
+++ b/ozgcloud-elastic-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/ozgcloud-elastic-operator/src/test/resources/junit-platform.properties b/ozgcloud-elastic-operator/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000..b059a65
--- /dev/null
+++ b/ozgcloud-elastic-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/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/OzgCloudOperatorApplication.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/KeycloakOperatorApplication.java
similarity index 91%
rename from ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/OzgCloudOperatorApplication.java
rename to ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/KeycloakOperatorApplication.java
index 894e32d..27c3b68 100644
--- a/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/OzgCloudOperatorApplication.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 OzgCloudOperatorApplication {
+public class KeycloakOperatorApplication {
 
 	public static void main(String[] args) {
-		SpringApplication.run(OzgCloudOperatorApplication.class, args);
+		SpringApplication.run(KeycloakOperatorApplication.class, args);
 	}
 }
diff --git a/ozgcloud-keycloak-operator/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 ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/OzgOperatorApplicationTests.java
rename to ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/KeycloakOperatorApplicationTest.java
index c1f4435..52a4ee1 100644
--- a/ozgcloud-keycloak-operator/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/pom.xml b/pom.xml
index ce13f5a..dfe0be2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,7 @@
 
 	<modules>
 		<module>ozgcloud-keycloak-operator</module>
+		<module>ozgcloud-elastic-operator</module>
 	</modules>
 
 	<properties>
-- 
GitLab