diff --git a/docs/_config.yml b/docs/_config.yml index a6c6eb709d1a2b904421cee05e9d22fe94d2005a..0d2a1aa774a83347c80b538a97d5dbfa1b7639b3 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -4,7 +4,6 @@ description: >- scalability of cloud-native applications. remote_theme: pmarsceill/just-the-docs -#color_scheme: "dark" aux_links: "Theodolite on GitHub": - "//github.com/cau-se/theodolite" @@ -14,4 +13,5 @@ exclude: - Gemfile - Gemfile.lock - README.md - - vendor + - vendor/ + - drafts/ diff --git a/docs/api-reference/crds.md b/docs/api-reference/crds.md index a8ce1d8d0ad2827fe07e0eee0a60a0cd2c71c93c..a91e991609f5fe10e90793f34f2ad04c6c5576d0 100644 --- a/docs/api-reference/crds.md +++ b/docs/api-reference/crds.md @@ -257,6 +257,24 @@ The loadGenResourceSets specifies all Kubernetes resources required to start the </tr> </thead> <tbody><tr> + <td><b><a href="#benchmarkspecloadgeneratorafteractionsindex">afterActions</a></b></td> + <td>[]object</td> + <td> + Load generator after actions are executed after the teardown of the load generator.<br/> + <br/> + <i>Default</i>: []<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecloadgeneratorbeforeactionsindex">beforeActions</a></b></td> + <td>[]object</td> + <td> + Load generator before actions are executed before the load generator is started.<br/> + <br/> + <i>Default</i>: []<br/> + </td> + <td>false</td> + </tr><tr> <td><b><a href="#benchmarkspecloadgeneratorresourcesindex">resources</a></b></td> <td>[]object</td> <td> @@ -269,6 +287,272 @@ The loadGenResourceSets specifies all Kubernetes resources required to start the </table> +### benchmark.spec.loadGenerator.afterActions[index] +<sup><sup>[↩ Parent](#benchmarkspecloadgenerator)</sup></sup> + + + + + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b><a href="#benchmarkspecloadgeneratorafteractionsindexexec">exec</a></b></td> + <td>object</td> + <td> + Specifies command to be executed.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecloadgeneratorafteractionsindexselector">selector</a></b></td> + <td>object</td> + <td> + The selector specifies which resource should be selected for the execution of the command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.loadGenerator.afterActions[index].exec +<sup><sup>[↩ Parent](#benchmarkspecloadgeneratorafteractionsindex)</sup></sup> + + + +Specifies command to be executed. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>command</b></td> + <td>[]string</td> + <td> + The command to be executed as string array.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b>timeoutSeconds</b></td> + <td>integer</td> + <td> + Specifies the timeout (in seconds) for the specified command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.loadGenerator.afterActions[index].selector +<sup><sup>[↩ Parent](#benchmarkspecloadgeneratorafteractionsindex)</sup></sup> + + + +The selector specifies which resource should be selected for the execution of the command. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>container</b></td> + <td>string</td> + <td> + Specifies the container.<br/> + <br/> + <i>Default</i>: <br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecloadgeneratorafteractionsindexselectorpod">pod</a></b></td> + <td>object</td> + <td> + Specifies the pod.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.loadGenerator.afterActions[index].selector.pod +<sup><sup>[↩ Parent](#benchmarkspecloadgeneratorafteractionsindexselector)</sup></sup> + + + +Specifies the pod. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>matchLabels</b></td> + <td>map[string]string</td> + <td> + The matchLabels of the desired pod.<br/> + <br/> + <i>Default</i>: map[]<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.loadGenerator.beforeActions[index] +<sup><sup>[↩ Parent](#benchmarkspecloadgenerator)</sup></sup> + + + + + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b><a href="#benchmarkspecloadgeneratorbeforeactionsindexexec">exec</a></b></td> + <td>object</td> + <td> + Specifies command to be executed.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecloadgeneratorbeforeactionsindexselector">selector</a></b></td> + <td>object</td> + <td> + The selector specifies which resource should be selected for the execution of the command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.loadGenerator.beforeActions[index].exec +<sup><sup>[↩ Parent](#benchmarkspecloadgeneratorbeforeactionsindex)</sup></sup> + + + +Specifies command to be executed. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>command</b></td> + <td>[]string</td> + <td> + The command to be executed as string array.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b>timeoutSeconds</b></td> + <td>integer</td> + <td> + Specifies the timeout (in seconds) for the specified command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.loadGenerator.beforeActions[index].selector +<sup><sup>[↩ Parent](#benchmarkspecloadgeneratorbeforeactionsindex)</sup></sup> + + + +The selector specifies which resource should be selected for the execution of the command. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>container</b></td> + <td>string</td> + <td> + Specifies the container.<br/> + <br/> + <i>Default</i>: <br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecloadgeneratorbeforeactionsindexselectorpod">pod</a></b></td> + <td>object</td> + <td> + Specifies the pod.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.loadGenerator.beforeActions[index].selector.pod +<sup><sup>[↩ Parent](#benchmarkspecloadgeneratorbeforeactionsindexselector)</sup></sup> + + + +Specifies the pod. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>matchLabels</b></td> + <td>map[string]string</td> + <td> + The matchLabels of the desired pod.<br/> + <br/> + <i>Default</i>: map[]<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + ### benchmark.spec.loadGenerator.resources[index] <sup><sup>[↩ Parent](#benchmarkspecloadgenerator)</sup></sup> @@ -550,6 +834,24 @@ The appResourceSets specifies all Kubernetes resources required to start the sut </tr> </thead> <tbody><tr> + <td><b><a href="#benchmarkspecsutafteractionsindex">afterActions</a></b></td> + <td>[]object</td> + <td> + <br/> + <br/> + <i>Default</i>: []<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecsutbeforeactionsindex">beforeActions</a></b></td> + <td>[]object</td> + <td> + SUT before actions are executed before the SUT is started.<br/> + <br/> + <i>Default</i>: []<br/> + </td> + <td>false</td> + </tr><tr> <td><b><a href="#benchmarkspecsutresourcesindex">resources</a></b></td> <td>[]object</td> <td> @@ -562,7 +864,7 @@ The appResourceSets specifies all Kubernetes resources required to start the sut </table> -### benchmark.spec.sut.resources[index] +### benchmark.spec.sut.afterActions[index] <sup><sup>[↩ Parent](#benchmarkspecsut)</sup></sup> @@ -579,29 +881,29 @@ The appResourceSets specifies all Kubernetes resources required to start the sut </tr> </thead> <tbody><tr> - <td><b><a href="#benchmarkspecsutresourcesindexconfigmap">configMap</a></b></td> + <td><b><a href="#benchmarkspecsutafteractionsindexexec">exec</a></b></td> <td>object</td> <td> - The configMap resourceSet loads the Kubernetes manifests from an Kubernetes configMap.<br/> + Specifies command to be executed.<br/> </td> <td>false</td> </tr><tr> - <td><b><a href="#benchmarkspecsutresourcesindexfilesystem">fileSystem</a></b></td> + <td><b><a href="#benchmarkspecsutafteractionsindexselector">selector</a></b></td> <td>object</td> <td> - The fileSystem resourceSet loads the Kubernetes manifests from the filesystem.<br/> + The selector specifies which resource should be selected for the execution of the command.<br/> </td> <td>false</td> </tr></tbody> </table> -### benchmark.spec.sut.resources[index].configMap -<sup><sup>[↩ Parent](#benchmarkspecsutresourcesindex)</sup></sup> +### benchmark.spec.sut.afterActions[index].exec +<sup><sup>[↩ Parent](#benchmarkspecsutafteractionsindex)</sup></sup> -The configMap resourceSet loads the Kubernetes manifests from an Kubernetes configMap. +Specifies command to be executed. <table> <thead> @@ -613,7 +915,273 @@ The configMap resourceSet loads the Kubernetes manifests from an Kubernetes conf </tr> </thead> <tbody><tr> - <td><b>files</b></td> + <td><b>command</b></td> + <td>[]string</td> + <td> + The command to be executed as string array.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b>timeoutSeconds</b></td> + <td>integer</td> + <td> + Specifies the timeout (in seconds) for the specified command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.afterActions[index].selector +<sup><sup>[↩ Parent](#benchmarkspecsutafteractionsindex)</sup></sup> + + + +The selector specifies which resource should be selected for the execution of the command. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>container</b></td> + <td>string</td> + <td> + Specifies the container.<br/> + <br/> + <i>Default</i>: <br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecsutafteractionsindexselectorpod">pod</a></b></td> + <td>object</td> + <td> + Specifies the pod.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.afterActions[index].selector.pod +<sup><sup>[↩ Parent](#benchmarkspecsutafteractionsindexselector)</sup></sup> + + + +Specifies the pod. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>matchLabels</b></td> + <td>map[string]string</td> + <td> + The matchLabels of the desired pod.<br/> + <br/> + <i>Default</i>: map[]<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.beforeActions[index] +<sup><sup>[↩ Parent](#benchmarkspecsut)</sup></sup> + + + + + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b><a href="#benchmarkspecsutbeforeactionsindexexec">exec</a></b></td> + <td>object</td> + <td> + Specifies command to be executed.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecsutbeforeactionsindexselector">selector</a></b></td> + <td>object</td> + <td> + The selector specifies which resource should be selected for the execution of the command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.beforeActions[index].exec +<sup><sup>[↩ Parent](#benchmarkspecsutbeforeactionsindex)</sup></sup> + + + +Specifies command to be executed. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>command</b></td> + <td>[]string</td> + <td> + The command to be executed as string array.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b>timeoutSeconds</b></td> + <td>integer</td> + <td> + Specifies the timeout (in seconds) for the specified command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.beforeActions[index].selector +<sup><sup>[↩ Parent](#benchmarkspecsutbeforeactionsindex)</sup></sup> + + + +The selector specifies which resource should be selected for the execution of the command. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>container</b></td> + <td>string</td> + <td> + Specifies the container.<br/> + <br/> + <i>Default</i>: <br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecsutbeforeactionsindexselectorpod">pod</a></b></td> + <td>object</td> + <td> + Specifies the pod.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.beforeActions[index].selector.pod +<sup><sup>[↩ Parent](#benchmarkspecsutbeforeactionsindexselector)</sup></sup> + + + +Specifies the pod. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>matchLabels</b></td> + <td>map[string]string</td> + <td> + The matchLabels of the desired pod.<br/> + <br/> + <i>Default</i>: map[]<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.resources[index] +<sup><sup>[↩ Parent](#benchmarkspecsut)</sup></sup> + + + + + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b><a href="#benchmarkspecsutresourcesindexconfigmap">configMap</a></b></td> + <td>object</td> + <td> + The configMap resourceSet loads the Kubernetes manifests from an Kubernetes configMap.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecsutresourcesindexfilesystem">fileSystem</a></b></td> + <td>object</td> + <td> + The fileSystem resourceSet loads the Kubernetes manifests from the filesystem.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.sut.resources[index].configMap +<sup><sup>[↩ Parent](#benchmarkspecsutresourcesindex)</sup></sup> + + + +The configMap resourceSet loads the Kubernetes manifests from an Kubernetes configMap. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>files</b></td> <td>[]string</td> <td> (Optional) Specifies which files from the configMap should be loaded. If this field is not set, all files are loaded.<br/> @@ -681,6 +1249,24 @@ The fileSystem resourceSet loads the Kubernetes manifests from the filesystem. </tr> </thead> <tbody><tr> + <td><b><a href="#benchmarkspecinfrastructureafteractionsindex">afterActions</a></b></td> + <td>[]object</td> + <td> + Infrastructure after actions are executed after the teardown of the infrastructure.<br/> + <br/> + <i>Default</i>: []<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecinfrastructurebeforeactionsindex">beforeActions</a></b></td> + <td>[]object</td> + <td> + Infrastructure before actions are executed before the infrastructure is set up.<br/> + <br/> + <i>Default</i>: []<br/> + </td> + <td>false</td> + </tr><tr> <td><b><a href="#benchmarkspecinfrastructureresourcesindex">resources</a></b></td> <td>[]object</td> <td> @@ -693,6 +1279,272 @@ The fileSystem resourceSet loads the Kubernetes manifests from the filesystem. </table> +### benchmark.spec.infrastructure.afterActions[index] +<sup><sup>[↩ Parent](#benchmarkspecinfrastructure)</sup></sup> + + + + + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b><a href="#benchmarkspecinfrastructureafteractionsindexexec">exec</a></b></td> + <td>object</td> + <td> + Specifies command to be executed.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecinfrastructureafteractionsindexselector">selector</a></b></td> + <td>object</td> + <td> + The selector specifies which resource should be selected for the execution of the command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.infrastructure.afterActions[index].exec +<sup><sup>[↩ Parent](#benchmarkspecinfrastructureafteractionsindex)</sup></sup> + + + +Specifies command to be executed. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>command</b></td> + <td>[]string</td> + <td> + The command to be executed as string array.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b>timeoutSeconds</b></td> + <td>integer</td> + <td> + Specifies the timeout (in seconds) for the specified command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.infrastructure.afterActions[index].selector +<sup><sup>[↩ Parent](#benchmarkspecinfrastructureafteractionsindex)</sup></sup> + + + +The selector specifies which resource should be selected for the execution of the command. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>container</b></td> + <td>string</td> + <td> + Specifies the container.<br/> + <br/> + <i>Default</i>: <br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecinfrastructureafteractionsindexselectorpod">pod</a></b></td> + <td>object</td> + <td> + Specifies the pod.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.infrastructure.afterActions[index].selector.pod +<sup><sup>[↩ Parent](#benchmarkspecinfrastructureafteractionsindexselector)</sup></sup> + + + +Specifies the pod. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>matchLabels</b></td> + <td>map[string]string</td> + <td> + The matchLabels of the desired pod.<br/> + <br/> + <i>Default</i>: map[]<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.infrastructure.beforeActions[index] +<sup><sup>[↩ Parent](#benchmarkspecinfrastructure)</sup></sup> + + + + + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b><a href="#benchmarkspecinfrastructurebeforeactionsindexexec">exec</a></b></td> + <td>object</td> + <td> + Specifies command to be executed.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecinfrastructurebeforeactionsindexselector">selector</a></b></td> + <td>object</td> + <td> + The selector specifies which resource should be selected for the execution of the command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.infrastructure.beforeActions[index].exec +<sup><sup>[↩ Parent](#benchmarkspecinfrastructurebeforeactionsindex)</sup></sup> + + + +Specifies command to be executed. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>command</b></td> + <td>[]string</td> + <td> + The command to be executed as string array.<br/> + </td> + <td>false</td> + </tr><tr> + <td><b>timeoutSeconds</b></td> + <td>integer</td> + <td> + Specifies the timeout (in seconds) for the specified command.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.infrastructure.beforeActions[index].selector +<sup><sup>[↩ Parent](#benchmarkspecinfrastructurebeforeactionsindex)</sup></sup> + + + +The selector specifies which resource should be selected for the execution of the command. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>container</b></td> + <td>string</td> + <td> + Specifies the container.<br/> + <br/> + <i>Default</i>: <br/> + </td> + <td>false</td> + </tr><tr> + <td><b><a href="#benchmarkspecinfrastructurebeforeactionsindexselectorpod">pod</a></b></td> + <td>object</td> + <td> + Specifies the pod.<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + +### benchmark.spec.infrastructure.beforeActions[index].selector.pod +<sup><sup>[↩ Parent](#benchmarkspecinfrastructurebeforeactionsindexselector)</sup></sup> + + + +Specifies the pod. + +<table> + <thead> + <tr> + <th>Name</th> + <th>Type</th> + <th>Description</th> + <th>Required</th> + </tr> + </thead> + <tbody><tr> + <td><b>matchLabels</b></td> + <td>map[string]string</td> + <td> + The matchLabels of the desired pod.<br/> + <br/> + <i>Default</i>: map[]<br/> + </td> + <td>false</td> + </tr></tbody> +</table> + + ### benchmark.spec.infrastructure.resources[index] <sup><sup>[↩ Parent](#benchmarkspecinfrastructure)</sup></sup> diff --git a/docs/drafts/actions.md b/docs/drafts/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..8092fddb088b3fe8fc64f51bff03bb0c6504b74f --- /dev/null +++ b/docs/drafts/actions.md @@ -0,0 +1,62 @@ +## Infrastructure +The necessary infrastructure for an execution can be defined in the benchmark manifests. The related resources are create *before* an execution is started, and removed *after* an execution is finished. + +### Example + +```yaml + infrastructure: + resources: + - configMap: + name: "example-configmap" + files: + - "uc1-kstreams-deployment.yaml" +``` + +## Action Commands +Theodolite allows to execute commands on running pods (similar to the `kubectl exec -it <pod-name> -- <command>` command). This commands can be run either before (via so called `beforeActions`) or after (via so called `afterActions`) an experiment is executed. + +Theodolite checks if all required pods are available for the specified actions (i.e. the pods must either be defined as infrastructure or already deployed in the cluster). If not all pods/resources are available, the benchmark will not be set as `Ready`. Consequently, an action cannot be executed on a pod that is defined as an SUT or loadGen resource. + +### Example + +```yaml +# For the system under test + sut: + resources: ... + beforeActions: + - selector: + pod: + matchLabels: + app: busybox1 + exec: + command: ["touch", "test-file-sut"] + timeoutSeconds: 90 + afterActions: + - selector: + pod: + matchLabels: + app: busybox1 + exec: + command: [ "touch", "test-file-sut-after" ] + timeoutSeconds: 90 + +# analog, for the load generator + loadGenerator: + resources: ... + beforeActions: + - selector: + pod: + matchLabels: + app: busybox1 + exec: + command: ["touch", "test-file-loadGen"] + timeoutSeconds: 90 + afterActions: + - selector: + pod: + matchLabels: + app: busybox1 + exec: + command: [ "touch", "test-file-loadGen-after" ] + timeoutSeconds: 90 +``` \ No newline at end of file diff --git a/theodolite/crd/crd-benchmark.yaml b/theodolite/crd/crd-benchmark.yaml index 7ab2e5f3b890a883f68dbbd36805f3791158f256..cd9c9f1e07c38a8727bcd23939319c0955e07645 100644 --- a/theodolite/crd/crd-benchmark.yaml +++ b/theodolite/crd/crd-benchmark.yaml @@ -64,13 +64,87 @@ spec: type: array items: type: string + beforeActions: + type: array + default: [] + description: Infrastructure before actions are executed before the infrastructure is set up. + items: + type: object + properties: + selector: + type: object + description: The selector specifies which resource should be selected for the execution of the command. + properties: + pod: + type: object + description: Specifies the pod. + properties: + matchLabels: + type: object + description: The matchLabels of the desired pod. + additionalProperties: true + x-kubernetes-map-type: "granular" + default: { } + container: + description: Specifies the container. + default: "" + type: string + exec: + type: object + description: Specifies command to be executed. + properties: + command: + type: array + description: The command to be executed as string array. + items: + type: string + timeoutSeconds: + description: Specifies the timeout (in seconds) for the specified command. + type: integer + afterActions: + type: array + default: [] + description: Infrastructure after actions are executed after the teardown of the infrastructure. + items: + type: object + properties: + selector: + type: object + description: The selector specifies which resource should be selected for the execution of the command. + properties: + pod: + type: object + description: Specifies the pod. + properties: + matchLabels: + type: object + description: The matchLabels of the desired pod. + additionalProperties: true + x-kubernetes-map-type: "granular" + default: { } + container: + description: Specifies the container. + default: "" + type: string + exec: + type: object + description: Specifies command to be executed. + properties: + command: + type: array + description: The command to be executed as string array. + items: + type: string + timeoutSeconds: + description: Specifies the timeout (in seconds) for the specified command. + type: integer sut: description: The appResourceSets specifies all Kubernetes resources required to start the sut. A resourceSet can be either a configMap resourceSet or a fileSystem resourceSet. type: object properties: resources: type: array - default: [ ] + default: [] items: type: object oneOf: @@ -101,6 +175,79 @@ spec: type: array items: type: string + beforeActions: + type: array + default: [] + description: SUT before actions are executed before the SUT is started. + items: + type: object + properties: + selector: + type: object + description: The selector specifies which resource should be selected for the execution of the command. + properties: + pod: + type: object + description: Specifies the pod. + properties: + matchLabels: + type: object + description: The matchLabels of the desired pod. + additionalProperties: true + x-kubernetes-map-type: "granular" + default: { } + container: + description: Specifies the container. + default: "" + type: string + exec: + type: object + description: Specifies command to be executed. + properties: + command: + type: array + description: The command to be executed as string array. + items: + type: string + timeoutSeconds: + description: Specifies the timeout (in seconds) for the specified command. + type: integer + afterActions: + type: array + default: [] + items: + type: object + properties: + selector: + type: object + description: The selector specifies which resource should be selected for the execution of the command. + properties: + pod: + type: object + description: Specifies the pod. + properties: + matchLabels: + type: object + description: The matchLabels of the desired pod. + additionalProperties: true + x-kubernetes-map-type: "granular" + default: { } + container: + description: Specifies the container. + default: "" + type: string + exec: + type: object + description: Specifies command to be executed. + properties: + command: + type: array + description: The command to be executed as string array. + items: + type: string + timeoutSeconds: + description: Specifies the timeout (in seconds) for the specified command. + type: integer loadGenerator: description: The loadGenResourceSets specifies all Kubernetes resources required to start the load generator. A resourceSet can be either a configMap resourceSet or a fileSystem resourceSet. type: object @@ -138,6 +285,80 @@ spec: type: array items: type: string + beforeActions: + type: array + default: [ ] + description: Load generator before actions are executed before the load generator is started. + items: + type: object + properties: + selector: + type: object + description: The selector specifies which resource should be selected for the execution of the command. + properties: + pod: + type: object + description: Specifies the pod. + properties: + matchLabels: + type: object + description: The matchLabels of the desired pod. + additionalProperties: true + x-kubernetes-map-type: "granular" + default: { } + container: + description: Specifies the container. + default: "" + type: string + exec: + type: object + description: Specifies command to be executed. + properties: + command: + type: array + description: The command to be executed as string array. + items: + type: string + timeoutSeconds: + description: Specifies the timeout (in seconds) for the specified command. + type: integer + afterActions: + type: array + default: [] + description: Load generator after actions are executed after the teardown of the load generator. + items: + type: object + properties: + selector: + type: object + description: The selector specifies which resource should be selected for the execution of the command. + properties: + pod: + type: object + description: Specifies the pod. + properties: + matchLabels: + type: object + description: The matchLabels of the desired pod. + additionalProperties: true + x-kubernetes-map-type: "granular" + default: { } + container: + description: Specifies the container. + default: "" + type: string + exec: + type: object + description: Specifies command to be executed. + properties: + command: + type: array + description: The command to be executed as string array. + items: + type: string + timeoutSeconds: + description: Specifies the timeout (in seconds) for the specified command. + type: integer resourceTypes: description: A list of resource types that can be scaled for this `benchmark` resource. For each resource type the concrete values are defined in the `execution` object. type: array diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/Action.kt b/theodolite/src/main/kotlin/theodolite/benchmark/Action.kt new file mode 100644 index 0000000000000000000000000000000000000000..35efebdc0fb2a3748660cb76cdd5499b4ca5f622 --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/benchmark/Action.kt @@ -0,0 +1,48 @@ +package theodolite.benchmark + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import io.fabric8.kubernetes.client.NamespacedKubernetesClient +import io.quarkus.runtime.annotations.RegisterForReflection +import theodolite.util.ActionCommandFailedException +import theodolite.util.Configuration + +@JsonDeserialize +@RegisterForReflection +@JsonInclude(JsonInclude.Include.NON_NULL) +class Action { + + lateinit var selector: ActionSelector + lateinit var exec: Command + + fun exec(client: NamespacedKubernetesClient) { + val exitCode = ActionCommand(client = client) + .exec( + matchLabels = selector.pod.matchLabels, + container = selector.container, + timeout = exec.timeoutSeconds, + command = exec.command + ) + if(exitCode != 0){ + throw ActionCommandFailedException("Error while executing action, finished with exit code $exitCode") + } + } +} + +@JsonDeserialize +@RegisterForReflection +class ActionSelector { + lateinit var pod: PodSelector + var container: String = "" +} +@JsonDeserialize +@RegisterForReflection +class PodSelector { + lateinit var matchLabels: MutableMap<String, String> +} +@JsonDeserialize +@RegisterForReflection +class Command { + lateinit var command: Array<String> + var timeoutSeconds: Long = Configuration.TIMEOUT_SECONDS +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/ActionCommand.kt b/theodolite/src/main/kotlin/theodolite/benchmark/ActionCommand.kt new file mode 100644 index 0000000000000000000000000000000000000000..966fa56329c8d7d466dd14858bcbc06bb5b857c3 --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/benchmark/ActionCommand.kt @@ -0,0 +1,161 @@ +package theodolite.benchmark + +import io.fabric8.kubernetes.api.model.Status +import io.fabric8.kubernetes.client.KubernetesClientException +import io.fabric8.kubernetes.client.NamespacedKubernetesClient +import io.fabric8.kubernetes.client.dsl.ExecListener +import io.fabric8.kubernetes.client.dsl.ExecWatch +import io.fabric8.kubernetes.client.utils.Serialization +import mu.KotlinLogging +import okhttp3.Response +import theodolite.util.ActionCommandFailedException +import theodolite.util.Configuration +import java.io.ByteArrayOutputStream +import java.time.Duration +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + + +private val logger = KotlinLogging.logger {} + +class ActionCommand(val client: NamespacedKubernetesClient) { + var out: ByteArrayOutputStream = ByteArrayOutputStream() + var error: ByteArrayOutputStream = ByteArrayOutputStream() + var errChannelStream: ByteArrayOutputStream = ByteArrayOutputStream() + private val execLatch = CountDownLatch(1) + + /** + * Executes an action command. + * + * @param matchLabels matchLabels specifies on which pod the command should be executed. For this, the principle + * `of any` of is used and the command is called on one of the possible pods. + * @param container (Optional) The container to run the command. Is optional iff exactly one container exist. + * @param command The command to be executed. + * @return the exit code of this executed command + */ + fun exec( + matchLabels: MutableMap<String, String>, + command: Array<String>, + timeout: Long = Configuration.TIMEOUT_SECONDS, + container: String = "" + ): Int { + try { + val execWatch: ExecWatch = if (container.isNotEmpty()) { + client.pods() + .inNamespace(client.namespace) + .withName(getPodName(matchLabels, 3)) + .inContainer(container) + + } else { + client.pods() + .inNamespace(client.namespace) + .withName(getPodName(matchLabels, 3)) + } + .writingOutput(out) + .writingError(error) + .writingErrorChannel(errChannelStream) + .usingListener(ActionCommandListener(execLatch)) + .exec(*command) + + val latchTerminationStatus = execLatch.await(timeout, TimeUnit.SECONDS) + if (!latchTerminationStatus) { + throw ActionCommandFailedException("Latch could not terminate within specified time") + } + execWatch.close() + } catch (e: Exception) { + when (e) { + is InterruptedException -> { + Thread.currentThread().interrupt() + throw ActionCommandFailedException("Interrupted while waiting for the exec", e) + } + is KubernetesClientException -> { + throw ActionCommandFailedException("Error while executing command", e) + } + else -> { + throw e + } + } + } + logger.debug { "Execution Output Stream is \n $out" } + logger.debug { "Execution Error Stream is \n $error" } + logger.debug { "Execution ErrorChannel is: \n $errChannelStream" } + return getExitCode(errChannelStream) + } + + private fun getExitCode(errChannelStream: ByteArrayOutputStream): Int { + val status: Status? + try { + status = Serialization.unmarshal(errChannelStream.toString(), Status::class.java) + } catch (e: Exception) { + throw ActionCommandFailedException("Could not determine the exit code, no information given") + } + + if (status == null) { + throw ActionCommandFailedException("Could not determine the exit code, no information given") + } + + return if (status.status.equals("Success")) { + 0 + } else status.details.causes.stream() + .filter { it.reason.equals("ExitCode") } + .map { it.message } + .findFirst() + .orElseThrow { + ActionCommandFailedException("Status is not SUCCESS but contains no exit code - Status: $status") + }.toInt() + } + + /** + * Find pod with matching labels. The matching pod must have the status `Running`. + * + * @param matchLabels the match labels + * @param tries specifies the number of times to look for a matching pod. When pods are newly created, + * it can take a while until the status is ready and the pod can be selected. + * @return the name of the pod or throws [ActionCommandFailedException] + */ + fun getPodName(matchLabels: MutableMap<String, String>, tries: Int): String { + for (i in 1..tries) { + + try { + return getPodName(matchLabels) + } catch (e: Exception) { + logger.warn { "Could not found any pod with specified matchlabels or pod is not ready." } + } + Thread.sleep(Duration.ofSeconds(5).toMillis()) + } + throw ActionCommandFailedException("Couldn't find any pod that matches the specified labels.") + } + + private fun getPodName(matchLabels: MutableMap<String, String>): String { + return try { + val podNames = this.client + .pods() + .withLabels(matchLabels) + .list() + .items + .map { it.metadata.name } + + podNames.first { + this.client.pods().withName(it).isReady + } + + } catch (e: NoSuchElementException) { + throw ActionCommandFailedException("Couldn't find any pod that matches the specified labels.", e) + } + } + + private class ActionCommandListener(val execLatch: CountDownLatch) : ExecListener { + override fun onOpen(response: Response) { + } + + override fun onFailure(throwable: Throwable, response: Response) { + execLatch.countDown() + throw ActionCommandFailedException("Some error encountered while executing action, caused ${throwable.message})") + } + + override fun onClose(code: Int, reason: String) { + execLatch.countDown() + } + } + +} diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt b/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt index 0b81f8701f92a95662efef6e0d58839c9a2f6f3b..2514c32158f07f822b34697cb7c4810848bfd27b 100644 --- a/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt +++ b/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmark.kt @@ -47,7 +47,7 @@ class KubernetesBenchmark : KubernetesResource, Benchmark { var namespace = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE @Transient - private val client: NamespacedKubernetesClient = DefaultKubernetesClient().inNamespace(namespace) + private var client: NamespacedKubernetesClient = DefaultKubernetesClient().inNamespace(namespace) /** * Loads [KubernetesResource]s. @@ -59,6 +59,7 @@ class KubernetesBenchmark : KubernetesResource, Benchmark { } override fun setupInfrastructure() { + this.infrastructure.beforeActions.forEach { it.exec(client = client) } val kubernetesManager = K8sManager(this.client) loadKubernetesResources(this.infrastructure.resources) .map{it.second} @@ -70,7 +71,8 @@ class KubernetesBenchmark : KubernetesResource, Benchmark { loadKubernetesResources(this.infrastructure.resources) .map{it.second} .forEach { kubernetesManager.remove(it) } - } + this.infrastructure.afterActions.forEach { it.exec(client = client) } + } /** * Builds a deployment. @@ -110,6 +112,10 @@ class KubernetesBenchmark : KubernetesResource, Benchmark { } } return KubernetesBenchmarkDeployment( + sutBeforeActions = sut.beforeActions, + sutAfterActions = sut.afterActions, + loadGenBeforeActions = loadGenerator.beforeActions, + loadGenAfterActions = loadGenerator.afterActions, appResources = appResources.map { it.second }, loadGenResources = loadGenResources.map { it.second }, loadGenerationDelay = loadGenerationDelay, @@ -119,4 +125,13 @@ class KubernetesBenchmark : KubernetesResource, Benchmark { client = this.client ) } + + /** + * This function can be used to set the Kubernetes client manually. This is for example necessary for testing. + * + * @param client + */ + fun setClient(client: NamespacedKubernetesClient) { + this.client = client + } } diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt b/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt index 423ac92c654ff55057796d9642c2cb408bc62fe5..9d32a4eeab656143e10b5057a173e04245d6f22b 100644 --- a/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt +++ b/theodolite/src/main/kotlin/theodolite/benchmark/KubernetesBenchmarkDeployment.kt @@ -23,6 +23,10 @@ private val logger = KotlinLogging.logger {} */ @RegisterForReflection class KubernetesBenchmarkDeployment( + private val sutBeforeActions: List<Action>, + private val sutAfterActions: List<Action>, + private val loadGenBeforeActions: List<Action>, + private val loadGenAfterActions: List<Action>, val appResources: List<KubernetesResource>, val loadGenResources: List<KubernetesResource>, private val loadGenerationDelay: Long, @@ -45,10 +49,13 @@ class KubernetesBenchmarkDeployment( val kafkaTopics = this.topics.filter { !it.removeOnly } .map { NewTopic(it.name, it.numPartitions, it.replicationFactor) } kafkaController.createTopics(kafkaTopics) + sutBeforeActions.forEach { it.exec(client = client) } appResources.forEach { kubernetesManager.deploy(it) } logger.info { "Wait ${this.loadGenerationDelay} seconds before starting the load generator." } Thread.sleep(Duration.ofSeconds(this.loadGenerationDelay).toMillis()) + loadGenBeforeActions.forEach { it.exec(client = client) } loadGenResources.forEach { kubernetesManager.deploy(it) } + } /** @@ -59,7 +66,9 @@ class KubernetesBenchmarkDeployment( */ override fun teardown() { loadGenResources.forEach { kubernetesManager.remove(it) } + loadGenAfterActions.forEach { it.exec(client = client) } appResources.forEach { kubernetesManager.remove(it) } + sutAfterActions.forEach { it.exec(client = client) } kafkaController.removeTopics(this.topics.map { topic -> topic.name }) ResourceByLabelHandler(client).removePods( labelName = LAG_EXPORTER_POD_LABEL_NAME, diff --git a/theodolite/src/main/kotlin/theodolite/benchmark/Resources.kt b/theodolite/src/main/kotlin/theodolite/benchmark/Resources.kt index 0187735b8fd273419874942cb7ed68797732c84c..fccbd2c41a646a2ef85ef77c65763e7f793d1e91 100644 --- a/theodolite/src/main/kotlin/theodolite/benchmark/Resources.kt +++ b/theodolite/src/main/kotlin/theodolite/benchmark/Resources.kt @@ -1,7 +1,6 @@ package theodolite.benchmark import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.quarkus.runtime.annotations.RegisterForReflection @JsonDeserialize @@ -9,5 +8,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection class Resources { lateinit var resources: List<ResourceSets> + lateinit var beforeActions: List<Action> + lateinit var afterActions: List<Action> } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateChecker.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateChecker.kt new file mode 100644 index 0000000000000000000000000000000000000000..959b04a8e5c94806aea1753af56b2518436aed12 --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/BenchmarkStateChecker.kt @@ -0,0 +1,201 @@ +package theodolite.execution.operator + +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.MixedOperation +import io.fabric8.kubernetes.client.dsl.Resource +import theodolite.benchmark.Action +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.KubernetesBenchmarkList + +class BenchmarkStateChecker( + private val benchmarkCRDClient: MixedOperation<BenchmarkCRD, KubernetesBenchmarkList, Resource<BenchmarkCRD>>, + private val benchmarkStateHandler: BenchmarkStateHandler, + private val client: NamespacedKubernetesClient + +) { + + fun start(running: Boolean) { + Thread { + while (running) { + updateBenchmarkStatus() + Thread.sleep(100 * 1) + } + }.start() + } + + /** + * Checks and updates the states off all deployed benchmarks. + * + */ + fun updateBenchmarkStatus() { + this.benchmarkCRDClient + .list() + .items + .map { it.spec.name = it.metadata.name; it } + .map { Pair(it, checkState(it.spec)) } + .forEach { setState(it.first, it.second) } + } + + private fun setState(resource: BenchmarkCRD, state: BenchmarkStates) { + benchmarkStateHandler.setResourceSetState(resource.spec.name, state) + } + + /** + * Checks the state of the benchmark. + * + * @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 + ) { + BenchmarkStates.READY + } else { + BenchmarkStates.PENDING + } + } + + /** + * Checks if all specified actions of the given benchmark could be executed or not + * + * @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 { + 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 + } else { + BenchmarkStates.PENDING + } + } + + /** + * Action commands are called on a pod. To verify that an action command can be executed, + * it checks that the specified pods are either currently running in the cluster or + * have been specified as infrastructure in the benchmark. + * + * @param benchmark the benchmark to check + * @param actions the actions + * @return true if all actions could be executed, else false + */ + private fun checkIfActionPossible(resourcesSets: List<ResourceSets>, actions: List<Action>): Boolean { + return !actions.map { + checkIfResourceIsDeployed(it.selector) || checkIfResourceIsInfrastructure(resourcesSets, it.selector) + }.contains(false) + } + + /** + * Checks for the given actionSelector whether the required resources are already deployed in the cluster or not + * + * @param selector the actionSelector to check + * @return true if the required resources are found, else false + */ + fun checkIfResourceIsDeployed(selector: ActionSelector): Boolean { + val pods = this.client + .pods() + .withLabels(selector.pod.matchLabels) + .list() + .items + + return if (pods.isNotEmpty() && selector.container.isNotEmpty()) { + pods.map { pod -> + pod + .spec + .containers + .map { it.name } + .contains(selector.container) + }.contains(true) + } else { + pods.isNotEmpty() + } + } + + /** + * Checks for the given actionSelector whether the required resources are specified as infrastructure or not + * + * @param benchmark the benchmark to check + * @param selector the actionSelector to check + * @return true if the required resources are found, else false + */ + fun checkIfResourceIsInfrastructure(resourcesSets: List<ResourceSets>, selector: ActionSelector): Boolean { + val resources = resourcesSets.flatMap { it.loadResourceSet(this.client) } + if (resources.isEmpty()) { + return false + } + + var podExist = resources.map { it.second } + .filterIsInstance<Deployment>() + .filter { it.metadata.labels.containsMatchLabels(selector.pod.matchLabels) } + .any { + if (selector.container.isNotEmpty()) { + it.spec.template.spec.containers.map { it.name }.contains(selector.container) + } else { + true + } + } + + if (podExist) { + return true + } + + podExist = resources.map { it.second } + .filterIsInstance<StatefulSet>() + .filter { it.metadata.labels.containsMatchLabels(selector.pod.matchLabels) } + .any { + if (selector.container.isNotEmpty()) { + it.spec.template.spec.containers.map { it.name }.contains(selector.container) + } else { + true + } + } + + if (podExist) { + return true + } + + return false + } + + /** + * 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] + */ + fun checkResources(benchmark: KubernetesBenchmark): BenchmarkStates { + 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 + } else { + BenchmarkStates.PENDING + } + } catch (e: Exception) { + BenchmarkStates.PENDING + } + } +} + +private fun <K, V> MutableMap<K, V>.containsMatchLabels(matchLabels: MutableMap<V, V>): Boolean { + for (kv in matchLabels) { + if (kv.value != this[kv.key as K]) { + return false + } + } + return true +} + diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt index 70e30cf84ef40796eb085a0d68eb2e323232fde9..f066c01024fef98fc3e6e2070b0ed98235a1f8bb 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteController.kt @@ -29,7 +29,7 @@ class TheodoliteController( private val executionCRDClient: MixedOperation<ExecutionCRD, BenchmarkExecutionList, Resource<ExecutionCRD>>, private val benchmarkCRDClient: MixedOperation<BenchmarkCRD, KubernetesBenchmarkList, Resource<BenchmarkCRD>>, private val executionStateHandler: ExecutionStateHandler, - private val benchmarkStateHandler: BenchmarkStateHandler + private val benchmarkStateChecker: BenchmarkStateChecker ) { lateinit var executor: TheodoliteExecutor @@ -41,7 +41,7 @@ class TheodoliteController( sleep(5000) // wait until all states are correctly set while (true) { reconcile() - updateBenchmarkStatus() + benchmarkStateChecker.start(true) sleep(2000) } } @@ -49,7 +49,6 @@ class TheodoliteController( private fun reconcile() { do { val execution = getNextExecution() - updateBenchmarkStatus() if (execution != null) { val benchmark = getBenchmarks() .map { it.spec } @@ -110,8 +109,7 @@ class TheodoliteController( type = "WARNING", reason = "Execution failed", message = "An error occurs while executing: ${e.message}") - logger.error { "Failure while executing execution ${execution.name} with benchmark ${benchmark.name}." } - logger.error { "Problem is: $e" } + logger.error(e) { "Failure while executing execution ${execution.name} with benchmark ${benchmark.name}." } executionStateHandler.setExecutionState(execution.name, ExecutionStates.FAILURE) } executionStateHandler.stopDurationStateTimer() @@ -139,7 +137,6 @@ class TheodoliteController( } } - /** * Get the [BenchmarkExecution] for the next run. Which [BenchmarkExecution] * is selected for the next execution depends on three points: @@ -173,34 +170,7 @@ class TheodoliteController( .firstOrNull() } - private fun updateBenchmarkStatus() { - this.benchmarkCRDClient - .list() - .items - .map { it.spec.name = it.metadata.name; it } - .map { Pair(it, checkResource(it.spec)) } - .forEach { setState(it.first, it.second ) } - } - - private fun setState(resource: BenchmarkCRD, state: BenchmarkStates) { - benchmarkStateHandler.setResourceSetState(resource.spec.name, state) - } - private fun checkResource(benchmark: KubernetesBenchmark): BenchmarkStates { - return try { - val appResources = - benchmark.loadKubernetesResources(resourceSet = benchmark.sut.resources) - val loadGenResources = - benchmark.loadKubernetesResources(resourceSet = benchmark.sut.resources) - if(appResources.isNotEmpty() && loadGenResources.isNotEmpty()) { - BenchmarkStates.READY - } else { - BenchmarkStates.PENDING - } - } catch (e: Exception) { - BenchmarkStates.PENDING - } - } fun isExecutionRunning(executionName: String): Boolean { if (!::executor.isInitialized) return false diff --git a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt index 4850a44fdddba117178e29d3170f44a95df646e7..135ffeaef1a5165482d9d6f7f8f5f3dffd596574 100644 --- a/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt +++ b/theodolite/src/main/kotlin/theodolite/execution/operator/TheodoliteOperator.kt @@ -34,6 +34,7 @@ class TheodoliteOperator { private lateinit var controller: TheodoliteController private lateinit var executionStateHandler: ExecutionStateHandler private lateinit var benchmarkStateHandler: BenchmarkStateHandler + private lateinit var benchmarkStateChecker: BenchmarkStateChecker fun start() { @@ -71,7 +72,7 @@ class TheodoliteOperator { controller = getController( client = client, executionStateHandler = getExecutionStateHandler(client = client), - benchmarkStateHandler = getBenchmarkStateHandler(client = client) + benchmarkStateChecker = getBenchmarkStateChecker(client = client) ) getExecutionEventHandler(controller, client).startAllRegisteredInformers() @@ -112,17 +113,28 @@ class TheodoliteOperator { return benchmarkStateHandler } + fun getBenchmarkStateChecker(client: NamespacedKubernetesClient) : BenchmarkStateChecker { + if (!::benchmarkStateChecker.isInitialized) { + this.benchmarkStateChecker = BenchmarkStateChecker( + client = client, + benchmarkStateHandler = getBenchmarkStateHandler(client = client), + benchmarkCRDClient = getBenchmarkClient(client = client)) + } + return benchmarkStateChecker + } + + fun getController( client: NamespacedKubernetesClient, executionStateHandler: ExecutionStateHandler, - benchmarkStateHandler: BenchmarkStateHandler + benchmarkStateChecker: BenchmarkStateChecker ): TheodoliteController { if (!::controller.isInitialized) { this.controller = TheodoliteController( benchmarkCRDClient = getBenchmarkClient(client), executionCRDClient = getExecutionClient(client), executionStateHandler = executionStateHandler, - benchmarkStateHandler = benchmarkStateHandler + benchmarkStateChecker = benchmarkStateChecker ) } return this.controller diff --git a/theodolite/src/main/kotlin/theodolite/util/ActionCommandFailedException.kt b/theodolite/src/main/kotlin/theodolite/util/ActionCommandFailedException.kt new file mode 100644 index 0000000000000000000000000000000000000000..c1a8fc401961370d2f07bfffe43f0ae4dc441d25 --- /dev/null +++ b/theodolite/src/main/kotlin/theodolite/util/ActionCommandFailedException.kt @@ -0,0 +1,4 @@ +package theodolite.util + +class ActionCommandFailedException(message: String, e: Exception? = null) : DeploymentFailedException(message,e) { +} \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/util/Configuration.kt b/theodolite/src/main/kotlin/theodolite/util/Configuration.kt index dac3b943e69bd7e208d318f2a788275f19db11e4..7b1232cd9ba72344cdb438f974cd6c4d17fd690d 100644 --- a/theodolite/src/main/kotlin/theodolite/util/Configuration.kt +++ b/theodolite/src/main/kotlin/theodolite/util/Configuration.kt @@ -13,6 +13,11 @@ class Configuration( val NAMESPACE = System.getenv("NAMESPACE") ?: DEFAULT_NAMESPACE val COMPONENT_NAME = System.getenv("COMPONENT_NAME") ?: DEFAULT_COMPONENT_NAME val EXECUTION_MODE = System.getenv("MODE") ?: ExecutionModes.STANDALONE.value + + /** + * Specifies how long Theodolite should wait (in sec) before aborting the execution of an action command. + */ + const val TIMEOUT_SECONDS: Long = 30L } } diff --git a/theodolite/src/test/kotlin/theodolite/benchmark/ActionCommandTest.kt b/theodolite/src/test/kotlin/theodolite/benchmark/ActionCommandTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e40fca5caf9fe721c547e09d2ba22c25860a1bf --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/benchmark/ActionCommandTest.kt @@ -0,0 +1,128 @@ +package theodolite.benchmark + +import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.PodBuilder +import io.fabric8.kubernetes.api.model.PodListBuilder +import io.fabric8.kubernetes.client.server.mock.KubernetesServer +import io.fabric8.kubernetes.client.utils.Utils +import io.quarkus.test.junit.QuarkusTest +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.assertEquals +import theodolite.execution.operator.TheodoliteController +import theodolite.execution.operator.TheodoliteOperator +import theodolite.util.ActionCommandFailedException + +@QuarkusTest +class ActionCommandTest { + private val server = KubernetesServer(false, false) + lateinit var controller: TheodoliteController + + @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) + ) + + val pod: Pod = PodBuilder().withNewMetadata() + .withName("pod1") + .withResourceVersion("1") + .withLabels<String, String>(mapOf("app" to "pod")) + .withNamespace("test").and() + .build() + + val ready: Pod = createReadyFrom(pod, "True") + + val podList = PodListBuilder().build() + podList.items.add(0, ready) + + server + .expect() + .withPath("/api/v1/namespaces/test/pods?labelSelector=${Utils.toUrlEncoded("app=pod")}") + .andReturn(200, podList) + .always() + + server + .expect() + .get() + .withPath("/api/v1/namespaces/test/pods/pod1") + .andReturn(200, ready) + .always() + + server + .expect() + .withPath("/api/v1/namespaces/test/pods/pod1/exec?command=ls&stdout=true&stderr=true") + .andUpgradeToWebSocket() + .open(ErrorChannelMessage("{\"metadata\":{},\"status\":\"Success\"}\n")) + .done() + .always() + + server + .expect() + .withPath("/api/v1/namespaces/test/pods/pod1/exec?command=error-command&stdout=true&stderr=true") + .andUpgradeToWebSocket() + .open(ErrorChannelMessage("{\"metadata\":{},\"status\":\"failed\", \"details\":{}}\n")) + .done() + .always() + } + + /** + * Copied from fabric8 Kubernetes Client repository + * + * @param pod + * @param status + * @return + */ + fun createReadyFrom(pod: Pod, status: String): Pod { + return PodBuilder(pod) + .withNewStatus() + .addNewCondition() + .withType("Ready") + .withStatus(status) + .endCondition() + .endStatus() + .build() + } + + @AfterEach + fun tearDown() { + server.after() + } + + @Test + fun testGetPodName() { + assertEquals("pod1", ActionCommand(client = server.client).getPodName(mutableMapOf("app" to "pod"), 1)) + } + + @Test + fun testActionSuccess() { + val action = Action() + action.selector = ActionSelector() + action.selector.pod = PodSelector() + action.selector.pod.matchLabels = mutableMapOf("app" to "pod") + action.exec = Command() + action.exec.command = arrayOf("ls") + action.exec.timeoutSeconds = 10L + + action.exec(server.client) + assertEquals( + "/api/v1/namespaces/test/pods/pod1/exec?command=ls&stdout=true&stderr=true", + server.lastRequest.path) + } + + @Test + fun testActionFailed() { + val action = Action() + action.selector = ActionSelector() + action.selector.pod = PodSelector() + action.selector.pod.matchLabels = mutableMapOf("app" to "pod") + action.exec = Command() + action.exec.command = arrayOf("error-command") + action.exec.timeoutSeconds = 10L + + assertThrows<ActionCommandFailedException> { run { action.exec(server.client) } } + } +} diff --git a/theodolite/src/test/kotlin/theodolite/benchmark/ErrorChannelMessage.kt b/theodolite/src/test/kotlin/theodolite/benchmark/ErrorChannelMessage.kt new file mode 100644 index 0000000000000000000000000000000000000000..df57a2529653a39ccbde14b4a91d30352224457e --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/benchmark/ErrorChannelMessage.kt @@ -0,0 +1,17 @@ +package theodolite.benchmark + +import io.fabric8.mockwebserver.internal.WebSocketMessage +import java.nio.charset.StandardCharsets + +class ErrorChannelMessage(body: String) : WebSocketMessage(0L, getBodyBytes(OUT_STREAM_ID, body), true, true) { + companion object { + private const val OUT_STREAM_ID: Byte = 3 + private fun getBodyBytes(prefix: Byte, body: String): ByteArray { + val original = body.toByteArray(StandardCharsets.UTF_8) + val prefixed = ByteArray(original.size + 1) + prefixed[0] = prefix + System.arraycopy(original, 0, prefixed, 1, original.size) + return prefixed + } + } +} diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt index e294ea539ea60104cc00e9f73de790302ad52670..b4d5950542c40aba0f39b1be772823a3de389793 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkCRDummy.kt @@ -34,6 +34,13 @@ class BenchmarkCRDummy(name: String) { benchmark.sut.resources = emptyList() benchmark.loadGenerator.resources = emptyList() + benchmark.infrastructure.beforeActions = emptyList() + benchmark.infrastructure.afterActions = emptyList() + benchmark.sut.beforeActions = emptyList() + benchmark.sut.afterActions = emptyList() + benchmark.loadGenerator.beforeActions = emptyList() + benchmark.loadGenerator.afterActions = emptyList() + benchmark.resourceTypes = emptyList() benchmark.loadTypes = emptyList() benchmark.kafkaConfig = kafkaConfig diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkStateCheckerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkStateCheckerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3af42548d3bfc0d12e9f664d11cce1ae424e748 --- /dev/null +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/BenchmarkStateCheckerTest.kt @@ -0,0 +1,177 @@ +package theodolite.execution.operator + +import com.google.gson.Gson +import io.fabric8.kubernetes.api.model.ConfigMapBuilder +import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.PodBuilder +import io.fabric8.kubernetes.api.model.PodListBuilder +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder +import io.fabric8.kubernetes.client.server.mock.KubernetesServer +import io.fabric8.kubernetes.client.server.mock.OutputStreamMessage +import io.fabric8.kubernetes.client.utils.Utils +import org.junit.jupiter.api.AfterEach +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 + +internal class BenchmarkStateCheckerTest { + private val server = KubernetesServer(false, false) + private val serverCrud = KubernetesServer(false, true) + private lateinit var checker: BenchmarkStateChecker + private lateinit var checkerCrud: BenchmarkStateChecker + + @BeforeEach + fun setUp() { + server.before() + serverCrud.before() + val operator = TheodoliteOperator() + checker = BenchmarkStateChecker( + client = server.client, + benchmarkCRDClient = operator.getBenchmarkClient(server.client), + benchmarkStateHandler = operator.getBenchmarkStateHandler(server.client) + ) + + checkerCrud = BenchmarkStateChecker( + client = serverCrud.client, + benchmarkCRDClient = operator.getBenchmarkClient(serverCrud.client), + benchmarkStateHandler = operator.getBenchmarkStateHandler(serverCrud.client) + ) + + val pod: Pod = PodBuilder().withNewMetadata() + .withName("pod1") + .withResourceVersion("1") + .withLabels<String, String>(mapOf("app" to "pod")) + .withNamespace("test").and() + .build() + + val ready: Pod = createReadyFrom(pod, "True") + + val podList = PodListBuilder().build() + podList.items.add(0, ready) + + + server + .expect() + .withPath("/api/v1/namespaces/test/pods?labelSelector=${Utils.toUrlEncoded("app=pod1")}") + .andReturn(200, podList) + .always() + + server + .expect() + .withPath("/api/v1/namespaces/test/pods?labelSelector=${Utils.toUrlEncoded("app=pod0")}") + .andReturn(200, emptyMap<String, String>()) + .always() + + + server + .expect() + .get() + .withPath("/api/v1/namespaces/test/pods/pod1") + .andReturn(200, ready) + .always() + + server + .expect() + .withPath("/api/v1/namespaces/test/pods/pod1/exec?command=ls&stdout=true&stderr=true") + .andUpgradeToWebSocket() + .open(OutputStreamMessage("Test-Output")) + .done() + .always() + } + + @AfterEach + fun tearDown() { + server.after() + serverCrud.after() + } + + /** + * Copied from fabric8 Kubernetes Client repository + * + * @param pod + * @param status + * @return + */ + private fun createReadyFrom(pod: Pod, status: String): Pod { + return PodBuilder(pod) + .withNewStatus() + .addNewCondition() + .withType("Ready") + .withStatus(status) + .endCondition() + .endStatus() + .build() + } + + private fun getActionSelector(label: Pair<String, String>): ActionSelector { + val podSelector = PodSelector() + val actionSelector = ActionSelector() + actionSelector.pod = podSelector + + // pod with matching labels are deployed + podSelector.matchLabels = mutableMapOf(label) + return actionSelector + } + + private fun createAndDeployConfigmapResourceSet(): ResourceSets { + // create test deployment + val resourceBuilder = DeploymentBuilder() + resourceBuilder.withNewSpec().endSpec() + resourceBuilder.withNewMetadata().endMetadata() + val resource = resourceBuilder.build() + resource.metadata.name = "test-deployment" + resource.metadata.labels = mutableMapOf("app" to "pod1") + val resourceString = Gson().toJson(resource) + + // create and deploy configmap + val configMap1 = ConfigMapBuilder() + .withNewMetadata().withName("test-configmap").endMetadata() + .addToData("test-resource.yaml",resourceString) + .build() + + serverCrud.client.configMaps().createOrReplace(configMap1) + + // create configmap resource set + val resourceSet = ConfigMapResourceSet() + resourceSet.name = "test-configmap" + + // create ResourceSetsList + val set = ResourceSets() + set.configMap = resourceSet + return set + } + + @Test + fun checkIfResourceIsDeployed() { + // pod with matching labels are deployed + assertTrue(checker.checkIfResourceIsDeployed(getActionSelector("app" to "pod1"))) + + // no pod with matching labels are deployed + assertFalse(checker.checkIfResourceIsDeployed(getActionSelector("app" to "pod0"))) + } + + @Test + fun checkIfResourceIsInfrastructure() { + val resourceSets = listOf(createAndDeployConfigmapResourceSet()) + assertTrue(checkerCrud.checkIfResourceIsInfrastructure(resourceSets, getActionSelector("app" to "pod1"))) + assertFalse(checkerCrud.checkIfResourceIsInfrastructure(resourceSets, getActionSelector("app" to "pod0"))) + + } + + @Test + fun checkResources() { + val benchmark = BenchmarkCRDummy( + name = "test-benchmark" + ) + benchmark.getCR().spec.setClient(serverCrud.client) + val resourceSet = Resources() + resourceSet.resources = listOf(createAndDeployConfigmapResourceSet()) + benchmark.getCR().spec.infrastructure = resourceSet + benchmark.getCR().spec.loadGenerator = resourceSet + benchmark.getCR().spec.sut = resourceSet + + assertEquals(BenchmarkStates.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 7e0532aff36cac2fb1a1c718415315b8f54052c2..6ea69689847afeb8f9fc36de2944c6fdcf4702ad 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/ControllerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ControllerTest.kt @@ -32,10 +32,11 @@ class ControllerTest { @BeforeEach fun setUp() { server.before() - this.controller = TheodoliteOperator().getController( + val operator = TheodoliteOperator() + this.controller = operator.getController( client = server.client, - executionStateHandler = ExecutionStateHandler(server.client), - benchmarkStateHandler = BenchmarkStateHandler(server.client) + executionStateHandler = operator.getExecutionStateHandler(client = server.client), + benchmarkStateChecker = operator.getBenchmarkStateChecker(client = server.client) ) // benchmark diff --git a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt index d8db7ab3b64ce3856984ddbc279ef148aa325e73..c850e84f225bab7fc0b5eb145f9e655567de43d0 100644 --- a/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt +++ b/theodolite/src/test/kotlin/theodolite/execution/operator/ExecutionEventHandlerTest.kt @@ -36,8 +36,8 @@ class ExecutionEventHandlerTest { val operator = TheodoliteOperator() this.controller = operator.getController( client = server.client, - executionStateHandler = ExecutionStateHandler(client = server.client), - benchmarkStateHandler = BenchmarkStateHandler(client = server.client) + executionStateHandler = operator.getExecutionStateHandler(client = server.client), + benchmarkStateChecker = operator.getBenchmarkStateChecker(client = server.client) ) this.factory = operator.getExecutionEventHandler(this.controller, server.client)