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