diff --git a/execution/helm/templates/theodolite/crd-benchmark.yaml b/execution/helm/templates/theodolite/crd-benchmark.yaml index 9d7468b490fa2f2a6cf829bdcafab8c4bd6fc5bf..848ecb37213f2810853a47fd45d3869198acd720 100644 --- a/execution/helm/templates/theodolite/crd-benchmark.yaml +++ b/execution/helm/templates/theodolite/crd-benchmark.yaml @@ -1,15 +1,121 @@ {{- if .Values.benchmarkCRD.create -}} -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: benchmarks.theodolite.com spec: group: theodolite.com - version: v1alpha1 names: kind: benchmark plural: benchmarks + shortNames: + - bench + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: [] + properties: + name: + type: string + appResource: + type: array + minItems: 1 + items: + type: string + loadGenResource: + type: array + minItems: 1 + items: + type: string + resourceTypes: + type: array + minItems: 1 + items: + type: object + properties: + typeName: + type: string + patchers: + type: array + minItems: 1 + items: + type: object + properties: + type: + type: string + default: "" + resource: + type: string + default: "" + container: + type: string + default: "" + variableName: + type: string + default: "" + loadTypes: + type: array + minItems: 1 + items: + type: object + properties: + typeName: + type: string + patchers: + type: array + minItems: 1 + items: + type: object + properties: + type: + type: string + default: "" + resource: + type: string + default: "" + container: + type: string + default: "" + variableName: + type: string + default: "" + kafkaConfig: + type: object + properties: + bootstrapServer: + type: string + topics: + type: array + minItems: 1 + items: + type: object + required: [] + properties: + name: + type: string + default: "" + numPartitions: + type: integer + default: 0 + replicationFactor: + type: integer + default: 0 + removeOnly: + type: boolean + default: false + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + subresources: + status: {} scope: Namespaced - subresources: - status: {} {{- end }} \ No newline at end of file diff --git a/execution/helm/templates/theodolite/crd-execution.yaml b/execution/helm/templates/theodolite/crd-execution.yaml index 73b58397b8c1fc15ffef5da74e8f1dbdabaa3a30..92835ee1d5a016d0fe6e2db874ae222d7f49f461 100644 --- a/execution/helm/templates/theodolite/crd-execution.yaml +++ b/execution/helm/templates/theodolite/crd-execution.yaml @@ -1,15 +1,132 @@ {{- if .Values.executionCRD.create -}} -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: executions.theodolite.com spec: group: theodolite.com - version: v1alpha1 names: kind: execution plural: executions + shortNames: + - exec + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: ["benchmark", "load", "resources", "slos", "execution", "configOverrides"] + properties: + name: + type: string + default: "" + benchmark: + type: string + load: # definition of the load dimension + type: object + required: ["loadType", "loadValues"] + properties: + loadType: + type: string + loadValues: + type: array + items: + type: integer + resources: # definition of the resource dimension + type: object + required: ["resourceType", "resourceValues"] + properties: + resourceType: + type: string + resourceValues: + type: array + items: + type: integer + slos: # def of service level objectives + type: array + items: + type: object + required: ["sloType", "threshold", "prometheusUrl", "externalSloUrl", "offset", "warmup"] + properties: + sloType: + type: string + threshold: + type: integer + prometheusUrl: + type: string + externalSloUrl: + type: string + offset: + type: integer + warmup: + type: integer + execution: # def execution config + type: object + required: ["strategy", "duration", "repetitions", "restrictions"] + properties: + strategy: + type: string + duration: + type: integer + repetitions: + type: integer + loadGenerationDelay: + type: integer + restrictions: + type: array + items: + type: string + configOverrides: + type: array + items: + type: object + properties: + patcher: + type: object + properties: + type: + type: string + default: "" + resource: + type: string + default: "" + container: + type: string + default: "" + variableName: + type: string + default: "" + value: + type: string + status: + type: object + properties: + executionState: + description: "" + type: string + executionDuration: + description: "Duration of the execution in seconds" + type: string + additionalPrinterColumns: + - name: STATUS + type: string + description: State of the execution + jsonPath: .status.executionState + - name: Duration + type: string + description: Duration of the execution + jsonPath: .status.executionDuration + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + subresources: + status: {} scope: Namespaced - subresources: - status: {} + status: {} {{- end }} \ No newline at end of file diff --git a/theodolite-quarkus/config/example-operator-benchmark.yaml b/theodolite-quarkus/config/example-operator-benchmark.yaml index 3ed5218d8a8988b130e8d549c120cbca7329ffe3..5cf39492a8d88dc0260db8bf98a966b4ce1bccb7 100644 --- a/theodolite-quarkus/config/example-operator-benchmark.yaml +++ b/theodolite-quarkus/config/example-operator-benchmark.yaml @@ -1,35 +1,35 @@ -apiVersion: theodolite.com/v1alpha1 +apiVersion: theodolite.com/v1 kind: benchmark metadata: name: uc1-kstreams -#name: "uc1-kstreams" -appResource: - - "uc1-kstreams-deployment.yaml" - - "aggregation-service.yaml" - - "jmx-configmap.yaml" - - "uc1-service-monitor.yaml" -loadGenResource: - - "uc1-load-generator-deployment.yaml" - - "uc1-load-generator-service.yaml" -resourceTypes: - - typeName: "Instances" - patchers: - - type: "ReplicaPatcher" - resource: "uc1-kstreams-deployment.yaml" -loadTypes: - - typeName: "NumSensors" - patchers: - - type: "EnvVarPatcher" - resource: "uc1-load-generator-deployment.yaml" - container: "workload-generator" - variableName: "NUM_SENSORS" - - type: "NumSensorsLoadGeneratorReplicaPatcher" - resource: "uc1-load-generator-deployment.yaml" -kafkaConfig: - bootstrapServer: "theodolite-cp-kafka:9092" - topics: - - name: "input" - numPartitions: 40 - replicationFactor: 1 - - name: "theodolite-.*" - removeOnly: True \ No newline at end of file +spec: + appResource: + - "uc1-kstreams-deployment.yaml" + - "aggregation-service.yaml" + - "jmx-configmap.yaml" + - "uc1-service-monitor.yaml" + loadGenResource: + - "uc1-load-generator-deployment.yaml" + - "uc1-load-generator-service.yaml" + resourceTypes: + - typeName: "Instances" + patchers: + - type: "ReplicaPatcher" + resource: "uc1-kstreams-deployment.yaml" + loadTypes: + - typeName: "NumSensors" + patchers: + - type: "EnvVarPatcher" + resource: "uc1-load-generator-deployment.yaml" + container: "workload-generator" + variableName: "NUM_SENSORS" + - type: "NumSensorsLoadGeneratorReplicaPatcher" + resource: "uc1-load-generator-deployment.yaml" + kafkaConfig: + bootstrapServer: "theodolite-cp-kafka:9092" + topics: + - name: "input" + numPartitions: 40 + replicationFactor: 1 + - name: "theodolite-.*" + removeOnly: True \ No newline at end of file diff --git a/theodolite-quarkus/config/example-operator-execution.yaml b/theodolite-quarkus/config/example-operator-execution.yaml index 882c38a97c882ac180a2416e0b5046fa6d467efd..e01ea377e0762a56132a709a73fb418e4c914e26 100644 --- a/theodolite-quarkus/config/example-operator-execution.yaml +++ b/theodolite-quarkus/config/example-operator-execution.yaml @@ -1,53 +1,53 @@ -apiVersion: theodolite.com/v1alpha1 +apiVersion: theodolite.com/v1 kind: execution metadata: name: example-execution -#name: example-execution -benchmark: "uc1-kstreams" -load: - loadType: "NumSensors" - loadValues: [25000, 50000, 75000, 100000, 125000, 150000] -resources: - resourceType: "Instances" - resourceValues: [1, 2, 3, 4, 5] -slos: - - sloType: "lag trend" - threshold: 2000 - prometheusUrl: "http://prometheus-operated:9090" - externalSloUrl: "http://localhost:80/evaluate-slope" - offset: 0 - warmup: 60 # in seconds -execution: - strategy: "LinearSearch" - duration: 300 # in seconds - repetitions: 1 - delay: 30 # in seconds - restrictions: - - "LowerBound" -configOverrides: [] -# - patcher: -# type: "NodeSelectorPatcher" -# resource: "uc1-load-generator-deployment.yaml" -# variableName: "env" -# value: "prod" -# - patcher: -# type: "NodeSelectorPatcher" -# resource: "uc1-kstreams-deployment.yaml" -# variableName: "env" -# value: "prod" -# - patcher: -# type: "ResourceLimitPatcher" -# resource: "uc1-kstreams-deployment.yaml" -# container: "uc-application" -# variableName: "cpu" -# value: "1000m" -# - patcher: -# type: "ResourceLimitPatcher" -# resource: "uc1-kstreams-deployment.yaml" -# container: "uc-application" -# variableName: "memory" -# value: "2Gi" -# - patcher: -# type: "SchedulerNamePatcher" -# resource: "uc1-kstreams-deployment.yaml" -# value: "random-scheduler" +spec: + benchmark: "uc1-kstreams" + load: + loadType: "NumSensors" + loadValues: [25000, 50000, 75000, 100000, 125000, 150000] + resources: + resourceType: "Instances" + resourceValues: [1, 2, 3, 4, 5] + slos: + - sloType: "lag trend" + threshold: 2000 + prometheusUrl: "http://prometheus-operated:9090" + externalSloUrl: "http://localhost:80/evaluate-slope" + offset: 0 + warmup: 60 # in seconds + execution: + strategy: "LinearSearch" + duration: 300 # in seconds + repetitions: 1 + delay: 30 # in seconds + restrictions: + - "LowerBound" + configOverrides: [] + # - patcher: + # type: "NodeSelectorPatcher" + # resource: "uc1-load-generator-deployment.yaml" + # variableName: "env" + # value: "prod" + # - patcher: + # type: "NodeSelectorPatcher" + # resource: "uc1-kstreams-deployment.yaml" + # variableName: "env" + # value: "prod" + # - patcher: + # type: "ResourceLimitPatcher" + # resource: "uc1-kstreams-deployment.yaml" + # container: "uc-application" + # variableName: "cpu" + # value: "1000m" + # - patcher: + # type: "ResourceLimitPatcher" + # resource: "uc1-kstreams-deployment.yaml" + # container: "uc-application" + # variableName: "memory" + # value: "2Gi" + # - patcher: + # type: "SchedulerNamePatcher" + # resource: "uc1-kstreams-deployment.yaml" + # value: "random-scheduler" diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/Benchmark.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/Benchmark.kt index 05d021b1bcfb77fa8ffeb0522510d49e39ef501c..d57a28e8bbcf4dc101e4814ecaa0d52fe28c08a9 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/Benchmark.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/Benchmark.kt @@ -1,5 +1,8 @@ package theodolite.benchmark +import io.fabric8.kubernetes.api.model.KubernetesResource +import io.fabric8.kubernetes.api.model.Namespaced +import io.fabric8.kubernetes.client.CustomResource import io.quarkus.runtime.annotations.RegisterForReflection import theodolite.util.ConfigurationOverride import theodolite.util.LoadDimension diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt index 38d0f0389ce92a8720df05e892d11cf4f1ac480a..62ab75898d16ff2732ab6aa5c254ec8f87fb7266 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt @@ -2,8 +2,6 @@ package theodolite.benchmark import com.fasterxml.jackson.databind.annotation.JsonDeserialize import io.fabric8.kubernetes.api.model.KubernetesResource -import io.fabric8.kubernetes.api.model.Namespaced -import io.fabric8.kubernetes.client.CustomResource import io.quarkus.runtime.annotations.RegisterForReflection import theodolite.util.ConfigurationOverride import kotlin.properties.Delegates @@ -26,7 +24,7 @@ import kotlin.properties.Delegates */ @JsonDeserialize @RegisterForReflection -class BenchmarkExecution : CustomResource(), Namespaced { +class BenchmarkExecution : KubernetesResource { var executionId: Int = 0 lateinit var name: String lateinit var benchmark: String @@ -34,7 +32,7 @@ class BenchmarkExecution : CustomResource(), Namespaced { lateinit var resources: ResourceDefinition lateinit var slos: List<Slo> lateinit var execution: Execution - lateinit var configOverrides: List<ConfigurationOverride?> + lateinit var configOverrides: MutableList<ConfigurationOverride?> /** * This execution encapsulates the [strategy], the [duration], the [repetitions], and the [restrictions] diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecutionList.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecutionList.kt deleted file mode 100644 index 50e8967f20aebad880ebd218136749af8e3ea6ee..0000000000000000000000000000000000000000 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecutionList.kt +++ /dev/null @@ -1,5 +0,0 @@ -package theodolite.benchmark - -import io.fabric8.kubernetes.client.CustomResourceList - -class BenchmarkExecutionList : CustomResourceList<BenchmarkExecution>() \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt index c89e9e85323d6e51c104b1d34ff1ef9d8d4d60cd..aa9c36ad912437e3b104dccf6ff1f4dea5905946 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt @@ -1,5 +1,6 @@ package theodolite.benchmark +import com.fasterxml.jackson.databind.annotation.JsonDeserialize import io.fabric8.kubernetes.api.model.KubernetesResource import io.fabric8.kubernetes.api.model.Namespaced import io.fabric8.kubernetes.client.CustomResource @@ -30,16 +31,18 @@ private var DEFAULT_NAMESPACE = "default" * for the deserializing in the [theodolite.execution.operator.TheodoliteOperator]. * @constructor construct an empty Benchmark. */ +@JsonDeserialize @RegisterForReflection -class KubernetesBenchmark : Benchmark, CustomResource(), Namespaced { +class KubernetesBenchmark: KubernetesResource, Benchmark{ lateinit var name: String lateinit var appResource: List<String> lateinit var loadGenResource: List<String> lateinit var resourceTypes: List<TypeName> lateinit var loadTypes: List<TypeName> lateinit var kafkaConfig: KafkaConfig - private val namespace = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE - var path = System.getenv("THEODOLITE_APP_RESOURCES") ?: "./config" + var namespace = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE + var path = System.getenv("THEODOLITE_APP_RESOURCES") ?: "./config" + /** * Loads [KubernetesResource]s. diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkList.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkList.kt deleted file mode 100644 index 0930875e96146fda58301478bda68b00c229e99f..0000000000000000000000000000000000000000 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkList.kt +++ /dev/null @@ -1,5 +0,0 @@ -package theodolite.benchmark - -import io.fabric8.kubernetes.client.CustomResourceList - -class KubernetesBenchmarkList : CustomResourceList<KubernetesBenchmark>() \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt index 54201ca330db7882c8637ad6e8bb33816dc3372a..d38b50b70c63c90e6bbb618386e0ed897087e6f1 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt @@ -95,7 +95,6 @@ class TheodoliteExecutor( return this.kubernetesBenchmark } - /** * Run all experiments which are specified in the corresponding * execution and benchmark objects. diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..75cbcad051e2055f25d876e66e0fffcdc249c4f5 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt @@ -0,0 +1,55 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.api.model.Namespaced +import io.fabric8.kubernetes.client.CustomResource +import io.fabric8.kubernetes.client.CustomResourceDoneable +import io.fabric8.kubernetes.client.CustomResourceList +import io.fabric8.kubernetes.client.KubernetesClient +import io.fabric8.kubernetes.client.dsl.MixedOperation +import io.fabric8.kubernetes.client.dsl.Resource +import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext +import java.lang.Thread.sleep + +abstract class AbstractStateHandler<T,L,D>( + private val context: CustomResourceDefinitionContext, + private val client: KubernetesClient, + private val crd: Class<T>, + private val crdList: Class<L>, + private val donableCRD: Class<D> + ): StateHandler where T : CustomResource, T: Namespaced, L: CustomResourceList<T>, D: CustomResourceDoneable<T> { + + private val crdClient: MixedOperation<T, L, D, Resource<T, D>> = + this.client.customResources(this.context, this.crd, this.crdList, this.donableCRD) + + @Synchronized + override fun setState(resourceName: String, f: (CustomResource) -> CustomResource?) { + this.crdClient + .inNamespace(this.client.namespace) + .list().items + .filter { item -> item.metadata.name == resourceName } + .map { customResource -> f(customResource) } + .forEach { this.crdClient.updateStatus(it as T) } + } + + @Synchronized + override fun getState(resourceName: String, f: (CustomResource) -> String?): String? { + return this.crdClient + .inNamespace(this.client.namespace) + .list().items + .filter { item -> item.metadata.name == resourceName } + .map { customResource -> f(customResource) } + .firstOrNull() + } + + @Synchronized + override fun blockUntilStateIsSet(resourceName: String, desiredStatusString: String, f: (CustomResource) -> String?, maxTries: Int): Boolean { + for (i in 0.rangeTo(maxTries)) { + val currentStatus = getState(resourceName, f) + if(currentStatus == desiredStatusString) { + return true + } + sleep(50) + } + return false + } +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/BenchmarkEventHandler.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/BenchmarkEventHandler.kt deleted file mode 100644 index 7c8188e3c342cfc1b22fefdd3ca91e7dbce85905..0000000000000000000000000000000000000000 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/BenchmarkEventHandler.kt +++ /dev/null @@ -1,67 +0,0 @@ -package theodolite.execution.operator - -import io.fabric8.kubernetes.client.informers.ResourceEventHandler -import mu.KotlinLogging -import theodolite.benchmark.KubernetesBenchmark - -private val logger = KotlinLogging.logger {} - -/** - * Handles adding, updating and deleting KubernetesBenchmarks. - * - * @param controller The TheodoliteController that handles the application state - * - * @see TheodoliteController - * @see KubernetesBenchmark - */ -class BenchmarkEventHandler(private val controller: TheodoliteController) : ResourceEventHandler<KubernetesBenchmark> { - - /** - * Add a KubernetesBenchmark. - * - * @param benchmark the KubernetesBenchmark to add - * - * @see KubernetesBenchmark - */ - override fun onAdd(benchmark: KubernetesBenchmark) { - benchmark.name = benchmark.metadata.name - logger.info { "Add new benchmark ${benchmark.name}." } - this.controller.benchmarks[benchmark.name] = benchmark - } - - /** - * Update a KubernetesBenchmark. - * - * @param oldBenchmark the KubernetesBenchmark to update - * @param newBenchmark the updated KubernetesBenchmark - * - * @see KubernetesBenchmark - */ - override fun onUpdate(oldBenchmark: KubernetesBenchmark, newBenchmark: KubernetesBenchmark) { - logger.info { "Update benchmark ${newBenchmark.metadata.name}." } - newBenchmark.name = newBenchmark.metadata.name - if (this.controller.isInitialized() && this.controller.executor.getBenchmark().name == oldBenchmark.metadata.name) { - this.controller.isUpdated.set(true) - this.controller.executor.executor.run.compareAndSet(true, false) - } else { - onAdd(newBenchmark) - } - } - - /** - * Delete a KubernetesBenchmark. - * - * @param benchmark the KubernetesBenchmark to delete - * - * @see KubernetesBenchmark - */ - override fun onDelete(benchmark: KubernetesBenchmark, b: Boolean) { - logger.info { "Delete benchmark ${benchmark.metadata.name}." } - this.controller.benchmarks.remove(benchmark.metadata.name) - if (this.controller.isInitialized() && this.controller.executor.getBenchmark().name == benchmark.metadata.name) { - this.controller.isUpdated.set(true) - this.controller.executor.executor.run.compareAndSet(true, false) - logger.info { "Current benchmark stopped." } - } - } -} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ClusterSetup.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ClusterSetup.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c8c48f791543b6d8a7716cf26a80bdb449ee7a7 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ClusterSetup.kt @@ -0,0 +1,76 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.NamespacedKubernetesClient +import io.fabric8.kubernetes.client.dsl.MixedOperation +import io.fabric8.kubernetes.client.dsl.Resource +import mu.KotlinLogging +import org.json.JSONObject +import theodolite.execution.Shutdown +import theodolite.k8s.K8sContextFactory +import theodolite.model.crd.* + +private val logger = KotlinLogging.logger {} + +class ClusterSetup( + private val executionCRDClient: MixedOperation<ExecutionCRD, BenchmarkExecutionList, DoneableExecution, Resource<ExecutionCRD, DoneableExecution>>, + private val benchmarkCRDClient: MixedOperation<BenchmarkCRD, KubernetesBenchmarkList, DoneableBenchmark, Resource<BenchmarkCRD, DoneableBenchmark>>, + private val client: NamespacedKubernetesClient + + ) { + private val serviceMonitorContext = K8sContextFactory().create( + api = "v1", + scope = "Namespaced", + group = "monitoring.coreos.com", + plural = "servicemonitors" + ) + + fun clearClusterState(){ + stopRunningExecution() + clearByLabel() + } + + private fun stopRunningExecution() { + executionCRDClient + .inNamespace(client.namespace) + .list() + .items + .asSequence() + .filter { it.status.executionState == States.RUNNING.value } + .forEach { execution -> + val benchmark = benchmarkCRDClient + .inNamespace(client.namespace) + .list() + .items + .firstOrNull { it.metadata.name == execution.spec.benchmark } + + if (benchmark != null) { + execution.spec.name = execution.metadata.name + benchmark.spec.name = benchmark.metadata.name + Shutdown(execution.spec, benchmark.spec).start() + } else { + logger.error { + "Execution with state ${States.RUNNING.value} was found, but no corresponding benchmark. " + + "Could not initialize cluster." } + } + + + } + } + + private fun clearByLabel() { + this.client.services().withLabel("app.kubernetes.io/created-by=theodolite").delete() + this.client.apps().deployments().withLabel("app.kubernetes.io/created-by=theodolite").delete() + this.client.apps().statefulSets().withLabel("app.kubernetes.io/created-by=theodolite").delete() + this.client.configMaps().withLabel("app.kubernetes.io/created-by=theodolite").delete() + + val serviceMonitors = JSONObject( + this.client.customResource(serviceMonitorContext) + .list(client.namespace, mapOf(Pair("app.kubernetes.io/created-by", "theodolite"))) + ) + .getJSONArray("items") + + (0 until serviceMonitors.length()) + .map { serviceMonitors.getJSONObject(it).getJSONObject("metadata").getString("name") } + .forEach { this.client.customResource(serviceMonitorContext).delete(client.namespace, it) } + } +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt index ea62b7b895fce772a1f89019ea4aaac0f3957dc1..a1617b4988d500baab7b02bf5fa993f7a4ae76a3 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt @@ -1,8 +1,11 @@ package theodolite.execution.operator +import com.google.gson.Gson +import com.google.gson.GsonBuilder import io.fabric8.kubernetes.client.informers.ResourceEventHandler import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution +import theodolite.model.crd.* private val logger = KotlinLogging.logger {} @@ -14,17 +17,30 @@ private val logger = KotlinLogging.logger {} * @see TheodoliteController * @see BenchmarkExecution */ -class ExecutionHandler(private val controller: TheodoliteController) : ResourceEventHandler<BenchmarkExecution> { +class ExecutionHandler( + 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. * - * @param execution the execution to add + * @param ExecutionCRD the execution to add */ - override fun onAdd(execution: BenchmarkExecution) { - execution.name = execution.metadata.name - logger.info { "Add new execution ${execution.metadata.name} to queue." } - this.controller.executionsQueue.add(execution) + @Synchronized + override fun onAdd(execution: ExecutionCRD) { + logger.info { "Add execution ${execution.metadata.name}" } + execution.spec.name = execution.metadata.name + when (this.stateHandler.getExecutionState(execution.metadata.name)) { + null -> this.stateHandler.setExecutionState(execution.spec.name, States.PENDING) + States.RUNNING -> { + this.stateHandler.setExecutionState(execution.spec.name, States.RESTART) + if(this.controller.isExecutionRunning(execution.spec.name)){ + this.controller.stop(restart=true) + } + } + } } /** @@ -32,40 +48,39 @@ class ExecutionHandler(private val controller: TheodoliteController) : ResourceE * added to the beginning of the queue of the TheodoliteController. * Otherwise, it is just added to the beginning of the queue. * - * @param oldExecution the old execution - * @param newExecution the new execution + * @param oldExecutionCRD the old execution + * @param newExecutionCRD the new execution */ - override fun onUpdate(oldExecution: BenchmarkExecution, newExecution: BenchmarkExecution) { - logger.info { "Add updated execution to queue." } - newExecution.name = newExecution.metadata.name - try { - this.controller.executionsQueue.removeIf { e -> e.name == newExecution.metadata.name } - } catch (e: NullPointerException) { - logger.warn { "No execution found for deletion" } - } - this.controller.executionsQueue.addFirst(newExecution) - if (this.controller.isInitialized() && this.controller.executor.getExecution().name == newExecution.metadata.name) { - this.controller.isUpdated.set(true) - this.controller.executor.executor.run.compareAndSet(true, false) + @Synchronized + override fun onUpdate(oldExecution: ExecutionCRD, newExecution: ExecutionCRD) { + logger.info { "Receive update event for execution ${oldExecution.metadata.name}" } + newExecution.spec.name = newExecution.metadata.name + oldExecution.spec.name = oldExecution.metadata.name + if(gson.toJson(oldExecution.spec) != gson.toJson(newExecution.spec)) { + when(this.stateHandler.getExecutionState(newExecution.metadata.name)) { + States.RUNNING -> { + this.stateHandler.setExecutionState(newExecution.spec.name, States.RESTART) + if (this.controller.isExecutionRunning(newExecution.spec.name)){ + this.controller.stop(restart=true) + } + } + States.RESTART -> {} // should this set to pending? + else -> this.stateHandler.setExecutionState(newExecution.spec.name, States.PENDING) + } + } } - } /** * Delete an execution from the queue of the TheodoliteController. * - * @param execution the execution to delete + * @param ExecutionCRD the execution to delete */ - override fun onDelete(execution: BenchmarkExecution, b: Boolean) { - try { - this.controller.executionsQueue.removeIf { e -> e.name == execution.metadata.name } - logger.info { "Delete execution ${execution.metadata.name} from queue." } - } catch (e: NullPointerException) { - logger.warn { "No execution found for deletion" } - } - if (this.controller.isInitialized() && this.controller.executor.getExecution().name == execution.metadata.name) { - this.controller.isUpdated.set(true) - this.controller.executor.executor.run.compareAndSet(true, false) - logger.info { "Current benchmark stopped." } + @Synchronized + override fun onDelete(execution: ExecutionCRD, b: Boolean) { + logger.info { "Delete execution ${execution.metadata.name}" } + if(execution.status.executionState == States.RUNNING.value + && this.controller.isExecutionRunning(execution.spec.name)) { + this.controller.stop() } } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..fe1b95f95c74efe77913ea435dd1ac896805b065 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt @@ -0,0 +1,82 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.CustomResource +import io.fabric8.kubernetes.client.KubernetesClient +import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext +import theodolite.model.crd.BenchmarkExecutionList +import theodolite.model.crd.ExecutionCRD +import theodolite.model.crd.States +import java.lang.Thread.sleep +import java.time.Duration +import java.time.Instant +import java.util.concurrent.atomic.AtomicBoolean + +class ExecutionStateHandler(context: CustomResourceDefinitionContext, val client: KubernetesClient): + AbstractStateHandler<ExecutionCRD, BenchmarkExecutionList, DoneableExecution>( + context = context, + client = client, + crd = ExecutionCRD::class.java, + crdList = BenchmarkExecutionList::class.java, + donableCRD = DoneableExecution::class.java) { + + private var runExecutionDurationTimer: AtomicBoolean = AtomicBoolean(false) + + private fun getExecutionLambda() = { cr: CustomResource -> + var execState = "" + if (cr is ExecutionCRD) { execState = cr.status.executionState } + execState + } + + private fun getDurationLambda() = { cr: CustomResource -> + var execState = "" + if (cr is ExecutionCRD) { execState = cr.status.executionState } + execState + } + + fun setExecutionState(resourceName: String, status: States): Boolean { + setState(resourceName) {cr -> if(cr is ExecutionCRD) cr.status.executionState = status.value; cr} + return blockUntilStateIsSet(resourceName, status.value, getExecutionLambda()) + } + + fun getExecutionState(resourceName: String) : States? { + val status = this.getState(resourceName, getExecutionLambda()) + return States.values().firstOrNull { it.value == status } + } + + fun setDurationState(resourceName: String, duration: Duration) { + setState(resourceName) { cr -> if (cr is ExecutionCRD) cr.status.executionDuration = durationToK8sString(duration); cr } + blockUntilStateIsSet(resourceName, durationToK8sString(duration), getDurationLambda()) + } + + fun getDurationState(resourceName: String): String? { + return this.getState(resourceName, getDurationLambda()) + } + + private fun durationToK8sString(duration: Duration): String { + val sec = duration.seconds + return when { + sec <= 120 -> "${sec}s" // max 120s + sec < 60 * 99 -> "${duration.toMinutes()}m" // max 99m + sec < 60 * 60 * 99 -> "${duration.toHours()}h" // max 99h + else -> "${duration.toDays()}d + ${duration.minusDays(duration.toDays()).toHours()}h" + } + } + + fun startDurationStateTimer(resourceName: String) { + this.runExecutionDurationTimer.set(true) + val startTime = Instant.now().toEpochMilli() + Thread { + while (this.runExecutionDurationTimer.get()) { + val duration = Duration.ofMillis(Instant.now().minusMillis(startTime).toEpochMilli()) + setDurationState(resourceName, duration) + sleep(100 * 1) + } + }.start() + } + + @Synchronized + fun stopDurationStateTimer() { + this.runExecutionDurationTimer.set(false) + sleep(100 * 2) + } +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/StateHandler.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/StateHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..0fbd97e5cca4a9be220eb0b0c89ea0af129a7860 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/StateHandler.kt @@ -0,0 +1,15 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.CustomResource +private const val MAX_TRIES: Int = 5 + +interface StateHandler { + fun setState(resourceName: String, f: (CustomResource) -> CustomResource?) + fun getState(resourceName: String, f: (CustomResource) -> String?): String? + fun blockUntilStateIsSet( + resourceName: String, + desiredStatusString: String, + f: (CustomResource) -> String?, + maxTries: Int = MAX_TRIES): Boolean + +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt index 5c8a225a7afa4dd8094ee283af50cbd4efea9f50..1e3929da98be060e2cbebf305dbae2f25519798a 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt @@ -1,120 +1,194 @@ package theodolite.execution.operator import io.fabric8.kubernetes.client.NamespacedKubernetesClient -import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext +import io.fabric8.kubernetes.client.dsl.MixedOperation +import io.fabric8.kubernetes.client.dsl.Resource import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution import theodolite.benchmark.KubernetesBenchmark import theodolite.execution.TheodoliteExecutor +import theodolite.model.crd.* +import theodolite.util.ConfigurationOverride +import theodolite.util.PatcherDefinition import java.lang.Thread.sleep -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedDeque -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger private val logger = KotlinLogging.logger {} /** * The controller implementation for Theodolite. * - * Maintains a Dequeue, based on ConcurrentLinkedDequeue, of executions to be executed for a benchmark. - * - * @param client The NamespacedKubernetesClient - * @param executionContext The CustomResourceDefinitionContext - * * @see NamespacedKubernetesClient * @see CustomResourceDefinitionContext * @see BenchmarkExecution * @see KubernetesBenchmark * @see ConcurrentLinkedDeque */ + class TheodoliteController( - val client: NamespacedKubernetesClient, - val executionContext: CustomResourceDefinitionContext, - val path: String + private val namespace: String, + val path: String, + private val executionCRDClient: MixedOperation<ExecutionCRD, BenchmarkExecutionList, DoneableExecution, Resource<ExecutionCRD, DoneableExecution>>, + private val benchmarkCRDClient: MixedOperation<BenchmarkCRD, KubernetesBenchmarkList, DoneableBenchmark, Resource<BenchmarkCRD, DoneableBenchmark>>, + private val executionStateHandler: ExecutionStateHandler ) { lateinit var executor: TheodoliteExecutor - val executionsQueue: ConcurrentLinkedDeque<BenchmarkExecution> = ConcurrentLinkedDeque() - val benchmarks: ConcurrentHashMap<String, KubernetesBenchmark> = ConcurrentHashMap() - var isUpdated = AtomicBoolean(false) - /** + * * Runs the TheodoliteController forever. */ fun run() { + sleep(5000) // wait until all states are correctly set while (true) { - try { - reconcile() - logger.info { "Theodolite is waiting for new matching benchmark and execution." } - logger.info { "Currently available executions: " } - executionsQueue.forEach { - logger.info { "${it.name} : waiting for : ${it.benchmark}" } - } - logger.info { "Currently available benchmarks: " } - benchmarks.forEach { - logger.info { it.key } - } - sleep(2000) - } catch (e: InterruptedException) { - logger.error { "Execution interrupted with error: $e." } - } + reconcile() + sleep(2000) } } - /** - * Ensures that the application state corresponds to the defined KubernetesBenchmarks and BenchmarkExecutions. - * - * @see KubernetesBenchmark - * @see BenchmarkExecution - */ - @Synchronized private fun reconcile() { - while (executionsQueue.isNotEmpty()) { - val execution = executionsQueue.peek() - val benchmark = benchmarks[execution.benchmark] - - if (benchmark == null) { - logger.debug { "No benchmark found for execution ${execution.name}." } - sleep(1000) + do { + val execution = getNextExecution() + if (execution != null) { + val benchmark = getBenchmarks() + .firstOrNull { it.name == execution.benchmark } + if (benchmark != null) { + runExecution(execution, benchmark) + } } else { - runExecution(execution, benchmark) + logger.info { "Could not find executable execution." } } - } + } while (execution != null) } /** * Execute a benchmark with a defined KubernetesBenchmark and BenchmarkExecution * - * @see KubernetesBenchmark * @see BenchmarkExecution */ - @Synchronized - fun runExecution(execution: BenchmarkExecution, benchmark: KubernetesBenchmark) { - isUpdated.set(false) - benchmark.path = path - logger.info { "Start execution ${execution.name} with benchmark ${benchmark.name}." } - executor = TheodoliteExecutor(config = execution, kubernetesBenchmark = benchmark) - executor.run() + private fun runExecution(execution: BenchmarkExecution, benchmark: KubernetesBenchmark) { + setAdditionalLabels(execution.name, + "deployed-for-execution", + benchmark.appResource + benchmark.loadGenResource, + execution) + setAdditionalLabels(benchmark.name, + "deployed-for-benchmark", + benchmark.appResource + benchmark.loadGenResource, + execution) + setAdditionalLabels("theodolite", + "app.kubernetes.io/created-by", + benchmark.appResource + benchmark.loadGenResource, + execution) + + executionStateHandler.setExecutionState(execution.name, States.RUNNING) + executionStateHandler.startDurationStateTimer(execution.name) try { - if (!isUpdated.get()) { - this.executionsQueue.removeIf { e -> e.name == execution.name } - client.customResource(executionContext).delete(client.namespace, execution.metadata.name) + executor = TheodoliteExecutor(execution, benchmark) + executor.run() + when (executionStateHandler.getExecutionState(execution.name)) { + States.RESTART -> runExecution(execution, benchmark) + States.RUNNING -> { + executionStateHandler.setExecutionState(execution.name, States.FINISHED) + logger.info { "Execution of ${execution.name} is finally stopped." } + } } } catch (e: Exception) { - logger.warn { "Deletion skipped." } + logger.error { "Failure while executing execution ${execution.name} with benchmark ${benchmark.name}." } + logger.error { "Problem is: $e" } + executionStateHandler.setExecutionState(execution.name, States.FAILURE) } + executionStateHandler.stopDurationStateTimer() + } - logger.info { "Execution of ${execution.name} is finally stopped." } + @Synchronized + fun stop(restart: Boolean = false) { + if (!::executor.isInitialized) return + if (restart) { + executionStateHandler.setExecutionState(this.executor.getExecution().name, States.RESTART) + } else { + executionStateHandler.setExecutionState(this.executor.getExecution().name, States.INTERRUPTED) + logger.warn { "Execution ${executor.getExecution().name} unexpected interrupted" } + } + this.executor.executor.run.set(false) + } + + /** + * @return all available [BenchmarkCRD]s + */ + private fun getBenchmarks(): List<KubernetesBenchmark> { + return this.benchmarkCRDClient + .inNamespace(namespace) + .list() + .items + .map { it.spec.name = it.metadata.name; it } + .map { it.spec.path = path; it } + .map { it.spec } } /** - * @return true if the TheodoliteExecutor of this controller is initialized. Else returns false. + * Get the [BenchmarkExecution] for the next run. Which [BenchmarkExecution] + * is selected for the next execution depends on three points: * - * @see TheodoliteExecutor + * 1. Only executions are considered for which a matching benchmark is available on the cluster + * 2. The Status of the execution must be [States.PENDING] or [States.RESTART] + * 3. Of the remaining [BenchmarkCRD], those with status [States.RESTART] are preferred, + * then, if there is more than one, the oldest execution is chosen. + * + * @return the next execution or null */ - @Synchronized - fun isInitialized(): Boolean { - return ::executor.isInitialized + private fun getNextExecution(): BenchmarkExecution? { + val availableBenchmarkNames = getBenchmarks() + .map { it.name } + + return executionCRDClient + .inNamespace(namespace) + .list() + .items + .asSequence() + .map { it.spec.name = it.metadata.name; it } + .filter { + it.status.executionState == States.PENDING.value || + it.status.executionState == States.RESTART.value + } + .filter { availableBenchmarkNames.contains(it.spec.benchmark) } + .sortedWith(stateComparator().thenBy { it.metadata.creationTimestamp }) + .map { it.spec } + .firstOrNull() + } + + /** + * Simple comparator which can be used to order a list of [ExecutionCRD] such that executions with + * status [States.RESTART] are before all other executions. + */ + private fun stateComparator() = Comparator<ExecutionCRD> { a, b -> + when { + (a == null && b == null) -> 0 + (a.status.executionState == States.RESTART.value) -> -1 + else -> 1 + } + } + + fun isExecutionRunning(executionName: String): Boolean { + return this.executor.getExecution().name == executionName + } + + private fun setAdditionalLabels( + labelValue: String, + labelName: String, + resources: List<String>, + execution: BenchmarkExecution + ) { + val additionalConfigOverrides = mutableListOf<ConfigurationOverride>() + resources.forEach { + run { + val configurationOverride = ConfigurationOverride() + configurationOverride.patcher = PatcherDefinition() + configurationOverride.patcher.type = "LabelPatcher" + configurationOverride.patcher.variableName = labelName + configurationOverride.patcher.resource = it + configurationOverride.value = labelValue + additionalConfigOverrides.add(configurationOverride) + } + } + execution.configOverrides.addAll(additionalConfigOverrides) } -} +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt index 5e15a4a80e67f47a42d605d4af39102927139331..49fbf54eecf2a178ae8a0b38bedee1c597a80e1f 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt @@ -1,13 +1,12 @@ package theodolite.execution.operator import io.fabric8.kubernetes.client.DefaultKubernetesClient +import io.fabric8.kubernetes.client.dsl.MixedOperation +import io.fabric8.kubernetes.client.dsl.Resource import io.fabric8.kubernetes.internal.KubernetesDeserializer import mu.KotlinLogging -import theodolite.benchmark.BenchmarkExecution -import theodolite.benchmark.BenchmarkExecutionList -import theodolite.benchmark.KubernetesBenchmark -import theodolite.benchmark.KubernetesBenchmarkList import theodolite.k8s.K8sContextFactory +import theodolite.model.crd.* private const val DEFAULT_NAMESPACE = "default" @@ -16,7 +15,7 @@ private const val EXECUTION_SINGULAR = "execution" private const val EXECUTION_PLURAL = "executions" private const val BENCHMARK_SINGULAR = "benchmark" private const val BENCHMARK_PLURAL = "benchmarks" -private const val API_VERSION = "v1alpha1" +private const val API_VERSION = "v1" private const val RESYNC_PERIOD = 10 * 60 * 1000.toLong() private const val GROUP = "theodolite.com" private val logger = KotlinLogging.logger {} @@ -33,42 +32,82 @@ class TheodoliteOperator { * Start the operator. */ fun start() { + // FIXME("Remove all benchmark state handling") logger.info { "Using $namespace as namespace." } val client = DefaultKubernetesClient().inNamespace(namespace) + client.use { + KubernetesDeserializer.registerCustomKind( + "$GROUP/$API_VERSION", + EXECUTION_SINGULAR, + ExecutionCRD::class.java + ) - KubernetesDeserializer.registerCustomKind( - "$GROUP/$API_VERSION", - EXECUTION_SINGULAR, - BenchmarkExecution::class.java - ) - - KubernetesDeserializer.registerCustomKind( - "$GROUP/$API_VERSION", - BENCHMARK_SINGULAR, - KubernetesBenchmark::class.java - ) - - val contextFactory = K8sContextFactory() - val executionContext = contextFactory.create(API_VERSION, SCOPE, GROUP, EXECUTION_PLURAL) - val benchmarkContext = contextFactory.create(API_VERSION, SCOPE, GROUP, BENCHMARK_PLURAL) - - val appResource = System.getenv("THEODOLITE_APP_RESOURCES") ?: "./config" - val controller = TheodoliteController(client = client, executionContext = executionContext, path = appResource) - - val informerFactory = client.informers() - val informerExecution = informerFactory.sharedIndexInformerForCustomResource( - executionContext, BenchmarkExecution::class.java, - BenchmarkExecutionList::class.java, RESYNC_PERIOD - ) - val informerBenchmark = informerFactory.sharedIndexInformerForCustomResource( - benchmarkContext, KubernetesBenchmark::class.java, - KubernetesBenchmarkList::class.java, RESYNC_PERIOD - ) - - informerExecution.addEventHandler(ExecutionHandler(controller)) - informerBenchmark.addEventHandler(BenchmarkEventHandler(controller)) - informerFactory.startAllRegisteredInformers() - - controller.run() + KubernetesDeserializer.registerCustomKind( + "$GROUP/$API_VERSION", + BENCHMARK_SINGULAR, + BenchmarkCRD::class.java + ) + + val contextFactory = K8sContextFactory() + val executionContext = contextFactory.create(API_VERSION, SCOPE, GROUP, EXECUTION_PLURAL) + val benchmarkContext = contextFactory.create(API_VERSION, SCOPE, GROUP, BENCHMARK_PLURAL) + + val executionCRDClient: MixedOperation< + ExecutionCRD, + BenchmarkExecutionList, + DoneableExecution, + Resource<ExecutionCRD, DoneableExecution>> + = client.customResources( + executionContext, + ExecutionCRD::class.java, + BenchmarkExecutionList::class.java, + DoneableExecution::class.java) + + val benchmarkCRDClient: MixedOperation< + BenchmarkCRD, + KubernetesBenchmarkList, + DoneableBenchmark, + Resource<BenchmarkCRD, DoneableBenchmark>> + = client.customResources( + benchmarkContext, + BenchmarkCRD::class.java, + KubernetesBenchmarkList::class.java, + DoneableBenchmark::class.java) + + val executionStateHandler = ExecutionStateHandler( + context = executionContext, + client = client) + + val appResource = System.getenv("THEODOLITE_APP_RESOURCES") ?: "./config" + val controller = + TheodoliteController( + namespace = client.namespace, + path = appResource, + benchmarkCRDClient = benchmarkCRDClient, + executionCRDClient = executionCRDClient, + executionStateHandler = executionStateHandler) + + val informerFactory = client.informers() + val informerExecution = informerFactory.sharedIndexInformerForCustomResource( + executionContext, + ExecutionCRD::class.java, + BenchmarkExecutionList::class.java, + RESYNC_PERIOD + ) + + informerExecution.addEventHandler(ExecutionHandler( + controller = controller, + stateHandler = executionStateHandler)) + + ClusterSetup( + executionCRDClient = executionCRDClient, + benchmarkCRDClient = benchmarkCRDClient, + client = client + ).clearClusterState() + + informerFactory.startAllRegisteredInformers() + controller.run() + + } } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/k8s/ServiceMonitorWrapper.kt b/theodolite-quarkus/src/main/kotlin/theodolite/k8s/ServiceMonitorWrapper.kt index 4950cee225e103ff095def91de64471ec1894a79..56452d74968db0fd4c939f44f3ed8a7abbe7b928 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/k8s/ServiceMonitorWrapper.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/k8s/ServiceMonitorWrapper.kt @@ -53,4 +53,9 @@ class ServiceMonitorWrapper(private val serviceMonitor: Map<String, String>) : C val smAsMap = this.serviceMonitor["metadata"]!! as Map<String, String> return smAsMap["name"]!! } + + fun getLabels(): Map<String, String>{ + val smAsMap = this.serviceMonitor["metadata"]!! as Map<String, String> + return smAsMap["labels"]!! as Map<String, String> + } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt new file mode 100644 index 0000000000000000000000000000000000000000..326aa10a21bebd913eaafcb8315188288ae97ff1 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt @@ -0,0 +1,11 @@ +package theodolite.model.crd + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import io.fabric8.kubernetes.api.model.Namespaced +import io.fabric8.kubernetes.client.CustomResource +import theodolite.benchmark.KubernetesBenchmark + +@JsonDeserialize +class BenchmarkCRD( + var spec: KubernetesBenchmark = KubernetesBenchmark() +) : CustomResource(), Namespaced \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/BenchmarkExecutionList.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/BenchmarkExecutionList.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b2dcc07f9c37f1712109e3d092f2db0c139e1c8 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/BenchmarkExecutionList.kt @@ -0,0 +1,5 @@ +package theodolite.model.crd + +import io.fabric8.kubernetes.client.CustomResourceList + +class BenchmarkExecutionList : CustomResourceList<ExecutionCRD>() diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/DoneableBenchmark.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/DoneableBenchmark.kt new file mode 100644 index 0000000000000000000000000000000000000000..e00e8268b2ec8eba17b3706feb3940eded1b2b0c --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/DoneableBenchmark.kt @@ -0,0 +1,7 @@ +package theodolite.model.crd + +import io.fabric8.kubernetes.api.builder.Function +import io.fabric8.kubernetes.client.CustomResourceDoneable + +class DoneableBenchmark(resource: BenchmarkCRD, function: Function<BenchmarkCRD, BenchmarkCRD>) : + CustomResourceDoneable<BenchmarkCRD>(resource, function) \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/DoneableExecution.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/DoneableExecution.kt new file mode 100644 index 0000000000000000000000000000000000000000..be07725b405c29a0d9000b6e6ec455536ad111fb --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/DoneableExecution.kt @@ -0,0 +1,8 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.CustomResourceDoneable +import io.fabric8.kubernetes.api.builder.Function +import theodolite.model.crd.ExecutionCRD + +class DoneableExecution(resource: ExecutionCRD, function: Function<ExecutionCRD, ExecutionCRD>) : + CustomResourceDoneable<ExecutionCRD>(resource, function) \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt new file mode 100644 index 0000000000000000000000000000000000000000..79a387cee250d3abf0fdb576a5f0f33424596792 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt @@ -0,0 +1,13 @@ +package theodolite.model.crd + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import io.fabric8.kubernetes.api.model.KubernetesResource +import io.fabric8.kubernetes.api.model.Namespaced +import io.fabric8.kubernetes.client.CustomResource +import theodolite.benchmark.BenchmarkExecution + +@JsonDeserialize +class ExecutionCRD( + var spec: BenchmarkExecution = BenchmarkExecution(), + var status: ExecutionStatus = ExecutionStatus() + ) : CustomResource(), Namespaced \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/ExecutionStatus.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/ExecutionStatus.kt new file mode 100644 index 0000000000000000000000000000000000000000..43e9035b3120eb22304576f2006092eec376b6d2 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/ExecutionStatus.kt @@ -0,0 +1,13 @@ +package theodolite.model.crd + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import io.fabric8.kubernetes.api.model.KubernetesResource +import io.fabric8.kubernetes.api.model.Namespaced +import io.fabric8.kubernetes.client.CustomResource + +@JsonDeserialize +class ExecutionStatus(): KubernetesResource, CustomResource(), Namespaced { + var executionState: String = "" + var executionDuration: String = "-" + +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/KubernetesBenchmarkList.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/KubernetesBenchmarkList.kt new file mode 100644 index 0000000000000000000000000000000000000000..8ad0a493d948bf5f78741052100766dcf6e316ec --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/KubernetesBenchmarkList.kt @@ -0,0 +1,5 @@ +package theodolite.model.crd + +import io.fabric8.kubernetes.client.CustomResourceList + +class KubernetesBenchmarkList : CustomResourceList<BenchmarkCRD>() diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/States.kt b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/States.kt new file mode 100644 index 0000000000000000000000000000000000000000..79af297915b6703b209acb0c13913482e54db2be --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/model/crd/States.kt @@ -0,0 +1,11 @@ +package theodolite.model.crd + +enum class States(val value: String) { + RUNNING("RUNNING"), + PENDING("PENDING"), + FAILURE("FAILURE"), + FINISHED("FINISHED"), + RESTART("RESTART"), + INTERRUPTED("INTERRUPTED"), + NO_STATE("NoState") +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/LabelPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/LabelPatcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9feff00726c8c73483118276eeae7b7975d8c8e --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/LabelPatcher.kt @@ -0,0 +1,50 @@ +package theodolite.patcher + +import io.fabric8.kubernetes.api.model.ConfigMap +import io.fabric8.kubernetes.api.model.KubernetesResource +import io.fabric8.kubernetes.api.model.Service +import io.fabric8.kubernetes.api.model.apps.Deployment +import io.fabric8.kubernetes.api.model.apps.StatefulSet +import io.fabric8.kubernetes.client.CustomResource + +class LabelPatcher(private val k8sResource: KubernetesResource, val variableName: String) : + AbstractPatcher(k8sResource, variableName) { + + override fun <String> patch(labelValue: String) { + println("call patcher for resource $k8sResource !") + if(labelValue is kotlin.String){ + when(k8sResource){ + is Deployment -> { + if (k8sResource.metadata.labels == null){ + k8sResource.metadata.labels = mutableMapOf() + } + k8sResource.metadata.labels[this.variableName] = labelValue + } + is StatefulSet -> { + if (k8sResource.metadata.labels == null){ + k8sResource.metadata.labels = mutableMapOf() + } + k8sResource.metadata.labels[this.variableName] = labelValue + } + is Service -> { + if (k8sResource.metadata.labels == null){ + k8sResource.metadata.labels = mutableMapOf() + } + k8sResource.metadata.labels[this.variableName] = labelValue + } + is ConfigMap -> { + if (k8sResource.metadata.labels == null){ + k8sResource.metadata.labels = mutableMapOf() + } + k8sResource.metadata.labels[this.variableName] = labelValue + } + is CustomResource -> { + if (k8sResource.metadata.labels == null){ + k8sResource.metadata.labels = mutableMapOf() + } + k8sResource.metadata.labels[this.variableName] = labelValue + } + } + } + } +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt index 2ee1f6c7b46322cb0f8de03c37aabe64ccf0ba5a..9ca6570ff56a673ffde144b68d3f3d9c90913ef9 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt @@ -45,6 +45,7 @@ class PatcherFactory { patcherDefinition.variableName ) "SchedulerNamePatcher" -> SchedulerNamePatcher(resource) + "LabelPatcher" -> LabelPatcher(resource, patcherDefinition.variableName) else -> throw IllegalArgumentException("Patcher type ${patcherDefinition.type} not found") } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/KafkaConfig.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/KafkaConfig.kt index 398ff90bed8f48683321e2375458b3a065c39463..4e72ccb0d86749a6538c26556241ac114ef8d9a4 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/util/KafkaConfig.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/KafkaConfig.kt @@ -28,6 +28,7 @@ class KafkaConfig { * Wrapper for a topic definition. */ @RegisterForReflection + @JsonDeserialize class TopicWrapper { /** * The topic name diff --git a/theodolite-quarkus/src/main/resources/operator/benchmarkCRD.yaml b/theodolite-quarkus/src/main/resources/operator/benchmarkCRD.yaml index 8fb3de1928f051d338a78ee58da074a73ef933c1..4e481c51231999e2e7a1e75ecbc018d40db75c91 100644 --- a/theodolite-quarkus/src/main/resources/operator/benchmarkCRD.yaml +++ b/theodolite-quarkus/src/main/resources/operator/benchmarkCRD.yaml @@ -1,13 +1,119 @@ -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: benchmarks.theodolite.com spec: group: theodolite.com - version: v1alpha1 names: kind: benchmark plural: benchmarks - scope: Namespaced - subresources: - status: {} \ No newline at end of file + shortNames: + - bench + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: [] + properties: + name: + type: string + appResource: + type: array + minItems: 1 + items: + type: string + loadGenResource: + type: array + minItems: 1 + items: + type: string + resourceTypes: + type: array + minItems: 1 + items: + type: object + properties: + typeName: + type: string + patchers: + type: array + minItems: 1 + items: + type: object + properties: + type: + type: string + default: "" + resource: + type: string + default: "" + container: + type: string + default: "" + variableName: + type: string + default: "" + loadTypes: + type: array + minItems: 1 + items: + type: object + properties: + typeName: + type: string + patchers: + type: array + minItems: 1 + items: + type: object + properties: + type: + type: string + default: "" + resource: + type: string + default: "" + container: + type: string + default: "" + variableName: + type: string + default: "" + kafkaConfig: + type: object + properties: + bootstrapServer: + type: string + topics: + type: array + minItems: 1 + items: + type: object + required: [] + properties: + name: + type: string + default: "" + numPartitions: + type: integer + default: 0 + replicationFactor: + type: integer + default: 0 + removeOnly: + type: boolean + default: false + additionalPrinterColumns: + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + subresources: + status: {} + scope: Namespaced \ No newline at end of file diff --git a/theodolite-quarkus/src/main/resources/operator/example-benchmark-k8s-resource.yaml b/theodolite-quarkus/src/main/resources/operator/example-benchmark-k8s-resource.yaml index 19ec972be8236fbdcad123e9c9ef63945bb53d16..16c14b665b99a4863279880d9ad6c03c7435578c 100644 --- a/theodolite-quarkus/src/main/resources/operator/example-benchmark-k8s-resource.yaml +++ b/theodolite-quarkus/src/main/resources/operator/example-benchmark-k8s-resource.yaml @@ -1,31 +1,34 @@ -apiVersion: theodolite.com/v1alpha1 +apiVersion: theodolite.com/v1 kind: benchmark metadata: name: uc1-kstreams -appResource: - - "uc1-kstreams-deployment.yaml" - - "aggregation-service.yaml" - - "jmx-configmap.yaml" -loadGenResource: - - "uc1-load-generator-deployment.yaml" - - "uc1-load-generator-service.yaml" -resourceTypes: - - typeName: "Instances" - patchers: - - type: "ReplicaPatcher" - resource: "uc1-kstreams-deployment.yaml" -loadTypes: - - typeName: "NumSensors" - patchers: - - type: "EnvVarPatcher" - resource: "uc1-load-generator-deployment.yaml" - container: "workload-generator" - variableName: "NUM_SENSORS" -kafkaConfig: - bootstrapServer: "localhost:31290" - topics: - - name: "input" - numPartitions: 40 - replicationFactor: 1 - - name: "theodolite-.*" - removeOnly: True \ No newline at end of file +spec: + name: test + appResource: + - "uc1-kstreams-deployment.yaml" + - "aggregation-service.yaml" + - "jmx-configmap.yaml" + loadGenResource: + - "uc1-load-generator-deployment.yaml" + - "uc1-load-generator-service.yaml" + - "uc1-load-generator-service.yaml" + resourceTypes: + - typeName: "Instances" + patchers: + - type: "ReplicaPatcher" + resource: "uc1-kstreams-deployment.yaml" + loadTypes: + - typeName: "NumSensors" + patchers: + - type: "EnvVarPatcher" + resource: "uc1-load-generator-deployment.yaml" + container: "workload-generator" + variableName: "NUM_SENSORS" + kafkaConfig: + bootstrapServer: "localhost:31290" + topics: + - name: "input" + numPartitions: 40 + replicationFactor: 1 + - name: "theodolite-.*" + removeOnly: True \ No newline at end of file diff --git a/theodolite-quarkus/src/main/resources/operator/example-execution-k8s-resource.yaml b/theodolite-quarkus/src/main/resources/operator/example-execution-k8s-resource.yaml index 7f76b1bca0db77df08861e0611487642e19bbc1a..4227020e7750c8e93f92c469d7796e381eb452e3 100644 --- a/theodolite-quarkus/src/main/resources/operator/example-execution-k8s-resource.yaml +++ b/theodolite-quarkus/src/main/resources/operator/example-execution-k8s-resource.yaml @@ -1,38 +1,41 @@ -apiVersion: theodolite.com/v1alpha1 +apiVersion: theodolite.com/v1 kind: execution metadata: name: theodolite-example-execution -benchmark: "uc1-kstreams" -load: - loadType: "NumSensors" - loadValues: - - 50000 -resources: - resourceType: "Instances" - resourceValues: - - 1 -slos: - - sloType: "lag trend" - threshold: 1000 - prometheusUrl: "http://localhost:32656" - externalSloUrl: "http://localhost:80/evaluate-slope" - offset: 0 - warmup: 0 -execution: - strategy: "LinearSearch" - duration: 60 - repetitions: 1 - delay: 30 # in seconds - restrictions: - - "LowerBound" -configOverrides: - - patcher: - type: "NodeSelectorPatcher" - resource: "uc1-load-generator-deployment.yaml" - variableName: "env" - value: "prod" - - patcher: - type: "NodeSelectorPatcher" - resource: "uc1-kstreams-deployment.yaml" - variableName: "env" - value: "prod" \ No newline at end of file +spec: + benchmark: uc1-kstreams + load: + loadType: "NumSensors" + loadValues: + - 50000 + resources: + resourceType: "Instances" + resourceValues: + - 1 + slos: + - sloType: "lag trend" + threshold: 1000 + prometheusUrl: "http://localhost:32656" + externalSloUrl: "http://localhost:80/evaluate-slope" + offset: 0 + warmup: 0 + execution: + strategy: "LinearSearch" + duration: 60 + repetitions: 1 + loadGenerationDelay: 30 # in seconds + restrictions: + - "LowerBound" + configOverrides: + - patcher: + type: "NodeSelectorPatcher" + resource: "uc1-load-generator-deployment.yaml" + container: "" + variableName: "env" + value: "prod" + - patcher: + type: "NodeSelectorPatcher" + resource: "uc1-kstreams-deployment.yaml" + container: "" + variableName: "env" + value: "prod" diff --git a/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml b/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml index 0bdb83c6201112a750bad41b81321b7a108a66fa..8e1189572ee993c37dd565fc62a66996654766f2 100644 --- a/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml +++ b/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml @@ -1,13 +1,129 @@ -apiVersion: apiextensions.k8s.io/v1beta1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: executions.theodolite.com spec: group: theodolite.com - version: v1alpha1 names: kind: execution plural: executions - scope: Namespaced - subresources: - status: {} \ No newline at end of file + shortNames: + - exec + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: ["spec"] + properties: + spec: + type: object + required: ["benchmark", "load", "resources", "slos", "execution", "configOverrides"] + properties: + name: + type: string + default: "" + benchmark: + type: string + load: # definition of the load dimension + type: object + required: ["loadType", "loadValues"] + properties: + loadType: + type: string + loadValues: + type: array + items: + type: integer + resources: # definition of the resource dimension + type: object + required: ["resourceType", "resourceValues"] + properties: + resourceType: + type: string + resourceValues: + type: array + items: + type: integer + slos: # def of service level objectives + type: array + items: + type: object + required: ["sloType", "threshold", "prometheusUrl", "externalSloUrl", "offset", "warmup"] + properties: + sloType: + type: string + threshold: + type: integer + prometheusUrl: + type: string + externalSloUrl: + type: string + offset: + type: integer + warmup: + type: integer + execution: # def execution config + type: object + required: ["strategy", "duration", "repetitions", "restrictions"] + properties: + strategy: + type: string + duration: + type: integer + repetitions: + type: integer + loadGenerationDelay: + type: integer + restrictions: + type: array + items: + type: string + configOverrides: + type: array + items: + type: object + properties: + patcher: + type: object + properties: + type: + type: string + default: "" + resource: + type: string + default: "" + container: + type: string + default: "" + variableName: + type: string + default: "" + value: + type: string + status: + type: object + properties: + executionState: + description: "" + type: string + executionDuration: + description: "Duration of the execution in seconds" + type: string + additionalPrinterColumns: + - name: STATUS + type: string + description: State of the execution + jsonPath: .status.executionState + - name: Duration + type: string + description: Duration of the execution + jsonPath: .status.executionDuration + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + subresources: + status: {} + scope: Namespaced \ No newline at end of file