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/execution/helm/values.yaml b/execution/helm/values.yaml index 5a34ab24fa7b8942fce01dd7b7fd38baab6aa874..d245e37e6f50d3874a1a5c06bd0156009777a999 100644 --- a/execution/helm/values.yaml +++ b/execution/helm/values.yaml @@ -32,6 +32,9 @@ grafana: org_role: Admin # Role for unauthenticated users, other valid values are `Viewer`, `Editor` and `Admin` users: default_theme: light + #dashboards: # the following doesn't work but is planed + # Path to the default home dashboard. If this value is empty, then Grafana uses StaticRootPath + "dashboards/home.json" + #default_home_dashboard_path: "/tmp/dashboards/k8s-dashboard.json" ## Sidecars that collect the configmaps with specified label and stores the included files them into the respective folders ## Requires at least Grafana 5 to work and can't be used together with parameters dashboardProviders, datasources and dashboards sidecar: diff --git a/theodolite-quarkus/build.gradle b/theodolite-quarkus/build.gradle index 8b61bf506dd241c5b0d4e75b1357ef3fd5966135..8c0a13d1f24cfde01c8604285c49d3f48fe6a3b7 100644 --- a/theodolite-quarkus/build.gradle +++ b/theodolite-quarkus/build.gradle @@ -25,9 +25,12 @@ dependencies { implementation 'io.quarkus:quarkus-kubernetes-client' implementation 'org.apache.kafka:kafka-clients:2.7.0' implementation 'khttp:khttp:1.0.0' + compile 'junit:junit:4.12' + testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' + testImplementation 'org.junit-pioneer:junit-pioneer:1.4.0' } group 'theodolite' 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/evaluation/AnalysisExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt index 59a793bd7c6d2897fc715c58deda54c178d160f4..ef4d371173c7099eb091f90cddbe26d31e6522be 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt @@ -2,6 +2,7 @@ package theodolite.evaluation import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution +import theodolite.util.IOHandler import theodolite.util.LoadDimension import theodolite.util.Resource import java.text.Normalizer @@ -36,24 +37,25 @@ class AnalysisExecutor( */ fun analyze(load: LoadDimension, res: Resource, executionIntervals: List<Pair<Instant, Instant>>): Boolean { var result = false - val exporter = CsvExporter() var repetitionCounter = 1 try { - var resultsFolder: String = System.getenv("RESULTS_FOLDER") ?: "" - if (resultsFolder.isNotEmpty()){ - resultsFolder += "/" - } + val ioHandler = IOHandler() + val resultsFolder: String = ioHandler.getResultFolderURL() + val fileURL = "${resultsFolder}exp${executionId}_${load.get()}_${res.get()}_${slo.sloType.toSlug()}" + val prometheusData = executionIntervals .map { interval -> fetcher.fetchMetric( start = interval.first, end = interval.second, query = "sum by(group)(kafka_consumergroup_group_lag >= 0)") } - val fileName= "${resultsFolder}exp${executionId}_${load.get()}_${res.get()}_${slo.sloType.toSlug()}" prometheusData.forEach{ data -> - exporter.toCsv(name = "${fileName}_${repetitionCounter++}", prom = data) } - + ioHandler.writeToCSVFile( + fileURL = "${fileURL}_${repetitionCounter++}", + data = data.getResultAsList(), + columns = listOf("group", "timestamp", "value")) + } val sloChecker = SloCheckerFactory().create( sloType = slo.sloType, diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/CsvExporter.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/CsvExporter.kt deleted file mode 100644 index 970d1487014bfbddf7391e381d27ad5dbb246a7d..0000000000000000000000000000000000000000 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/CsvExporter.kt +++ /dev/null @@ -1,53 +0,0 @@ -package theodolite.evaluation - -import mu.KotlinLogging -import theodolite.util.PrometheusResponse -import java.io.File -import java.io.PrintWriter -import java.util.* - -private val logger = KotlinLogging.logger {} - -/** - * Used to document the data received from prometheus for additional offline analysis. - */ -class CsvExporter { - - /** - * Uses the [PrintWriter] to transform a [PrometheusResponse] to a CSV file. - * @param name of the file. - * @param prom Response that is documented. - * - */ - fun toCsv(name: String, prom: PrometheusResponse) { - val responseArray = promResponseToList(prom) - val csvOutputFile = File("$name.csv") - - PrintWriter(csvOutputFile).use { pw -> - pw.println(listOf("group", "timestamp", "value").joinToString(separator=",")) - responseArray.forEach { - pw.println(it.joinToString(separator=",")) - } - } - logger.info { "Wrote CSV file: $name to ${csvOutputFile.absolutePath}." } - } - - /** - * Converts a [PrometheusResponse] into a [List] of [List]s of [String]s - */ - private fun promResponseToList(prom: PrometheusResponse): List<List<String>> { - val name = prom.data?.result?.get(0)?.metric?.group.toString() - val values = prom.data?.result?.get(0)?.values - val dataList = mutableListOf<List<String>>() - - if (values != null) { - for (maybeValuePair in values) { - val valuePair = maybeValuePair as List<*> - val timestamp = (valuePair[0] as Double).toLong().toString() - val value = valuePair[1].toString() - dataList.add(listOf(name, timestamp, value)) - } - } - return Collections.unmodifiableList(dataList) - } -} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt index ded4943c14ff8250f3dcad5279ff670af5fd7fd3..d38b50b70c63c90e6bbb618386e0ed897087e6f1 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt @@ -1,22 +1,13 @@ package theodolite.execution -import com.google.gson.GsonBuilder import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution import theodolite.benchmark.KubernetesBenchmark import theodolite.patcher.PatcherDefinitionFactory import theodolite.strategies.StrategyFactory import theodolite.strategies.searchstrategy.CompositeStrategy -import theodolite.util.Config -import theodolite.util.LoadDimension -import theodolite.util.Resource -import theodolite.util.Results +import theodolite.util.* import java.io.File -import java.io.PrintWriter -import java.lang.IllegalArgumentException -import java.lang.Thread.sleep -import java.nio.file.Files -import java.nio.file.Path import java.time.Duration @@ -104,34 +95,16 @@ class TheodoliteExecutor( return this.kubernetesBenchmark } - private fun getResultFolderString(): String { - var resultsFolder: String = System.getenv("RESULTS_FOLDER") ?: "" - val createResultsFolder = System.getenv("CREATE_RESULTS_FOLDER") ?: "false" - - if (resultsFolder != ""){ - logger.info { "RESULT_FOLDER: $resultsFolder" } - val directory = File(resultsFolder) - if (!directory.exists()) { - logger.error { "Folder $resultsFolder does not exist" } - if (createResultsFolder.toBoolean()) { - directory.mkdirs() - } else { - throw IllegalArgumentException("Result folder not found") - } - } - resultsFolder += "/" - } - return resultsFolder - } - /** * Run all experiments which are specified in the corresponding * execution and benchmark objects. */ fun run() { - val resultsFolder = getResultFolderString() - storeAsFile(this.config, "$resultsFolder${this.config.executionId}-execution-configuration") - storeAsFile(kubernetesBenchmark, "$resultsFolder${this.config.executionId}-benchmark-configuration") + val ioHandler = IOHandler() + val resultsFolder = ioHandler.getResultFolderURL() + this.config.executionId = getAndIncrementExecutionID(resultsFolder+"expID.txt") + ioHandler.writeToJSONFile(this.config, "$resultsFolder${this.config.executionId}-execution-configuration") + ioHandler.writeToJSONFile(kubernetesBenchmark, "$resultsFolder${this.config.executionId}-benchmark-configuration") val config = buildConfig() // execute benchmarks for each load @@ -140,14 +113,17 @@ class TheodoliteExecutor( config.compositeStrategy.findSuitableResource(load, config.resources) } } - storeAsFile(config.compositeStrategy.benchmarkExecutor.results, "$resultsFolder${this.config.executionId}-result") + ioHandler.writeToJSONFile(config.compositeStrategy.benchmarkExecutor.results, "$resultsFolder${this.config.executionId}-result") } - private fun <T> storeAsFile(saveObject: T, filename: String) { - val gson = GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create() - - PrintWriter(filename).use { pw -> - pw.println(gson.toJson(saveObject)) - } + private fun getAndIncrementExecutionID(fileURL: String): Int { + val ioHandler = IOHandler() + var executionID = 0 + if (File(fileURL).exists()) { + executionID = ioHandler.readFileAsString(fileURL).toInt() + 1 + } + ioHandler.writeStringToTextFile(fileURL, (executionID).toString()) + return executionID } + } 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 0e889e0393e17fd65f4d8f12c5b95a3dbed7f593..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,122 +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) - var executionID = AtomicInteger(0) - /** + * * 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) { - execution.executionId = executionID.getAndSet(executionID.get() + 1) - 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 0acfc52889e8f922f4c9386422e9f9cc90836d99..0d55b0c1c1c76dba226d34554e0d96a3df77c1c3 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,13 @@ package theodolite.execution.operator import io.fabric8.kubernetes.client.DefaultKubernetesClient +import io.fabric8.kubernetes.client.NamespacedKubernetesClient +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 +16,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 {} @@ -28,7 +28,7 @@ private val logger = KotlinLogging.logger {} */ class TheodoliteOperator { private val namespace = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE - val client = DefaultKubernetesClient().inNamespace(namespace) + val client: NamespacedKubernetesClient = DefaultKubernetesClient().inNamespace(namespace) fun start() { @@ -44,40 +44,79 @@ class TheodoliteOperator { */ private fun startOperator() { logger.info { "Using $namespace as 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, + BenchmarkCRD::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 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 appResource = System.getenv("THEODOLITE_APP_RESOURCES") ?: "./config" - val controller = TheodoliteController(client = client, executionContext = executionContext, path = appResource) + val benchmarkCRDClient: MixedOperation< + BenchmarkCRD, + KubernetesBenchmarkList, + DoneableBenchmark, + Resource<BenchmarkCRD, DoneableBenchmark>> + = client.customResources( + benchmarkContext, + BenchmarkCRD::class.java, + KubernetesBenchmarkList::class.java, + DoneableBenchmark::class.java) - 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 - ) + 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() - informerExecution.addEventHandler(ExecutionHandler(controller)) - informerBenchmark.addEventHandler(BenchmarkEventHandler(controller)) - informerFactory.startAllRegisteredInformers() + informerFactory.startAllRegisteredInformers() + controller.run() - 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/IOHandler.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/IOHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d379fcf0543257edafd2e45383a02ba0254563d --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/IOHandler.kt @@ -0,0 +1,94 @@ +package theodolite.util + +import com.google.gson.GsonBuilder +import mu.KotlinLogging +import java.io.File +import java.io.PrintWriter + +private val logger = KotlinLogging.logger {} + +/** + * The IOHandler handles most common I/O operations within the Theodolite framework + */ +class IOHandler { + + /** + * The location in which Theodolite store result and configuration file are depends on + * the values of the environment variables `RESULT_FOLDER` and `CREATE_RESULTS_FOLDER` + * + * @return the URL of the result folder + */ + fun getResultFolderURL(): String { + var resultsFolder: String = System.getenv("RESULTS_FOLDER") ?: "" + val createResultsFolder = System.getenv("CREATE_RESULTS_FOLDER") ?: "false" + + if (resultsFolder != ""){ + logger.info { "RESULT_FOLDER: $resultsFolder" } + val directory = File(resultsFolder) + if (!directory.exists()) { + logger.error { "Folder $resultsFolder does not exist" } + if (createResultsFolder.toBoolean()) { + directory.mkdirs() + } else { + throw IllegalArgumentException("Result folder not found") + } + } + resultsFolder += "/" + } + return resultsFolder + } + + /** + * Read a file as String + * + * @param fileURL the URL of the file + * @return The content of the file as String + */ + fun readFileAsString(fileURL: String): String { + return File(fileURL).inputStream().readBytes().toString(Charsets.UTF_8).trim() + } + + /** + * Creates a JSON string of the given object and store them to file + * + * @param T class of the object to save + * @param objectToSave object which should be saved as file + * @param fileURL the URL of the file + */ + fun <T> writeToJSONFile(objectToSave: T, fileURL: String) { + val gson = GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create() + writeStringToTextFile(fileURL, gson.toJson(objectToSave)) + } + + /** + * Write to CSV file + * + * @param fileURL the URL of the file + * @param data the data to write in the file, as list of list, each subList corresponds to a row in the CSV file + * @param columns columns of the CSV file + */ + fun writeToCSVFile(fileURL: String, data: List<List<String>>, columns: List<String>) { + val outputFile = File("$fileURL.csv") + PrintWriter(outputFile).use { pw -> + pw.println(columns.joinToString(separator=",")) + data.forEach { + pw.println(it.joinToString(separator=",")) + } + } + logger.info { "Wrote CSV file: $fileURL to ${outputFile.absolutePath}." } + } + + /** + * Write to text file + * + * @param fileURL the URL of the file + * @param data the data to write in the file as String + */ + fun writeStringToTextFile(fileURL: String, data: String) { + val outputFile = File("$fileURL") + outputFile.printWriter().use { + it.println(data) + } + logger.info { "Wrote txt file: $fileURL to ${outputFile.absolutePath}." } + } +} \ No newline at end of file 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/kotlin/theodolite/util/PrometheusResponse.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt index d1d59c482e64fd14c4744d8fcd606f286da24fb4..846577387c425e920da1c2fca1f972c880e1540a 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt @@ -1,6 +1,7 @@ package theodolite.util import io.quarkus.runtime.annotations.RegisterForReflection +import java.util.* /** * This class corresponds to the JSON response format of a Prometheus @@ -17,6 +18,27 @@ data class PrometheusResponse( */ var data: PromData? = null ) +{ + /** + * Return the data of the PrometheusResponse as [List] of [List]s of [String]s + * The format of the returned list is: `[[ group, timestamp, value ], [ group, timestamp, value ], ... ]` + */ + fun getResultAsList(): List<List<String>> { + val group = data?.result?.get(0)?.metric?.group.toString() + val values = data?.result?.get(0)?.values + val result = mutableListOf<List<String>>() + + if (values != null) { + for (value in values) { + val valueList = value as List<*> + val timestamp = (valueList[0] as Double).toLong().toString() + val value = valueList[1].toString() + result.add(listOf(group, timestamp, value)) + } + } + return Collections.unmodifiableList(result) + } +} /** * Description of Prometheus data. @@ -56,4 +78,5 @@ data class PromResult( @RegisterForReflection data class PromMetric( var group: String? = null -) \ No newline at end of file +) + 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 diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt index 82e4bc5d77f3f35d217c56a377513c0e7d329170..4f4308a35df598dc4932a8ac89a6c07fb967e416 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt @@ -21,7 +21,7 @@ import theodolite.util.PatcherDefinition */ @QuarkusTest class ResourceLimitPatcherTest { - val testPath = "./src/main/resources/testYaml/" + val testPath = "./src/test/resources/" val loader = K8sResourceLoader(DefaultKubernetesClient().inNamespace("")) val patcherFactory = PatcherFactory() diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt index 3cd6b012f09c5471b1b011b5cd03e61a0fab1c4e..7214422705a64f507a196a4b5f9b334665407422 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt @@ -21,7 +21,7 @@ import theodolite.util.PatcherDefinition */ @QuarkusTest class ResourceRequestPatcherTest { - val testPath = "./src/main/resources/testYaml/" + val testPath = "./src/test/resources/" val loader = K8sResourceLoader(DefaultKubernetesClient().inNamespace("")) val patcherFactory = PatcherFactory() diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/util/IOHandlerTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/util/IOHandlerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6984c3935da3d26f95565b758547a98e0eeb155f --- /dev/null +++ b/theodolite-quarkus/src/test/kotlin/theodolite/util/IOHandlerTest.kt @@ -0,0 +1,134 @@ +package theodolite.util + +import com.google.gson.GsonBuilder +import io.quarkus.test.junit.QuarkusTest +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Rule +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.rules.TemporaryFolder +import org.junitpioneer.jupiter.ClearEnvironmentVariable +import org.junitpioneer.jupiter.SetEnvironmentVariable + + +const val FOLDER_URL = "Test-Folder" +@QuarkusTest +internal class IOHandlerTest { + + @Rule + private var temporaryFolder = TemporaryFolder() + + @Test + fun testWriteStringToText() { + temporaryFolder.create() + val testContent = "Test-File-Content" + val folder = temporaryFolder.newFolder(FOLDER_URL) + + IOHandler().writeStringToTextFile( + fileURL = "${folder.absolutePath}/test-file.txt", + data = testContent) + + assertEquals( + testContent, + IOHandler().readFileAsString("${folder.absolutePath}/test-file.txt") + ) + } + + @Test + fun testWriteToCSVFile() { + temporaryFolder.create() + val folder = temporaryFolder.newFolder(FOLDER_URL) + + val testContent = listOf( + listOf("apples","red"), + listOf("bananas","yellow"), + listOf("avocado","brown")) + val columns = listOf("Fruit", "Color") + + IOHandler().writeToCSVFile( + fileURL = "${folder.absolutePath}/test-file", + data = testContent, + columns = columns) + + var expected = "Fruit,Color\n" + testContent.forEach { expected += it[0] + "," + it[1] + "\n" } + + assertEquals( + expected.trim(), + IOHandler().readFileAsString("${folder.absolutePath}/test-file.csv") + ) + } + + @Test + fun testWriteToJSONFile() { + temporaryFolder.create() + val folder = temporaryFolder.newFolder(FOLDER_URL) + val testContent = Resource(0, emptyList()) + + IOHandler().writeToJSONFile( + fileURL = "${folder.absolutePath}/test-file.json", + objectToSave = testContent) + + val expected = GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create().toJson(testContent) + + assertEquals( + expected, + IOHandler().readFileAsString("${folder.absolutePath}/test-file.json") + ) + } + + // Test the function `getResultFolderString` + + @Test + @ClearEnvironmentVariable.ClearEnvironmentVariables( + ClearEnvironmentVariable(key = "RESULTS_FOLDER"), + ClearEnvironmentVariable(key = "CREATE_RESULTS_FOLDER") + ) + fun testGetResultFolderURL_emptyEnvironmentVars() { + assertEquals("",IOHandler().getResultFolderURL()) + } + + + @Test() + @SetEnvironmentVariable.SetEnvironmentVariables( + SetEnvironmentVariable(key = "RESULTS_FOLDER", value = "./src/test/resources"), + SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "false") + ) + fun testGetResultFolderURL_FolderExist() { + assertEquals("./src/test/resources/", IOHandler().getResultFolderURL()) + } + + @Test() + @SetEnvironmentVariable.SetEnvironmentVariables( + SetEnvironmentVariable(key = "RESULTS_FOLDER", value = "$FOLDER_URL-0"), + SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "false") + ) + fun testGetResultFolderURL_FolderNotExist() { + var exceptionWasThrown = false + try { + IOHandler().getResultFolderURL() + } catch (e: Exception){ + exceptionWasThrown = true + assertThat(e.toString(), containsString("Result folder not found")); + } + assertTrue(exceptionWasThrown) + } + + @Test() + @SetEnvironmentVariable.SetEnvironmentVariables( + SetEnvironmentVariable(key = "RESULTS_FOLDER", value = FOLDER_URL), + SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "true") + ) + fun testGetResultFolderURL_CreateFolderIfNotExist() { + assertEquals("$FOLDER_URL/", IOHandler().getResultFolderURL()) + } + + @Test() + @ClearEnvironmentVariable(key = "RESULTS_FOLDER" ) + @SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "true") + fun testGetResultFolderURL_CreateFolderButNoFolderGiven() { + assertEquals("", IOHandler().getResultFolderURL()) + } +} diff --git a/theodolite-quarkus/src/main/resources/testYaml/cpu-deployment.yaml b/theodolite-quarkus/src/test/resources/cpu-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/cpu-deployment.yaml rename to theodolite-quarkus/src/test/resources/cpu-deployment.yaml diff --git a/theodolite-quarkus/src/main/resources/testYaml/cpu-memory-deployment.yaml b/theodolite-quarkus/src/test/resources/cpu-memory-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/cpu-memory-deployment.yaml rename to theodolite-quarkus/src/test/resources/cpu-memory-deployment.yaml diff --git a/theodolite-quarkus/src/main/resources/testYaml/memory-deployment.yaml b/theodolite-quarkus/src/test/resources/memory-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/memory-deployment.yaml rename to theodolite-quarkus/src/test/resources/memory-deployment.yaml diff --git a/theodolite-quarkus/src/main/resources/testYaml/no-resources-deployment.yaml b/theodolite-quarkus/src/test/resources/no-resources-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/no-resources-deployment.yaml rename to theodolite-quarkus/src/test/resources/no-resources-deployment.yaml