diff --git a/theodolite/build.gradle b/theodolite/build.gradle index 5b98e2ace9e9fd040642c566800de35e67f8f4ef..e9f47df879c09b746cbed2e816e711299902cdfd 100644 --- a/theodolite/build.gradle +++ b/theodolite/build.gradle @@ -35,9 +35,11 @@ dependencies { // compile 'junit:junit:4.12' testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.quarkus:quarkus-test-kubernetes-client' testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.junit-pioneer:junit-pioneer:1.5.0' - testImplementation 'io.fabric8:kubernetes-server-mock:5.10.1' + //testImplementation 'io.fabric8:kubernetes-server-mock:5.10.1' + testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" } group 'theodolite' diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt index 7b17f508465522bee82e538bc25e9165eb6cb65f..8cd469394ac8f2b67d73a0b3d2565cd0f37d7318 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt @@ -12,30 +12,30 @@ import mu.KotlinLogging import java.lang.Thread.sleep private val logger = KotlinLogging.logger {} -abstract class AbstractStateHandler<T, L, D>( +private const val MAX_RETRIES: Int = 5 + +abstract class AbstractStateHandler<T : HasMetadata>( private val client: NamespacedKubernetesClient, - private val crd: Class<T>, - private val crdList: Class<L> -) : StateHandler<T> where T : CustomResource<*, *>?, T : HasMetadata, T : Namespaced, L : KubernetesResourceList<T> { + private val crd: Class<T> +) { - private val crdClient: MixedOperation<T, L, Resource<T>> = - this.client.customResources(this.crd, this.crdList) + private val crdClient: MixedOperation<T, KubernetesResourceList<T>, Resource<T>> = this.client.resources(this.crd) @Synchronized - override fun setState(resourceName: String, f: (T) -> T?) { + fun setState(resourceName: String, f: (T) -> T?) { try { - this.crdClient - .list().items - .filter { it.metadata.name == resourceName } - .map { customResource -> f(customResource) } - .forEach { this.crdClient.updateStatus(it) } + val resource = this.crdClient.withName(resourceName).get() + if (resource != null) { + val resourcePatched = f(resource) + this.crdClient.patchStatus(resourcePatched) + } } catch (e: KubernetesClientException) { - logger.warn { "Status cannot be set for resource $resourceName" } + logger.warn(e) { "Status cannot be set for resource $resourceName." } } } @Synchronized - override fun getState(resourceName: String, f: (T) -> String?): String? { + fun getState(resourceName: String, f: (T) -> String?): String? { return this.crdClient .list().items .filter { it.metadata.name == resourceName } @@ -44,11 +44,11 @@ abstract class AbstractStateHandler<T, L, D>( } @Synchronized - override fun blockUntilStateIsSet( + fun blockUntilStateIsSet( resourceName: String, desiredStatusString: String, f: (T) -> String?, - maxRetries: Int + maxRetries: Int = MAX_RETRIES ): Boolean { for (i in 0.rangeTo(maxRetries)) { val currentStatus = getState(resourceName, f) diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt index adca2a8b7fdb9b3e610f15e57c011679869df14c..80cee0a3a30c0734ff2e12ef0d0291015d157f9c 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt @@ -4,10 +4,9 @@ import io.fabric8.kubernetes.client.NamespacedKubernetesClient import theodolite.model.crd.* class BenchmarkStateHandler(val client: NamespacedKubernetesClient) : - AbstractStateHandler<BenchmarkCRD, KubernetesBenchmarkList, ExecutionStatus>( + AbstractStateHandler<BenchmarkCRD>( client = client, - crd = BenchmarkCRD::class.java, - crdList = KubernetesBenchmarkList::class.java + crd = BenchmarkCRD::class.java ) { private fun getBenchmarkResourceState() = { cr: BenchmarkCRD -> cr.status.resourceSetsState } diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt index 16c4ea98ba614bb3dcdd7d9f486f4e65ae70d380..86276af35dd13457cb6c971144153612705dc420 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt @@ -17,20 +17,21 @@ private val logger = KotlinLogging.logger {} * @see TheodoliteController * @see BenchmarkExecution */ -class ExecutionHandler( +class ExecutionEventHandler( private val controller: TheodoliteController, private val stateHandler: ExecutionStateHandler ) : ResourceEventHandler<ExecutionCRD> { + private val gson: Gson = GsonBuilder().enableComplexMapKeySerialization().create() /** - * Add an execution to the end of the queue of the TheodoliteController. + * Adds an execution to the end of the queue of the TheodoliteController. * - * @param ExecutionCRD the execution to add + * @param execution the execution to add */ @Synchronized override fun onAdd(execution: ExecutionCRD) { - logger.info { "Add execution ${execution.metadata.name}" } + logger.info { "Add execution ${execution.metadata.name}." } execution.spec.name = execution.metadata.name when (this.stateHandler.getExecutionState(execution.metadata.name)) { ExecutionStates.NO_STATE -> this.stateHandler.setExecutionState(execution.spec.name, ExecutionStates.PENDING) @@ -44,19 +45,19 @@ class ExecutionHandler( } /** - * Updates an execution. If this execution is running at the time this function is called, it is stopped and + * To be called on update of an execution. If this execution is running at the time this function is called, it is stopped and * added to the beginning of the queue of the TheodoliteController. * Otherwise, it is just added to the beginning of the queue. * - * @param oldExecutionCRD the old execution - * @param newExecutionCRD the new execution + * @param oldExecution the old execution + * @param newExecution the new execution */ @Synchronized override fun onUpdate(oldExecution: ExecutionCRD, newExecution: ExecutionCRD) { newExecution.spec.name = newExecution.metadata.name oldExecution.spec.name = oldExecution.metadata.name if (gson.toJson(oldExecution.spec) != gson.toJson(newExecution.spec)) { - logger.info { "Receive update event for execution ${oldExecution.metadata.name}" } + logger.info { "Receive update event for execution ${oldExecution.metadata.name}." } when (this.stateHandler.getExecutionState(newExecution.metadata.name)) { ExecutionStates.RUNNING -> { this.stateHandler.setExecutionState(newExecution.spec.name, ExecutionStates.RESTART) @@ -74,11 +75,11 @@ class ExecutionHandler( /** * Delete an execution from the queue of the TheodoliteController. * - * @param ExecutionCRD the execution to delete + * @param execution the execution to delete */ @Synchronized - override fun onDelete(execution: ExecutionCRD, b: Boolean) { - logger.info { "Delete execution ${execution.metadata.name}" } + override fun onDelete(execution: ExecutionCRD, deletedFinalStateUnknown: Boolean) { + logger.info { "Delete execution ${execution.metadata.name}." } if (execution.status.executionState == ExecutionStates.RUNNING.value && this.controller.isExecutionRunning(execution.metadata.name) ) { diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt index 9f49cf3ee4f9f62e7006dbf6697340e1af152f27..a412805621fc7868d1efc215cdf4ff81b52d914e 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt @@ -11,10 +11,9 @@ import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean class ExecutionStateHandler(val client: NamespacedKubernetesClient) : - AbstractStateHandler<ExecutionCRD, BenchmarkExecutionList, ExecutionStatus>( + AbstractStateHandler<ExecutionCRD>( client = client, - crd = ExecutionCRD::class.java, - crdList = BenchmarkExecutionList::class.java + crd = ExecutionCRD::class.java ) { private var runExecutionDurationTimer: AtomicBoolean = AtomicBoolean(false) @@ -24,7 +23,7 @@ class ExecutionStateHandler(val client: NamespacedKubernetesClient) : private fun getDurationLambda() = { cr: ExecutionCRD -> cr.status.executionDuration } fun setExecutionState(resourceName: String, status: ExecutionStates): Boolean { - setState(resourceName) { cr -> cr.status.executionState = status.value; cr } + super.setState(resourceName) { cr -> cr.status.executionState = status.value; cr } return blockUntilStateIsSet(resourceName, status.value, getExecutionLambda()) } @@ -44,11 +43,7 @@ class ExecutionStateHandler(val client: NamespacedKubernetesClient) : fun getDurationState(resourceName: String): String { val status = getState(resourceName, getDurationLambda()) - return if (status.isNullOrBlank()) { - "-" - } else { - status - } + return if (status.isNullOrBlank()) "-" else status } private fun durationToK8sString(duration: Duration): String { diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt index ba8b22f02db9fb4a741dc8892eafeb5fa090da71..28563ac5a640d0226224b812a8e0691cde83942a 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt @@ -2,6 +2,7 @@ package theodolite.execution.operator private const val MAX_RETRIES: Int = 5 +@Deprecated("should not be needed") interface StateHandler<T> { fun setState(resourceName: String, f: (T) -> T?) diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt index 70e30cf84ef40796eb085a0d68eb2e323232fde9..5c2665f30235eb85aab8f7f3551c3d7a70517b81 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt @@ -22,7 +22,6 @@ const val CREATED_BY_LABEL_VALUE = "theodolite" * * @see BenchmarkExecution * @see KubernetesBenchmark - * @see ConcurrentLinkedDeque */ class TheodoliteController( @@ -34,7 +33,6 @@ class TheodoliteController( lateinit var executor: TheodoliteExecutor /** - * * Runs the TheodoliteController forever. */ fun run() { diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt index 26219f62197c2c9cf4897f26991c8047cc2530b8..11e1763292abb52796527013e6d863c24bb7d486 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt @@ -90,7 +90,7 @@ class TheodoliteOperator { ExecutionCRD::class.java, RESYNC_PERIOD ).addEventHandler( - ExecutionHandler( + ExecutionEventHandler( controller = controller, stateHandler = ExecutionStateHandler(client) ) diff --git a/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt b/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt index c7197ce07beb5540f30b728f17d40de29cab8eb0..518b8eae211dd064e3c12b0713382bf3b12bb1ba 100644 --- a/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt @@ -96,10 +96,9 @@ class ResourceByLabelHandler(private val client: NamespacedKubernetesClient) { /** * Block until all pods with are deleted * - * @param [labelName] the label name - * @param [labelValue] the value of this label + * @param matchLabels Map of label keys to label values to be deleted * */ - fun blockUntilPodsDeleted(matchLabels: MutableMap<String, String>) { + fun blockUntilPodsDeleted(matchLabels: Map<String, String>) { while ( !this.client .pods() diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt index b6468fff523e57b124e144d5b9fef6477973655a..2e9ffafb83734b3daceb3e9e523e900b887d785a 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt @@ -7,13 +7,21 @@ import io.fabric8.kubernetes.client.CustomResource import io.fabric8.kubernetes.model.annotation.Group import io.fabric8.kubernetes.model.annotation.Kind import io.fabric8.kubernetes.model.annotation.Version +import theodolite.benchmark.BenchmarkExecution import theodolite.benchmark.KubernetesBenchmark @JsonDeserialize @Version("v1") @Group("theodolite.com") @Kind("benchmark") -class BenchmarkCRD( - var spec: KubernetesBenchmark = KubernetesBenchmark(), - var status: BenchmarkStatus = BenchmarkStatus() -) : CustomResource<KubernetesBenchmark, BenchmarkStatus>(), Namespaced, HasMetadata \ No newline at end of file +class BenchmarkCRD : CustomResource<KubernetesBenchmark, BenchmarkStatus>(), Namespaced, HasMetadata { + + override fun initSpec(): KubernetesBenchmark { + return KubernetesBenchmark() + } + + override fun initStatus(): BenchmarkStatus { + return BenchmarkStatus() + } + +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt index 659621e8c3b1d5308a10d81240575dd3d432b53f..3be0aaf2a30cd4ef279edd34854eb936cc6e7e7c 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt @@ -12,7 +12,14 @@ import theodolite.benchmark.BenchmarkExecution @Version("v1") @Group("theodolite.com") @Kind("execution") -class ExecutionCRD( - var spec: BenchmarkExecution = BenchmarkExecution(), - var status: ExecutionStatus = ExecutionStatus() -) : CustomResource<BenchmarkExecution, ExecutionStatus>(), Namespaced +class ExecutionCRD: CustomResource<BenchmarkExecution, ExecutionStatus>(), Namespaced { + + override fun initSpec(): BenchmarkExecution { + return BenchmarkExecution() + } + + override fun initStatus(): ExecutionStatus { + return ExecutionStatus() + } + +} diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStates.kt b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStates.kt index ad68bf380b18af1a654c201817bb7fc982804c8b..368fc39a3cba1bc702f1f0831c96637a61548358 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStates.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStates.kt @@ -1,7 +1,6 @@ package theodolite.model.crd enum class ExecutionStates(val value: String) { - // Execution states RUNNING("Running"), PENDING("Pending"), FAILURE("Failure"), diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt index e294ea539ea60104cc00e9f73de790302ad52670..9b11b31404d2d858db8c75f5880bd47441555814 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt @@ -8,7 +8,7 @@ import theodolite.util.KafkaConfig class BenchmarkCRDummy(name: String) { private val benchmark = KubernetesBenchmark() - private val benchmarkCR = BenchmarkCRD(benchmark) + private val benchmarkCR = BenchmarkCRD() fun getCR(): BenchmarkCRD { return benchmarkCR diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt index 51347d41b396bf375c14d5580b0f2619ce5b518c..e8010f345140f41dc2edfbe387316f6d21511915 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt @@ -9,7 +9,7 @@ class ExecutionCRDummy(name: String, benchmark: String) { private val execution = BenchmarkExecution() private val executionState = ExecutionStatus() - private val executionCR = ExecutionCRD(execution, executionState) + private val executionCR = ExecutionCRD() fun getCR(): ExecutionCRD { return this.executionCR @@ -25,6 +25,7 @@ class ExecutionCRDummy(name: String, benchmark: String) { executionCR.metadata.name = name executionCR.kind = "Execution" executionCR.apiVersion = "v1" + executionCR.status = executionState // configure execution val loadType = BenchmarkExecution.LoadDefinition() diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt index 15af0d28a3171c29f832ea44222ea01b69dcd07d..ec14d7d8fefb384efc53d79f3b9772c4ccc1e270 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt @@ -1,227 +1,266 @@ package theodolite.execution.operator -import io.fabric8.kubernetes.api.model.KubernetesResource -import io.fabric8.kubernetes.client.informers.SharedInformerFactory +import io.fabric8.kubernetes.api.model.KubernetesResourceList +import io.fabric8.kubernetes.client.dsl.MixedOperation +import io.fabric8.kubernetes.client.dsl.Resource import io.fabric8.kubernetes.client.server.mock.KubernetesServer import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.kubernetes.client.KubernetesTestServer +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import theodolite.k8s.K8sManager -import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.* +import theodolite.model.crd.ExecutionCRD import theodolite.model.crd.ExecutionStates +import theodolite.model.crd.ExecutionStatus +import java.io.FileInputStream import java.lang.Thread.sleep +import java.util.stream.Stream +// TODO move somewhere else +typealias ExecutionClient = MixedOperation<ExecutionCRD, KubernetesResourceList<ExecutionCRD>, Resource<ExecutionCRD>> -private const val SLEEP_AFTER_REQUEST_MS = 500L - - +@WithKubernetesTestServer @QuarkusTest class ExecutionEventHandlerTest { - private final val server = KubernetesServer(false, true) - private val testResourcePath = "./src/test/resources/k8s-resource-files/" - private final val executionName = "example-execution" - lateinit var factory: SharedInformerFactory - lateinit var executionVersion1: KubernetesResource - lateinit var executionVersion2: KubernetesResource - lateinit var stateHandler: ExecutionStateHandler - lateinit var manager: K8sManager + + @KubernetesTestServer + private lateinit var server: KubernetesServer + + lateinit var executionClient: ExecutionClient + lateinit var controller: TheodoliteController + lateinit var stateHandler: ExecutionStateHandler + + lateinit var eventHandler: ExecutionEventHandler + @BeforeEach fun setUp() { server.before() - val operator = TheodoliteOperator() - this.controller = operator.getController( - client = server.client, - executionStateHandler = ExecutionStateHandler(client = server.client), - benchmarkStateHandler = BenchmarkStateHandler(client = server.client) - ) - - this.factory = operator.getExecutionEventHandler(this.controller, server.client) - this.stateHandler = TheodoliteOperator().getExecutionStateHandler(client = server.client) - - this.executionVersion1 = K8sResourceLoaderFromFile(server.client) - .loadK8sResource("Execution", testResourcePath + "test-execution.yaml") - this.executionVersion2 = K8sResourceLoaderFromFile(server.client) - .loadK8sResource("Execution", testResourcePath + "test-execution-update.yaml") + this.server.client + .apiextensions().v1() + .customResourceDefinitions() + .load(FileInputStream("crd/crd-execution.yaml")) + .create() - this.stateHandler = operator.getExecutionStateHandler(server.client) + this.executionClient = this.server.client.resources(ExecutionCRD::class.java) - this.manager = K8sManager((server.client)) + this.controller = mock() + this.stateHandler = ExecutionStateHandler(server.client) + this.eventHandler = ExecutionEventHandler(this.controller, this.stateHandler) } @AfterEach fun tearDown() { server.after() - factory.stopAllRegisteredInformers() } @Test - @DisplayName("Check namespaced property of informers") - fun testNamespaced() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - server.lastRequest - // the second request must be namespaced (this is the first `GET` request) - assert( - server - .lastRequest - .toString() - .contains("namespaces") - ) + fun testCrdRegistered() { + val crds = this.server.client.apiextensions().v1().customResourceDefinitions().list(); + assertEquals(1, crds.items.size) + assertEquals("execution", crds.items[0].spec.names.kind) } @Test - @DisplayName("Test onAdd method for executions without execution state") - fun testWithoutState() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(SLEEP_AFTER_REQUEST_MS) - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + fun testExecutionDeploy() { + getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + + val executions = executionClient.list().items + assertEquals(1, executions.size) } @Test - @DisplayName("Test onAdd method for executions with execution state `RUNNING`") - fun testWithStateIsRunning() { - manager.deploy(executionVersion1) - stateHandler - .setExecutionState( - resourceName = executionName, - status = ExecutionStates.RUNNING - ) - factory.startAllRegisteredInformers() - sleep(SLEEP_AFTER_REQUEST_MS) - assertEquals( - ExecutionStates.RESTART, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + fun testStatusSet() { + val execCreated = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + assertNotNull(execCreated.status) + val execResponse = this.executionClient.withName(execCreated.metadata.name) + val execResponseItem = execResponse.get() + assertNotNull(execResponseItem.status) } @Test - @DisplayName("Test onUpdate method for execution with execution state `PENDING`") - fun testOnUpdatePending() { - manager.deploy(executionVersion1) - - factory.startAllRegisteredInformers() - sleep(SLEEP_AFTER_REQUEST_MS) + @DisplayName("Test onAdd method for executions without execution state") + fun testOnAddWithoutStatus() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val execution = executionResource.create() + val executionName = execution.metadata.name - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + // Get execution from server + val executionResponse = this.executionClient.withName(executionName).get() + this.eventHandler.onAdd(executionResponse) - manager.deploy(executionVersion2) - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + assertEquals(ExecutionStates.PENDING.value, this.executionClient.withName(executionName).get().status.executionState) } @Test - @DisplayName("Test onUpdate method for execution with execution state `FINISHED`") - fun testOnUpdateFinished() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(SLEEP_AFTER_REQUEST_MS) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.FINISHED - ) - - manager.deploy(executionVersion2) - sleep(SLEEP_AFTER_REQUEST_MS) - - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + @DisplayName("Test onAdd method for executions with execution state `RUNNING`") + fun testOnAddWithStatusRunning() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val execution = executionResource.create() + val executionName = execution.metadata.name + stateHandler.setExecutionState(executionName, ExecutionStates.RUNNING) + + // Update status of execution + execution.status.executionState = ExecutionStates.RUNNING.value + executionResource.patchStatus(execution) + + + // Get execution from server + val executionResponse = this.executionClient.withName(executionName).get() + // Assert that status at server matches set status + assertEquals(ExecutionStates.RUNNING.value, this.executionClient.withName(executionName).get().status.executionState) + + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + this.eventHandler.onAdd(executionResponse) + + verify(this.controller).stop(true) + assertEquals(ExecutionStates.RESTART.value, this.executionClient.withName(executionName).get().status.executionState) } @Test - @DisplayName("Test onUpdate method for execution with execution state `FAILURE`") - fun testOnUpdateFailure() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(SLEEP_AFTER_REQUEST_MS) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.FAILURE - ) - - manager.deploy(executionVersion2) - sleep(SLEEP_AFTER_REQUEST_MS) - - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + @DisplayName("Test onUpdate method for execution with no status") + fun testOnUpdateWithoutStatus() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution at server has no status + assertEquals("", firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + + this.eventHandler.onUpdate(firstExecutionResponse, secondExecutionResponse) + + // Get execution from server and assert that new status matches expected one + assertEquals(ExecutionStates.PENDING.value, this.executionClient.withName(executionName).get().status.executionState) } + @ParameterizedTest + @MethodSource("provideOnUpdateTestArguments") + @DisplayName("Test onUpdate method for execution with different status") + fun testOnUpdateWithStatus(beforeState: ExecutionStates, expectedState: ExecutionStates) { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution + firstExecution.status.executionState = beforeState.value + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that status at server matches set status + assertEquals(beforeState.value, firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + + this.eventHandler.onUpdate(firstExecutionResponse, secondExecutionResponse) + + // Get execution from server and assert that new status matches expected one + assertEquals(expectedState.value, this.executionClient.withName(executionName).get().status.executionState) + } @Test - @DisplayName("Test onUpdate method for execution with execution state `RUNNING`") - fun testOnUpdateRunning() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(SLEEP_AFTER_REQUEST_MS) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.RUNNING - ) - - manager.deploy(executionVersion2) - sleep(SLEEP_AFTER_REQUEST_MS) - - assertEquals( - ExecutionStates.RESTART, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + fun testOnDeleteWithExecutionRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionStates.RUNNING.value + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNull(secondExecutionResponse) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + this.eventHandler.onDelete(firstExecutionResponse, true) + + verify(this.controller).stop(false) } @Test - @DisplayName("Test onUpdate method for execution with execution state `RESTART`") - fun testOnUpdateRestart() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(SLEEP_AFTER_REQUEST_MS) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.RESTART - ) - - manager.deploy(executionVersion2) - sleep(SLEEP_AFTER_REQUEST_MS) - - assertEquals( - ExecutionStates.RESTART, - stateHandler.getExecutionState( - resourceName = executionName + fun testOnDeleteWithExecutionNotRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionStates.RUNNING.value + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNull(secondExecutionResponse) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(false) + + this.eventHandler.onDelete(firstExecutionResponse, true) + + verify(this.controller, never()).stop(false) + } + + private fun getExecutionFromSystemResource(resourceName: String): Resource<ExecutionCRD> { + return executionClient.load(ClassLoader.getSystemResourceAsStream(resourceName)) + } + + companion object { + @JvmStatic + fun provideOnUpdateTestArguments(): Stream<Arguments> = + Stream.of( + // before state -> expected state + Arguments.of(ExecutionStates.PENDING, ExecutionStates.PENDING), + Arguments.of(ExecutionStates.FINISHED, ExecutionStates.PENDING), + Arguments.of(ExecutionStates.FAILURE, ExecutionStates.PENDING), + Arguments.of(ExecutionStates.RUNNING, ExecutionStates.RESTART), + Arguments.of(ExecutionStates.RESTART, ExecutionStates.RESTART) ) - ) } + } \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTestWithInformer.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTestWithInformer.kt new file mode 100644 index 0000000000000000000000000000000000000000..b0904fdb7319a31a1845efa5c71282f03e490381 --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTestWithInformer.kt @@ -0,0 +1,282 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.dsl.Resource +import io.fabric8.kubernetes.client.server.mock.KubernetesServer +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.kubernetes.client.KubernetesTestServer +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.* +import theodolite.model.crd.ExecutionCRD +import theodolite.model.crd.ExecutionStates +import java.io.FileInputStream +import java.util.concurrent.CountDownLatch +import java.util.stream.Stream + +@WithKubernetesTestServer +@QuarkusTest +class ExecutionEventHandlerTestWithInformer { + + @KubernetesTestServer + private lateinit var server: KubernetesServer + + lateinit var executionClient: ExecutionClient + + lateinit var controller: TheodoliteController + + lateinit var stateHandler: ExecutionStateHandler + + lateinit var addCountDownLatch: CountDownLatch + lateinit var updateCountDownLatch: CountDownLatch + lateinit var deleteCountDownLatch: CountDownLatch + + lateinit var eventHandler: ExecutionEventHandlerWrapper + + @BeforeEach + fun setUp() { + server.before() + + this.server.client + .apiextensions().v1() + .customResourceDefinitions() + .load(FileInputStream("crd/crd-execution.yaml")) + .create() + + this.executionClient = this.server.client.resources(ExecutionCRD::class.java) + + this.controller = mock() + this.stateHandler = ExecutionStateHandler(server.client) + this.addCountDownLatch = CountDownLatch(1) + this.updateCountDownLatch = CountDownLatch(2) + this.deleteCountDownLatch = CountDownLatch(1) + this.eventHandler = ExecutionEventHandlerWrapper( + ExecutionEventHandler(this.controller, this.stateHandler), + { addCountDownLatch.countDown() }, + { updateCountDownLatch.countDown() }, + { deleteCountDownLatch.countDown() } + ) + } + + @AfterEach + fun tearDown() { + server.after() + this.server.client.informers().stopAllRegisteredInformers() + } + + @Test + fun testCrdRegistered() { + val crds = this.server.client.apiextensions().v1().customResourceDefinitions().list(); + assertEquals(1, crds.items.size) + assertEquals("execution", crds.items[0].spec.names.kind) + } + + @Test + fun testExecutionDeploy() { + getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + + val executions = executionClient.list().items + assertEquals(1, executions.size) + } + + @Test + fun testStatusSet() { + val execCreated = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + assertNotNull(execCreated.status) + val execResponse = this.executionClient.withName(execCreated.metadata.name) + val execResponseItem = execResponse.get() + assertNotNull(execResponseItem.status) + } + + @Test + @DisplayName("Test onAdd method for executions without execution state") + fun testOnAddWithoutStatus() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val execution = executionResource.create() + val executionName = execution.metadata.name + + // Start informer + this.executionClient.inform(eventHandler) + + // Await informer called + this.addCountDownLatch.await() + assertEquals(ExecutionStates.PENDING.value, this.executionClient.withName(executionName).get().status.executionState) + } + + @Test + @DisplayName("Test onAdd method for executions with execution state `RUNNING`") + fun testOnAddWithStatusRunning() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val executionName = executionResource.get().metadata.name + + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + // Start informer + this.executionClient.inform(eventHandler) + + val execution = executionResource.create() + + // Update status of execution + execution.status.executionState = ExecutionStates.RUNNING.value + executionResource.patchStatus(execution) + + // Assert that status at server matches set status + // assertEquals(ExecutionStates.RUNNING.value, this.executionClient.withName(executionName).get().status.executionState) + + // Await informer called + this.addCountDownLatch.await() + verify(this.controller).stop(true) + assertEquals(ExecutionStates.RESTART.value, this.executionClient.withName(executionName).get().status.executionState) + } + + @Test + @DisplayName("Test onUpdate method for execution with no status") + fun testOnUpdateWithoutStatus() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Start informer + this.executionClient.inform(eventHandler) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution at server has pending status + assertEquals(ExecutionStates.PENDING.value, firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + + // Await informer called + this.updateCountDownLatch.await() + // Get execution from server and assert that new status matches expected one + assertEquals(ExecutionStates.PENDING.value, this.executionClient.withName(executionName).get().status.executionState) + } + + @ParameterizedTest + @MethodSource("provideOnUpdateTestArguments") + @DisplayName("Test onUpdate method for execution with different status") + fun testOnUpdateWithStatus(beforeState: ExecutionStates, expectedState: ExecutionStates) { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution + firstExecution.status.executionState = beforeState.value + firstExecutionResource.patchStatus(firstExecution) + + // Start informer + this.executionClient.inform(eventHandler) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that status at server matches set status + assertEquals(beforeState.value, firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + + // Await informer called + this.updateCountDownLatch.await() + // Get execution from server and assert that new status matches expected one + assertEquals(expectedState.value, this.executionClient.withName(executionName).get().status.executionState) + } + + @Test + @Disabled("Informer also called onAdd and changes status") + fun testOnDeleteWithExecutionRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionStates.RUNNING.value + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Start informer + this.executionClient.inform(eventHandler) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution deleted at server + assertNull(secondExecutionResponse) + + // Await informer called + this.deleteCountDownLatch.await() + + verify(this.controller).stop(false) + } + + @Test + fun testOnDeleteWithExecutionNotRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionStates.RUNNING.value + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Start informer + this.executionClient.inform(eventHandler) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(false) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNull(secondExecutionResponse) + + // Await informer called + this.deleteCountDownLatch.await() + + verify(this.controller, never()).stop(false) + } + + private fun getExecutionFromSystemResource(resourceName: String): Resource<ExecutionCRD> { + return executionClient.load(ClassLoader.getSystemResourceAsStream(resourceName)) + } + + companion object { + @JvmStatic + fun provideOnUpdateTestArguments(): Stream<Arguments> = + Stream.of( + // before state -> expected state + Arguments.of(ExecutionStates.PENDING, ExecutionStates.PENDING), + Arguments.of(ExecutionStates.FINISHED, ExecutionStates.PENDING), + Arguments.of(ExecutionStates.FAILURE, ExecutionStates.PENDING), + // Arguments.of(ExecutionStates.RUNNING, ExecutionStates.RESTART), // see testOnDeleteWithExecutionRunning + Arguments.of(ExecutionStates.RESTART, ExecutionStates.RESTART) + ) + } + +} \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerWrapper.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerWrapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..5dbc515a7799dd51e6395153f13d80650587d7fa --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerWrapper.kt @@ -0,0 +1,27 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.informers.ResourceEventHandler +import theodolite.model.crd.ExecutionCRD + +class ExecutionEventHandlerWrapper( + private val executionEventHandler: ExecutionEventHandler, + private val afterOnAddCallback: () -> Unit, + private val afterOnUpdateCallback: () -> Unit, + private val afterOnDeleteCallback: () -> Unit +) : ResourceEventHandler<ExecutionCRD> { + + override fun onAdd(execution: ExecutionCRD) { + this.executionEventHandler.onAdd(execution) + this.afterOnAddCallback() + } + + override fun onUpdate(oldExecution: ExecutionCRD, newExecution: ExecutionCRD) { + this.executionEventHandler.onUpdate(oldExecution, newExecution) + this.afterOnUpdateCallback() + } + + override fun onDelete(execution: ExecutionCRD, deletedFinalStateUnknown: Boolean) { + this.executionEventHandler.onDelete(execution, deletedFinalStateUnknown) + this.afterOnDeleteCallback() + } +} \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt index a54f4ed6db559f8f7f15ae82deecf3fedf8b4abe..44b6fce2796222d33c7f49091e4b61c79da770b8 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt @@ -1,6 +1,9 @@ package theodolite.execution.operator import io.fabric8.kubernetes.client.server.mock.KubernetesServer +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.kubernetes.client.KubernetesTestServer +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -12,9 +15,13 @@ import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile import theodolite.model.crd.ExecutionStates import java.time.Duration +@WithKubernetesTestServer +@QuarkusTest class StateHandlerTest { private val testResourcePath = "./src/test/resources/k8s-resource-files/" - private val server = KubernetesServer(false, true) + + @KubernetesTestServer + private lateinit var server: KubernetesServer @BeforeEach fun setUp() { diff --git a/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt b/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt index 7332e53f9e1814f28b8ff37a595b31b0eb931ea7..5d50514857a7a206a64a0612c2270936fe01aa3b 100644 --- a/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt +++ b/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt @@ -19,7 +19,6 @@ class ExecutionStateComparatorTest { execution2.getStatus().executionState = ExecutionStates.PENDING.value val list = listOf(execution2.getCR(), execution1.getCR()) - assertEquals( list.reversed(), list.sortedWith(comparator) diff --git a/theodolite/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/theodolite/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000000000000000000000000000000000..1f0955d450f0dc49ca715b1a0a88a5aa746ee11e --- /dev/null +++ b/theodolite/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline