diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index d988ea4325f..a2592f16301 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -770,6 +770,7 @@ paths: - $ref: "#/components/parameters/ID" - $ref: "#/components/parameters/Namespace" - $ref: "#/components/parameters/LastNDays" + - $ref: "#/components/parameters/TestSuiteExecutionName" tags: - api - test-suites @@ -1131,7 +1132,7 @@ paths: - $ref: "#/components/parameters/Namespace" - $ref: "#/components/parameters/Selector" - $ref: "#/components/parameters/ExecutionSelector" - - $ref: "#/components/parameters/ConcurrencyLevel" + - $ref: "#/components/parameters/ConcurrencyLevel" tags: - api - tests @@ -1847,6 +1848,7 @@ paths: parameters: - $ref: "#/components/parameters/ID" - $ref: "#/components/parameters/Namespace" + - $ref: "#/components/parameters/TestExecutionName" tags: - api - tests @@ -3328,6 +3330,35 @@ components: runningContext: $ref: "#/components/schemas/RunningContext" description: running context for the test suite execution + testSuiteExecutionName: + type: string + description: test suite execution name started the test suite execution + + TestSuiteExecutionCR: + type: object + required: + - testSuite + properties: + testSuite: + $ref: "#/components/schemas/ObjectRef" + description: test suite name and namespace + executionRequest: + $ref: "#/components/schemas/TestSuiteExecutionRequest" + description: test suite execution request parameters + status: + $ref: "#/components/schemas/TestSuiteExecutionStatusCR" + description: test suite execution status + + TestSuiteExecutionStatusCR: + type: object + description: test suite execution status + properties: + latestExecution: + $ref: "#/components/schemas/TestSuiteExecution" + generation: + type: integer + format: int64 + description: test suite execution generation TestSuiteExecutionStatus: type: string @@ -3556,6 +3587,32 @@ components: status: $ref: "#/components/schemas/TestStatus" + TestExecutionCR: + type: object + required: + - test + properties: + test: + $ref: "#/components/schemas/ObjectRef" + description: test name and namespace + executionRequest: + $ref: "#/components/schemas/ExecutionRequest" + description: test execution request parameters + status: + $ref: "#/components/schemas/TestExecutionStatusCR" + description: test execution status + + TestExecutionStatusCR: + type: object + description: test execution status + properties: + latestExecution: + $ref: "#/components/schemas/Execution" + generation: + type: integer + format: int64 + description: test execution generation + TestContent: type: object properties: @@ -3857,7 +3914,10 @@ components: containerShell: type: string description: shell used in container executor - example: "/bin/sh" + example: "/bin/sh" + testExecutionName: + type: string + description: test execution name started the test execution Artifact: type: object @@ -4379,6 +4439,9 @@ components: runningContext: $ref: "#/components/schemas/RunningContext" description: running context for the test execution + testExecutionName: + type: string + description: test execution name started the test execution ExecutionUpdateRequest: description: test execution request update body @@ -4456,6 +4519,9 @@ components: format: int32 description: number of tests run in parallel example: 10 + testSuiteExecutionName: + type: string + description: test suite execution name started the test suite execution TestSuiteExecutionUpdateRequest: description: test suite execution update request body @@ -5374,6 +5440,18 @@ components: schema: type: integer default: 10 + TestExecutionName: + in: query + name: testExecutionName + schema: + type: string + description: test execution name stated the test execution + TestSuiteExecutionName: + in: query + name: testSuiteExecutionName + schema: + type: string + description: test suite execution name stated the test suite execution Namespace: in: query name: namespace diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index af3b784e28e..9999884d3d4 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -59,9 +59,11 @@ import ( kubeclient "github.com/kubeshop/testkube-operator/client" executorsclientv1 "github.com/kubeshop/testkube-operator/client/executors/v1" scriptsclient "github.com/kubeshop/testkube-operator/client/scripts/v2" + testexecutionsclientv1 "github.com/kubeshop/testkube-operator/client/testexecutions/v1" testsclientv1 "github.com/kubeshop/testkube-operator/client/tests" testsclientv3 "github.com/kubeshop/testkube-operator/client/tests/v3" testsourcesclientv1 "github.com/kubeshop/testkube-operator/client/testsources/v1" + testsuiteexecutionsclientv1 "github.com/kubeshop/testkube-operator/client/testsuiteexecutions/v1" testsuitesclientv2 "github.com/kubeshop/testkube-operator/client/testsuites/v2" testsuitesclientv3 "github.com/kubeshop/testkube-operator/client/testsuites/v3" apiv1 "github.com/kubeshop/testkube/internal/app/api/v1" @@ -159,6 +161,8 @@ func main() { testsuitesClientV2 := testsuitesclientv2.NewClient(kubeClient, cfg.TestkubeNamespace) testsuitesClientV3 := testsuitesclientv3.NewClient(kubeClient, cfg.TestkubeNamespace) testsourcesClient := testsourcesclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) + testExecutionsClient := testexecutionsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) + testsuiteExecutionsClient := testsuiteexecutionsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) clientset, err := k8sclient.ConnectToK8s() if err != nil { @@ -316,6 +320,7 @@ func main() { configMapConfig, testsClientV3, clientset, + testExecutionsClient, cfg.TestkubeRegistry, cfg.TestkubePodStartTimeout, clusterId, @@ -340,6 +345,7 @@ func main() { configMapConfig, executorsClient, testsClientV3, + testExecutionsClient, cfg.TestkubeRegistry, cfg.TestkubePodStartTimeout, clusterId, @@ -363,6 +369,7 @@ func main() { log.DefaultLogger, configMapConfig, configMapClient, + testsuiteExecutionsClient, ) slackLoader, err := newSlackLoader(cfg) diff --git a/docs/docs/articles/crds.md b/docs/docs/articles/crds.md index 77164c21429..d4295c783c4 100644 --- a/docs/docs/articles/crds.md +++ b/docs/docs/articles/crds.md @@ -10,13 +10,15 @@ kubectl get crds -n testkube ```sh title="Expected output:" NAME CREATED AT -executors.executor.testkube.io 2023-06-15T14:49:11Z -scripts.tests.testkube.io 2023-06-15T14:49:11Z -tests.tests.testkube.io 2023-06-15T14:49:11Z -testsources.tests.testkube.io 2023-06-15T14:49:11Z -testsuites.tests.testkube.io 2023-06-15T14:49:11Z -testtriggers.tests.testkube.io 2023-06-15T14:49:11Z -webhooks.executor.testkube.io 2023-06-15T14:49:11Z +executors.executor.testkube.io 2023-06-15T14:49:11Z +scripts.tests.testkube.io 2023-06-15T14:49:11Z +testexecutions.tests.testkube.io 2023-06-15T14:49:11Z +tests.tests.testkube.io 2023-06-15T14:49:11Z +testsources.tests.testkube.io 2023-06-15T14:49:11Z +testsuiteexecutions.tests.testkube.io 2023-06-15T14:49:11Z +testsuites.tests.testkube.io 2023-06-15T14:49:11Z +testtriggers.tests.testkube.io 2023-06-15T14:49:11Z +webhooks.executor.testkube.io 2023-06-15T14:49:11Z ``` To check details on one of the CRDs, use `describe`: diff --git a/docs/docs/articles/test-executions.md b/docs/docs/articles/test-executions.md new file mode 100644 index 00000000000..8d1d7c501d8 --- /dev/null +++ b/docs/docs/articles/test-executions.md @@ -0,0 +1,69 @@ +# Test and Test Suite Execution CRDs + +Testkube allows you to automatically run tests and test suites by creating or updating Test or Test Suite Execution CRDs. + +## What are Testkube Execution CRDs? + +In generic terms, an _Execution_ defines a _test_ or _testsuite_ which will be executed when CRD is created or updated. For example, we could define a _TestExecution_ which _runs_ a _Test_ when a _TestExecution_ gets _modified_. + +#### Selecting Resource + +Names are used when we want to select a specific resource. + +```yaml +test: + name: Testkube test name +``` + +or + +```yaml +testSuite: + name: Testkube test suite name +``` + +### Execution Request + +An Execution Request defines execution parameters for each specific resource. + +## Example + +Here are examples for a **Test Execution** *testexecution-example* which runs the **Test** *test-example* +when a **Test Execution** is created or updated and a **Test Suite Execution** *testsuiteexecution-example* +which runs the **Test Suite** *testsuite-example * when a **Test Suite Execution** is created or updated. + +```yaml +apiVersion: tests.testkube.io/v1 +kind: TestExecution +metadata: + name: testexecution-example +spec: + test: + name: test-example + executionRequest: + variables: + VAR_TEST: + name: VAR_TEST + value: "ANY" + type: basic +``` + +```yaml +apiVersion: tests.testkube.io/v1 +kind: TestSuiteExecution +metadata: + name: testsuiteexecution-example +spec: + testSuite: + name: testsuite-example + executionRequest: + variables: + VAR_TEST: + name: VAR_TEST + value: "ANY" + type: basic +``` + +## Architecture + +Testkube uses a Kubernetes Operator to reconcile Test and Test Suite Execution CRDs state and run the corresponding test and test suite when resource generation is changed. \ No newline at end of file diff --git a/docs/redirects.js b/docs/redirects.js index abb7a8d1353..274f137aafb 100644 --- a/docs/redirects.js +++ b/docs/redirects.js @@ -204,6 +204,10 @@ const redirects = [ from: "/concepts/test-sources", to: "/articles/test-sources", }, + { + from: "/concepts/test-executions", + to: "/articles/test-executions", + }, { from: [ "/guides/going-to-production/exposing-testkube/overview", diff --git a/docs/sidebars.js b/docs/sidebars.js index b2eccb4b3bc..8fd59945d8b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -79,6 +79,7 @@ const sidebars = { "articles/test-triggers", "articles/webhooks", "articles/test-sources", + "articles/test-executions", ], }, { diff --git a/go.mod b/go.mod index 2c5e16cf0a8..2af81e52f52 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/joshdk/go-junit v1.0.0 github.com/kelseyhightower/envconfig v1.4.0 - github.com/kubeshop/testkube-operator v1.10.8-0.20230706105320-8815bd839764 + github.com/kubeshop/testkube-operator v1.10.8-0.20230811095859-071c677e571a github.com/minio/minio-go/v7 v7.0.47 github.com/montanaflynn/stats v0.6.6 github.com/moogar0880/problems v0.1.1 @@ -78,6 +78,7 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.1 // indirect github.com/go-openapi/swag v0.22.3 // indirect diff --git a/go.sum b/go.sum index 891c1e95713..d547edf8355 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,7 @@ github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTx github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -208,9 +209,11 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= @@ -389,6 +392,22 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubeshop/testkube-operator v1.10.8-0.20230706105320-8815bd839764 h1:r6JErrRK9Tpzi/JlwAKMeaBlJIkFyZVYtDbd2UA0QP4= github.com/kubeshop/testkube-operator v1.10.8-0.20230706105320-8815bd839764/go.mod h1:6Rs8MugOzaMcthGzobf6GBlRzbOFiK/GJiuYN6MCfEw= +github.com/kubeshop/testkube-operator v1.10.8-0.20230726181828-3b9d1b2cba71 h1:7HIWhpJiLuXO1+90baDBvBNNE51j0Frz05vbtxMbNKI= +github.com/kubeshop/testkube-operator v1.10.8-0.20230726181828-3b9d1b2cba71/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230801130705-c12432823829 h1:FGAFZ/uBxpxwCagWEFZsLkneRDS7CUgWZ/mhMqG7WZQ= +github.com/kubeshop/testkube-operator v1.10.8-0.20230801130705-c12432823829/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230801134951-f927998a7367 h1:WMfr/9G6ioyij12K0l6STIC46PlmCuY8X0gs+uZQIRE= +github.com/kubeshop/testkube-operator v1.10.8-0.20230801134951-f927998a7367/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230803134239-5e8e729b0cd2 h1:utq0ZU34uUFlS7X7y7ZFyxsSxtIptLSdjaFDUTKUtxA= +github.com/kubeshop/testkube-operator v1.10.8-0.20230803134239-5e8e729b0cd2/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230803181918-bb833ffe3256 h1:PLHBpNNtqxBKdA1Wj5fwELejEPOzZJD8JmiANBQQhKk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230803181918-bb833ffe3256/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230809185112-22328f643146 h1:kJ0nbwdMPM0lf9MO7diHJWA3rX9QGDa4nlC4z6nBu1w= +github.com/kubeshop/testkube-operator v1.10.8-0.20230809185112-22328f643146/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230810132521-4808cf664f7e h1:Ud63CIX4XIV8BY0HtEHRqn8ls30xDE0Whv8g4oQeLNU= +github.com/kubeshop/testkube-operator v1.10.8-0.20230810132521-4808cf664f7e/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= +github.com/kubeshop/testkube-operator v1.10.8-0.20230811095859-071c677e571a h1:3/qFrD87cOetsWuoRhf8Bevkl5ZZgcJDhmCokoFU0pY= +github.com/kubeshop/testkube-operator v1.10.8-0.20230811095859-071c677e571a/go.mod h1:ELhxGeqtRRihizW+4yd4wT+3QFCdg2JvDANqXkpAWdk= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= @@ -636,11 +655,15 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -871,6 +894,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/internal/app/api/v1/executions.go b/internal/app/api/v1/executions.go index edcd7b806bd..a1d2d2137d6 100644 --- a/internal/app/api/v1/executions.go +++ b/internal/app/api/v1/executions.go @@ -81,6 +81,7 @@ func (s *TestkubeAPI) ExecuteTestsHandler() fiber.Handler { } var results []testkube.Execution if len(tests) != 0 { + request.TestExecutionName = c.Query("testExecutionName") concurrencyLevel, err := strconv.Atoi(c.Query("concurrency", strconv.Itoa(scheduler.DefaultConcurrencyLevel))) if err != nil { return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: can't detect concurrency level: %w", errPrefix, err)) diff --git a/internal/app/api/v1/testsuites.go b/internal/app/api/v1/testsuites.go index aa59845bcf6..7d2239447b5 100644 --- a/internal/app/api/v1/testsuites.go +++ b/internal/app/api/v1/testsuites.go @@ -631,6 +631,7 @@ func (s TestkubeAPI) ExecuteTestSuitesHandler() fiber.Handler { var results []testkube.TestSuiteExecution if len(testSuites) != 0 { + request.TestSuiteExecutionName = c.Query("testSuiteExecutionName") concurrencyLevel, err := strconv.Atoi(c.Query("concurrency", strconv.Itoa(scheduler.DefaultConcurrencyLevel))) if err != nil { return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: can't detect concurrency level: %w", errPrefix, err)) diff --git a/pkg/api/v1/testkube/model_execution.go b/pkg/api/v1/testkube/model_execution.go index 3eadfb45c45..91c2ad22c5d 100644 --- a/pkg/api/v1/testkube/model_execution.go +++ b/pkg/api/v1/testkube/model_execution.go @@ -71,4 +71,6 @@ type Execution struct { RunningContext *RunningContext `json:"runningContext,omitempty"` // shell used in container executor ContainerShell string `json:"containerShell,omitempty"` + // test execution name started the test execution + TestExecutionName string `json:"testExecutionName,omitempty"` } diff --git a/pkg/api/v1/testkube/model_execution_request.go b/pkg/api/v1/testkube/model_execution_request.go index 4240935bee8..42f90c64806 100644 --- a/pkg/api/v1/testkube/model_execution_request.go +++ b/pkg/api/v1/testkube/model_execution_request.go @@ -81,4 +81,6 @@ type ExecutionRequest struct { // secret references EnvSecrets []EnvReference `json:"envSecrets,omitempty"` RunningContext *RunningContext `json:"runningContext,omitempty"` + // test execution name started the test execution + TestExecutionName string `json:"testExecutionName,omitempty"` } diff --git a/pkg/api/v1/testkube/model_execution_update_request.go b/pkg/api/v1/testkube/model_execution_update_request.go index 7160c5c6828..be509827387 100644 --- a/pkg/api/v1/testkube/model_execution_update_request.go +++ b/pkg/api/v1/testkube/model_execution_update_request.go @@ -81,4 +81,6 @@ type ExecutionUpdateRequest struct { // secret references EnvSecrets *[]EnvReference `json:"envSecrets,omitempty"` RunningContext *RunningContext `json:"runningContext,omitempty"` + // test execution name started the test execution + TestExecutionName *string `json:"testExecutionName,omitempty"` } diff --git a/pkg/api/v1/testkube/model_running_context_extended.go b/pkg/api/v1/testkube/model_running_context_extended.go index 847e8b7573a..b1b4db2a0ae 100644 --- a/pkg/api/v1/testkube/model_running_context_extended.go +++ b/pkg/api/v1/testkube/model_running_context_extended.go @@ -3,10 +3,12 @@ package testkube type RunningContextType string const ( - RunningContextTypeUserCLI RunningContextType = "user-cli" - RunningContextTypeUserUI RunningContextType = "user-ui" - RunningContextTypeTestSuite RunningContextType = "testsuite" - RunningContextTypeTestTrigger RunningContextType = "testtrigger" - RunningContextTypeScheduler RunningContextType = "scheduler" - RunningContextTypeEmpty RunningContextType = "" + RunningContextTypeUserCLI RunningContextType = "user-cli" + RunningContextTypeUserUI RunningContextType = "user-ui" + RunningContextTypeTestSuite RunningContextType = "testsuite" + RunningContextTypeTestTrigger RunningContextType = "testtrigger" + RunningContextTypeScheduler RunningContextType = "scheduler" + RunningContextTypeTestExecution RunningContextType = "testexecution" + RunningContextTypeTestSuiteExecution RunningContextType = "testsuiteexecution" + RunningContextTypeEmpty RunningContextType = "" ) diff --git a/pkg/api/v1/testkube/model_test_execution_cr.go b/pkg/api/v1/testkube/model_test_execution_cr.go new file mode 100644 index 00000000000..f81670956c8 --- /dev/null +++ b/pkg/api/v1/testkube/model_test_execution_cr.go @@ -0,0 +1,16 @@ +/* + * Testkube API + * + * Testkube provides a Kubernetes-native framework for test definition, execution and results + * + * API version: 1.0.0 + * Contact: testkube@kubeshop.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ +package testkube + +type TestExecutionCr struct { + Test *ObjectRef `json:"test"` + ExecutionRequest *ExecutionRequest `json:"executionRequest,omitempty"` + Status *TestExecutionStatusCr `json:"status,omitempty"` +} diff --git a/pkg/api/v1/testkube/model_test_execution_status_cr.go b/pkg/api/v1/testkube/model_test_execution_status_cr.go new file mode 100644 index 00000000000..3b342143c82 --- /dev/null +++ b/pkg/api/v1/testkube/model_test_execution_status_cr.go @@ -0,0 +1,17 @@ +/* + * Testkube API + * + * Testkube provides a Kubernetes-native framework for test definition, execution and results + * + * API version: 1.0.0 + * Contact: testkube@kubeshop.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ +package testkube + +// test execution status +type TestExecutionStatusCr struct { + LatestExecution *Execution `json:"latestExecution,omitempty"` + // test execution generation + Generation int64 `json:"generation,omitempty"` +} diff --git a/pkg/api/v1/testkube/model_test_suite_execution.go b/pkg/api/v1/testkube/model_test_suite_execution.go index acf097d9216..e9e314b0463 100644 --- a/pkg/api/v1/testkube/model_test_suite_execution.go +++ b/pkg/api/v1/testkube/model_test_suite_execution.go @@ -42,4 +42,6 @@ type TestSuiteExecution struct { // test suite labels Labels map[string]string `json:"labels,omitempty"` RunningContext *RunningContext `json:"runningContext,omitempty"` + // test suite execution name started the test suite execution + TestSuiteExecutionName string `json:"testSuiteExecutionName,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_suite_execution_cr.go b/pkg/api/v1/testkube/model_test_suite_execution_cr.go new file mode 100644 index 00000000000..1220bc3e374 --- /dev/null +++ b/pkg/api/v1/testkube/model_test_suite_execution_cr.go @@ -0,0 +1,16 @@ +/* + * Testkube API + * + * Testkube provides a Kubernetes-native framework for test definition, execution and results + * + * API version: 1.0.0 + * Contact: testkube@kubeshop.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ +package testkube + +type TestSuiteExecutionCr struct { + TestSuite *ObjectRef `json:"testSuite"` + ExecutionRequest *TestSuiteExecutionRequest `json:"executionRequest,omitempty"` + Status *TestSuiteExecutionStatusCr `json:"status,omitempty"` +} diff --git a/pkg/api/v1/testkube/model_test_suite_execution_extended.go b/pkg/api/v1/testkube/model_test_suite_execution_extended.go index a17bb2c69ce..6ed2ab60b3f 100644 --- a/pkg/api/v1/testkube/model_test_suite_execution_extended.go +++ b/pkg/api/v1/testkube/model_test_suite_execution_extended.go @@ -24,15 +24,16 @@ func NewQueuedTestSuiteExecution(name, namespace string) *TestSuiteExecution { func NewStartedTestSuiteExecution(testSuite TestSuite, request TestSuiteExecutionRequest) TestSuiteExecution { testExecution := TestSuiteExecution{ - Id: primitive.NewObjectID().Hex(), - StartTime: time.Now(), - Name: request.Name, - Status: TestSuiteExecutionStatusRunning, - SecretUUID: request.SecretUUID, - TestSuite: testSuite.GetObjectRef(), - Labels: common.MergeMaps(testSuite.Labels, request.ExecutionLabels), - Variables: map[string]Variable{}, - RunningContext: request.RunningContext, + Id: primitive.NewObjectID().Hex(), + StartTime: time.Now(), + Name: request.Name, + Status: TestSuiteExecutionStatusRunning, + SecretUUID: request.SecretUUID, + TestSuite: testSuite.GetObjectRef(), + Labels: common.MergeMaps(testSuite.Labels, request.ExecutionLabels), + Variables: map[string]Variable{}, + RunningContext: request.RunningContext, + TestSuiteExecutionName: request.TestSuiteExecutionName, } if testSuite.ExecutionRequest != nil { diff --git a/pkg/api/v1/testkube/model_test_suite_execution_request.go b/pkg/api/v1/testkube/model_test_suite_execution_request.go index ff81002ee93..fb732540da6 100644 --- a/pkg/api/v1/testkube/model_test_suite_execution_request.go +++ b/pkg/api/v1/testkube/model_test_suite_execution_request.go @@ -38,4 +38,6 @@ type TestSuiteExecutionRequest struct { CronJobTemplate string `json:"cronJobTemplate,omitempty"` // number of tests run in parallel ConcurrencyLevel int32 `json:"concurrencyLevel,omitempty"` + // test suite execution name started the test suite execution + TestSuiteExecutionName string `json:"testSuiteExecutionName,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_suite_execution_status_cr.go b/pkg/api/v1/testkube/model_test_suite_execution_status_cr.go new file mode 100644 index 00000000000..d201c68d426 --- /dev/null +++ b/pkg/api/v1/testkube/model_test_suite_execution_status_cr.go @@ -0,0 +1,17 @@ +/* + * Testkube API + * + * Testkube provides a Kubernetes-native framework for test definition, execution and results + * + * API version: 1.0.0 + * Contact: testkube@kubeshop.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ +package testkube + +// test suite execution status +type TestSuiteExecutionStatusCr struct { + LatestExecution *TestSuiteExecution `json:"latestExecution,omitempty"` + // test suite execution generation + Generation int64 `json:"generation,omitempty"` +} diff --git a/pkg/api/v1/testkube/model_test_suite_execution_update_request.go b/pkg/api/v1/testkube/model_test_suite_execution_update_request.go index a731bb07ede..82873ef584e 100644 --- a/pkg/api/v1/testkube/model_test_suite_execution_update_request.go +++ b/pkg/api/v1/testkube/model_test_suite_execution_update_request.go @@ -38,4 +38,6 @@ type TestSuiteExecutionUpdateRequest struct { CronJobTemplate *string `json:"cronJobTemplate,omitempty"` // number of tests run in parallel ConcurrencyLevel *int32 `json:"concurrencyLevel,omitempty"` + // test suite execution name started the test suite execution + TestSuiteExecutionName *string `json:"testSuiteExecutionName,omitempty"` } diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go index b316a79d813..0036eb81e05 100644 --- a/pkg/executor/client/job.go +++ b/pkg/executor/client/job.go @@ -30,6 +30,7 @@ import ( kyaml "sigs.k8s.io/kustomize/kyaml/yaml" + testexecutionsv1 "github.com/kubeshop/testkube-operator/client/testexecutions/v1" testsv3 "github.com/kubeshop/testkube-operator/client/tests/v3" "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/event" @@ -37,6 +38,7 @@ import ( "github.com/kubeshop/testkube/pkg/executor/env" "github.com/kubeshop/testkube/pkg/executor/output" "github.com/kubeshop/testkube/pkg/log" + testexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testexecutions" testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests" "github.com/kubeshop/testkube/pkg/telemetry" "github.com/kubeshop/testkube/pkg/utils" @@ -76,25 +78,27 @@ func NewJobExecutor( configMap config.Repository, testsClient testsv3.Interface, clientset kubernetes.Interface, + testExecutionsClient testexecutionsv1.Interface, registry string, podStartTimeout time.Duration, clusterID string, ) (client *JobExecutor, err error) { return &JobExecutor{ - ClientSet: clientset, - Repository: repo, - Log: log.DefaultLogger, - Namespace: namespace, - images: images, - jobTemplate: jobTemplate, - serviceAccountName: serviceAccountName, - metrics: metrics, - Emitter: emiter, - configMap: configMap, - testsClient: testsClient, - registry: registry, - podStartTimeout: podStartTimeout, - clusterID: clusterID, + ClientSet: clientset, + Repository: repo, + Log: log.DefaultLogger, + Namespace: namespace, + images: images, + jobTemplate: jobTemplate, + serviceAccountName: serviceAccountName, + metrics: metrics, + Emitter: emiter, + configMap: configMap, + testsClient: testsClient, + testExecutionsClient: testExecutionsClient, + registry: registry, + podStartTimeout: podStartTimeout, + clusterID: clusterID, }, nil } @@ -104,21 +108,22 @@ type ExecutionCounter interface { // JobExecutor is container for managing job executor dependencies type JobExecutor struct { - Repository result.Repository - Log *zap.SugaredLogger - ClientSet kubernetes.Interface - Namespace string - Cmd string - images executor.Images - jobTemplate string - serviceAccountName string - metrics ExecutionCounter - Emitter *event.Emitter - configMap config.Repository - testsClient testsv3.Interface - registry string - podStartTimeout time.Duration - clusterID string + Repository result.Repository + Log *zap.SugaredLogger + ClientSet kubernetes.Interface + Namespace string + Cmd string + images executor.Images + jobTemplate string + serviceAccountName string + metrics ExecutionCounter + Emitter *event.Emitter + configMap config.Repository + testsClient testsv3.Interface + testExecutionsClient testexecutionsv1.Interface + registry string + podStartTimeout time.Duration + clusterID string } type JobOptions struct { @@ -419,10 +424,22 @@ func (c *JobExecutor) stopExecution(ctx context.Context, l *zap.SugaredLogger, e return err } - if test != nil { - test.Status = testsmapper.MapExecutionToTestStatus(execution) - if err = c.testsClient.UpdateStatus(test); err != nil { - l.Errorw("updating test error", "error", err) + test.Status = testsmapper.MapExecutionToTestStatus(execution) + if err = c.testsClient.UpdateStatus(test); err != nil { + l.Errorw("updating test error", "error", err) + return err + } + + if execution.TestExecutionName != "" { + testExecution, err := c.testExecutionsClient.Get(execution.TestExecutionName) + if err != nil { + l.Errorw("getting test execution error", "error", err) + return err + } + + testExecution.Status = testexecutionsmapper.MapAPIToCRD(execution, testExecution.Generation) + if err = c.testExecutionsClient.UpdateStatus(testExecution); err != nil { + l.Errorw("updating test execution error", "error", err) return err } } diff --git a/pkg/executor/containerexecutor/containerexecutor.go b/pkg/executor/containerexecutor/containerexecutor.go index eae1fe599fe..5b6c3e15b50 100644 --- a/pkg/executor/containerexecutor/containerexecutor.go +++ b/pkg/executor/containerexecutor/containerexecutor.go @@ -21,6 +21,7 @@ import ( executorv1 "github.com/kubeshop/testkube-operator/apis/executor/v1" executorsclientv1 "github.com/kubeshop/testkube-operator/client/executors/v1" + testexecutionsv1 "github.com/kubeshop/testkube-operator/client/testexecutions/v1" testsv3 "github.com/kubeshop/testkube-operator/client/tests/v3" "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/executor" @@ -28,6 +29,7 @@ import ( "github.com/kubeshop/testkube/pkg/executor/output" "github.com/kubeshop/testkube/pkg/k8sclient" "github.com/kubeshop/testkube/pkg/log" + testexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testexecutions" testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests" "github.com/kubeshop/testkube/pkg/telemetry" ) @@ -58,6 +60,7 @@ func NewContainerExecutor( configMap config.Repository, executorsClient executorsclientv1.Interface, testsClient testsv3.Interface, + testExecutionsClient testexecutionsv1.Interface, registry string, podStartTimeout time.Duration, clusterID string, @@ -68,21 +71,22 @@ func NewContainerExecutor( } return &ContainerExecutor{ - clientSet: clientSet, - repository: repo, - log: log.DefaultLogger, - namespace: namespace, - images: images, - templates: templates, - configMap: configMap, - serviceAccountName: serviceAccountName, - metrics: metrics, - emitter: emiter, - testsClient: testsClient, - executorsClient: executorsClient, - registry: registry, - podStartTimeout: podStartTimeout, - clusterID: clusterID, + clientSet: clientSet, + repository: repo, + log: log.DefaultLogger, + namespace: namespace, + images: images, + templates: templates, + configMap: configMap, + serviceAccountName: serviceAccountName, + metrics: metrics, + emitter: emiter, + testsClient: testsClient, + executorsClient: executorsClient, + testExecutionsClient: testExecutionsClient, + registry: registry, + podStartTimeout: podStartTimeout, + clusterID: clusterID, }, nil } @@ -92,21 +96,22 @@ type ExecutionCounter interface { // ContainerExecutor is container for managing job executor dependencies type ContainerExecutor struct { - repository result.Repository - log *zap.SugaredLogger - clientSet kubernetes.Interface - namespace string - images executor.Images - templates executor.Templates - metrics ExecutionCounter - emitter EventEmitter - configMap config.Repository - serviceAccountName string - testsClient testsv3.Interface - executorsClient executorsclientv1.Interface - registry string - podStartTimeout time.Duration - clusterID string + repository result.Repository + log *zap.SugaredLogger + clientSet kubernetes.Interface + namespace string + images executor.Images + templates executor.Templates + metrics ExecutionCounter + emitter EventEmitter + configMap config.Repository + serviceAccountName string + testsClient testsv3.Interface + executorsClient executorsclientv1.Interface + testExecutionsClient testexecutionsv1.Interface + registry string + podStartTimeout time.Duration + clusterID string } type JobOptions struct { @@ -480,6 +485,20 @@ func (c *ContainerExecutor) stopExecution(ctx context.Context, execution *testku } } + if execution.TestExecutionName != "" { + testExecution, err := c.testExecutionsClient.Get(execution.TestExecutionName) + if err != nil { + c.log.Errorw("getting test execution error", "error", err) + } + + if testExecution != nil { + testExecution.Status = testexecutionsmapper.MapAPIToCRD(execution, testExecution.Generation) + if err = c.testExecutionsClient.UpdateStatus(testExecution); err != nil { + c.log.Errorw("updating test execution error", "error", err) + } + } + } + if result.IsPassed() { c.emitter.Notify(testkube.NewEventEndTestSuccess(execution)) } else if result.IsTimeout() { diff --git a/pkg/mapper/testexecutions/mapper.go b/pkg/mapper/testexecutions/mapper.go new file mode 100644 index 00000000000..cb53027163b --- /dev/null +++ b/pkg/mapper/testexecutions/mapper.go @@ -0,0 +1,190 @@ +package testexecutions + +import ( + corev1 "k8s.io/api/core/v1" + + testexecutionv1 "github.com/kubeshop/testkube-operator/apis/testexecution/v1" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +// MapCRDVariables maps variables between API and operator CRDs +func MapCRDVariables(in map[string]testkube.Variable) map[string]testexecutionv1.Variable { + out := map[string]testexecutionv1.Variable{} + for k, v := range in { + variable := testexecutionv1.Variable{ + Name: v.Name, + Type_: string(*v.Type_), + Value: v.Value, + } + + if v.SecretRef != nil { + variable.ValueFrom = corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: v.SecretRef.Name, + }, + Key: v.SecretRef.Key, + }, + } + } + + if v.ConfigMapRef != nil { + variable.ValueFrom = corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: v.ConfigMapRef.Name, + }, + Key: v.ConfigMapRef.Key, + }, + } + } + + out[k] = variable + } + return out +} + +// MapContentToSpecContent maps TestContent OpenAPI spec to TestContent CRD spec +func MapContentToSpecContent(content *testkube.TestContent) (specContent *testexecutionv1.TestContent) { + if content == nil { + return + } + + var repository *testexecutionv1.Repository + if content.Repository != nil { + repository = &testexecutionv1.Repository{ + Type_: content.Repository.Type_, + Uri: content.Repository.Uri, + Branch: content.Repository.Branch, + Commit: content.Repository.Commit, + Path: content.Repository.Path, + WorkingDir: content.Repository.WorkingDir, + CertificateSecret: content.Repository.CertificateSecret, + AuthType: testexecutionv1.GitAuthType(content.Repository.AuthType), + } + + if content.Repository.UsernameSecret != nil { + repository.UsernameSecret = &testexecutionv1.SecretRef{ + Name: content.Repository.UsernameSecret.Name, + Key: content.Repository.UsernameSecret.Key, + } + } + + if content.Repository.TokenSecret != nil { + repository.TokenSecret = &testexecutionv1.SecretRef{ + Name: content.Repository.TokenSecret.Name, + Key: content.Repository.TokenSecret.Key, + } + } + } + + return &testexecutionv1.TestContent{ + Repository: repository, + Data: content.Data, + Uri: content.Uri, + Type_: testexecutionv1.TestContentType(content.Type_), + } +} + +// MapExecutionResultToCRD maps OpenAPI spec ExecutionResult to CRD ExecutionResult +func MapExecutionResultToCRD(result *testkube.ExecutionResult) *testexecutionv1.ExecutionResult { + if result == nil { + return nil + } + + var status *testexecutionv1.ExecutionStatus + if result.Status != nil { + value := testexecutionv1.ExecutionStatus(*result.Status) + status = &value + } + + var steps []testexecutionv1.ExecutionStepResult + for _, step := range result.Steps { + var asserstions []testexecutionv1.AssertionResult + for _, asserstion := range step.AssertionResults { + asserstions = append(asserstions, testexecutionv1.AssertionResult{ + Name: asserstion.Name, + Status: asserstion.Status, + ErrorMessage: asserstion.ErrorMessage, + }) + } + + steps = append(steps, testexecutionv1.ExecutionStepResult{ + Name: step.Name, + Duration: step.Duration, + Status: step.Status, + AssertionResults: asserstions, + }) + } + + var reports *testexecutionv1.ExecutionResultReports + if result.Reports != nil { + reports = &testexecutionv1.ExecutionResultReports{ + Junit: result.Reports.Junit, + } + } + + return &testexecutionv1.ExecutionResult{ + Status: status, + ErrorMessage: result.ErrorMessage, + Steps: steps, + Reports: reports, + } +} + +// MapAPIToCRD maps OpenAPI spec Execution to CRD TestExecutionStatus +func MapAPIToCRD(request *testkube.Execution, generation int64) testexecutionv1.TestExecutionStatus { + var artifactRequest *testexecutionv1.ArtifactRequest + if request.ArtifactRequest != nil { + artifactRequest = &testexecutionv1.ArtifactRequest{ + StorageClassName: request.ArtifactRequest.StorageClassName, + VolumeMountPath: request.ArtifactRequest.VolumeMountPath, + Dirs: request.ArtifactRequest.Dirs, + } + } + + var runningContext *testexecutionv1.RunningContext + if request.RunningContext != nil { + runningContext = &testexecutionv1.RunningContext{ + Type_: testexecutionv1.RunningContextType(request.RunningContext.Type_), + Context: request.RunningContext.Context, + } + } + + result := testexecutionv1.TestExecutionStatus{ + Generation: generation, + LatestExecution: &testexecutionv1.Execution{ + Id: request.Id, + TestName: request.TestName, + TestSuiteName: request.TestSuiteName, + TestNamespace: request.TestNamespace, + TestType: request.TestType, + Name: request.Name, + Number: request.Number, + Envs: request.Envs, + Command: request.Command, + Args: request.Args, + ArgsMode: testexecutionv1.ArgsModeType(request.ArgsMode), + Variables: MapCRDVariables(request.Variables), + IsVariablesFileUploaded: request.IsVariablesFileUploaded, + VariablesFile: request.VariablesFile, + TestSecretUUID: request.TestSecretUUID, + Content: MapContentToSpecContent(request.Content), + Duration: request.Duration, + DurationMs: request.DurationMs, + ExecutionResult: MapExecutionResultToCRD(request.ExecutionResult), + Labels: request.Labels, + Uploads: request.Uploads, + BucketName: request.BucketName, + ArtifactRequest: artifactRequest, + PreRunScript: request.PreRunScript, + PostRunScript: request.PostRunScript, + RunningContext: runningContext, + ContainerShell: request.ContainerShell, + }, + } + + result.LatestExecution.StartTime.Time = request.StartTime + result.LatestExecution.EndTime.Time = request.EndTime + return result +} diff --git a/pkg/mapper/testsuiteexecutions/mapper.go b/pkg/mapper/testsuiteexecutions/mapper.go new file mode 100644 index 00000000000..094ed012a5b --- /dev/null +++ b/pkg/mapper/testsuiteexecutions/mapper.go @@ -0,0 +1,336 @@ +package testsuiteexecutions + +import ( + corev1 "k8s.io/api/core/v1" + + testsuiteexecutionv1 "github.com/kubeshop/testkube-operator/apis/testsuiteexecution/v1" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +// MapCRDVariables maps variables between API and operator CRDs +func MapCRDVariables(in map[string]testkube.Variable) map[string]testsuiteexecutionv1.Variable { + out := map[string]testsuiteexecutionv1.Variable{} + for k, v := range in { + variable := testsuiteexecutionv1.Variable{ + Name: v.Name, + Type_: string(*v.Type_), + Value: v.Value, + } + + if v.SecretRef != nil { + variable.ValueFrom = corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: v.SecretRef.Name, + }, + Key: v.SecretRef.Key, + }, + } + } + + if v.ConfigMapRef != nil { + variable.ValueFrom = corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: v.ConfigMapRef.Name, + }, + Key: v.ConfigMapRef.Key, + }, + } + } + + out[k] = variable + } + return out +} + +// MapContentToSpecContent maps TestContent OpenAPI spec to TestContent CRD spec +func MapContentToSpecContent(content *testkube.TestContent) (specContent *testsuiteexecutionv1.TestContent) { + if content == nil { + return + } + + var repository *testsuiteexecutionv1.Repository + if content.Repository != nil { + repository = &testsuiteexecutionv1.Repository{ + Type_: content.Repository.Type_, + Uri: content.Repository.Uri, + Branch: content.Repository.Branch, + Commit: content.Repository.Commit, + Path: content.Repository.Path, + WorkingDir: content.Repository.WorkingDir, + CertificateSecret: content.Repository.CertificateSecret, + AuthType: testsuiteexecutionv1.GitAuthType(content.Repository.AuthType), + } + + if content.Repository.UsernameSecret != nil { + repository.UsernameSecret = &testsuiteexecutionv1.SecretRef{ + Name: content.Repository.UsernameSecret.Name, + Key: content.Repository.UsernameSecret.Key, + } + } + + if content.Repository.TokenSecret != nil { + repository.TokenSecret = &testsuiteexecutionv1.SecretRef{ + Name: content.Repository.TokenSecret.Name, + Key: content.Repository.TokenSecret.Key, + } + } + } + + return &testsuiteexecutionv1.TestContent{ + Repository: repository, + Data: content.Data, + Uri: content.Uri, + Type_: testsuiteexecutionv1.TestContentType(content.Type_), + } +} + +// MapExecutionResultToCRD maps OpenAPI spec ExecutionResult to CRD ExecutionResult +func MapExecutionResultToCRD(result *testkube.ExecutionResult) *testsuiteexecutionv1.ExecutionResult { + if result == nil { + return nil + } + + var status *testsuiteexecutionv1.ExecutionStatus + if result.Status != nil { + value := testsuiteexecutionv1.ExecutionStatus(*result.Status) + status = &value + } + + var steps []testsuiteexecutionv1.ExecutionStepResult + for _, step := range result.Steps { + var asserstions []testsuiteexecutionv1.AssertionResult + for _, asserstion := range step.AssertionResults { + asserstions = append(asserstions, testsuiteexecutionv1.AssertionResult{ + Name: asserstion.Name, + Status: asserstion.Status, + ErrorMessage: asserstion.ErrorMessage, + }) + } + + steps = append(steps, testsuiteexecutionv1.ExecutionStepResult{ + Name: step.Name, + Duration: step.Duration, + Status: step.Status, + AssertionResults: asserstions, + }) + } + + var reports *testsuiteexecutionv1.ExecutionResultReports + if result.Reports != nil { + reports = &testsuiteexecutionv1.ExecutionResultReports{ + Junit: result.Reports.Junit, + } + } + + return &testsuiteexecutionv1.ExecutionResult{ + Status: status, + ErrorMessage: result.ErrorMessage, + Steps: steps, + Reports: reports, + } +} + +// MapExecutionCRD maps OpenAPI spec Execution to CRD +func MapExecutionCRD(request *testkube.Execution) *testsuiteexecutionv1.Execution { + if request == nil { + return nil + } + + var artifactRequest *testsuiteexecutionv1.ArtifactRequest + if request.ArtifactRequest != nil { + artifactRequest = &testsuiteexecutionv1.ArtifactRequest{ + StorageClassName: request.ArtifactRequest.StorageClassName, + VolumeMountPath: request.ArtifactRequest.VolumeMountPath, + Dirs: request.ArtifactRequest.Dirs, + } + } + + var runningContext *testsuiteexecutionv1.RunningContext + if request.RunningContext != nil { + runningContext = &testsuiteexecutionv1.RunningContext{ + Type_: testsuiteexecutionv1.RunningContextType(request.RunningContext.Type_), + Context: request.RunningContext.Context, + } + } + + result := &testsuiteexecutionv1.Execution{ + Id: request.Id, + TestName: request.TestName, + TestSuiteName: request.TestSuiteName, + TestNamespace: request.TestNamespace, + TestType: request.TestType, + Name: request.Name, + Number: request.Number, + Envs: request.Envs, + Command: request.Command, + Args: request.Args, + ArgsMode: testsuiteexecutionv1.ArgsModeType(request.ArgsMode), + Variables: MapCRDVariables(request.Variables), + IsVariablesFileUploaded: request.IsVariablesFileUploaded, + VariablesFile: request.VariablesFile, + TestSecretUUID: request.TestSecretUUID, + Content: MapContentToSpecContent(request.Content), + Duration: request.Duration, + DurationMs: request.DurationMs, + ExecutionResult: MapExecutionResultToCRD(request.ExecutionResult), + Labels: request.Labels, + Uploads: request.Uploads, + BucketName: request.BucketName, + ArtifactRequest: artifactRequest, + PreRunScript: request.PreRunScript, + PostRunScript: request.PostRunScript, + RunningContext: runningContext, + ContainerShell: request.ContainerShell, + } + + result.StartTime.Time = request.StartTime + result.EndTime.Time = request.EndTime + return result +} + +func MapTestSuiteStepV2ToCRD(request *testkube.TestSuiteStepV2) *testsuiteexecutionv1.TestSuiteStepV2 { + if request == nil { + return nil + } + + var execute *testsuiteexecutionv1.TestSuiteStepExecuteTestV2 + var delay *testsuiteexecutionv1.TestSuiteStepDelayV2 + + if request.Execute != nil { + execute = &testsuiteexecutionv1.TestSuiteStepExecuteTestV2{ + Name: request.Execute.Name, + Namespace: request.Execute.Namespace, + } + } + + if request.Delay != nil { + delay = &testsuiteexecutionv1.TestSuiteStepDelayV2{ + Duration: request.Delay.Duration, + } + } + + return &testsuiteexecutionv1.TestSuiteStepV2{ + StopTestOnFailure: request.StopTestOnFailure, + Execute: execute, + Delay: delay, + } +} + +func MapTestSuiteBatchStepToCRD(request *testkube.TestSuiteBatchStep) *testsuiteexecutionv1.TestSuiteBatchStep { + if request == nil { + return nil + } + + var steps []testsuiteexecutionv1.TestSuiteStep + for _, step := range request.Execute { + steps = append(steps, testsuiteexecutionv1.TestSuiteStep{ + Test: step.Test, + Delay: step.Delay, + }) + } + + return &testsuiteexecutionv1.TestSuiteBatchStep{ + StopOnFailure: request.StopOnFailure, + Execute: steps, + } +} + +// MapAPIToCRD maps OpenAPI spec Execution to CRD TestSuiteExecutionStatus +func MapAPIToCRD(request *testkube.TestSuiteExecution, generation int64) testsuiteexecutionv1.TestSuiteExecutionStatus { + var testSuite *testsuiteexecutionv1.ObjectRef + if request.TestSuite != nil { + testSuite = &testsuiteexecutionv1.ObjectRef{ + Name: request.TestSuite.Name, + Namespace: request.TestSuite.Namespace, + } + } + + var status *testsuiteexecutionv1.SuiteExecutionStatus + if request.Status != nil { + value := testsuiteexecutionv1.SuiteExecutionStatus(*request.Status) + status = &value + } + + var runningContext *testsuiteexecutionv1.RunningContext + if request.RunningContext != nil { + runningContext = &testsuiteexecutionv1.RunningContext{ + Type_: testsuiteexecutionv1.RunningContextType(request.RunningContext.Type_), + Context: request.RunningContext.Context, + } + } + + var stepResults []testsuiteexecutionv1.TestSuiteStepExecutionResultV2 + for _, stepResult := range request.StepResults { + var test *testsuiteexecutionv1.ObjectRef + if stepResult.Test != nil { + test = &testsuiteexecutionv1.ObjectRef{ + Name: stepResult.Test.Name, + Namespace: stepResult.Test.Namespace, + } + } + + stepResults = append(stepResults, testsuiteexecutionv1.TestSuiteStepExecutionResultV2{ + Step: MapTestSuiteStepV2ToCRD(stepResult.Step), + Test: test, + Execution: MapExecutionCRD(stepResult.Execution), + }) + } + + var executeStepResults []testsuiteexecutionv1.TestSuiteBatchStepExecutionResult + for _, stepResult := range request.ExecuteStepResults { + var steps []testsuiteexecutionv1.TestSuiteStepExecutionResult + for _, step := range stepResult.Execute { + var testSuiteStep *testsuiteexecutionv1.TestSuiteStep + if step.Step != nil { + testSuiteStep = &testsuiteexecutionv1.TestSuiteStep{ + Test: step.Step.Test, + Delay: step.Step.Delay, + } + } + + var test *testsuiteexecutionv1.ObjectRef + if step.Test != nil { + test = &testsuiteexecutionv1.ObjectRef{ + Name: step.Test.Name, + Namespace: step.Test.Namespace, + } + } + + steps = append(steps, testsuiteexecutionv1.TestSuiteStepExecutionResult{ + Step: testSuiteStep, + Test: test, + Execution: MapExecutionCRD(step.Execution), + }) + } + + executeStepResults = append(executeStepResults, testsuiteexecutionv1.TestSuiteBatchStepExecutionResult{ + Step: MapTestSuiteBatchStepToCRD(stepResult.Step), + Execute: steps, + }) + } + + result := testsuiteexecutionv1.TestSuiteExecutionStatus{ + Generation: generation, + LatestExecution: &testsuiteexecutionv1.SuiteExecution{ + Id: request.Id, + Name: request.Name, + TestSuite: testSuite, + Status: status, + Envs: request.Envs, + Variables: MapCRDVariables(request.Variables), + SecretUUID: request.SecretUUID, + Duration: request.Duration, + DurationMs: request.DurationMs, + StepResults: stepResults, + ExecuteStepResults: executeStepResults, + Labels: request.Labels, + RunningContext: runningContext, + }, + } + + result.LatestExecution.StartTime.Time = request.StartTime + result.LatestExecution.EndTime.Time = request.EndTime + return result +} diff --git a/pkg/scheduler/service.go b/pkg/scheduler/service.go index 7d0408ac5a2..2952a8b654c 100644 --- a/pkg/scheduler/service.go +++ b/pkg/scheduler/service.go @@ -8,6 +8,7 @@ import ( executorsv1 "github.com/kubeshop/testkube-operator/client/executors/v1" testsv3 "github.com/kubeshop/testkube-operator/client/tests/v3" testsourcesv1 "github.com/kubeshop/testkube-operator/client/testsources/v1" + testsuiteexecutionsclientv1 "github.com/kubeshop/testkube-operator/client/testsuiteexecutions/v1" testsuitesv3 "github.com/kubeshop/testkube-operator/client/testsuites/v3" v1 "github.com/kubeshop/testkube/internal/app/api/metrics" "github.com/kubeshop/testkube/pkg/configmap" @@ -19,20 +20,21 @@ import ( ) type Scheduler struct { - metrics v1.Metrics - executor client.Executor - containerExecutor client.Executor - executionResults result.Repository - testExecutionResults testresult.Repository - executorsClient executorsv1.Interface - testsClient testsv3.Interface - testSuitesClient testsuitesv3.Interface - testSourcesClient testsourcesv1.Interface - secretClient secret.Interface - events *event.Emitter - logger *zap.SugaredLogger - configMap config.Repository - configMapClient configmap.Interface + metrics v1.Metrics + executor client.Executor + containerExecutor client.Executor + executionResults result.Repository + testExecutionResults testresult.Repository + executorsClient executorsv1.Interface + testsClient testsv3.Interface + testSuitesClient testsuitesv3.Interface + testSourcesClient testsourcesv1.Interface + secretClient secret.Interface + events *event.Emitter + logger *zap.SugaredLogger + configMap config.Repository + configMapClient configmap.Interface + testSuiteExecutionsClient testsuiteexecutionsclientv1.Interface } func NewScheduler( @@ -50,21 +52,23 @@ func NewScheduler( logger *zap.SugaredLogger, configMap config.Repository, configMapClient configmap.Interface, + testSuiteExecutionsClient testsuiteexecutionsclientv1.Interface, ) *Scheduler { return &Scheduler{ - metrics: metrics, - executor: executor, - containerExecutor: containerExecutor, - secretClient: secretClient, - executionResults: executionResults, - testExecutionResults: testExecutionResults, - executorsClient: executorsClient, - testsClient: testsClient, - testSuitesClient: testSuitesClient, - testSourcesClient: testSourcesClient, - events: events, - logger: logger, - configMap: configMap, - configMapClient: configMapClient, + metrics: metrics, + executor: executor, + containerExecutor: containerExecutor, + secretClient: secretClient, + executionResults: executionResults, + testExecutionResults: testExecutionResults, + executorsClient: executorsClient, + testsClient: testsClient, + testSuitesClient: testSuitesClient, + testSourcesClient: testSourcesClient, + events: events, + logger: logger, + configMap: configMap, + configMapClient: configMapClient, + testSuiteExecutionsClient: testSuiteExecutionsClient, } } diff --git a/pkg/scheduler/test_scheduler.go b/pkg/scheduler/test_scheduler.go index 95b33532248..1054a16c3bb 100644 --- a/pkg/scheduler/test_scheduler.go +++ b/pkg/scheduler/test_scheduler.go @@ -229,6 +229,7 @@ func newExecutionFromExecutionOptions(options client.ExecuteOptions) testkube.Ex execution.PreRunScript = options.Request.PreRunScript execution.PostRunScript = options.Request.PostRunScript execution.RunningContext = options.Request.RunningContext + execution.TestExecutionName = options.Request.TestExecutionName return execution } diff --git a/pkg/scheduler/test_scheduler_test.go b/pkg/scheduler/test_scheduler_test.go index cb8f38e6190..5332c90b237 100644 --- a/pkg/scheduler/test_scheduler_test.go +++ b/pkg/scheduler/test_scheduler_test.go @@ -164,6 +164,7 @@ func TestGetExecuteOptions(t *testing.T) { RunningContext: &testkube.RunningContext{ Type_: string(testkube.RunningContextTypeUserCLI), }, + TestExecutionName: "", } got, err := sc.getExecuteOptions("namespace", "id", req) diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go index 00a4f13459e..9edbf55afa7 100644 --- a/pkg/scheduler/testsuite_scheduler.go +++ b/pkg/scheduler/testsuite_scheduler.go @@ -11,6 +11,7 @@ import ( testsuitesv3 "github.com/kubeshop/testkube-operator/apis/testsuite/v3" "github.com/kubeshop/testkube/pkg/api/v1/testkube" + testsuiteexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testsuiteexecutions" testsuitesmapper "github.com/kubeshop/testkube/pkg/mapper/testsuites" "github.com/kubeshop/testkube/pkg/telemetry" "github.com/kubeshop/testkube/pkg/version" @@ -209,26 +210,40 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE } } -func (s *Scheduler) runAfterEachStep(ctx context.Context, testsuiteExecution *testkube.TestSuiteExecution, wg *sync.WaitGroup) { - testsuiteExecution.Stop() - err := s.testExecutionResults.EndExecution(ctx, *testsuiteExecution) +func (s *Scheduler) runAfterEachStep(ctx context.Context, execution *testkube.TestSuiteExecution, wg *sync.WaitGroup) { + execution.Stop() + err := s.testExecutionResults.EndExecution(ctx, *execution) if err != nil { s.logger.Errorw("error setting end time", "error", err.Error()) } wg.Done() - if testsuiteExecution.TestSuite != nil { - testSuite, err := s.testSuitesClient.Get(testsuiteExecution.TestSuite.Name) + if execution.TestSuite != nil { + testSuite, err := s.testSuitesClient.Get(execution.TestSuite.Name) if err != nil { s.logger.Errorw("getting test suite error", "error", err) } if testSuite != nil { - testSuite.Status = testsuitesmapper.MapExecutionToTestSuiteStatus(testsuiteExecution) + testSuite.Status = testsuitesmapper.MapExecutionToTestSuiteStatus(execution) if err = s.testSuitesClient.UpdateStatus(testSuite); err != nil { s.logger.Errorw("updating test suite error", "error", err) } + + if execution.TestSuiteExecutionName != "" { + testSuiteExecution, err := s.testSuiteExecutionsClient.Get(execution.TestSuiteExecutionName) + if err != nil { + s.logger.Errorw("getting test suite execution error", "error", err) + } + + if testSuiteExecution != nil { + testSuiteExecution.Status = testsuiteexecutionsmapper.MapAPIToCRD(execution, testSuiteExecution.Generation) + if err = s.testSuiteExecutionsClient.UpdateStatus(testSuiteExecution); err != nil { + s.logger.Errorw("updating test suite execution error", "error", err) + } + } + } } } @@ -252,15 +267,15 @@ func (s *Scheduler) runAfterEachStep(ctx context.Context, testsuiteExecution *te } status := "" - if testsuiteExecution.Status != nil { - status = string(*testsuiteExecution.Status) + if execution.Status != nil { + status = string(*execution.Status) } out, err := telemetry.SendRunEvent("testkube_api_run_test_suite", telemetry.RunParams{ AppVersion: version.Version, Host: host, ClusterID: clusterID, - DurationMs: testsuiteExecution.DurationMs, + DurationMs: execution.DurationMs, Status: status, }) diff --git a/pkg/triggers/executor_test.go b/pkg/triggers/executor_test.go index 37e8f76e584..737b67b9ce8 100644 --- a/pkg/triggers/executor_test.go +++ b/pkg/triggers/executor_test.go @@ -14,6 +14,7 @@ import ( executorsclientv1 "github.com/kubeshop/testkube-operator/client/executors/v1" testsclientv3 "github.com/kubeshop/testkube-operator/client/tests/v3" testsourcesv1 "github.com/kubeshop/testkube-operator/client/testsources/v1" + testsuiteexecutionsv1 "github.com/kubeshop/testkube-operator/client/testsuiteexecutions/v1" testsuitesv3 "github.com/kubeshop/testkube-operator/client/testsuites/v3" "github.com/kubeshop/testkube/internal/app/api/metrics" "github.com/kubeshop/testkube/pkg/api/v1/testkube" @@ -48,6 +49,7 @@ func TestExecute(t *testing.T) { mockSecretClient := secret.NewMockInterface(mockCtrl) configMapConfig := config.NewMockRepository(mockCtrl) mockConfigMapClient := configmap.NewMockInterface(mockCtrl) + mockTestSuiteExecutionsClient := testsuiteexecutionsv1.NewMockInterface(mockCtrl) mockExecutor := client.NewMockExecutor(mockCtrl) @@ -112,6 +114,7 @@ func TestExecute(t *testing.T) { log.DefaultLogger, configMapConfig, mockConfigMapClient, + mockTestSuiteExecutionsClient, ) s := &Service{ triggerStatus: make(map[statusKey]*triggerStatus), diff --git a/pkg/triggers/service_test.go b/pkg/triggers/service_test.go index 512b13ea149..db6f1ec4572 100644 --- a/pkg/triggers/service_test.go +++ b/pkg/triggers/service_test.go @@ -18,6 +18,7 @@ import ( executorsclientv1 "github.com/kubeshop/testkube-operator/client/executors/v1" testsclientv3 "github.com/kubeshop/testkube-operator/client/tests/v3" testsourcesv1 "github.com/kubeshop/testkube-operator/client/testsources/v1" + testsuiteexecutionsv1 "github.com/kubeshop/testkube-operator/client/testsuiteexecutions/v1" testsuitesv3 "github.com/kubeshop/testkube-operator/client/testsuites/v3" faketestkube "github.com/kubeshop/testkube-operator/pkg/clientset/versioned/fake" "github.com/kubeshop/testkube/internal/app/api/metrics" @@ -54,6 +55,7 @@ func TestService_Run(t *testing.T) { mockSecretClient := secret.NewMockInterface(mockCtrl) configMapConfig := config.NewMockRepository(mockCtrl) mockConfigMapClient := configmap.NewMockInterface(mockCtrl) + mockTestSuiteExecutionsClient := testsuiteexecutionsv1.NewMockInterface(mockCtrl) mockExecutor := client.NewMockExecutor(mockCtrl) @@ -126,6 +128,7 @@ func TestService_Run(t *testing.T) { testLogger, configMapConfig, mockConfigMapClient, + mockTestSuiteExecutionsClient, ) mockLeaseBackend := NewMockLeaseBackend(mockCtrl)