diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 01630912c52ccc728870d72c33a8f02cdf7c2c7f..f2f1bd6263e917ff4c9c78f7d851b61cacc6c57f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -307,11 +307,23 @@ deploy-theodolite: # Theodolite SLO Checker: Lag Trend +test-slo-checker-lag-trend: + stage: test + image: python:3.7-slim + tags: + - exec-docker + script: + - cd slope-evaluator + - pip install -r requirements.txt + - cd app + - python -m unittest + deploy-slo-checker-lag-trend: stage: deploy extends: - .dind - needs: [] + needs: + - test-slo-checker-lag-trend script: - DOCKER_TAG_NAME=$(echo $CI_COMMIT_REF_SLUG- | sed 's/^master-$//') - docker build --pull -t theodolite-slo-checker-lag-trend slope-evaluator diff --git a/execution/helm/.gitignore b/execution/helm/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..80bf7fc709ac6d08e703fe9f24d7d5776e26830e --- /dev/null +++ b/execution/helm/.gitignore @@ -0,0 +1 @@ +charts \ No newline at end of file diff --git a/execution/helm/templates/grafana/dashboard-config-map.yaml b/execution/helm/templates/grafana/dashboard-config-map.yaml index 1125d7833cc62e78c049436f38b854d926e2a216..41365e5efefaddc92a9f2f25f867a9d895e4ca3d 100644 --- a/execution/helm/templates/grafana/dashboard-config-map.yaml +++ b/execution/helm/templates/grafana/dashboard-config-map.yaml @@ -253,7 +253,7 @@ data: "steppedLine": false, "targets": [ { - "expr": "sum by(group, topic) (kafka_consumergroup_group_lag > 0)", + "expr": "sum by(group, topic) (kafka_consumergroup_group_lag >= 0)", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{topic}}", @@ -436,7 +436,7 @@ data: "steppedLine": false, "targets": [ { - "expr": "sum by(group,topic) (kafka_consumergroup_group_offset > 0)", + "expr": "sum by(group,topic) (kafka_consumergroup_group_offset >= 0)", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{topic}}", @@ -527,7 +527,7 @@ data: "steppedLine": false, "targets": [ { - "expr": "count by(group,topic) (kafka_consumergroup_group_offset > 0)", + "expr": "count by(group,topic) (kafka_consumergroup_group_offset >= 0)", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{topic}}", @@ -892,7 +892,7 @@ data: "steppedLine": false, "targets": [ { - "expr": "sum by(group) (kafka_consumergroup_group_lag > 0)", + "expr": "sum by(group) (kafka_consumergroup_group_lag >= 0)", "format": "time_series", "intervalFactor": 1, "legendFormat": "total lag", diff --git a/execution/helm/templates/theodolite/crd-benchmark.yaml b/execution/helm/templates/theodolite/crd-benchmark.yaml index 9d7468b490fa2f2a6cf829bdcafab8c4bd6fc5bf..319eee18cee87d12cd7051e5fc3414f99c8d8a30 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: "" + config: + type: object + additionalProperties: true + x-kubernetes-map-type: "granular" + 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: "" + config: + type: object + additionalProperties: true + x-kubernetes-map-type: "granular" + 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..41ba1b0b5b19b53d9b7caec48241285edf4ba396 100644 --- a/execution/helm/templates/theodolite/crd-execution.yaml +++ b/execution/helm/templates/theodolite/crd-execution.yaml @@ -1,15 +1,129 @@ -{{- 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 - scope: Namespaced - subresources: - status: {} -{{- end }} \ 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: "" + config: + type: object + additionalProperties: true + x-kubernetes-map-type: "granular" + 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/execution/helm/templates/theodolite/role.yaml b/execution/helm/templates/theodolite/role.yaml index ee699a5b861c64e355ca2ba44330a7d600756b77..630a84c0491328a9bb1900c1f99a8f4643020e17 100644 --- a/execution/helm/templates/theodolite/role.yaml +++ b/execution/helm/templates/theodolite/role.yaml @@ -56,5 +56,14 @@ rules: - watch - update - patch + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - delete + - get + - create + - update {{- end }} {{- end }} \ No newline at end of file diff --git a/execution/helm/values.yaml b/execution/helm/values.yaml index b4f2ffdea14c4c98a5b23b8f15056880a190e1a4..d245e37e6f50d3874a1a5c06bd0156009777a999 100644 --- a/execution/helm/values.yaml +++ b/execution/helm/values.yaml @@ -25,8 +25,16 @@ grafana: adminUser: admin adminPassword: admin grafana.ini: + #org_name: Theodolite + auth.anonymous: + # enable anonymous access + enabled: true + 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/execution/infrastructure/kubernetes/rbac/role.yaml b/execution/infrastructure/kubernetes/rbac/role.yaml index a21fd554f0f56b3955e9a9b6cf4bf95442b5d7af..e45814eedacd30715075f66e520f9f9e6bfc42ad 100644 --- a/execution/infrastructure/kubernetes/rbac/role.yaml +++ b/execution/infrastructure/kubernetes/rbac/role.yaml @@ -52,4 +52,12 @@ rules: - watch - update - patch - + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - delete + - get + - create + - update \ No newline at end of file diff --git a/slope-evaluator/README.md b/slope-evaluator/README.md index 12e8a61783532efce9e8722f500cf5ff880bace7..cd9e6820ed46452ce44d57d0c7e5cd5ae05e5a3b 100644 --- a/slope-evaluator/README.md +++ b/slope-evaluator/README.md @@ -5,7 +5,7 @@ For development: ```sh -uvicorn main:app --reload +uvicorn main:app --reload # run this command inside the app/ folder ``` ## Build the docker image: @@ -32,7 +32,7 @@ The running webserver provides a REST API with the following route: * /evaluate-slope * Method: POST * Body: - * total_lag + * total_lags * threshold * warmup @@ -40,14 +40,16 @@ The body of the request must be a JSON string that satisfies the following condi * **total_lag**: This property is based on the [Range Vector type](https://www.prometheus.io/docs/prometheus/latest/querying/api/#range-vectors) from Prometheus and must have the following JSON structure: ``` - { - "metric": { - "group": "<label_value>" - }, - "values": [ - [ - <unix_timestamp>, - "<sample_value>" + { + [ + "metric": { + "group": "<label_value>" + }, + "values": [ + [ + <unix_timestamp>, + "<sample_value>" + ] ] ] } diff --git a/slope-evaluator/app/main.py b/slope-evaluator/app/main.py index 5995a1175f781c4eb51ab3d3083a665fbc02d6fd..6f6788f0ca84b7710be5b509ca4f0641047e963d 100644 --- a/slope-evaluator/app/main.py +++ b/slope-evaluator/app/main.py @@ -5,6 +5,7 @@ import os import pandas as pd import json import sys +from statistics import median app = FastAPI() @@ -20,7 +21,7 @@ elif os.getenv('LOG_LEVEL') == 'WARNING': elif os.getenv('LOG_LEVEL') == 'DEBUG': logger.setLevel(logging.DEBUG) -def execute(results, threshold, warmup): +def calculate_slope_trend(results, warmup): d = [] for result in results: group = result['metric']['group'] @@ -39,13 +40,16 @@ def execute(results, threshold, warmup): logger.error('Mark this subexperiment as not successful and continue benchmark.') return False - result = trend_slope < threshold - logger.info("Computed lag trend slope is '%s'. Result is: %s", trend_slope, result) - return result + logger.info("Computed lag trend slope is '%s'", trend_slope) + return trend_slope + +def check_service_level_objective(results, threshold): + return median(results) < threshold @app.post("/evaluate-slope",response_model=bool) async def evaluate_slope(request: Request): data = json.loads(await request.body()) - return execute(data['total_lag'], data['threshold'], data['warmup']) + results = [calculate_slope_trend(total_lag, data['warmup']) for total_lag in data['total_lags']] + return check_service_level_objective(results=results, threshold=data["threshold"]) -logger.info("Slope evaluator is online") \ No newline at end of file +logger.info("SLO evaluator is online") \ No newline at end of file diff --git a/slope-evaluator/app/test.py b/slope-evaluator/app/test.py new file mode 100644 index 0000000000000000000000000000000000000000..9b165ea479bb9a552edaba7692df4fd4ef3f4ab4 --- /dev/null +++ b/slope-evaluator/app/test.py @@ -0,0 +1,30 @@ +import unittest +from main import app, check_service_level_objective +import json +from fastapi.testclient import TestClient + +class TestSloEvaluation(unittest.TestCase): + client = TestClient(app) + + def test_1_rep(self): + with open('../resources/test-1-rep-success.json') as json_file: + data = json.load(json_file) + response = self.client.post("/evaluate-slope", json=data) + self.assertEquals(response.json(), True) + + def test_3_rep(self): + with open('../resources/test-3-rep-success.json') as json_file: + data = json.load(json_file) + response = self.client.post("/evaluate-slope", json=data) + self.assertEquals(response.json(), True) + + def test_check_service_level_objective(self): + list = [1,2,3,4] + self.assertEquals(check_service_level_objective(list, 2), False) + self.assertEquals(check_service_level_objective(list, 3), True) + list = [1,2,3,4,5] + self.assertEquals(check_service_level_objective(list, 2), False) + self.assertEquals(check_service_level_objective(list, 4), True) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/slope-evaluator/requirements.txt b/slope-evaluator/requirements.txt index ca77b6c891136b1388aaf56c5ae269d6ee4b5729..670815f35b18361951a2fa7b2142eee6bc86b01d 100644 --- a/slope-evaluator/requirements.txt +++ b/slope-evaluator/requirements.txt @@ -1,3 +1,5 @@ fastapi==0.55.1 scikit-learn==0.20.3 pandas==1.0.3 +uvicorn +requests diff --git a/slope-evaluator/resources/test-1-rep-success.json b/slope-evaluator/resources/test-1-rep-success.json new file mode 100644 index 0000000000000000000000000000000000000000..9e315c707be7b2a874c58fcb1093aa86f7676560 --- /dev/null +++ b/slope-evaluator/resources/test-1-rep-success.json @@ -0,0 +1,139 @@ +{ + "total_lags": [ + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621008960827E9, + "234" + ], + [ + 1.621008965827E9, + "234" + ], + [ + 1.621008970827E9, + "234" + ], + [ + 1.621008975827E9, + "719" + ], + [ + 1.621008980827E9, + "719" + ], + [ + 1.621008985827E9, + "719" + ], + [ + 1.621008990827E9, + "1026" + ], + [ + 1.621008995827E9, + "1026" + ], + [ + 1.621009000827E9, + "1026" + ], + [ + 1.621009005827E9, + "534" + ], + [ + 1.621009010827E9, + "534" + ], + [ + 1.621009015827E9, + "534" + ], + [ + 1.621009020827E9, + "943" + ], + [ + 1.621009025827E9, + "943" + ], + [ + 1.621009030827E9, + "943" + ], + [ + 1.621009035827E9, + "66" + ], + [ + 1.621009040827E9, + "66" + ], + [ + 1.621009045827E9, + "66" + ], + [ + 1.621009050827E9, + "841" + ], + [ + 1.621009055827E9, + "841" + ], + [ + 1.621009060827E9, + "841" + ], + [ + 1.621009065827E9, + "405" + ], + [ + 1.621009070827E9, + "405" + ], + [ + 1.621009075827E9, + "405" + ], + [ + 1.621009080827E9, + "201" + ], + [ + 1.621009085827E9, + "201" + ], + [ + 1.621009090827E9, + "201" + ], + [ + 1.621009095827E9, + "227" + ], + [ + 1.621009100827E9, + "227" + ], + [ + 1.621009105827E9, + "227" + ], + [ + 1.621009110827E9, + "943" + ] + ] + } + ] + ], + "threshold": 2000, + "warmup": 0 +} \ No newline at end of file diff --git a/slope-evaluator/resources/test-3-rep-success.json b/slope-evaluator/resources/test-3-rep-success.json new file mode 100644 index 0000000000000000000000000000000000000000..485966cba40f01e4a646e626914510ba49b707bc --- /dev/null +++ b/slope-evaluator/resources/test-3-rep-success.json @@ -0,0 +1,289 @@ +{ + "total_lags": [ + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621012384232E9, + "6073" + ], + [ + 1.621012389232E9, + "6073" + ], + [ + 1.621012394232E9, + "6073" + ], + [ + 1.621012399232E9, + "227" + ], + [ + 1.621012404232E9, + "227" + ], + [ + 1.621012409232E9, + "227" + ], + [ + 1.621012414232E9, + "987" + ], + [ + 1.621012419232E9, + "987" + ], + [ + 1.621012424232E9, + "987" + ], + [ + 1.621012429232E9, + "100" + ], + [ + 1.621012434232E9, + "100" + ], + [ + 1.621012439232E9, + "100" + ], + [ + 1.621012444232E9, + "959" + ], + [ + 1.621012449232E9, + "959" + ], + [ + 1.621012454232E9, + "959" + ], + [ + 1.621012459232E9, + "625" + ], + [ + 1.621012464232E9, + "625" + ], + [ + 1.621012469232E9, + "625" + ], + [ + 1.621012474232E9, + "683" + ], + [ + 1.621012479232E9, + "683" + ], + [ + 1.621012484232E9, + "683" + ], + [ + 1.621012489232E9, + "156" + ] + ] + } + ], + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621012545211E9, + "446" + ], + [ + 1.621012550211E9, + "446" + ], + [ + 1.621012555211E9, + "446" + ], + [ + 1.621012560211E9, + "801" + ], + [ + 1.621012565211E9, + "801" + ], + [ + 1.621012570211E9, + "801" + ], + [ + 1.621012575211E9, + "773" + ], + [ + 1.621012580211E9, + "773" + ], + [ + 1.621012585211E9, + "773" + ], + [ + 1.621012590211E9, + "509" + ], + [ + 1.621012595211E9, + "509" + ], + [ + 1.621012600211E9, + "509" + ], + [ + 1.621012605211E9, + "736" + ], + [ + 1.621012610211E9, + "736" + ], + [ + 1.621012615211E9, + "736" + ], + [ + 1.621012620211E9, + "903" + ], + [ + 1.621012625211E9, + "903" + ], + [ + 1.621012630211E9, + "903" + ], + [ + 1.621012635211E9, + "512" + ], + [ + 1.621012640211E9, + "512" + ], + [ + 1.621012645211E9, + "512" + ] + ] + } + ], + [ + { + "metric": { + "group": "theodolite-uc1-application-0.0.1" + }, + "values": [ + [ + 1.621012700748E9, + "6484" + ], + [ + 1.621012705748E9, + "6484" + ], + [ + 1.621012710748E9, + "6484" + ], + [ + 1.621012715748E9, + "505" + ], + [ + 1.621012720748E9, + "505" + ], + [ + 1.621012725748E9, + "505" + ], + [ + 1.621012730748E9, + "103" + ], + [ + 1.621012735748E9, + "103" + ], + [ + 1.621012740748E9, + "103" + ], + [ + 1.621012745748E9, + "201" + ], + [ + 1.621012750748E9, + "201" + ], + [ + 1.621012755748E9, + "201" + ], + [ + 1.621012760748E9, + "965" + ], + [ + 1.621012765748E9, + "965" + ], + [ + 1.621012770748E9, + "965" + ], + [ + 1.621012775748E9, + "876" + ], + [ + 1.621012780748E9, + "876" + ], + [ + 1.621012785748E9, + "876" + ], + [ + 1.621012790748E9, + "380" + ], + [ + 1.621012795748E9, + "380" + ], + [ + 1.621012800748E9, + "380" + ] + ] + } + ] + ], + "threshold": 2000, + "warmup": 0 +} \ No newline at end of file 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/README.md b/theodolite-quarkus/config/README.md index 4f53d87ba9ac12099091faa315913cf93ed62bb7..23337d77375ebba8f624e7a11f714502fe3d5e67 100644 --- a/theodolite-quarkus/config/README.md +++ b/theodolite-quarkus/config/README.md @@ -19,14 +19,16 @@ resourceTypes: patchers: - type: String resources: String - <Patcher Arguments> ... + properties: + <Patcher Arguments> ... ... loadTypes: - typeName: String patchers: - type: String resources: String - <Patcher Arguments> ... + properties: + <Patcher Arguments> ... ... kafkaConfig: bootstrapServer: String @@ -49,7 +51,7 @@ The properties have the following definitions: * **patchers**: List of [patchers](#Patchers) used to scale this resource type. Each patcher has the following structure: * **type**: Type of the [patcher](#Patchers). The concrete types can be looked up in the list of [patchers](#Patchers). * **resources**: Specifies the Kubernetes resource to be patched. - * **Patcher Arguments**: (Optional) Patcher specific additional arguments. + * **properties**: *Patcher Arguments*: (Optional) Patcher specific additional arguments. * **loadTypes**: A list of load types that can be scaled for this *benchmark*. For each load type the concrete values are defined in the *execution* object. Each load type has the following structure: * **typeName**: Name of the load type. * **patchers**: List of patchers used to scale * **resourceTypes**: A list of resource types that can be scaled for this *benchmark*. For each resource type the concrete values are defined in the *execution* resource object.Each resource type has the following structure: @@ -57,7 +59,7 @@ The properties have the following definitions: * **patchers**: List of patchers used to scale this resource type. Each patcher has the following structure: * **type**: Type of the Patcher. The concrete types can be looked up in the list of patchers. * **resources**: Specifies the Kubernetes resource to be patched. - * **Patcher Arguments**: (Optional) Patcher specific additional arguments. + * **properties**: *Patcher Arguments*: (Optional) Patcher specific additional arguments as Map<String, String>. * **kafkaConfig**: Contains the Kafka configuration. * **bootstrapServers**: The bootstrap servers connection string. * **topics**: List of topics to be created for each [experiment](#Experiment). Alternative theodolite offers the possibility to remove certain topics after each experiment. @@ -105,7 +107,8 @@ configurationOverrides: - patcher: type: String resource: String - <Patcher Arguments> ... + properties: + <Patcher Arguments> ... ... ``` @@ -136,7 +139,7 @@ The properties have the following definitions: * **patcher**: Patcher used to patch a resource. Each patcher has the following structure: * **type**: Type of the Patcher. The concrete types can be looked up in the list of patchers. * **resources**: Specifies the Kubernetes resource to be patched. - * **Patcher Arguments**: (Optional) Patcher specific additional arguments. + * **properties**: *Patcher Arguments*: (Optional) Patcher specific additional arguments. ## Patchers @@ -159,19 +162,22 @@ The properties have the following definitions: * **EnvVarPatcher**: Allows to modify the value of an environment variable for a container in a kubernetes deployment. * **type**: "EnvVarPatcher" * **resource**: "uc1-load-generator-deployment.yaml" - * **container**: "workload-generator" - * **variableName**: "NUM_SENSORS" + * **properties**: + * container: "workload-generator" + * variableName: "NUM_SENSORS" * **NodeSelectorPatcher**: Changes the node selection field in kubernetes resources. * **type**: "NodeSelectorPatcher" * **resource**: "uc1-load-generator-deployment.yaml" - * **variableName**: "env" + * **properties**: + * variableName: "env" * **value**: "prod" * **ResourceLimitPatcher**: Changes the resource limit for a kubernetes resource. * **resource**: "uc1-kstreams-deployment.yaml" - * **container**: "uc-application" - * **variableName**: "cpu" or "memory" + * **properties**: + * container: "uc-application" + * variableName: "cpu" or "memory" * **value**:"1000m" or "2Gi" * **SchedulerNamePatcher**: Changes the sheduler for kubernetes resources. @@ -182,7 +188,8 @@ The properties have the following definitions: * **ImagePatcher**: Changes the image of a kubernetes resource. Currently not fully implemented. * **type**: "ImagePatcher" * **resource**: "uc1-kstreams-deployment.yaml" - * **container**: "uc-application" + * **properties**: + * container: "uc-application" * **value**: "dockerhubrepo/imagename" diff --git a/theodolite-quarkus/config/example-benchmark-yaml-resource.yaml b/theodolite-quarkus/config/example-benchmark-yaml-resource.yaml index cdc2122d9e6b568f1a75b0d55eff8a0af6450983..623df71121b11b670584a83e153b4dd2962525e7 100644 --- a/theodolite-quarkus/config/example-benchmark-yaml-resource.yaml +++ b/theodolite-quarkus/config/example-benchmark-yaml-resource.yaml @@ -17,10 +17,13 @@ loadTypes: patchers: - type: "EnvVarPatcher" resource: "uc1-load-generator-deployment.yaml" - container: "workload-generator" - variableName: "NUM_SENSORS" + properties: + variableName: "NUM_SENSORS" + container: "workload-generator" - type: "NumSensorsLoadGeneratorReplicaPatcher" resource: "uc1-load-generator-deployment.yaml" + properties: + loadGenMaxRecords: "15000" kafkaConfig: bootstrapServer: "localhost:31290" topics: diff --git a/theodolite-quarkus/config/example-execution-yaml-resource.yaml b/theodolite-quarkus/config/example-execution-yaml-resource.yaml index e0b327a022099410fea8028fb5d37fee5672a857..e46a6cf417442b851650fe9699f73e1367dcc794 100644 --- a/theodolite-quarkus/config/example-execution-yaml-resource.yaml +++ b/theodolite-quarkus/config/example-execution-yaml-resource.yaml @@ -20,29 +20,33 @@ execution: loadGenerationDelay: 30 # in seconds, optional field, default is 0 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" +configOverrides: + - patcher: + type: "NodeSelectorPatcher" + resource: "uc1-load-generator-deployment.yaml" + properties: + variableName: "env" + value: "prod" + - patcher: + type: "NodeSelectorPatcher" + resource: "uc1-kstreams-deployment.yaml" + properties: + variableName: "env" + value: "prod" + - patcher: + type: "ResourceLimitPatcher" + resource: "uc1-kstreams-deployment.yaml" + properties: + container: "uc-application" + limitedResource: "cpu" + value: "1000m" + - patcher: + type: "ResourceLimitPatcher" + resource: "uc1-kstreams-deployment.yaml" + properties: + container: "uc-application" + limitedResource: "memory" + value: "2Gi" # - patcher: # type: "SchedulerNamePatcher" # resource: "uc1-kstreams-deployment.yaml" diff --git a/theodolite-quarkus/config/example-operator-benchmark.yaml b/theodolite-quarkus/config/example-operator-benchmark.yaml index 3ed5218d8a8988b130e8d549c120cbca7329ffe3..8f9af3e955fe18de26326b0094578a71517a5aef 100644 --- a/theodolite-quarkus/config/example-operator-benchmark.yaml +++ b/theodolite-quarkus/config/example-operator-benchmark.yaml @@ -1,35 +1,38 @@ -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" + properties: + variableName: "NUM_SENSORS" + container: "workload-generator" + - type: "NumSensorsLoadGeneratorReplicaPatcher" + resource: "uc1-load-generator-deployment.yaml" + properties: + loadGenMaxRecords: "15000" + 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..9b2a1facbd8be3411407dfcf3cad39fd9f3de6b6 100644 --- a/theodolite-quarkus/config/example-operator-execution.yaml +++ b/theodolite-quarkus/config/example-operator-execution.yaml @@ -1,53 +1,57 @@ -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 + loadGenerationDelay: 30 # in seconds + restrictions: + - "LowerBound" + configOverrides: + # - patcher: + # type: "NodeSelectorPatcher" + # resource: "uc1-load-generator-deployment.yaml" + # properties: + # variableName: "env" + # value: "prod" + # - patcher: + # type: "NodeSelectorPatcher" + # resource: "uc1-kstreams-deployment.yaml" + # properties: + # variableName: "env" + # value: "prod" + # - patcher: + # type: "ResourceLimitPatcher" + # resource: "uc1-kstreams-deployment.yaml" + # properties: + # container: "uc-application" + # limitedResource: "cpu" + # value: "1000m" + # - patcher: + # type: "ResourceLimitPatcher" + # resource: "uc1-kstreams-deployment.yaml" + # properties: + # container: "uc-application" + # limitedResource: "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 95af36d1b14eb4d664ffd0d3d463df17fd8fb534..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 @@ -22,6 +25,7 @@ interface Benchmark { load: LoadDimension, res: Resource, configurationOverrides: List<ConfigurationOverride?>, - delay: Long + loadGenerationDelay: Long, + afterTeardownDelay: Long ): BenchmarkDeployment } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/BenchmarkExecution.kt index 40ab663e179735be61356947df7c37e9edd0a2ea..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] @@ -48,6 +46,7 @@ class BenchmarkExecution : CustomResource(), Namespaced { var repetitions by Delegates.notNull<Int>() lateinit var restrictions: List<String> var loadGenerationDelay = 0L + var afterTeardownDelay = 5L } /** 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 b5316a3df78e7291569ec1d36e561a5445b6f86d..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. @@ -71,7 +74,8 @@ class KubernetesBenchmark : Benchmark, CustomResource(), Namespaced { load: LoadDimension, res: Resource, configurationOverrides: List<ConfigurationOverride?>, - loadGenerationDelay: Long + loadGenerationDelay: Long, + afterTeardownDelay: Long ): BenchmarkDeployment { logger.info { "Using $namespace as namespace." } logger.info { "Using $path as resource path." } @@ -100,6 +104,7 @@ class KubernetesBenchmark : Benchmark, CustomResource(), Namespaced { appResources = appResources.map { it.second }, loadGenResources = loadGenResources.map { it.second }, loadGenerationDelay = loadGenerationDelay, + afterTeardownDelay = afterTeardownDelay, kafkaConfig = hashMapOf("bootstrap.servers" to kafkaConfig.bootstrapServer), topics = kafkaConfig.topics, client = DefaultKubernetesClient().inNamespace(namespace) diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt index 7d1c3f4ba727b8c149e3c5678ad406fa293c8f5b..6cf239676ddb24752f4754a85fc62657f9eb6603 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt @@ -26,6 +26,7 @@ class KubernetesBenchmarkDeployment( val appResources: List<KubernetesResource>, val loadGenResources: List<KubernetesResource>, private val loadGenerationDelay: Long, + private val afterTeardownDelay: Long, private val kafkaConfig: HashMap<String, Any>, private val topics: List<KafkaConfig.TopicWrapper>, private val client: NamespacedKubernetesClient @@ -33,7 +34,6 @@ class KubernetesBenchmarkDeployment( private val kafkaController = TopicManager(this.kafkaConfig) private val kubernetesManager = K8sManager(client) private val LAG_EXPORTER_POD_LABEL = "app.kubernetes.io/name=kafka-lag-exporter" - private val SLEEP_AFTER_TEARDOWN = 5000L /** * Setup a [KubernetesBenchmark] using the [TopicManager] and the [K8sManager]: @@ -61,7 +61,7 @@ class KubernetesBenchmarkDeployment( appResources.forEach { kubernetesManager.remove(it) } kafkaController.removeTopics(this.topics.map { topic -> topic.name }) KafkaLagExporterRemover(client).remove(LAG_EXPORTER_POD_LABEL) - logger.info { "Teardown complete. Wait $SLEEP_AFTER_TEARDOWN ms to let everything come down." } - Thread.sleep(SLEEP_AFTER_TEARDOWN) + logger.info { "Teardown complete. Wait $afterTeardownDelay ms to let everything come down." } + Thread.sleep(Duration.ofSeconds(afterTeardownDelay).toMillis()) } } 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 c5d691eb8f1437726bcce22500ad995bc1b6e4da..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 @@ -34,25 +35,28 @@ class AnalysisExecutor( * @param executionDuration of the experiment. * @return true if the experiment succeeded. */ - fun analyze(load: LoadDimension, res: Resource, executionDuration: Duration): Boolean { + fun analyze(load: LoadDimension, res: Resource, executionIntervals: List<Pair<Instant, Instant>>): Boolean { var result = false + var repetitionCounter = 1 try { - val prometheusData = fetcher.fetchMetric( - start = Instant.now().minus(executionDuration), - end = Instant.now(), - query = "sum by(group)(kafka_consumergroup_group_lag >= 0)" - ) + 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)") } - var resultsFolder: String = System.getenv("RESULTS_FOLDER") - if (resultsFolder.isNotEmpty()){ - resultsFolder += "/" + prometheusData.forEach{ data -> + ioHandler.writeToCSVFile( + fileURL = "${fileURL}_${repetitionCounter++}", + data = data.getResultAsList(), + columns = listOf("group", "timestamp", "value")) } - CsvExporter().toCsv( - name = "${resultsFolder}exp${executionId}_${load.get()}_${res.get()}_${slo.sloType.toSlug()}", - prom = prometheusData - ) val sloChecker = SloCheckerFactory().create( sloType = slo.sloType, externalSlopeURL = slo.externalSloUrl, @@ -60,10 +64,7 @@ class AnalysisExecutor( warmup = slo.warmup ) - result = sloChecker.evaluate( - start = Instant.now().minus(executionDuration), - end = Instant.now(), fetchedData = prometheusData - ) + result = sloChecker.evaluate(prometheusData) } catch (e: Exception) { logger.error { "Evaluation failed for resource '${res.get()}' and load '${load.get()}'. Error: $e" } 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/evaluation/ExternalSloChecker.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt index 8eac05a815e0bc45e1abc38bcdf0352e61bd7730..f7ebee8faf740583dbe6a37381a599e9bde19280 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt @@ -35,10 +35,10 @@ class ExternalSloChecker( * @return true if the experiment was successful(the threshold was not exceeded. * @throws ConnectException if the external service could not be reached. */ - override fun evaluate(start: Instant, end: Instant, fetchedData: PrometheusResponse): Boolean { + override fun evaluate(fetchedData: List<PrometheusResponse>): Boolean { var counter = 0 val data = Gson().toJson(mapOf( - "total_lag" to fetchedData.data?.result, + "total_lags" to fetchedData.map { it.data?.result}, "threshold" to threshold, "warmup" to warmup)) diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt index 758dcdefcaee79654d8ff65f0f798832aafc1294..9ee5fe7ef34ce5b6214882ce2c1d19677f1d7130 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/SloChecker.kt @@ -1,14 +1,12 @@ package theodolite.evaluation import theodolite.util.PrometheusResponse -import java.time.Instant /** * A SloChecker can be used to evaluate data from Prometheus. * @constructor Creates an empty SloChecker */ interface SloChecker { - /** * Evaluates [fetchedData] and returns if the experiment was successful. * Returns if the evaluated experiment was successful. @@ -18,5 +16,5 @@ interface SloChecker { * @param fetchedData from Prometheus that will be evaluated. * @return true if experiment was successful. Otherwise false. */ - fun evaluate(start: Instant, end: Instant, fetchedData: PrometheusResponse): Boolean + fun evaluate(fetchedData: List<PrometheusResponse>): Boolean } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt index 909ae77a95dfef7dfc1dafd7bd0a326a72ef1424..e7b511d8c83b5abccece1204aad2a4a9ecfdfd26 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutor.kt @@ -26,8 +26,10 @@ abstract class BenchmarkExecutor( val executionDuration: Duration, val configurationOverrides: List<ConfigurationOverride?>, val slo: BenchmarkExecution.Slo, + val repetitions: Int, val executionId: Int, - val loadGenerationDelay: Long + val loadGenerationDelay: Long, + val afterTeardownDelay: Long ) { var run: AtomicBoolean = AtomicBoolean(true) diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt index 22db5076bee51e03318aa9ac95e66c2d61d27f28..3afc85f0a8cb67011763498a662b447ce2c07f0f 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/BenchmarkExecutorImpl.kt @@ -5,11 +5,9 @@ import mu.KotlinLogging import theodolite.benchmark.Benchmark import theodolite.benchmark.BenchmarkExecution import theodolite.evaluation.AnalysisExecutor -import theodolite.util.ConfigurationOverride -import theodolite.util.LoadDimension -import theodolite.util.Resource -import theodolite.util.Results +import theodolite.util.* import java.time.Duration +import java.time.Instant private val logger = KotlinLogging.logger {} @@ -20,41 +18,56 @@ class BenchmarkExecutorImpl( executionDuration: Duration, configurationOverrides: List<ConfigurationOverride?>, slo: BenchmarkExecution.Slo, + repetitions: Int, executionId: Int, - loadGenerationDelay: Long -) : BenchmarkExecutor(benchmark, results, executionDuration, configurationOverrides, slo, executionId, loadGenerationDelay) { + loadGenerationDelay: Long, + afterTeardownDelay: Long +) : BenchmarkExecutor(benchmark, results, executionDuration, configurationOverrides, slo, repetitions, executionId, loadGenerationDelay, afterTeardownDelay) { override fun runExperiment(load: LoadDimension, res: Resource): Boolean { var result = false - val benchmarkDeployment = benchmark.buildDeployment(load, res, configurationOverrides, loadGenerationDelay) + val executionIntervals: MutableList<Pair<Instant, Instant>> = ArrayList() - try { - benchmarkDeployment.setup() - this.waitAndLog() - } catch (e: Exception) { - logger.error { "Error while setting up experiment with id ${this.executionId}." } - logger.error { "Error is: $e" } - this.run.set(false) + for (i in 1.rangeTo(repetitions)) { + logger.info { "Run repetition $i/$repetitions" } + if (this.run.get()) { + executionIntervals.add(runSingleExperiment(load,res)) + } else { + break + } } /** * Analyse the experiment, if [run] is true, otherwise the experiment was canceled by the user. */ if (this.run.get()) { - result = AnalysisExecutor(slo = slo, executionId = executionId).analyze( - load = load, - res = res, - executionDuration = executionDuration - ) + result =AnalysisExecutor(slo = slo, executionId = executionId) + .analyze( + load = load, + res = res, + executionIntervals = executionIntervals) this.results.setResult(Pair(load, res), result) } + return result + } + private fun runSingleExperiment(load: LoadDimension, res: Resource): Pair<Instant, Instant> { + val benchmarkDeployment = benchmark.buildDeployment(load, res, this.configurationOverrides, this.loadGenerationDelay, this.afterTeardownDelay) + val from = Instant.now() + try { + benchmarkDeployment.setup() + this.waitAndLog() + } catch (e: Exception) { + logger.error { "Error while setup experiment." } + logger.error { "Error is: $e" } + this.run.set(false) + } + val to = Instant.now() try { benchmarkDeployment.teardown() } catch (e: Exception) { logger.warn { "Error while tearing down the benchmark deployment." } logger.debug { "Teardown failed, caused by: $e" } } - - return result + return Pair(from,to) } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/Shutdown.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/Shutdown.kt index 80e1ac2341d9f8608d70b39a0d99f7b88dd7f108..0ff8379a0af4b11154214dde021d7c60609631d1 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/Shutdown.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/Shutdown.kt @@ -31,7 +31,8 @@ class Shutdown(private val benchmarkExecution: BenchmarkExecution, private val b load = LoadDimension(0, emptyList()), res = Resource(0, emptyList()), configurationOverrides = benchmarkExecution.configOverrides, - loadGenerationDelay = 0L + loadGenerationDelay = 0L, + afterTeardownDelay = 5L ) deployment.teardown() } catch (e: Exception) { diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt index c297251e30c17e5824c2541365164d9824718073..c73aaae08489c25a40163d4edb1607247fae010a 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 @@ -71,10 +62,24 @@ class TheodoliteExecutor( executionDuration = executionDuration, configurationOverrides = config.configOverrides, slo = config.slos[0], + repetitions = config.execution.repetitions, executionId = config.executionId, - loadGenerationDelay = config.execution.loadGenerationDelay + loadGenerationDelay = config.execution.loadGenerationDelay, + afterTeardownDelay = config.execution.afterTeardownDelay ) + if (config.load.loadValues != config.load.loadValues.sorted()) { + config.load.loadValues = config.load.loadValues.sorted() + logger.info { "Load values are not sorted correctly, Theodolite sorts them in ascending order." + + "New order is: ${config.load.loadValues}" } + } + + if (config.resources.resourceValues != config.resources.resourceValues.sorted()) { + config.resources.resourceValues = config.resources.resourceValues.sorted() + logger.info { "Load values are not sorted correctly, Theodolite sorts them in ascending order." + + "New order is: ${config.resources.resourceValues}" } + } + return Config( loads = config.load.loadValues.map { load -> LoadDimension(load, loadDimensionPatcherDefinition) }, resources = config.resources.resourceValues.map { resource -> @@ -102,34 +107,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 @@ -138,14 +125,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/TheodoliteYamlExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt index 8b2909f7658f4dffcfd961cad8cd00eb013a160c..b9977029703c8012ada7fb3d7766bfa321a836c3 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteYamlExecutor.kt @@ -31,8 +31,8 @@ class TheodoliteYamlExecutor { fun start() { logger.info { "Theodolite started" } - val executionPath = System.getenv("THEODOLITE_EXECUTION") ?: "./config/BenchmarkExecution.yaml" - val benchmarkPath = System.getenv("THEODOLITE_BENCHMARK") ?: "./config/BenchmarkType.yaml" + val executionPath = System.getenv("THEODOLITE_EXECUTION") ?: "./config/example-execution-yaml-resource.yaml" + val benchmarkPath = System.getenv("THEODOLITE_BENCHMARK") ?: "./config/example-benchmark-yaml-resource.yaml" logger.info { "Using $executionPath for BenchmarkExecution" } logger.info { "Using $benchmarkPath for BenchmarkType" } 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/LeaderElector.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/LeaderElector.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d093e4851e5c43d29a3fea3057ccf01be612e63 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/operator/LeaderElector.kt @@ -0,0 +1,43 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.DefaultKubernetesClient +import io.fabric8.kubernetes.client.NamespacedKubernetesClient +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks +import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfigBuilder +import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock +import mu.KotlinLogging +import java.time.Duration +import java.util.* +import kotlin.reflect.KFunction0 + +private val logger = KotlinLogging.logger {} + +class LeaderElector( + val client: NamespacedKubernetesClient, + val name: String + ) { + + fun getLeadership(leader: KFunction0<Unit>) { + val lockIdentity: String = UUID.randomUUID().toString() + DefaultKubernetesClient().use { kc -> + kc.leaderElector() + .withConfig( + LeaderElectionConfigBuilder() + .withName("Theodolite") + .withLeaseDuration(Duration.ofSeconds(15L)) + .withLock(LeaseLock(client.namespace, name, lockIdentity)) + .withRenewDeadline(Duration.ofSeconds(10L)) + .withRetryPeriod(Duration.ofSeconds(2L)) + .withLeaderCallbacks(LeaderCallbacks( + { Thread{leader()}.start() }, + { logger.info { "STOPPED LEADERSHIP" } } + ) { newLeader: String? -> + logger.info { "New leader elected $newLeader" } + }) + .build() + ) + .build().run() + } + } + +} \ 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..3053c364e2d6d9bc9797c190f0a87d861089b556 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.properties = mutableMapOf("variableName" to 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..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,47 +28,95 @@ private val logger = KotlinLogging.logger {} */ class TheodoliteOperator { private val namespace = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE + val client: NamespacedKubernetesClient = DefaultKubernetesClient().inNamespace(namespace) + + + fun start() { + LeaderElector( + client = client, + name = "theodolite-operator" + ) + .getLeadership(::startOperator) + } /** * Start the operator. */ - fun start() { + private fun startOperator() { 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, + 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/AbstractPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/AbstractPatcher.kt index c0d17244b6a7a3f37b8d8a57713659b85b9b65b1..df80e9cbd2503685a7dbed35db5319920dfc42cb 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/AbstractPatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/AbstractPatcher.kt @@ -20,7 +20,5 @@ import io.fabric8.kubernetes.api.model.KubernetesResource * */ abstract class AbstractPatcher( - k8sResource: KubernetesResource, - container: String? = null, - variableName: String? = null + k8sResource: KubernetesResource ) : Patcher diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/EnvVarPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/EnvVarPatcher.kt index b640df1da2ca1c139bb5b02e9e42bad9e7d08d74..416aec74a3af9b74594f5e6cd018682bf91cbf63 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/EnvVarPatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/EnvVarPatcher.kt @@ -16,7 +16,7 @@ class EnvVarPatcher( private val k8sResource: KubernetesResource, private val container: String, private val variableName: String -) : AbstractPatcher(k8sResource, container, variableName) { +) : AbstractPatcher(k8sResource) { override fun <String> patch(value: String) { if (k8sResource is Deployment) { diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ImagePatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ImagePatcher.kt index b29d8d9925311aa25b7336dcd6805783ca62c3e7..8f6753372076c119324dc962112928253633b6b0 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ImagePatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ImagePatcher.kt @@ -11,7 +11,7 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet * @param container Container to be patched. */ class ImagePatcher(private val k8sResource: KubernetesResource, private val container: String) : - AbstractPatcher(k8sResource, container) { + AbstractPatcher(k8sResource) { override fun <String> patch(imagePath: String) { if (k8sResource is Deployment) { 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..5ee5807cd8378c9f2bbd62435203208d61131f15 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/LabelPatcher.kt @@ -0,0 +1,49 @@ +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) { + + override fun <String> patch(labelValue: String) { + 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/NodeSelectorPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NodeSelectorPatcher.kt index 0a668a908e66577f96ea1268b85a38ad73bb16a7..0e8cd553a6c6a9ed6fa2c8cc1b84e4cfebe79d73 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NodeSelectorPatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NodeSelectorPatcher.kt @@ -10,7 +10,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment * @param variableName The `label-key` of the node for which the `label-value` is to be patched. */ class NodeSelectorPatcher(private val k8sResource: KubernetesResource, private val variableName: String) : - AbstractPatcher(k8sResource, variableName) { + AbstractPatcher(k8sResource) { override fun <String> patch(value: String) { if (k8sResource is Deployment) { k8sResource.spec.template.spec.nodeSelector = mapOf(variableName to value as kotlin.String) diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumNestedGroupsLoadGeneratorReplicaPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumNestedGroupsLoadGeneratorReplicaPatcher.kt index d7136740a7e17f956eed16bc6e3fcd4954ab91b8..65489a96974ad566fe7cbd88cf6ff7fb49135e1d 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumNestedGroupsLoadGeneratorReplicaPatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumNestedGroupsLoadGeneratorReplicaPatcher.kt @@ -4,16 +4,17 @@ import io.fabric8.kubernetes.api.model.KubernetesResource import io.fabric8.kubernetes.api.model.apps.Deployment import kotlin.math.pow -private const val NUM_SENSORS = 4.0 -private const val LOAD_GEN_MAX_RECORDS = 150000 - -class NumNestedGroupsLoadGeneratorReplicaPatcher(private val k8sResource: KubernetesResource) : +class NumNestedGroupsLoadGeneratorReplicaPatcher( + private val k8sResource: KubernetesResource, + private val numSensors: String, + private val loadGenMaxRecords: String + ) : AbstractPatcher(k8sResource) { override fun <String> patch(value: String) { if (k8sResource is Deployment) { if (value is kotlin.String) { - val approxNumSensors = NUM_SENSORS.pow(Integer.parseInt(value).toDouble()) - val loadGenInstances = (approxNumSensors + LOAD_GEN_MAX_RECORDS - 1) / LOAD_GEN_MAX_RECORDS + val approxNumSensors = numSensors.toDouble().pow(Integer.parseInt(value).toDouble()) + val loadGenInstances = (approxNumSensors + loadGenMaxRecords.toDouble() - 1) / loadGenMaxRecords.toDouble() this.k8sResource.spec.replicas = loadGenInstances.toInt() } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumSensorsLoadGeneratorReplicaPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumSensorsLoadGeneratorReplicaPatcher.kt index d29280a648cd4c912b8e2717b51c4f9c3f8a2271..f6a06324e36d7942d3944a492fee263f428376c1 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumSensorsLoadGeneratorReplicaPatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/NumSensorsLoadGeneratorReplicaPatcher.kt @@ -3,14 +3,16 @@ package theodolite.patcher import io.fabric8.kubernetes.api.model.KubernetesResource import io.fabric8.kubernetes.api.model.apps.Deployment -private const val LOAD_GEN_MAX_RECORDS = 150000 -class NumSensorsLoadGeneratorReplicaPatcher(private val k8sResource: KubernetesResource) : +class NumSensorsLoadGeneratorReplicaPatcher( + private val k8sResource: KubernetesResource, + private val loadGenMaxRecords: String +) : AbstractPatcher(k8sResource) { override fun <String> patch(value: String) { if (k8sResource is Deployment) { if (value is kotlin.String) { - val loadGenInstances = (Integer.parseInt(value) + LOAD_GEN_MAX_RECORDS - 1) / LOAD_GEN_MAX_RECORDS + val loadGenInstances = (Integer.parseInt(value) + loadGenMaxRecords.toInt() - 1) / loadGenMaxRecords.toInt() this.k8sResource.spec.replicas = loadGenInstances } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherDefinitionFactory.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherDefinitionFactory.kt index bcb568f716449cb1981112ab23a81411a0f7c54d..d5a6f3821d2688651475625506a78efc6061ab82 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherDefinitionFactory.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherDefinitionFactory.kt @@ -21,8 +21,7 @@ class PatcherDefinitionFactory { * value of the requiredType. */ fun createPatcherDefinition(requiredType: String, patcherTypes: List<TypeName>): List<PatcherDefinition> { - return patcherTypes - .filter { type -> type.typeName == requiredType } - .flatMap { type -> type.patchers } + return patcherTypes.firstOrNull() { type -> type.typeName == requiredType } + ?.patchers ?: throw IllegalArgumentException("typeName $requiredType not found.") } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt index 2ee1f6c7b46322cb0f8de03c37aabe64ccf0ba5a..45e50113c964d671962fadc718994a29b2da81f4 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/PatcherFactory.kt @@ -1,6 +1,8 @@ package theodolite.patcher import io.fabric8.kubernetes.api.model.KubernetesResource +import theodolite.util.DeploymentFailedException +import theodolite.util.InvalidPatcherConfigurationException import theodolite.util.PatcherDefinition /** @@ -27,25 +29,56 @@ class PatcherFactory { k8sResources: List<Pair<String, KubernetesResource>> ): Patcher { val resource = - k8sResources.filter { it.first == patcherDefinition.resource }.map { resource -> resource.second }[0] - return when (patcherDefinition.type) { - "ReplicaPatcher" -> ReplicaPatcher(resource) - "NumNestedGroupsLoadGeneratorReplicaPatcher" -> NumNestedGroupsLoadGeneratorReplicaPatcher(resource) - "NumSensorsLoadGeneratorReplicaPatcher" -> NumSensorsLoadGeneratorReplicaPatcher(resource) - "EnvVarPatcher" -> EnvVarPatcher(resource, patcherDefinition.container, patcherDefinition.variableName) - "NodeSelectorPatcher" -> NodeSelectorPatcher(resource, patcherDefinition.variableName) - "ResourceLimitPatcher" -> ResourceLimitPatcher( - resource, - patcherDefinition.container, - patcherDefinition.variableName - ) - "ResourceRequestPatcher" -> ResourceRequestPatcher( - resource, - patcherDefinition.container, - patcherDefinition.variableName - ) - "SchedulerNamePatcher" -> SchedulerNamePatcher(resource) - else -> throw IllegalArgumentException("Patcher type ${patcherDefinition.type} not found") + k8sResources.filter { it.first == patcherDefinition.resource } + .map { resource -> resource.second } + .firstOrNull() + ?: throw DeploymentFailedException("Could not find resource ${patcherDefinition.resource}") + + return try { + when (patcherDefinition.type) { + "ReplicaPatcher" -> ReplicaPatcher( + k8sResource = resource + ) + "NumNestedGroupsLoadGeneratorReplicaPatcher" -> NumNestedGroupsLoadGeneratorReplicaPatcher( + k8sResource = resource, + loadGenMaxRecords = patcherDefinition.properties["loadGenMaxRecords"] !!, + numSensors = patcherDefinition.properties["numSensors"] !! + ) + "NumSensorsLoadGeneratorReplicaPatcher" -> NumSensorsLoadGeneratorReplicaPatcher( + k8sResource = resource, + loadGenMaxRecords = patcherDefinition.properties["loadGenMaxRecords"] !! + ) + "EnvVarPatcher" -> EnvVarPatcher( + k8sResource = resource, + container = patcherDefinition.properties["container"] !!, + variableName = patcherDefinition.properties["variableName"] !! + ) + "NodeSelectorPatcher" -> NodeSelectorPatcher( + k8sResource = resource, + variableName = patcherDefinition.properties["variableName"] !! + ) + "ResourceLimitPatcher" -> ResourceLimitPatcher( + k8sResource = resource, + container = patcherDefinition.properties["container"] !!, + limitedResource = patcherDefinition.properties["limitedResource"] !! + ) + "ResourceRequestPatcher" -> ResourceRequestPatcher( + k8sResource = resource, + container = patcherDefinition.properties["container"] !!, + requestedResource = patcherDefinition.properties["requestedResource"] !! + ) + "SchedulerNamePatcher" -> SchedulerNamePatcher( + k8sResource = resource + ) + "LabelPatcher" -> LabelPatcher( + k8sResource = resource, + variableName = patcherDefinition.properties["variableName"] !! + ) + else -> throw InvalidPatcherConfigurationException("Patcher type ${patcherDefinition.type} not found.") + } + } catch (e: Exception) { + throw InvalidPatcherConfigurationException("Could not create patcher with type ${patcherDefinition.type}" + + " Probably a required patcher argument was not specified." ) } } } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceLimitPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceLimitPatcher.kt index 30f82e804f23b770457ec968f25ba7c00d72aefd..1a6fa35a944d00634ec0607b0bff34f4cb9d9b9c 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceLimitPatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceLimitPatcher.kt @@ -18,7 +18,7 @@ class ResourceLimitPatcher( private val k8sResource: KubernetesResource, private val container: String, private val limitedResource: String -) : AbstractPatcher(k8sResource, container, limitedResource) { +) : AbstractPatcher(k8sResource) { override fun <String> patch(value: String) { when (k8sResource) { diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceRequestPatcher.kt b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceRequestPatcher.kt index 24b2a6d40525f16448b77c50fba8aba0969d075a..9bf8c3c72f656d326ca3070cd5843778e5cdff42 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceRequestPatcher.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/patcher/ResourceRequestPatcher.kt @@ -18,7 +18,7 @@ class ResourceRequestPatcher( private val k8sResource: KubernetesResource, private val container: String, private val requestedResource: String -) : AbstractPatcher(k8sResource, container, requestedResource) { +) : AbstractPatcher(k8sResource) { override fun <String> patch(value: String) { when (k8sResource) { diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/DeploymentFailedException.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/DeploymentFailedException.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e276d7de4e205a75eb309a71a793e70f7565ea4 --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/DeploymentFailedException.kt @@ -0,0 +1,5 @@ +package theodolite.util + + +class DeploymentFailedException(message:String): Exception(message) { +} \ No newline at end of file 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/InvalidPatcherConfigurationException.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/InvalidPatcherConfigurationException.kt new file mode 100644 index 0000000000000000000000000000000000000000..c103ef1f35a1b3ffa56dad50c7cf6c1db51eb57f --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/InvalidPatcherConfigurationException.kt @@ -0,0 +1,5 @@ +package theodolite.util + +class InvalidPatcherConfigurationException(message:String): Exception(message) { +} + 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/PatcherDefinition.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/PatcherDefinition.kt index b24f887d6ff6e3096a2e740f541861d76804775b..6ec0cce36751ec0343d40aa49fefa44f4c7fc918 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/util/PatcherDefinition.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/PatcherDefinition.kt @@ -1,6 +1,7 @@ package theodolite.util import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.quarkus.runtime.annotations.RegisterForReflection /** @@ -19,13 +20,6 @@ class PatcherDefinition { */ lateinit var resource: String - /** - * The container which the patcher is applied to - */ - lateinit var container: String - - /** - * The variable name for the patcher - */ - lateinit var variableName: String + @JsonSerialize + lateinit var properties: MutableMap<String, String> } 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..b76821f6e7cca5408f604ba9bbf83cf1b43a37de 100644 --- a/theodolite-quarkus/src/main/resources/operator/benchmarkCRD.yaml +++ b/theodolite-quarkus/src/main/resources/operator/benchmarkCRD.yaml @@ -1,13 +1,117 @@ -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: "" + properties: + type: object + additionalProperties: true + x-kubernetes-map-type: "granular" + 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: "" + properties: + type: object + additionalProperties: true + x-kubernetes-map-type: "granular" + 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..7a728d97c52a84d81bcc44fd9f20c56e6b574527 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,39 @@ -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" + properties: + container: "workload-generator" + variableName: "NUM_SENSORS" + - type: "NumSensorsLoadGeneratorReplicaPatcher" + resource: "uc1-load-generator-deployment.yaml" + properties: + loadGenMaxRecords: "15000" + 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..b81bbcd442834136283dc080f5f6a79bbc1cd415 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,29 @@ -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: [] \ No newline at end of file diff --git a/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml b/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml index 0bdb83c6201112a750bad41b81321b7a108a66fa..b984f3ebe5ca7c8868adb9d3593e5d87d73fc2bd 100644 --- a/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml +++ b/theodolite-quarkus/src/main/resources/operator/executionCRD.yaml @@ -1,13 +1,128 @@ -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: "" + properties: + type: object + additionalProperties: true + x-kubernetes-map-type: "granular" + 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/CompositeStrategyTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/CompositeStrategyTest.kt index 726d4490b053b5c56f0a0387f54ad557d4c1865c..c2b30ab7eb23d60db39778218ad9d6a4c12799a6 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/CompositeStrategyTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/CompositeStrategyTest.kt @@ -31,7 +31,7 @@ class CompositeStrategyTest { val results = Results() val benchmark = TestBenchmark() val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() - val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker, 0, 0) + val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker, 0, 0, 5) val linearSearch = LinearSearch(benchmarkExecutor) val lowerBoundRestriction = LowerBoundRestriction(results) val strategy = @@ -65,7 +65,7 @@ class CompositeStrategyTest { val benchmark = TestBenchmark() val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() val benchmarkExecutorImpl = - TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker, 0, 0) + TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker, 0, 0, 0) val binarySearch = BinarySearch(benchmarkExecutorImpl) val lowerBoundRestriction = LowerBoundRestriction(results) val strategy = @@ -98,7 +98,7 @@ class CompositeStrategyTest { val results = Results() val benchmark = TestBenchmark() val sloChecker: BenchmarkExecution.Slo = BenchmarkExecution.Slo() - val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker, 0, 0) + val benchmarkExecutor = TestBenchmarkExecutorImpl(mockResults, benchmark, results, sloChecker, 0, 0,0) val binarySearch = BinarySearch(benchmarkExecutor) val lowerBoundRestriction = LowerBoundRestriction(results) val strategy = diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt index 82e4bc5d77f3f35d217c56a377513c0e7d329170..4ae9ce23b31204b366dce7bcf000e4114d7f3f8f 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() @@ -31,27 +31,32 @@ class ResourceLimitPatcherTest { val k8sResource = loader.loadK8sResource("Deployment", testPath + fileName) as Deployment val defCPU = PatcherDefinition() - defCPU.variableName = "cpu" defCPU.resource = "cpu-memory-deployment.yaml" - defCPU.container = "uc-application" defCPU.type = "ResourceLimitPatcher" + defCPU.properties = mutableMapOf( + "limitedResource" to "cpu", + "container" to "application" + ) val defMEM = PatcherDefinition() - defMEM.variableName = "memory" defMEM.resource = "cpu-memory-deployment.yaml" - defMEM.container = "uc-application" defMEM.type = "ResourceLimitPatcher" + defMEM.properties = mutableMapOf( + "limitedResource" to "memory", + "container" to "uc-application" + ) patcherFactory.createPatcher( patcherDefinition = defCPU, k8sResources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)) ).patch(value = cpuValue) + patcherFactory.createPatcher( patcherDefinition = defMEM, k8sResources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)) ).patch(value = memValue) - k8sResource.spec.template.spec.containers.filter { it.name == defCPU.container } + k8sResource.spec.template.spec.containers.filter { it.name == defCPU.properties["container"] !! } .forEach { assertTrue(it.resources.limits["cpu"].toString() == cpuValue) assertTrue(it.resources.limits["memory"].toString() == memValue) diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt index 3cd6b012f09c5471b1b011b5cd03e61a0fab1c4e..6f232961871c9d11929fc912a483e6e805cc7daf 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() @@ -31,16 +31,20 @@ class ResourceRequestPatcherTest { val k8sResource = loader.loadK8sResource("Deployment", testPath + fileName) as Deployment val defCPU = PatcherDefinition() - defCPU.variableName = "cpu" defCPU.resource = "cpu-memory-deployment.yaml" - defCPU.container = "uc-application" defCPU.type = "ResourceRequestPatcher" + defCPU.properties = mutableMapOf( + "requestedResource" to "cpu", + "container" to "application" + ) val defMEM = PatcherDefinition() - defMEM.variableName = "memory" defMEM.resource = "cpu-memory-deployment.yaml" - defMEM.container = "uc-application" defMEM.type = "ResourceRequestPatcher" + defMEM.properties = mutableMapOf( + "requestedResource" to "memory", + "container" to "application" + ) patcherFactory.createPatcher( patcherDefinition = defCPU, @@ -51,7 +55,7 @@ class ResourceRequestPatcherTest { k8sResources = listOf(Pair("cpu-memory-deployment.yaml", k8sResource)) ).patch(value = memValue) - k8sResource.spec.template.spec.containers.filter { it.name == defCPU.container } + k8sResource.spec.template.spec.containers.filter { it.name == defCPU.properties["container"] !! } .forEach { assertTrue(it.resources.requests["cpu"].toString() == cpuValue) assertTrue(it.resources.requests["memory"].toString() == memValue) diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmark.kt b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmark.kt index 71e3cbcc78208cc4227e369d59d841e39adb87ed..6ddca296acced658609fcccb307a83047238e118 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmark.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmark.kt @@ -12,7 +12,8 @@ class TestBenchmark : Benchmark { load: LoadDimension, res: Resource, configurationOverrides: List<ConfigurationOverride?>, - loadGenerationDelay: Long + loadGenerationDelay: Long, + afterTeardownDelay: Long ): BenchmarkDeployment { return TestBenchmarkDeployment() } diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt index 294727d6a252a29e312ff532937e7fdf9f079ac7..cbd2d5926d61b0bfd4de6fab0c14422ddf88f190 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/TestBenchmarkExecutorImpl.kt @@ -14,7 +14,8 @@ class TestBenchmarkExecutorImpl( results: Results, slo: BenchmarkExecution.Slo, executionId: Int, - loadGenerationDelay: Long + loadGenerationDelay: Long, + afterTeardownDelay: Long ) : BenchmarkExecutor( benchmark, @@ -22,8 +23,10 @@ class TestBenchmarkExecutorImpl( executionDuration = Duration.ofSeconds(1), configurationOverrides = emptyList(), slo = slo, + repetitions = 1, executionId = executionId, - loadGenerationDelay = loadGenerationDelay + loadGenerationDelay = loadGenerationDelay, + afterTeardownDelay = afterTeardownDelay ) { override fun runExperiment(load: LoadDimension, res: Resource): Boolean { 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