diff --git a/ozgcloud-elastic-operator/pom.xml b/ozgcloud-elastic-operator/pom.xml index 175a048030bf56af8c6f35388752bf9df6f63a57..bc621e55a2acbcce53183bf9a8eaa5d1860dd63d 100644 --- a/ozgcloud-elastic-operator/pom.xml +++ b/ozgcloud-elastic-operator/pom.xml @@ -19,6 +19,17 @@ <properties> <spring-boot.build-image.imageName>docker.ozg-sh.de/ozgcloud-elastic-operator:build-latest</spring-boot.build-image.imageName> </properties> + + <dependencies> + <dependency> + <groupId>co.elastic.clients</groupId> + <artifactId>elasticsearch-java</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </dependency> + </dependencies> <build> <plugins> 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 index 2c35d6d4fe7f34dba321603dbc7d22be3ec417b4..cad0e94677b1443993d4b1f900adf01227c147cc 100644 --- a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticOperatorApplication.java +++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/ElasticOperatorApplication.java @@ -32,4 +32,4 @@ public class ElasticOperatorApplication { public static void main(String[] args) { SpringApplication.run(ElasticOperatorApplication.class, args); } -} +} \ No newline at end of file diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchProperties.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..1aeef178a3d96626e24d239a307b702b315adb4b --- /dev/null +++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchProperties.java @@ -0,0 +1,17 @@ +package de.ozgcloud.operator.common.elasticsearch; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Getter; + +@Getter +@ConfigurationProperties("ozgcloud.elastic") +public class ElasticSearchProperties { + + private String namespace; + private String secretName; + private String secretData; + private String host; + private String address; + private int port; +} diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchRemoteService.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchRemoteService.java new file mode 100644 index 0000000000000000000000000000000000000000..76a212790e738ff76499f2818f54d4e729d7f094 --- /dev/null +++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchRemoteService.java @@ -0,0 +1,137 @@ +package de.ozgcloud.operator.common.elasticsearch; + +import java.io.IOException; +import java.util.Objects; +import java.util.logging.Level; + +import jakarta.annotation.PostConstruct; + +import org.apache.commons.collections.MapUtils; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.elasticsearch.client.RestClient; +import org.springframework.stereotype.Component; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch.indices.ExistsRequest; +import co.elastic.clients.elasticsearch.security.GetRoleRequest; +import co.elastic.clients.elasticsearch.security.GetUserRequest; +import co.elastic.clients.elasticsearch.security.PutRoleRequest; +import co.elastic.clients.elasticsearch.security.PutUserRequest; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import de.ozgcloud.operator.common.kubernetes.KubernetesService; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; + +@Log +@RequiredArgsConstructor +@Component +class ElasticSearchRemoteService { + + private final ElasticSearchProperties elasticSearchProperties; + + private final KubernetesService kubernetesService; + + private ElasticsearchClient client = null; + + //TOTHINK START: Als Bean umsetzen?/Kann man den Client eleganter erstellen? + @PostConstruct + private void initClient() { + if (Objects.isNull(client)) { + client = createClient(elasticSearchProperties.getSecretData(), getPassword()); + } + } + + ElasticsearchClient createClient(String userName, String password) { + var credentialsProvider = createCredentialsProvider(userName, password); + var restClient = buildRestClient(credentialsProvider); + var transport = createRestClientTransport(restClient); + return new ElasticsearchClient(transport); + } + + private BasicCredentialsProvider createCredentialsProvider(String userName, String password) { + var credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY,new UsernamePasswordCredentials(userName, password)); + return credentialsProvider; + } + + private RestClient buildRestClient(BasicCredentialsProvider credentialsProvider) { + return RestClient.builder(createHttpHost()) + .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)) + .build(); + } + + private HttpHost createHttpHost() { + return new HttpHost(elasticSearchProperties.getHost(), elasticSearchProperties.getPort()); + } + + private RestClientTransport createRestClientTransport(RestClient restClient) { + return new RestClientTransport(restClient, new JacksonJsonpMapper()); + } + + private String getPassword() { + var resource = kubernetesService.getSecretResource(elasticSearchProperties.getNamespace(), ""); + var password = MapUtils.getString(resource.get().getStringData(), elasticSearchProperties.getSecretData()); + return password; + } + //TOTHINK END + + public boolean existsIndex(String index) throws Exception { + try { + log.info("Exists index " + index + "..."); + var exists = client.indices().exists(ExistsRequest.of(builder -> builder.index(index))).value(); + log.info("Exists index: " + exists); + return exists; + } catch (ElasticsearchException | IOException e) { + log.log(Level.SEVERE, "Error checking index '" + index + "': " + e); + throw e; + } + } + + public void createIndex(String indexName) throws Exception { + try { + log.info("Create index " + indexName + "..."); + client.indices().create(builder -> builder.index(indexName)); + log.info("Create index successful."); + } catch(Exception e) { + log.log(Level.SEVERE, "Create index failed." + e); + throw e; + } + } + + public boolean existsSecurityRole(String roleName) throws Exception { + var role = client.security().getRole(GetRoleRequest.of(builder -> builder.name(roleName))); + return !role.result().isEmpty(); + } + + public void createSecurityRole(PutRoleRequest request) throws Exception { + try { + log.info("Create SecurityRole " + request.name() + "..."); + client.security().putRole(request); + log.info("Create SecurityRole successful."); + } catch(Exception e) { + log.log(Level.SEVERE, "Create SecurityRole failed." + e); + throw e; + } + } + + public boolean existsSecurityUser(String userName) throws Exception { + var user = client.security().getUser(GetUserRequest.of(builder -> builder.username(userName))); + return !user.result().isEmpty(); + } + + public void createSecurityUser(PutUserRequest request) throws Exception { + try { + log.info("Create SecurityUser " + request.username() + "..."); + client.security().putUser(request); + log.info("Create SecurityUser successful."); + } catch(Exception e) { + log.log(Level.SEVERE, "Create SecurityUser failed." + e); + throw e; + } + } +} \ No newline at end of file diff --git a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchService.java b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchService.java new file mode 100644 index 0000000000000000000000000000000000000000..a5bc270018901d1efd41db43cbc09fbc649c30af --- /dev/null +++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchService.java @@ -0,0 +1,78 @@ +package de.ozgcloud.operator.common.elasticsearch; + +import org.springframework.stereotype.Component; + +import co.elastic.clients.elasticsearch.security.IndicesPrivileges; +import co.elastic.clients.elasticsearch.security.PutRoleRequest; +import co.elastic.clients.elasticsearch.security.PutUserRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; + +@Log +@RequiredArgsConstructor +@Component +public class ElasticSearchService { + + static final String PRIVILEGES_ALL = "all"; + + private final ElasticSearchRemoteService remoteService; + +// curl -k -X PUT -u elastic:$ELASTICSEARCH_PASSWORD -H 'Content-Type: application/json' 'https://ozg-search-cluster-es-http:9200/'$ES_NS_USER + public void checkIndex(String namespace) throws Exception { + log.info("Check index..."); + if(!remoteService.existsIndex(namespace)) { + remoteService.createIndex(namespace); + } + log.info("Check index successful."); + } + +// 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"] } ] }' + public void checkSecurityRole(String namespace) throws Exception { + log.info("Check security role..."); + if(!remoteService.existsSecurityRole(namespace)) { + remoteService.createSecurityRole(createPutRoleRequest(namespace)); + } + log.info("Check security role successful."); + } + + PutRoleRequest createPutRoleRequest(String namespace) { + return PutRoleRequest.of(requestBuilder -> buildRequest(requestBuilder, namespace)); + } + + private PutRoleRequest.Builder buildRequest(PutRoleRequest.Builder requestBuilder, String namespace) { + requestBuilder.name(namespace); + requestBuilder.indices(builder -> buildIndicesPrivilegesRequest(builder, namespace)); + + return requestBuilder; + } + + private IndicesPrivileges.Builder buildIndicesPrivilegesRequest(IndicesPrivileges.Builder builder, String namespace) { + builder.names(namespace); + builder.privileges(PRIVILEGES_ALL); + + return builder; + } + +// 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'" ]}' + public void checkSecurityUser(String namespace, String password) throws Exception { + log.info("Check security user..."); + if(!remoteService.existsSecurityUser(namespace)) { + remoteService.createSecurityUser(createPutUserRequest(namespace, password)); + } + log.info("Check security user successful."); + } + + PutUserRequest createPutUserRequest(String namespace, String password) { + return PutUserRequest.of(requestBuilder -> buildPutUserRequest(requestBuilder, namespace, password)); + } + + private PutUserRequest.Builder buildPutUserRequest(PutUserRequest.Builder requestBuilder, String namespace, String password) { + requestBuilder.username(namespace); + requestBuilder.roles(namespace); + requestBuilder.password(password); + + return requestBuilder; + } +} \ No newline at end of file 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/ElasticSecretHelper.java similarity index 61% rename from ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSecretBuilder.java rename to ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticSecretHelper.java index b4f9efedca845f5ba163a6b5e46f27d890ca0486..54272d59eb3afd9167952a84c2787cb5ed213110 100644 --- a/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticUserSecretBuilder.java +++ b/ozgcloud-elastic-operator/src/main/java/de/ozgcloud/operator/user/ElasticSecretHelper.java @@ -3,33 +3,44 @@ package de.ozgcloud.operator.user; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.stereotype.Component; +import de.ozgcloud.operator.common.elasticsearch.ElasticSearchProperties; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor @Component -class ElasticUserSecretBuilder { +public class ElasticSecretHelper { 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; + + private final ElasticSearchProperties properties; - public Secret build(ElasticUserCustomResource resource, String name) { - var namespace = resource.getMetadata().getNamespace(); + public Secret buildCredentialSecret(String namespace, String name) { return new SecretBuilder() .withType(SECRET_TYPE) .withMetadata(createMetaData(name, namespace)) - .addToStringData(SECRET_ADDRESS_FIELD, SECRET_ADDRESS_VALUE) + .addToStringData(SECRET_ADDRESS_FIELD, buildSecretAddress()) .addToStringData(SECRET_INDEX_FIELD, namespace) - .addToStringData(SECRET_PASSWORD_FIELD, RandomStringUtils.randomAlphabetic(PASSWORD_LENGTH)) + .addToStringData(SECRET_PASSWORD_FIELD, generatePassword()) .addToStringData(SECRET_USERNAME_FIELD, namespace) .build(); } + + private String buildSecretAddress() { + return String.format("%s:%s", properties.getAddress(), properties.getPort()); + } + + private String generatePassword() { + return RandomStringUtils.randomAlphabetic(PASSWORD_LENGTH); + } private ObjectMeta createMetaData(String name, String namespace) { var metadata = new ObjectMeta(); 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 index 9c37257978d7f7256e9fefeca020ec7c39be9c10..43f3ff4121f5f002d524a8c523a55edb9e260edf 100644 --- 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 @@ -1,7 +1,12 @@ package de.ozgcloud.operator.user; +import org.apache.commons.collections.MapUtils; import org.springframework.stereotype.Component; +import de.ozgcloud.operator.CustomResourceStatus; +import de.ozgcloud.operator.OperatorConfig; +import de.ozgcloud.operator.common.elasticsearch.ElasticSearchService; +import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -16,11 +21,35 @@ import lombok.extern.java.Log; public class ElasticUserReconciler implements Reconciler<ElasticUserCustomResource> { private final ElasticUserService service; + private final ElasticSearchService searchService; @Override - public UpdateControl<ElasticUserCustomResource> reconcile(ElasticUserCustomResource resource, Context<ElasticUserCustomResource> context) - throws Exception { - log.info("Reconcile user: " + resource.getCRDName()); - return service.checkSecret(resource, context); + public UpdateControl<ElasticUserCustomResource> reconcile(ElasticUserCustomResource resource, Context<ElasticUserCustomResource> context) { + try { + log.info("Reconcile user " + resource.getCRDName() + "..."); + var namespace = resource.getMetadata().getNamespace(); + var secret = service.getOrCreateCredentialSecret(resource, context); + searchService.checkIndex(namespace); + searchService.checkSecurityRole(namespace); + searchService.checkSecurityUser(namespace, getPassword(secret)); + log.info("Reconcile user successful."); + return ElasticUserUpdateControlBuilder.fromResource(resource).withStatus(CustomResourceStatus.OK).build(); + } catch (Exception e) { + log.info("Reconcile user failed: " + e); + return buildExceptionUpdateControl(resource, e); + } } -} + + private String getPassword(Secret secret) { + return MapUtils.getString(secret.getStringData(), ElasticSecretHelper.SECRET_PASSWORD_FIELD); + } + + UpdateControl<ElasticUserCustomResource> buildExceptionUpdateControl(ElasticUserCustomResource resource, Exception e) { + return ElasticUserUpdateControlBuilder + .fromResource(resource) + .withStatus(CustomResourceStatus.ERROR) + .withReschedule(OperatorConfig.RECONCILER_RETRY_SECONDS_ON_ERROR) + .withMessage(e.getMessage()) + .build(); + } +} \ No newline at end of file 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 index 71324cbb538d86aa11a3bac6d772818532f86887..7c7174d0192916e593841c4938879491083a9f7f 100644 --- 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 @@ -4,14 +4,11 @@ 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; @@ -22,47 +19,38 @@ class ElasticUserService { static final String ELASTIC_USER_SECRET_NAME = "elasticsearch-credentials"; - private final ElasticUserSecretBuilder secretBuilder; + private final ElasticSecretHelper secretHelper; private final KubernetesService kubernetesService; - public UpdateControl<ElasticUserCustomResource> checkSecret(ElasticUserCustomResource resource, Context<ElasticUserCustomResource> context) { + public Secret getOrCreateCredentialSecret(ElasticUserCustomResource resource, Context<ElasticUserCustomResource> context) { try { - log.info("Check secret..."); - var secretResource = getSecretResource(resource); + log.info("Get or create secret..."); + var namespace = resource.getMetadata().getNamespace(); + var secretResource = getSecretResource(namespace); + if (Objects.isNull(secretResource.get())) { log.info("Secret not exists, create one..."); - - createAdapter(secretResource).create(secretBuilder.build(resource, ELASTIC_USER_SECRET_NAME)); - + createCredentialSecret(secretResource, namespace); log.info("Secret creation successful."); } - return ElasticUserUpdateControlBuilder.fromResource(resource).withStatus(CustomResourceStatus.OK).build(); + log.info("Secret already exists, return existing."); + return secretResource.get(); } catch (Exception e) { log.info("Secret creation failed: " + e); - return buildExceptionUpdateControl(resource, e); + throw e; } } + + private Resource<Secret> getSecretResource(String namespace) { + return kubernetesService.getSecretResource(namespace, ELASTIC_USER_SECRET_NAME); + } - private Resource<Secret> getSecretResource(ElasticUserCustomResource resource) { - return kubernetesService.getSecretResource(resource.getMetadata().getNamespace(), ELASTIC_USER_SECRET_NAME); + private void createCredentialSecret(Resource<Secret> resource, String namespace) { + createAdapter(resource).create(secretHelper.buildCredentialSecret(namespace, 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/resources/application.yml b/ozgcloud-elastic-operator/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..1154419c5a8c12808b905389395e025d716d4381 --- /dev/null +++ b/ozgcloud-elastic-operator/src/main/resources/application.yml @@ -0,0 +1,8 @@ +ozgcloud: + elastic: + namespace: elastic-system + secretName: ozg-search-cluster-es-elastic-user + secretData: elastic + host: https://ozg-search-cluster-es-http + address: ozg-search-cluster-es-http.elastic-system + port: 9200 \ No newline at end of file diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchRemoteServiceTest.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchRemoteServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..94334ef7f1126f55f5cd6aad6786e3384d39533a --- /dev/null +++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchRemoteServiceTest.java @@ -0,0 +1,24 @@ +package de.ozgcloud.operator.common.elasticsearch; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import de.ozgcloud.operator.common.kubernetes.KubernetesService; + +class ElasticSearchRemoteServiceTest { + + @InjectMocks + private ElasticSearchRemoteService service; + @Mock + private ElasticSearchProperties elasticSearchProperties; + @Mock + private KubernetesService kubernetesService; + + @DisplayName("Exists index") + @Nested + class TestExistsIndex { + + } +} diff --git a/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchServiceTest.java b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8a60a19dc9d9fbdd4585b45434c62dc9b514d8f4 --- /dev/null +++ b/ozgcloud-elastic-operator/src/test/java/de/ozgcloud/operator/common/elasticsearch/ElasticSearchServiceTest.java @@ -0,0 +1,165 @@ +package de.ozgcloud.operator.common.elasticsearch; + +import static de.ozgcloud.operator.user.ObjectMetaTestFactory.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +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 co.elastic.clients.elasticsearch.security.PutRoleRequest; +import co.elastic.clients.elasticsearch.security.PutUserRequest; +import lombok.SneakyThrows; + +class ElasticSearchServiceTest { + + @Spy + @InjectMocks + private ElasticSearchService service; + @Mock + private ElasticSearchRemoteService remoteService; + + @DisplayName("Check index") + @Nested + class TestCheckIndex { + + @SneakyThrows + @Test + void shouldCheckIfIndexExists() { + service.checkIndex(NAMESPACE); + + verify(remoteService).existsIndex(NAMESPACE); + } + + @SneakyThrows + @Test + void shouldCreateIndexIfMissing() { + when(remoteService.existsIndex(any())).thenReturn(false); + + service.checkIndex(NAMESPACE); + + verify(remoteService).createIndex(NAMESPACE); + } + } + + @DisplayName("Check security role") + @Nested + class TestCheckSecurityRole { + + private final PutRoleRequest putRoleRequest = PutRoleRequest.of(b -> b.name("DummyName")); + + @SneakyThrows + @Test + void shouldCheckIfSecurityRoleExists() { + service.checkSecurityRole(NAMESPACE); + + verify(remoteService).existsSecurityRole(NAMESPACE); + } + @SneakyThrows + @Test + void shouldCreateSecurityRoleIfMissing() { + when(remoteService.existsSecurityRole(any())).thenReturn(false); + doReturn(putRoleRequest).when(service).createPutRoleRequest(any()); + + service.checkSecurityRole(NAMESPACE); + + verify(remoteService).createSecurityRole(putRoleRequest); + } + + + @DisplayName("create put role request") + @Nested + class TestCreatePutRoleRequest { + + @Test + void shouldContainName() { + var request = service.createPutRoleRequest(NAMESPACE); + + assertThat(request.name()).isEqualTo(NAMESPACE); + } + + @DisplayName("indices") + @Nested + class TestIndices { + + @Test + void shouldHaveSize() { + var request = service.createPutRoleRequest(NAMESPACE); + + assertThat(request.indices()).hasSize(1); + } + + @Test + void shouldContainName() { + var request = service.createPutRoleRequest(NAMESPACE); + + assertThat(request.indices().get(0).names()).containsExactly(NAMESPACE); + } + + @Test + void shouldContainPrivileges() { + var request = service.createPutRoleRequest(NAMESPACE); + + assertThat(request.indices().get(0).privileges()).containsExactly(ElasticSearchService.PRIVILEGES_ALL); + } + } + } + } + + @DisplayName("Check security user") + @Nested + class TestCheckSecurityUser { + + private final static String PASSWORD = "DummyPassword"; + private final PutUserRequest putUserRequest = PutUserRequest.of(b -> b.username("DummyName")); + + @SneakyThrows + @Test + void shouldCheckIfSecurityUserExists() { + service.checkSecurityUser(NAMESPACE, PASSWORD); + + verify(remoteService).existsSecurityUser(NAMESPACE); + } + @SneakyThrows + @Test + void shouldCreateSecurityUserIfMissing() { + when(remoteService.existsSecurityUser(any())).thenReturn(false); + doReturn(putUserRequest).when(service).createPutUserRequest(any(), any()); + + service.checkSecurityUser(NAMESPACE, PASSWORD); + + verify(remoteService).createSecurityUser(putUserRequest); + } + + @DisplayName("create put user request") + @Nested + class TestCreatePutUserRequest { + + @Test + void shouldContainUser() { + var request = service.createPutUserRequest(NAMESPACE, PASSWORD); + + assertThat(request.username()).isEqualTo(NAMESPACE); + } + + @Test + void shouldContainRole() { + var request = service.createPutUserRequest(NAMESPACE, PASSWORD); + + assertThat(request.roles()).containsExactly(NAMESPACE); + } + + @Test + void shouldContainPassword() { + var request = service.createPutUserRequest(NAMESPACE, PASSWORD); + + assertThat(request.password()).isEqualTo(PASSWORD); + } + } + } +} 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 index debcddfcac36c3392f2e2808d35b5e613a8c7f74..ff8a3b337971cd0d78b6fda3da83696f738da59b 100644 --- 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 @@ -6,7 +6,7 @@ import io.fabric8.kubernetes.api.model.SecretBuilder; public class SecretTestFactory { - final static String NAME = "secretName"; + public final static String NAME = "secretName"; public static final Secret create() { return createBuilder().build(); 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 index 97e47dcc0d897cefb8b7e434e96258467e33ab39..f04a07e273f02a0607690802f357390cb43b7109 100644 --- 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 @@ -1,23 +1,34 @@ 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.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.CustomResourceStatus; +import de.ozgcloud.operator.common.elasticsearch.ElasticSearchService; +import de.ozgcloud.operator.common.kubernetes.SecretTestFactory; +import io.fabric8.kubernetes.api.model.Secret; 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 ElasticSearchService searchService; @DisplayName("Reconcile") @Nested @@ -28,16 +39,113 @@ class ElasticUserReconcilerTest { private final ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create(); - @Test - void shouldCreateSecret() { - reconcile(); + private final static String PASSWORD = "dummyPassword"; + private final Secret secret = SecretTestFactory.createBuilder().addToStringData(ElasticSecretHelper.SECRET_PASSWORD_FIELD, PASSWORD).build(); + + @DisplayName("process flow") + @Nested + class TestProcessFlow { + + @BeforeEach + void mockCredentialSecret() { + when(service.getOrCreateCredentialSecret(any(), any())).thenReturn(secret); + } + + @Test + void shouldGetCredentialSecret() { + reconcile(); + + verify(service).getOrCreateCredentialSecret(resource, context); + } + + @SneakyThrows + @Test + void shouldCheckIndex() { + reconcile(); + + verify(searchService).checkIndex(ObjectMetaTestFactory.NAMESPACE); + } + + @SneakyThrows + @Test + void shouldCheckSecurityRole() { + reconcile(); + + verify(searchService).checkSecurityRole(ObjectMetaTestFactory.NAMESPACE); + } + + @SneakyThrows + @Test + void shouldCheckSecurityUser() { + reconcile(); + + verify(searchService).checkSecurityUser(ObjectMetaTestFactory.NAMESPACE, PASSWORD); + } + } + + @DisplayName("on exception") + @Nested + class TestOnException { - verify(service).checkSecret(resource, context); + private final Exception exception = new RuntimeException(); + + @BeforeEach + void mock() { + when(service.getOrCreateCredentialSecret(any(), any())).thenThrow(exception); + } + + @Test + void shouldBuildExceptionUpdateControl() { + reconcile(); + + verify(reconciler).buildExceptionUpdateControl(resource, exception); + } } @SneakyThrows private UpdateControl<ElasticUserCustomResource> reconcile() { return reconciler.reconcile(resource, context); } + + @DisplayName("build exception update control") + @Nested + class TestBuildExceptionUpdateControl { + + private final ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create(); + private final static String EXCEPTION_MESSAGE = "ExeptionMessage"; + private final Exception exception = new RuntimeException(EXCEPTION_MESSAGE); + + @Test + void shouldContainResource() { + var updateControl = buildExceptionUpdateControl(); + + assertThat(updateControl.getResource()).isEqualTo(resource); + } + + @Test + void shouldContainUpdateStatus() { + var updateControl = buildExceptionUpdateControl(); + + assertThat(updateControl.getResource().getStatus().getStatus()).isEqualTo(CustomResourceStatus.ERROR); + } + + @Test + void shouldContainReschedule() { + var updateControl = buildExceptionUpdateControl(); + + assertThat(updateControl.getScheduleDelay()).hasValue(60000L); + } + + @Test + void shouldContainMessage() { + var updateControl = buildExceptionUpdateControl(); + + assertThat(updateControl.getResource().getStatus().getMessage()).isEqualTo(EXCEPTION_MESSAGE); + } + + private UpdateControl<ElasticUserCustomResource> buildExceptionUpdateControl() { + return reconciler.buildExceptionUpdateControl(resource, exception); + } + } } -} +} \ No newline at end of file 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 index 5172cb9b746a0ffd7ee46bff1dc64ab65f593d86..88f2365f011cea12eee87f9dac43f7467cf8b171 100644 --- 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 @@ -1,76 +1,117 @@ package de.ozgcloud.operator.user; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.ozgcloud.operator.common.elasticsearch.ElasticSearchProperties; +import de.ozgcloud.operator.common.kubernetes.SecretTestFactory; +import io.fabric8.kubernetes.api.model.Secret; class ElasticUserSecretBuilderTest { - private final ElasticUserSecretBuilder builder = new ElasticUserSecretBuilder(); + @Spy + @InjectMocks + private ElasticSecretHelper builder; + @Mock + private ElasticSearchProperties properties; - @DisplayName("Build") + @DisplayName("Build credential secret") @Nested - class TestBuild { + class TestBuildCredentialsSecret { + + private final static String ADDRESS = "dummyAddress"; + private final static int PORT = 42; - private ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create(); - private final String secretName = "SecretName"; + @BeforeEach + void mockProperties() { + when(properties.getAddress()).thenReturn(ADDRESS); + when(properties.getPort()).thenReturn(PORT); + } @Test void shouldContainType() { - var secret = builder.build(resource, secretName); + var secret = buildCredentialSecret(); - assertThat(secret.getType()).isEqualTo(ElasticUserSecretBuilder.SECRET_TYPE); + assertThat(secret.getType()).isEqualTo(ElasticSecretHelper.SECRET_TYPE); } - @DisplayName("metadata") @Nested class TestMetadata { @Test void shouldContainName() { - var secret = builder.build(resource, secretName); + var secret = buildCredentialSecret(); - assertThat(secret.getMetadata().getName()).isEqualTo(secretName); + assertThat(secret.getMetadata().getName()).isEqualTo(SecretTestFactory.NAME); } @Test void shouldContainNamespace() { - var secret = builder.build(resource, secretName); + var secret = buildCredentialSecret(); assertThat(secret.getMetadata().getNamespace()).isEqualTo(ObjectMetaTestFactory.NAMESPACE); } } - @Test - void shouldContainAddress() { - var secret = builder.build(resource, secretName); + @DisplayName("address") + @Nested + class TestAddress { + + @Test + void shouldBeSet() { + var secret = buildCredentialSecret(); + + assertThat(secret.getStringData()).containsEntry(ElasticSecretHelper.SECRET_ADDRESS_FIELD, String.format("%s:%s", ADDRESS, PORT)); + } - assertThat(secret.getStringData()).containsEntry(ElasticUserSecretBuilder.SECRET_ADDRESS_FIELD, ElasticUserSecretBuilder.SECRET_ADDRESS_VALUE); + @Test + void shouldGetAddressFromProperties() { + buildCredentialSecret(); + + verify(properties).getAddress(); + } + + @Test + void shouldGetPortFromPorperties() { + buildCredentialSecret(); + + verify(properties).getPort(); + } } @Test void shouldContainIndex() { - var secret = builder.build(resource, secretName); + var secret = buildCredentialSecret(); - assertThat(secret.getStringData()).containsEntry(ElasticUserSecretBuilder.SECRET_INDEX_FIELD, ObjectMetaTestFactory.NAMESPACE); + assertThat(secret.getStringData()).containsEntry(ElasticSecretHelper.SECRET_INDEX_FIELD, ObjectMetaTestFactory.NAMESPACE); } @Test void shouldContainPassword() { - var secret = builder.build(resource, secretName); + var secret = buildCredentialSecret(); - assertThat(secret.getStringData()).containsKey(ElasticUserSecretBuilder.SECRET_PASSWORD_FIELD); - assertThat(secret.getStringData().get(ElasticUserSecretBuilder.SECRET_PASSWORD_FIELD)).isNotNull(); + assertThat(secret.getStringData()).containsKey(ElasticSecretHelper.SECRET_PASSWORD_FIELD); + assertThat(secret.getStringData().get(ElasticSecretHelper.SECRET_PASSWORD_FIELD)).isNotNull(); } @Test void shouldContainUsername() { - var secret = builder.build(resource, secretName); + var secret = buildCredentialSecret(); - assertThat(secret.getStringData()).containsEntry(ElasticUserSecretBuilder.SECRET_USERNAME_FIELD, ObjectMetaTestFactory.NAMESPACE); + assertThat(secret.getStringData()).containsEntry(ElasticSecretHelper.SECRET_USERNAME_FIELD, ObjectMetaTestFactory.NAMESPACE); + } + + private Secret buildCredentialSecret() { + return builder.buildCredentialSecret(ObjectMetaTestFactory.NAMESPACE, SecretTestFactory.NAME); } } } 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 index 828d8b58c6120081b5ba341fc1496de60536c9a4..e9380b124c265cf632c4ed5bedef512f4180a4a3 100644 --- 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 @@ -5,7 +5,6 @@ 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; @@ -26,7 +25,7 @@ class ElasticUserServiceTest { @InjectMocks private ElasticUserService service; @Mock - private ElasticUserSecretBuilder secretBuilder; + private ElasticSecretHelper secretHelper; @Mock private KubernetesService kubernetesService; @@ -36,125 +35,66 @@ class ElasticUserServiceTest { @Mock private Context<ElasticUserCustomResource> context; + @Mock + private ResourceAdapter<Secret> resourceAdapter; + @Mock + private Resource<Secret> secretResource; + + private final Secret secret = SecretTestFactory.create(); private final ElasticUserCustomResource resource = ElasticUserCustomResourceTestFactory.create(); + @BeforeEach + void mock() { + when(kubernetesService.getSecretResource(any(), any())).thenReturn(secretResource); + when(secretResource.get()).thenReturn(secret); + } + @Test void shouldGetSecret() { - service.checkSecret(resource, context); + service.getOrCreateCredentialSecret(resource, context); verify(kubernetesService).getSecretResource(ObjectMetaTestFactory.NAMESPACE, ElasticUserService.ELASTIC_USER_SECRET_NAME); } - @DisplayName("on missing secret") + @DisplayName("on existing") @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); - } + class TestOnExisting { @Test - void shouldCreateSecret() { - when(secretBuilder.build(any(), any())).thenReturn(secret); - - service.checkSecret(resource, context); + void shouldReturnSecret() { + var secret = service.getOrCreateCredentialSecret(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); - } + assertThat(secret).isNotNull(); } } - @DisplayName("On Exception") + @DisplayName("on missing secret") @Nested - class TestOnException { - - private final Exception ex = new RuntimeException(); - - @Test - void shouldBuildExceptionUpdateControlOnException() { - doThrow(ex).when(kubernetesService).getSecretResource(any(), any()); - - service.checkSecret(resource, context); + class TestOnMissingSecret { - 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); + @BeforeEach + void mock() { + when(secretResource.get()).thenReturn(null); - assertThat(updateControl.getResource()).isEqualTo(resource); + doReturn(resourceAdapter).when(service).createAdapter(any()); } - @Disabled("FIXME") @Test - void shouldContainUpdateStatus() { - var updateControl = service.buildExceptionUpdateControl(resource, exception); + void shouldBuildSecret() { + service.getOrCreateCredentialSecret(resource, context); - assertThat(updateControl.isUpdateResource()).isFalse(); - assertThat(updateControl.isPatchStatus()).isFalse(); - assertThat(updateControl.isNoUpdate()).isFalse(); + verify(secretHelper).buildCredentialSecret(ObjectMetaTestFactory.NAMESPACE, ElasticUserService.ELASTIC_USER_SECRET_NAME); } @Test - void shouldContainReschedule() { - var updateControl = service.buildExceptionUpdateControl(resource, exception); + void shouldCreateSecret() { + when(secretHelper.buildCredentialSecret(any(), any())).thenReturn(secret); - assertThat(updateControl.getScheduleDelay()).hasValue(60000L); - } - - @Disabled("FIXME") - @Test - void shouldContainMessage() { - var updateControl = service.buildExceptionUpdateControl(resource, exception); + service.getOrCreateCredentialSecret(resource, context); - //TBD? + verify(resourceAdapter).create(secret); } } } -} +} \ No newline at end of file