diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..1b6985c --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/e2e/build.gradle b/e2e/build.gradle new file mode 100644 index 0000000..98b4024 --- /dev/null +++ b/e2e/build.gradle @@ -0,0 +1,35 @@ +plugins { + // Apply the java-library plugin for API and implementation separation. + id 'java-library' +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + testImplementation 'io.cucumber:cucumber-java:7.11.1' + testImplementation 'org.junit.platform:junit-platform-suite:1.9.2' + testImplementation 'io.cucumber:cucumber-junit-platform-engine:7.11.1' + testImplementation 'io.fabric8:kubernetes-client:6.6.2' + testImplementation 'org.slf4j:slf4j-api:2.0.5' + testImplementation 'org.slf4j:slf4j-simple:2.0.7' + testImplementation 'co.elastic.clients:elasticsearch-java:8.7.1' + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.15.1' + testImplementation 'org.assertj:assertj-core:3.24.2' + testImplementation 'org.awaitility:awaitility:4.2.0' + testImplementation 'org.yaml:snakeyaml:2.0' + + testCompileOnly 'org.projectlombok:lombok:1.18.26' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.26' + + // JUnit 5 + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} diff --git a/e2e/gradle/wrapper/gradle-wrapper.properties b/e2e/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8049c68 --- /dev/null +++ b/e2e/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/e2e/gradlew b/e2e/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/e2e/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/e2e/gradlew.bat b/e2e/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/e2e/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/e2e/settings.gradle b/e2e/settings.gradle new file mode 100644 index 0000000..44aed9e --- /dev/null +++ b/e2e/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.5/userguide/multi_project_builds.html + */ + +rootProject.name = 'eck-cr-e2e-tests' \ No newline at end of file diff --git a/e2e/src/test/java/sk/xco/eckcr/E2eTests.java b/e2e/src/test/java/sk/xco/eckcr/E2eTests.java new file mode 100644 index 0000000..1625f9a --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/E2eTests.java @@ -0,0 +1,11 @@ +package sk.xco.eckcr; + +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.Suite; + +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("features") +public class E2eTests { +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/Common.java b/e2e/src/test/java/sk/xco/eckcr/step/Common.java new file mode 100644 index 0000000..d76cdbb --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/Common.java @@ -0,0 +1,49 @@ +package sk.xco.eckcr.step; + +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.K8sClient.withK8sClient; + +import io.cucumber.java.en.And; +import io.cucumber.java.en.Given; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Common { + + private static final String ECK_CR_POD_NAME = "eck-custom-resources-operator"; + private static final String ES_POD_NAME_PATTERN = "%s-es"; + + @Given("Kubernetes cluster is available") + public void kubernetesClusterAvailable() { + withK8sClient().run(client -> { + client.pods().inNamespace("default").list(); + log.info("K8s available"); + return null; + }); + } + + @And("ECK-CR operator is installed") + public void eckCRInstalled() { + withK8sClient().run(client -> { + var pods = client.pods().inNamespace("default").list().getItems(); + if (pods.stream().noneMatch(pod -> pod.getMetadata().getName().contains(ECK_CR_POD_NAME))) { + fail("ECK-CR not installed"); + } + log.info("ECK-CR present"); + return null; + }); + } + + @And("Elasticsearch {string} is available") + public void elasticsearchAvailable(String esName) { + withK8sClient().run(client -> { + var pods = client.pods().inNamespace("default").list().getItems(); + var esPodName = ES_POD_NAME_PATTERN.formatted(esName); + if (pods.stream().noneMatch(pod -> pod.getMetadata().getName().contains(esPodName))) { + fail("ES %s not installed".formatted(esName)); + } + log.info("ES {} present", esName); + return null; + }); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/K8sResource.java b/e2e/src/test/java/sk/xco/eckcr/step/K8sResource.java new file mode 100644 index 0000000..124cdee --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/K8sResource.java @@ -0,0 +1,160 @@ +package sk.xco.eckcr.step; + +import static sk.xco.eckcr.util.K8sClient.withK8sClient; + +import io.cucumber.java.After; +import io.cucumber.java.ParameterType; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.When; +import io.fabric8.kubernetes.client.KubernetesClientException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.yaml.snakeyaml.Yaml; +import sk.xco.eckcr.util.ApiType; +import sk.xco.eckcr.util.ESClient; + +@Slf4j +public class K8sResource { + private final Set toCleanup = new HashSet<>(); + + @When("the {string} is applied") + public void applyResource(String fileName) throws IOException, InterruptedException { + + var resource = + new String(K8sResource.class.getResourceAsStream(getResourcePath(fileName)).readAllBytes()); + + apply(fileName, resource); + } + + @When("the {string} is applied with {string} set to {string}") + public void applyResourceWithReplacement(String fileName, String replaceKey, String replaceValue) + throws InterruptedException { + String modified = getModifiedResource(fileName, replaceKey, replaceValue); + + apply(fileName, modified); + } + + @Given("the {ApiType} {string} defined in {string} is present") + public void givenResource(ApiType apiType, String resourceName, String fileName) + throws IOException { + var resource = + new String(K8sResource.class.getResourceAsStream(getResourcePath(fileName)).readAllBytes()); + + apply(resourceName, resource); + + waitForResource(apiType, resourceName); + } + + @Given("the {ApiType} {string} defined in {string} is present with {string} set to {string}") + public void givenResourceWithReplacement( + ApiType apiType, + String resourceName, + String fileName, + String replaceKey, + String replaceValue) { + String modified = getModifiedResource(fileName, replaceKey, replaceValue); + + apply(fileName, modified); + + waitForResource(apiType, resourceName); + } + + @Given("the resource defined in {string} is deleted") + public void deleteResource(String fileName) { + withK8sClient() + .run( + client -> { + client + .load(K8sResource.class.getResourceAsStream(getResourcePath(fileName))) + .inNamespace("default") + .delete(); + toCleanup.remove(fileName); + return null; + }); + } + + @After + public void afterScenario() { + withK8sClient() + .run( + client -> { + toCleanup.forEach( + r -> { + log.info("Cleaning-up {}", r); + try { + client + .load(K8sResource.class.getResourceAsStream(getResourcePath(r))) + .inNamespace("default") + .delete(); + } catch (KubernetesClientException e) { + log.warn("Failed to cleanup {}", r, e); + } + }); + return null; + }); + toCleanup.clear(); + } + + @ParameterType( + "Index|Index Template|Index Lifecycle Policy|Ingest Pipeline|Snapshot Repository|Snapshot Lifecycle Policy|User|Role") + public ApiType ApiType(String stringifiedApiType) { + return switch (stringifiedApiType) { + case "Index Template" -> ApiType.IndexTemplate; + case "Index Lifecycle Policy" -> ApiType.IndexLifecyclePolicy; + case "Ingest Pipeline" -> ApiType.IngestPipeline; + case "Snapshot Repository" -> ApiType.SnapshotRepo; + case "Snapshot Lifecycle Policy" -> ApiType.SnapshotLifecyclePolicy; + default -> ApiType.valueOf(stringifiedApiType); + }; + } + + private void waitForResource(ApiType apiType, String resourceName) { + switch (apiType) { + case Index -> ESClient.waitForResource(resourceName, ESClient::getIndexState); + case IndexTemplate -> ESClient.waitForResource(resourceName, ESClient::getTemplate); + case IndexLifecyclePolicy -> ESClient.waitForResource(resourceName, ESClient::getIlmPolicy); + case IngestPipeline -> ESClient.waitForResource(resourceName, ESClient::getIngestPipeline); + case SnapshotRepo -> ESClient.waitForResource(resourceName, ESClient::getSnapshotRepo); + case SnapshotLifecyclePolicy -> ESClient.waitForResource( + resourceName, ESClient::getSnapshotLifecyclePolicy); + case User -> ESClient.waitForResource(resourceName, ESClient::getUser); + case Role -> ESClient.waitForResource(resourceName, ESClient::getRole); + default -> throw new UnsupportedOperationException("Api type not supported"); + } + } + + private void apply(String fileName, String resource) { + withK8sClient() + .run( + client -> { + client + .load(new ByteArrayInputStream(resource.getBytes())) + .inNamespace("default") + .serverSideApply(); + toCleanup.add(fileName); + return null; + }); + } + + private String getModifiedResource(String fileName, String replaceKey, String replaceValue) { + Yaml yaml = new Yaml(); + Map input = + yaml.load(K8sResource.class.getResourceAsStream(getResourcePath(fileName))); + ((Map) input.get("spec")) + .put( + "body", + ((String) ((Map) input.get("spec")).get("body")) + .replace("$" + replaceKey, replaceValue)); + + var modified = yaml.dump(input); + return modified; + } + + private String getResourcePath(String resourceName) { + return "/resources/%s".formatted(resourceName); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/Index.java b/e2e/src/test/java/sk/xco/eckcr/step/es/Index.java new file mode 100644 index 0000000..cc13cfa --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/Index.java @@ -0,0 +1,37 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getIndexState; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.Then; +import lombok.extern.slf4j.Slf4j; +import sk.xco.eckcr.util.Await; +import sk.xco.eckcr.util.ESClient; + +@Slf4j +public class Index { + + @Then( + "the Index with name {string} with {int} replica shards is present in {string} Elasticsearch") + public void indexWithNameAndReplicasPresent( + String indexName, int replicas, String elasticsearchName) { + Await.untilAsserted( + () -> { + try { + var indexState = getIndexState(indexName); + assertThat(indexState.settings()).isNotNull(); + assertThat(Integer.valueOf(indexState.settings().index().numberOfReplicas())) + .isEqualTo(replicas); + } catch (ElasticsearchException e) { + fail("Failed to get Index", e); + } + }); + } + + @Then("the Index with name {string} is not present in {string} Elasticsearch") + public void indexNotPresent(String indexName, String elasticsearchName) { + ESClient.awaitResourceNotPresent(indexName, ESClient::getIndexState); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/IndexLifecyclePolicy.java b/e2e/src/test/java/sk/xco/eckcr/step/es/IndexLifecyclePolicy.java new file mode 100644 index 0000000..e298f4e --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/IndexLifecyclePolicy.java @@ -0,0 +1,50 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getIlmPolicy; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Then; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import sk.xco.eckcr.util.ESClient; + +public class IndexLifecyclePolicy { + + @Then("the Index Lifecycle Policy with name {string} is present in {string} Elasticsearch") + public void ilmPresent(String policyName, String esName) { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try { + var ilmPolicy = getIlmPolicy(policyName); + assertThat(ilmPolicy).isNotNull(); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @And("the Index Lifecycle Policy with name {string} got delete min age set to {string}") + public void ilmGotDeleteSet(String policyName, String minAge) { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try { + var ilmPolicy = getIlmPolicy(policyName); + assertThat(ilmPolicy.phases().delete().minAge().time()).isEqualTo(minAge); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @Then("the Index Lifecycle Policy with name {string} is not present in {string} Elasticsearch") + public void ilmNotPresent(String policyName, String esName) { + ESClient.awaitResourceNotPresent(policyName, ESClient::getIlmPolicy); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/IndexTemplate.java b/e2e/src/test/java/sk/xco/eckcr/step/es/IndexTemplate.java new file mode 100644 index 0000000..d5b5322 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/IndexTemplate.java @@ -0,0 +1,36 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getTemplate; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.Then; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; +import sk.xco.eckcr.util.ESClient; + +@Slf4j +public class IndexTemplate { + + @Then("the Index Template with name {string} is present in {string} Elasticsearch") + public void indexTemplateWithNamePresent(String templateName, String elasticsearchName) { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try { + var template = getTemplate(templateName); + assertThat(template).isNotNull(); + } catch (ElasticsearchException e) { + fail("Failed to get Index Template", e); + } + }); + } + + @Then("the Index Template with name {string} is not present in {string} Elasticsearch") + public void indexTemplateNotPresent(String templateName, String elasticsearchName) { + ESClient.awaitResourceNotPresent(templateName, ESClient::getTemplate); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/IngestPipeline.java b/e2e/src/test/java/sk/xco/eckcr/step/es/IngestPipeline.java new file mode 100644 index 0000000..7771873 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/IngestPipeline.java @@ -0,0 +1,52 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getIngestPipeline; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.Then; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import sk.xco.eckcr.util.ESClient; + +public class IngestPipeline { + + @Then("the Ingest Pipeline with name {string} is present in {string} Elasticsearch") + public void pipelinePresent(String resourceName, String esName) { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try { + var pipeline = getIngestPipeline(resourceName); + assertThat(pipeline).isNotNull(); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @Then( + "the Ingest Pipeline with name {string} and value {string} is present in {string} Elasticsearch") + public void pipelinePresentWithValue(String resourceName, String value, String esName) { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try { + var pipeline = getIngestPipeline(resourceName); + assertThat(pipeline).isNotNull(); + assertThat(pipeline.processors().get(0).set().value().toJson().toString()) + .isEqualTo(value); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @Then("the Ingest Pipeline with name {string} is not present in {string} Elasticsearch") + public void pipelineNotPresent(String resourceName, String esName) { + ESClient.awaitResourceNotPresent(resourceName, ESClient::getIngestPipeline); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/Role.java b/e2e/src/test/java/sk/xco/eckcr/step/es/Role.java new file mode 100644 index 0000000..4756c51 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/Role.java @@ -0,0 +1,32 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getRole; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.Then; +import sk.xco.eckcr.util.Await; +import sk.xco.eckcr.util.ESClient; + +public class Role { + @Then( + "the Role with name {string} is present in {string} Elasticsearch with {string} set to {string}") + public void rolePresent(String roleName, String esName, String attrKey, String attrValue) { + Await.untilAsserted( + () -> { + try { + var role = getRole(roleName); + assertThat(role).isNotNull(); + assertThat(role.indices().get(0).names()).contains(attrValue); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @Then("the Role with name {string} is not present in {string} Elasticsearch") + public void roleNotPresent(String roleName, String esName) { + ESClient.awaitResourceNotPresent(roleName, ESClient::getRole); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/SnapshotLifecyclePolicy.java b/e2e/src/test/java/sk/xco/eckcr/step/es/SnapshotLifecyclePolicy.java new file mode 100644 index 0000000..432b4a2 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/SnapshotLifecyclePolicy.java @@ -0,0 +1,36 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getSnapshotLifecyclePolicy; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.Then; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import sk.xco.eckcr.util.ESClient; + +public class SnapshotLifecyclePolicy { + @Then( + "the Snapshot Lifecycle Policy with name {string} is present in {string} Elasticsearch with {string} set to {string}") + public void policyPresent(String policyName, String esName, String attrKey, String attrValue) { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try { + var policy = getSnapshotLifecyclePolicy(policyName); + assertThat(policy).isNotNull(); + assertThat(policy.policy().config().ignoreUnavailable()) + .isEqualTo(Boolean.valueOf(attrValue)); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @Then("the Snapshot Lifecycle Policy with name {string} is not present in {string} Elasticsearch") + public void policyNotPresent(String policyName, String esName) { + ESClient.awaitResourceNotPresent(policyName, ESClient::getSnapshotLifecyclePolicy); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/SnapshotRepo.java b/e2e/src/test/java/sk/xco/eckcr/step/es/SnapshotRepo.java new file mode 100644 index 0000000..ed58dd8 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/SnapshotRepo.java @@ -0,0 +1,33 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getSnapshotRepo; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.Then; +import sk.xco.eckcr.util.Await; +import sk.xco.eckcr.util.ESClient; + +public class SnapshotRepo { + + @Then( + "the Snapshot Repository with name {string} is present in {string} Elasticsearch with {string} set to {string}") + public void repoPresent(String repoName, String esName, String attrKey, String attrValue) { + Await.untilAsserted( + () -> { + try { + var repo = getSnapshotRepo(repoName); + assertThat(repo).isNotNull(); + assertThat(repo.settings().compress()).isEqualTo(Boolean.valueOf(attrValue)); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @Then("the Snapshot Repository with name {string} is not present in {string} Elasticsearch") + public void repoNotPresent(String repoName, String esName) { + ESClient.awaitResourceNotPresent(repoName, ESClient::getSnapshotRepo); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/step/es/User.java b/e2e/src/test/java/sk/xco/eckcr/step/es/User.java new file mode 100644 index 0000000..74e6b9a --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/step/es/User.java @@ -0,0 +1,34 @@ +package sk.xco.eckcr.step.es; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static sk.xco.eckcr.util.ESClient.getUser; + +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import io.cucumber.java.en.Then; +import lombok.extern.slf4j.Slf4j; +import sk.xco.eckcr.util.Await; +import sk.xco.eckcr.util.ESClient; + +@Slf4j +public class User { + @Then( + "the User with name {string} is present in {string} Elasticsearch with {string} set to {string}") + public void userPresent(String userName, String esName, String attrKey, String attrValue) { + Await.untilAsserted( + () -> { + try { + var user = getUser(userName); + assertThat(user).isNotNull(); + assertThat(user.fullName()).isEqualTo(attrValue); + } catch (ElasticsearchException e) { + fail("Failed to get resource", e); + } + }); + } + + @Then("the User with name {string} is not present in {string} Elasticsearch") + public void userNotPresent(String userName, String esName) { + ESClient.awaitResourceNotPresent(userName, ESClient::getUser); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/util/ApiType.java b/e2e/src/test/java/sk/xco/eckcr/util/ApiType.java new file mode 100644 index 0000000..1bc0f98 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/util/ApiType.java @@ -0,0 +1,21 @@ +package sk.xco.eckcr.util; + +import lombok.Getter; + +@Getter +public enum ApiType { + Index("index.es.eck.github.com"), + IndexTemplate("indextemplate.es.eck.github.com"), + IndexLifecyclePolicy("indexlifecyclepolicy.es.eck.github.com"), + IngestPipeline("ingestpipeline.es.eck.github.com"), + SnapshotRepo("snapshotrepository.es.eck.github.com"), + SnapshotLifecyclePolicy("snapshotlifecyclepolicy.es.eck.github.com"), + User("elasticsearchuser.es.eck.github.com"), + Role("elasticsearchrole.es.eck.github.com"); + + private final String resourceType; + + ApiType(String resourceType) { + this.resourceType = resourceType; + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/util/Await.java b/e2e/src/test/java/sk/xco/eckcr/util/Await.java new file mode 100644 index 0000000..8680ed1 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/util/Await.java @@ -0,0 +1,11 @@ +package sk.xco.eckcr.util; + +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.awaitility.core.ThrowingRunnable; + +public class Await { + public static void untilAsserted(ThrowingRunnable runnable) { + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAsserted(runnable); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/util/ESClient.java b/e2e/src/test/java/sk/xco/eckcr/util/ESClient.java new file mode 100644 index 0000000..e896171 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/util/ESClient.java @@ -0,0 +1,191 @@ +package sk.xco.eckcr.util; + +import static java.util.Objects.nonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch.ilm.GetLifecycleRequest; +import co.elastic.clients.elasticsearch.ilm.IlmPolicy; +import co.elastic.clients.elasticsearch.indices.*; +import co.elastic.clients.elasticsearch.indices.get_index_template.IndexTemplateItem; +import co.elastic.clients.elasticsearch.ingest.GetPipelineRequest; +import co.elastic.clients.elasticsearch.ingest.Pipeline; +import co.elastic.clients.elasticsearch.security.GetRoleRequest; +import co.elastic.clients.elasticsearch.security.GetUserRequest; +import co.elastic.clients.elasticsearch.security.User; +import co.elastic.clients.elasticsearch.security.get_role.Role; +import co.elastic.clients.elasticsearch.slm.SnapshotLifecycle; +import co.elastic.clients.elasticsearch.snapshot.GetRepositoryRequest; +import co.elastic.clients.elasticsearch.snapshot.Repository; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.TransportUtils; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import javax.net.ssl.SSLContext; +import lombok.extern.slf4j.Slf4j; +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.awaitility.Awaitility; +import org.elasticsearch.client.RestClient; + +@Slf4j +public class ESClient { + + public static final String DEFAULT_ES_NAME = "quickstart"; + + public static IndexState getIndexState(String indexName) { + try { + return getClient() + .indices() + .get(new GetIndexRequest.Builder().index(indexName).build()) + .get(indexName); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static IndexTemplateItem getTemplate(String templateName) { + try { + return getClient() + .indices() + .getIndexTemplate(new GetIndexTemplateRequest.Builder().name(templateName).build()) + .indexTemplates() + .stream() + .findFirst() + .get(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static IlmPolicy getIlmPolicy(String policyName) { + try { + return getClient() + .ilm() + .getLifecycle(new GetLifecycleRequest.Builder().name(policyName).build()) + .get(policyName) + .policy(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Pipeline getIngestPipeline(String pipelineName) { + try { + return getClient() + .ingest() + .getPipeline(new GetPipelineRequest.Builder().id(pipelineName).build()) + .get(pipelineName); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Repository getSnapshotRepo(String repoName) { + try { + return getClient() + .snapshot() + .getRepository(new GetRepositoryRequest.Builder().name(repoName).build()) + .get(repoName); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static SnapshotLifecycle getSnapshotLifecyclePolicy(String policyName) { + try { + return getClient() + .slm() + .getLifecycle( + new co.elastic.clients.elasticsearch.slm.GetLifecycleRequest.Builder() + .policyId(policyName) + .build()) + .get(policyName); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static User getUser(String userName) { + try { + return getClient() + .security() + .getUser(new GetUserRequest.Builder().username(userName).build()) + .get(userName); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Role getRole(String roleName) { + try { + return getClient() + .security() + .getRole(new GetRoleRequest.Builder().name(roleName).build()) + .get(roleName); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static ElasticsearchClient getClient() { + var user = K8sClient.getElasticsearchUserFromSecret(DEFAULT_ES_NAME); + var caCrt = K8sClient.getElasticsearchCACertFromSecret(DEFAULT_ES_NAME); + + BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); + credsProv.setCredentials( + AuthScope.ANY, new UsernamePasswordCredentials(user.username(), user.password())); + + SSLContext sslContext = + TransportUtils.sslContextFromHttpCaCrt(new ByteArrayInputStream(caCrt.getBytes())); + + RestClient restClient = + RestClient.builder(new HttpHost("quickstart-es-http", 9200, "https")) + .setHttpClientConfigCallback( + hc -> hc.setSSLContext(sslContext).setDefaultCredentialsProvider(credsProv)) + .build(); + + ElasticsearchTransport transport = + new RestClientTransport(restClient, new JacksonJsonpMapper()); + + return new ElasticsearchClient(transport); + } + + public static void awaitResourceNotPresent( + String resourceName, Function getResourceFunction) { + Await.untilAsserted( + () -> { + try { + T resource = getResourceFunction.apply(resourceName); + if (nonNull(resource)) { + fail("Resource %s present in Elasticsearch: %s".formatted(resourceName, resource)); + } + } catch (ElasticsearchException e) { + assertThat(e.status()).isEqualTo(404); + } + }); + } + + public static void waitForResource( + String resourceName, Function getResourceFunction) { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> { + try { + getResourceFunction.apply(resourceName); + return true; + } catch (ElasticsearchException e) { + return false; + } + }); + } +} diff --git a/e2e/src/test/java/sk/xco/eckcr/util/ElasticsearchUser.java b/e2e/src/test/java/sk/xco/eckcr/util/ElasticsearchUser.java new file mode 100644 index 0000000..8c38e19 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/util/ElasticsearchUser.java @@ -0,0 +1,3 @@ +package sk.xco.eckcr.util; + +public record ElasticsearchUser(String username, String password) {} diff --git a/e2e/src/test/java/sk/xco/eckcr/util/K8sClient.java b/e2e/src/test/java/sk/xco/eckcr/util/K8sClient.java new file mode 100644 index 0000000..b9cc025 --- /dev/null +++ b/e2e/src/test/java/sk/xco/eckcr/util/K8sClient.java @@ -0,0 +1,93 @@ +package sk.xco.eckcr.util; + +import static org.junit.jupiter.api.Assertions.fail; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import java.util.Base64; +import java.util.Map; +import java.util.function.Function; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class K8sClient { + + public static final String NAMESPACE = "default"; + private static final K8sClient INSTANCE = new K8sClient(); + private static final String ES_USER_SECRET_PATTERN = "%s-es-elastic-user"; + private static final String ES_CERTS_SECRET_PATTERN = "%s-es-http-certs-internal"; + + public static K8sClient withK8sClient() { + return INSTANCE; + } + + public T run(Function fn) { + var builder = new KubernetesClientBuilder(); + try (KubernetesClient client = builder.build()) { + return fn.apply(client); + } catch (KubernetesClientException e) { + fail("Kubernetes client exception", e); + } catch (Exception e) { + fail("Exception occurred during k8s client operation", e); + } + throw new IllegalStateException("Failed to execute"); + } + + protected static ElasticsearchUser getElasticsearchUserFromSecret(String esName) { + return withK8sClient() + .run( + c -> + c.secrets().inNamespace(NAMESPACE).list().getItems().stream() + .filter( + s -> + ES_USER_SECRET_PATTERN + .formatted(esName) + .equals(s.getMetadata().getName())) + .findFirst() + .map(secret -> secret.getData().entrySet()) + .map( + data -> + data.stream() + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Secret does not contain data"))) + .map( + entry -> + new ElasticsearchUser( + entry.getKey(), + new String(Base64.getDecoder().decode(entry.getValue())))) + .orElseThrow( + () -> new IllegalArgumentException("No secret with ES user data found"))); + } + + protected static String getElasticsearchCACertFromSecret(String esName) { + return withK8sClient() + .run( + c -> + c.secrets().inNamespace(NAMESPACE).list().getItems().stream() + .filter( + s -> + ES_CERTS_SECRET_PATTERN + .formatted(esName) + .equals(s.getMetadata().getName())) + .findFirst() + .map(secret -> secret.getData().entrySet()) + .map( + data -> + data.stream() + .filter(e -> e.getKey().equals("ca.crt")) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Secret does not contain data"))) + .map(Map.Entry::getValue) + .map(encoded -> new String(Base64.getDecoder().decode(encoded))) + .orElseThrow( + () -> new IllegalArgumentException("No secret with ES user data found"))); + } +} diff --git a/e2e/src/test/resources/features/elasticsearch/index-lifecycle-policy.feature b/e2e/src/test/resources/features/elasticsearch/index-lifecycle-policy.feature new file mode 100644 index 0000000..9e47b2a --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/index-lifecycle-policy.feature @@ -0,0 +1,22 @@ +@Elasticsearch +Feature: Index lifecycle policy + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + + Scenario: Create an Index Lifecycle Policy + When the "index-lifecycle-policy.yaml" is applied with "minAge" set to "15d" + Then the Index Lifecycle Policy with name "test" is present in "quickstart" Elasticsearch + + Scenario: Update an Index Lifecycle Policy + Given the Index Lifecycle Policy "test" defined in "index-lifecycle-policy.yaml" is present with "minAge" set to "15d" + When the "index-lifecycle-policy.yaml" is applied with "minAge" set to "30d" + Then the Index Lifecycle Policy with name "test" is present in "quickstart" Elasticsearch + And the Index Lifecycle Policy with name "test" got delete min age set to "30d" + + Scenario: Delete an Index Lifecycle Policy + Given the Index Lifecycle Policy "test" defined in "index-lifecycle-policy.yaml" is present with "minAge" set to "15d" + When the resource defined in "index-lifecycle-policy.yaml" is deleted + Then the Index Lifecycle Policy with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/features/elasticsearch/index-template.feature b/e2e/src/test/resources/features/elasticsearch/index-template.feature new file mode 100644 index 0000000..4aaf7f3 --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/index-template.feature @@ -0,0 +1,24 @@ +@Elasticsearch +Feature: Index templates + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + + Scenario: Create an Index Template + When the "index-template.yaml" is applied with "replicas" set to "3" + And the "index-template_index.yaml" is applied + Then the Index Template with name "test" is present in "quickstart" Elasticsearch + Then the Index with name "index-tpl-1" with 3 replica shards is present in "quickstart" Elasticsearch + + Scenario: Update an Index Template + Given the Index Template "test" defined in "index-template.yaml" is present with "replicas" set to "0" + When the "index-template.yaml" is applied with "replicas" set to "2" + And the "index-template_index.yaml" is applied + Then the Index with name "index-tpl-1" with 2 replica shards is present in "quickstart" Elasticsearch + + Scenario: Delete an Index Template + Given the Index Template "test" defined in "index-template.yaml" is present with "replicas" set to "0" + When the resource defined in "index-template.yaml" is deleted + Then the Index Template with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/features/elasticsearch/index.feature b/e2e/src/test/resources/features/elasticsearch/index.feature new file mode 100644 index 0000000..6a81b92 --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/index.feature @@ -0,0 +1,21 @@ +@Elasticsearch +Feature: Indices + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + + Scenario: Create an Index + When the "index.yaml" is applied with "replicas" set to "0" + Then the Index with name "test" with 0 replica shards is present in "quickstart" Elasticsearch + + Scenario: Update an Index + Given the Index "test" defined in "index.yaml" is present with "replicas" set to "0" + When the "index.yaml" is applied with "replicas" set to "1" + Then the Index with name "test" with 1 replica shards is present in "quickstart" Elasticsearch + + Scenario: Delete an Index + Given the Index "test" defined in "index.yaml" is present with "replicas" set to "0" + When the resource defined in "index.yaml" is deleted + Then the Index with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/features/elasticsearch/ingest-pipeline.feature b/e2e/src/test/resources/features/elasticsearch/ingest-pipeline.feature new file mode 100644 index 0000000..eb9893b --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/ingest-pipeline.feature @@ -0,0 +1,21 @@ +@Elasticsearch +Feature: Ingest Pipelines + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + + Scenario: Create an Ingest Pipeline + When the "ingest-pipeline.yaml" is applied with "value" set to "true" + Then the Ingest Pipeline with name "test" is present in "quickstart" Elasticsearch + + Scenario: Update an Ingest Pipeline + Given the Ingest Pipeline "test" defined in "ingest-pipeline.yaml" is present with "value" set to "true" + When the "ingest-pipeline.yaml" is applied with "value" set to "false" + Then the Ingest Pipeline with name "test" and value "false" is present in "quickstart" Elasticsearch + + Scenario: Delete an Ingest Pipeline + Given the Ingest Pipeline "test" defined in "ingest-pipeline.yaml" is present with "value" set to "true" + When the resource defined in "ingest-pipeline.yaml" is deleted + Then the Ingest Pipeline with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/features/elasticsearch/role.feature b/e2e/src/test/resources/features/elasticsearch/role.feature new file mode 100644 index 0000000..2b741fd --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/role.feature @@ -0,0 +1,21 @@ +@Elasticsearch +Feature: User Roles + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + + Scenario: Create a Role + When the "role.yaml" is applied with "indexName" set to "index1" + Then the Role with name "test" is present in "quickstart" Elasticsearch with "indexName" set to "index1" + + Scenario: Update a Role + Given the Role "test" defined in "role.yaml" is present with "indexName" set to "index1" + When the "role.yaml" is applied with "indexName" set to "index2" + Then the Role with name "test" is present in "quickstart" Elasticsearch with "indexName" set to "index2" + + Scenario: Delete a Role + Given the Role "test" defined in "role.yaml" is present with "indexName" set to "index1" + When the resource defined in "role.yaml" is deleted + Then the Role with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/features/elasticsearch/snapshot-lifecycle-policy.feature b/e2e/src/test/resources/features/elasticsearch/snapshot-lifecycle-policy.feature new file mode 100644 index 0000000..c3e374d --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/snapshot-lifecycle-policy.feature @@ -0,0 +1,22 @@ +@Elasticsearch +Feature: Snapshot Lifecycle Policies + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + And the Snapshot Repository "test" defined in "snapshot-repo.yaml" is present with "compress" set to "false" + + Scenario: Create a Snapshot Lifecycle Policy + When the "snapshot-lifecycle-policy.yaml" is applied with "ignoreUnavailable" set to "false" + Then the Snapshot Lifecycle Policy with name "test" is present in "quickstart" Elasticsearch with "ignoreUnavailable" set to "false" + + Scenario: Update a Snapshot Lifecycle Policy + Given the Snapshot Lifecycle Policy "test" defined in "snapshot-lifecycle-policy.yaml" is present with "ignoreUnavailable" set to "false" + When the "snapshot-lifecycle-policy.yaml" is applied with "ignoreUnavailable" set to "true" + Then the Snapshot Lifecycle Policy with name "test" is present in "quickstart" Elasticsearch with "ignoreUnavailable" set to "true" + + Scenario: Delete a Snapshot Lifecycle Policy + Given the Snapshot Lifecycle Policy "test" defined in "snapshot-lifecycle-policy.yaml" is present with "ignoreUnavailable" set to "false" + When the resource defined in "snapshot-lifecycle-policy.yaml" is deleted + Then the Snapshot Lifecycle Policy with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/features/elasticsearch/snapshot-repository.feature b/e2e/src/test/resources/features/elasticsearch/snapshot-repository.feature new file mode 100644 index 0000000..7d41df8 --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/snapshot-repository.feature @@ -0,0 +1,21 @@ +@Elasticsearch +Feature: Snapshot repositories + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + + Scenario: Create a Snapshot repository + When the "snapshot-repo.yaml" is applied with "compress" set to "false" + Then the Snapshot Repository with name "test" is present in "quickstart" Elasticsearch with "compress" set to "false" + + Scenario: Update a Snapshot repository + Given the Snapshot Repository "test" defined in "snapshot-repo.yaml" is present with "compress" set to "false" + When the "snapshot-repo.yaml" is applied with "compress" set to "true" + Then the Snapshot Repository with name "test" is present in "quickstart" Elasticsearch with "compress" set to "true" + + Scenario: Delete a Snapshot repository + Given the Snapshot Repository "test" defined in "snapshot-repo.yaml" is present with "compress" set to "false" + When the resource defined in "snapshot-repo.yaml" is deleted + Then the Snapshot Repository with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/features/elasticsearch/user.feature b/e2e/src/test/resources/features/elasticsearch/user.feature new file mode 100644 index 0000000..0f8038b --- /dev/null +++ b/e2e/src/test/resources/features/elasticsearch/user.feature @@ -0,0 +1,22 @@ +@Elasticsearch +Feature: Users + + Background: + Given Kubernetes cluster is available + And ECK-CR operator is installed + And Elasticsearch "quickstart" is available + And the "user-secret.yaml" is applied + + Scenario: Create an User + When the "user.yaml" is applied with "fullName" set to "John Doe" + Then the User with name "test" is present in "quickstart" Elasticsearch with "fullName" set to "John Doe" + + Scenario: Update an User + Given the User "test" defined in "user.yaml" is present with "fullName" set to "John Doe" + When the "user.yaml" is applied with "fullName" set to "Jane Doe" + Then the User with name "test" is present in "quickstart" Elasticsearch with "fullName" set to "Jane Doe" + + Scenario: Delete an User + Given the User "test" defined in "user.yaml" is present with "fullName" set to "John Doe" + When the resource defined in "user.yaml" is deleted + Then the User with name "test" is not present in "quickstart" Elasticsearch diff --git a/e2e/src/test/resources/resources/index-lifecycle-policy.yaml b/e2e/src/test/resources/resources/index-lifecycle-policy.yaml new file mode 100644 index 0000000..153c273 --- /dev/null +++ b/e2e/src/test/resources/resources/index-lifecycle-policy.yaml @@ -0,0 +1,34 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: IndexLifecyclePolicy +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "policy": { + "_meta": { + "description": "used as example for lifecycle policy", + "project": { + "name": "eck-custom-resources" + } + }, + "phases": { + "warm": { + "min_age": "$minAge", + "actions": { + "forcemerge": { + "max_num_segments": 1 + } + } + }, + "delete": { + "min_age": "30d", + "actions": { + "delete": {} + } + } + } + } + } diff --git a/e2e/src/test/resources/resources/index-template.yaml b/e2e/src/test/resources/resources/index-template.yaml new file mode 100644 index 0000000..1e16298 --- /dev/null +++ b/e2e/src/test/resources/resources/index-template.yaml @@ -0,0 +1,18 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: IndexTemplate +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "index_patterns" : ["index-tpl-*"], + "priority" : 1, + "template": { + "settings" : { + "number_of_shards" : 1, + "number_of_replicas" : $replicas + } + } + } diff --git a/e2e/src/test/resources/resources/index-template_index.yaml b/e2e/src/test/resources/resources/index-template_index.yaml new file mode 100644 index 0000000..4355fd0 --- /dev/null +++ b/e2e/src/test/resources/resources/index-template_index.yaml @@ -0,0 +1,21 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: Index +metadata: + name: index-tpl-1 +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "settings": { + "number_of_shards": 1 + }, + "mappings": { + "properties": { + "field1": { "type": "text" } + } + }, + "aliases": { + "index-sample-alias": {} + } + } diff --git a/e2e/src/test/resources/resources/index.yaml b/e2e/src/test/resources/resources/index.yaml new file mode 100644 index 0000000..b71307f --- /dev/null +++ b/e2e/src/test/resources/resources/index.yaml @@ -0,0 +1,22 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: Index +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "settings": { + "number_of_shards": 1, + "number_of_replicas": $replicas + }, + "mappings": { + "properties": { + "field1": { "type": "text" } + } + }, + "aliases": { + "index-sample-alias": {} + } + } diff --git a/e2e/src/test/resources/resources/ingest-pipeline.yaml b/e2e/src/test/resources/resources/ingest-pipeline.yaml new file mode 100644 index 0000000..b4cfbc3 --- /dev/null +++ b/e2e/src/test/resources/resources/ingest-pipeline.yaml @@ -0,0 +1,20 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: IngestPipeline +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "description" : "Ingest pipeline sample", + "processors" : [ + { + "set" : { + "description" : "Ingest pipeline sample processor", + "field": "eck-custom-resources", + "value": $value + } + } + ] + } diff --git a/e2e/src/test/resources/resources/role.yaml b/e2e/src/test/resources/resources/role.yaml new file mode 100644 index 0000000..2a16062 --- /dev/null +++ b/e2e/src/test/resources/resources/role.yaml @@ -0,0 +1,20 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: ElasticsearchRole +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "cluster": ["all"], + "indices": [ + { + "names": [ "$indexName"], + "privileges": ["all"] + } + ], + "metadata" : { + "version" : 1 + } + } \ No newline at end of file diff --git a/e2e/src/test/resources/resources/snapshot-lifecycle-policy.yaml b/e2e/src/test/resources/resources/snapshot-lifecycle-policy.yaml new file mode 100644 index 0000000..64d1d6a --- /dev/null +++ b/e2e/src/test/resources/resources/snapshot-lifecycle-policy.yaml @@ -0,0 +1,23 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: SnapshotLifecyclePolicy +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "schedule": "0 30 1 * * ?", + "name": "", + "repository": "test", + "config": { + "indices": ["*"], + "ignore_unavailable": $ignoreUnavailable, + "include_global_state": true + }, + "retention": { + "expire_after": "30d", + "min_count": 5, + "max_count": 50 + } + } \ No newline at end of file diff --git a/e2e/src/test/resources/resources/snapshot-repo.yaml b/e2e/src/test/resources/resources/snapshot-repo.yaml new file mode 100644 index 0000000..d372ab7 --- /dev/null +++ b/e2e/src/test/resources/resources/snapshot-repo.yaml @@ -0,0 +1,15 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: SnapshotRepository +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + body: | + { + "type": "fs", + "settings": { + "location": "/tmp/", + "compress": $compress + } + } \ No newline at end of file diff --git a/e2e/src/test/resources/resources/user-secret.yaml b/e2e/src/test/resources/resources/user-secret.yaml new file mode 100644 index 0000000..ea97098 --- /dev/null +++ b/e2e/src/test/resources/resources/user-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test-elasticsearchuser-secret +type: Opaque +data: + test: c2FtcGxlLnBhc3N3b3Jk diff --git a/e2e/src/test/resources/resources/user.yaml b/e2e/src/test/resources/resources/user.yaml new file mode 100644 index 0000000..2478d0f --- /dev/null +++ b/e2e/src/test/resources/resources/user.yaml @@ -0,0 +1,18 @@ +apiVersion: es.eck.github.com/v1alpha1 +kind: ElasticsearchUser +metadata: + name: test +spec: + targetInstance: + name: elasticsearch-quickstart + secretName: test-elasticsearchuser-secret + body: | + { + "roles" : [ "admin" ], + "full_name" : "$fullName", + "email" : "rfeynman@example.com", + "metadata" : { + "intelligence" : 7 + } + } +