diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 66fb891898a1c57f8d814394a698a17bb7935164..48d3681dab13b31ade355d9a1f13704cdc2e9c2e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -143,6 +143,7 @@ test-benchmarks: - build-benchmarks script: ./gradlew test --continue artifacts: + when: always reports: junit: - "theodolite-benchmarks/**/build/test-results/test/TEST-*.xml" @@ -396,6 +397,7 @@ test-theodolite: #- build-theodolite-native script: ./gradlew test --stacktrace artifacts: + when: always reports: junit: - "theodolite/**/build/test-results/test/TEST-*.xml" @@ -474,6 +476,22 @@ test-slo-checker-dropped-records-kstreams: - when: manual allow_failure: true +test-slo-checker-generic: + stage: test + needs: [] + image: python:3.7-slim + before_script: + - cd slo-checker/generic + script: + - pip install -r requirements.txt + - cd app + - python -m unittest + rules: + - changes: + - slo-checker/generic/**/* + - when: manual + allow_failure: true + deploy-slo-checker-lag-trend: stage: deploy extends: @@ -510,6 +528,24 @@ deploy-slo-checker-dropped-records-kstreams: when: manual allow_failure: true +deploy-slo-checker-generic: + stage: deploy + extends: + - .kaniko-push + needs: + - test-slo-checker-generic + before_script: + - cd slo-checker/generic + variables: + IMAGE_NAME: theodolite-slo-checker-generic + rules: + - changes: + - slo-checker/generic/**/* + if: "$CR_HOST && $CR_ORG && $CR_USER && $CR_PW" + - if: "$CR_HOST && $CR_ORG && $CR_USER && $CR_PW" + when: manual + allow_failure: true + # Theodolite Random Scheduler diff --git a/CITATION.cff b/CITATION.cff index ab95efe7b82bcc8e2fb3228376f4cfc1efac05bc..07c2dcee319f73604f95414b987f8ed5274f7e82 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,7 +8,7 @@ authors: given-names: Wilhelm orcid: "https://orcid.org/0000-0001-6625-4335" title: Theodolite -version: "0.6.0" +version: "0.6.1" repository-code: "https://github.com/cau-se/theodolite" license: "Apache-2.0" doi: "10.1016/j.bdr.2021.100209" diff --git a/binder/requirements.txt b/binder/requirements.txt new file mode 120000 index 0000000000000000000000000000000000000000..6de15663a8c83876719aa07d6cb09b5a7b71df21 --- /dev/null +++ b/binder/requirements.txt @@ -0,0 +1 @@ +../analysis/requirements.txt \ No newline at end of file diff --git a/codemeta.json b/codemeta.json index 32b490f588ac38f610447e3eea579c6584d75c8f..2a190092b96adb3462c011e49db3c160d639d6fe 100644 --- a/codemeta.json +++ b/codemeta.json @@ -5,10 +5,10 @@ "codeRepository": "https://github.com/cau-se/theodolite", "dateCreated": "2020-03-13", "datePublished": "2020-07-27", - "dateModified": "2022-01-12", + "dateModified": "2022-01-17", "downloadUrl": "https://github.com/cau-se/theodolite/releases", "name": "Theodolite", - "version": "0.6.0", + "version": "0.6.1", "description": "Theodolite is a framework for benchmarking the horizontal and vertical scalability of cloud-native applications.", "developmentStatus": "active", "relatedLink": [ diff --git a/docs/api-reference/crds.md b/docs/api-reference/crds.md index a91e991609f5fe10e90793f34f2ad04c6c5576d0..0d7e46e3a72aea642fdc629f1abb664a4f8b93f3 100644 --- a/docs/api-reference/crds.md +++ b/docs/api-reference/crds.md @@ -2069,10 +2069,19 @@ Specifies the scaling resource that is benchmarked. </tr> </thead> <tbody><tr> + <td><b>completionTime</b></td> + <td>string</td> + <td> + Time when this execution was stopped<br/> + <br/> + <i>Format</i>: date-time<br/> + </td> + <td>false</td> + </tr><tr> <td><b>executionDuration</b></td> <td>string</td> <td> - Duration of the execution in seconds<br/> + Duration of the execution<br/> </td> <td>false</td> </tr><tr> @@ -2082,5 +2091,14 @@ Specifies the scaling resource that is benchmarked. <br/> </td> <td>false</td> + </tr><tr> + <td><b>startTime</b></td> + <td>string</td> + <td> + Time this execution started<br/> + <br/> + <i>Format</i>: date-time<br/> + </td> + <td>false</td> </tr></tbody> </table> \ No newline at end of file diff --git a/docs/creating-an-execution.md b/docs/creating-an-execution.md index e70893e7ea4364bfbb30465df95273703ec7f43b..263d630ff2db82927c72d2c2482fcddc09705bfc 100644 --- a/docs/creating-an-execution.md +++ b/docs/creating-an-execution.md @@ -58,7 +58,29 @@ As a Benchmark may define multiple supported load and resource types, an Executi ## Definition of SLOs SLOs provide a way to quantify whether a certain load intensity can be handled by a certain amount of provisioned resources. -An Execution must at least specify one SLO to be checked. +In Theodolite, SLO are evaluated by requesting monitoring data from Prometheus and analyzing it in a benchmark-specific way. +An Execution must at least define one SLO to be checked. + +A good choice to get started is defining an SLO of type `generic`: + +```yaml +- sloType: "generic" + prometheusUrl: "http://prometheus-operated:9090" + offset: 0 + properties: + externalSloUrl: "http://localhost:8082" + promQLQuery: "sum by(job) (kafka_streams_stream_task_metrics_dropped_records_total>=0)" + warmup: 60 # in seconds + queryAggregation: max + repetitionAggregation: median + operator: lte + threshold: 1000 +``` + +All you have to do is to define a [PromQL query](https://prometheus.io/docs/prometheus/latest/querying/basics/) describing which metrics should be requested (`promQLQuery`) and how the resulting time series should be evaluated. With `queryAggregation` you specify how the resulting time series is aggregated to a single value and `repetitionAggregation` describes how the results of multiple repetitions are aggregated. Possible values are +`mean`, `median`, `mode`, `sum`, `count`, `max`, `min`, `std`, `var`, `skew`, `kurt` as well as percentiles such as `p99` or `p99.9`. The result of aggregation all repetitions is checked against `threshold`. This check is performed using an `operator`, which describes that the result must be "less than" (`lt`), "less than equal" (`lte`), "greater than" (`gt`) or "greater than equal" (`gte`) to the threshold. + +In case you need to evaluate monitoring data in a more flexible fashion, you can also change the value of `externalSloUrl` to your custom SLO checker. Have a look at the source code of the [generic SLO checker](https://github.com/cau-se/theodolite/tree/master/slo-checker/generic) to get started. ## Experimental Setup @@ -72,7 +94,7 @@ The experimental setup can be configured by: ## Configuration Overrides -In cases where only small modifications of a system under test should be benchmarked, it is not necessarily required to [create a new benchmark](creating-a-benchmark). +In cases where only small modifications of a system under test should be benchmarked, it is not necessary to [create a new benchmark](creating-a-benchmark). Instead, also Executions allow to do small reconfigurations, such as switching on or off a specific Pod scheduler. This is done by defining `configOverrides` in the Execution. Each override consists of a patcher, defining which Kubernetes resource should be patched in which way, and a value the patcher is applied with. diff --git a/docs/index.yaml b/docs/index.yaml index 995d7523e8e47914a59ee99aad07d25a86322a0c..185ff1b0616b760c647a809006c48bf26c554490 100644 --- a/docs/index.yaml +++ b/docs/index.yaml @@ -1,6 +1,41 @@ apiVersion: v1 entries: theodolite: + - apiVersion: v2 + appVersion: 0.6.1 + created: "2022-01-18T10:40:00.557347616+01:00" + dependencies: + - condition: grafana.enabled + name: grafana + repository: https://grafana.github.io/helm-charts + version: 6.17.5 + - condition: kube-prometheus-stack.enabled + name: kube-prometheus-stack + repository: https://prometheus-community.github.io/helm-charts + version: 20.0.1 + - condition: cp-helm-charts.enabled + name: cp-helm-charts + repository: https://soerenhenning.github.io/cp-helm-charts + version: 0.6.0 + - condition: kafka-lag-exporter.enabled + name: kafka-lag-exporter + repository: https://lightbend.github.io/kafka-lag-exporter/repo/ + version: 0.6.7 + description: Theodolite is a framework for benchmarking the horizontal and vertical + scalability of cloud-native applications. + digest: 4896111999375c248d7dda0bdff090c155f464b79416decc0e0b47dc6710b5c7 + home: https://www.theodolite.rocks + maintainers: + - email: soeren.henning@email.uni-kiel.de + name: Sören Henning + url: https://www.se.informatik.uni-kiel.de/en/team/soeren-henning-m-sc + name: theodolite + sources: + - https://github.com/cau-se/theodolite + type: application + urls: + - https://github.com/cau-se/theodolite/releases/download/v0.6.1/theodolite-0.6.1.tgz + version: 0.6.1 - apiVersion: v2 appVersion: 0.6.0 created: "2022-01-12T13:53:08.413006558+01:00" @@ -141,4 +176,4 @@ entries: urls: - https://github.com/cau-se/theodolite/releases/download/v0.4.0/theodolite-0.4.0.tgz version: 0.4.0 -generated: "2022-01-12T13:53:08.367695997+01:00" +generated: "2022-01-18T10:40:00.486387187+01:00" diff --git a/docs/running-benchmarks.md b/docs/running-benchmarks.md index eda817d28b6a10b2e2f33e6986a3b018e089beff..7da1c7e5f8385a2818ae587b4c3ab3715a6c2bb2 100644 --- a/docs/running-benchmarks.md +++ b/docs/running-benchmarks.md @@ -11,6 +11,7 @@ Running scalability benchmarks with Theodolite involves the following steps: 1. [Deploying a benchmark to Kubernetes](#deploying-a-benchmark) 1. [Creating an execution](#creating-an-execution), which describes the experimental setup for running the benchmark 1. [Accessing benchmark results](#accessing-benchmark-results) +1. [Analyzing benchmark results](#analyzing-benchmark-results) with Theodolite's Jupyter notebooks ## Deploying a Benchmark @@ -131,3 +132,32 @@ For installations without persistence, but also as an alternative for installati ```sh kubectl cp $(kubectl get pod -l app=theodolite -o jsonpath="{.items[0].metadata.name}"):/results . -c results-access ``` + +## Analyzing Benchmark Results + +Theodolite comes with Jupyter notebooks for analyzing and visualizing benchmark execution results. +The easiest way to use them is at MyBinder: + +[Launch Notebooks](https://mybinder.org/v2/gh/cau-se/theodolite/HEAD?labpath=analysis){: .btn .btn-primary } +{: .text-center } + +Alternatively, you can also [run these notebook locally](https://github.com/cau-se/theodolite/tree/master/analysis), for example, with Docker or Visual Studio Code. + +The notebooks allow to compute a scalability function using its *demand* metric and to visualize multiple such functions in plots: + +### Computing the *demand* metric with `demand-metric.ipynb` (optional) + +After finishing a benchmark execution, Theodolite creates a `exp<id>_demand.csv` file. It maps the tested load intensities to the minimal required resources for that load. If the monitoring data collected during benchmark execution should be analyzed in more detail, the `demand-metric.ipynb` notebook can be used. + +Theodolite stores monitoring data for each conducted SLO experiment in `exp<id>_<load>_<resources>_<slo-slug>_<rep>.csv` files, where `<id>` is the ID of an execution, `<load>` the corresponding load intensity value, `<resources>` the resources value, `<slo-slug>` the [name of the SLO](creating-an-execution.html#definition-of-slos) and `<rep>` the repetition counter. +The `demand-metric.ipynb` notebook reads these files and generates a new CSV file mapping load intensities to the minimal required resources. The format of this file corresponds to the original `exp<id>_demand.csv` file created when running the benchmark, but allows, for example, to evaluate different warm-up periods. + +Currently, the `demand-metric.ipynb` notebook only supports benchmarks with the *lag trend SLO* out-of-the-box, but can easily be adjusted to perform any other type of analysis. + +### Plotting benchmark results with the *demand* metric with `demand-metric-plot.ipynb` + +The `demand-metric-plot.ipynb` takes one or multiple `exp<id>_demand.csv` files as input and visualize them together in a plot. +Input files can either be taken directly from Theodolite, or created from the `demand-metric.ipynb` notebooks. + +All plotting code is only intended to serve as a template. Adjust it as needed to change colors, labels, formatting, etc. as needed. +Please refer to the official docs of [MatPlotLib](https://matplotlib.org/) and the [ggplot](https://matplotlib.org/stable/gallery/style_sheets/ggplot.html) style, which are used to generate the plots. diff --git a/helm/preconfigs/minimal.yaml b/helm/preconfigs/minimal.yaml index b0828c2f424e8456933dc626a66a199cd60aa5da..80a83f06cc9838e01f812e730932b9b79d947161 100644 --- a/helm/preconfigs/minimal.yaml +++ b/helm/preconfigs/minimal.yaml @@ -8,5 +8,8 @@ cp-helm-charts: offsets.topic.replication.factor: "1" operator: + sloChecker: + droppedRecordsKStreams: + enabled: false resultsVolume: enabled: false diff --git a/helm/templates/theodolite/theodolite-operator.yaml b/helm/templates/theodolite/theodolite-operator.yaml index c7ced880cbbfbb9795ef59156ea1df7d5b860ec6..ff9c7e4de87c703af3350f7d9c797a5a53e2e675 100644 --- a/helm/templates/theodolite/theodolite-operator.yaml +++ b/helm/templates/theodolite/theodolite-operator.yaml @@ -31,6 +31,19 @@ spec: volumeMounts: - name: theodolite-results-volume mountPath: "/deployments/results" + {{- if .Values.operator.sloChecker.droppedRecordsKStreams.enabled }} + - name: slo-checker-generic + image: "{{ .Values.operator.sloChecker.generic.image }}:{{ .Values.operator.sloChecker.generic.imageTag }}" + imagePullPolicy: "{{ .Values.operator.sloChecker.generic.imagePullPolicy }}" + ports: + - containerPort: 8082 + name: analysis + env: + - name: PORT + value: "8082" + - name: LOG_LEVEL + value: INFO + {{- end }} {{- if .Values.operator.sloChecker.lagTrend.enabled }} - name: lag-trend-slo-checker image: "{{ .Values.operator.sloChecker.lagTrend.image }}:{{ .Values.operator.sloChecker.lagTrend.imageTag }}" diff --git a/helm/values.yaml b/helm/values.yaml index 1e57b42c485eb20a5525f25cfc0ef616e65a325c..ba58b040974886518ab111d668cb0db1140b2eb8 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -256,6 +256,11 @@ operator: nodeSelector: {} sloChecker: + generic: + enabled: true + image: ghcr.io/cau-se/theodolite-slo-checker-generic + imageTag: latest + imagePullPolicy: Always lagTrend: enabled: true image: ghcr.io/cau-se/theodolite-slo-checker-lag-trend diff --git a/slo-checker/generic/Dockerfile b/slo-checker/generic/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..032b8153a6989ca04631ba553289dacb3620a38d --- /dev/null +++ b/slo-checker/generic/Dockerfile @@ -0,0 +1,6 @@ +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY ./app /app \ No newline at end of file diff --git a/slo-checker/generic/README.md b/slo-checker/generic/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1a1358a06dc4165c678bca8745dd40473a7c5880 --- /dev/null +++ b/slo-checker/generic/README.md @@ -0,0 +1,89 @@ +# Generic SLO Evaluator + +## Execution + +For development: + +```sh +uvicorn main:app --reload +``` + +## Build the docker image: + +```sh +docker build . -t theodolite-evaluator +``` + +Run the Docker image: + +```sh +docker run -p 80:80 theodolite-evaluator +``` + +## Configuration + +You can set the `HOST` and the `PORT` (and a lot of more parameters) via environment variables. Default is `0.0.0.0:80`. +For more information see the [Gunicorn/FastAPI Docker docs](https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker#advanced-usage). + +## API Documentation + +The running webserver provides a REST API with the following route: + +* / + * Method: POST + * Body: + * results + * metric-metadata + * values + * metadata + * warmup + * queryAggregation + * repetitionAggregation + * operator + * threshold + +The body of the request must be a JSON string that satisfies the following conditions: + +* **dropped records**: 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*: + + ```json + { + "results": [ + [ + { + "metric": { + "<label-name>": "<label-value>" + }, + "values": [ + [ + <unix_timestamp>, // 1.634624989695E9 + "<sample_value>" // integer + ] + ] + } + ] + ], + "metadata": { + "warmup": 60, + "queryAggregation": "max", + "repetitionAggregation": "median", + "operator": "lt", + "threshold": 2000000 + } + } + ``` + +### description + +* results: + * metric-metadata: + * Labels of this metric. The `generic` slo checker does not use labels in the calculation of the service level objective. + * results + * The `<unix_timestamp>` provided as the first element of each element in the "values" array must be the timestamp of the measurement value in seconds (with optional decimal precision) + * The `<sample_value>` must be the measurement value as string. +* metadata: For the calculation of the service level objective require metadata. + * **warmup**: Specifies the warmup time in seconds that are ignored for evaluating the SLO. + * **queryAggregation**: Specifies the function used to aggregate a query. + * **repetitionAggregation**: Specifies the function used to aggregate a the results of multiple query aggregations. + * **operator**: Specifies how the result should be checked agains a threshold. Possible values are `lt`, `lte`, `gt` and `gte`. + * **threshold**: Must be an unsigned integer that specifies the threshold for the SLO evaluation. diff --git a/slo-checker/generic/app/main.py b/slo-checker/generic/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..f36c8739da00128ad94feb1f2d7871df7e2ff137 --- /dev/null +++ b/slo-checker/generic/app/main.py @@ -0,0 +1,72 @@ +from fastapi import FastAPI,Request +import logging +import os +import json +import sys +import re +import pandas as pd + + +app = FastAPI() + +logging.basicConfig(stream=sys.stdout, + format="%(asctime)s %(levelname)s %(name)s: %(message)s") +logger = logging.getLogger("API") + + +if os.getenv('LOG_LEVEL') == 'INFO': + logger.setLevel(logging.INFO) +elif os.getenv('LOG_LEVEL') == 'WARNING': + logger.setLevel(logging.WARNING) +elif os.getenv('LOG_LEVEL') == 'DEBUG': + logger.setLevel(logging.DEBUG) + + +def get_aggr_func(func_string: str): + if func_string in ['mean', 'median', 'mode', 'sum', 'count', 'max', 'min', 'std', 'var', 'skew', 'kurt']: + return func_string + elif re.search(r'^p\d\d?(\.\d+)?$', func_string): # matches strings like 'p99', 'p99.99', 'p1', 'p0.001' + def percentile(x): + return x.quantile(float(func_string[1:]) / 100) + percentile.__name__ = func_string + return percentile + else: + raise ValueError('Invalid function string.') + +def aggr_query(values: dict, warmup: int, aggr_func): + df = pd.DataFrame.from_dict(values) + df.columns = ['timestamp', 'value'] + filtered = df[df['timestamp'] >= (df['timestamp'][0] + warmup)] + filtered['value'] = filtered['value'].astype(int) + return filtered['value'].aggregate(aggr_func) + +def check_result(result, operator: str, threshold): + if operator == 'lt': + return result < threshold + if operator == 'lte': + return result <= threshold + if operator == 'gt': + return result > threshold + if operator == 'gte': + return result >= threshold + else: + raise ValueError('Invalid operator string.') + + + +@app.post("/",response_model=bool) +async def check_slo(request: Request): + data = json.loads(await request.body()) + logger.info('Received request with metadata: %s', data['metadata']) + + warmup = int(data['metadata']['warmup']) + query_aggregation = get_aggr_func(data['metadata']['queryAggregation']) + rep_aggregation = get_aggr_func(data['metadata']['repetitionAggregation']) + operator = data['metadata']['operator'] + threshold = int(data['metadata']['threshold']) + + query_results = [aggr_query(r[0]["values"], warmup, query_aggregation) for r in data["results"]] + result = pd.DataFrame(query_results).aggregate(rep_aggregation).at[0] + return check_result(result, operator, threshold) + +logger.info("SLO evaluator is online") \ No newline at end of file diff --git a/slo-checker/generic/app/test.py b/slo-checker/generic/app/test.py new file mode 100644 index 0000000000000000000000000000000000000000..2609225ddc9e6e96cdcd01db197cebbdd6501102 --- /dev/null +++ b/slo-checker/generic/app/test.py @@ -0,0 +1,56 @@ +import unittest +from main import app, get_aggr_func, check_result +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("/", json=data) + self.assertEqual(response.json(), True) + + def test_get_aggr_func_mean(self): + self.assertEqual(get_aggr_func('median'), 'median') + + def test_get_aggr_func_p99(self): + self.assertTrue(callable(get_aggr_func('p99'))) + + def test_get_aggr_func_p99_9(self): + self.assertTrue(callable(get_aggr_func('p99.9'))) + + def test_get_aggr_func_p99_99(self): + self.assertTrue(callable(get_aggr_func('p99.99'))) + + def test_get_aggr_func_p0_1(self): + self.assertTrue(callable(get_aggr_func('p0.1'))) + + def test_get_aggr_func_p99_(self): + self.assertRaises(ValueError, get_aggr_func, 'p99.') + + def test_get_aggr_func_p99_(self): + self.assertRaises(ValueError, get_aggr_func, 'q99') + + def test_get_aggr_func_p99_(self): + self.assertRaises(ValueError, get_aggr_func, 'mux') + + def test_check_result_lt(self): + self.assertEqual(check_result(100, 'lt', 200), True) + + def test_check_result_lte(self): + self.assertEqual(check_result(200, 'lte', 200), True) + + def test_check_result_gt(self): + self.assertEqual(check_result(100, 'gt', 200), False) + + def test_check_result_gte(self): + self.assertEqual(check_result(300, 'gte', 200), True) + + def test_check_result_invalid(self): + self.assertRaises(ValueError, check_result, 100, 'xyz', 200) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/slo-checker/generic/requirements.txt b/slo-checker/generic/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..87972ab01a276cbb63033e214e1ad53d38b5c8d8 --- /dev/null +++ b/slo-checker/generic/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.65.2 +pandas==1.0.3 +uvicorn +requests diff --git a/slo-checker/generic/resources/test-1-rep-success.json b/slo-checker/generic/resources/test-1-rep-success.json new file mode 100644 index 0000000000000000000000000000000000000000..b70f461cf620d8eee8c4d9d93feb46db7498626f --- /dev/null +++ b/slo-checker/generic/resources/test-1-rep-success.json @@ -0,0 +1,276 @@ +{ + "results": [ + [ + { + "metric": { + "job": "titan-ccp-aggregation" + }, + "values": [ + [ + 1.634624674695E9, + "0" + ], + [ + 1.634624679695E9, + "0" + ], + [ + 1.634624684695E9, + "0" + ], + [ + 1.634624689695E9, + "0" + ], + [ + 1.634624694695E9, + "0" + ], + [ + 1.634624699695E9, + "0" + ], + [ + 1.634624704695E9, + "0" + ], + [ + 1.634624709695E9, + "0" + ], + [ + 1.634624714695E9, + "0" + ], + [ + 1.634624719695E9, + "0" + ], + [ + 1.634624724695E9, + "0" + ], + [ + 1.634624729695E9, + "0" + ], + [ + 1.634624734695E9, + "0" + ], + [ + 1.634624739695E9, + "0" + ], + [ + 1.634624744695E9, + "1" + ], + [ + 1.634624749695E9, + "3" + ], + [ + 1.634624754695E9, + "4" + ], + [ + 1.634624759695E9, + "4" + ], + [ + 1.634624764695E9, + "4" + ], + [ + 1.634624769695E9, + "4" + ], + [ + 1.634624774695E9, + "4" + ], + [ + 1.634624779695E9, + "4" + ], + [ + 1.634624784695E9, + "4" + ], + [ + 1.634624789695E9, + "4" + ], + [ + 1.634624794695E9, + "4" + ], + [ + 1.634624799695E9, + "4" + ], + [ + 1.634624804695E9, + "176" + ], + [ + 1.634624809695E9, + "176" + ], + [ + 1.634624814695E9, + "176" + ], + [ + 1.634624819695E9, + "176" + ], + [ + 1.634624824695E9, + "176" + ], + [ + 1.634624829695E9, + "159524" + ], + [ + 1.634624834695E9, + "209870" + ], + [ + 1.634624839695E9, + "278597" + ], + [ + 1.634624844695E9, + "460761" + ], + [ + 1.634624849695E9, + "460761" + ], + [ + 1.634624854695E9, + "460761" + ], + [ + 1.634624859695E9, + "460761" + ], + [ + 1.634624864695E9, + "460761" + ], + [ + 1.634624869695E9, + "606893" + ], + [ + 1.634624874695E9, + "653534" + ], + [ + 1.634624879695E9, + "755796" + ], + [ + 1.634624884695E9, + "919317" + ], + [ + 1.634624889695E9, + "919317" + ], + [ + 1.634624894695E9, + "955926" + ], + [ + 1.634624899695E9, + "955926" + ], + [ + 1.634624904695E9, + "955926" + ], + [ + 1.634624909695E9, + "955926" + ], + [ + 1.634624914695E9, + "955926" + ], + [ + 1.634624919695E9, + "1036530" + ], + [ + 1.634624924695E9, + "1078477" + ], + [ + 1.634624929695E9, + "1194775" + ], + [ + 1.634624934695E9, + "1347755" + ], + [ + 1.634624939695E9, + "1352151" + ], + [ + 1.634624944695E9, + "1360428" + ], + [ + 1.634624949695E9, + "1360428" + ], + [ + 1.634624954695E9, + "1360428" + ], + [ + 1.634624959695E9, + "1360428" + ], + [ + 1.634624964695E9, + "1360428" + ], + [ + 1.634624969695E9, + "1525685" + ], + [ + 1.634624974695E9, + "1689296" + ], + [ + 1.634624979695E9, + "1771358" + ], + [ + 1.634624984695E9, + "1854284" + ], + [ + 1.634624989695E9, + "1854284" + ] + ] + } + ] + ], + "metadata": { + "warmup": 60, + "queryAggregation": "max", + "repetitionAggregation": "median", + "operator": "lt", + "threshold": 2000000 + } +} \ No newline at end of file diff --git a/theodolite/.gitignore b/theodolite/.gitignore index a1eff0e1d4dddacdbcafa2c235b28616cb53e7bf..285b6baee527835a20f0b79f1ecece49b80f7d42 100644 --- a/theodolite/.gitignore +++ b/theodolite/.gitignore @@ -31,3 +31,6 @@ nb-configuration.xml # patch *.orig *.rej + +# Local environment +.env diff --git a/theodolite/README.md b/theodolite/README.md index fe3b4f704a2c288aa56ef8067f6d4d86823d2989..96f56c20db1d0796ba692cc497b93532517526ff 100644 --- a/theodolite/README.md +++ b/theodolite/README.md @@ -12,14 +12,8 @@ You can run your application in dev mode using: ./gradlew quarkusDev ``` -### Hint for running with k3s (or k3d) +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. -You may need to add the following dependencies to the `build.gradle` file when running Theodolite with k3s. - -``` -implementation 'org.bouncycastle:bcprov-ext-jdk15on:1.68' -implementation 'org.bouncycastle:bcpkix-jdk15on:1.68' -``` ## Packaging and running the application @@ -59,7 +53,7 @@ Or, if you don't have GraalVM installed, you can run the native executable build You can then execute your native executable with: ```./build/theodolite-0.7.0-SNAPSHOT-runner``` -If you want to learn more about building native executables, please consult <https://quarkus.io/guides/gradle-tooling>. +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. ## Build docker images diff --git a/theodolite/build.gradle b/theodolite/build.gradle index a758bfbae778b94a9ea5d6b6a9b49a9db75ba03d..06d451cc24395824650e88d2fe516eb4015a266e 100644 --- a/theodolite/build.gradle +++ b/theodolite/build.gradle @@ -1,9 +1,9 @@ plugins { - id 'org.jetbrains.kotlin.jvm' version "1.3.72" - id "org.jetbrains.kotlin.plugin.allopen" version "1.3.72" + id 'org.jetbrains.kotlin.jvm' version "1.5.31" + id "org.jetbrains.kotlin.plugin.allopen" version "1.5.31" id 'io.quarkus' - id "io.gitlab.arturbosch.detekt" version "1.15.0" //For code style - id "org.jlleitschuh.gradle.ktlint" version "10.0.0" // same as above + id "io.gitlab.arturbosch.detekt" version "1.15.0" + id "org.jlleitschuh.gradle.ktlint" version "10.0.0" } repositories { @@ -18,21 +18,28 @@ dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' implementation 'io.quarkus:quarkus-arc' implementation 'io.quarkus:quarkus-resteasy' - implementation 'com.google.code.gson:gson:2.8.5' - implementation 'org.slf4j:slf4j-simple:1.7.29' - implementation 'io.github.microutils:kotlin-logging:1.12.0' - implementation('io.fabric8:kubernetes-client:5.4.1'){force = true} - implementation('io.fabric8:kubernetes-model-core:5.4.1'){force = true} - implementation('io.fabric8:kubernetes-model-common:5.4.1'){force = true} - implementation 'org.apache.kafka:kafka-clients:2.7.0' + implementation 'io.quarkus:quarkus-kubernetes-client' + + implementation 'org.bouncycastle:bcprov-ext-jdk15on:1.69' + implementation 'org.bouncycastle:bcpkix-jdk15on:1.69' + + implementation 'com.google.code.gson:gson:2.8.9' + implementation 'org.slf4j:slf4j-simple:1.7.32' + implementation 'io.github.microutils:kotlin-logging:2.1.16' + //implementation('io.fabric8:kubernetes-client:5.4.1'){force = true} + //implementation('io.fabric8:kubernetes-model-core:5.4.1'){force = true} + //implementation('io.fabric8:kubernetes-model-common:5.4.1'){force = true} + implementation 'org.apache.kafka:kafka-clients:2.8.0' implementation 'khttp:khttp:1.0.0' - compile 'junit:junit:4.12' + // compile 'junit:junit:4.12' testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.quarkus:quarkus-test-kubernetes-client' testImplementation 'io.rest-assured:rest-assured' - testImplementation 'org.junit-pioneer:junit-pioneer:1.4.0' - testImplementation ('io.fabric8:kubernetes-server-mock:5.4.1'){force = true} + testImplementation 'org.junit-pioneer:junit-pioneer:1.5.0' + //testImplementation 'io.fabric8:kubernetes-server-mock:5.10.1' + testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" } group 'theodolite' @@ -57,6 +64,7 @@ compileKotlin { compileTestKotlin { kotlinOptions.jvmTarget = JavaVersion.VERSION_11 } + detekt { failFast = true // fail build on any finding buildUponDefaultConfig = true diff --git a/theodolite/crd/crd-benchmark.yaml b/theodolite/crd/crd-benchmark.yaml index cd9c9f1e07c38a8727bcd23939319c0955e07645..55bf6ed69e44287905bce85b63f66bb43ea65669 100644 --- a/theodolite/crd/crd-benchmark.yaml +++ b/theodolite/crd/crd-benchmark.yaml @@ -467,7 +467,7 @@ spec: - name: Age type: date jsonPath: .metadata.creationTimestamp - - name: STATUS + - name: Status type: string description: The status of a Benchmark indicates whether all resources are available to start the benchmark or not. jsonPath: .status.resourceSetsState diff --git a/theodolite/crd/crd-execution.yaml b/theodolite/crd/crd-execution.yaml index d9cd41903bb2fdc18bd6640bdbe2eb764b2106ab..92a8ca18d87009143620097caf2abfe8da202c82 100644 --- a/theodolite/crd/crd-execution.yaml +++ b/theodolite/crd/crd-execution.yaml @@ -133,10 +133,18 @@ spec: description: "" type: string executionDuration: - description: "Duration of the execution in seconds" + description: "Duration of the execution" type: string + startTime: + description: "Time this execution started" + type: string + format: date-time + completionTime: + description: "Time when this execution was stopped" + type: string + format: date-time additionalPrinterColumns: - - name: STATUS + - name: Status type: string description: State of the execution jsonPath: .status.executionState diff --git a/theodolite/gradle.properties b/theodolite/gradle.properties index d7e4187c25e76dfb440650274b2d383f75a32242..76ed8f2136f14263460bc391d420c78de200d659 100644 --- a/theodolite/gradle.properties +++ b/theodolite/gradle.properties @@ -1,8 +1,8 @@ #Gradle properties +quarkusPluginVersion=2.5.2.Final +quarkusPlatformArtifactId=quarkus-bom quarkusPluginId=io.quarkus -quarkusPluginVersion=1.10.3.Final -quarkusPlatformGroupId=io.quarkus -quarkusPlatformArtifactId=quarkus-universe-bom -quarkusPlatformVersion=1.10.3.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformVersion=2.5.2.Final -org.gradle.logging.level=INFO \ No newline at end of file +#org.gradle.logging.level=INFO \ No newline at end of file diff --git a/theodolite/gradle/wrapper/gradle-wrapper.properties b/theodolite/gradle/wrapper/gradle-wrapper.properties index bb8b2fc26b2e572c79d7212a4f6f11057c6787f7..e750102e09269a4ac558e10a6612998e5ca4c0f2 100644 --- a/theodolite/gradle/wrapper/gradle-wrapper.properties +++ b/theodolite/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/theodolite/gradlew.bat b/theodolite/gradlew.bat old mode 100755 new mode 100644 diff --git a/theodolite/src/main/docker/Dockerfile.jvm b/theodolite/src/main/docker/Dockerfile.jvm index 4d51240e0225bb571cc4a625e40c9ec76fd8f10d..03035752038fee2e5ce4c477c61adc84991f3729 100644 --- a/theodolite/src/main/docker/Dockerfile.jvm +++ b/theodolite/src/main/docker/Dockerfile.jvm @@ -14,14 +14,14 @@ # docker run -i --rm -p 8080:8080 quarkus/theodolite-jvm # # If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050 +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 # # Then run the container using : # # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/theodolite-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 @@ -38,14 +38,18 @@ RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ && chown 1001 /deployments/run-java.sh \ && chmod 540 /deployments/run-java.sh \ - && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -COPY build/lib/* /deployments/lib/ -COPY build/*-runner.jar /deployments/app.jar +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=1001 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=1001 build/quarkus-app/*.jar /deployments/ +COPY --chown=1001 build/quarkus-app/app/ /deployments/app/ +COPY --chown=1001 build/quarkus-app/quarkus/ /deployments/quarkus/ EXPOSE 8080 USER 1001 ENTRYPOINT [ "/deployments/run-java.sh" ] + diff --git a/theodolite/src/main/docker/Dockerfile.fast-jar b/theodolite/src/main/docker/Dockerfile.legacy-jar similarity index 67% rename from theodolite/src/main/docker/Dockerfile.fast-jar rename to theodolite/src/main/docker/Dockerfile.legacy-jar index 16853dd8f064565ae017bee9dae3597b63085006..f9dffd188570c14087bafaec838b58b61a4e5912 100644 --- a/theodolite/src/main/docker/Dockerfile.fast-jar +++ b/theodolite/src/main/docker/Dockerfile.legacy-jar @@ -3,25 +3,25 @@ # # Before building the container image run: # -# ./gradlew build -Dquarkus.package.type=fast-jar +# ./gradlew build -Dquarkus.package.type=legacy-jar # # Then, build the image with: # -# docker build -f src/main/docker/Dockerfile.fast-jar -t quarkus/theodolite-fast-jar . +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/theodolite-legacy-jar . # # Then run the container using: # -# docker run -i --rm -p 8080:8080 quarkus/theodolite-fast-jar +# docker run -i --rm -p 8080:8080 quarkus/theodolite-legacy-jar # # If you want to include the debug port into your docker image -# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050 +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 # # Then run the container using : # -# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/theodolite-fast-jar +# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/theodolite-legacy-jar # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 @@ -38,15 +38,12 @@ RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ && chown 1001 /deployments/run-java.sh \ && chmod 540 /deployments/run-java.sh \ - && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -# We make four distinct layers so if there are application changes the library layers can be re-used -COPY --chown=1001 build/quarkus-app/lib/ /deployments/lib/ -COPY --chown=1001 build/quarkus-app/*.jar /deployments/ -COPY --chown=1001 build/quarkus-app/app/ /deployments/app/ -COPY --chown=1001 build/quarkus-app/quarkus/ /deployments/quarkus/ +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/app.jar EXPOSE 8080 USER 1001 diff --git a/theodolite/src/main/docker/Dockerfile.native b/theodolite/src/main/docker/Dockerfile.native index 95ef4fb51d7dc1ac520fb4c5a9af1b2d0a32fd09..04a1dd6f2b6cc99511bf705eed5d98be1da25b05 100644 --- a/theodolite/src/main/docker/Dockerfile.native +++ b/theodolite/src/main/docker/Dockerfile.native @@ -14,8 +14,8 @@ # docker run -i --rm -p 8080:8080 quarkus/theodolite # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3 -WORKDIR /deployments +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 +WORKDIR /deployments/ RUN chown 1001 /deployments \ && chmod "g+rwX" /deployments \ && chown 1001:root /deployments diff --git a/theodolite/src/main/docker/Dockerfile.native-distroless b/theodolite/src/main/docker/Dockerfile.native-distroless new file mode 100644 index 0000000000000000000000000000000000000000..1ed64110dd931bf3fea9100e3318318ad40b6966 --- /dev/null +++ b/theodolite/src/main/docker/Dockerfile.native-distroless @@ -0,0 +1,24 @@ +#### +# This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/theodolite . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/theodolite +# +### +FROM quay.io/quarkus/quarkus-distroless-image:1.0 +WORKDIR /deployments/ +COPY build/*-runner /deployments/application + +EXPOSE 8080 +USER nonroot + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/ConfigMapResourceSet.kt b/theodolite/src/main/kotlin/theodolite/benchmark/ConfigMapResourceSet.kt index 273a13170e77ae9e2f5f09869ebbc5cc06185715..27e3206ad7b60d61cab94caaef8a3279d834fe65 100644 --- a/theodolite/src/main/kotlin/theodolite/benchmark/ConfigMapResourceSet.kt +++ b/theodolite/src/main/kotlin/theodolite/benchmark/ConfigMapResourceSet.kt @@ -5,14 +5,10 @@ import io.fabric8.kubernetes.api.model.KubernetesResource import io.fabric8.kubernetes.client.KubernetesClientException import io.fabric8.kubernetes.client.NamespacedKubernetesClient import io.quarkus.runtime.annotations.RegisterForReflection -import mu.KotlinLogging import theodolite.k8s.resourceLoader.K8sResourceLoaderFromString import theodolite.util.DeploymentFailedException import theodolite.util.YamlParserFromString import java.lang.IllegalArgumentException -import java.lang.IllegalStateException - -private val logger = KotlinLogging.logger {} @RegisterForReflection @JsonDeserialize @@ -20,30 +16,26 @@ class ConfigMapResourceSet: ResourceSet, KubernetesResource { lateinit var name: String lateinit var files: List<String> // load all files, iff files is not set - @OptIn(ExperimentalStdlibApi::class) override fun getResourceSet(client: NamespacedKubernetesClient): Collection<Pair<String, KubernetesResource>> { val loader = K8sResourceLoaderFromString(client) var resources: Map<String, String> try { - resources = client + resources = (client .configMaps() .withName(name) - .get() + .get() ?: throw DeploymentFailedException("Cannot find ConfigMap with name '$name'.")) .data - .filter { it.key.endsWith(".yaml") } // consider only yaml files, e.g. ignore readme files + .filter { it.key.endsWith(".yaml") } } catch (e: KubernetesClientException) { - throw DeploymentFailedException("can not find or read configmap: $name", e) - } catch (e: IllegalStateException) { - throw DeploymentFailedException("can not find configmap or data section is null $name", e) + throw DeploymentFailedException("Cannot find or read ConfigMap with name '$name'.", e) } if (::files.isInitialized){ - resources = resources - .filter { files.contains(it.key) } + resources = resources.filter { files.contains(it.key) } if (resources.size != files.size) { - throw DeploymentFailedException("Could not find all specified Kubernetes manifests files") + throw DeploymentFailedException("Could not find all specified Kubernetes manifests files") } } @@ -57,7 +49,7 @@ class ConfigMapResourceSet: ResourceSet, KubernetesResource { it.second.key, loader.loadK8sResource(it.first, it.second.value)) } } catch (e: IllegalArgumentException) { - throw DeploymentFailedException("Can not creat resource set from specified configmap", e) + throw DeploymentFailedException("Can not create resource set from specified configmap", e) } } diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/FileSystemResourceSet.kt b/theodolite/src/main/kotlin/theodolite/benchmark/FileSystemResourceSet.kt index 92df1bec3cd6f21b1f830e73b466f70e37a9f4c8..e769f8b9883b98d9787f2de65571fc94056c3b9c 100644 --- a/theodolite/src/main/kotlin/theodolite/benchmark/FileSystemResourceSet.kt +++ b/theodolite/src/main/kotlin/theodolite/benchmark/FileSystemResourceSet.kt @@ -2,10 +2,8 @@ package theodolite.benchmark import com.fasterxml.jackson.databind.annotation.JsonDeserialize import io.fabric8.kubernetes.api.model.KubernetesResource -import io.fabric8.kubernetes.client.DefaultKubernetesClient import io.fabric8.kubernetes.client.NamespacedKubernetesClient import io.quarkus.runtime.annotations.RegisterForReflection -import mu.KotlinLogging import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile import theodolite.util.DeploymentFailedException import theodolite.util.YamlParserFromFile @@ -13,8 +11,6 @@ import java.io.File import java.io.FileNotFoundException import java.lang.IllegalArgumentException -private val logger = KotlinLogging.logger {} - @RegisterForReflection @JsonDeserialize class FileSystemResourceSet: ResourceSet, KubernetesResource { diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt b/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt index 2514c32158f07f822b34697cb7c4810848bfd27b..70d8b241c84d1c6875c8da3d74cd90b3f57956d6 100644 --- a/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt +++ b/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt @@ -1,6 +1,5 @@ package theodolite.benchmark -import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.annotation.JsonDeserialize import io.fabric8.kubernetes.api.model.KubernetesResource import io.fabric8.kubernetes.client.DefaultKubernetesClient @@ -44,7 +43,7 @@ class KubernetesBenchmark : KubernetesResource, Benchmark { lateinit var infrastructure: Resources lateinit var sut: Resources lateinit var loadGenerator: Resources - var namespace = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE + private var namespace = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE @Transient private var client: NamespacedKubernetesClient = DefaultKubernetesClient().inNamespace(namespace) diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/ResourceSets.kt b/theodolite/src/main/kotlin/theodolite/benchmark/ResourceSets.kt index a4fe443e7f304c411792ee06c32592ba3c9e692a..b6364949727d4ea134e348ce8b79e22334753c1c 100644 --- a/theodolite/src/main/kotlin/theodolite/benchmark/ResourceSets.kt +++ b/theodolite/src/main/kotlin/theodolite/benchmark/ResourceSets.kt @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize import io.fabric8.kubernetes.api.model.KubernetesResource import io.fabric8.kubernetes.client.NamespacedKubernetesClient import io.quarkus.runtime.annotations.RegisterForReflection -import mu.KotlinLogging import theodolite.util.DeploymentFailedException @JsonDeserialize @@ -14,7 +13,7 @@ import theodolite.util.DeploymentFailedException class ResourceSets: KubernetesResource { @JsonProperty("configMap") @JsonInclude(JsonInclude.Include.NON_NULL) - var configMap: ConfigMapResourceSet? = null + var configMap: ConfigMapResourceSet? = null @JsonProperty("fileSystem") @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt b/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt index 281c68e318784ee8206473cd014f814b3f5152a9..be3e48be406b631e03ca2fd32909a442b592f259 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt @@ -1,6 +1,5 @@ package theodolite.evaluation -import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution import theodolite.util.EvaluationFailedException import theodolite.util.IOHandler @@ -12,8 +11,6 @@ import java.time.Instant import java.util.* import java.util.regex.Pattern -private val logger = KotlinLogging.logger {} - /** * Contains the analysis. Fetches a metric from Prometheus, documents it, and evaluates it. * @param slo Slo that is used for the analysis. @@ -37,7 +34,6 @@ class AnalysisExecutor( * @return true if the experiment succeeded. */ fun analyze(load: LoadDimension, res: Resource, executionIntervals: List<Pair<Instant, Instant>>): Boolean { - var result: Boolean var repetitionCounter = 1 try { @@ -50,7 +46,7 @@ class AnalysisExecutor( fetcher.fetchMetric( start = interval.first, end = interval.second, - query = SloConfigHandler.getQueryString(sloType = slo.sloType) + query = SloConfigHandler.getQueryString(slo = slo) ) } @@ -68,12 +64,11 @@ class AnalysisExecutor( load = load ) - result = sloChecker.evaluate(prometheusData) + return sloChecker.evaluate(prometheusData) } catch (e: Exception) { - throw EvaluationFailedException("Evaluation failed for resource '${res.get()}' and load '${load.get()} ", e) + throw EvaluationFailedException("Evaluation failed for resource '${res.get()}' and load '${load.get()}", e) } - return result } private val NONLATIN: Pattern = Pattern.compile("[^\\w-]") @@ -83,6 +78,6 @@ class AnalysisExecutor( val noWhitespace: String = WHITESPACE.matcher(this).replaceAll("-") val normalized: String = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD) val slug: String = NONLATIN.matcher(normalized).replaceAll("") - return slug.toLowerCase(Locale.ENGLISH) + return slug.lowercase(Locale.ENGLISH) } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt b/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt index d646286b70bc5880df1f603afdc2bda22bcc3259..7fb5417e200f64b0db74a8bebe69a751c5d484b8 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt @@ -1,6 +1,5 @@ package theodolite.evaluation -import com.google.gson.Gson import khttp.post import mu.KotlinLogging import theodolite.util.PrometheusResponse @@ -9,13 +8,11 @@ import java.net.ConnectException /** * [SloChecker] that uses an external source for the concrete evaluation. * @param externalSlopeURL The url under which the external evaluation can be reached. - * @param threshold threshold that should not be exceeded to evaluate to true. - * @param warmup time that is not taken into consideration for the evaluation. + * @param metadata metadata passed to the external SLO checker. */ class ExternalSloChecker( private val externalSlopeURL: String, - private val threshold: Int, - private val warmup: Int + private val metadata: Map<String, Any> ) : SloChecker { private val RETRIES = 2 @@ -28,29 +25,25 @@ class ExternalSloChecker( * Will try to reach the external service until success or [RETRIES] times. * Each request will timeout after [TIMEOUT]. * - * @param start point of the experiment. - * @param end point of the experiment. * @param fetchedData that should be evaluated - * @return true if the experiment was successful(the threshold was not exceeded. + * @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(fetchedData: List<PrometheusResponse>): Boolean { var counter = 0 - val data = SloJson.Builder() - .results(fetchedData.map { it.data?.result }) - .addMetadata("threshold", threshold) - .addMetadata( "warmup", warmup) - .build() - .toJson() + val data = SloJson( + results = fetchedData.map { it.data?.result ?: listOf() }, + metadata = metadata + ).toJson() while (counter < RETRIES) { val result = post(externalSlopeURL, data = data, timeout = TIMEOUT) if (result.statusCode != 200) { counter++ - logger.error { "Could not reach external SLO checker" } + logger.error { "Could not reach external SLO checker." } } else { val booleanResult = result.text.toBoolean() - logger.info { "SLO checker result is: $booleanResult" } + logger.info { "SLO checker result is: $booleanResult." } return booleanResult } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt index af70fa5dca3f0556d38791ed96c2af30b9a44a68..82f903f5be868731d58ebefd6279d5d438bd5eab 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt @@ -11,7 +11,7 @@ interface SloChecker { * Evaluates [fetchedData] and returns if the experiments were successful. * * @param fetchedData from Prometheus that will be evaluated. - * @return true if experiments were successful. Otherwise false. + * @return true if experiments were successful. Otherwise, false. */ fun evaluate(fetchedData: List<PrometheusResponse>): Boolean } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt index 64f9110cd931feef41dc65f88d6623e82f4e03a2..f57cebfcb13d0e86919ec15a0a479d1258e318a6 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt @@ -43,15 +43,32 @@ class SloCheckerFactory { properties: MutableMap<String, String>, load: LoadDimension ): SloChecker { - return when (sloType.toLowerCase()) { - SloTypes.LAG_TREND.value, SloTypes.DROPPED_RECORDS.value -> ExternalSloChecker( + return when (SloTypes.from(sloType)) { + SloTypes.GENERIC -> ExternalSloChecker( externalSlopeURL = properties["externalSloUrl"] ?: throw IllegalArgumentException("externalSloUrl expected"), - threshold = properties["threshold"]?.toInt() ?: throw IllegalArgumentException("threshold expected"), - warmup = properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected") + // TODO validate property contents + metadata = mapOf( + "warmup" to (properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected")), + "queryAggregation" to (properties["queryAggregation"] + ?: throw IllegalArgumentException("queryAggregation expected")), + "repetitionAggregation" to (properties["repetitionAggregation"] + ?: throw IllegalArgumentException("repetitionAggregation expected")), + "operator" to (properties["operator"] ?: throw IllegalArgumentException("operator expected")), + "threshold" to (properties["threshold"]?.toInt() + ?: throw IllegalArgumentException("threshold expected")) + ) ) - - SloTypes.LAG_TREND_RATIO.value, SloTypes.DROPPED_RECORDS_RATIO.value -> { + SloTypes.LAG_TREND, SloTypes.DROPPED_RECORDS -> ExternalSloChecker( + externalSlopeURL = properties["externalSloUrl"] + ?: throw IllegalArgumentException("externalSloUrl expected"), + metadata = mapOf( + "warmup" to (properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected")), + "threshold" to (properties["threshold"]?.toInt() + ?: throw IllegalArgumentException("threshold expected")) + ) + ) + SloTypes.LAG_TREND_RATIO, SloTypes.DROPPED_RECORDS_RATIO -> { val thresholdRatio = properties["ratio"]?.toDouble() ?: throw IllegalArgumentException("ratio for threshold expected") @@ -64,11 +81,13 @@ class SloCheckerFactory { ExternalSloChecker( externalSlopeURL = properties["externalSloUrl"] ?: throw IllegalArgumentException("externalSloUrl expected"), - threshold = threshold, - warmup = properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected") + metadata = mapOf( + "warmup" to (properties["warmup"]?.toInt() + ?: throw IllegalArgumentException("warmup expected")), + "threshold" to threshold + ) ) } - else -> throw IllegalArgumentException("Slotype $sloType not found.") } } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt index 93929218c822030ff065dafb19cce1fbaa69a179..425a4f3b0634d53f8b1d5c4b8abdba9ca81c3f2b 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt @@ -1,5 +1,6 @@ package theodolite.evaluation +import theodolite.benchmark.BenchmarkExecution import theodolite.util.InvalidPatcherConfigurationException import javax.enterprise.context.ApplicationScoped @@ -7,13 +8,14 @@ private const val CONSUMER_LAG_QUERY = "sum by(group)(kafka_consumergroup_group_ private const val DROPPED_RECORDS_QUERY = "sum by(job) (kafka_streams_stream_task_metrics_dropped_records_total>=0)" @ApplicationScoped -class SloConfigHandler() { +class SloConfigHandler { companion object { - fun getQueryString(sloType: String): String { - return when (sloType.toLowerCase()) { + fun getQueryString(slo: BenchmarkExecution.Slo): String { + return when (slo.sloType.toLowerCase()) { + SloTypes.GENERIC.value -> slo.properties["promQLQuery"] ?: throw IllegalArgumentException("promQLQuery expected") SloTypes.LAG_TREND.value, SloTypes.LAG_TREND_RATIO.value -> CONSUMER_LAG_QUERY SloTypes.DROPPED_RECORDS.value, SloTypes.DROPPED_RECORDS_RATIO.value -> DROPPED_RECORDS_QUERY - else -> throw InvalidPatcherConfigurationException("Could not find Prometheus query string for slo type $sloType") + else -> throw InvalidPatcherConfigurationException("Could not find Prometheus query string for slo type $slo.sloType") } } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt index fc9fe17b255dbb5ae68881538d8d2a50a191edb1..205389276f2c1adef6cba6c745baf99744c8d2dd 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt @@ -3,61 +3,17 @@ package theodolite.evaluation import com.google.gson.Gson import theodolite.util.PromResult -class SloJson private constructor( - val results: List<List<PromResult>?>? = null, - var metadata: MutableMap<String, Any>? = null +class SloJson constructor( + val results: List<List<PromResult>>, + var metadata: Map<String, Any> ) { - data class Builder( - var results:List<List<PromResult>?>? = null, - var metadata: MutableMap<String, Any>? = null - ) { - - /** - * Set the results - * - * @param results list of prometheus results - */ - fun results(results: List<List<PromResult>?>) = apply { this.results = results } - - /** - * Add metadata as key value pairs - * - * @param key key of the metadata to be added - * @param value value of the metadata to be added - */ - fun addMetadata(key: String, value: String) = apply { - if (this.metadata.isNullOrEmpty()) { - this.metadata = mutableMapOf(key to value) - } else { - this.metadata!![key] = value - } - } - - /** - * Add metadata as key value pairs - * - * @param key key of the metadata to be added - * @param value value of the metadata to be added - */ - fun addMetadata(key: String, value: Int) = apply { - if (this.metadata.isNullOrEmpty()) { - this.metadata = mutableMapOf(key to value) - } else { - this.metadata!![key] = value - } - } - - fun build() = SloJson( - results = results, - metadata = metadata + fun toJson(): String { + return Gson().toJson( + mapOf( + "results" to this.results, + "metadata" to this.metadata + ) ) } - - fun toJson(): String { - return Gson().toJson(mapOf( - "results" to this.results, - "metadata" to this.metadata - )) - } } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt index ac9de35861b0bd9c012bfb0b8cfcb2e1aa5aed68..812b50de779d2f3abfd5788b8aee145edc959e6c 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt @@ -1,10 +1,14 @@ package theodolite.evaluation enum class SloTypes(val value: String) { + GENERIC("generic"), LAG_TREND("lag trend"), LAG_TREND_RATIO("lag trend ratio"), DROPPED_RECORDS("dropped records"), - DROPPED_RECORDS_RATIO("dropped records ratio") - + DROPPED_RECORDS_RATIO("dropped records ratio"); + companion object { + fun from(type: String): SloTypes = + values().find { it.value == type } ?: throw IllegalArgumentException("Requested SLO does not exist") + } } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/execution/ExecutionModes.kt b/theodolite/src/main/kotlin/theodolite/execution/ExecutionModes.kt index bf947be01b534fd000d3967f0b72ef25978d4110..370b87e062d942a512e059ee4041dca776376ddf 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/ExecutionModes.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/ExecutionModes.kt @@ -2,6 +2,5 @@ package theodolite.execution enum class ExecutionModes(val value: String) { OPERATOR("operator"), - YAML_EXECUTOR("yaml-executor"), STANDALONE("standalone") } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/execution/Main.kt b/theodolite/src/main/kotlin/theodolite/execution/Main.kt index 11f696ddd739e987e92ecec724390948714d898b..17b3d4e7b86f9e430abfb6093e79aa7865cd5923 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/Main.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/Main.kt @@ -17,8 +17,8 @@ object Main { val mode = Configuration.EXECUTION_MODE logger.info { "Start Theodolite with mode $mode" } - when (mode.toLowerCase()) { - ExecutionModes.STANDALONE.value, ExecutionModes.YAML_EXECUTOR.value -> TheodoliteStandalone().start() // TODO remove standalone (#209) + when (mode.lowercase()) { + ExecutionModes.STANDALONE.value -> TheodoliteStandalone().start() ExecutionModes.OPERATOR.value -> TheodoliteOperator().start() else -> { logger.error { "MODE $mode not found" } diff --git a/theodolite/src/main/kotlin/theodolite/execution/Shutdown.kt b/theodolite/src/main/kotlin/theodolite/execution/Shutdown.kt index 6dedc94af864269d7d15929c69ec54aa384fc8e3..29ac39c122f68636e08c6c5ecd5a6c01751edafb 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/Shutdown.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/Shutdown.kt @@ -14,14 +14,13 @@ private val logger = KotlinLogging.logger {} * @property benchmarkExecution * @property benchmark */ -class Shutdown(private val benchmarkExecution: BenchmarkExecution, private val benchmark: KubernetesBenchmark) : - Thread() { +class Shutdown(private val benchmarkExecution: BenchmarkExecution, private val benchmark: KubernetesBenchmark) { /** * Run * Delete all Kubernetes resources which are related to the execution and the benchmark. */ - override fun run() { + fun run() { // Build Configuration to teardown try { logger.info { "Received shutdown signal -> Shutting down" } @@ -34,9 +33,7 @@ class Shutdown(private val benchmarkExecution: BenchmarkExecution, private val b afterTeardownDelay = 5L ) deployment.teardown() - logger.info { - "Finished teardown of all benchmark resources." - } + logger.info { "Finished teardown of all benchmark resources." } } catch (e: Exception) { logger.warn { "Could not delete all specified resources from Kubernetes. " + diff --git a/theodolite/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt b/theodolite/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt index 315d1cf1afe7fd2ffbfc1c437d725d4dff29f637..8596576e0a7984c32b6dabf90c6bbf06961d2bb1 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt @@ -137,6 +137,12 @@ class TheodoliteExecutor( config.compositeStrategy.benchmarkExecutor.results, "${resultsFolder}exp${this.config.executionId}-result" ) + // Create expXYZ_demand.csv file + ioHandler.writeToCSVFile( + "${resultsFolder}exp${this.config.executionId}_demand", + calculateDemandMetric(config.loads, config.compositeStrategy.benchmarkExecutor.results), + listOf("load","resources") + ) } kubernetesBenchmark.teardownInfrastructure() } @@ -151,4 +157,8 @@ class TheodoliteExecutor( return executionID } + private fun calculateDemandMetric(loadDimensions: List<LoadDimension>, results: Results): List<List<String>> { + return loadDimensions.map { listOf(it.get().toString(), results.getMinRequiredInstances(it).get().toString()) } + } + } diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt index 0b5d6040bdea1316f8fb55bcc3f204c5443f6eee..93536282e2eefe6e476c3fde3fd86860fa24dcc3 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/AbstractStateHandler.kt @@ -2,8 +2,6 @@ package theodolite.execution.operator import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.api.model.KubernetesResourceList -import io.fabric8.kubernetes.api.model.Namespaced -import io.fabric8.kubernetes.client.CustomResource import io.fabric8.kubernetes.client.KubernetesClientException import io.fabric8.kubernetes.client.NamespacedKubernetesClient import io.fabric8.kubernetes.client.dsl.MixedOperation @@ -12,30 +10,30 @@ import mu.KotlinLogging import java.lang.Thread.sleep private val logger = KotlinLogging.logger {} -abstract class AbstractStateHandler<T, L, D>( +private const val MAX_RETRIES: Int = 5 + +abstract class AbstractStateHandler<S : HasMetadata>( private val client: NamespacedKubernetesClient, - private val crd: Class<T>, - private val crdList: Class<L> -) : StateHandler<T> where T : CustomResource<*, *>?, T : HasMetadata, T : Namespaced, L : KubernetesResourceList<T> { + private val crd: Class<S> +) { - private val crdClient: MixedOperation<T, L, Resource<T>> = - this.client.customResources(this.crd, this.crdList) + private val crdClient: MixedOperation<S, KubernetesResourceList<S>, Resource<S>> = this.client.resources(this.crd) @Synchronized - override fun setState(resourceName: String, f: (T) -> T?) { + fun setState(resourceName: String, setter: (S) -> S?) { try { - this.crdClient - .list().items - .filter { it.metadata.name == resourceName } - .map { customResource -> f(customResource) } - .forEach { this.crdClient.updateStatus(it) } + val resource = this.crdClient.withName(resourceName).get() + if (resource != null) { + val resourcePatched = setter(resource) + this.crdClient.patchStatus(resourcePatched) + } } catch (e: KubernetesClientException) { - logger.warn { "Status cannot be set for resource $resourceName" } + logger.warn(e) { "Status cannot be set for resource $resourceName." } } } @Synchronized - override fun getState(resourceName: String, f: (T) -> String?): String? { + fun getState(resourceName: String, f: (S) -> String?): String? { return this.crdClient .list().items .filter { it.metadata.name == resourceName } @@ -44,13 +42,13 @@ abstract class AbstractStateHandler<T, L, D>( } @Synchronized - override fun blockUntilStateIsSet( + fun blockUntilStateIsSet( resourceName: String, desiredStatusString: String, - f: (T) -> String?, - maxTries: Int + f: (S) -> String?, + maxRetries: Int = MAX_RETRIES ): Boolean { - for (i in 0.rangeTo(maxTries)) { + for (i in 0.rangeTo(maxRetries)) { val currentStatus = getState(resourceName, f) if (currentStatus == desiredStatusString) { return true diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateChecker.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateChecker.kt index 959b04a8e5c94806aea1753af56b2518436aed12..40f5b7ddbbfc9da4514b8a88946d97149b94b390 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateChecker.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateChecker.kt @@ -10,7 +10,7 @@ import theodolite.benchmark.ActionSelector import theodolite.benchmark.KubernetesBenchmark import theodolite.benchmark.ResourceSets import theodolite.model.crd.BenchmarkCRD -import theodolite.model.crd.BenchmarkStates +import theodolite.model.crd.BenchmarkState import theodolite.model.crd.KubernetesBenchmarkList class BenchmarkStateChecker( @@ -42,7 +42,7 @@ class BenchmarkStateChecker( .forEach { setState(it.first, it.second) } } - private fun setState(resource: BenchmarkCRD, state: BenchmarkStates) { + private fun setState(resource: BenchmarkCRD, state: BenchmarkState) { benchmarkStateHandler.setResourceSetState(resource.spec.name, state) } @@ -52,13 +52,13 @@ class BenchmarkStateChecker( * @param benchmark The benchmark to check * @return [BenchmarkStates.READY] iff all resource could be loaded and all actions could be executed, [BenchmarkStates.PENDING] else */ - private fun checkState(benchmark: KubernetesBenchmark): BenchmarkStates { - return if (checkActionCommands(benchmark) == BenchmarkStates.READY - && checkResources(benchmark) == BenchmarkStates.READY + private fun checkState(benchmark: KubernetesBenchmark): BenchmarkState { + return if (checkActionCommands(benchmark) == BenchmarkState.READY + && checkResources(benchmark) == BenchmarkState.READY ) { - BenchmarkStates.READY + BenchmarkState.READY } else { - BenchmarkStates.PENDING + BenchmarkState.PENDING } } @@ -68,15 +68,15 @@ class BenchmarkStateChecker( * @param benchmark The benchmark to check * @return The state of this benchmark. [BenchmarkStates.READY] if all actions could be executed, else [BenchmarkStates.PENDING] */ - private fun checkActionCommands(benchmark: KubernetesBenchmark): BenchmarkStates { + private fun checkActionCommands(benchmark: KubernetesBenchmark): BenchmarkState { return if (checkIfActionPossible(benchmark.infrastructure.resources, benchmark.sut.beforeActions) && checkIfActionPossible(benchmark.infrastructure.resources, benchmark.sut.afterActions) && checkIfActionPossible(benchmark.infrastructure.resources, benchmark.loadGenerator.beforeActions) && checkIfActionPossible(benchmark.infrastructure.resources, benchmark.loadGenerator.beforeActions) ) { - BenchmarkStates.READY + BenchmarkState.READY } else { - BenchmarkStates.PENDING + BenchmarkState.PENDING } } @@ -171,21 +171,21 @@ class BenchmarkStateChecker( * Checks if it is possible to load all specified Kubernetes manifests. * * @param benchmark The benchmark to check - * @return The state of this benchmark. [BenchmarkStates.READY] if all resources could be loaded, else [BenchmarkStates.PENDING] + * @return The state of this benchmark. [BenchmarkState.READY] if all resources could be loaded, else [BenchmarkState.PENDING] */ - fun checkResources(benchmark: KubernetesBenchmark): BenchmarkStates { + fun checkResources(benchmark: KubernetesBenchmark): BenchmarkState { return try { val appResources = benchmark.loadKubernetesResources(resourceSet = benchmark.sut.resources) val loadGenResources = benchmark.loadKubernetesResources(resourceSet = benchmark.loadGenerator.resources) if (appResources.isNotEmpty() && loadGenResources.isNotEmpty()) { - BenchmarkStates.READY + BenchmarkState.READY } else { - BenchmarkStates.PENDING + BenchmarkState.PENDING } } catch (e: Exception) { - BenchmarkStates.PENDING + BenchmarkState.PENDING } } } diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt index adca2a8b7fdb9b3e610f15e57c011679869df14c..3b46859737d86a34b58a5514c0ae31ae215b9c7d 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateHandler.kt @@ -4,25 +4,24 @@ import io.fabric8.kubernetes.client.NamespacedKubernetesClient import theodolite.model.crd.* class BenchmarkStateHandler(val client: NamespacedKubernetesClient) : - AbstractStateHandler<BenchmarkCRD, KubernetesBenchmarkList, ExecutionStatus>( + AbstractStateHandler<BenchmarkCRD>( client = client, - crd = BenchmarkCRD::class.java, - crdList = KubernetesBenchmarkList::class.java + crd = BenchmarkCRD::class.java ) { - private fun getBenchmarkResourceState() = { cr: BenchmarkCRD -> cr.status.resourceSetsState } + private fun getBenchmarkResourceState() = { cr: BenchmarkCRD -> cr.status.resourceSetsState.value } - fun setResourceSetState(resourceName: String, status: BenchmarkStates): Boolean { - setState(resourceName) { cr -> cr.status.resourceSetsState = status.value; cr } + fun setResourceSetState(resourceName: String, status: BenchmarkState): Boolean { + setState(resourceName) { cr -> cr.status.resourceSetsState = status; cr } return blockUntilStateIsSet(resourceName, status.value, getBenchmarkResourceState()) } - fun getResourceSetState(resourceName: String): ExecutionStates { + fun getResourceSetState(resourceName: String): ExecutionState { val status = this.getState(resourceName, getBenchmarkResourceState()) return if (status.isNullOrBlank()) { - ExecutionStates.NO_STATE + ExecutionState.NO_STATE } else { - ExecutionStates.values().first { it.value == status } + ExecutionState.values().first { it.value == status } } } } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/ClusterSetup.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/ClusterSetup.kt index efca98f8bf72024daa0367c6c57574f0644872e4..885315df6eda0d91a27567720056738b997a8ec1 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/ClusterSetup.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/ClusterSetup.kt @@ -3,14 +3,11 @@ 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 theodolite.execution.Shutdown import theodolite.k8s.K8sContextFactory import theodolite.k8s.ResourceByLabelHandler import theodolite.model.crd.* -private val logger = KotlinLogging.logger {} - class ClusterSetup( private val executionCRDClient: MixedOperation<ExecutionCRD, BenchmarkExecutionList, Resource<ExecutionCRD>>, private val benchmarkCRDClient: MixedOperation<BenchmarkCRD, KubernetesBenchmarkList, Resource<BenchmarkCRD>>, @@ -41,7 +38,7 @@ class ClusterSetup( .list() .items .asSequence() - .filter { it.status.executionState == ExecutionStates.RUNNING.value } + .filter { it.status.executionState == ExecutionState.RUNNING } .forEach { execution -> val benchmark = benchmarkCRDClient .inNamespace(client.namespace) @@ -52,9 +49,9 @@ class ClusterSetup( if (benchmark != null) { execution.spec.name = execution.metadata.name benchmark.spec.name = benchmark.metadata.name - Shutdown(execution.spec, benchmark.spec).start() + Shutdown(execution.spec, benchmark.spec).run() } else { - throw IllegalStateException("Execution with state ${ExecutionStates.RUNNING.value} was found, but no corresponding benchmark. " + + throw IllegalStateException("Execution with state ${ExecutionState.RUNNING.value} was found, but no corresponding benchmark. " + "Could not initialize cluster.") } } diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt index 16c4ea98ba614bb3dcdd7d9f486f4e65ae70d380..25c627a350e3939530c4b453ec6db846b546cc08 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionEventHandler.kt @@ -17,25 +17,26 @@ private val logger = KotlinLogging.logger {} * @see TheodoliteController * @see BenchmarkExecution */ -class ExecutionHandler( +class ExecutionEventHandler( private val controller: TheodoliteController, private val stateHandler: ExecutionStateHandler ) : ResourceEventHandler<ExecutionCRD> { + private val gson: Gson = GsonBuilder().enableComplexMapKeySerialization().create() /** - * Add an execution to the end of the queue of the TheodoliteController. + * Adds an execution to the end of the queue of the TheodoliteController. * - * @param ExecutionCRD the execution to add + * @param execution the execution to add */ @Synchronized override fun onAdd(execution: ExecutionCRD) { - logger.info { "Add execution ${execution.metadata.name}" } + logger.info { "Add execution ${execution.metadata.name}." } execution.spec.name = execution.metadata.name when (this.stateHandler.getExecutionState(execution.metadata.name)) { - ExecutionStates.NO_STATE -> this.stateHandler.setExecutionState(execution.spec.name, ExecutionStates.PENDING) - ExecutionStates.RUNNING -> { - this.stateHandler.setExecutionState(execution.spec.name, ExecutionStates.RESTART) + ExecutionState.NO_STATE -> this.stateHandler.setExecutionState(execution.spec.name, ExecutionState.PENDING) + ExecutionState.RUNNING -> { + this.stateHandler.setExecutionState(execution.spec.name, ExecutionState.RESTART) if (this.controller.isExecutionRunning(execution.spec.name)) { this.controller.stop(restart = true) } @@ -44,29 +45,29 @@ class ExecutionHandler( } /** - * Updates an execution. If this execution is running at the time this function is called, it is stopped and + * To be called on update of an execution. If this execution is running at the time this function is called, it is stopped and * added to the beginning of the queue of the TheodoliteController. * Otherwise, it is just added to the beginning of the queue. * - * @param oldExecutionCRD the old execution - * @param newExecutionCRD the new execution + * @param oldExecution the old execution + * @param newExecution the new execution */ @Synchronized override fun onUpdate(oldExecution: ExecutionCRD, newExecution: ExecutionCRD) { newExecution.spec.name = newExecution.metadata.name oldExecution.spec.name = oldExecution.metadata.name if (gson.toJson(oldExecution.spec) != gson.toJson(newExecution.spec)) { - logger.info { "Receive update event for execution ${oldExecution.metadata.name}" } + logger.info { "Receive update event for execution ${oldExecution.metadata.name}." } when (this.stateHandler.getExecutionState(newExecution.metadata.name)) { - ExecutionStates.RUNNING -> { - this.stateHandler.setExecutionState(newExecution.spec.name, ExecutionStates.RESTART) + ExecutionState.RUNNING -> { + this.stateHandler.setExecutionState(newExecution.spec.name, ExecutionState.RESTART) if (this.controller.isExecutionRunning(newExecution.spec.name)) { this.controller.stop(restart = true) } } - ExecutionStates.RESTART -> { + ExecutionState.RESTART -> { } // should this set to pending? - else -> this.stateHandler.setExecutionState(newExecution.spec.name, ExecutionStates.PENDING) + else -> this.stateHandler.setExecutionState(newExecution.spec.name, ExecutionState.PENDING) } } } @@ -74,12 +75,12 @@ class ExecutionHandler( /** * Delete an execution from the queue of the TheodoliteController. * - * @param ExecutionCRD the execution to delete + * @param execution the execution to delete */ @Synchronized - override fun onDelete(execution: ExecutionCRD, b: Boolean) { - logger.info { "Delete execution ${execution.metadata.name}" } - if (execution.status.executionState == ExecutionStates.RUNNING.value + override fun onDelete(execution: ExecutionCRD, deletedFinalStateUnknown: Boolean) { + logger.info { "Delete execution ${execution.metadata.name}." } + if (execution.status.executionState == ExecutionState.RUNNING && this.controller.isExecutionRunning(execution.metadata.name) ) { this.controller.stop() diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt index 9f49cf3ee4f9f62e7006dbf6697340e1af152f27..340044e5be954d4d7673120e5bf2cba5aed02d92 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/ExecutionStateHandler.kt @@ -1,80 +1,54 @@ package theodolite.execution.operator +import io.fabric8.kubernetes.api.model.MicroTime import io.fabric8.kubernetes.client.NamespacedKubernetesClient -import theodolite.model.crd.BenchmarkExecutionList import theodolite.model.crd.ExecutionCRD -import theodolite.model.crd.ExecutionStatus -import theodolite.model.crd.ExecutionStates +import theodolite.model.crd.ExecutionState import java.lang.Thread.sleep -import java.time.Duration import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean class ExecutionStateHandler(val client: NamespacedKubernetesClient) : - AbstractStateHandler<ExecutionCRD, BenchmarkExecutionList, ExecutionStatus>( + AbstractStateHandler<ExecutionCRD>( client = client, - crd = ExecutionCRD::class.java, - crdList = BenchmarkExecutionList::class.java + crd = ExecutionCRD::class.java ) { private var runExecutionDurationTimer: AtomicBoolean = AtomicBoolean(false) - private fun getExecutionLambda() = { cr: ExecutionCRD -> cr.status.executionState } + private fun getExecutionLambda() = { cr: ExecutionCRD -> cr.status.executionState.value } - private fun getDurationLambda() = { cr: ExecutionCRD -> cr.status.executionDuration } - - fun setExecutionState(resourceName: String, status: ExecutionStates): Boolean { - setState(resourceName) { cr -> cr.status.executionState = status.value; cr } + fun setExecutionState(resourceName: String, status: ExecutionState): Boolean { + super.setState(resourceName) { cr -> cr.status.executionState = status; cr } return blockUntilStateIsSet(resourceName, status.value, getExecutionLambda()) } - fun getExecutionState(resourceName: String): ExecutionStates { - val status = this.getState(resourceName, getExecutionLambda()) - return if (status.isNullOrBlank()) { - ExecutionStates.NO_STATE - } else { - ExecutionStates.values().first { it.value == status } - } - } - - fun setDurationState(resourceName: String, duration: Duration): Boolean { - setState(resourceName) { cr -> cr.status.executionDuration = durationToK8sString(duration); cr } - return blockUntilStateIsSet(resourceName, durationToK8sString(duration), getDurationLambda()) + fun getExecutionState(resourceName: String): ExecutionState { + val statusString = this.getState(resourceName, getExecutionLambda()) + return ExecutionState.values().first { it.value == statusString } } - fun getDurationState(resourceName: String): String { - val status = getState(resourceName, getDurationLambda()) - return if (status.isNullOrBlank()) { - "-" - } else { - status - } - } - - 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" - } + private fun updateDurationState(resourceName: String) { + super.setState(resourceName) { cr -> cr } } fun startDurationStateTimer(resourceName: String) { this.runExecutionDurationTimer.set(true) - val startTime = Instant.now().toEpochMilli() + + super.setState(resourceName) { cr -> cr.status.completionTime = null; cr } + super.setState(resourceName) { cr -> cr.status.startTime = MicroTime(Instant.now().toString()); cr } + Thread { while (this.runExecutionDurationTimer.get()) { - val duration = Duration.ofMillis(Instant.now().minusMillis(startTime).toEpochMilli()) - setDurationState(resourceName, duration) + updateDurationState(resourceName) sleep(100 * 1) } }.start() } @Synchronized - fun stopDurationStateTimer() { + fun stopDurationStateTimer(resourceName: String) { + super.setState(resourceName) { cr -> cr.status.completionTime = MicroTime(Instant.now().toString()); cr } this.runExecutionDurationTimer.set(false) sleep(100 * 2) } diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt index e2cfaa354443cdc940abf92ef2c7474d028daecf..28563ac5a640d0226224b812a8e0691cde83942a 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/StateHandler.kt @@ -1,15 +1,19 @@ package theodolite.execution.operator -private const val MAX_TRIES: Int = 5 +private const val MAX_RETRIES: Int = 5 +@Deprecated("should not be needed") interface StateHandler<T> { + fun setState(resourceName: String, f: (T) -> T?) + fun getState(resourceName: String, f: (T) -> String?): String? + fun blockUntilStateIsSet( resourceName: String, desiredStatusString: String, f: (T) -> String?, - maxTries: Int = MAX_TRIES + maxRetries: Int = MAX_RETRIES ): Boolean } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt index f066c01024fef98fc3e6e2070b0ed98235a1f8bb..2b6f83c76ce6e31f85cdfec1962f9523c3d297b8 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt @@ -22,7 +22,6 @@ const val CREATED_BY_LABEL_VALUE = "theodolite" * * @see BenchmarkExecution * @see KubernetesBenchmark - * @see ConcurrentLinkedDeque */ class TheodoliteController( @@ -34,7 +33,6 @@ class TheodoliteController( lateinit var executor: TheodoliteExecutor /** - * * Runs the TheodoliteController forever. */ fun run() { @@ -87,20 +85,20 @@ class TheodoliteController( labelName = CREATED_BY_LABEL_NAME ) - executionStateHandler.setExecutionState(execution.name, ExecutionStates.RUNNING) + executionStateHandler.setExecutionState(execution.name, ExecutionState.RUNNING) executionStateHandler.startDurationStateTimer(execution.name) executor = TheodoliteExecutor(execution, benchmark) executor.run() when (executionStateHandler.getExecutionState(execution.name)) { - ExecutionStates.RESTART -> runExecution(execution, benchmark) - ExecutionStates.RUNNING -> { - executionStateHandler.setExecutionState(execution.name, ExecutionStates.FINISHED) + ExecutionState.RESTART -> runExecution(execution, benchmark) + ExecutionState.RUNNING -> { + executionStateHandler.setExecutionState(execution.name, ExecutionState.FINISHED) logger.info { "Execution of ${execution.name} is finally stopped." } } else -> { - executionStateHandler.setExecutionState(execution.name, ExecutionStates.FAILURE) - logger.warn { "Unexpected execution state, set state to ${ExecutionStates.FAILURE.value}" } + executionStateHandler.setExecutionState(execution.name, ExecutionState.FAILURE) + logger.warn { "Unexpected execution state, set state to ${ExecutionState.FAILURE.value}" } } } } catch (e: Exception) { @@ -110,16 +108,16 @@ class TheodoliteController( reason = "Execution failed", message = "An error occurs while executing: ${e.message}") logger.error(e) { "Failure while executing execution ${execution.name} with benchmark ${benchmark.name}." } - executionStateHandler.setExecutionState(execution.name, ExecutionStates.FAILURE) + executionStateHandler.setExecutionState(execution.name, ExecutionState.FAILURE) } - executionStateHandler.stopDurationStateTimer() + executionStateHandler.stopDurationStateTimer(execution.name) } @Synchronized fun stop(restart: Boolean = false) { if (!::executor.isInitialized) return if (restart) { - executionStateHandler.setExecutionState(this.executor.getExecution().name, ExecutionStates.RESTART) + executionStateHandler.setExecutionState(this.executor.getExecution().name, ExecutionState.RESTART) } this.executor.executor.run.set(false) } @@ -142,16 +140,16 @@ class TheodoliteController( * is selected for the next execution depends on three points: * * 1. Only executions are considered for which a matching benchmark is available on the cluster - * 2. The Status of the execution must be [ExecutionStates.PENDING] or [ExecutionStates.RESTART] - * 3. Of the remaining [BenchmarkCRD], those with status [ExecutionStates.RESTART] are preferred, + * 2. The Status of the execution must be [ExecutionState.PENDING] or [ExecutionState.RESTART] + * 3. Of the remaining [BenchmarkCRD], those with status [ExecutionState.RESTART] are preferred, * then, if there is more than one, the oldest execution is chosen. * * @return the next execution or null */ private fun getNextExecution(): BenchmarkExecution? { - val comparator = ExecutionStateComparator(ExecutionStates.RESTART) + val comparator = ExecutionStateComparator(ExecutionState.RESTART) val availableBenchmarkNames = getBenchmarks() - .filter { it.status.resourceSetsState == BenchmarkStates.READY.value } + .filter { it.status.resourceSetsState == BenchmarkState.READY } .map { it.spec } .map { it.name } @@ -161,8 +159,7 @@ class TheodoliteController( .asSequence() .map { it.spec.name = it.metadata.name; it } .filter { - it.status.executionState == ExecutionStates.PENDING.value || - it.status.executionState == ExecutionStates.RESTART.value + it.status.executionState == ExecutionState.PENDING || it.status.executionState == ExecutionState.RESTART } .filter { availableBenchmarkNames.contains(it.spec.benchmark) } .sortedWith(comparator.thenBy { it.metadata.creationTimestamp }) @@ -170,8 +167,6 @@ class TheodoliteController( .firstOrNull() } - - fun isExecutionRunning(executionName: String): Boolean { if (!::executor.isInitialized) return false return this.executor.getExecution().name == executionName diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt index 135ffeaef1a5165482d9d6f7f8f5f3dffd596574..071bd06071345499d01595df72e5de4c8535b3fc 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt @@ -91,7 +91,7 @@ class TheodoliteOperator { ExecutionCRD::class.java, RESYNC_PERIOD ).addEventHandler( - ExecutionHandler( + ExecutionEventHandler( controller = controller, stateHandler = ExecutionStateHandler(client) ) diff --git a/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt b/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt index 28a72c8947bffe7b57203cacf2460d7080fa7b51..518b8eae211dd064e3c12b0713382bf3b12bb1ba 100644 --- a/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/k8s/ResourceByLabelHandler.kt @@ -96,10 +96,9 @@ class ResourceByLabelHandler(private val client: NamespacedKubernetesClient) { /** * Block until all pods with are deleted * - * @param [labelName] the label name - * @param [labelValue] the value of this label + * @param matchLabels Map of label keys to label values to be deleted * */ - fun blockUntilPodsDeleted(matchLabels: MutableMap<String, String>) { + fun blockUntilPodsDeleted(matchLabels: Map<String, String>) { while ( !this.client .pods() @@ -108,7 +107,7 @@ class ResourceByLabelHandler(private val client: NamespacedKubernetesClient) { .items .isNullOrEmpty() ) { - logger.info { "Wait for pods with label ${matchLabels.toString()} to be deleted." } + logger.info { "Wait for pods with label $matchLabels to be deleted." } Thread.sleep(1000) } } diff --git a/theodolite/src/main/kotlin/theodolite/k8s/TopicManager.kt b/theodolite/src/main/kotlin/theodolite/k8s/TopicManager.kt index f2afd71f6e4b4cf8e7106a8fc8a9bd113d9f36e6..ed1e06571d20c53fc82439833c8a31800a48b602 100644 --- a/theodolite/src/main/kotlin/theodolite/k8s/TopicManager.kt +++ b/theodolite/src/main/kotlin/theodolite/k8s/TopicManager.kt @@ -99,7 +99,7 @@ class TopicManager(private val kafkaConfig: Map<String, Any>) { val toDelete = topics.filter { kafkaAdmin.listTopics().names().get().contains(it) } - if (toDelete.isNullOrEmpty()) { + if (toDelete.isEmpty()) { deleted = true } else { logger.info { "Deletion of Kafka topics failed, will retry in ${RETRY_TIME / 1000} seconds." } diff --git a/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/AbstractK8sLoader.kt b/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/AbstractK8sLoader.kt index 862de14e2a7a4721e15215b0a1389e14f943fe24..871b8cf43907fcb8b0b5ea501c6b47f82e56ff69 100644 --- a/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/AbstractK8sLoader.kt +++ b/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/AbstractK8sLoader.kt @@ -9,7 +9,7 @@ private val logger = KotlinLogging.logger {} abstract class AbstractK8sLoader: K8sResourceLoader { fun loadK8sResource(kind: String, resourceString: String): KubernetesResource { - return when (kind.replaceFirst(kind[0],kind[0].toUpperCase())) { + return when (kind.replaceFirst(kind[0],kind[0].uppercaseChar())) { "Deployment" -> loadDeployment(resourceString) "Service" -> loadService(resourceString) "ServiceMonitor" -> loadServiceMonitor(resourceString) @@ -18,13 +18,13 @@ abstract class AbstractK8sLoader: K8sResourceLoader { "Execution" -> loadExecution(resourceString) "Benchmark" -> loadBenchmark(resourceString) else -> { - logger.error { "Error during loading of unspecified resource Kind $kind" } - throw java.lang.IllegalArgumentException("error while loading resource with kind: $kind") + logger.error { "Error during loading of unspecified resource Kind '$kind'." } + throw IllegalArgumentException("error while loading resource with kind: $kind") } } } - fun <T> loadGenericResource(resourceString: String, f: (String) -> T): T { + fun <T : KubernetesResource> loadGenericResource(resourceString: String, f: (String) -> T): T { var resource: T? = null try { diff --git a/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/K8sResourceLoaderFromString.kt b/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/K8sResourceLoaderFromString.kt index e9611aaa82870dfb676820029cf42c5aab63d672..639e4c4584d47968cd718d601f1cd7064d85eda2 100644 --- a/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/K8sResourceLoaderFromString.kt +++ b/theodolite/src/main/kotlin/theodolite/k8s/resourceLoader/K8sResourceLoaderFromString.kt @@ -2,42 +2,37 @@ package theodolite.k8s.resourceLoader 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.NamespacedKubernetesClient import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext import theodolite.k8s.CustomResourceWrapper import theodolite.util.YamlParserFromString import java.io.ByteArrayInputStream +import java.io.InputStream class K8sResourceLoaderFromString(private val client: NamespacedKubernetesClient): AbstractK8sLoader(), K8sResourceLoader { - @OptIn(ExperimentalStdlibApi::class) - override fun loadService(resource: String): KubernetesResource { - return loadGenericResource(resource) { x: String -> - val stream = ByteArrayInputStream(x.encodeToByteArray()) - client.services().load(stream).get() } + override fun loadService(resource: String): Service { + return loadAnyResource(resource) { stream -> client.services().load(stream).get() } } - @OptIn(ExperimentalStdlibApi::class) override fun loadDeployment(resource: String): Deployment { - return loadGenericResource(resource) { x: String -> - val stream = ByteArrayInputStream(x.encodeToByteArray()) - client.apps().deployments().load(stream).get() } + return loadAnyResource(resource) { stream -> client.apps().deployments().load(stream).get() } } - @OptIn(ExperimentalStdlibApi::class) override fun loadConfigmap(resource: String): ConfigMap { - return loadGenericResource(resource) { x: String -> - val stream = ByteArrayInputStream(x.encodeToByteArray()) - client.configMaps().load(stream).get() } + return loadAnyResource(resource) { stream -> client.configMaps().load(stream).get() } } - @OptIn(ExperimentalStdlibApi::class) - override fun loadStatefulSet(resource: String): KubernetesResource { - return loadGenericResource(resource) { x: String -> - val stream = ByteArrayInputStream(x.encodeToByteArray()) - client.apps().statefulSets().load(stream).get() } + override fun loadStatefulSet(resource: String): StatefulSet { + return loadAnyResource(resource) { stream -> client.apps().statefulSets().load(stream).get() } + } + + private fun <T : KubernetesResource> loadAnyResource(resource: String, f: (InputStream) -> T): T { + return loadGenericResource(resource) { f(ByteArrayInputStream(it.encodeToByteArray())) } } /** diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt index b6468fff523e57b124e144d5b9fef6477973655a..0ec6decbdea5e192721a4f9b6d0d85ea65665a5a 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkCRD.kt @@ -1,7 +1,6 @@ package theodolite.model.crd import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.api.model.Namespaced import io.fabric8.kubernetes.client.CustomResource import io.fabric8.kubernetes.model.annotation.Group @@ -13,7 +12,14 @@ import theodolite.benchmark.KubernetesBenchmark @Version("v1") @Group("theodolite.com") @Kind("benchmark") -class BenchmarkCRD( - var spec: KubernetesBenchmark = KubernetesBenchmark(), - var status: BenchmarkStatus = BenchmarkStatus() -) : CustomResource<KubernetesBenchmark, BenchmarkStatus>(), Namespaced, HasMetadata \ No newline at end of file +class BenchmarkCRD : CustomResource<KubernetesBenchmark, BenchmarkStatus>(), Namespaced { + + override fun initSpec(): KubernetesBenchmark { + return KubernetesBenchmark() + } + + override fun initStatus(): BenchmarkStatus { + return BenchmarkStatus() + } + +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkState.kt b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkState.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc2c6f9ba971367c0bb142a54745629eb29c07d5 --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkState.kt @@ -0,0 +1,8 @@ +package theodolite.model.crd + +import com.fasterxml.jackson.annotation.JsonValue + +enum class BenchmarkState(@JsonValue val value: String) { + PENDING("Pending"), + READY("Ready") +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkStates.kt b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkStates.kt deleted file mode 100644 index f52f2c168765ebb8bcc4f390795aa470b968021b..0000000000000000000000000000000000000000 --- a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkStates.kt +++ /dev/null @@ -1,6 +0,0 @@ -package theodolite.model.crd - -enum class BenchmarkStates(val value: String) { - PENDING("Pending"), - READY("Ready") -} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkStatus.kt b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkStatus.kt index f51cb7a76d015d6ecd900279e68d41baa26e876a..d4a17dbefb6cf3a53d545c32cb18e1d9acd7067f 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkStatus.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/BenchmarkStatus.kt @@ -6,6 +6,6 @@ import io.fabric8.kubernetes.api.model.Namespaced @JsonDeserialize class BenchmarkStatus: KubernetesResource, Namespaced { - var resourceSetsState = "-" + var resourceSetsState: BenchmarkState = BenchmarkState.PENDING } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt index 659621e8c3b1d5308a10d81240575dd3d432b53f..3be0aaf2a30cd4ef279edd34854eb936cc6e7e7c 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionCRD.kt @@ -12,7 +12,14 @@ import theodolite.benchmark.BenchmarkExecution @Version("v1") @Group("theodolite.com") @Kind("execution") -class ExecutionCRD( - var spec: BenchmarkExecution = BenchmarkExecution(), - var status: ExecutionStatus = ExecutionStatus() -) : CustomResource<BenchmarkExecution, ExecutionStatus>(), Namespaced +class ExecutionCRD: CustomResource<BenchmarkExecution, ExecutionStatus>(), Namespaced { + + override fun initSpec(): BenchmarkExecution { + return BenchmarkExecution() + } + + override fun initStatus(): ExecutionStatus { + return ExecutionStatus() + } + +} diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStates.kt b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionState.kt similarity index 65% rename from theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStates.kt rename to theodolite/src/main/kotlin/theodolite/model/crd/ExecutionState.kt index ad68bf380b18af1a654c201817bb7fc982804c8b..9ce38d9f56a968ccc408966e56609ee4f70570a4 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStates.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionState.kt @@ -1,7 +1,8 @@ package theodolite.model.crd -enum class ExecutionStates(val value: String) { - // Execution states +import com.fasterxml.jackson.annotation.JsonValue + +enum class ExecutionState(@JsonValue val value: String) { RUNNING("Running"), PENDING("Pending"), FAILURE("Failure"), diff --git a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStatus.kt b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStatus.kt index 252738959762aa5d0732babc5589c698d7bd4e9f..1f843ccf9152676e778bc4ed359776e37205e998 100644 --- a/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStatus.kt +++ b/theodolite/src/main/kotlin/theodolite/model/crd/ExecutionStatus.kt @@ -1,11 +1,58 @@ package theodolite.model.crd +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import io.fabric8.kubernetes.api.model.Duration import io.fabric8.kubernetes.api.model.KubernetesResource +import io.fabric8.kubernetes.api.model.MicroTime import io.fabric8.kubernetes.api.model.Namespaced +import java.time.Clock +import java.time.Instant +import java.time.Duration as JavaDuration + @JsonDeserialize -class ExecutionStatus : KubernetesResource, Namespaced { - var executionState: String = "" - var executionDuration: String = "-" +@JsonIgnoreProperties(ignoreUnknown = true) +class ExecutionStatus( + private val clock: Clock = Clock.systemUTC() +) : KubernetesResource, Namespaced { + + var executionState: ExecutionState = ExecutionState.NO_STATE + + var startTime: MicroTime? = null + + var completionTime: MicroTime? = null + + @get:JsonSerialize(using = DurationSerializer::class) + val executionDuration: Duration? + get() { + val startTime = this.startTime?.toInstant() + val completionTime = this.completionTime?.toInstant() ?: clock.instant()!! + return startTime?.let {Duration(JavaDuration.between(it, completionTime)) } + } + + private fun MicroTime.toInstant(): Instant { + return Instant.parse(this.time) + } + + class DurationSerializer : JsonSerializer<Duration?>() { + + override fun serialize(duration: Duration?, generator: JsonGenerator, serProvider: SerializerProvider) { + generator.writeObject(duration?.duration?.toK8sString()) + } + + private fun JavaDuration.toK8sString(): String { + return when { + this <= JavaDuration.ofSeconds(2) -> "${this.toSeconds()}s" + this < JavaDuration.ofMinutes(99) -> "${this.toMinutes()}m" + this < JavaDuration.ofHours(99) -> "${this.toHours()}h" + else -> "${this.toDays()}d + ${this.minusDays(this.toDays()).toHours()}h" + } + } + + } } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/patcher/PatcherFactory.kt b/theodolite/src/main/kotlin/theodolite/patcher/PatcherFactory.kt index ebad5de74a6b819dbf7887dfad91faac37ed5074..88b3e19e999a889cdcb8345ca7c90c37a6e6d275 100644 --- a/theodolite/src/main/kotlin/theodolite/patcher/PatcherFactory.kt +++ b/theodolite/src/main/kotlin/theodolite/patcher/PatcherFactory.kt @@ -1,7 +1,6 @@ package theodolite.patcher import io.fabric8.kubernetes.api.model.KubernetesResource -import theodolite.util.DeploymentFailedException import theodolite.util.InvalidPatcherConfigurationException import theodolite.util.PatcherDefinition diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/CompositeStrategy.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/CompositeStrategy.kt index 41cc5c325163ade54469398e815fdb8d95c6e6cd..d6ace6f564239e73a0d59f8eb7900f50018482c5 100644 --- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/CompositeStrategy.kt +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/CompositeStrategy.kt @@ -23,7 +23,7 @@ class CompositeStrategy( override fun findSuitableResource(load: LoadDimension, resources: List<Resource>): Resource? { var restrictedResources = resources.toList() for (strategy in this.restrictionStrategies) { - restrictedResources = restrictedResources.intersect(strategy.apply(load, resources)).toList() + restrictedResources = restrictedResources.intersect(strategy.apply(load, resources).toSet()).toList() } return this.searchStrategy.findSuitableResource(load, restrictedResources) } diff --git a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/FullSearch.kt b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/FullSearch.kt index cb0dd2d8ab528e42e8290f59f26c8b9b32f384c7..83c4abbdf44f1a1c2f3a27714d796580feedee49 100644 --- a/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/FullSearch.kt +++ b/theodolite/src/main/kotlin/theodolite/strategies/searchstrategy/FullSearch.kt @@ -22,7 +22,7 @@ class FullSearch(benchmarkExecutor: BenchmarkExecutor) : SearchStrategy(benchmar for (res in resources) { logger.info { "Running experiment with load '${load.get()}' and resources '${res.get()}'" } val result = this.benchmarkExecutor.runExperiment(load, res) - if (result && minimalSuitableResources != null) { + if (result && minimalSuitableResources == null) { minimalSuitableResources = res } } diff --git a/theodolite/src/main/kotlin/theodolite/util/Configuration.kt b/theodolite/src/main/kotlin/theodolite/util/Configuration.kt index 7b1232cd9ba72344cdb438f974cd6c4d17fd690d..0a63cfa84de9e60fba04707372ef884d77a1543b 100644 --- a/theodolite/src/main/kotlin/theodolite/util/Configuration.kt +++ b/theodolite/src/main/kotlin/theodolite/util/Configuration.kt @@ -7,8 +7,7 @@ private const val DEFAULT_NAMESPACE = "default" private const val DEFAULT_COMPONENT_NAME = "theodolite-operator" -class Configuration( -) { +class Configuration { companion object { val NAMESPACE = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE val COMPONENT_NAME = System.getenv("COMPONENT_NAME") ?: DEFAULT_COMPONENT_NAME diff --git a/theodolite/src/main/kotlin/theodolite/util/EvaluationFailedException.kt b/theodolite/src/main/kotlin/theodolite/util/EvaluationFailedException.kt index c67ed7ffd79afc733a97dae05c3203f8e78722ea..ebdf5a37b4e82c8d4b1870d065f5e77133154735 100644 --- a/theodolite/src/main/kotlin/theodolite/util/EvaluationFailedException.kt +++ b/theodolite/src/main/kotlin/theodolite/util/EvaluationFailedException.kt @@ -1,4 +1,3 @@ package theodolite.util -class EvaluationFailedException(message: String, e: Exception? = null) : ExecutionFailedException(message,e) { -} +class EvaluationFailedException(message: String, e: Exception? = null) : ExecutionFailedException(message,e) diff --git a/theodolite/src/main/kotlin/theodolite/util/ExecutionFailedException.kt b/theodolite/src/main/kotlin/theodolite/util/ExecutionFailedException.kt index 6566a451a3e273214f59962531b6bd17b33a850d..2e181dad35786d386226f8a57dfffbc2c3966754 100644 --- a/theodolite/src/main/kotlin/theodolite/util/ExecutionFailedException.kt +++ b/theodolite/src/main/kotlin/theodolite/util/ExecutionFailedException.kt @@ -1,4 +1,3 @@ package theodolite.util -open class ExecutionFailedException(message: String, e: Exception? = null) : TheodoliteException(message,e) { -} \ No newline at end of file +open class ExecutionFailedException(message: String, e: Exception? = null) : TheodoliteException(message,e) diff --git a/theodolite/src/main/kotlin/theodolite/util/ExecutionStateComparator.kt b/theodolite/src/main/kotlin/theodolite/util/ExecutionStateComparator.kt index 8a6b0e9a49362afa401cf3c1279e7f7f6cddf85d..81bf350b58901bc10535f143d5ccdb295b5fe85f 100644 --- a/theodolite/src/main/kotlin/theodolite/util/ExecutionStateComparator.kt +++ b/theodolite/src/main/kotlin/theodolite/util/ExecutionStateComparator.kt @@ -1,18 +1,17 @@ package theodolite.util import theodolite.model.crd.ExecutionCRD -import theodolite.model.crd.ExecutionStates +import theodolite.model.crd.ExecutionState -class ExecutionStateComparator(private val preferredState: ExecutionStates): Comparator<ExecutionCRD> { +class ExecutionStateComparator(private val preferredState: ExecutionState): Comparator<ExecutionCRD> { /** - * Simple comparator which can be used to order a list of [ExecutionCRD] such that executions with - * status [ExecutionStates.RESTART] are before all other executions. + * Simple comparator which can be used to order a list of [ExecutionCRD]s such that executions with + * status [ExecutionState.RESTART] are before all other executions. */ override fun compare(p0: ExecutionCRD, p1: ExecutionCRD): Int { return when { - (p0 == null && p1 == null) -> 0 - (p0.status.executionState == preferredState.value) -> -1 + (p0.status.executionState == preferredState) -> -1 else -> 1 } } diff --git a/theodolite/src/main/kotlin/theodolite/util/IOHandler.kt b/theodolite/src/main/kotlin/theodolite/util/IOHandler.kt index 57032189412d0937e4d77ddbf4354c78ffcc71a3..8b580c733ab7ae527d99c676223f4b09b392c6fd 100644 --- a/theodolite/src/main/kotlin/theodolite/util/IOHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/util/IOHandler.kt @@ -85,7 +85,7 @@ class IOHandler { * @param data the data to write in the file as String */ fun writeStringToTextFile(fileURL: String, data: String) { - val outputFile = File("$fileURL") + val outputFile = File(fileURL) outputFile.printWriter().use { it.println(data) } diff --git a/theodolite/src/main/kotlin/theodolite/util/YamlParserFromString.kt b/theodolite/src/main/kotlin/theodolite/util/YamlParserFromString.kt index 61db189ee99fa5fe36113b0fdecf589ad1114852..0e197908a501c0f6b89761a61989580b18e21f64 100644 --- a/theodolite/src/main/kotlin/theodolite/util/YamlParserFromString.kt +++ b/theodolite/src/main/kotlin/theodolite/util/YamlParserFromString.kt @@ -2,9 +2,6 @@ package theodolite.util import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.constructor.Constructor -import java.io.File -import java.io.FileInputStream -import java.io.InputStream /** * The YamlParser parses a YAML string diff --git a/theodolite/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt b/theodolite/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt index 46758583172c3fcd6417e17ff5bab85f8659734b..b7fc2d9f1b2d5110f974b3805584baa3903d5eb1 100644 --- a/theodolite/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt +++ b/theodolite/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt @@ -3,7 +3,7 @@ package theodolite import io.fabric8.kubernetes.api.model.apps.Deployment import io.fabric8.kubernetes.client.DefaultKubernetesClient import io.quarkus.test.junit.QuarkusTest -import io.smallrye.common.constraint.Assert.assertTrue +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile import theodolite.patcher.PatcherFactory diff --git a/theodolite/src/test/kotlin/theodolite/benchmark/ConfigMapResourceSetTest.kt b/theodolite/src/test/kotlin/theodolite/benchmark/ConfigMapResourceSetTest.kt index 2cc8f931418e28ae8841b592f93df8d88440cf3c..bc3263aa5fd06a8a19609d9f677db51f173cf54f 100644 --- a/theodolite/src/test/kotlin/theodolite/benchmark/ConfigMapResourceSetTest.kt +++ b/theodolite/src/test/kotlin/theodolite/benchmark/ConfigMapResourceSetTest.kt @@ -8,16 +8,17 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder import io.fabric8.kubernetes.client.server.mock.KubernetesServer import io.quarkus.test.junit.QuarkusTest -import io.smallrye.common.constraint.Assert.assertTrue -import junit.framework.Assert.assertEquals import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import theodolite.k8s.CustomResourceWrapper import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile import theodolite.util.DeploymentFailedException -private val testResourcePath = "./src/test/resources/k8s-resource-files/" +private const val testResourcePath = "./src/test/resources/k8s-resource-files/" @QuarkusTest class ConfigMapResourceSetTest { @@ -206,21 +207,17 @@ class ConfigMapResourceSetTest { val createdResourcesSet = resourceSet.getResourceSet(server.client) - assertEquals(1,createdResourcesSet.size ) + assertEquals(1, createdResourcesSet.size ) assert(createdResourcesSet.toMutableSet().first().second is Deployment) } - @Test() + @Test fun testConfigMapNotExist() { val resourceSet = ConfigMapResourceSet() resourceSet.name = "test-configmap1" - lateinit var ex: Exception - try { + assertThrows<DeploymentFailedException> { resourceSet.getResourceSet(server.client) - } catch (e: Exception) { - ex = e } - assertTrue(ex is DeploymentFailedException) } } \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/benchmark/FileSystemResourceSetTest.kt b/theodolite/src/test/kotlin/theodolite/benchmark/FileSystemResourceSetTest.kt index 59ad2be3248f67442ce352788f8b94b26f3b6b90..f15685c8e0ecd67b99caabb77f68cc35a78b47f2 100644 --- a/theodolite/src/test/kotlin/theodolite/benchmark/FileSystemResourceSetTest.kt +++ b/theodolite/src/test/kotlin/theodolite/benchmark/FileSystemResourceSetTest.kt @@ -5,16 +5,16 @@ 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.server.mock.KubernetesServer -import io.smallrye.common.constraint.Assert.assertTrue -import junit.framework.Assert.assertEquals import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import theodolite.k8s.CustomResourceWrapper import theodolite.util.DeploymentFailedException -import java.lang.IllegalStateException -private val testResourcePath = "./src/test/resources/k8s-resource-files/" +private const val testResourcePath = "./src/test/resources/k8s-resource-files/" class FileSystemResourceSetTest { @@ -104,12 +104,8 @@ class FileSystemResourceSetTest { fun testWrongPath() { val resourceSet = FileSystemResourceSet() resourceSet.path = "/abc/not-exist" - lateinit var ex: Exception - try { + assertThrows<DeploymentFailedException> { resourceSet.getResourceSet(server.client) - } catch (e: Exception) { - println(e) - ex = e } - assertTrue(ex is DeploymentFailedException) } + } } \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt index b4d5950542c40aba0f39b1be772823a3de389793..cbddbfbfc5d6f838677c6d04b0a0c79f59d8bc66 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt @@ -8,7 +8,7 @@ import theodolite.util.KafkaConfig class BenchmarkCRDummy(name: String) { private val benchmark = KubernetesBenchmark() - private val benchmarkCR = BenchmarkCRD(benchmark) + private val benchmarkCR = BenchmarkCRD() fun getCR(): BenchmarkCRD { return benchmarkCR diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkStateCheckerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkStateCheckerTest.kt index f3af42548d3bfc0d12e9f664d11cce1ae424e748..528cfac8066c28bf6382fb97cddf280b3c1de622 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkStateCheckerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkStateCheckerTest.kt @@ -14,7 +14,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.* import theodolite.benchmark.* -import theodolite.model.crd.BenchmarkStates +import theodolite.model.crd.BenchmarkState internal class BenchmarkStateCheckerTest { private val server = KubernetesServer(false, false) @@ -172,6 +172,6 @@ internal class BenchmarkStateCheckerTest { benchmark.getCR().spec.loadGenerator = resourceSet benchmark.getCR().spec.sut = resourceSet - assertEquals(BenchmarkStates.READY,checkerCrud.checkResources(benchmark.getCR().spec)) + assertEquals(BenchmarkState.READY,checkerCrud.checkResources(benchmark.getCR().spec)) } } \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ControllerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ControllerTest.kt index 6ea69689847afeb8f9fc36de2944c6fdcf4702ad..7d40f7e45d6aa2c93206a1bad22754fe93b0c100 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/ControllerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ControllerTest.kt @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test import theodolite.benchmark.BenchmarkExecution import theodolite.benchmark.KubernetesBenchmark import theodolite.model.crd.BenchmarkCRD -import theodolite.model.crd.BenchmarkStates +import theodolite.model.crd.BenchmarkState import theodolite.model.crd.ExecutionCRD @QuarkusTest @@ -41,7 +41,7 @@ class ControllerTest { // benchmark val benchmark1 = BenchmarkCRDummy(name = "Test-Benchmark") - benchmark1.getCR().status.resourceSetsState = BenchmarkStates.READY.value + benchmark1.getCR().status.resourceSetsState = BenchmarkState.READY val benchmark2 = BenchmarkCRDummy(name = "Test-Benchmark-123") benchmarkResourceList.items = listOf(benchmark1.getCR(), benchmark2.getCR()) diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt index 51347d41b396bf375c14d5580b0f2619ce5b518c..9274e283b48a6fd9b30d5ce0aff3cb8b995e0ce5 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionCRDummy.kt @@ -3,13 +3,13 @@ package theodolite.execution.operator import theodolite.benchmark.BenchmarkExecution import theodolite.model.crd.ExecutionCRD import theodolite.model.crd.ExecutionStatus -import theodolite.model.crd.ExecutionStates +import theodolite.model.crd.ExecutionState class ExecutionCRDummy(name: String, benchmark: String) { private val execution = BenchmarkExecution() private val executionState = ExecutionStatus() - private val executionCR = ExecutionCRD(execution, executionState) + private val executionCR = ExecutionCRD() fun getCR(): ExecutionCRD { return this.executionCR @@ -25,6 +25,7 @@ class ExecutionCRDummy(name: String, benchmark: String) { executionCR.metadata.name = name executionCR.kind = "Execution" executionCR.apiVersion = "v1" + executionCR.status = executionState // configure execution val loadType = BenchmarkExecution.LoadDefinition() @@ -51,6 +52,6 @@ class ExecutionCRDummy(name: String, benchmark: String) { execution.configOverrides = mutableListOf() execution.name = executionCR.metadata.name - executionState.executionState = ExecutionStates.PENDING.value + executionState.executionState = ExecutionState.PENDING } } \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt index c850e84f225bab7fc0b5eb145f9e655567de43d0..c08e0565375de84a228a28b6d68a0b713af97d0f 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt @@ -1,227 +1,264 @@ package theodolite.execution.operator -import io.fabric8.kubernetes.api.model.KubernetesResource -import io.fabric8.kubernetes.client.informers.SharedInformerFactory +import io.fabric8.kubernetes.api.model.KubernetesResourceList +import io.fabric8.kubernetes.client.dsl.MixedOperation +import io.fabric8.kubernetes.client.dsl.Resource import io.fabric8.kubernetes.client.server.mock.KubernetesServer import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.kubernetes.client.KubernetesTestServer +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import theodolite.k8s.K8sManager -import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile -import theodolite.model.crd.ExecutionStates -import java.lang.Thread.sleep +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.* +import theodolite.model.crd.ExecutionCRD +import theodolite.model.crd.ExecutionState +import java.io.FileInputStream +import java.util.stream.Stream + +// TODO move somewhere else +typealias ExecutionClient = MixedOperation<ExecutionCRD, KubernetesResourceList<ExecutionCRD>, Resource<ExecutionCRD>> + +@WithKubernetesTestServer +@QuarkusTest +class ExecutionEventHandlerTest { + @KubernetesTestServer + private lateinit var server: KubernetesServer -private const val RESYNC_PERIOD = 1000 * 1000.toLong() + lateinit var executionClient: ExecutionClient + lateinit var controller: TheodoliteController -@QuarkusTest -class ExecutionEventHandlerTest { - private final val server = KubernetesServer(false, true) - private val testResourcePath = "./src/test/resources/k8s-resource-files/" - private final val executionName = "example-execution" - lateinit var factory: SharedInformerFactory - lateinit var executionVersion1: KubernetesResource - lateinit var executionVersion2: KubernetesResource lateinit var stateHandler: ExecutionStateHandler - lateinit var manager: K8sManager - lateinit var controller: TheodoliteController + + lateinit var eventHandler: ExecutionEventHandler @BeforeEach fun setUp() { server.before() - val operator = TheodoliteOperator() - this.controller = operator.getController( - client = server.client, - executionStateHandler = operator.getExecutionStateHandler(client = server.client), - benchmarkStateChecker = operator.getBenchmarkStateChecker(client = server.client) - ) - this.factory = operator.getExecutionEventHandler(this.controller, server.client) - this.stateHandler = TheodoliteOperator().getExecutionStateHandler(client = server.client) + this.server.client + .apiextensions().v1() + .customResourceDefinitions() + .load(FileInputStream("crd/crd-execution.yaml")) + .create() - this.executionVersion1 = K8sResourceLoaderFromFile(server.client) - .loadK8sResource("Execution", testResourcePath + "test-execution.yaml") + this.executionClient = this.server.client.resources(ExecutionCRD::class.java) - this.executionVersion2 = K8sResourceLoaderFromFile(server.client) - .loadK8sResource("Execution", testResourcePath + "test-execution-update.yaml") - - this.stateHandler = operator.getExecutionStateHandler(server.client) - - this.manager = K8sManager((server.client)) + this.controller = mock() + this.stateHandler = ExecutionStateHandler(server.client) + this.eventHandler = ExecutionEventHandler(this.controller, this.stateHandler) } @AfterEach fun tearDown() { server.after() - factory.stopAllRegisteredInformers() } @Test - @DisplayName("Check namespaced property of informers") - fun testNamespaced() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - server.lastRequest - // the second request must be namespaced (this is the first `GET` request) - assert( - server - .lastRequest - .toString() - .contains("namespaces") - ) + fun testCrdRegistered() { + val crds = this.server.client.apiextensions().v1().customResourceDefinitions().list(); + assertEquals(1, crds.items.size) + assertEquals("execution", crds.items[0].spec.names.kind) } @Test - @DisplayName("Test onAdd method for executions without execution state") - fun testWithoutState() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(500) - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + fun testExecutionDeploy() { + getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + + val executions = executionClient.list().items + assertEquals(1, executions.size) } @Test - @DisplayName("Test onAdd method for executions with execution state `RUNNING`") - fun testWithStateIsRunning() { - manager.deploy(executionVersion1) - stateHandler - .setExecutionState( - resourceName = executionName, - status = ExecutionStates.RUNNING - ) - factory.startAllRegisteredInformers() - sleep(500) - assertEquals( - ExecutionStates.RESTART, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + fun testStatusSet() { + val execCreated = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + assertNotNull(execCreated.status) + val execResponse = this.executionClient.withName(execCreated.metadata.name) + val execResponseItem = execResponse.get() + assertNotNull(execResponseItem.status) } @Test - @DisplayName("Test onUpdate method for execution with execution state `PENDING`") - fun testOnUpdatePending() { - manager.deploy(executionVersion1) - - factory.startAllRegisteredInformers() - sleep(500) + @DisplayName("Test onAdd method for executions without execution state") + fun testOnAddWithoutStatus() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val execution = executionResource.create() + val executionName = execution.metadata.name - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + // Get execution from server + val executionResponse = this.executionClient.withName(executionName).get() + this.eventHandler.onAdd(executionResponse) - manager.deploy(executionVersion2) - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + assertEquals(ExecutionState.PENDING, this.executionClient.withName(executionName).get().status.executionState) } @Test - @DisplayName("Test onUpdate method for execution with execution state `FINISHED`") - fun testOnUpdateFinished() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(500) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.FINISHED - ) - - manager.deploy(executionVersion2) - sleep(500) - - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + @DisplayName("Test onAdd method for executions with execution state `RUNNING`") + fun testOnAddWithStatusRunning() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val execution = executionResource.create() + val executionName = execution.metadata.name + stateHandler.setExecutionState(executionName, ExecutionState.RUNNING) + + // Update status of execution + execution.status.executionState = ExecutionState.RUNNING + executionResource.patchStatus(execution) + + + // Get execution from server + val executionResponse = this.executionClient.withName(executionName).get() + // Assert that status at server matches set status + assertEquals(ExecutionState.RUNNING, this.executionClient.withName(executionName).get().status.executionState) + + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + this.eventHandler.onAdd(executionResponse) + + verify(this.controller).stop(true) + assertEquals(ExecutionState.RESTART, this.executionClient.withName(executionName).get().status.executionState) } @Test - @DisplayName("Test onUpdate method for execution with execution state `FAILURE`") - fun testOnUpdateFailure() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(500) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.FAILURE - ) - - manager.deploy(executionVersion2) - sleep(500) - - assertEquals( - ExecutionStates.PENDING, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + @DisplayName("Test onUpdate method for execution with no status") + fun testOnUpdateWithoutStatus() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution at server has no status + assertEquals(ExecutionState.NO_STATE, firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + + this.eventHandler.onUpdate(firstExecutionResponse, secondExecutionResponse) + + // Get execution from server and assert that new status matches expected one + assertEquals(ExecutionState.PENDING, this.executionClient.withName(executionName).get().status.executionState) } + @ParameterizedTest + @MethodSource("provideOnUpdateTestArguments") + @DisplayName("Test onUpdate method for execution with different status") + fun testOnUpdateWithStatus(beforeState: ExecutionState, expectedState: ExecutionState) { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution + firstExecution.status.executionState = beforeState + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that status at server matches set status + assertEquals(beforeState, firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + + this.eventHandler.onUpdate(firstExecutionResponse, secondExecutionResponse) + + // Get execution from server and assert that new status matches expected one + assertEquals(expectedState, this.executionClient.withName(executionName).get().status.executionState) + } @Test - @DisplayName("Test onUpdate method for execution with execution state `RUNNING`") - fun testOnUpdateRunning() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(500) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.RUNNING - ) - - manager.deploy(executionVersion2) - sleep(500) - - assertEquals( - ExecutionStates.RESTART, - stateHandler.getExecutionState( - resourceName = executionName - ) - ) + fun testOnDeleteWithExecutionRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionState.RUNNING + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNull(secondExecutionResponse) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + this.eventHandler.onDelete(firstExecutionResponse, true) + + verify(this.controller).stop(false) } @Test - @DisplayName("Test onUpdate method for execution with execution state `RESTART`") - fun testOnUpdateRestart() { - manager.deploy(executionVersion1) - factory.startAllRegisteredInformers() - sleep(500) - - stateHandler.setExecutionState( - resourceName = executionName, - status = ExecutionStates.RESTART - ) - - manager.deploy(executionVersion2) - sleep(500) - - assertEquals( - ExecutionStates.RESTART, - stateHandler.getExecutionState( - resourceName = executionName + fun testOnDeleteWithExecutionNotRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionState.RUNNING + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNull(secondExecutionResponse) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(false) + + this.eventHandler.onDelete(firstExecutionResponse, true) + + verify(this.controller, never()).stop(false) + } + + private fun getExecutionFromSystemResource(resourceName: String): Resource<ExecutionCRD> { + return executionClient.load(ClassLoader.getSystemResourceAsStream(resourceName)) + } + + companion object { + @JvmStatic + fun provideOnUpdateTestArguments(): Stream<Arguments> = + Stream.of( + // before state -> expected state + Arguments.of(ExecutionState.PENDING, ExecutionState.PENDING), + Arguments.of(ExecutionState.FINISHED, ExecutionState.PENDING), + Arguments.of(ExecutionState.FAILURE, ExecutionState.PENDING), + Arguments.of(ExecutionState.RUNNING, ExecutionState.RESTART), + Arguments.of(ExecutionState.RESTART, ExecutionState.RESTART) ) - ) } + } \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTestWithInformer.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTestWithInformer.kt new file mode 100644 index 0000000000000000000000000000000000000000..adddc705616935e5440c1c601615ce9a065df4c4 --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTestWithInformer.kt @@ -0,0 +1,283 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.dsl.Resource +import io.fabric8.kubernetes.client.server.mock.KubernetesServer +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.kubernetes.client.KubernetesTestServer +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.* +import theodolite.model.crd.ExecutionCRD +import theodolite.model.crd.ExecutionState +import java.io.FileInputStream +import java.util.concurrent.CountDownLatch +import java.util.stream.Stream + +@WithKubernetesTestServer +@QuarkusTest +class ExecutionEventHandlerTestWithInformer { + + @KubernetesTestServer + private lateinit var server: KubernetesServer + + lateinit var executionClient: ExecutionClient + + lateinit var controller: TheodoliteController + + lateinit var stateHandler: ExecutionStateHandler + + lateinit var addCountDownLatch: CountDownLatch + lateinit var updateCountDownLatch: CountDownLatch + lateinit var deleteCountDownLatch: CountDownLatch + + lateinit var eventHandler: ExecutionEventHandlerWrapper + + @BeforeEach + fun setUp() { + server.before() + + this.server.client + .apiextensions().v1() + .customResourceDefinitions() + .load(FileInputStream("crd/crd-execution.yaml")) + .create() + + this.executionClient = this.server.client.resources(ExecutionCRD::class.java) + + this.controller = mock() + this.stateHandler = ExecutionStateHandler(server.client) + this.addCountDownLatch = CountDownLatch(1) + this.updateCountDownLatch = CountDownLatch(2) + this.deleteCountDownLatch = CountDownLatch(1) + this.eventHandler = ExecutionEventHandlerWrapper( + ExecutionEventHandler(this.controller, this.stateHandler), + { addCountDownLatch.countDown() }, + { updateCountDownLatch.countDown() }, + { deleteCountDownLatch.countDown() } + ) + } + + @AfterEach + fun tearDown() { + server.after() + this.server.client.informers().stopAllRegisteredInformers() + } + + @Test + fun testCrdRegistered() { + val crds = this.server.client.apiextensions().v1().customResourceDefinitions().list(); + assertEquals(1, crds.items.size) + assertEquals("execution", crds.items[0].spec.names.kind) + } + + @Test + fun testExecutionDeploy() { + getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + + val executions = executionClient.list().items + assertEquals(1, executions.size) + } + + @Test + fun testStatusSet() { + val execCreated = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml").create() + assertNotNull(execCreated.status) + val execResponse = this.executionClient.withName(execCreated.metadata.name) + val execResponseItem = execResponse.get() + assertNotNull(execResponseItem.status) + } + + @Test + @DisplayName("Test onAdd method for executions without execution state") + fun testOnAddWithoutStatus() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val execution = executionResource.create() + val executionName = execution.metadata.name + + // Start informer + this.executionClient.inform(eventHandler) + + // Await informer called + this.addCountDownLatch.await() + assertEquals(ExecutionState.PENDING, this.executionClient.withName(executionName).get().status.executionState) + } + + @Test + @DisplayName("Test onAdd method for executions with execution state `RUNNING`") + @Disabled("Flaky test due to multiple informer events.") + fun testOnAddWithStatusRunning() { + // Create first version of execution resource + val executionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val executionName = executionResource.get().metadata.name + + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + // Start informer + this.executionClient.inform(eventHandler) + + val execution = executionResource.create() + + // Update status of execution + execution.status.executionState = ExecutionState.RUNNING + executionResource.patchStatus(execution) + + // Assert that status at server matches set status + // assertEquals(ExecutionStates.RUNNING, this.executionClient.withName(executionName).get().status.executionState) + + // Await informer called + this.addCountDownLatch.await() + verify(this.controller).stop(true) + assertEquals(ExecutionState.RESTART, this.executionClient.withName(executionName).get().status.executionState) + } + + @Test + @DisplayName("Test onUpdate method for execution with no status") + fun testOnUpdateWithoutStatus() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Start informer + this.executionClient.inform(eventHandler) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution at server has pending status + assertEquals(ExecutionState.PENDING, firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + + // Await informer called + this.updateCountDownLatch.await() + // Get execution from server and assert that new status matches expected one + assertEquals(ExecutionState.PENDING, this.executionClient.withName(executionName).get().status.executionState) + } + + @ParameterizedTest + @MethodSource("provideOnUpdateTestArguments") + @DisplayName("Test onUpdate method for execution with different status") + fun testOnUpdateWithStatus(beforeState: ExecutionState, expectedState: ExecutionState) { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution + firstExecution.status.executionState = beforeState + firstExecutionResource.patchStatus(firstExecution) + + // Start informer + this.executionClient.inform(eventHandler) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that status at server matches set status + assertEquals(beforeState, firstExecutionResponse.status.executionState) + + // Create new version of execution and update at server + getExecutionFromSystemResource("k8s-resource-files/test-execution-update.yaml").createOrReplace() + + // Await informer called + this.updateCountDownLatch.await() + // Get execution from server and assert that new status matches expected one + assertEquals(expectedState, this.executionClient.withName(executionName).get().status.executionState) + } + + @Test + @Disabled("Informer also called onAdd and changes status") + fun testOnDeleteWithExecutionRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionState.RUNNING + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Start informer + this.executionClient.inform(eventHandler) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(true) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution deleted at server + assertNull(secondExecutionResponse) + + // Await informer called + this.deleteCountDownLatch.await() + + verify(this.controller).stop(false) + } + + @Test + fun testOnDeleteWithExecutionNotRunning() { + // Create first version of execution resource + val firstExecutionResource = getExecutionFromSystemResource("k8s-resource-files/test-execution.yaml") + val firstExecution = firstExecutionResource.create() + val executionName = firstExecution.metadata.name + + // Update status of execution to be running + firstExecution.status.executionState = ExecutionState.RUNNING + firstExecutionResource.patchStatus(firstExecution) + + // Get execution from server + val firstExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNotNull(firstExecutionResponse) + + // Start informer + this.executionClient.inform(eventHandler) + + // We consider execution to be running + whenever(this.controller.isExecutionRunning(executionName)).thenReturn(false) + + // Delete execution + this.executionClient.delete(firstExecutionResponse) + + // Get execution from server + val secondExecutionResponse = this.executionClient.withName(executionName).get() + // Assert that execution created at server + assertNull(secondExecutionResponse) + + // Await informer called + this.deleteCountDownLatch.await() + + verify(this.controller, never()).stop(false) + } + + private fun getExecutionFromSystemResource(resourceName: String): Resource<ExecutionCRD> { + return executionClient.load(ClassLoader.getSystemResourceAsStream(resourceName)) + } + + companion object { + @JvmStatic + fun provideOnUpdateTestArguments(): Stream<Arguments> = + Stream.of( + // before state -> expected state + Arguments.of(ExecutionState.PENDING, ExecutionState.PENDING), + Arguments.of(ExecutionState.FINISHED, ExecutionState.PENDING), + Arguments.of(ExecutionState.FAILURE, ExecutionState.PENDING), + // Arguments.of(ExecutionStates.RUNNING, ExecutionStates.RESTART), // see testOnDeleteWithExecutionRunning + Arguments.of(ExecutionState.RESTART, ExecutionState.RESTART) + ) + } + +} \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerWrapper.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerWrapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..5dbc515a7799dd51e6395153f13d80650587d7fa --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerWrapper.kt @@ -0,0 +1,27 @@ +package theodolite.execution.operator + +import io.fabric8.kubernetes.client.informers.ResourceEventHandler +import theodolite.model.crd.ExecutionCRD + +class ExecutionEventHandlerWrapper( + private val executionEventHandler: ExecutionEventHandler, + private val afterOnAddCallback: () -> Unit, + private val afterOnUpdateCallback: () -> Unit, + private val afterOnDeleteCallback: () -> Unit +) : ResourceEventHandler<ExecutionCRD> { + + override fun onAdd(execution: ExecutionCRD) { + this.executionEventHandler.onAdd(execution) + this.afterOnAddCallback() + } + + override fun onUpdate(oldExecution: ExecutionCRD, newExecution: ExecutionCRD) { + this.executionEventHandler.onUpdate(oldExecution, newExecution) + this.afterOnUpdateCallback() + } + + override fun onDelete(execution: ExecutionCRD, deletedFinalStateUnknown: Boolean) { + this.executionEventHandler.onDelete(execution, deletedFinalStateUnknown) + this.afterOnDeleteCallback() + } +} \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt index a54f4ed6db559f8f7f15ae82deecf3fedf8b4abe..138f79eadc6bdee17e62cc7a961eb7de539fa3df 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/StateHandlerTest.kt @@ -1,6 +1,9 @@ package theodolite.execution.operator import io.fabric8.kubernetes.client.server.mock.KubernetesServer +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.kubernetes.client.KubernetesTestServer +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -9,12 +12,16 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import theodolite.k8s.K8sManager import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile -import theodolite.model.crd.ExecutionStates +import theodolite.model.crd.ExecutionState import java.time.Duration +@QuarkusTest +@WithKubernetesTestServer class StateHandlerTest { private val testResourcePath = "./src/test/resources/k8s-resource-files/" - private val server = KubernetesServer(false, true) + + @KubernetesTestServer + private lateinit var server: KubernetesServer @BeforeEach fun setUp() { @@ -47,14 +54,7 @@ class StateHandlerTest { @DisplayName("Test empty execution state") fun executionWithoutExecutionStatusTest() { val handler = ExecutionStateHandler(client = server.client) - assertEquals(ExecutionStates.NO_STATE, handler.getExecutionState("example-execution")) - } - - @Test - @DisplayName("Test empty duration state") - fun executionWithoutDurationStatusTest() { - val handler = ExecutionStateHandler(client = server.client) - assertEquals("-", handler.getDurationState("example-execution")) + assertEquals(ExecutionState.NO_STATE, handler.getExecutionState("example-execution")) } @Test @@ -62,16 +62,8 @@ class StateHandlerTest { fun executionStatusTest() { val handler = ExecutionStateHandler(client = server.client) - assertTrue(handler.setExecutionState("example-execution", ExecutionStates.INTERRUPTED)) - assertEquals(ExecutionStates.INTERRUPTED, handler.getExecutionState("example-execution")) + assertTrue(handler.setExecutionState("example-execution", ExecutionState.INTERRUPTED)) + assertEquals(ExecutionState.INTERRUPTED, handler.getExecutionState("example-execution")) } - @Test - @DisplayName("Test set and get of the duration state") - fun durationStatusTest() { - val handler = ExecutionStateHandler(client = server.client) - - assertTrue(handler.setDurationState("example-execution", Duration.ofMillis(100))) - assertEquals("0s", handler.getDurationState("example-execution")) - } } \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/k8s/K8sManagerTest.kt b/theodolite/src/test/kotlin/theodolite/k8s/K8sManagerTest.kt index 7c69618de03f730f5b6f1cb83c5df544e2cd120c..ffc3f2f2b8083ab8b8170fa77c19de3a6ef387e7 100644 --- a/theodolite/src/test/kotlin/theodolite/k8s/K8sManagerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/k8s/K8sManagerTest.kt @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder import io.fabric8.kubernetes.client.server.mock.KubernetesServer import io.quarkus.test.junit.QuarkusTest -import mu.KotlinLogging import org.json.JSONObject import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals @@ -17,9 +16,6 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import theodolite.k8s.resourceLoader.K8sResourceLoaderFromFile - -private val logger = KotlinLogging.logger {} - @QuarkusTest @JsonIgnoreProperties(ignoreUnknown = true) class K8sManagerTest { diff --git a/theodolite/src/test/kotlin/theodolite/model/crd/ExecutionStatusTest.kt b/theodolite/src/test/kotlin/theodolite/model/crd/ExecutionStatusTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..157bc1c03cc40375c928677189f549052e1e134d --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/model/crd/ExecutionStatusTest.kt @@ -0,0 +1,144 @@ +package theodolite.model.crd + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.exc.InvalidFormatException +import io.fabric8.kubernetes.api.model.MicroTime +import io.fabric8.kubernetes.api.model.Duration as K8sDuration +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.Clock +import java.time.Duration +import java.time.Instant +import java.time.ZoneId + + +internal class ExecutionStatusTest { + + @Test + fun testDefaultStateSerialization() { + val objectMapper = ObjectMapper() + val executionStatus = ExecutionStatus() + val jsonString = objectMapper.writeValueAsString(executionStatus) + val json = objectMapper.readTree(jsonString) + val jsonField = json.get("executionState") + assertTrue(jsonField.isTextual) + assertEquals(ExecutionState.NO_STATE.value, json.get("executionState").asText()) + } + + @Test + fun testCustomStateSerialization() { + val objectMapper = ObjectMapper() + val executionStatus = ExecutionStatus() + executionStatus.executionState = ExecutionState.PENDING + val jsonString = objectMapper.writeValueAsString(executionStatus) + val json = objectMapper.readTree(jsonString) + val jsonField = json.get("executionState") + assertTrue(jsonField.isTextual) + assertEquals(ExecutionState.PENDING.value, json.get("executionState").asText()) + } + + @Test + fun testStateDeserialization() { + val objectMapper = ObjectMapper() + val json = objectMapper.createObjectNode() + json.put("executionState", ExecutionState.RUNNING.value) + json.put("executionDuration", "") + val jsonString = objectMapper.writeValueAsString(json) + val executionStatus = objectMapper.readValue(jsonString, ExecutionStatus::class.java) + val executionState = executionStatus.executionState + assertNotNull(executionState) + assertEquals(ExecutionState.RUNNING, executionState) + } + + @Test + fun testInvalidStateDeserialization() { + val objectMapper = ObjectMapper() + val json = objectMapper.createObjectNode() + json.put("executionState", "invalid-state") + json.put("executionDuration", "") + val jsonString = objectMapper.writeValueAsString(json) + assertThrows<InvalidFormatException> { + objectMapper.readValue(jsonString, ExecutionStatus::class.java) + } + } + + @Test + fun `test duration for no start and completion time`() { + val executionStatus = ExecutionStatus() + assertNull(executionStatus.startTime) + assertNull(executionStatus.completionTime) + assertNull(executionStatus.executionDuration) + } + + @Test + fun `test duration for no start but completion time`() { + val executionStatus = ExecutionStatus() + executionStatus.completionTime = MicroTime(Instant.parse("2022-01-02T18:59:20.492103Z").toString()) + assertNull(executionStatus.startTime) + assertNull(executionStatus.executionDuration) + } + + @Test + fun `test duration for non completed execution`() { + val startInstant = Instant.parse("2022-01-02T18:59:20.492103Z") + val sinceStart = Duration.ofMinutes(5) + val executionStatus = ExecutionStatus(clock = Clock.fixed(startInstant.plus(sinceStart), ZoneId.systemDefault())) + executionStatus.startTime = MicroTime(startInstant.toString()) + assertNotNull(executionStatus.executionDuration) + assertEquals(K8sDuration(sinceStart), executionStatus.executionDuration) + } + + @Test + fun `test duration for completed execution`() { + val startInstant = Instant.parse("2022-01-02T18:59:20.492103Z") + val sinceStart = Duration.ofMinutes(5) + val executionStatus = ExecutionStatus() + executionStatus.startTime = MicroTime(startInstant.toString()) + executionStatus.completionTime = MicroTime(startInstant.plus(sinceStart).toString()) + assertNotNull(executionStatus.executionDuration) + assertEquals(K8sDuration(sinceStart), executionStatus.executionDuration) + } + + @Test + fun testDurationSerialization() { + val objectMapper = ObjectMapper() + val executionStatus = ExecutionStatus() + val startInstant = Instant.parse("2022-01-02T18:59:20.492103Z") + executionStatus.startTime = MicroTime(startInstant.toString()) + executionStatus.completionTime = MicroTime(startInstant.plus(Duration.ofMinutes(15)).toString()) + val jsonString = objectMapper.writeValueAsString(executionStatus) + val json = objectMapper.readTree(jsonString) + val jsonField = json.get("executionDuration") + assertTrue(jsonField.isTextual) + assertEquals("15m", jsonField.asText()) + } + + @Test + fun testNotStartedDurationSerialization() { + val objectMapper = ObjectMapper() + val executionStatus = ExecutionStatus() + val jsonString = objectMapper.writeValueAsString(executionStatus) + val json = objectMapper.readTree(jsonString) + + assertTrue(json.get("startTime").isNull) + assertTrue(json.get("completionTime").isNull) + assertTrue(json.get("executionDuration").isNull) + } + + @Test + fun testWrongDurationDeserialization() { + val startTime = "2022-01-02T18:59:20.492103Z" + val completionTime = "2022-01-02T19:14:20.492103Z" + val objectMapper = ObjectMapper() + val json = objectMapper.createObjectNode() + json.put("executionState", ExecutionState.RUNNING.value) + json.put("executionDuration", "20m") + json.put("startTime", startTime) + json.put("completionTime", completionTime) + val jsonString = objectMapper.writeValueAsString(json) + val executionStatus = objectMapper.readValue(jsonString, ExecutionStatus::class.java) + assertNotNull(executionStatus.executionDuration) + assertEquals(Duration.ofMinutes(15), executionStatus.executionDuration?.duration) + } +} \ No newline at end of file diff --git a/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt b/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt index 7332e53f9e1814f28b8ff37a595b31b0eb931ea7..ae80312afd2c128f0f542306a8ffda7f3f53876b 100644 --- a/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt +++ b/theodolite/src/test/kotlin/theodolite/util/ExecutionStateComparatorTest.kt @@ -4,7 +4,7 @@ import io.quarkus.test.junit.QuarkusTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import theodolite.execution.operator.ExecutionCRDummy -import theodolite.model.crd.ExecutionStates +import theodolite.model.crd.ExecutionState @QuarkusTest @@ -12,14 +12,13 @@ class ExecutionStateComparatorTest { @Test fun testCompare() { - val comparator = ExecutionStateComparator(ExecutionStates.RESTART) + val comparator = ExecutionStateComparator(ExecutionState.RESTART) val execution1 = ExecutionCRDummy("dummy1", "default-benchmark") val execution2 = ExecutionCRDummy("dummy2", "default-benchmark") - execution1.getStatus().executionState = ExecutionStates.RESTART.value - execution2.getStatus().executionState = ExecutionStates.PENDING.value + execution1.getStatus().executionState = ExecutionState.RESTART + execution2.getStatus().executionState = ExecutionState.PENDING val list = listOf(execution2.getCR(), execution1.getCR()) - assertEquals( list.reversed(), list.sortedWith(comparator) diff --git a/theodolite/src/test/kotlin/theodolite/util/IOHandlerTest.kt b/theodolite/src/test/kotlin/theodolite/util/IOHandlerTest.kt index 6b8aa1d567fd2c93c1301fe3f953273e0f5d5420..f84536bfc029a829c1798293938386965eedcf47 100644 --- a/theodolite/src/test/kotlin/theodolite/util/IOHandlerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/util/IOHandlerTest.kt @@ -96,7 +96,7 @@ internal class IOHandlerTest { } - @Test() + @Test @SetEnvironmentVariable.SetEnvironmentVariables( SetEnvironmentVariable(key = "RESULTS_FOLDER", value = "./src/test/resources"), SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "false") @@ -105,7 +105,7 @@ internal class IOHandlerTest { assertEquals("./src/test/resources/", IOHandler().getResultFolderURL()) } - @Test() + @Test @SetEnvironmentVariable.SetEnvironmentVariables( SetEnvironmentVariable(key = "RESULTS_FOLDER", value = "$FOLDER_URL-0"), SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "false") @@ -121,7 +121,7 @@ internal class IOHandlerTest { assertTrue(exceptionWasThrown) } - @Test() + @Test @SetEnvironmentVariable.SetEnvironmentVariables( SetEnvironmentVariable(key = "RESULTS_FOLDER", value = FOLDER_URL), SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "true") @@ -130,7 +130,7 @@ internal class IOHandlerTest { assertEquals("$FOLDER_URL/", IOHandler().getResultFolderURL()) } - @Test() + @Test @ClearEnvironmentVariable(key = "RESULTS_FOLDER") @SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "true") fun testGetResultFolderURL_CreateFolderButNoFolderGiven() { diff --git a/theodolite/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/theodolite/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000000000000000000000000000000000..1f0955d450f0dc49ca715b1a0a88a5aa746ee11e --- /dev/null +++ b/theodolite/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline