From ebf07d9cc1416ff4987f38d341d73d8e45e041db Mon Sep 17 00:00:00 2001 From: Tomasz Konieczny Date: Mon, 28 Aug 2023 17:55:26 +0200 Subject: [PATCH 01/59] Test timeouts added (#4300) --- test/artillery/executor-smoke/crd/crd.yaml | 4 +++- .../executor-smoke/crd/curl.yaml | 2 ++ .../executor-smoke/crd/cypress.yaml | 5 ++-- .../executor-smoke/crd/k6.yaml | 3 ++- .../executor-smoke/crd/playwright.yaml | 3 ++- test/curl/executor-tests/crd/smoke.yaml | 4 +++- test/cypress/executor-tests/crd/crd.yaml | 24 ++++++++++++++++++- test/dashboard-e2e/crd/crd.yaml | 1 + test/ginkgo/executor-tests/crd/smoke.yaml | 4 +++- test/gradle/executor-smoke/crd/crd.yaml | 7 +++++- test/jmeter/executor-tests/crd/smoke.yaml | 6 ++++- test/k6/executor-tests/crd/other.yaml | 3 ++- test/k6/executor-tests/crd/smoke.yaml | 5 +++- test/kubepug/executor-smoke/crd/crd.yaml | 4 +++- test/maven/executor-smoke/crd/crd.yaml | 5 +++- test/playwright/executor-tests/crd/crd.yaml | 3 ++- test/soapui/executor-smoke/crd/crd.yaml | 4 +++- 17 files changed, 71 insertions(+), 16 deletions(-) diff --git a/test/artillery/executor-smoke/crd/crd.yaml b/test/artillery/executor-smoke/crd/crd.yaml index dbdc33b252c..4451c48daa4 100644 --- a/test/artillery/executor-smoke/crd/crd.yaml +++ b/test/artillery/executor-smoke/crd/crd.yaml @@ -15,6 +15,7 @@ spec: path: test/artillery/executor-smoke/artillery-smoke-test.yaml executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -33,4 +34,5 @@ spec: path: test/artillery/executor-smoke/artillery-smoke-test-negative.yaml executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 128m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/curl.yaml b/test/container-executor/executor-smoke/crd/curl.yaml index 46eb3bd52ac..d0069d8cc6c 100644 --- a/test/container-executor/executor-smoke/crd/curl.yaml +++ b/test/container-executor/executor-smoke/crd/curl.yaml @@ -15,6 +15,7 @@ spec: type: basic value: https://testkube.kubeshop.io/ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -34,3 +35,4 @@ spec: value: https://testkube.non.existing.url.example negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/cypress.yaml b/test/container-executor/executor-smoke/crd/cypress.yaml index c6b1e422125..bb61bc2d0e2 100644 --- a/test/container-executor/executor-smoke/crd/cypress.yaml +++ b/test/container-executor/executor-smoke/crd/cypress.yaml @@ -31,7 +31,7 @@ spec: dirs: - ./ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -65,4 +65,5 @@ spec: volumeMountPath: /data/artifacts dirs: - ./ - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/k6.yaml b/test/container-executor/executor-smoke/crd/k6.yaml index 216a962553d..80d05e4ba91 100644 --- a/test/container-executor/executor-smoke/crd/k6.yaml +++ b/test/container-executor/executor-smoke/crd/k6.yaml @@ -36,4 +36,5 @@ spec: workingDir: test/k6/executor-tests executionRequest: args: ["run", "k6-smoke-test-without-envs.js"] - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/playwright.yaml b/test/container-executor/executor-smoke/crd/playwright.yaml index 7d648be5d9b..8a826a39420 100644 --- a/test/container-executor/executor-smoke/crd/playwright.yaml +++ b/test/container-executor/executor-smoke/crd/playwright.yaml @@ -20,4 +20,5 @@ spec: volumeMountPath: /data/artifacts dirs: - ./ - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 \ No newline at end of file diff --git a/test/curl/executor-tests/crd/smoke.yaml b/test/curl/executor-tests/crd/smoke.yaml index b078536441f..22a434f289e 100644 --- a/test/curl/executor-tests/crd/smoke.yaml +++ b/test/curl/executor-tests/crd/smoke.yaml @@ -15,6 +15,7 @@ spec: path: test/curl/executor-tests/curl-smoke-test.json executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -33,4 +34,5 @@ spec: path: test/curl/executor-tests/curl-smoke-test-negative.json executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/cypress/executor-tests/crd/crd.yaml b/test/cypress/executor-tests/crd/crd.yaml index 26cb71e5bd3..df16098422b 100644 --- a/test/cypress/executor-tests/crd/crd.yaml +++ b/test/cypress/executor-tests/crd/crd.yaml @@ -25,6 +25,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -55,6 +56,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -85,6 +87,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -113,6 +116,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -143,6 +147,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -173,6 +178,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -201,6 +207,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -229,6 +236,7 @@ spec: - --config - video=true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -261,6 +269,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -289,6 +298,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- # cypress-default-executor-smoke-electron-testsource TestSource apiVersion: tests.testkube.io/v1 @@ -326,6 +336,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- # cypress-default-executor-smoke-electron-testsource-git-dir - TestSource apiVersion: tests.testkube.io/v1 @@ -363,6 +374,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -391,6 +403,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -421,6 +434,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -451,6 +465,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -469,6 +484,7 @@ spec: path: test/cypress/executor-tests/cypress-9 executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -492,6 +508,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -515,6 +532,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -536,6 +554,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -559,6 +578,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -582,6 +602,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -602,4 +623,5 @@ spec: args: - --some-incorrect-argument negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 1Gi\n cpu: 1\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 1Gi\n cpu: 1\n" + activeDeadlineSeconds: 120 diff --git a/test/dashboard-e2e/crd/crd.yaml b/test/dashboard-e2e/crd/crd.yaml index 18e59e65ad4..1dbace08ae6 100644 --- a/test/dashboard-e2e/crd/crd.yaml +++ b/test/dashboard-e2e/crd/crd.yaml @@ -31,4 +31,5 @@ spec: artifactRequest: storageClassName: standard jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 4Gi\n cpu: 3\n" + activeDeadlineSeconds: 240 schedule: "15 */4 * * *" \ No newline at end of file diff --git a/test/ginkgo/executor-tests/crd/smoke.yaml b/test/ginkgo/executor-tests/crd/smoke.yaml index 1041b23b911..1a1fe5be3dc 100644 --- a/test/ginkgo/executor-tests/crd/smoke.yaml +++ b/test/ginkgo/executor-tests/crd/smoke.yaml @@ -15,6 +15,7 @@ spec: path: test/ginkgo/executor-tests/smoke executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 @@ -34,4 +35,5 @@ spec: path: test/ginkgo/executor-tests/smoke-negative executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/gradle/executor-smoke/crd/crd.yaml b/test/gradle/executor-smoke/crd/crd.yaml index 44a9d0529ed..cc453d29df1 100644 --- a/test/gradle/executor-smoke/crd/crd.yaml +++ b/test/gradle/executor-smoke/crd/crd.yaml @@ -22,6 +22,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -45,6 +46,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -68,6 +70,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -91,6 +94,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -109,4 +113,5 @@ spec: path: examples/hello-gradle-jdk18 executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/jmeter/executor-tests/crd/smoke.yaml b/test/jmeter/executor-tests/crd/smoke.yaml index 3d4aa8b3807..6ea84e41e59 100644 --- a/test/jmeter/executor-tests/crd/smoke.yaml +++ b/test/jmeter/executor-tests/crd/smoke.yaml @@ -15,6 +15,7 @@ spec: path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -35,6 +36,7 @@ spec: args: - "jmeter-executor-smoke.jmx" jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -60,6 +62,7 @@ spec: args: - "-JURL_PROPERTY=testkube.io" jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -78,4 +81,5 @@ spec: path: test/jmeter/executor-tests/jmeter-executor-smoke-negative.jmx executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/k6/executor-tests/crd/other.yaml b/test/k6/executor-tests/crd/other.yaml index cabc35acbd0..a4242529bca 100644 --- a/test/k6/executor-tests/crd/other.yaml +++ b/test/k6/executor-tests/crd/other.yaml @@ -24,4 +24,5 @@ spec: - -e - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value - k6-smoke-test.js - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/k6/executor-tests/crd/smoke.yaml b/test/k6/executor-tests/crd/smoke.yaml index f776a09cf9b..77242df0d25 100644 --- a/test/k6/executor-tests/crd/smoke.yaml +++ b/test/k6/executor-tests/crd/smoke.yaml @@ -23,6 +23,7 @@ spec: - -e - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -49,6 +50,7 @@ spec: - -e - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -67,4 +69,5 @@ spec: path: test/k6/executor-tests/k6-smoke-test-negative.js executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/kubepug/executor-smoke/crd/crd.yaml b/test/kubepug/executor-smoke/crd/crd.yaml index 0d997dcb270..701d225b53a 100644 --- a/test/kubepug/executor-smoke/crd/crd.yaml +++ b/test/kubepug/executor-smoke/crd/crd.yaml @@ -15,6 +15,7 @@ spec: path: test/kubepug/executor-smoke/crd/crd.yaml executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -33,4 +34,5 @@ spec: path: test/kubepug/executor-smoke/kubepug-smoke-test-negative.yaml executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/maven/executor-smoke/crd/crd.yaml b/test/maven/executor-smoke/crd/crd.yaml index ab896d5db2f..2e6d3117ea6 100644 --- a/test/maven/executor-smoke/crd/crd.yaml +++ b/test/maven/executor-smoke/crd/crd.yaml @@ -22,6 +22,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -45,6 +46,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -63,4 +65,5 @@ spec: path: examples/hello-maven-jdk18 executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n" + activeDeadlineSeconds: 60 \ No newline at end of file diff --git a/test/playwright/executor-tests/crd/crd.yaml b/test/playwright/executor-tests/crd/crd.yaml index e35835895c4..90a43f3d9c0 100644 --- a/test/playwright/executor-tests/crd/crd.yaml +++ b/test/playwright/executor-tests/crd/crd.yaml @@ -14,4 +14,5 @@ spec: branch: main path: test/playwright/executor-tests/playwright-project executionRequest: - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" + activeDeadlineSeconds: 120 \ No newline at end of file diff --git a/test/soapui/executor-smoke/crd/crd.yaml b/test/soapui/executor-smoke/crd/crd.yaml index 0738d84f40a..2e939dd1e2b 100644 --- a/test/soapui/executor-smoke/crd/crd.yaml +++ b/test/soapui/executor-smoke/crd/crd.yaml @@ -15,6 +15,7 @@ spec: path: test/soapui/executor-smoke/soapui-smoke-test.xml executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -33,4 +34,5 @@ spec: path: test/soapui/executor-smoke/soapui-smoke-test-negative.xml executionRequest: negativeTest: true - jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 512m\n" \ No newline at end of file + jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 512m\n" + activeDeadlineSeconds: 60 \ No newline at end of file From d8b2e9f4434717a4e73d8c3aabb60c1f720eeb06 Mon Sep 17 00:00:00 2001 From: nicufk <89570185+nicufk@users.noreply.github.com> Date: Wed, 30 Aug 2023 18:43:58 +0300 Subject: [PATCH 02/59] feat: add pprof server (#4304) --- cmd/api-server/main.go | 10 ++++++++++ internal/app/api/debug/server.go | 19 +++++++++++++++++++ internal/config/config.go | 2 ++ 3 files changed, 31 insertions(+) create mode 100644 internal/app/api/debug/server.go diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 78ad866db86..517b4175587 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -43,6 +43,7 @@ import ( "github.com/pkg/errors" + "github.com/kubeshop/testkube/internal/app/api/debug" "github.com/kubeshop/testkube/internal/app/api/metrics" "github.com/kubeshop/testkube/pkg/agent" kubeexecutor "github.com/kubeshop/testkube/pkg/executor" @@ -153,6 +154,15 @@ func main() { grpcClient = cloud.NewTestKubeCloudAPIClient(grpcConn) } + if cfg.EnableDebugServer { + debugSrv := debug.NewDebugServer(cfg.DebugListenAddr) + + g.Go(func() error { + log.DefaultLogger.Infof("starting debug pprof server") + return debugSrv.ListenAndServe() + }) + } + // k8s scriptsClient := scriptsclient.NewClient(kubeClient, cfg.TestkubeNamespace) testsClientV1 := testsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) diff --git a/internal/app/api/debug/server.go b/internal/app/api/debug/server.go new file mode 100644 index 00000000000..4012a3a18c8 --- /dev/null +++ b/internal/app/api/debug/server.go @@ -0,0 +1,19 @@ +package debug + +import ( + "net/http" + "net/http/pprof" +) + +func NewDebugServer(addr string) *http.Server { + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + return &http.Server{ + Addr: addr, + Handler: mux, + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 7f5c32fafd4..a473febc533 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -64,6 +64,8 @@ type Config struct { TestkubeClusterName string `envconfig:"TESTKUBE_CLUSTER_NAME" default:""` CompressArtifacts bool `envconfig:"COMPRESSARTIFACTS" default:"false"` TestkubeHelmchartVersion string `envconfig:"TESTKUBE_HELMCHART_VERSION" default:""` + DebugListenAddr string `envconfig:"DEBUG_LISTEN_ADDR" default:"0.0.0.0:1337"` + EnableDebugServer bool `envconfig:"ENABLE_DEBUG_SERVER" default:"false"` } func Get() (*Config, error) { From 55e29790a5769d714ac1896061a7ea111544b2ac Mon Sep 17 00:00:00 2001 From: Ale <93217218+alelthomas@users.noreply.github.com> Date: Thu, 31 Aug 2023 08:29:53 -0400 Subject: [PATCH 03/59] docs: fix default commands format (#4307) --- docs/docs/test-types/executor-ginkgo.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/test-types/executor-ginkgo.md b/docs/docs/test-types/executor-ginkgo.md index 53ad6be2a31..15520ab8e4a 100644 --- a/docs/docs/test-types/executor-ginkgo.md +++ b/docs/docs/test-types/executor-ginkgo.md @@ -4,10 +4,11 @@ import Admonition from "@theme/Admonition"; Our dedicated Ginkgo executor allows running Ginkgo tests with Testkube - directly from your Git repository. -Default command for this executor: ginkgo -Default arguments for this executor command: -r -p --randomize-all --randomize-suites --keep-going --trace --junit-report <reportFile> <envVars> <runPath> +* Default command for this executor: `ginkgo` +* Default arguments for this executor command: `-r` `-p` `--randomize-all` `--randomize-suites` `--keep-going` `--trace` `--junit-report` `` `` `` (parameters in <> are calculated at test execution) + export const ExecutorInfo = () => { return (
From 70d4e6ccd0579167bd887c2b2d2c8dfbdc54cc09 Mon Sep 17 00:00:00 2001 From: Tomasz Konieczny Date: Sun, 3 Sep 2023 21:08:44 +0200 Subject: [PATCH 04/59] docs: Webhook docs extended (#4310) * Webhook docs extended * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Updated after CR --------- Co-authored-by: Julianne Fermi --- docs/docs/articles/webhooks.mdx | 227 ++++++++++++++++-- docs/docs/img/dashboard-create-webhook-1.png | Bin 0 -> 33945 bytes docs/docs/img/dashboard-create-webhook-2.png | Bin 0 -> 21411 bytes ...ard-create-webhook-resource-identifier.png | Bin 0 -> 6549 bytes docs/docs/img/dashboard-webhook-headers.png | Bin 0 -> 13861 bytes docs/docs/img/dashboard-webhook-payload.png | Bin 0 -> 15240 bytes .../img/dashboard-webhook-settings-action.png | Bin 0 -> 16221 bytes docs/docs/img/dashboard-webhooks-icon.png | Bin 0 -> 3070 bytes 8 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 docs/docs/img/dashboard-create-webhook-1.png create mode 100644 docs/docs/img/dashboard-create-webhook-2.png create mode 100644 docs/docs/img/dashboard-create-webhook-resource-identifier.png create mode 100644 docs/docs/img/dashboard-webhook-headers.png create mode 100644 docs/docs/img/dashboard-webhook-payload.png create mode 100644 docs/docs/img/dashboard-webhook-settings-action.png create mode 100644 docs/docs/img/dashboard-webhooks-icon.png diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index 77cfe991eb2..5b099133724 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -5,13 +5,134 @@ import TabItem from "@theme/TabItem"; Webhooks allow you to integrate Testkube with external systems by sending HTTP POST payloads containing information about Testkube executions and their current state when specific events occur. To set up webhooks in Testkube, you'll need to have an HTTPS endpoint to receive the events and a payload template to be sent along with the data. -Here's an example format for creating a webhook in Testkube using either the CLI or custom response: +## Creating a Webhook +The webhook can be created using the Dashboard, CLI, or a Custom Resource. + +If you prefer to use the Dashboard, you can view existing webhooks by going to the Webhooks tab. + +![Dashboard menu - webhooks icon](../img/dashboard-webhooks-icon.png) + +Here you can also create a new webhook by clicking the `Create a new webhook` button. + +Then, fill in the webhook details: +![Dashboard webhook - create dialog 1](../img/dashboard-create-webhook-1.png) +- Name - your webhook name (in this case `example-webhook`) +- Resource identifier - the resource (or resources) selected by `label` for which the webhook can be triggered (in the example: `test-type:postman-collection` - any postman test) +- Triggered events - events that will trigger the webhook (in this case `start-test`, `end-test-success`, and `end-test-failed`). All available trigger events can be found in the [Supported Event types](#supported-event-types) section. + +![Dashboard webhook - create dialog 2](../img/dashboard-create-webhook-2.png) + +Set your webhook URI - the HTTPS endpoint where you want to receive the webhook events. +After the webhook is created, the custom payload and headers can be set in Settings->Action. + + + + +Webhooks can be created with Testkube CLI using the `create webhook` command. + +```sh +testkube create webhook --name example-webhook --events start-test --events end-test-success --events end-test-failed --uri +``` +`--name` - Your webhook name (in this case `example-webhook`). +`--events` - Event that will trigger a webhook. Multiple `--events` can be defined (in this case `--events start-test --events end-test-success --events end-test-failed`). All available trigger events can be found in the [Supported Event types](#supported-event-types) section. +`--uri` - The HTTPS endpoint where you want to receive the webhook events. + + + + + +```yaml title="webhook.yaml" +apiVersion: executor.testkube.io/v1 +kind: Webhook +metadata: + name: example-webhook + namespace: testkube +spec: + uri: + events: + - start-test + - end-test-success + - end-test-failed + selector: "" +``` +Where should be replaced with the HTTPS endpoint URL where you want to receive the webhook events. + +And then apply with: + +```sh +kubectl apply -f webhook.yaml +``` + + + + + +### Resource Selector (labels) +In order to limit webhook triggers to a specific resource, or resources, the Resource Selector can be used. It allows you to select the specific resource by label, or labels. + + + + +![Dashboard webhook - resource identifier](../img/dashboard-create-webhook-resource-identifier.png) + + -Create a webhook template payload file: +The Resource Selector can be set with `--selector`. +For example, `--selector test-type=postman-collection` will limit the resources to the postman tests (label: `test-type=postman-collection`) + + + + + +```yaml +spec: + selector: test-type=postman-collection +``` + +So, the complete definition may look like this: + +```yaml title="webhook.yaml" +apiVersion: executor.testkube.io/v1 +kind: Webhook +metadata: + name: example-webhook + namespace: testkube +spec: + uri: + events: + - start-test + - end-test-success + - end-test-failed + selector: test-type=postman-collection +``` + + + + + +### Webhook Payload + +Webhook payload can be configured - in this example, `event id`: +``` +{"text": "event id {{ .Id }}"} +``` + + + +Webhook payload can be configured in Webhook Settings->Action + +![Dashboard webhook - webhook settings action`](../img/dashboard-webhook-settings-action.png) + +![Dashboard webhook - webhook payload](../img/dashboard-webhook-payload.png) + + + + +Create a webhook payload template file: ```json title="template.json" { @@ -19,9 +140,13 @@ Create a webhook template payload file: } ``` +And set it with `--payload-template template.json`. + ```sh -testkube create webhook --name example-webhook --events start-test --events end-test-failed --payload-template template.json --uri +testkube create webhook --name example-webhook --events start-test --events end-test-passed --events end-test-failed --payload-template template.json --uri ``` +Where should be replaced with the HTTPS endpoint URL where you want to receive the webhook events. + ```sh title="Expected output:" Webhook created example-webhook 🥇 @@ -31,7 +156,14 @@ Webhook created example-webhook 🥇 -```yaml title="webhook.yaml" +Payload template can be configured with `spec.payloadTemplate`. +``` + payloadTemplate: | + {"text": "event id {{ .Id }}"} +``` + +Example: +``` apiVersion: executor.testkube.io/v1 kind: Webhook metadata: @@ -47,23 +179,14 @@ spec: payloadObjectField: "" payloadTemplate: | {"text": "event id {{ .Id }}"} - headers: - X-Token: "12345" -``` - -And then apply with: - -```sh -kubectl apply -f webhook.yaml ``` -In the example above, replace with the HTTPS endpoint URL where you want to receive the webhook events. The payload template can be customized to include additional information. In the above example, only the event `Id` is being sent. The template's variables will be replaced with data when events occur. - -You can add additional HTTP headers like `Authorization` or `x-api-key` to have a secret token. +#### Customizing Webhook Payload +The payload template can be customized to include additional information. In the above example, only the event `Id` is being sent. The template's variables will be replaced with data when events occur. It's possible to get access to env variables of testkube-api-server pod in webhook template: @@ -71,6 +194,80 @@ It's possible to get access to env variables of testkube-api-server pod in webho TESTKUBE_CLOUD_URL: {{ index .Envs "TESTKUBE_CLOUD_URL" }} ``` +### HTTP Headers +You can add additional HTTP headers like `Authorization` or `x-api-key` to have a secret token. + + + + +Webhook headers can be configured in Webhook Settings->Action. + +![Dashboard webhook - webhook settings action`](../img/dashboard-webhook-settings-action.png) + +![Dashboard webhook - webhook headers](../img/dashboard-webhook-headers.png) + + + + +Custom headers can be set using `--header` - for example: + +`--header X-Token="12345"` + + + + + +```yaml +spec: + headers: + X-Token: "12345" +``` + +```yaml title="webhook.yaml" +apiVersion: executor.testkube.io/v1 +kind: Webhook +metadata: + name: example-webhook + namespace: testkube +spec: + uri: + events: + - start-test + - end-test-success + - end-test-failed + selector: "" + headers: + X-Token: "12345" +``` + + + + +## Supported Event types +Webhooks can be triggered on any of the following events: +- start-test +- end-test-success +- end-test-failed +- end-test-aborted +- end-test-timeout +- start-testsuite +- end-testsuite-success +- end-testsuite-failed +- end-testsuite-aborted +- end-testsuite-timeout +- created +- updated +- deleted + +They can be triggered by the following resources: +- test +- testsuite +- executor +- trigger +- webhook +- testexecution +- testsuiteexecution + ## Testing Webhooks If you are just getting started and want to test your webhook configuration, you can use public and free services that act as HTTP catch-all apps. Here are a couple of options: diff --git a/docs/docs/img/dashboard-create-webhook-1.png b/docs/docs/img/dashboard-create-webhook-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5b6f568773b5f4d688c278175e9d51c9c0bb0ffb GIT binary patch literal 33945 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 literal 0 HcmV?d00001 diff --git a/docs/docs/img/dashboard-create-webhook-2.png b/docs/docs/img/dashboard-create-webhook-2.png new file mode 100644 index 0000000000000000000000000000000000000000..a7df407e85643720bc655c11138701e5f40376f8 GIT binary patch literal 21411 zcmce;Wk6KX`z|_!q9P!nh?J7jDIF?EH%N!HbaxDjNHZecA|>4&LpKaHz|h?t14GT- z{Ql>DxZmzM=YI}gklC~MUh7@&dgFPXWvGghG~Sa}Pe33Lo~(?d8VH2>6a>O3c=QnX z#FB-=82E+l@=;df5%A;t$Sef-o6Pl-wyU~>g{z0LvpLAp-oehC&BfH&+}z&ftApzy z7E}!Q(%btlNjRGuyIMKezt*s_GY6S^d$7Ld;dJK9~xM1jXlVw^(Oqq{sOx^oQ9Hw=?U|n z028ci{pwhmYYP3pO7#sGZFNQD z-H)o5xZ76A?DXoLGn@ zgP^P|tm++pY=uMx_X(s;L~}D`%47+Rm>By!((>?k-*uL&jUq5_s+77qf$tnfxf+NQ z0pZ49wE-Mr_0E-6^VaPj9kO>@6w<|V#w?3I0$=5p6kGf_@lTx~fQ@{L*cGYJa43;S z2VD3s+w~H&rX=vg(+h_}vw{WvRs`TSaN3jvkA_3(d)u!)Uf(!SN_!Yt_ll@JopKgY z?S{)=Vw9y6C{O(W%v{s#8!< zjN5_#Yoae>`l594UajuzL;+vu?CRF8>FVxWg8Jjx+b8{7PjIa__g1-IO|SMf4((r^ zKd@^Y7G$O)V-u7Y>ii@3_4qJ;!KV0ug&1J|bUz8H|KEP##ZcE+?}VE3AAmMg7cOeu z))c4c>=-_pI}PzhZ50{Tw2q-KeiHO;9RO?B`zA}+QQ3miWtZwR!%^>=QZ4(w)mR-B zeK;w-%Z9>_U}?vVJ+GVEy)VO%Z~>PM{f8d9fVnVkHWMvvZ2347hb$BrV%B+lN-UmR zC}zAz?&%cwoA<5A#;hA$siR>SrC0!=2^tiu`BYtEyhdvBa3Ny+H=HQtlrWGYtnBdE2tvTAoCocsCh z*NGxpbLK16fl|Db+P$&~vZI-SiGP|*Sq-d!qfeA0S{;4fT678r_N^q51&oxXRPCM> zd=>x+IIZv+_#ij;>T%=UZm*EOzG=@131y{g+=Vc0Z(@H`RuZ$NJDBO*SxiE(4eDa8 z5nEd?U)-HA0s|#@A9z*m>i?m z=sBfgYdJCNtqq>|;C0gUw17NHbav{Oa028rc}okb-q&6nWE7FopUnX+pE=mrklT#1 zvqFD#d4hV;3Wo>DCzLEf&B@21X+3>0-yUYz?i6ShOC~7Ud6h_+JFgve+w!?=%y~@~ zUu>r6oYwi$(+$~CPB*7hEEs~$E*D>GmLk4Z`>hEFl8GFB1_`(9(;#d4n-TqSv?b ziAksF?R;ZZRu4-cqJ_sUfyDk~Q7V{J7o<`w*G2Lz(`nT^eoB!XE>HNfK(qeTW9|}i zUUPQ&)R1~ESod9_ZgJzS;cS(yT;p)_0g6AF6DiT)VdYR=;(6TOr0xrPSE!Y1THC^O z*Vwr2N9=5 zhLzEQ!4Tca&xLBy9E_22qxL6d�+kW9BxAcS97DCD%P_xf-D+zVD8e=0nw6mRD7Y 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 literal 0 HcmV?d00001 diff --git a/docs/docs/img/dashboard-webhook-payload.png b/docs/docs/img/dashboard-webhook-payload.png new file mode 100644 index 0000000000000000000000000000000000000000..ca344879707c9e8b21b86d296ef3d00a8ceb2c66 GIT binary patch literal 15240 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 B#2f$s literal 0 HcmV?d00001 diff --git a/docs/docs/img/dashboard-webhook-settings-action.png b/docs/docs/img/dashboard-webhook-settings-action.png new file mode 100644 index 0000000000000000000000000000000000000000..01044ed2e58e1e89fda87f13d3bc03ccb178bba8 GIT binary patch literal 16221 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 R|7mjnAu98|QdrmjzW}W%YP0|V literal 0 HcmV?d00001 diff --git a/docs/docs/img/dashboard-webhooks-icon.png b/docs/docs/img/dashboard-webhooks-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..619d58e8918ea812f99bafd6e8cd67195a82104a GIT binary patch literal 3070 zcmd5;i8mB%7avP#Ve(3r7_z>JCS%E(vSzPDDnuH}GQu!42Bo*LMI!sYH1)n&BD*Ne zObjM75sev3mO%_-n;F|Xec$&Vyx(`u_nh-QzkAQU=RWt`bIr3WWx#-Tl+d=}|S4cbiEEo7gO|7p9aZi-c-FU7p1-Ed8I|cc`5ndrsfG;d45ULUC z9Rh{ILhl8^8GIe*xlX_Taxx2ndcp4p!Q`*q4}=1|A0pJ{wKU~J??dGwXCYeZ@(?Wp z9f-kMy)UOuy8-}*-K@?Iq9QN5gh_u~1taX+Wpc>W6rRyYEj44{OP*ADUIhI$yc8${T56VUdv}&nhJpAoefZ zTSaNEpeFTAw%9usbH8HhPT12|#qK${Jq1I*kuVPa7=rwjyFv?cZW1Qav8d4-Ux$VpIBkdv@@05?7(1u~Mip9^jS!Bh+EI^01L4ax zOP=QIO;41fP*JDA#%3PN7mbW=#=wH^$)kQqqZz^pi6;XtD*>RsiKDp6BXD$$}yD{B_-|Txj>+;RU^gg+8uIF zU3*nw)01KRe5uoc>1ARti?-5~){RT$DXy7N%DL5!dH2cSx@KkyNMAcKHu@qUVmrcc z%)iGZ{OhFQw^SP?&eXw;^&YXi0UqP)F+@{!*$*Wpk9PH2Z)&VDnSN}E`JAavmm;jP zlBl?t;KzhAbWd*$PPoeUh5@JNM+!1zpQpqek8^Tz@BhQ*7Jfg(EmuZ@QaM!}pVr!kLv@(@Y|GqrF+uG@1swD4)a2Ek9k>zxmYAUxEM!=Ge zj^G79|9Gji6>@)dvPS-A!b{LE8!f7b{$mlL8KFpNgq42mIry#jND_#|M0k7i<>Z~a z?D`z6H~3Ohr3I6vii*oknG@a+z8kPtI7L3t>EtfuU3yNAxVX4rJ>HaC`{K@gT2;2@ zVBBNDtG8|uhK2vq*m%yikreyj%b*49?cEx9S@aTwg@QP($?n8r-}39gXJ;;y9sWY+ z)oua&KZxGZ3SV-9Tr_+;Bjy-dAwJZn_G5Ju*Z)HS{?_0MshZdMzfPF`t05j6<=nR>g0CkgPfV}LUwdl z`Dzq`N<6fH(;^~hb@fg*u2Ln7F z&IkuI*kBevH2w?|c*@`CpT{jg+b-rhRoB}}QgnAlqNoHD1KeCP=^noxwE!=WI^~qD z3oRuRU3S+D_gJ$K^vt>bY+U!WQscmPP-bf3Sx5>SLFDo5A>}p)Mt^}odoT`ej7{NZAWKkoz~6w(OdMEjl2l|(QSYXE$^bI=Y>4V zyHT~~K$sfPq$UiHC;2!|@F~2@c%5^h?`Hl1^_zOt%6bb+_kWvF7jKASq5I60`ogW$ft0t# z02!0u7a2DY^^uBiM^ll&=a9uShYmR)up=VsD1{H#);d7QjfI}KIpli*0n=%!(S%-S z6L*6`1NZLf3`5uV4?GC&xKC+udbx=FxMr(MpR*Eca`J2F0+OP; zCU+!?rKi_87tU=qzp)e)jN4p)?G=|}?8}4PlFi`Kn`ObJ>f<;Wk1CaazLEU*BDiwC zEnI~)r_*?rca`^Ggb# zvZ^XvMIejC8pH$@=c$A|6CJEW__Rb7(7<|6PQco*ztVVZYuPHx8~1pAcvt$@KiAeT znmvU;XDjU*K7)`I$%%X9?c%=ZvL~YWbsu)56^qEWIg^jryA$NZZO$&f-hpGbsgn@J zQhX|CF4NZXud@m*&Q;Y^ZYG?gX0hkHkCp>9`RQgyc>b-@O@E>DnT;_lNtKj%=v!5P u!71?RFm9=AZu6f0NJ4UkZ2m{{Yofz(S){YZq~+J953sgyFt0N8eEc`3@Z?kg literal 0 HcmV?d00001 From 6786487abd80c83acf371982bed7a44a725bb5b9 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 4 Sep 2023 13:37:55 +0300 Subject: [PATCH 05/59] fix: use regexp for checks testing (#4314) --- contrib/executor/k6/pkg/runner/runner.go | 14 +++++++++----- contrib/executor/k6/pkg/runner/runner_test.go | 13 +++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/contrib/executor/k6/pkg/runner/runner.go b/contrib/executor/k6/pkg/runner/runner.go index 625bd94ab5e..37669472b71 100644 --- a/contrib/executor/k6/pkg/runner/runner.go +++ b/contrib/executor/k6/pkg/runner/runner.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/pkg/errors" @@ -237,14 +238,17 @@ func isSuccessful(summary string) bool { // if any of the checks failed func areChecksSuccessful(summary string) bool { lines := splitSummaryBody(summary) + re, err := regexp.Compile(`checks\.+: `) + if err != nil { + outputPkg.PrintLogf("%s Regexp error: %s", ui.IconWarning, err.Error()) + return true + } + for _, line := range lines { - if !strings.Contains(line, "checks") { + if !re.MatchString(line) { continue } - if strings.Contains(line, "100.00%") { - return true - } - return false + return strings.Contains(line, "100.00%") } return true diff --git a/contrib/executor/k6/pkg/runner/runner_test.go b/contrib/executor/k6/pkg/runner/runner_test.go index d8ba785c627..1912b05263e 100644 --- a/contrib/executor/k6/pkg/runner/runner_test.go +++ b/contrib/executor/k6/pkg/runner/runner_test.go @@ -37,6 +37,19 @@ func TestExecutionResult(t *testing.T) { assert.Equal(t, testkube.ExecutionStatusPassed, result.Status) assert.Len(t, result.Steps, 2) }) + + t.Run("Get successful checks for k6 execution result", func(t *testing.T) { + t.Parallel() + // setup + summary, err := os.ReadFile("../../examples/k6-test-scenarios.txt") + if err != nil { + assert.FailNow(t, "Unable to read k6 test summary") + } + + result := areChecksSuccessful(string(summary)) + assert.Equal(t, true, result) + }) + } func TestParse(t *testing.T) { From e8670f906572668d3f71b2374c5fd6f35c613a3d Mon Sep 17 00:00:00 2001 From: Tomasz Konieczny Date: Mon, 4 Sep 2023 20:40:39 +0200 Subject: [PATCH 06/59] fix: Test timeouts increased (#4317) * Test timeouts increased * Test timeouts increased * Test timeouts increased --- test/artillery/executor-smoke/crd/crd.yaml | 2 +- .../executor-smoke/crd/curl.yaml | 4 +- .../executor-smoke/crd/cypress.yaml | 4 +- .../executor-smoke/crd/k6.yaml | 3 +- .../executor-smoke/crd/playwright.yaml | 2 +- test/curl/executor-tests/crd/smoke.yaml | 4 +- test/cypress/executor-tests/crd/crd.yaml | 44 +++++++++---------- test/dashboard-e2e/crd/crd.yaml | 2 +- test/ginkgo/executor-tests/crd/smoke.yaml | 4 +- test/gradle/executor-smoke/crd/crd.yaml | 10 ++--- test/jmeter/executor-tests/crd/smoke.yaml | 8 ++-- test/k6/executor-tests/crd/other.yaml | 2 +- test/k6/executor-tests/crd/smoke.yaml | 6 +-- test/kubepug/executor-smoke/crd/crd.yaml | 4 +- test/maven/executor-smoke/crd/crd.yaml | 6 +-- test/playwright/executor-tests/crd/crd.yaml | 2 +- test/soapui/executor-smoke/crd/crd.yaml | 4 +- 17 files changed, 56 insertions(+), 55 deletions(-) diff --git a/test/artillery/executor-smoke/crd/crd.yaml b/test/artillery/executor-smoke/crd/crd.yaml index 4451c48daa4..558bc70c9bb 100644 --- a/test/artillery/executor-smoke/crd/crd.yaml +++ b/test/artillery/executor-smoke/crd/crd.yaml @@ -35,4 +35,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/curl.yaml b/test/container-executor/executor-smoke/crd/curl.yaml index d0069d8cc6c..0a1f2dbac0b 100644 --- a/test/container-executor/executor-smoke/crd/curl.yaml +++ b/test/container-executor/executor-smoke/crd/curl.yaml @@ -15,7 +15,7 @@ spec: type: basic value: https://testkube.kubeshop.io/ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -35,4 +35,4 @@ spec: value: https://testkube.non.existing.url.example negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/cypress.yaml b/test/container-executor/executor-smoke/crd/cypress.yaml index bb61bc2d0e2..e01af334ac7 100644 --- a/test/container-executor/executor-smoke/crd/cypress.yaml +++ b/test/container-executor/executor-smoke/crd/cypress.yaml @@ -31,7 +31,7 @@ spec: dirs: - ./ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -66,4 +66,4 @@ spec: dirs: - ./ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 \ No newline at end of file + activeDeadlineSeconds: 600 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/k6.yaml b/test/container-executor/executor-smoke/crd/k6.yaml index 80d05e4ba91..4c66abd728c 100644 --- a/test/container-executor/executor-smoke/crd/k6.yaml +++ b/test/container-executor/executor-smoke/crd/k6.yaml @@ -17,6 +17,7 @@ spec: executionRequest: args: ["run", "k6-smoke-test-without-envs.js"] jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -37,4 +38,4 @@ spec: executionRequest: args: ["run", "k6-smoke-test-without-envs.js"] jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/container-executor/executor-smoke/crd/playwright.yaml b/test/container-executor/executor-smoke/crd/playwright.yaml index 8a826a39420..4b61119076d 100644 --- a/test/container-executor/executor-smoke/crd/playwright.yaml +++ b/test/container-executor/executor-smoke/crd/playwright.yaml @@ -21,4 +21,4 @@ spec: dirs: - ./ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 \ No newline at end of file + activeDeadlineSeconds: 600 \ No newline at end of file diff --git a/test/curl/executor-tests/crd/smoke.yaml b/test/curl/executor-tests/crd/smoke.yaml index 22a434f289e..043a8c7ce4c 100644 --- a/test/curl/executor-tests/crd/smoke.yaml +++ b/test/curl/executor-tests/crd/smoke.yaml @@ -15,7 +15,7 @@ spec: path: test/curl/executor-tests/curl-smoke-test.json executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -35,4 +35,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 32Mi\n cpu: 32m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/cypress/executor-tests/crd/crd.yaml b/test/cypress/executor-tests/crd/crd.yaml index df16098422b..15c1b5aa98b 100644 --- a/test/cypress/executor-tests/crd/crd.yaml +++ b/test/cypress/executor-tests/crd/crd.yaml @@ -25,7 +25,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -56,7 +56,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -87,7 +87,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -116,7 +116,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -147,7 +147,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -178,7 +178,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -207,7 +207,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -236,7 +236,7 @@ spec: - --config - video=true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -269,7 +269,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -298,7 +298,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- # cypress-default-executor-smoke-electron-testsource TestSource apiVersion: tests.testkube.io/v1 @@ -336,7 +336,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- # cypress-default-executor-smoke-electron-testsource-git-dir - TestSource apiVersion: tests.testkube.io/v1 @@ -374,7 +374,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 600 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -403,7 +403,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -434,7 +434,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -465,7 +465,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -484,7 +484,7 @@ spec: path: test/cypress/executor-tests/cypress-9 executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -508,7 +508,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -532,7 +532,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -554,7 +554,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -578,7 +578,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -602,7 +602,7 @@ spec: - --config - video=false jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -624,4 +624,4 @@ spec: - --some-incorrect-argument negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 1Gi\n cpu: 1\n" - activeDeadlineSeconds: 120 + activeDeadlineSeconds: 180 diff --git a/test/dashboard-e2e/crd/crd.yaml b/test/dashboard-e2e/crd/crd.yaml index 1dbace08ae6..7d77fe64317 100644 --- a/test/dashboard-e2e/crd/crd.yaml +++ b/test/dashboard-e2e/crd/crd.yaml @@ -31,5 +31,5 @@ spec: artifactRequest: storageClassName: standard jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 4Gi\n cpu: 3\n" - activeDeadlineSeconds: 240 + activeDeadlineSeconds: 600 schedule: "15 */4 * * *" \ No newline at end of file diff --git a/test/ginkgo/executor-tests/crd/smoke.yaml b/test/ginkgo/executor-tests/crd/smoke.yaml index 1a1fe5be3dc..82d4d820ff7 100644 --- a/test/ginkgo/executor-tests/crd/smoke.yaml +++ b/test/ginkgo/executor-tests/crd/smoke.yaml @@ -15,7 +15,7 @@ spec: path: test/ginkgo/executor-tests/smoke executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 @@ -36,4 +36,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/gradle/executor-smoke/crd/crd.yaml b/test/gradle/executor-smoke/crd/crd.yaml index cc453d29df1..fa9ebd5c038 100644 --- a/test/gradle/executor-smoke/crd/crd.yaml +++ b/test/gradle/executor-smoke/crd/crd.yaml @@ -22,7 +22,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -46,7 +46,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -70,7 +70,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -94,7 +94,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -114,4 +114,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/jmeter/executor-tests/crd/smoke.yaml b/test/jmeter/executor-tests/crd/smoke.yaml index 6ea84e41e59..0b29bef8ce8 100644 --- a/test/jmeter/executor-tests/crd/smoke.yaml +++ b/test/jmeter/executor-tests/crd/smoke.yaml @@ -15,7 +15,7 @@ spec: path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -36,7 +36,7 @@ spec: args: - "jmeter-executor-smoke.jmx" jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -62,7 +62,7 @@ spec: args: - "-JURL_PROPERTY=testkube.io" jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -82,4 +82,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/k6/executor-tests/crd/other.yaml b/test/k6/executor-tests/crd/other.yaml index a4242529bca..e60694fbeb6 100644 --- a/test/k6/executor-tests/crd/other.yaml +++ b/test/k6/executor-tests/crd/other.yaml @@ -25,4 +25,4 @@ spec: - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value - k6-smoke-test.js jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/k6/executor-tests/crd/smoke.yaml b/test/k6/executor-tests/crd/smoke.yaml index 77242df0d25..60abb1f4b38 100644 --- a/test/k6/executor-tests/crd/smoke.yaml +++ b/test/k6/executor-tests/crd/smoke.yaml @@ -23,7 +23,7 @@ spec: - -e - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -50,7 +50,7 @@ spec: - -e - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -70,4 +70,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/kubepug/executor-smoke/crd/crd.yaml b/test/kubepug/executor-smoke/crd/crd.yaml index 701d225b53a..2017bf5d99f 100644 --- a/test/kubepug/executor-smoke/crd/crd.yaml +++ b/test/kubepug/executor-smoke/crd/crd.yaml @@ -15,7 +15,7 @@ spec: path: test/kubepug/executor-smoke/crd/crd.yaml executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -35,4 +35,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/maven/executor-smoke/crd/crd.yaml b/test/maven/executor-smoke/crd/crd.yaml index 2e6d3117ea6..0d8c4a2f65f 100644 --- a/test/maven/executor-smoke/crd/crd.yaml +++ b/test/maven/executor-smoke/crd/crd.yaml @@ -22,7 +22,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -46,7 +46,7 @@ spec: value: "true" type: basic jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -66,4 +66,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file diff --git a/test/playwright/executor-tests/crd/crd.yaml b/test/playwright/executor-tests/crd/crd.yaml index 90a43f3d9c0..16b9bc30c96 100644 --- a/test/playwright/executor-tests/crd/crd.yaml +++ b/test/playwright/executor-tests/crd/crd.yaml @@ -15,4 +15,4 @@ spec: path: test/playwright/executor-tests/playwright-project executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n" - activeDeadlineSeconds: 120 \ No newline at end of file + activeDeadlineSeconds: 600 \ No newline at end of file diff --git a/test/soapui/executor-smoke/crd/crd.yaml b/test/soapui/executor-smoke/crd/crd.yaml index 2e939dd1e2b..0468b0e3554 100644 --- a/test/soapui/executor-smoke/crd/crd.yaml +++ b/test/soapui/executor-smoke/crd/crd.yaml @@ -15,7 +15,7 @@ spec: path: test/soapui/executor-smoke/soapui-smoke-test.xml executionRequest: jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 + activeDeadlineSeconds: 180 --- apiVersion: tests.testkube.io/v3 kind: Test @@ -35,4 +35,4 @@ spec: executionRequest: negativeTest: true jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 512m\n" - activeDeadlineSeconds: 60 \ No newline at end of file + activeDeadlineSeconds: 180 \ No newline at end of file From 35982d9d883fd8efe5ac322161c7733280aabf3b Mon Sep 17 00:00:00 2001 From: Tomasz Konieczny Date: Tue, 5 Sep 2023 13:07:59 +0200 Subject: [PATCH 07/59] feat: Cypress test - multi files case (#4321) * Cypress tests - multi files * Cypress tests - multi files - without envs --- .../executor-tests/cypress-12/cypress/e2e/smoke.cy.js | 5 +---- .../executor-tests/cypress-12/cypress/e2e/smoke2.cy.js | 5 +++++ .../cypress/e2e/smoke-without-envs-2.cy.js | 5 +++++ .../cypress/e2e/smoke-without-envs.cy.js | 6 +++--- 4 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 test/cypress/executor-tests/cypress-12/cypress/e2e/smoke2.cy.js create mode 100644 test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs-2.cy.js diff --git a/test/cypress/executor-tests/cypress-12/cypress/e2e/smoke.cy.js b/test/cypress/executor-tests/cypress-12/cypress/e2e/smoke.cy.js index 0ea71f2e66c..c6554cc2dec 100644 --- a/test/cypress/executor-tests/cypress-12/cypress/e2e/smoke.cy.js +++ b/test/cypress/executor-tests/cypress-12/cypress/e2e/smoke.cy.js @@ -1,7 +1,4 @@ -describe('Testkube website', () => { - it.skip('Open Testkube website', () => { - cy.visit('/') - }) +describe('Smoke test', () => { it(`Validate CYPRESS_CUSTOM_ENV ENV (${Cypress.env('CUSTOM_ENV')})`, () => { expect('CYPRESS_CUSTOM_ENV_value').to.equal(Cypress.env('CUSTOM_ENV')) //CYPRESS_CUSTOM_ENV - "cypress" prefix - auto-loaded from global ENVs }) diff --git a/test/cypress/executor-tests/cypress-12/cypress/e2e/smoke2.cy.js b/test/cypress/executor-tests/cypress-12/cypress/e2e/smoke2.cy.js new file mode 100644 index 00000000000..efbec61b67c --- /dev/null +++ b/test/cypress/executor-tests/cypress-12/cypress/e2e/smoke2.cy.js @@ -0,0 +1,5 @@ +describe('Smoke test 2', () => { + it(`expect 1=1`, () => { + expect('1').to.equal('1') + }) +}) diff --git a/test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs-2.cy.js b/test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs-2.cy.js new file mode 100644 index 00000000000..5ff6102fe8b --- /dev/null +++ b/test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs-2.cy.js @@ -0,0 +1,5 @@ +describe('Smoke test 2', () => { + it(`expect 2=2`, () => { + expect('2').to.equal('2') + }) +}) diff --git a/test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs.cy.js b/test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs.cy.js index 7d12ff498ec..f800dac1365 100644 --- a/test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs.cy.js +++ b/test/cypress/executor-tests/cypress-without-envs/cypress/e2e/smoke-without-envs.cy.js @@ -1,5 +1,5 @@ -describe('Testkube website', () => { - it('Open Testkube website', () => { - cy.visit('/') +describe('Smoke test 1', () => { + it(`expect 1=1`, () => { + expect('1').to.equal('1') }) }) From 99e9935025f8dd6ef471e7d4e832e8676de3f5e8 Mon Sep 17 00:00:00 2001 From: Jacek Wysocki Date: Tue, 5 Sep 2023 13:06:56 +0200 Subject: [PATCH 08/59] feat: add orgid and env to agent context (#4320) --- pkg/agent/agent.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 0255d1f1621..049315f9b69 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -28,8 +28,14 @@ import ( const ( apiKeyMeta = "api-key" clusterIDMeta = "cluster-id" - cloudMigrate = "migrate" + cloudMigrateMeta = "migrate" + orgIdMeta = "environment-id" + envIdMeta = "organization-id" healthcheckCommand = "healthcheck" + + cloudMigrateEnvName = "TESTKUBE_CLOUD_MIGRATE" + cloudEnvIdEnvName = "TESTKUBE_CLOUD_ENV_ID" + cloudOrgIdEnvName = "TESTKUBE_CLOUD_ORG_ID" ) // buffer up to five messages per worker @@ -208,7 +214,9 @@ func (ag *Agent) runCommandLoop(ctx context.Context) error { ctx = AddAPIKeyMeta(ctx, ag.apiKey) ctx = metadata.AppendToOutgoingContext(ctx, clusterIDMeta, ag.clusterID) - ctx = metadata.AppendToOutgoingContext(ctx, cloudMigrate, os.Getenv("TESTKUBE_CLOUD_MIGRATE")) + ctx = metadata.AppendToOutgoingContext(ctx, cloudMigrateMeta, os.Getenv(cloudMigrateEnvName)) + ctx = metadata.AppendToOutgoingContext(ctx, envIdMeta, os.Getenv(cloudEnvIdEnvName)) + ctx = metadata.AppendToOutgoingContext(ctx, orgIdMeta, os.Getenv(cloudOrgIdEnvName)) ag.logger.Infow("initiating streaming connection with Cloud API") // creates a new Stream from the client side. ctx is used for the lifetime of the stream. From 415f433636129597b3dfff90e50bd37922402c21 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 24 Aug 2023 17:13:31 +0300 Subject: [PATCH 09/59] feat: template api model --- api/v1/testkube.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index c78b91862f5..6df82a79127 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -5356,6 +5356,36 @@ components: description: deleted test sources example: ["name7", "name8", "name9"] + Template: + description: Golang based template + type: object + required: + - name + - type + - body + properties: + name: + type: string + description: template name for reference + example: "webhook-template" + type: + $ref: "#/components/schemas/TemplateType" + body: + type: string + description: template body to use + example: "{\"id\": \"{{ .Id }}\"}" + + TemplateType: + description: template type by purpose + type: string + enum: + - job + - container + - cronjob + - scraper + - pvc + - webhook + # # Errors # From 612933f3d01ff3c42ce35478c52b559526b775f4 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 25 Aug 2023 21:25:00 +0300 Subject: [PATCH 10/59] feat: template references --- api/v1/testkube.yaml | 42 +++++++++++++++++++ .../v1/testkube/model_execution_request.go | 14 ++++++- .../model_execution_update_request.go | 14 ++++++- pkg/api/v1/testkube/model_executor.go | 2 + .../testkube/model_executor_update_request.go | 2 + .../testkube/model_executor_upsert_request.go | 2 + pkg/api/v1/testkube/model_template.go | 19 +++++++++ pkg/api/v1/testkube/model_template_type.go | 23 ++++++++++ .../model_test_suite_execution_request.go | 14 +++++++ ...del_test_suite_execution_update_request.go | 14 +++++++ pkg/api/v1/testkube/model_webhook.go | 2 + .../testkube/model_webhook_create_request.go | 2 + .../testkube/model_webhook_update_request.go | 2 + 13 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 pkg/api/v1/testkube/model_template.go create mode 100644 pkg/api/v1/testkube/model_template_type.go diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index 6df82a79127..13c867ef81b 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -4470,9 +4470,15 @@ components: jobTemplate: type: string description: job template extensions + jobTemplateReference: + type: string + description: name of the template resource cronJobTemplate: type: string description: cron job template extensions + cronJobTemplateReference: + type: string + description: name of the template resource contentRequest: $ref: "#/components/schemas/TestContentRequest" description: adjusting parameters for test content @@ -4487,6 +4493,15 @@ components: scraperTemplate: type: string description: scraper template extensions + scraperTemplateReference: + type: string + description: name of the template resource + pvcTemplate: + type: string + description: pvc template extensions + pvcTemplateReference: + type: string + description: name of the template resource envConfigMaps: type: array description: "config map references" @@ -4572,9 +4587,30 @@ components: runningContext: $ref: "#/components/schemas/RunningContext" description: running context for the test suite execution + jobTemplate: + type: string + description: job template extensions + jobTemplateReference: + type: string + description: name of the template resource cronJobTemplate: type: string description: cron job template extensions + cronJobTemplateReference: + type: string + description: name of the template resource + scraperTemplate: + type: string + description: scraper template extensions + scraperTemplateReference: + type: string + description: name of the template resource + pvcTemplate: + type: string + description: pvc template extensions + pvcTemplateReference: + type: string + description: name of the template resource concurrencyLevel: type: integer format: int32 @@ -4739,6 +4775,9 @@ components: jobTemplate: description: Job template to launch executor type: string + jobTemplateReference: + type: string + description: name of the template resource labels: type: object description: "executor labels" @@ -4871,6 +4910,9 @@ components: payloadTemplate: type: string description: golang based template for notification payload + payloadTemplateReference: + type: string + description: name of the template resource headers: type: object description: "webhook headers" diff --git a/pkg/api/v1/testkube/model_execution_request.go b/pkg/api/v1/testkube/model_execution_request.go index 42f90c64806..962cc091c3f 100644 --- a/pkg/api/v1/testkube/model_execution_request.go +++ b/pkg/api/v1/testkube/model_execution_request.go @@ -67,15 +67,25 @@ type ExecutionRequest struct { ArtifactRequest *ArtifactRequest `json:"artifactRequest,omitempty"` // job template extensions JobTemplate string `json:"jobTemplate,omitempty"` + // name of the template resource + JobTemplateReference string `json:"jobTemplateReference,omitempty"` // cron job template extensions - CronJobTemplate string `json:"cronJobTemplate,omitempty"` - ContentRequest *TestContentRequest `json:"contentRequest,omitempty"` + CronJobTemplate string `json:"cronJobTemplate,omitempty"` + // name of the template resource + CronJobTemplateReference string `json:"cronJobTemplateReference,omitempty"` + ContentRequest *TestContentRequest `json:"contentRequest,omitempty"` // script to run before test execution PreRunScript string `json:"preRunScript,omitempty"` // script to run after test execution PostRunScript string `json:"postRunScript,omitempty"` // scraper template extensions ScraperTemplate string `json:"scraperTemplate,omitempty"` + // name of the template resource + ScraperTemplateReference string `json:"scraperTemplateReference,omitempty"` + // pvc template extensions + PvcTemplate string `json:"pvcTemplate,omitempty"` + // name of the template resource + PvcTemplateReference string `json:"pvcTemplateReference,omitempty"` // config map references EnvConfigMaps []EnvReference `json:"envConfigMaps,omitempty"` // secret references diff --git a/pkg/api/v1/testkube/model_execution_update_request.go b/pkg/api/v1/testkube/model_execution_update_request.go index be509827387..27b8401dad8 100644 --- a/pkg/api/v1/testkube/model_execution_update_request.go +++ b/pkg/api/v1/testkube/model_execution_update_request.go @@ -67,15 +67,25 @@ type ExecutionUpdateRequest struct { ArtifactRequest **ArtifactUpdateRequest `json:"artifactRequest,omitempty"` // job template extensions JobTemplate *string `json:"jobTemplate,omitempty"` + // name of the template resource + JobTemplateReference *string `json:"jobTemplateReference,omitempty"` // cron job template extensions - CronJobTemplate *string `json:"cronJobTemplate,omitempty"` - ContentRequest **TestContentUpdateRequest `json:"contentRequest,omitempty"` + CronJobTemplate *string `json:"cronJobTemplate,omitempty"` + // name of the template resource + CronJobTemplateReference *string `json:"cronJobTemplateReference,omitempty"` + ContentRequest **TestContentUpdateRequest `json:"contentRequest,omitempty"` // script to run before test execution PreRunScript *string `json:"preRunScript,omitempty"` // script to run after test execution PostRunScript *string `json:"postRunScript,omitempty"` // scraper template extensions ScraperTemplate *string `json:"scraperTemplate,omitempty"` + // name of the template resource + ScraperTemplateReference *string `json:"scraperTemplateReference,omitempty"` + // pvc template extensions + PvcTemplate *string `json:"pvcTemplate,omitempty"` + // name of the template resource + PvcTemplateReference *string `json:"pvcTemplateReference,omitempty"` // config *map references EnvConfigMaps *[]EnvReference `json:"envConfigMaps,omitempty"` // secret references diff --git a/pkg/api/v1/testkube/model_executor.go b/pkg/api/v1/testkube/model_executor.go index a71b3c00c7f..1b2a1ac4c06 100644 --- a/pkg/api/v1/testkube/model_executor.go +++ b/pkg/api/v1/testkube/model_executor.go @@ -29,6 +29,8 @@ type Executor struct { ContentTypes []string `json:"contentTypes,omitempty"` // Job template to launch executor JobTemplate string `json:"jobTemplate,omitempty"` + // name of the template resource + JobTemplateReference string `json:"jobTemplateReference,omitempty"` // executor labels Labels map[string]string `json:"labels,omitempty"` // Available executor features diff --git a/pkg/api/v1/testkube/model_executor_update_request.go b/pkg/api/v1/testkube/model_executor_update_request.go index 589078bcc08..8223d7aca81 100644 --- a/pkg/api/v1/testkube/model_executor_update_request.go +++ b/pkg/api/v1/testkube/model_executor_update_request.go @@ -33,6 +33,8 @@ type ExecutorUpdateRequest struct { ContentTypes *[]string `json:"contentTypes,omitempty"` // Job template to launch executor JobTemplate *string `json:"jobTemplate,omitempty"` + // name of the template resource + JobTemplateReference *string `json:"jobTemplateReference,omitempty"` // executor labels Labels *map[string]string `json:"labels,omitempty"` // Available executor features diff --git a/pkg/api/v1/testkube/model_executor_upsert_request.go b/pkg/api/v1/testkube/model_executor_upsert_request.go index 8b8fdd6b764..30c14c2801f 100644 --- a/pkg/api/v1/testkube/model_executor_upsert_request.go +++ b/pkg/api/v1/testkube/model_executor_upsert_request.go @@ -33,6 +33,8 @@ type ExecutorUpsertRequest struct { ContentTypes []string `json:"contentTypes,omitempty"` // Job template to launch executor JobTemplate string `json:"jobTemplate,omitempty"` + // name of the template resource + JobTemplateReference string `json:"jobTemplateReference,omitempty"` // executor labels Labels map[string]string `json:"labels,omitempty"` // Available executor features diff --git a/pkg/api/v1/testkube/model_template.go b/pkg/api/v1/testkube/model_template.go new file mode 100644 index 00000000000..f43449dddfa --- /dev/null +++ b/pkg/api/v1/testkube/model_template.go @@ -0,0 +1,19 @@ +/* + * 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 + +// Golang based template +type Template struct { + // template name for reference + Name string `json:"name"` + Type_ *TemplateType `json:"type"` + // template body to use + Body string `json:"body"` +} diff --git a/pkg/api/v1/testkube/model_template_type.go b/pkg/api/v1/testkube/model_template_type.go new file mode 100644 index 00000000000..1b715a57edc --- /dev/null +++ b/pkg/api/v1/testkube/model_template_type.go @@ -0,0 +1,23 @@ +/* + * 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 + +// TemplateType : template type by purpose +type TemplateType string + +// List of TemplateType +const ( + JOB_TemplateType TemplateType = "job" + CONTAINER_TemplateType TemplateType = "container" + CRONJOB_TemplateType TemplateType = "cronjob" + SCRAPER_TemplateType TemplateType = "scraper" + PVC_TemplateType TemplateType = "pvc" + WEBHOOK_TemplateType TemplateType = "webhook" +) 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 fb732540da6..4cbdb6dcdf3 100644 --- a/pkg/api/v1/testkube/model_test_suite_execution_request.go +++ b/pkg/api/v1/testkube/model_test_suite_execution_request.go @@ -34,8 +34,22 @@ type TestSuiteExecutionRequest struct { Timeout int32 `json:"timeout,omitempty"` ContentRequest *TestContentRequest `json:"contentRequest,omitempty"` RunningContext *RunningContext `json:"runningContext,omitempty"` + // job template extensions + JobTemplate string `json:"jobTemplate,omitempty"` + // name of the template resource + JobTemplateReference string `json:"jobTemplateReference,omitempty"` // cron job template extensions CronJobTemplate string `json:"cronJobTemplate,omitempty"` + // name of the template resource + CronJobTemplateReference string `json:"cronJobTemplateReference,omitempty"` + // scraper template extensions + ScraperTemplate string `json:"scraperTemplate,omitempty"` + // name of the template resource + ScraperTemplateReference string `json:"scraperTemplateReference,omitempty"` + // pvc template extensions + PvcTemplate string `json:"pvcTemplate,omitempty"` + // name of the template resource + PvcTemplateReference string `json:"pvcTemplateReference,omitempty"` // number of tests run in parallel ConcurrencyLevel int32 `json:"concurrencyLevel,omitempty"` // test suite execution name started the test suite execution 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 82873ef584e..10a7a8e0796 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 @@ -34,8 +34,22 @@ type TestSuiteExecutionUpdateRequest struct { Timeout *int32 `json:"timeout,omitempty"` ContentRequest **TestContentUpdateRequest `json:"contentRequest,omitempty"` RunningContext *RunningContext `json:"runningContext,omitempty"` + // job template extensions + JobTemplate *string `json:"jobTemplate,omitempty"` + // name of the template resource + JobTemplateReference *string `json:"jobTemplateReference,omitempty"` // cron job template extensions CronJobTemplate *string `json:"cronJobTemplate,omitempty"` + // name of the template resource + CronJobTemplateReference *string `json:"cronJobTemplateReference,omitempty"` + // scraper template extensions + ScraperTemplate *string `json:"scraperTemplate,omitempty"` + // name of the template resource + ScraperTemplateReference *string `json:"scraperTemplateReference,omitempty"` + // pvc template extensions + PvcTemplate *string `json:"pvcTemplate,omitempty"` + // name of the template resource + PvcTemplateReference *string `json:"pvcTemplateReference,omitempty"` // number of tests run in parallel ConcurrencyLevel *int32 `json:"concurrencyLevel,omitempty"` // test suite execution name started the test suite execution diff --git a/pkg/api/v1/testkube/model_webhook.go b/pkg/api/v1/testkube/model_webhook.go index 87472bcde21..46c46d0966b 100644 --- a/pkg/api/v1/testkube/model_webhook.go +++ b/pkg/api/v1/testkube/model_webhook.go @@ -21,6 +21,8 @@ type Webhook struct { PayloadObjectField string `json:"payloadObjectField,omitempty"` // golang based template for notification payload PayloadTemplate string `json:"payloadTemplate,omitempty"` + // name of the template resource + PayloadTemplateReference string `json:"payloadTemplateReference,omitempty"` // webhook headers Headers map[string]string `json:"headers,omitempty"` // webhook labels diff --git a/pkg/api/v1/testkube/model_webhook_create_request.go b/pkg/api/v1/testkube/model_webhook_create_request.go index cad406d497d..8ed5a43869a 100644 --- a/pkg/api/v1/testkube/model_webhook_create_request.go +++ b/pkg/api/v1/testkube/model_webhook_create_request.go @@ -21,6 +21,8 @@ type WebhookCreateRequest struct { PayloadObjectField string `json:"payloadObjectField,omitempty"` // golang based template for notification payload PayloadTemplate string `json:"payloadTemplate,omitempty"` + // name of the template resource + PayloadTemplateReference string `json:"payloadTemplateReference,omitempty"` // webhook headers Headers map[string]string `json:"headers,omitempty"` // webhook labels diff --git a/pkg/api/v1/testkube/model_webhook_update_request.go b/pkg/api/v1/testkube/model_webhook_update_request.go index dc65e7c4a4f..c63b3fd46d5 100644 --- a/pkg/api/v1/testkube/model_webhook_update_request.go +++ b/pkg/api/v1/testkube/model_webhook_update_request.go @@ -21,6 +21,8 @@ type WebhookUpdateRequest struct { PayloadObjectField *string `json:"payloadObjectField,omitempty"` // golang based template for notification payload PayloadTemplate *string `json:"payloadTemplate,omitempty"` + // name of the template resource + PayloadTemplateReference *string `json:"payloadTemplateReference,omitempty"` // webhook headers Headers *map[string]string `json:"headers,omitempty"` // webhook labels From 6adad37e4180bcc614877987a135fe28b4dd2301 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 29 Aug 2023 20:25:16 +0300 Subject: [PATCH 11/59] feat: api methods for templates --- api/v1/testkube.yaml | 25 +++ cmd/api-server/main.go | 5 + docs/docs/articles/crds.md | 1 + go.mod | 2 +- go.sum | 4 +- internal/app/api/v1/server.go | 4 + internal/app/api/v1/template.go | 192 ++++++++++++++++++ pkg/api/v1/testkube/model_template.go | 8 +- .../testkube/model_template_create_request.go | 23 +++ .../testkube/model_template_update_request.go | 23 +++ pkg/crd/crd.go | 6 +- pkg/crd/templates/template.tmpl | 19 ++ pkg/executor/client/job.go | 4 + .../containerexecutor/containerexecutor.go | 4 + pkg/mapper/executors/mapper.go | 85 ++++---- pkg/mapper/templates/mapper.go | 101 +++++++++ pkg/mapper/tests/kube_openapi.go | 85 +++++--- pkg/mapper/tests/openapi_kube.go | 85 +++++--- pkg/mapper/testsuites/kube_openapi.go | 57 +++++- pkg/mapper/webhooks/mapper.go | 40 ++-- 20 files changed, 644 insertions(+), 129 deletions(-) create mode 100644 internal/app/api/v1/template.go create mode 100644 pkg/api/v1/testkube/model_template_create_request.go create mode 100644 pkg/api/v1/testkube/model_template_update_request.go create mode 100644 pkg/crd/templates/template.tmpl create mode 100644 pkg/mapper/templates/mapper.go diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index 13c867ef81b..b24a41be2c5 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -5410,12 +5410,24 @@ components: type: string description: template name for reference example: "webhook-template" + namespace: + type: string + description: template namespace + example: "testkube" type: $ref: "#/components/schemas/TemplateType" body: type: string description: template body to use example: "{\"id\": \"{{ .Id }}\"}" + labels: + type: object + description: "template labels" + additionalProperties: + type: string + example: + env: "prod" + app: "backend" TemplateType: description: template type by purpose @@ -5428,6 +5440,19 @@ components: - pvc - webhook + TemplateCreateRequest: + description: template create request body + type: object + allOf: + - $ref: "#/components/schemas/Template" + + TemplateUpdateRequest: + description: template update request body + type: object + nullable: true + allOf: + - $ref: "#/components/schemas/Template" + # # Errors # diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 517b4175587..e1d17a3f790 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -61,6 +61,7 @@ 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" + templatesclientv1 "github.com/kubeshop/testkube-operator/client/templates/v1" 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" @@ -174,6 +175,7 @@ func main() { testsourcesClient := testsourcesclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) testExecutionsClient := testexecutionsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) testsuiteExecutionsClient := testsuiteexecutionsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) + templatesClient := templatesclientv1.NewClient(kubeClient, cfg.TestkubeNamespace) clientset, err := k8sclient.ConnectToK8s() if err != nil { @@ -342,6 +344,7 @@ func main() { testsClientV3, clientset, testExecutionsClient, + templatesClient, cfg.TestkubeRegistry, cfg.TestkubePodStartTimeout, clusterId, @@ -367,6 +370,7 @@ func main() { executorsClient, testsClientV3, testExecutionsClient, + templatesClient, cfg.TestkubeRegistry, cfg.TestkubePodStartTimeout, clusterId, @@ -422,6 +426,7 @@ func main() { storageClient, cfg.GraphqlPort, artifactStorage, + templatesClient, cfg.CDEventsTarget, cfg.TestkubeDashboardURI, cfg.TestkubeHelmchartVersion, diff --git a/docs/docs/articles/crds.md b/docs/docs/articles/crds.md index d4295c783c4..dd83e1f99b1 100644 --- a/docs/docs/articles/crds.md +++ b/docs/docs/articles/crds.md @@ -12,6 +12,7 @@ kubectl get crds -n testkube NAME CREATED AT executors.executor.testkube.io 2023-06-15T14:49:11Z scripts.tests.testkube.io 2023-06-15T14:49:11Z +templates.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 diff --git a/go.mod b/go.mod index 13621e15bda..4b788ba22de 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.20230824144544-8fda6043174c + github.com/kubeshop/testkube-operator v1.10.8-0.20230829170322-3e88740c6e7d github.com/minio/minio-go/v7 v7.0.47 github.com/montanaflynn/stats v0.6.6 github.com/moogar0880/problems v0.1.1 diff --git a/go.sum b/go.sum index fdbf611d206..c2e2001a0cb 100644 --- a/go.sum +++ b/go.sum @@ -393,8 +393,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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.20230824144544-8fda6043174c h1:/b4Jo/tWoc7EqutFMWpVwoWel5Y8ToQTf0NyXG00xAQ= -github.com/kubeshop/testkube-operator v1.10.8-0.20230824144544-8fda6043174c/go.mod h1:UmigDOKMVJa6Y/imKafWmhcvfLisOzr5X04kuOsH/B0= +github.com/kubeshop/testkube-operator v1.10.8-0.20230829170322-3e88740c6e7d h1:527VCINS75vdhlpxGsaL/c5TrEMftNtWNs0xGKFEfhA= +github.com/kubeshop/testkube-operator v1.10.8-0.20230829170322-3e88740c6e7d/go.mod h1:UmigDOKMVJa6Y/imKafWmhcvfLisOzr5X04kuOsH/B0= 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= diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go index c5767902be7..52a1c7f4323 100644 --- a/internal/app/api/v1/server.go +++ b/internal/app/api/v1/server.go @@ -30,6 +30,7 @@ import ( "github.com/kelseyhightower/envconfig" executorsclientv1 "github.com/kubeshop/testkube-operator/client/executors/v1" + templatesclientv1 "github.com/kubeshop/testkube-operator/client/templates/v1" testsclientv3 "github.com/kubeshop/testkube-operator/client/tests/v3" testsourcesclientv1 "github.com/kubeshop/testkube-operator/client/testsources/v1" testsuitesclientv3 "github.com/kubeshop/testkube-operator/client/testsuites/v3" @@ -79,6 +80,7 @@ func NewTestkubeAPI( storage storage.Client, graphqlPort string, artifactsStorage storage.ArtifactsStorage, + templatesClient *templatesclientv1.TemplatesClient, cdeventsTarget string, dashboardURI string, helmchartVersion string, @@ -122,6 +124,7 @@ func NewTestkubeAPI( Storage: storage, graphqlPort: graphqlPort, artifactsStorage: artifactsStorage, + TemplatesClient: templatesClient, helmchartVersion: helmchartVersion, mode: mode, } @@ -175,6 +178,7 @@ type TestkubeAPI struct { slackLoader *slack.SlackLoader graphqlPort string artifactsStorage storage.ArtifactsStorage + TemplatesClient *templatesclientv1.TemplatesClient helmchartVersion string mode string } diff --git a/internal/app/api/v1/template.go b/internal/app/api/v1/template.go new file mode 100644 index 00000000000..1f0b7d43399 --- /dev/null +++ b/internal/app/api/v1/template.go @@ -0,0 +1,192 @@ +package v1 + +import ( + "bytes" + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/yaml" + + templatev1 "github.com/kubeshop/testkube-operator/apis/template/v1" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" + "github.com/kubeshop/testkube/pkg/crd" + templatesmapper "github.com/kubeshop/testkube/pkg/mapper/templates" +) + +func (s TestkubeAPI) CreateTemplateHandler() fiber.Handler { + return func(c *fiber.Ctx) error { + errPrefix := "failed to create template" + var template templatev1.Template + if string(c.Request().Header.ContentType()) == mediaTypeYAML { + templateSpec := string(c.Body()) + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(templateSpec), len(templateSpec)) + if err := decoder.Decode(&template); err != nil { + return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err)) + } + } else { + var request testkube.TemplateCreateRequest + err := c.BodyParser(&request) + if err != nil { + return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse json request: %w", errPrefix, err)) + } + + if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { + if request.Body != "" { + request.Body = fmt.Sprintf("%q", request.Body) + } + + data, err := crd.GenerateYAML(crd.TemplateTemplate, []testkube.TemplateCreateRequest{request}) + return s.getCRDs(c, data, err) + } + + template = templatesmapper.MapAPIToCRD(request) + template.Namespace = s.Namespace + } + + created, err := s.TemplatesClient.Create(&template) + if err != nil { + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not create template: %w", errPrefix, err)) + } + + c.Status(http.StatusCreated) + return c.JSON(created) + } +} + +func (s TestkubeAPI) UpdateTemplateHandler() fiber.Handler { + return func(c *fiber.Ctx) error { + errPrefix := "failed to update template" + var request testkube.TemplateUpdateRequest + if string(c.Request().Header.ContentType()) == mediaTypeYAML { + var template templatev1.Template + templateSpec := string(c.Body()) + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(templateSpec), len(templateSpec)) + if err := decoder.Decode(&template); err != nil { + return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err)) + } + + request = templatesmapper.MapSpecToUpdate(&template) + } else { + err := c.BodyParser(&request) + if err != nil { + return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse json request: %w", errPrefix, err)) + } + } + + var name string + if request.Name != nil { + name = *request.Name + } + errPrefix = errPrefix + " " + name + // we need to get resource first and load its metadata.ResourceVersion + template, err := s.TemplatesClient.Get(name) + if err != nil { + if errors.IsNotFound(err) { + return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: client found no template: %w", errPrefix, err)) + } + + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get template: %w", errPrefix, err)) + } + + // map update template but load spec only to not override metadata.ResourceVersion + templateSpec := templatesmapper.MapUpdateToSpec(request, template) + + updatedTemplate, err := s.TemplatesClient.Update(templateSpec) + if err != nil { + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not update template: %w", errPrefix, err)) + } + + return c.JSON(updatedTemplate) + } +} + +func (s TestkubeAPI) ListTemplatesHandler() fiber.Handler { + return func(c *fiber.Ctx) error { + errPrefix := "failed to list templates" + + list, err := s.TemplatesClient.List(c.Query("selector")) + if err != nil { + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list templates: %w", errPrefix, err)) + } + + results := []testkube.Template{} + for _, item := range list.Items { + results = append(results, templatesmapper.MapCRDToAPI(item)) + + } + + if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { + for i := range results { + if results[i].Body != "" { + results[i].Body = fmt.Sprintf("%q", results[i].Body) + } + } + + data, err := crd.GenerateYAML(crd.TemplateTemplate, results) + return s.getCRDs(c, data, err) + } + + return c.JSON(results) + } +} + +func (s TestkubeAPI) GetTemplateHandler() fiber.Handler { + return func(c *fiber.Ctx) error { + name := c.Params("name") + errPrefix := fmt.Sprintf("failed to get template %s", name) + + item, err := s.TemplatesClient.Get(name) + if err != nil { + if errors.IsNotFound(err) { + return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: template not found: %w", errPrefix, err)) + } + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get template: %w", errPrefix, err)) + } + + result := templatesmapper.MapCRDToAPI(*item) + if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { + if result.Body != "" { + result.Body = fmt.Sprintf("%q", result.Body) + } + + data, err := crd.GenerateYAML(crd.TemplateTemplate, []testkube.Template{result}) + return s.getCRDs(c, data, err) + } + + return c.JSON(result) + } +} + +func (s TestkubeAPI) DeleteTemplateHandler() fiber.Handler { + return func(c *fiber.Ctx) error { + name := c.Params("name") + errPrefix := fmt.Sprintf("failed to delete template %s", name) + + err := s.TemplatesClient.Delete(name) + if err != nil { + if errors.IsNotFound(err) { + return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: template not found: %w", errPrefix, err)) + } + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete template: %w", errPrefix, err)) + } + + c.Status(http.StatusNoContent) + return nil + } +} + +func (s TestkubeAPI) DeleteTemplatesHandler() fiber.Handler { + return func(c *fiber.Ctx) error { + errPrefix := "failed to delete templates" + + err := s.TemplatesClient.DeleteByLabels(c.Query("selector")) + if err != nil { + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete templates: %w", errPrefix, err)) + } + + c.Status(http.StatusNoContent) + return nil + } +} diff --git a/pkg/api/v1/testkube/model_template.go b/pkg/api/v1/testkube/model_template.go index f43449dddfa..7f922b9ab60 100644 --- a/pkg/api/v1/testkube/model_template.go +++ b/pkg/api/v1/testkube/model_template.go @@ -12,8 +12,12 @@ package testkube // Golang based template type Template struct { // template name for reference - Name string `json:"name"` - Type_ *TemplateType `json:"type"` + Name string `json:"name"` + // template namespace + Namespace string `json:"namespace,omitempty"` + Type_ *TemplateType `json:"type"` // template body to use Body string `json:"body"` + // template labels + Labels map[string]string `json:"labels,omitempty"` } diff --git a/pkg/api/v1/testkube/model_template_create_request.go b/pkg/api/v1/testkube/model_template_create_request.go new file mode 100644 index 00000000000..fad9cc071cb --- /dev/null +++ b/pkg/api/v1/testkube/model_template_create_request.go @@ -0,0 +1,23 @@ +/* + * 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 + +// template create request body +type TemplateCreateRequest struct { + // template name for reference + Name string `json:"name"` + // template namespace + Namespace string `json:"namespace,omitempty"` + Type_ *TemplateType `json:"type"` + // template body to use + Body string `json:"body"` + // template labels + Labels map[string]string `json:"labels,omitempty"` +} diff --git a/pkg/api/v1/testkube/model_template_update_request.go b/pkg/api/v1/testkube/model_template_update_request.go new file mode 100644 index 00000000000..77cae63a20b --- /dev/null +++ b/pkg/api/v1/testkube/model_template_update_request.go @@ -0,0 +1,23 @@ +/* + * 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 + +// template update request body +type TemplateUpdateRequest struct { + // template name for reference + Name *string `json:"name"` + // template namespace + Namespace *string `json:"namespace,omitempty"` + Type_ *TemplateType `json:"type"` + // template body to use + Body *string `json:"body"` + // template labels + Labels *map[string]string `json:"labels,omitempty"` +} diff --git a/pkg/crd/crd.go b/pkg/crd/crd.go index ec97268569f..28630e45620 100644 --- a/pkg/crd/crd.go +++ b/pkg/crd/crd.go @@ -28,6 +28,8 @@ const ( TemplateTestTrigger Template = "testtrigger" // TemplateTestSource is test source crd template TemplateTestSource Template = "testsource" + // TemplateTemplate is template crd template + TemplateTemplate Template = "template" ) // Gettable is an interface of gettable objects @@ -42,7 +44,9 @@ type Gettable interface { testkube.TestTrigger | testkube.TestTriggerUpsertRequest | testkube.TestSource | - testkube.TestSourceUpsertRequest + testkube.TestSourceUpsertRequest | + testkube.Template | + testkube.TemplateCreateRequest } // ExecuteTemplate executes crd template diff --git a/pkg/crd/templates/template.tmpl b/pkg/crd/templates/template.tmpl new file mode 100644 index 00000000000..36c3b1cd386 --- /dev/null +++ b/pkg/crd/templates/template.tmpl @@ -0,0 +1,19 @@ +apiVersion: tests.testkube.io/v1 +kind: Template +metadata: + name: {{ .Name }} + namespace: {{ .Namespace }} + {{- if ne (len .Labels) 0 }} + labels: + {{- range $key, $value := .Labels }} + {{ $key }}: {{ $value }} + {{- end }} + {{- end }} +spec: + {{- if .Type_ }} + type: {{ .Type_ }} + {{- end }} + {{- if .Body }} + body: {{ .Body }} + {{- end }} + diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go index b2ec1fe5717..5cdcaf5a443 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" + templatesv1 "github.com/kubeshop/testkube-operator/client/templates/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" @@ -79,6 +80,7 @@ func NewJobExecutor( testsClient testsv3.Interface, clientset kubernetes.Interface, testExecutionsClient testexecutionsv1.Interface, + templatesClient templatesv1.Interface, registry string, podStartTimeout time.Duration, clusterID string, @@ -96,6 +98,7 @@ func NewJobExecutor( configMap: configMap, testsClient: testsClient, testExecutionsClient: testExecutionsClient, + templatesClient: templatesClient, registry: registry, podStartTimeout: podStartTimeout, clusterID: clusterID, @@ -121,6 +124,7 @@ type JobExecutor struct { configMap config.Repository testsClient testsv3.Interface testExecutionsClient testexecutionsv1.Interface + templatesClient templatesv1.Interface registry string podStartTimeout time.Duration clusterID string diff --git a/pkg/executor/containerexecutor/containerexecutor.go b/pkg/executor/containerexecutor/containerexecutor.go index 5b6c3e15b50..344748e6d43 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" + templatesv1 "github.com/kubeshop/testkube-operator/client/templates/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" @@ -61,6 +62,7 @@ func NewContainerExecutor( executorsClient executorsclientv1.Interface, testsClient testsv3.Interface, testExecutionsClient testexecutionsv1.Interface, + templatesClient templatesv1.Interface, registry string, podStartTimeout time.Duration, clusterID string, @@ -84,6 +86,7 @@ func NewContainerExecutor( testsClient: testsClient, executorsClient: executorsClient, testExecutionsClient: testExecutionsClient, + templatesClient: templatesClient, registry: registry, podStartTimeout: podStartTimeout, clusterID: clusterID, @@ -109,6 +112,7 @@ type ContainerExecutor struct { testsClient testsv3.Interface executorsClient executorsclientv1.Interface testExecutionsClient testexecutionsv1.Interface + templatesClient templatesv1.Interface registry string podStartTimeout time.Duration clusterID string diff --git a/pkg/mapper/executors/mapper.go b/pkg/mapper/executors/mapper.go index 33c9f500e9e..3ab675f54e7 100644 --- a/pkg/mapper/executors/mapper.go +++ b/pkg/mapper/executors/mapper.go @@ -11,20 +11,21 @@ import ( // MapCRDToAPI maps Executor CRD to OpenAPI spec Executor func MapCRDToAPI(item executorv1.Executor) testkube.ExecutorUpsertRequest { return testkube.ExecutorUpsertRequest{ - Name: item.Name, - Namespace: item.Namespace, - Labels: item.Labels, - ExecutorType: string(item.Spec.ExecutorType), - Types: item.Spec.Types, - Uri: item.Spec.URI, - Image: item.Spec.Image, - ImagePullSecrets: mapImagePullSecretsToAPI(item.Spec.ImagePullSecrets), - Command: item.Spec.Command, - Args: item.Spec.Args, - JobTemplate: item.Spec.JobTemplate, - Features: MapFeaturesToAPI(item.Spec.Features), - ContentTypes: MapContentTypesToAPI(item.Spec.ContentTypes), - Meta: MapMetaToAPI(item.Spec.Meta), + Name: item.Name, + Namespace: item.Namespace, + Labels: item.Labels, + ExecutorType: string(item.Spec.ExecutorType), + Types: item.Spec.Types, + Uri: item.Spec.URI, + Image: item.Spec.Image, + ImagePullSecrets: mapImagePullSecretsToAPI(item.Spec.ImagePullSecrets), + Command: item.Spec.Command, + Args: item.Spec.Args, + JobTemplate: item.Spec.JobTemplate, + JobTemplateReference: item.Spec.JobTemplateReference, + Features: MapFeaturesToAPI(item.Spec.Features), + ContentTypes: MapContentTypesToAPI(item.Spec.ContentTypes), + Meta: MapMetaToAPI(item.Spec.Meta), } } @@ -37,17 +38,18 @@ func MapAPIToCRD(request testkube.ExecutorUpsertRequest) executorv1.Executor { Labels: request.Labels, }, Spec: executorv1.ExecutorSpec{ - ExecutorType: executorv1.ExecutorType(request.ExecutorType), - Types: request.Types, - URI: request.Uri, - Image: request.Image, - ImagePullSecrets: mapImagePullSecretsToCRD(request.ImagePullSecrets), - Command: request.Command, - Args: request.Args, - JobTemplate: request.JobTemplate, - Features: MapFeaturesToCRD(request.Features), - ContentTypes: MapContentTypesToCRD(request.ContentTypes), - Meta: MapMetaToCRD(request.Meta), + ExecutorType: executorv1.ExecutorType(request.ExecutorType), + Types: request.Types, + URI: request.Uri, + Image: request.Image, + ImagePullSecrets: mapImagePullSecretsToCRD(request.ImagePullSecrets), + Command: request.Command, + Args: request.Args, + JobTemplate: request.JobTemplate, + JobTemplateReference: request.JobTemplateReference, + Features: MapFeaturesToCRD(request.Features), + ContentTypes: MapContentTypesToCRD(request.ContentTypes), + Meta: MapMetaToCRD(request.Meta), }, } } @@ -57,18 +59,19 @@ func MapExecutorCRDToExecutorDetails(item executorv1.Executor) testkube.Executor return testkube.ExecutorDetails{ Name: item.Name, Executor: &testkube.Executor{ - ExecutorType: string(item.Spec.ExecutorType), - Image: item.Spec.Image, - ImagePullSecrets: mapImagePullSecretsToAPI(item.Spec.ImagePullSecrets), - Command: item.Spec.Command, - Args: item.Spec.Args, - Types: item.Spec.Types, - Uri: item.Spec.URI, - JobTemplate: item.Spec.JobTemplate, - Labels: item.Labels, - Features: MapFeaturesToAPI(item.Spec.Features), - ContentTypes: MapContentTypesToAPI(item.Spec.ContentTypes), - Meta: MapMetaToAPI(item.Spec.Meta), + ExecutorType: string(item.Spec.ExecutorType), + Image: item.Spec.Image, + ImagePullSecrets: mapImagePullSecretsToAPI(item.Spec.ImagePullSecrets), + Command: item.Spec.Command, + Args: item.Spec.Args, + Types: item.Spec.Types, + Uri: item.Spec.URI, + JobTemplate: item.Spec.JobTemplate, + JobTemplateReference: item.Spec.JobTemplateReference, + Labels: item.Labels, + Features: MapFeaturesToAPI(item.Spec.Features), + ContentTypes: MapContentTypesToAPI(item.Spec.ContentTypes), + Meta: MapMetaToAPI(item.Spec.Meta), }, } } @@ -167,6 +170,10 @@ func MapUpdateToSpec(request testkube.ExecutorUpdateRequest, executor *executorv request.JobTemplate, &executor.Spec.JobTemplate, }, + { + request.JobTemplateReference, + &executor.Spec.JobTemplateReference, + }, } for _, field := range fields { @@ -276,6 +283,10 @@ func MapSpecToUpdate(executor *executorv1.Executor) (request testkube.ExecutorUp &executor.Spec.JobTemplate, &request.JobTemplate, }, + { + &executor.Spec.JobTemplateReference, + &request.JobTemplateReference, + }, } for _, field := range fields { diff --git a/pkg/mapper/templates/mapper.go b/pkg/mapper/templates/mapper.go new file mode 100644 index 00000000000..a2f7444a550 --- /dev/null +++ b/pkg/mapper/templates/mapper.go @@ -0,0 +1,101 @@ +package templates + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + templatev1 "github.com/kubeshop/testkube-operator/apis/template/v1" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +// MapCRDToAPI maps Template CRD to OpenAPI spec Template +func MapCRDToAPI(item templatev1.Template) testkube.Template { + return testkube.Template{ + Name: item.Name, + Namespace: item.Namespace, + Body: item.Spec.Body, + Type_: (*testkube.TemplateType)(item.Spec.Type_), + Labels: item.Labels, + } +} + +// MapAPIToCRD maps OpenAPI spec TemplateCreateRequest to CRD Template +func MapAPIToCRD(request testkube.TemplateCreateRequest) templatev1.Template { + return templatev1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: request.Name, + Namespace: request.Namespace, + Labels: request.Labels, + }, + Spec: templatev1.TemplateSpec{ + Type_: (*templatev1.TemplateType)(request.Type_), + Body: request.Body, + }, + } +} + +// MapUpdateToSpec maps TemplateUpdateRequest to Wehook CRD spec +func MapUpdateToSpec(request testkube.TemplateUpdateRequest, template *templatev1.Template) *templatev1.Template { + var fields = []struct { + source *string + destination *string + }{ + { + request.Name, + &template.Name, + }, + { + request.Namespace, + &template.Namespace, + }, + { + request.Body, + &template.Spec.Body, + }, + } + + for _, field := range fields { + if field.source != nil { + *field.destination = *field.source + } + } + + if request.Type_ != nil { + *template.Spec.Type_ = (templatev1.TemplateType)(*request.Type_) + } + + if request.Labels != nil { + template.Labels = *request.Labels + } + + return template +} + +// MapSpecToUpdate maps Template CRD to TemplateUpdate Request to spec +func MapSpecToUpdate(template *templatev1.Template) (request testkube.TemplateUpdateRequest) { + var fields = []struct { + source *string + destination **string + }{ + { + &template.Name, + &request.Name, + }, + { + &template.Namespace, + &request.Namespace, + }, + { + &template.Spec.Body, + &request.Body, + }, + } + + for _, field := range fields { + *field.destination = field.source + } + + request.Type_ = (*testkube.TemplateType)(template.Spec.Type_) + request.Labels = &template.Labels + + return request +} diff --git a/pkg/mapper/tests/kube_openapi.go b/pkg/mapper/tests/kube_openapi.go index 1f965a6af0f..c16ddf3512b 100644 --- a/pkg/mapper/tests/kube_openapi.go +++ b/pkg/mapper/tests/kube_openapi.go @@ -128,36 +128,41 @@ func MapExecutionRequestFromSpec(specExecutionRequest *testsv3.ExecutionRequest) } return &testkube.ExecutionRequest{ - Name: specExecutionRequest.Name, - TestSuiteName: specExecutionRequest.TestSuiteName, - Number: specExecutionRequest.Number, - ExecutionLabels: specExecutionRequest.ExecutionLabels, - Namespace: specExecutionRequest.Namespace, - IsVariablesFileUploaded: specExecutionRequest.IsVariablesFileUploaded, - VariablesFile: specExecutionRequest.VariablesFile, - Variables: MergeVariablesAndParams(specExecutionRequest.Variables, nil), - TestSecretUUID: specExecutionRequest.TestSecretUUID, - TestSuiteSecretUUID: specExecutionRequest.TestSuiteSecretUUID, - Command: specExecutionRequest.Command, - Args: specExecutionRequest.Args, - ArgsMode: string(specExecutionRequest.ArgsMode), - Image: specExecutionRequest.Image, - ImagePullSecrets: MapImagePullSecrets(specExecutionRequest.ImagePullSecrets), - Envs: specExecutionRequest.Envs, - SecretEnvs: specExecutionRequest.SecretEnvs, - Sync: specExecutionRequest.Sync, - HttpProxy: specExecutionRequest.HttpProxy, - HttpsProxy: specExecutionRequest.HttpsProxy, - ActiveDeadlineSeconds: specExecutionRequest.ActiveDeadlineSeconds, - ArtifactRequest: artifactRequest, - JobTemplate: specExecutionRequest.JobTemplate, - CronJobTemplate: specExecutionRequest.CronJobTemplate, - PreRunScript: specExecutionRequest.PreRunScript, - PostRunScript: specExecutionRequest.PostRunScript, - ScraperTemplate: specExecutionRequest.ScraperTemplate, - NegativeTest: specExecutionRequest.NegativeTest, - EnvConfigMaps: MapEnvReferences(specExecutionRequest.EnvConfigMaps), - EnvSecrets: MapEnvReferences(specExecutionRequest.EnvSecrets), + Name: specExecutionRequest.Name, + TestSuiteName: specExecutionRequest.TestSuiteName, + Number: specExecutionRequest.Number, + ExecutionLabels: specExecutionRequest.ExecutionLabels, + Namespace: specExecutionRequest.Namespace, + IsVariablesFileUploaded: specExecutionRequest.IsVariablesFileUploaded, + VariablesFile: specExecutionRequest.VariablesFile, + Variables: MergeVariablesAndParams(specExecutionRequest.Variables, nil), + TestSecretUUID: specExecutionRequest.TestSecretUUID, + TestSuiteSecretUUID: specExecutionRequest.TestSuiteSecretUUID, + Command: specExecutionRequest.Command, + Args: specExecutionRequest.Args, + ArgsMode: string(specExecutionRequest.ArgsMode), + Image: specExecutionRequest.Image, + ImagePullSecrets: MapImagePullSecrets(specExecutionRequest.ImagePullSecrets), + Envs: specExecutionRequest.Envs, + SecretEnvs: specExecutionRequest.SecretEnvs, + Sync: specExecutionRequest.Sync, + HttpProxy: specExecutionRequest.HttpProxy, + HttpsProxy: specExecutionRequest.HttpsProxy, + ActiveDeadlineSeconds: specExecutionRequest.ActiveDeadlineSeconds, + ArtifactRequest: artifactRequest, + JobTemplate: specExecutionRequest.JobTemplate, + JobTemplateReference: specExecutionRequest.JobTemplateReference, + CronJobTemplate: specExecutionRequest.CronJobTemplate, + CronJobTemplateReference: specExecutionRequest.CronJobTemplateReference, + PreRunScript: specExecutionRequest.PreRunScript, + PostRunScript: specExecutionRequest.PostRunScript, + PvcTemplate: specExecutionRequest.PvcTemplate, + PvcTemplateReference: specExecutionRequest.PvcTemplateReference, + ScraperTemplate: specExecutionRequest.ScraperTemplate, + ScraperTemplateReference: specExecutionRequest.ScraperTemplateReference, + NegativeTest: specExecutionRequest.NegativeTest, + EnvConfigMaps: MapEnvReferences(specExecutionRequest.EnvConfigMaps), + EnvSecrets: MapEnvReferences(specExecutionRequest.EnvSecrets), } } @@ -403,6 +408,10 @@ func MapSpecExecutionRequestToExecutionUpdateRequest( &request.JobTemplate, &executionRequest.JobTemplate, }, + { + &request.JobTemplateReference, + &executionRequest.JobTemplateReference, + }, { &request.PreRunScript, &executionRequest.PreRunScript, @@ -415,10 +424,26 @@ func MapSpecExecutionRequestToExecutionUpdateRequest( &request.CronJobTemplate, &executionRequest.CronJobTemplate, }, + { + &request.CronJobTemplateReference, + &executionRequest.CronJobTemplateReference, + }, + { + &request.PvcTemplate, + &executionRequest.PvcTemplate, + }, + { + &request.PvcTemplateReference, + &executionRequest.PvcTemplateReference, + }, { &request.ScraperTemplate, &executionRequest.ScraperTemplate, }, + { + &request.ScraperTemplateReference, + &executionRequest.ScraperTemplateReference, + }, } for _, field := range fields { diff --git a/pkg/mapper/tests/openapi_kube.go b/pkg/mapper/tests/openapi_kube.go index 37a8468c0da..3be14cfe786 100644 --- a/pkg/mapper/tests/openapi_kube.go +++ b/pkg/mapper/tests/openapi_kube.go @@ -140,36 +140,41 @@ func MapExecutionRequestToSpecExecutionRequest(executionRequest *testkube.Execut } return &testsv3.ExecutionRequest{ - Name: executionRequest.Name, - TestSuiteName: executionRequest.TestSuiteName, - Number: executionRequest.Number, - ExecutionLabels: executionRequest.ExecutionLabels, - Namespace: executionRequest.Namespace, - IsVariablesFileUploaded: executionRequest.IsVariablesFileUploaded, - VariablesFile: executionRequest.VariablesFile, - Variables: MapCRDVariables(executionRequest.Variables), - TestSecretUUID: executionRequest.TestSecretUUID, - TestSuiteSecretUUID: executionRequest.TestSuiteSecretUUID, - Args: executionRequest.Args, - ArgsMode: testsv3.ArgsModeType(executionRequest.ArgsMode), - Envs: executionRequest.Envs, - SecretEnvs: executionRequest.SecretEnvs, - Sync: executionRequest.Sync, - HttpProxy: executionRequest.HttpProxy, - HttpsProxy: executionRequest.HttpsProxy, - Image: executionRequest.Image, - ImagePullSecrets: mapImagePullSecrets(executionRequest.ImagePullSecrets), - ActiveDeadlineSeconds: executionRequest.ActiveDeadlineSeconds, - Command: executionRequest.Command, - ArtifactRequest: artifactRequest, - JobTemplate: executionRequest.JobTemplate, - CronJobTemplate: executionRequest.CronJobTemplate, - PreRunScript: executionRequest.PreRunScript, - PostRunScript: executionRequest.PostRunScript, - ScraperTemplate: executionRequest.ScraperTemplate, - NegativeTest: executionRequest.NegativeTest, - EnvConfigMaps: mapEnvReferences(executionRequest.EnvConfigMaps), - EnvSecrets: mapEnvReferences(executionRequest.EnvSecrets), + Name: executionRequest.Name, + TestSuiteName: executionRequest.TestSuiteName, + Number: executionRequest.Number, + ExecutionLabels: executionRequest.ExecutionLabels, + Namespace: executionRequest.Namespace, + IsVariablesFileUploaded: executionRequest.IsVariablesFileUploaded, + VariablesFile: executionRequest.VariablesFile, + Variables: MapCRDVariables(executionRequest.Variables), + TestSecretUUID: executionRequest.TestSecretUUID, + TestSuiteSecretUUID: executionRequest.TestSuiteSecretUUID, + Args: executionRequest.Args, + ArgsMode: testsv3.ArgsModeType(executionRequest.ArgsMode), + Envs: executionRequest.Envs, + SecretEnvs: executionRequest.SecretEnvs, + Sync: executionRequest.Sync, + HttpProxy: executionRequest.HttpProxy, + HttpsProxy: executionRequest.HttpsProxy, + Image: executionRequest.Image, + ImagePullSecrets: mapImagePullSecrets(executionRequest.ImagePullSecrets), + ActiveDeadlineSeconds: executionRequest.ActiveDeadlineSeconds, + Command: executionRequest.Command, + ArtifactRequest: artifactRequest, + JobTemplate: executionRequest.JobTemplate, + JobTemplateReference: executionRequest.JobTemplateReference, + CronJobTemplate: executionRequest.CronJobTemplate, + CronJobTemplateReference: executionRequest.CronJobTemplateReference, + PreRunScript: executionRequest.PreRunScript, + PostRunScript: executionRequest.PostRunScript, + PvcTemplate: executionRequest.PvcTemplate, + PvcTemplateReference: executionRequest.PvcTemplateReference, + ScraperTemplate: executionRequest.ScraperTemplate, + ScraperTemplateReference: executionRequest.ScraperTemplateReference, + NegativeTest: executionRequest.NegativeTest, + EnvConfigMaps: mapEnvReferences(executionRequest.EnvConfigMaps), + EnvSecrets: mapEnvReferences(executionRequest.EnvSecrets), } } @@ -464,6 +469,10 @@ func MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest *testkube. executionRequest.JobTemplate, &request.JobTemplate, }, + { + executionRequest.JobTemplateReference, + &request.JobTemplateReference, + }, { executionRequest.PreRunScript, &request.PreRunScript, @@ -476,10 +485,26 @@ func MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest *testkube. executionRequest.CronJobTemplate, &request.CronJobTemplate, }, + { + executionRequest.CronJobTemplateReference, + &request.CronJobTemplateReference, + }, + { + executionRequest.PvcTemplate, + &request.PvcTemplate, + }, + { + executionRequest.PvcTemplateReference, + &request.PvcTemplateReference, + }, { executionRequest.ScraperTemplate, &request.ScraperTemplate, }, + { + executionRequest.ScraperTemplateReference, + &request.ScraperTemplateReference, + }, } for _, field := range fields { diff --git a/pkg/mapper/testsuites/kube_openapi.go b/pkg/mapper/testsuites/kube_openapi.go index 1d2244bed67..75386cfb298 100644 --- a/pkg/mapper/testsuites/kube_openapi.go +++ b/pkg/mapper/testsuites/kube_openapi.go @@ -163,17 +163,24 @@ func MapExecutionRequestFromSpec(specExecutionRequest *testsuitesv3.TestSuiteExe } return &testkube.TestSuiteExecutionRequest{ - Name: specExecutionRequest.Name, - Labels: specExecutionRequest.Labels, - ExecutionLabels: specExecutionRequest.ExecutionLabels, - Namespace: specExecutionRequest.Namespace, - Variables: MergeVariablesAndParams(specExecutionRequest.Variables, nil), - SecretUUID: specExecutionRequest.SecretUUID, - Sync: specExecutionRequest.Sync, - HttpProxy: specExecutionRequest.HttpProxy, - HttpsProxy: specExecutionRequest.HttpsProxy, - Timeout: specExecutionRequest.Timeout, - CronJobTemplate: specExecutionRequest.CronJobTemplate, + Name: specExecutionRequest.Name, + Labels: specExecutionRequest.Labels, + ExecutionLabels: specExecutionRequest.ExecutionLabels, + Namespace: specExecutionRequest.Namespace, + Variables: MergeVariablesAndParams(specExecutionRequest.Variables, nil), + SecretUUID: specExecutionRequest.SecretUUID, + Sync: specExecutionRequest.Sync, + HttpProxy: specExecutionRequest.HttpProxy, + HttpsProxy: specExecutionRequest.HttpsProxy, + Timeout: specExecutionRequest.Timeout, + JobTemplate: specExecutionRequest.JobTemplate, + JobTemplateReference: specExecutionRequest.JobTemplateReference, + CronJobTemplate: specExecutionRequest.CronJobTemplate, + CronJobTemplateReference: specExecutionRequest.CronJobTemplateReference, + PvcTemplate: specExecutionRequest.PvcTemplate, + PvcTemplateReference: specExecutionRequest.PvcTemplateReference, + ScraperTemplate: specExecutionRequest.ScraperTemplate, + ScraperTemplateReference: specExecutionRequest.ScraperTemplateReference, } } @@ -287,10 +294,38 @@ func MapSpecExecutionRequestToExecutionUpdateRequest(request *testsuitesv3.TestS &request.HttpsProxy, &executionRequest.HttpsProxy, }, + { + &request.JobTemplate, + &executionRequest.JobTemplate, + }, + { + &request.JobTemplateReference, + &executionRequest.JobTemplateReference, + }, { &request.CronJobTemplate, &executionRequest.CronJobTemplate, }, + { + &request.CronJobTemplateReference, + &executionRequest.CronJobTemplateReference, + }, + { + &request.PvcTemplate, + &executionRequest.PvcTemplate, + }, + { + &request.PvcTemplateReference, + &executionRequest.PvcTemplateReference, + }, + { + &request.ScraperTemplate, + &executionRequest.ScraperTemplate, + }, + { + &request.ScraperTemplateReference, + &executionRequest.ScraperTemplateReference, + }, } for _, field := range fields { diff --git a/pkg/mapper/webhooks/mapper.go b/pkg/mapper/webhooks/mapper.go index 63e23755196..39b3ba63ae7 100644 --- a/pkg/mapper/webhooks/mapper.go +++ b/pkg/mapper/webhooks/mapper.go @@ -10,15 +10,16 @@ import ( // MapCRDToAPI maps Webhook CRD to OpenAPI spec Webhook func MapCRDToAPI(item executorv1.Webhook) testkube.Webhook { return testkube.Webhook{ - Name: item.Name, - Namespace: item.Namespace, - Uri: item.Spec.Uri, - Events: MapEventArrayToCRDEvents(item.Spec.Events), - Selector: item.Spec.Selector, - Labels: item.Labels, - PayloadObjectField: item.Spec.PayloadObjectField, - PayloadTemplate: item.Spec.PayloadTemplate, - Headers: item.Spec.Headers, + Name: item.Name, + Namespace: item.Namespace, + Uri: item.Spec.Uri, + Events: MapEventArrayToCRDEvents(item.Spec.Events), + Selector: item.Spec.Selector, + Labels: item.Labels, + PayloadObjectField: item.Spec.PayloadObjectField, + PayloadTemplate: item.Spec.PayloadTemplate, + PayloadTemplateReference: item.Spec.PayloadTemplateReference, + Headers: item.Spec.Headers, } } @@ -47,12 +48,13 @@ func MapAPIToCRD(request testkube.WebhookCreateRequest) executorv1.Webhook { Labels: request.Labels, }, Spec: executorv1.WebhookSpec{ - Uri: request.Uri, - Events: MapEventTypesToStringArray(request.Events), - Selector: request.Selector, - PayloadObjectField: request.PayloadObjectField, - PayloadTemplate: request.PayloadTemplate, - Headers: request.Headers, + Uri: request.Uri, + Events: MapEventTypesToStringArray(request.Events), + Selector: request.Selector, + PayloadObjectField: request.PayloadObjectField, + PayloadTemplate: request.PayloadTemplate, + PayloadTemplateReference: request.PayloadTemplateReference, + Headers: request.Headers, }, } } @@ -95,6 +97,10 @@ func MapUpdateToSpec(request testkube.WebhookUpdateRequest, webhook *executorv1. request.PayloadTemplate, &webhook.Spec.PayloadTemplate, }, + { + request.PayloadTemplateReference, + &webhook.Spec.PayloadTemplateReference, + }, } for _, field := range fields { @@ -148,6 +154,10 @@ func MapSpecToUpdate(webhook *executorv1.Webhook) (request testkube.WebhookUpdat &webhook.Spec.PayloadTemplate, &request.PayloadTemplate, }, + { + &webhook.Spec.PayloadTemplateReference, + &request.PayloadTemplateReference, + }, } for _, field := range fields { From 20d6410e343a8140d53470aedd8f6c2126a0e827 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 29 Aug 2023 20:42:55 +0300 Subject: [PATCH 12/59] feat: quote fields --- pkg/api/v1/testkube/model_test_base_extended.go | 1 + .../v1/testkube/model_test_suite_base_extended.go | 12 ++++++++++++ .../model_test_suite_upsert_request_extended.go | 12 ++++++++++++ .../testkube/model_test_upsert_request_extended.go | 1 + 4 files changed, 26 insertions(+) diff --git a/pkg/api/v1/testkube/model_test_base_extended.go b/pkg/api/v1/testkube/model_test_base_extended.go index 1056540c318..a3209665649 100644 --- a/pkg/api/v1/testkube/model_test_base_extended.go +++ b/pkg/api/v1/testkube/model_test_base_extended.go @@ -84,6 +84,7 @@ func (test *Test) QuoteTestTextFields() { &test.ExecutionRequest.CronJobTemplate, &test.ExecutionRequest.PreRunScript, &test.ExecutionRequest.PostRunScript, + &test.ExecutionRequest.PvcTemplate, &test.ExecutionRequest.ScraperTemplate, } diff --git a/pkg/api/v1/testkube/model_test_suite_base_extended.go b/pkg/api/v1/testkube/model_test_suite_base_extended.go index a7431993af3..0626d5ff88c 100644 --- a/pkg/api/v1/testkube/model_test_suite_base_extended.go +++ b/pkg/api/v1/testkube/model_test_suite_base_extended.go @@ -66,8 +66,20 @@ func (t *TestSuite) QuoteTestSuiteTextFields() { } } + if t.ExecutionRequest.JobTemplate != "" { + t.ExecutionRequest.JobTemplate = fmt.Sprintf("%q", t.ExecutionRequest.JobTemplate) + } + if t.ExecutionRequest.CronJobTemplate != "" { t.ExecutionRequest.CronJobTemplate = fmt.Sprintf("%q", t.ExecutionRequest.CronJobTemplate) } + + if t.ExecutionRequest.PvcTemplate != "" { + t.ExecutionRequest.PvcTemplate = fmt.Sprintf("%q", t.ExecutionRequest.PvcTemplate) + } + + if t.ExecutionRequest.ScraperTemplate != "" { + t.ExecutionRequest.ScraperTemplate = fmt.Sprintf("%q", t.ExecutionRequest.ScraperTemplate) + } } } diff --git a/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go b/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go index 1d54eb5b8df..e484a997b59 100644 --- a/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go +++ b/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go @@ -21,8 +21,20 @@ func (testSuite *TestSuiteUpsertRequest) QuoteTestSuiteTextFields() { } } + if testSuite.ExecutionRequest.JobTemplate != "" { + testSuite.ExecutionRequest.JobTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.JobTemplate) + } + if testSuite.ExecutionRequest.CronJobTemplate != "" { testSuite.ExecutionRequest.CronJobTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.CronJobTemplate) } + + if testSuite.ExecutionRequest.PvcTemplate != "" { + testSuite.ExecutionRequest.PvcTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.PvcTemplate) + } + + if testSuite.ExecutionRequest.ScraperTemplate != "" { + testSuite.ExecutionRequest.ScraperTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.ScraperTemplate) + } } } diff --git a/pkg/api/v1/testkube/model_test_upsert_request_extended.go b/pkg/api/v1/testkube/model_test_upsert_request_extended.go index 5e34185ed63..9e72df6e1b3 100644 --- a/pkg/api/v1/testkube/model_test_upsert_request_extended.go +++ b/pkg/api/v1/testkube/model_test_upsert_request_extended.go @@ -27,6 +27,7 @@ func (test *TestUpsertRequest) QuoteTestTextFields() { &test.ExecutionRequest.CronJobTemplate, &test.ExecutionRequest.PreRunScript, &test.ExecutionRequest.PostRunScript, + &test.ExecutionRequest.PvcTemplate, &test.ExecutionRequest.ScraperTemplate, } From b8b50341486c0a708a5837e5900dc94ece3fc49c Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 31 Aug 2023 16:12:10 +0300 Subject: [PATCH 13/59] feat: using template reference --- api/v1/testkube.yaml | 237 ++++++++++++++++++ .../commands/executors/common.go | 32 ++- .../commands/executors/create.go | 7 +- .../commands/executors/get.go | 1 + .../commands/executors/update.go | 7 +- cmd/kubectl-testkube/commands/tests/common.go | 92 +++++-- cmd/kubectl-testkube/commands/tests/create.go | 10 + .../commands/tests/renderer/test_obj.go | 30 ++- cmd/kubectl-testkube/commands/tests/run.go | 19 ++ cmd/kubectl-testkube/commands/tests/update.go | 10 + .../commands/testsuites/common.go | 120 ++++++++- .../commands/testsuites/create.go | 14 ++ .../testsuites/renderer/testsuite_obj.go | 36 ++- .../commands/testsuites/run.go | 42 +++- .../commands/testsuites/update.go | 14 ++ internal/app/api/v1/executions_test.go | 7 +- internal/app/api/v1/server.go | 2 +- .../resolvers/executors_resolver_test.go | 25 +- internal/graphql/services/executors_test.go | 64 ++--- pkg/api/v1/client/interface.go | 24 +- pkg/api/v1/client/test.go | 8 + pkg/api/v1/client/testsuite.go | 42 ++-- pkg/crd/templates/executor.tmpl | 3 + pkg/crd/templates/test.tmpl | 17 +- pkg/crd/templates/testsuite.tmpl | 23 +- pkg/crd/templates/webhook.tmpl | 3 + pkg/cronjob/client.go | 8 +- pkg/event/kind/webhook/loader.go | 28 ++- pkg/event/kind/webhook/loader_test.go | 9 +- pkg/executor/client/job.go | 15 +- .../containerexecutor/containerexecutor.go | 7 +- .../containerexecutor_test.go | 88 +++++-- pkg/executor/containerexecutor/tmpl.go | 55 +++- pkg/mapper/testsuites/openapi_kube.go | 57 ++++- pkg/scheduler/test_scheduler.go | 16 ++ pkg/scheduler/test_scheduler_test.go | 50 ++-- pkg/scheduler/testsuite_scheduler.go | 30 +++ pkg/triggers/executor_test.go | 23 +- pkg/triggers/service_test.go | 23 +- 39 files changed, 1078 insertions(+), 220 deletions(-) diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index b24a41be2c5..ab48f196336 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -2500,6 +2500,243 @@ paths: items: $ref: "#/components/schemas/Problem" + /templates: + get: + tags: + - templates + - api + summary: "List templates" + description: "List templates available in cluster" + operationId: listTemplates + parameters: + - $ref: "#/components/parameters/Selector" + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Template" + text/yaml: + schema: + type: string + 400: + description: "problem with input for CRD generation" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 502: + description: "problem with read information from kubernetes cluster" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + post: + tags: + - template + - api + summary: "Create new template" + description: "Create new template based on variables passed in request" + operationId: createTemplate + requestBody: + description: template request body data + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TemplateCreateRequest" + text/yaml: + schema: + type: string + responses: + 200: + description: "successful operation" + content: + text/yaml: + schema: + type: string + 201: + description: "successful operation" + content: + application/json: + schema: + $ref: "#/components/schemas/Template" + 400: + description: "problem with template definition - probably some bad input occurs (invalid JSON body or similar)" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 502: + description: "problem with communicating with kubernetes cluster" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + delete: + tags: + - template + - api + summary: "Delete templates" + description: "Deletes labeled templates" + operationId: deleteTemplates + parameters: + - $ref: "#/components/parameters/Selector" + responses: + 204: + description: "no content" + 502: + description: "problem with read information from kubernetes cluster" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + + /templates/{id}: + delete: + parameters: + - $ref: "#/components/parameters/ID" + tags: + - api + - template + summary: "Delete template" + description: "Deletes template by its name" + operationId: deleteTemplate + responses: + 204: + description: template deleted successfuly + 404: + description: "template not found" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 502: + description: "problem with communicating with kubernetes cluster" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + + get: + parameters: + - $ref: "#/components/parameters/ID" + tags: + - api + - template + summary: "Get template details" + description: "Returns template" + operationId: getTemplate + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Template" + text/yaml: + schema: + type: string + 400: + description: "problem with input for CRD generation" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 404: + description: "template not found" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 500: + description: "problem with getting template data" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 502: + description: "problem with communicating with kubernetes cluster" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + patch: + parameters: + - $ref: "#/components/parameters/ID" + tags: + - template + - api + summary: "Update new template" + description: "Update new template based on variables passed in request" + operationId: updateTemplate + requestBody: + description: template request body data + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TemplateUpdateRequest" + text/yaml: + schema: + type: string + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + $ref: "#/components/schemas/Template" + 400: + description: "problem with template definition - probably some bad input occurs (invalid JSON body or similar)" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 404: + description: "template not found" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + 502: + description: "problem with communicating with kubernetes cluster" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + /config: patch: tags: diff --git a/cmd/kubectl-testkube/commands/executors/common.go b/cmd/kubectl-testkube/commands/executors/common.go index 37d084097fd..7cdf14b2d10 100644 --- a/cmd/kubectl-testkube/commands/executors/common.go +++ b/cmd/kubectl-testkube/commands/executors/common.go @@ -32,6 +32,7 @@ func NewUpsertExecutorOptionsFromFlags(cmd *cobra.Command) (options apiClient.Up return options, err } + jobTemplateReference := cmd.Flag("job-template-reference").Value.String() jobTemplate := cmd.Flag("job-template").Value.String() jobTemplateContent := "" if jobTemplate != "" { @@ -83,19 +84,20 @@ func NewUpsertExecutorOptionsFromFlags(cmd *cobra.Command) (options apiClient.Up } options = apiClient.UpsertExecutorOptions{ - Name: name, - Types: types, - ExecutorType: executorType, - Image: image, - ImagePullSecrets: imageSecrets, - Command: command, - Args: executorArgs, - Uri: uri, - ContentTypes: contentTypes, - JobTemplate: jobTemplateContent, - Features: features, - Labels: labels, - Meta: meta, + Name: name, + Types: types, + ExecutorType: executorType, + Image: image, + ImagePullSecrets: imageSecrets, + Command: command, + Args: executorArgs, + Uri: uri, + ContentTypes: contentTypes, + JobTemplate: jobTemplateContent, + JobTemplateReference: jobTemplateReference, + Features: features, + Labels: labels, + Meta: meta, } return options, nil @@ -123,6 +125,10 @@ func NewUpdateExecutorOptionsFromFlags(cmd *cobra.Command) (options apiClient.Up "image", &options.Image, }, + { + "job-template-reference", + &options.JobTemplateReference, + }, } for _, field := range fields { diff --git a/cmd/kubectl-testkube/commands/executors/create.go b/cmd/kubectl-testkube/commands/executors/create.go index 1dbf04372cd..6b552871d8d 100644 --- a/cmd/kubectl-testkube/commands/executors/create.go +++ b/cmd/kubectl-testkube/commands/executors/create.go @@ -14,9 +14,9 @@ import ( func NewCreateExecutorCmd() *cobra.Command { var ( - types, command, executorArgs, imagePullSecretNames, features, contentTypes []string - name, executorType, image, uri, jobTemplate, iconURI, docsURI string - labels, tooltips map[string]string + types, command, executorArgs, imagePullSecretNames, features, contentTypes []string + name, executorType, image, uri, jobTemplate, iconURI, docsURI, jobTemplateReference string + labels, tooltips map[string]string ) cmd := &cobra.Command{ @@ -72,6 +72,7 @@ func NewCreateExecutorCmd() *cobra.Command { cmd.Flags().StringArrayVar(&command, "command", []string{}, "command passed to image in executor") cmd.Flags().StringArrayVar(&executorArgs, "args", []string{}, "args passed to image in executor") cmd.Flags().StringVarP(&jobTemplate, "job-template", "j", "", "if executor needs to be launched using custom job specification, then a path to template file should be provided") + cmd.Flags().StringVarP(&jobTemplateReference, "job-template-reference", "", "", "reference to job template for using with executor") cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringArrayVar(&features, "feature", []string{}, "feature provided by executor") cmd.Flags().StringVarP(&iconURI, "icon-uri", "", "", "URI to executor icon") diff --git a/cmd/kubectl-testkube/commands/executors/get.go b/cmd/kubectl-testkube/commands/executors/get.go index 47f3f2fd9a0..033561ded0d 100644 --- a/cmd/kubectl-testkube/commands/executors/get.go +++ b/cmd/kubectl-testkube/commands/executors/get.go @@ -86,6 +86,7 @@ func mapExecutorDetailsToCreateExecutorOptions(namespace string, executor *testk options.Args = executor.Executor.Args options.Uri = executor.Executor.Uri options.Labels = executor.Executor.Labels + options.JobTemplateReference = executor.Executor.JobTemplateReference if executor.Executor.JobTemplate != "" { options.JobTemplate = fmt.Sprintf("%q", executor.Executor.JobTemplate) } diff --git a/cmd/kubectl-testkube/commands/executors/update.go b/cmd/kubectl-testkube/commands/executors/update.go index bc8f08ab1c2..f87c1700791 100644 --- a/cmd/kubectl-testkube/commands/executors/update.go +++ b/cmd/kubectl-testkube/commands/executors/update.go @@ -9,9 +9,9 @@ import ( func UpdateExecutorCmd() *cobra.Command { var ( - types, command, executorArgs, imagePullSecretNames, features, contentTypes []string - name, executorType, image, uri, jobTemplate, iconURI, docsURI string - labels, tooltips map[string]string + types, command, executorArgs, imagePullSecretNames, features, contentTypes []string + name, executorType, image, uri, jobTemplate, iconURI, docsURI, jobTemplateReference string + labels, tooltips map[string]string ) cmd := &cobra.Command{ @@ -52,6 +52,7 @@ func UpdateExecutorCmd() *cobra.Command { cmd.Flags().StringArrayVar(&command, "command", []string{}, "command passed to image in executor") cmd.Flags().StringArrayVar(&executorArgs, "args", []string{}, "args passed to image in executor") cmd.Flags().StringVarP(&jobTemplate, "job-template", "j", "", "if executor needs to be launched using custom job specification, then a path to template file should be provided") + cmd.Flags().StringVarP(&jobTemplateReference, "job-template-reference", "", "", "reference to job template for using with executor") cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringArrayVar(&features, "feature", []string{}, "feature provided by executor") cmd.Flags().StringVarP(&iconURI, "icon-uri", "", "", "URI to executor icon") diff --git a/cmd/kubectl-testkube/commands/tests/common.go b/cmd/kubectl-testkube/commands/tests/common.go index e8b044e2100..059f92b7755 100644 --- a/cmd/kubectl-testkube/commands/tests/common.go +++ b/cmd/kubectl-testkube/commands/tests/common.go @@ -389,6 +389,7 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi imageSecrets = append(imageSecrets, testkube.LocalObjectReference{Name: secretName}) } + jobTemplateReference := cmd.Flag("job-template-reference").Value.String() jobTemplateContent := "" jobTemplate := cmd.Flag("job-template").Value.String() if jobTemplate != "" { @@ -400,6 +401,7 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi jobTemplateContent = string(b) } + cronJobTemplateReference := cmd.Flag("cronjob-template-reference").Value.String() cronJobTemplateContent := "" cronJobTemplate := cmd.Flag("cronjob-template").Value.String() if cronJobTemplate != "" { @@ -433,6 +435,7 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi postRunScriptContent = string(b) } + scraperTemplateReference := cmd.Flag("scraper-template-reference").Value.String() scraperTemplateContent := "" scraperTemplate := cmd.Flag("scraper-template").Value.String() if scraperTemplate != "" { @@ -444,32 +447,49 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi scraperTemplateContent = string(b) } + pvcTemplateReference := cmd.Flag("pvc-template-reference").Value.String() + pvcTemplateContent := "" + pvcTemplate := cmd.Flag("pvc-template").Value.String() + if pvcTemplate != "" { + b, err := os.ReadFile(scraperTemplate) + if err != nil { + return nil, err + } + + scraperTemplateContent = string(b) + } + envConfigMaps, envSecrets, err := newEnvReferencesFromFlags(cmd) if err != nil { return nil, err } request = &testkube.ExecutionRequest{ - Name: executionName, - Variables: variables, - Image: image, - Command: command, - Args: executorArgs, - ArgsMode: mode, - ImagePullSecrets: imageSecrets, - Envs: envs, - SecretEnvs: secretEnvs, - HttpProxy: httpProxy, - HttpsProxy: httpsProxy, - ActiveDeadlineSeconds: timeout, - JobTemplate: jobTemplateContent, - CronJobTemplate: cronJobTemplateContent, - PreRunScript: preRunScriptContent, - PostRunScript: postRunScriptContent, - ScraperTemplate: scraperTemplateContent, - NegativeTest: negativeTest, - EnvConfigMaps: envConfigMaps, - EnvSecrets: envSecrets, + Name: executionName, + Variables: variables, + Image: image, + Command: command, + Args: executorArgs, + ArgsMode: mode, + ImagePullSecrets: imageSecrets, + Envs: envs, + SecretEnvs: secretEnvs, + HttpProxy: httpProxy, + HttpsProxy: httpsProxy, + ActiveDeadlineSeconds: timeout, + JobTemplate: jobTemplateContent, + JobTemplateReference: jobTemplateReference, + CronJobTemplate: cronJobTemplateContent, + CronJobTemplateReference: cronJobTemplateReference, + PreRunScript: preRunScriptContent, + PostRunScript: postRunScriptContent, + ScraperTemplate: scraperTemplateContent, + ScraperTemplateReference: scraperTemplateReference, + PvcTemplate: pvcTemplateContent, + PvcTemplateReference: pvcTemplateReference, + NegativeTest: negativeTest, + EnvConfigMaps: envConfigMaps, + EnvSecrets: envSecrets, } request.ArtifactRequest, err = newArtifactRequestFromFlags(cmd) @@ -798,6 +818,22 @@ func newExecutionUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.E "args-mode", &request.ArgsMode, }, + { + "job-template-reference", + &request.JobTemplateReference, + }, + { + "cronjob-template-reference", + &request.CronJobTemplateReference, + }, + { + "scraper-template-reference", + &request.ScraperTemplateReference, + }, + { + "pvc-template-reference", + &request.PvcTemplateReference, + }, } var nonEmpty bool @@ -999,6 +1035,22 @@ func newExecutionUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.E nonEmpty = true } + if cmd.Flag("pvc-template").Changed { + pvcTemplateContent := "" + pvcTemplate := cmd.Flag("pvc-template").Value.String() + if pvcTemplate != "" { + b, err := os.ReadFile(pvcTemplate) + if err != nil { + return nil, err + } + + pvcTemplateContent = string(b) + } + + request.PvcTemplate = &pvcTemplateContent + nonEmpty = true + } + if cmd.Flag("mount-configmap").Changed || cmd.Flag("variable-configmap").Changed { envConfigMaps, _, err := newEnvReferencesFromFlags(cmd) if err != nil { diff --git a/cmd/kubectl-testkube/commands/tests/create.go b/cmd/kubectl-testkube/commands/tests/create.go index 7210b343565..eeea2dd3682 100644 --- a/cmd/kubectl-testkube/commands/tests/create.go +++ b/cmd/kubectl-testkube/commands/tests/create.go @@ -39,10 +39,15 @@ type CreateCommonFlags struct { ArtifactVolumeMountPath string ArtifactDirs []string JobTemplate string + JobTemplateReference string CronJobTemplate string + CronJobTemplateReference string PreRunScript string PostRunScript string ScraperTemplate string + ScraperTemplateReference string + PvcTemplate string + PvcTemplateReference string NegativeTest bool MountConfigMaps map[string]string VariableConfigMaps []string @@ -210,10 +215,15 @@ func AddCreateFlags(cmd *cobra.Command, flags *CreateCommonFlags) { cmd.Flags().StringVar(&flags.ArtifactVolumeMountPath, "artifact-volume-mount-path", "", "artifact volume mount path for container executor") cmd.Flags().StringArrayVarP(&flags.ArtifactDirs, "artifact-dir", "", []string{}, "artifact dirs for scraping") cmd.Flags().StringVar(&flags.JobTemplate, "job-template", "", "job template file path for extensions to job template") + cmd.Flags().StringVar(&flags.JobTemplateReference, "job-template-reference", "", "reference to job template to use for the test") cmd.Flags().StringVar(&flags.CronJobTemplate, "cronjob-template", "", "cron job template file path for extensions to cron job template") + cmd.Flags().StringVar(&flags.CronJobTemplateReference, "cronjob-template-reference", "", "reference to cron job template to use for the test") cmd.Flags().StringVarP(&flags.PreRunScript, "prerun-script", "", "", "path to script to be run before test execution") cmd.Flags().StringVarP(&flags.PostRunScript, "postrun-script", "", "", "path to script to be run after test execution") cmd.Flags().StringVar(&flags.ScraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template") + cmd.Flags().StringVar(&flags.ScraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test") + cmd.Flags().StringVar(&flags.PvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template") + cmd.Flags().StringVar(&flags.PvcTemplateReference, "pvc-template-reference", "", "reference to pvc template to use for the test") cmd.Flags().BoolVar(&flags.NegativeTest, "negative-test", false, "negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa") cmd.Flags().StringToStringVarP(&flags.MountConfigMaps, "mount-configmap", "", map[string]string{}, "config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath") cmd.Flags().StringArrayVar(&flags.VariableConfigMaps, "variable-configmap", []string{}, "config map name used to map all keys to basis variables") diff --git a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go index 1096ab30f4d..1e8dd8f1f77 100644 --- a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go +++ b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go @@ -133,23 +133,43 @@ func TestRenderer(ui *ui.UI, obj interface{}) error { } if test.ExecutionRequest.JobTemplate != "" { - ui.Warn(" Job template: ", "\n", test.ExecutionRequest.JobTemplate) + ui.Warn(" Job template: ", "\n", test.ExecutionRequest.JobTemplate) + } + + if test.ExecutionRequest.JobTemplateReference != "" { + ui.Warn(" Job template reference: ", "\n", test.ExecutionRequest.JobTemplateReference) } if test.ExecutionRequest.CronJobTemplate != "" { - ui.Warn(" Cron job template: ", "\n", test.ExecutionRequest.CronJobTemplate) + ui.Warn(" Cron job template: ", "\n", test.ExecutionRequest.CronJobTemplate) + } + + if test.ExecutionRequest.CronJobTemplateReference != "" { + ui.Warn(" Cron job template reference: ", "\n", test.ExecutionRequest.CronJobTemplateReference) } if test.ExecutionRequest.PreRunScript != "" { - ui.Warn(" Pre run script: ", "\n", test.ExecutionRequest.PreRunScript) + ui.Warn(" Pre run script: ", "\n", test.ExecutionRequest.PreRunScript) } if test.ExecutionRequest.PostRunScript != "" { - ui.Warn(" Post run script: ", "\n", test.ExecutionRequest.PostRunScript) + ui.Warn(" Post run script: ", "\n", test.ExecutionRequest.PostRunScript) } if test.ExecutionRequest.ScraperTemplate != "" { - ui.Warn(" Scraper template: ", "\n", test.ExecutionRequest.ScraperTemplate) + ui.Warn(" Scraper template: ", "\n", test.ExecutionRequest.ScraperTemplate) + } + + if test.ExecutionRequest.ScraperTemplateReference != "" { + ui.Warn(" Scraper template reference: ", "\n", test.ExecutionRequest.ScraperTemplateReference) + } + + if test.ExecutionRequest.PvcTemplate != "" { + ui.Warn(" PVC template: ", "\n", test.ExecutionRequest.PvcTemplate) + } + + if test.ExecutionRequest.PvcTemplateReference != "" { + ui.Warn(" PVC template reference: ", "\n", test.ExecutionRequest.PvcTemplateReference) } var mountConfigMaps, mountSecrets []mountParams diff --git a/cmd/kubectl-testkube/commands/tests/run.go b/cmd/kubectl-testkube/commands/tests/run.go index c956082274e..9dda9d04ee0 100644 --- a/cmd/kubectl-testkube/commands/tests/run.go +++ b/cmd/kubectl-testkube/commands/tests/run.go @@ -42,6 +42,7 @@ func NewRunTestCmd() *cobra.Command { artifactVolumeMountPath string artifactDirs []string jobTemplate string + jobTemplateReference string gitBranch string gitCommit string gitPath string @@ -49,6 +50,9 @@ func NewRunTestCmd() *cobra.Command { preRunScript string postRunScript string scraperTemplate string + scraperTemplateReference string + pvcTemplate string + pvcTemplateReference string negativeTest bool mountConfigMaps map[string]string variableConfigMaps []string @@ -110,6 +114,13 @@ func NewRunTestCmd() *cobra.Command { scraperTemplateContent = string(b) } + pvcTemplateContent := "" + if pvcTemplate != "" { + b, err := os.ReadFile(pvcTemplate) + ui.ExitOnError("reading pvc template", err) + pvcTemplateContent = string(b) + } + mode := "" if cmd.Flag("args-mode").Changed { mode = argsMode @@ -131,9 +142,13 @@ func NewRunTestCmd() *cobra.Command { Envs: envs, Image: image, JobTemplate: jobTemplateContent, + JobTemplateReference: jobTemplateReference, PreRunScriptContent: preRunScriptContent, PostRunScriptContent: postRunScriptContent, ScraperTemplate: scraperTemplateContent, + ScraperTemplateReference: scraperTemplateReference, + PvcTemplate: pvcTemplateContent, + PvcTemplateReference: pvcTemplateReference, IsNegativeTestChangedOnRun: false, EnvConfigMaps: envConfigMaps, EnvSecrets: envSecrets, @@ -285,6 +300,7 @@ func NewRunTestCmd() *cobra.Command { cmd.Flags().StringVar(&artifactVolumeMountPath, "artifact-volume-mount-path", "", "artifact volume mount path for container executor") cmd.Flags().StringArrayVarP(&artifactDirs, "artifact-dir", "", []string{}, "artifact dirs for scraping") cmd.Flags().StringVar(&jobTemplate, "job-template", "", "job template file path for extensions to job template") + cmd.Flags().StringVar(&jobTemplateReference, "job-template-reference", "", "reference to job template to use for the test") cmd.Flags().StringVarP(&gitBranch, "git-branch", "", "", "if uri is git repository we can set additional branch parameter") cmd.Flags().StringVarP(&gitCommit, "git-commit", "", "", "if uri is git repository we can use commit id (sha) parameter") cmd.Flags().StringVarP(&gitPath, "git-path", "", "", "if repository is big we need to define additional path to directory/file to checkout partially") @@ -292,6 +308,9 @@ func NewRunTestCmd() *cobra.Command { cmd.Flags().StringVarP(&preRunScript, "prerun-script", "", "", "path to script to be run before test execution") cmd.Flags().StringVarP(&postRunScript, "postrun-script", "", "", "path to script to be run after test execution") cmd.Flags().StringVar(&scraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template") + cmd.Flags().StringVar(&scraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test") + cmd.Flags().StringVar(&pvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template") + cmd.Flags().StringVar(&pvcTemplateReference, "pvc-template-reference", "", "reference to pvc template to use for the test") cmd.Flags().BoolVar(&negativeTest, "negative-test", false, "negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa") cmd.Flags().StringToStringVarP(&mountConfigMaps, "mount-configmap", "", map[string]string{}, "config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath") cmd.Flags().StringArrayVar(&variableConfigMaps, "variable-configmap", []string{}, "config map name used to map all keys to basis variables") diff --git a/cmd/kubectl-testkube/commands/tests/update.go b/cmd/kubectl-testkube/commands/tests/update.go index 133e0bf37f9..48cc9dc416a 100644 --- a/cmd/kubectl-testkube/commands/tests/update.go +++ b/cmd/kubectl-testkube/commands/tests/update.go @@ -48,10 +48,15 @@ func NewUpdateTestsCmd() *cobra.Command { artifactVolumeMountPath string artifactDirs []string jobTemplate string + jobTemplateReference string cronJobTemplate string + cronJobTemplateReference string preRunScript string postRunScript string scraperTemplate string + scraperTemplateReference string + pvcTemplate string + pvcTemplateReference string negativeTest bool mountConfigMaps map[string]string variableConfigMaps []string @@ -132,10 +137,15 @@ func NewUpdateTestsCmd() *cobra.Command { cmd.Flags().StringVar(&artifactVolumeMountPath, "artifact-volume-mount-path", "", "artifact volume mount path for container executor") cmd.Flags().StringArrayVarP(&artifactDirs, "artifact-dir", "", []string{}, "artifact dirs for scraping") cmd.Flags().StringVar(&jobTemplate, "job-template", "", "job template file path for extensions to job template") + cmd.Flags().StringVar(&jobTemplateReference, "job-template-reference", "", "reference to job template to use for the test") cmd.Flags().StringVar(&cronJobTemplate, "cronjob-template", "", "cron job template file path for extensions to cron job template") + cmd.Flags().StringVar(&cronJobTemplateReference, "cronjob-template-reference", "", "reference to cron job template to use for the test") cmd.Flags().StringVarP(&preRunScript, "prerun-script", "", "", "path to script to be run before test execution") cmd.Flags().StringVarP(&postRunScript, "postrun-script", "", "", "path to script to be run after test execution") cmd.Flags().StringVar(&scraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template") + cmd.Flags().StringVar(&scraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test") + cmd.Flags().StringVar(&pvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template") + cmd.Flags().StringVar(&pvcTemplateReference, "pvc-template-reference", "", "reference to pvc template to use for the test") cmd.Flags().BoolVar(&negativeTest, "negative-test", false, "negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa") cmd.Flags().StringToStringVarP(&mountConfigMaps, "mount-configmap", "", map[string]string{}, "config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath") cmd.Flags().StringArrayVar(&variableConfigMaps, "variable-configmap", []string{}, "config map name used to map all keys to basis variables") diff --git a/cmd/kubectl-testkube/commands/testsuites/common.go b/cmd/kubectl-testkube/commands/testsuites/common.go index 7403c5a8616..68f3aee9650 100644 --- a/cmd/kubectl-testkube/commands/testsuites/common.go +++ b/cmd/kubectl-testkube/commands/testsuites/common.go @@ -149,6 +149,19 @@ func NewTestSuiteUpsertOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 return options, fmt.Errorf("validating schedule %w", err) } + jobTemplateReference := cmd.Flag("job-template-reference").Value.String() + jobTemplateContent := "" + jobTemplate := cmd.Flag("job-template").Value.String() + if jobTemplate != "" { + b, err := os.ReadFile(jobTemplate) + if err != nil { + return options, err + } + + jobTemplateContent = string(b) + } + + cronJobTemplateReference := cmd.Flag("cronjob-template-refeence").Value.String() cronJobTemplateContent := "" cronJobTemplate := cmd.Flag("cronjob-template").Value.String() if cronJobTemplate != "" { @@ -160,14 +173,45 @@ func NewTestSuiteUpsertOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 cronJobTemplateContent = string(b) } + scraperTemplateReference := cmd.Flag("scraper-template-reference").Value.String() + scraperTemplateContent := "" + scraperTemplate := cmd.Flag("scraper-template").Value.String() + if scraperTemplate != "" { + b, err := os.ReadFile(scraperTemplate) + if err != nil { + return options, err + } + + scraperTemplateContent = string(b) + } + + pvcTemplateReference := cmd.Flag("pvc-template-reference").Value.String() + pvcTemplateContent := "" + pvcTemplate := cmd.Flag("pvc-template").Value.String() + if pvcTemplate != "" { + b, err := os.ReadFile(pvcTemplate) + if err != nil { + return options, err + } + + pvcTemplateContent = string(b) + } + options.Schedule = schedule options.ExecutionRequest = &testkube.TestSuiteExecutionRequest{ - Variables: variables, - Name: cmd.Flag("execution-name").Value.String(), - HttpProxy: cmd.Flag("http-proxy").Value.String(), - HttpsProxy: cmd.Flag("https-proxy").Value.String(), - Timeout: timeout, - CronJobTemplate: cronJobTemplateContent, + Variables: variables, + Name: cmd.Flag("execution-name").Value.String(), + HttpProxy: cmd.Flag("http-proxy").Value.String(), + HttpsProxy: cmd.Flag("https-proxy").Value.String(), + Timeout: timeout, + JobTemplate: jobTemplateContent, + JobTemplateReference: jobTemplateReference, + CronJobTemplate: cronJobTemplateContent, + CronJobTemplateReference: cronJobTemplateReference, + ScraperTemplate: scraperTemplateContent, + ScraperTemplateReference: scraperTemplateReference, + PvcTemplate: pvcTemplateContent, + PvcTemplateReference: pvcTemplateReference, } return options, nil @@ -268,6 +312,22 @@ func NewTestSuiteUpdateOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 nonEmpty = true } + if cmd.Flag("job-template").Changed { + jobTemplateContent := "" + jobTemplate := cmd.Flag("job-template").Value.String() + if jobTemplate != "" { + b, err := os.ReadFile(jobTemplate) + if err != nil { + return options, err + } + + jobTemplateContent = string(b) + } + + executionRequest.JobTemplate = &jobTemplateContent + nonEmpty = true + } + if cmd.Flag("cronjob-template").Changed { cronJobTemplateContent := "" cronJobTemplate := cmd.Flag("cronjob-template").Value.String() @@ -284,6 +344,38 @@ func NewTestSuiteUpdateOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 nonEmpty = true } + if cmd.Flag("scraper-template").Changed { + scraperTemplateContent := "" + scraperTemplate := cmd.Flag("scraper-template").Value.String() + if scraperTemplate != "" { + b, err := os.ReadFile(scraperTemplate) + if err != nil { + return options, err + } + + scraperTemplateContent = string(b) + } + + executionRequest.ScraperTemplate = &scraperTemplateContent + nonEmpty = true + } + + if cmd.Flag("pvc-template").Changed { + pvcTemplateContent := "" + pvcTemplate := cmd.Flag("pvc-template").Value.String() + if pvcTemplate != "" { + b, err := os.ReadFile(pvcTemplate) + if err != nil { + return options, err + } + + pvcTemplateContent = string(b) + } + + executionRequest.ScraperTemplate = &pvcTemplateContent + nonEmpty = true + } + var executionFields = []struct { name string destination **string @@ -300,6 +392,22 @@ func NewTestSuiteUpdateOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 "https-proxy", &executionRequest.HttpsProxy, }, + { + "job-template-reference", + &executionRequest.JobTemplateReference, + }, + { + "cronjob-template-reference", + &executionRequest.CronJobTemplateReference, + }, + { + "scraper-template-reference", + &executionRequest.ScraperTemplateReference, + }, + { + "pvc-template-reference", + &executionRequest.PvcTemplateReference, + }, } for _, field := range executionFields { diff --git a/cmd/kubectl-testkube/commands/testsuites/create.go b/cmd/kubectl-testkube/commands/testsuites/create.go index 018ff38a907..97d041d4290 100644 --- a/cmd/kubectl-testkube/commands/testsuites/create.go +++ b/cmd/kubectl-testkube/commands/testsuites/create.go @@ -26,7 +26,14 @@ func NewCreateTestSuitesCmd() *cobra.Command { httpProxy, httpsProxy string secretVariableReferences map[string]string timeout int32 + jobTemplate string cronJobTemplate string + scraperTemplate string + pvcTemplate string + jobTemplateReference string + cronJobTemplateReference string + scraperTemplateReference string + pvcTemplateReference string ) cmd := &cobra.Command{ @@ -79,7 +86,14 @@ func NewCreateTestSuitesCmd() *cobra.Command { cmd.Flags().StringVar(&httpsProxy, "https-proxy", "", "https proxy for executor containers") cmd.Flags().StringToStringVarP(&secretVariableReferences, "secret-variable-reference", "", nil, "secret variable references in a form name1=secret_name1=secret_key1") cmd.Flags().Int32Var(&timeout, "timeout", 0, "duration in seconds for test suite to timeout. 0 disables timeout.") + cmd.Flags().StringVar(&jobTemplate, "job-template", "", "job template file path for extensions to job template") cmd.Flags().StringVar(&cronJobTemplate, "cronjob-template", "", "cron job template file path for extensions to cron job template") + cmd.Flags().StringVar(&scraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template") + cmd.Flags().StringVar(&pvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template") + cmd.Flags().StringVar(&jobTemplateReference, "job-template-reference", "", "reference to job template to use for the test") + cmd.Flags().StringVar(&cronJobTemplateReference, "cronjob-template-reference", "", "reference to cron job template to use for the test") + cmd.Flags().StringVar(&scraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test") + cmd.Flags().StringVar(&pvcTemplateReference, "pvc-template-reference", "", "reference to pvc template to use for the test") return cmd } diff --git a/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go b/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go index 7595f139ef1..c12881e457c 100644 --- a/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go +++ b/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go @@ -33,7 +33,7 @@ func TestSuiteRenderer(ui *ui.UI, obj interface{}) error { if ts.ExecutionRequest != nil { ui.Warn("Execution request: ") if ts.ExecutionRequest.Name != "" { - ui.Warn(" Name: ", ts.ExecutionRequest.Name) + ui.Warn(" Name: ", ts.ExecutionRequest.Name) } if len(ts.ExecutionRequest.Variables) > 0 { @@ -41,15 +41,43 @@ func TestSuiteRenderer(ui *ui.UI, obj interface{}) error { } if ts.ExecutionRequest.HttpProxy != "" { - ui.Warn(" Http proxy: ", ts.ExecutionRequest.HttpProxy) + ui.Warn(" Http proxy: ", ts.ExecutionRequest.HttpProxy) } if ts.ExecutionRequest.HttpsProxy != "" { - ui.Warn(" Https proxy: ", ts.ExecutionRequest.HttpsProxy) + ui.Warn(" Https proxy: ", ts.ExecutionRequest.HttpsProxy) + } + + if ts.ExecutionRequest.JobTemplate != "" { + ui.Warn(" Job template: ", ts.ExecutionRequest.JobTemplate) + } + + if ts.ExecutionRequest.JobTemplateReference != "" { + ui.Warn(" Job template reference: ", ts.ExecutionRequest.JobTemplateReference) } if ts.ExecutionRequest.CronJobTemplate != "" { - ui.Warn(" Cron job template: ", ts.ExecutionRequest.CronJobTemplate) + ui.Warn(" Cron job template: ", ts.ExecutionRequest.CronJobTemplate) + } + + if ts.ExecutionRequest.CronJobTemplateReference != "" { + ui.Warn(" Cron job template reference: ", ts.ExecutionRequest.CronJobTemplateReference) + } + + if ts.ExecutionRequest.ScraperTemplate != "" { + ui.Warn(" Scraper template: ", ts.ExecutionRequest.ScraperTemplate) + } + + if ts.ExecutionRequest.ScraperTemplateReference != "" { + ui.Warn(" Scraper template reference: ", ts.ExecutionRequest.ScraperTemplateReference) + } + + if ts.ExecutionRequest.PvcTemplate != "" { + ui.Warn(" PVC template: ", ts.ExecutionRequest.PvcTemplate) + } + + if ts.ExecutionRequest.PvcTemplateReference != "" { + ui.Warn(" PVC template reference: ", ts.ExecutionRequest.PvcTemplateReference) } } diff --git a/cmd/kubectl-testkube/commands/testsuites/run.go b/cmd/kubectl-testkube/commands/testsuites/run.go index 76fdd1c92e4..fba589a5376 100644 --- a/cmd/kubectl-testkube/commands/testsuites/run.go +++ b/cmd/kubectl-testkube/commands/testsuites/run.go @@ -2,6 +2,7 @@ package testsuites import ( "fmt" + "os" "strings" "time" @@ -31,6 +32,12 @@ func NewRunTestSuiteCmd() *cobra.Command { gitPath string gitWorkingDir string runningContext string + jobTemplate string + scraperTemplate string + pvcTemplate string + jobTemplateReference string + scraperTemplateReference string + pvcTemplateReference string ) cmd := &cobra.Command{ @@ -46,6 +53,27 @@ func NewRunTestSuiteCmd() *cobra.Command { var executions []testkube.TestSuiteExecution + jobTemplateContent := "" + if jobTemplate != "" { + b, err := os.ReadFile(jobTemplate) + ui.ExitOnError("reading job template", err) + jobTemplateContent = string(b) + } + + scraperTemplateContent := "" + if scraperTemplate != "" { + b, err := os.ReadFile(scraperTemplate) + ui.ExitOnError("reading scraper template", err) + scraperTemplateContent = string(b) + } + + pvcTemplateContent := "" + if pvcTemplate != "" { + b, err := os.ReadFile(pvcTemplate) + ui.ExitOnError("reading pvc template", err) + pvcTemplateContent = string(b) + } + variables, err := common.CreateVariables(cmd, false) ui.WarnOnError("getting variables", err) options := apiv1.ExecuteTestSuiteOptions{ @@ -57,7 +85,13 @@ func NewRunTestSuiteCmd() *cobra.Command { Type_: string(testkube.RunningContextTypeUserCLI), Context: runningContext, }, - ConcurrencyLevel: int32(concurrencyLevel), + ConcurrencyLevel: int32(concurrencyLevel), + JobTemplate: jobTemplateContent, + JobTemplateReference: jobTemplateReference, + ScraperTemplate: scraperTemplateContent, + ScraperTemplateReference: scraperTemplateReference, + PvcTemplate: pvcTemplateContent, + PvcTemplateReference: pvcTemplateReference, } if gitBranch != "" || gitCommit != "" || gitPath != "" || gitWorkingDir != "" { @@ -139,6 +173,12 @@ func NewRunTestSuiteCmd() *cobra.Command { cmd.Flags().StringVarP(&gitPath, "git-path", "", "", "if repository is big we need to define additional path to directory/file to checkout partially") cmd.Flags().StringVarP(&gitWorkingDir, "git-working-dir", "", "", "if repository contains multiple directories with tests (like monorepo) and one starting directory we can set working directory parameter") cmd.Flags().StringVar(&runningContext, "context", "", "running context description for test suite execution") + cmd.Flags().StringVar(&jobTemplate, "job-template", "", "job template file path for extensions to job template") + cmd.Flags().StringVar(&scraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template") + cmd.Flags().StringVar(&pvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template") + cmd.Flags().StringVar(&jobTemplateReference, "job-template-reference", "", "reference to job template to use for the test") + cmd.Flags().StringVar(&scraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test") + cmd.Flags().StringVar(&pvcTemplateReference, "pvc-template-reference", "", "reference to pvc template to use for the test") return cmd } diff --git a/cmd/kubectl-testkube/commands/testsuites/update.go b/cmd/kubectl-testkube/commands/testsuites/update.go index 7e02ce52af9..6a0d3a30c0a 100644 --- a/cmd/kubectl-testkube/commands/testsuites/update.go +++ b/cmd/kubectl-testkube/commands/testsuites/update.go @@ -20,7 +20,14 @@ func UpdateTestSuitesCmd() *cobra.Command { httpProxy, httpsProxy string secretVariableReferences map[string]string timeout int32 + jobTemplate string cronJobTemplate string + scraperTemplate string + pvcTemplate string + jobTemplateReference string + cronJobTemplateReference string + scraperTemplateReference string + pvcTemplateReference string ) cmd := &cobra.Command{ @@ -65,7 +72,14 @@ func UpdateTestSuitesCmd() *cobra.Command { cmd.Flags().StringToStringVarP(&secretVariableReferences, "secret-variable-reference", "", nil, "secret variable references in a form name1=secret_name1=secret_key1") cmd.Flags().StringVar(&httpsProxy, "https-proxy", "", "https proxy for executor containers") cmd.Flags().Int32Var(&timeout, "timeout", 0, "duration in seconds for test suite to timeout. 0 disables timeout.") + cmd.Flags().StringVar(&jobTemplate, "job-template", "", "job template file path for extensions to job template") cmd.Flags().StringVar(&cronJobTemplate, "cronjob-template", "", "cron job template file path for extensions to cron job template") + cmd.Flags().StringVar(&scraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template") + cmd.Flags().StringVar(&pvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template") + cmd.Flags().StringVar(&jobTemplateReference, "job-template-reference", "", "reference to job template to use for the test") + cmd.Flags().StringVar(&cronJobTemplateReference, "cronjob-template-reference", "", "reference to cron job template to use for the test") + cmd.Flags().StringVar(&scraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test") + cmd.Flags().StringVar(&pvcTemplateReference, "pvc-template-reference", "", "reference to pvc template to use for the test") return cmd } diff --git a/internal/app/api/v1/executions_test.go b/internal/app/api/v1/executions_test.go index 53fac2e1b33..6bd91c58a07 100644 --- a/internal/app/api/v1/executions_test.go +++ b/internal/app/api/v1/executions_test.go @@ -245,9 +245,10 @@ func getMockExecutorClient() *executorsclientv1.ExecutorsClient { Namespace: "default", }, Spec: executorv1.ExecutorSpec{ - Types: []string{"curl/test"}, - ExecutorType: "", - JobTemplate: "", + Types: []string{"curl/test"}, + ExecutorType: "", + JobTemplate: "", + JobTemplateReference: "", }, Status: executorv1.ExecutorStatus{}, }, diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go index 52a1c7f4323..4d5f2ec4ad7 100644 --- a/internal/app/api/v1/server.go +++ b/internal/app/api/v1/server.go @@ -132,7 +132,7 @@ func NewTestkubeAPI( // will be reused in websockets handler s.WebsocketLoader = ws.NewWebsocketLoader() - s.Events.Loader.Register(webhook.NewWebhookLoader(webhookClient)) + s.Events.Loader.Register(webhook.NewWebhookLoader(webhookClient, templatesClient)) s.Events.Loader.Register(s.WebsocketLoader) s.Events.Loader.Register(s.slackLoader) diff --git a/internal/graphql/resolvers/executors_resolver_test.go b/internal/graphql/resolvers/executors_resolver_test.go index 48888f0e112..65b252657fc 100644 --- a/internal/graphql/resolvers/executors_resolver_test.go +++ b/internal/graphql/resolvers/executors_resolver_test.go @@ -15,18 +15,19 @@ var ( sample = testkube.ExecutorDetails{ Name: "sample", Executor: &testkube.Executor{ - ExecutorType: "job", - Image: "", - ImagePullSecrets: nil, - Command: nil, - Args: nil, - Types: []string{"curl/test"}, - Uri: "", - ContentTypes: nil, - JobTemplate: "", - Labels: map[string]string{"label-name": "label-value"}, - Features: nil, - Meta: nil, + ExecutorType: "job", + Image: "", + ImagePullSecrets: nil, + Command: nil, + Args: nil, + Types: []string{"curl/test"}, + Uri: "", + ContentTypes: nil, + JobTemplate: "", + JobTemplateReference: "", + Labels: map[string]string{"label-name": "label-value"}, + Features: nil, + Meta: nil, }, } ) diff --git a/internal/graphql/services/executors_test.go b/internal/graphql/services/executors_test.go index 58a04aa9312..45cad1eed6a 100644 --- a/internal/graphql/services/executors_test.go +++ b/internal/graphql/services/executors_test.go @@ -31,9 +31,10 @@ var ( }, }, Spec: executorv1.ExecutorSpec{ - Types: []string{"curl/test"}, - ExecutorType: "job", - JobTemplate: "", + Types: []string{"curl/test"}, + ExecutorType: "job", + JobTemplate: "", + JobTemplateReference: "", }, Status: executorv1.ExecutorStatus{}, }, @@ -41,18 +42,19 @@ var ( sample = testkube.ExecutorDetails{ Name: "sample", Executor: &testkube.Executor{ - ExecutorType: "job", - Image: "", - ImagePullSecrets: nil, - Command: nil, - Args: nil, - Types: []string{"curl/test"}, - Uri: "", - ContentTypes: nil, - JobTemplate: "", - Labels: map[string]string{"label-name": "label-value"}, - Features: nil, - Meta: nil, + ExecutorType: "job", + Image: "", + ImagePullSecrets: nil, + Command: nil, + Args: nil, + Types: []string{"curl/test"}, + Uri: "", + ContentTypes: nil, + JobTemplate: "", + JobTemplateReference: "", + Labels: map[string]string{"label-name": "label-value"}, + Features: nil, + Meta: nil, }, } k8sObjects2 = []k8sclient.Object{ @@ -69,9 +71,10 @@ var ( }, }, Spec: executorv1.ExecutorSpec{ - Types: []string{"other/test"}, - ExecutorType: "job", - JobTemplate: "", + Types: []string{"other/test"}, + ExecutorType: "job", + JobTemplate: "", + JobTemplateReference: "", }, Status: executorv1.ExecutorStatus{}, }, @@ -79,18 +82,19 @@ var ( sample2 = testkube.ExecutorDetails{ Name: "sample", Executor: &testkube.Executor{ - ExecutorType: "job", - Image: "", - ImagePullSecrets: nil, - Command: nil, - Args: nil, - Types: []string{"other/test"}, - Uri: "", - ContentTypes: nil, - JobTemplate: "", - Labels: map[string]string{"label-name": "label-value"}, - Features: nil, - Meta: nil, + ExecutorType: "job", + Image: "", + ImagePullSecrets: nil, + Command: nil, + Args: nil, + Types: []string{"other/test"}, + Uri: "", + ContentTypes: nil, + JobTemplate: "", + JobTemplateReference: "", + Labels: map[string]string{"label-name": "label-value"}, + Features: nil, + Meta: nil, }, } ) diff --git a/pkg/api/v1/client/interface.go b/pkg/api/v1/client/interface.go index 3ca1ba414ba..adc0e283e02 100644 --- a/pkg/api/v1/client/interface.go +++ b/pkg/api/v1/client/interface.go @@ -173,10 +173,14 @@ type ExecuteTestOptions struct { BucketName string ArtifactRequest *testkube.ArtifactRequest JobTemplate string + JobTemplateReference string ContentRequest *testkube.TestContentRequest PreRunScriptContent string PostRunScriptContent string ScraperTemplate string + ScraperTemplateReference string + PvcTemplate string + PvcTemplateReference string NegativeTest bool IsNegativeTestChangedOnRun bool EnvConfigMaps []testkube.EnvReference @@ -186,13 +190,19 @@ type ExecuteTestOptions struct { // ExecuteTestSuiteOptions contains test suite run options type ExecuteTestSuiteOptions struct { - ExecutionVariables map[string]testkube.Variable - HTTPProxy string - HTTPSProxy string - ExecutionLabels map[string]string - ContentRequest *testkube.TestContentRequest - RunningContext *testkube.RunningContext - ConcurrencyLevel int32 + ExecutionVariables map[string]testkube.Variable + HTTPProxy string + HTTPSProxy string + ExecutionLabels map[string]string + ContentRequest *testkube.TestContentRequest + RunningContext *testkube.RunningContext + ConcurrencyLevel int32 + JobTemplate string + JobTemplateReference string + ScraperTemplate string + ScraperTemplateReference string + PvcTemplate string + PvcTemplateReference string } // Gettable is an interface of gettable objects diff --git a/pkg/api/v1/client/test.go b/pkg/api/v1/client/test.go index 717c0207810..cb6bd8f7644 100644 --- a/pkg/api/v1/client/test.go +++ b/pkg/api/v1/client/test.go @@ -148,10 +148,14 @@ func (c TestClient) ExecuteTest(id, executionName string, options ExecuteTestOpt BucketName: options.BucketName, ArtifactRequest: options.ArtifactRequest, JobTemplate: options.JobTemplate, + JobTemplateReference: options.JobTemplateReference, ContentRequest: options.ContentRequest, PreRunScript: options.PreRunScriptContent, PostRunScript: options.PostRunScriptContent, ScraperTemplate: options.ScraperTemplate, + ScraperTemplateReference: options.ScraperTemplateReference, + PvcTemplate: options.PvcTemplate, + PvcTemplateReference: options.PvcTemplateReference, NegativeTest: options.NegativeTest, IsNegativeTestChangedOnRun: options.IsNegativeTestChangedOnRun, EnvConfigMaps: options.EnvConfigMaps, @@ -186,10 +190,14 @@ func (c TestClient) ExecuteTests(selector string, concurrencyLevel int, options BucketName: options.BucketName, ArtifactRequest: options.ArtifactRequest, JobTemplate: options.JobTemplate, + JobTemplateReference: options.JobTemplateReference, ContentRequest: options.ContentRequest, PreRunScript: options.PreRunScriptContent, PostRunScript: options.PostRunScriptContent, ScraperTemplate: options.ScraperTemplate, + ScraperTemplateReference: options.ScraperTemplateReference, + PvcTemplate: options.PvcTemplate, + PvcTemplateReference: options.PvcTemplateReference, NegativeTest: options.NegativeTest, IsNegativeTestChangedOnRun: options.IsNegativeTestChangedOnRun, RunningContext: options.RunningContext, diff --git a/pkg/api/v1/client/testsuite.go b/pkg/api/v1/client/testsuite.go index e3ecd8b3b60..797de0c2ad4 100644 --- a/pkg/api/v1/client/testsuite.go +++ b/pkg/api/v1/client/testsuite.go @@ -143,14 +143,20 @@ func (c TestSuiteClient) GetTestSuiteExecutionArtifacts(executionID string) (art func (c TestSuiteClient) ExecuteTestSuite(id, executionName string, options ExecuteTestSuiteOptions) (execution testkube.TestSuiteExecution, err error) { uri := c.testSuiteExecutionTransport.GetURI("/test-suites/%s/executions", id) executionRequest := testkube.TestSuiteExecutionRequest{ - Name: executionName, - Variables: options.ExecutionVariables, - HttpProxy: options.HTTPProxy, - HttpsProxy: options.HTTPSProxy, - ExecutionLabels: options.ExecutionLabels, - ContentRequest: options.ContentRequest, - RunningContext: options.RunningContext, - ConcurrencyLevel: options.ConcurrencyLevel, + Name: executionName, + Variables: options.ExecutionVariables, + HttpProxy: options.HTTPProxy, + HttpsProxy: options.HTTPSProxy, + ExecutionLabels: options.ExecutionLabels, + ContentRequest: options.ContentRequest, + RunningContext: options.RunningContext, + ConcurrencyLevel: options.ConcurrencyLevel, + JobTemplate: options.JobTemplate, + JobTemplateReference: options.JobTemplateReference, + ScraperTemplate: options.ScraperTemplate, + ScraperTemplateReference: options.ScraperTemplateReference, + PvcTemplate: options.PvcTemplate, + PvcTemplateReference: options.PvcTemplateReference, } body, err := json.Marshal(executionRequest) @@ -166,13 +172,19 @@ func (c TestSuiteClient) ExecuteTestSuite(id, executionName string, options Exec func (c TestSuiteClient) ExecuteTestSuites(selector string, concurrencyLevel int, options ExecuteTestSuiteOptions) (executions []testkube.TestSuiteExecution, err error) { uri := c.testSuiteExecutionTransport.GetURI("/test-suite-executions") executionRequest := testkube.TestSuiteExecutionRequest{ - Variables: options.ExecutionVariables, - HttpProxy: options.HTTPProxy, - HttpsProxy: options.HTTPSProxy, - ExecutionLabels: options.ExecutionLabels, - ContentRequest: options.ContentRequest, - RunningContext: options.RunningContext, - ConcurrencyLevel: options.ConcurrencyLevel, + Variables: options.ExecutionVariables, + HttpProxy: options.HTTPProxy, + HttpsProxy: options.HTTPSProxy, + ExecutionLabels: options.ExecutionLabels, + ContentRequest: options.ContentRequest, + RunningContext: options.RunningContext, + ConcurrencyLevel: options.ConcurrencyLevel, + JobTemplate: options.JobTemplate, + JobTemplateReference: options.JobTemplateReference, + ScraperTemplate: options.ScraperTemplate, + ScraperTemplateReference: options.ScraperTemplateReference, + PvcTemplate: options.PvcTemplate, + PvcTemplateReference: options.PvcTemplateReference, } body, err := json.Marshal(executionRequest) diff --git a/pkg/crd/templates/executor.tmpl b/pkg/crd/templates/executor.tmpl index 837797f697e..1f206f07478 100644 --- a/pkg/crd/templates/executor.tmpl +++ b/pkg/crd/templates/executor.tmpl @@ -28,6 +28,9 @@ spec: {{- if .JobTemplate }} job_template: {{ .JobTemplate }} {{- end }} + {{- if .JobTemplateReference }} + jobTemplateReference: {{ .JobTemplateReference }} + {{- end }} {{- if gt (len .Args) 0 }} args: {{- range $arg := .Args}} diff --git a/pkg/crd/templates/test.tmpl b/pkg/crd/templates/test.tmpl index 8a52fe7e0c9..b13cf03e639 100644 --- a/pkg/crd/templates/test.tmpl +++ b/pkg/crd/templates/test.tmpl @@ -82,7 +82,7 @@ spec: schedule: {{ .Schedule }} {{- end }} {{- if .ExecutionRequest }} - {{- if or (.ExecutionRequest.Name) (.ExecutionRequest.NegativeTest) (.ExecutionRequest.VariablesFile) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (ne (len .ExecutionRequest.Variables) 0) (ne (len .ExecutionRequest.Args) 0) (ne (len .ExecutionRequest.Envs) 0) (ne (len .ExecutionRequest.SecretEnvs) 0) (.ExecutionRequest.Image) (ne (len .ExecutionRequest.Command) 0) (.ExecutionRequest.ArgsMode) (ne (len .ExecutionRequest.ImagePullSecrets) 0) (ne .ExecutionRequest.ActiveDeadlineSeconds 0) (.ExecutionRequest.ArtifactRequest) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.PreRunScript) (.ExecutionRequest.PostRunScript) (.ExecutionRequest.ScraperTemplate) (ne (len .ExecutionRequest.EnvConfigMaps) 0) (ne (len .ExecutionRequest.EnvSecrets) 0) }} + {{- if or (.ExecutionRequest.Name) (.ExecutionRequest.NegativeTest) (.ExecutionRequest.VariablesFile) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (ne (len .ExecutionRequest.Variables) 0) (ne (len .ExecutionRequest.Args) 0) (ne (len .ExecutionRequest.Envs) 0) (ne (len .ExecutionRequest.SecretEnvs) 0) (.ExecutionRequest.Image) (ne (len .ExecutionRequest.Command) 0) (.ExecutionRequest.ArgsMode) (ne (len .ExecutionRequest.ImagePullSecrets) 0) (ne .ExecutionRequest.ActiveDeadlineSeconds 0) (.ExecutionRequest.ArtifactRequest) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.JobTemplateReference) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.CronJobTemplateReference) (.ExecutionRequest.PreRunScript) (.ExecutionRequest.PostRunScript) (.ExecutionRequest.ScraperTemplate) (.ExecutionRequest.ScraperTemplateReference) (.ExecutionRequest.PvcTemplate) (.ExecutionRequest.PvcTemplateReference) (ne (len .ExecutionRequest.EnvConfigMaps) 0) (ne (len .ExecutionRequest.EnvSecrets) 0) }} executionRequest: {{- if .ExecutionRequest.Name }} name: {{ .ExecutionRequest.Name }} @@ -198,9 +198,15 @@ spec: {{- if .ExecutionRequest.JobTemplate }} jobTemplate: {{ .ExecutionRequest.JobTemplate }} {{- end }} + {{- if .ExecutionRequest.JobTemplateReference }} + jobTemplateReference: {{ .ExecutionRequest.JobTemplateReference }} + {{- end }} {{- if .ExecutionRequest.CronJobTemplate }} cronJobTemplate: {{ .ExecutionRequest.CronJobTemplate }} {{- end }} + {{- if .ExecutionRequest.CronJobTemplateReference }} + cronJobTemplateReference: {{ .ExecutionRequest.CronJobTemplateReference }} + {{- end }} {{- if .ExecutionRequest.PreRunScript }} preRunScript: {{ .ExecutionRequest.PreRunScript }} {{- end }} @@ -210,6 +216,15 @@ spec: {{- if .ExecutionRequest.ScraperTemplate }} scraperTemplate: {{ .ExecutionRequest.ScraperTemplate }} {{- end }} + {{- if .ExecutionRequest.ScraperTemplateReference }} + scraperTemplateReference: {{ .ExecutionRequest.ScraperTemplateReference }} + {{- end }} + {{- if .ExecutionRequest.PvcTemplate }} + pvcTemplate: {{ .ExecutionRequest.PvcTemplate }} + {{- end }} + {{- if .ExecutionRequest.PvcTemplateReference }} + pvcTemplateReference: {{ .ExecutionRequest.PvcTemplateReference }} + {{- end }} {{- if ne (len .ExecutionRequest.EnvConfigMaps) 0 }} envConfigMaps: {{- range $configMap := .ExecutionRequest.EnvConfigMaps }} diff --git a/pkg/crd/templates/testsuite.tmpl b/pkg/crd/templates/testsuite.tmpl index bb48c772cec..fedb3ce8a7d 100644 --- a/pkg/crd/templates/testsuite.tmpl +++ b/pkg/crd/templates/testsuite.tmpl @@ -71,7 +71,7 @@ spec: repeats: {{ .Repeats }} {{- end }} {{- if .ExecutionRequest }} - {{- if or (.ExecutionRequest.Name) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (ne (len .ExecutionRequest.Variables) 0) (ne .ExecutionRequest.Timeout 0) (.ExecutionRequest.CronJobTemplate)}} + {{- if or (.ExecutionRequest.Name) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (ne (len .ExecutionRequest.Variables) 0) (ne .ExecutionRequest.Timeout 0) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.JobTemplateReference) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.CronJobTemplateReference) (.ExecutionRequest.ScraperTemplate) (.ExecutionRequest.ScraperTemplateReference) (.ExecutionRequest.PvcTemplate) (.ExecutionRequest.PvcTemplateReference)}} executionRequest: {{- if .ExecutionRequest.Name }} name: {{ .ExecutionRequest.Name }} @@ -118,9 +118,30 @@ spec: {{- if ne .ExecutionRequest.Timeout 0 }} timeout: {{ .ExecutionRequest.Timeout }} {{- end}} + {{- if .ExecutionRequest.JobTemplate }} + jobTemplate: {{ .ExecutionRequest.JobTemplate }} + {{- end}} + {{- if .ExecutionRequest.JobTemplateReference }} + jobTemplateReference: {{ .ExecutionRequest.JobTemplateReference }} + {{- end}} {{- if .ExecutionRequest.CronJobTemplate }} cronJobTemplate: {{ .ExecutionRequest.CronJobTemplate }} {{- end}} + {{- if .ExecutionRequest.CronJobTemplateReference }} + cronJobTemplateReference: {{ .ExecutionRequest.CronJobTemplateReference }} + {{- end}} + {{- if .ExecutionRequest.ScraperTemplate }} + scraperTemplate: {{ .ExecutionRequest.ScraperTemplate }} + {{- end }} + {{- if .ExecutionRequest.ScraperTemplateReference }} + scraperTemplateReference: {{ .ExecutionRequest.ScraperTemplateReference }} + {{- end }} + {{- if .ExecutionRequest.PvcTemplate }} + pvcTemplate: {{ .ExecutionRequest.PvcTemplate }} + {{- end }} + {{- if .ExecutionRequest.PvcTemplateReference }} + pvcTemplateReference: {{ .ExecutionRequest.PvcTemplateReference }} + {{- end }} {{- end }} {{- end }} {{- if .Status }} diff --git a/pkg/crd/templates/webhook.tmpl b/pkg/crd/templates/webhook.tmpl index 8302e9a7523..dae15d9bb7f 100644 --- a/pkg/crd/templates/webhook.tmpl +++ b/pkg/crd/templates/webhook.tmpl @@ -28,6 +28,9 @@ spec: {{- if .PayloadTemplate }} payloadTemplate: {{ .PayloadTemplate }} {{- end }} + {{- if .PayloadTemplateReference }} + payloadTemplateReference: {{ .PayloadTemplateReference }} + {{- end }} {{- if ne (len .Headers) 0 }} headers: {{- range $key, $value := .Headers }} diff --git a/pkg/cronjob/client.go b/pkg/cronjob/client.go index 96ae56af51b..2943c34883d 100644 --- a/pkg/cronjob/client.go +++ b/pkg/cronjob/client.go @@ -35,6 +35,7 @@ type CronJobOptions struct { Resource string Data string Labels map[string]string + CronJobTemplate string CronJobTemplateExtensions string } @@ -84,6 +85,11 @@ func (c *Client) Get(name string) (*v1.CronJob, error) { // Apply is a method to create or update a cron job func (c *Client) Apply(id, name string, options CronJobOptions) error { + template := c.cronJobTemplate + if options.CronJobTemplate != "" { + template = options.CronJobTemplate + } + cronJobClient := c.ClientSet.BatchV1().CronJobs(c.Namespace) ctx := context.Background() @@ -95,7 +101,7 @@ func (c *Client) Apply(id, name string, options CronJobOptions) error { ServicePort: c.servicePort, Schedule: options.Schedule, Resource: options.Resource, - CronJobTemplate: c.cronJobTemplate, + CronJobTemplate: template, CronJobTemplateExtensions: options.CronJobTemplateExtensions, Data: options.Data, Labels: options.Labels, diff --git a/pkg/event/kind/webhook/loader.go b/pkg/event/kind/webhook/loader.go index a47d708590f..c465629e737 100644 --- a/pkg/event/kind/webhook/loader.go +++ b/pkg/event/kind/webhook/loader.go @@ -4,6 +4,8 @@ import ( "fmt" executorsv1 "github.com/kubeshop/testkube-operator/apis/executor/v1" + templatesclientv1 "github.com/kubeshop/testkube-operator/client/templates/v1" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/event/kind/common" "github.com/kubeshop/testkube/pkg/mapper/webhooks" ) @@ -15,14 +17,16 @@ type WebhooksLister interface { List(selector string) (*executorsv1.WebhookList, error) } -func NewWebhookLoader(webhooksClient WebhooksLister) *WebhooksLoader { +func NewWebhookLoader(webhooksClient WebhooksLister, templatesClient templatesclientv1.Interface) *WebhooksLoader { return &WebhooksLoader{ - WebhooksClient: webhooksClient, + WebhooksClient: webhooksClient, + templatesClient: templatesClient, } } type WebhooksLoader struct { - WebhooksClient WebhooksLister + WebhooksClient WebhooksLister + templatesClient templatesclientv1.Interface } func (r WebhooksLoader) Kind() string { @@ -38,9 +42,25 @@ func (r WebhooksLoader) Load() (listeners common.Listeners, err error) { // and create listeners for each webhook spec for _, webhook := range webhookList.Items { + payloadTemplate := "" + if webhook.Spec.PayloadTemplateReference != "" { + template, err := r.templatesClient.Get(webhook.Spec.PayloadTemplateReference) + if err != nil { + return listeners, err + } + + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.WEBHOOK_TemplateType { + payloadTemplate = template.Spec.Body + } + } + + if webhook.Spec.PayloadTemplate != "" { + payloadTemplate = webhook.Spec.PayloadTemplate + } + types := webhooks.MapEventArrayToCRDEvents(webhook.Spec.Events) name := fmt.Sprintf("%s.%s", webhook.ObjectMeta.Namespace, webhook.ObjectMeta.Name) - listeners = append(listeners, NewWebhookListener(name, webhook.Spec.Uri, webhook.Spec.Selector, types, webhook.Spec.PayloadObjectField, webhook.Spec.PayloadTemplate, webhook.Spec.Headers)) + listeners = append(listeners, NewWebhookListener(name, webhook.Spec.Uri, webhook.Spec.Selector, types, webhook.Spec.PayloadObjectField, payloadTemplate, webhook.Spec.Headers)) } return listeners, nil diff --git a/pkg/event/kind/webhook/loader_test.go b/pkg/event/kind/webhook/loader_test.go index 9c137b148eb..ce1bf775892 100644 --- a/pkg/event/kind/webhook/loader_test.go +++ b/pkg/event/kind/webhook/loader_test.go @@ -3,9 +3,11 @@ package webhook import ( "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" executorsv1 "github.com/kubeshop/testkube-operator/apis/executor/v1" + templatesclientv1 "github.com/kubeshop/testkube-operator/client/templates/v1" ) type DummyLoader struct { @@ -21,7 +23,12 @@ func (l DummyLoader) List(selector string) (*executorsv1.WebhookList, error) { func TestWebhookLoader(t *testing.T) { t.Parallel() - webhooksLoader := NewWebhookLoader(&DummyLoader{}) + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl) + webhooksLoader := NewWebhookLoader(&DummyLoader{}, mockTemplatesClient) listeners, err := webhooksLoader.Load() assert.Equal(t, 1, len(listeners)) diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go index 5cdcaf5a443..d8926fd2aa2 100644 --- a/pkg/executor/client/job.go +++ b/pkg/executor/client/job.go @@ -306,7 +306,8 @@ func (c *JobExecutor) MonitorJobForTimeout(ctx context.Context, jobName string) // CreateJob creates new Kubernetes job based on execution and execute options func (c *JobExecutor) CreateJob(ctx context.Context, execution testkube.Execution, options ExecuteOptions) error { jobs := c.ClientSet.BatchV1().Jobs(c.Namespace) - jobOptions, err := NewJobOptions(c.images.Init, c.jobTemplate, c.serviceAccountName, c.registry, c.clusterID, execution, options) + jobOptions, err := NewJobOptions(c.templatesClient, c.images.Init, c.jobTemplate, c.serviceAccountName, c.registry, + c.clusterID, execution, options) if err != nil { return err } @@ -777,7 +778,7 @@ func NewJobSpec(log *zap.SugaredLogger, options JobOptions) (*batchv1.Job, error return &job, nil } -func NewJobOptions(initImage, jobTemplate string, serviceAccountName, registry, clusterID string, +func NewJobOptions(templatesClient templatesv1.Interface, initImage, jobTemplate, serviceAccountName, registry, clusterID string, execution testkube.Execution, options ExecuteOptions) (jobOptions JobOptions, err error) { jsn, err := json.Marshal(execution) if err != nil { @@ -793,6 +794,16 @@ func NewJobOptions(initImage, jobTemplate string, serviceAccountName, registry, if jobOptions.JobTemplate == "" { jobOptions.JobTemplate = jobTemplate } + + if options.Request.JobTemplateReference != "" { + template, err := templatesClient.Get(options.Request.JobTemplateReference) + if err != nil { + return jobOptions, err + } + + jobOptions.JobTemplate = template.Spec.Body + } + jobOptions.Variables = execution.Variables jobOptions.ServiceAccountName = serviceAccountName jobOptions.Registry = registry diff --git a/pkg/executor/containerexecutor/containerexecutor.go b/pkg/executor/containerexecutor/containerexecutor.go index 344748e6d43..0f3f0c05516 100644 --- a/pkg/executor/containerexecutor/containerexecutor.go +++ b/pkg/executor/containerexecutor/containerexecutor.go @@ -133,7 +133,7 @@ type JobOptions struct { ScraperImage string JobTemplate string ScraperTemplate string - PVCTemplate string + PvcTemplate string SecretEnvs map[string]string Envs map[string]string HTTPProxy string @@ -148,6 +148,7 @@ type JobOptions struct { DelaySeconds int JobTemplateExtensions string ScraperTemplateExtensions string + PvcTemplateExtensions string EnvConfigMaps []testkube.EnvReference EnvSecrets []testkube.EnvReference Labels map[string]string @@ -288,7 +289,8 @@ func (c *ContainerExecutor) ExecuteSync(ctx context.Context, execution *testkube func (c *ContainerExecutor) createJob(ctx context.Context, execution testkube.Execution, options client.ExecuteOptions) (*JobOptions, error) { jobsClient := c.clientSet.BatchV1().Jobs(c.namespace) - jobOptions, err := NewJobOptions(c.log, c.images, c.templates, c.serviceAccountName, c.registry, c.clusterID, execution, options) + jobOptions, err := NewJobOptions(c.log, c.templatesClient, c.images, c.templates, c.serviceAccountName, + c.registry, c.clusterID, execution, options) if err != nil { return nil, err } @@ -665,6 +667,7 @@ func NewJobOptionsFromExecutionOptions(options client.ExecuteOptions) *JobOption JobTemplate: options.ExecutorSpec.JobTemplate, JobTemplateExtensions: options.Request.JobTemplate, ScraperTemplateExtensions: options.Request.ScraperTemplate, + PvcTemplateExtensions: options.Request.PvcTemplate, EnvConfigMaps: options.Request.EnvConfigMaps, EnvSecrets: options.Request.EnvSecrets, Labels: labels, diff --git a/pkg/executor/containerexecutor/containerexecutor_test.go b/pkg/executor/containerexecutor/containerexecutor_test.go index ff7f443434e..4cf5d73ac7e 100644 --- a/pkg/executor/containerexecutor/containerexecutor_test.go +++ b/pkg/executor/containerexecutor/containerexecutor_test.go @@ -5,8 +5,7 @@ import ( "testing" "time" - "github.com/kubeshop/testkube/pkg/repository/result" - + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" @@ -16,10 +15,12 @@ import ( executorv1 "github.com/kubeshop/testkube-operator/apis/executor/v1" testsv3 "github.com/kubeshop/testkube-operator/apis/tests/v3" + templatesclientv1 "github.com/kubeshop/testkube-operator/client/templates/v1" v3 "github.com/kubeshop/testkube-operator/client/tests/v3" "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/executor" "github.com/kubeshop/testkube/pkg/executor/client" + "github.com/kubeshop/testkube/pkg/repository/result" ) var ctx = context.Background() @@ -76,13 +77,18 @@ func TestNewExecutorJobSpecEmptyArgs(t *testing.T) { t.Parallel() jobOptions := &JobOptions{ - Name: "name", - Namespace: "namespace", - InitImage: "kubeshop/testkube-init-executor:0.7.10", - Image: "ubuntu", - JobTemplate: defaultJobTemplate, - Command: []string{}, - Args: []string{}, + Name: "name", + Namespace: "namespace", + InitImage: "kubeshop/testkube-init-executor:0.7.10", + Image: "ubuntu", + JobTemplate: defaultJobTemplate, + ScraperTemplate: "", + PvcTemplate: "", + JobTemplateExtensions: "", + ScraperTemplateExtensions: "", + PvcTemplateExtensions: "", + Command: []string{}, + Args: []string{}, } spec, err := NewExecutorJobSpec(logger(), jobOptions) assert.NoError(t, err) @@ -93,17 +99,22 @@ func TestNewExecutorJobSpecWithArgs(t *testing.T) { t.Parallel() jobOptions := &JobOptions{ - Name: "name", - Namespace: "namespace", - InitImage: "kubeshop/testkube-init-executor:0.7.10", - Image: "curl", - JobTemplate: defaultJobTemplate, - ImagePullSecrets: []string{"secret-name"}, - Command: []string{"/bin/curl"}, - Args: []string{"-v", "https://testkube.kubeshop.io"}, - ActiveDeadlineSeconds: 100, - Envs: map[string]string{"key": "value"}, - Variables: map[string]testkube.Variable{"aa": {Name: "aa", Value: "bb", Type_: testkube.VariableTypeBasic}}, + Name: "name", + Namespace: "namespace", + InitImage: "kubeshop/testkube-init-executor:0.7.10", + Image: "curl", + JobTemplate: defaultJobTemplate, + ScraperTemplate: "", + PvcTemplate: "", + JobTemplateExtensions: "", + ScraperTemplateExtensions: "", + PvcTemplateExtensions: "", + ImagePullSecrets: []string{"secret-name"}, + Command: []string{"/bin/curl"}, + Args: []string{"-v", "https://testkube.kubeshop.io"}, + ActiveDeadlineSeconds: 100, + Envs: map[string]string{"key": "value"}, + Variables: map[string]testkube.Variable{"aa": {Name: "aa", Value: "bb", Type_: testkube.VariableTypeBasic}}, } spec, err := NewExecutorJobSpec(logger(), jobOptions) assert.NoError(t, err) @@ -140,13 +151,18 @@ func TestNewExecutorJobSpecWithoutInitImage(t *testing.T) { t.Parallel() jobOptions := &JobOptions{ - Name: "name", - Namespace: "namespace", - InitImage: "", - Image: "ubuntu", - JobTemplate: defaultJobTemplate, - Command: []string{}, - Args: []string{}, + Name: "name", + Namespace: "namespace", + InitImage: "", + Image: "ubuntu", + JobTemplate: defaultJobTemplate, + ScraperTemplate: "", + PvcTemplate: "", + JobTemplateExtensions: "", + ScraperTemplateExtensions: "", + PvcTemplateExtensions: "", + Command: []string{}, + Args: []string{}, } spec, err := NewExecutorJobSpec(logger(), jobOptions) assert.NoError(t, err) @@ -156,8 +172,14 @@ func TestNewExecutorJobSpecWithoutInitImage(t *testing.T) { func TestNewExecutorJobSpecWithWorkingDirRelative(t *testing.T) { t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl) + jobOptions, _ := NewJobOptions( logger(), + mockTemplatesClient, executor.Images{}, executor.Templates{}, "", @@ -191,8 +213,14 @@ func TestNewExecutorJobSpecWithWorkingDirRelative(t *testing.T) { func TestNewExecutorJobSpecWithWorkingDirAbsolute(t *testing.T) { t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl) + jobOptions, _ := NewJobOptions( logger(), + mockTemplatesClient, executor.Images{}, executor.Templates{}, "", @@ -226,8 +254,14 @@ func TestNewExecutorJobSpecWithWorkingDirAbsolute(t *testing.T) { func TestNewExecutorJobSpecWithoutWorkingDir(t *testing.T) { t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl) + jobOptions, _ := NewJobOptions( logger(), + mockTemplatesClient, executor.Images{}, executor.Templates{}, "", diff --git a/pkg/executor/containerexecutor/tmpl.go b/pkg/executor/containerexecutor/tmpl.go index 3972da90089..1f0e6d98856 100644 --- a/pkg/executor/containerexecutor/tmpl.go +++ b/pkg/executor/containerexecutor/tmpl.go @@ -18,6 +18,7 @@ import ( kyaml "sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml/merge2" + templatesv1 "github.com/kubeshop/testkube-operator/client/templates/v1" "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/executor" "github.com/kubeshop/testkube/pkg/executor/client" @@ -197,7 +198,7 @@ func NewScraperJobSpec(log *zap.SugaredLogger, options *JobOptions) (*batchv1.Jo // NewPersistentVolumeClaimSpec is a method to create new persistent volume claim spec func NewPersistentVolumeClaimSpec(log *zap.SugaredLogger, options *JobOptions) (*corev1.PersistentVolumeClaim, error) { - tmpl, err := template.New("volume-claim").Parse(options.PVCTemplate) + tmpl, err := template.New("volume-claim").Parse(options.PvcTemplate) if err != nil { return nil, fmt.Errorf("creating volume claim spec from pvc template error: %w", err) } @@ -209,6 +210,21 @@ func NewPersistentVolumeClaimSpec(log *zap.SugaredLogger, options *JobOptions) ( var pvc corev1.PersistentVolumeClaim pvcSpec := buffer.String() + if options.PvcTemplateExtensions != "" { + tmplExt, err := template.New("jobExt").Parse(options.PvcTemplateExtensions) + if err != nil { + return nil, fmt.Errorf("creating pvc extensions spec from executor template error: %w", err) + } + + var bufferExt bytes.Buffer + if err = tmplExt.ExecuteTemplate(&bufferExt, "jobExt", options); err != nil { + return nil, fmt.Errorf("executing pvc extensions spec executor template: %w", err) + } + + if pvcSpec, err = merge2.MergeStrings(bufferExt.String(), pvcSpec, false, kyaml.MergeOptions{}); err != nil { + return nil, fmt.Errorf("merging spvc spec executor templates: %w", err) + } + } log.Debug("Volume claim specification", pvcSpec) decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(pvcSpec), len(pvcSpec)) @@ -253,7 +269,7 @@ func InspectDockerImage(namespace, registry, image string, imageSecrets []string } // NewJobOptions provides job options for templates -func NewJobOptions(log *zap.SugaredLogger, images executor.Images, templates executor.Templates, +func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface, images executor.Images, templates executor.Templates, serviceAccountName, registry, clusterID string, execution testkube.Execution, options client.ExecuteOptions) (*JobOptions, error) { jobOptions := NewJobOptionsFromExecutionOptions(options) if execution.PreRunScript != "" || execution.PostRunScript != "" { @@ -290,8 +306,41 @@ func NewJobOptions(log *zap.SugaredLogger, images executor.Images, templates exe } } + if options.Request.JobTemplateReference != "" { + template, err := templatesClient.Get(options.Request.JobTemplateReference) + if err != nil { + return nil, err + } + + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { + jobOptions.JobTemplate = template.Spec.Body + } + } + jobOptions.ScraperTemplate = templates.Scraper - jobOptions.PVCTemplate = templates.PVC + if options.Request.ScraperTemplateReference != "" { + template, err := templatesClient.Get(options.Request.ScraperTemplateReference) + if err != nil { + return nil, err + } + + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.SCRAPER_TemplateType { + jobOptions.ScraperTemplate = template.Spec.Body + } + } + + jobOptions.PvcTemplate = templates.PVC + if options.Request.PvcTemplateReference != "" { + template, err := templatesClient.Get(options.Request.PvcTemplateReference) + if err != nil { + return nil, err + } + + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.PVC_TemplateType { + jobOptions.PvcTemplate = template.Spec.Body + } + } + jobOptions.Variables = execution.Variables jobOptions.ServiceAccountName = serviceAccountName jobOptions.Registry = registry diff --git a/pkg/mapper/testsuites/openapi_kube.go b/pkg/mapper/testsuites/openapi_kube.go index 94e34e85294..75015201847 100644 --- a/pkg/mapper/testsuites/openapi_kube.go +++ b/pkg/mapper/testsuites/openapi_kube.go @@ -200,17 +200,24 @@ func MapExecutionRequestToSpecExecutionRequest(executionRequest *testkube.TestSu } return &testsuitesv3.TestSuiteExecutionRequest{ - Name: executionRequest.Name, - Labels: executionRequest.Labels, - ExecutionLabels: executionRequest.ExecutionLabels, - Namespace: executionRequest.Namespace, - Variables: MapCRDVariables(executionRequest.Variables), - SecretUUID: executionRequest.SecretUUID, - Sync: executionRequest.Sync, - HttpProxy: executionRequest.HttpProxy, - HttpsProxy: executionRequest.HttpsProxy, - Timeout: executionRequest.Timeout, - CronJobTemplate: executionRequest.CronJobTemplate, + Name: executionRequest.Name, + Labels: executionRequest.Labels, + ExecutionLabels: executionRequest.ExecutionLabels, + Namespace: executionRequest.Namespace, + Variables: MapCRDVariables(executionRequest.Variables), + SecretUUID: executionRequest.SecretUUID, + Sync: executionRequest.Sync, + HttpProxy: executionRequest.HttpProxy, + HttpsProxy: executionRequest.HttpsProxy, + Timeout: executionRequest.Timeout, + JobTemplate: executionRequest.JobTemplate, + JobTemplateReference: executionRequest.JobTemplateReference, + CronJobTemplate: executionRequest.CronJobTemplate, + CronJobTemplateReference: executionRequest.CronJobTemplateReference, + ScraperTemplate: executionRequest.ScraperTemplate, + ScraperTemplateReference: executionRequest.ScraperTemplateReference, + PvcTemplate: executionRequest.PvcTemplate, + PvcTemplateReference: executionRequest.PvcTemplateReference, } } @@ -318,10 +325,38 @@ func MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest *testkube. executionRequest.HttpsProxy, &request.HttpsProxy, }, + { + executionRequest.JobTemplate, + &request.JobTemplate, + }, + { + executionRequest.JobTemplateReference, + &request.JobTemplateReference, + }, { executionRequest.CronJobTemplate, &request.CronJobTemplate, }, + { + executionRequest.CronJobTemplateReference, + &request.CronJobTemplateReference, + }, + { + executionRequest.ScraperTemplate, + &request.ScraperTemplate, + }, + { + executionRequest.ScraperTemplateReference, + &request.ScraperTemplateReference, + }, + { + executionRequest.PvcTemplate, + &request.PvcTemplate, + }, + { + executionRequest.PvcTemplateReference, + &request.PvcTemplateReference, + }, } for _, field := range fields { diff --git a/pkg/scheduler/test_scheduler.go b/pkg/scheduler/test_scheduler.go index 8ffb3f0a1c5..b57839b9311 100644 --- a/pkg/scheduler/test_scheduler.go +++ b/pkg/scheduler/test_scheduler.go @@ -290,6 +290,10 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe test.ExecutionRequest.JobTemplate, &request.JobTemplate, }, + { + test.ExecutionRequest.JobTemplateReference, + &request.JobTemplateReference, + }, { test.ExecutionRequest.PreRunScript, &request.PreRunScript, @@ -302,6 +306,18 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe test.ExecutionRequest.ScraperTemplate, &request.ScraperTemplate, }, + { + test.ExecutionRequest.ScraperTemplateReference, + &request.ScraperTemplateReference, + }, + { + test.ExecutionRequest.PvcTemplate, + &request.PvcTemplate, + }, + { + test.ExecutionRequest.PvcTemplateReference, + &request.PvcTemplateReference, + }, { test.ExecutionRequest.ArgsMode, &request.ArgsMode, diff --git a/pkg/scheduler/test_scheduler_test.go b/pkg/scheduler/test_scheduler_test.go index 5332c90b237..5e276bcd07c 100644 --- a/pkg/scheduler/test_scheduler_test.go +++ b/pkg/scheduler/test_scheduler_test.go @@ -94,17 +94,18 @@ func TestGetExecuteOptions(t *testing.T) { TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Namespace: "testkube", Name: "cypress"}, Spec: v1.ExecutorSpec{ - Types: []string{mockExecutorTypes}, - ExecutorType: "job", - URI: "", - Image: "cypress", - Args: []string{}, - Command: []string{"run"}, - ImagePullSecrets: []k8sv1.LocalObjectReference{{Name: "secret-name1"}, {Name: "secret-name2"}}, - Features: nil, - ContentTypes: nil, - JobTemplate: "", - Meta: nil, + Types: []string{mockExecutorTypes}, + ExecutorType: "job", + URI: "", + Image: "cypress", + Args: []string{}, + Command: []string{"run"}, + ImagePullSecrets: []k8sv1.LocalObjectReference{{Name: "secret-name1"}, {Name: "secret-name2"}}, + Features: nil, + ContentTypes: nil, + JobTemplate: "", + JobTemplateReference: "", + Meta: nil, }, } @@ -130,17 +131,22 @@ func TestGetExecuteOptions(t *testing.T) { SecretEnvs: map[string]string{ "secretEnv": "secretVar", }, - Sync: false, - HttpProxy: "", - HttpsProxy: "", - Uploads: []string{}, - ActiveDeadlineSeconds: 10, - ArtifactRequest: &testkube.ArtifactRequest{}, - JobTemplate: "", - CronJobTemplate: "", - PreRunScript: "", - PostRunScript: "", - ScraperTemplate: "", + Sync: false, + HttpProxy: "", + HttpsProxy: "", + Uploads: []string{}, + ActiveDeadlineSeconds: 10, + ArtifactRequest: &testkube.ArtifactRequest{}, + JobTemplate: "", + JobTemplateReference: "", + CronJobTemplate: "", + CronJobTemplateReference: "", + PreRunScript: "", + PostRunScript: "", + ScraperTemplate: "", + ScraperTemplateReference: "", + PvcTemplate: "", + PvcTemplateReference: "", EnvConfigMaps: []testkube.EnvReference{ { Reference: &testkube.LocalObjectReference{ diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go index 9edbf55afa7..3c860c7b927 100644 --- a/pkg/scheduler/testsuite_scheduler.go +++ b/pkg/scheduler/testsuite_scheduler.go @@ -71,6 +71,30 @@ func (s *Scheduler) executeTestSuite(ctx context.Context, testSuite testkube.Tes if request.Timeout == 0 && testSuite.ExecutionRequest.Timeout != 0 { request.Timeout = testSuite.ExecutionRequest.Timeout } + + if request.JobTemplate == "" && testSuite.ExecutionRequest.JobTemplate != "" { + request.JobTemplate = testSuite.ExecutionRequest.JobTemplate + } + + if request.JobTemplateReference == "" && testSuite.ExecutionRequest.JobTemplateReference != "" { + request.JobTemplateReference = testSuite.ExecutionRequest.JobTemplateReference + } + + if request.ScraperTemplate == "" && testSuite.ExecutionRequest.ScraperTemplate != "" { + request.ScraperTemplate = testSuite.ExecutionRequest.ScraperTemplate + } + + if request.ScraperTemplateReference == "" && testSuite.ExecutionRequest.ScraperTemplateReference != "" { + request.ScraperTemplateReference = testSuite.ExecutionRequest.ScraperTemplateReference + } + + if request.PvcTemplate == "" && testSuite.ExecutionRequest.PvcTemplate != "" { + request.PvcTemplate = testSuite.ExecutionRequest.PvcTemplate + } + + if request.PvcTemplateReference == "" && testSuite.ExecutionRequest.PvcTemplateReference != "" { + request.PvcTemplateReference = testSuite.ExecutionRequest.PvcTemplateReference + } } s.logger.Infow("Executing testsuite", "test", testSuite.Name, "request", request, "ExecutionRequest", testSuite.ExecutionRequest) @@ -413,6 +437,12 @@ func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution test Type_: string(testkube.RunningContextTypeTestSuite), Context: testsuiteExecution.Name, }, + JobTemplate: request.JobTemplate, + JobTemplateReference: request.JobTemplateReference, + ScraperTemplate: request.ScraperTemplate, + ScraperTemplateReference: request.ScraperTemplateReference, + PvcTemplate: request.PvcTemplate, + PvcTemplateReference: request.PvcTemplateReference, } requests := make([]workerpool.Request[testkube.Test, testkube.ExecutionRequest, testkube.Execution], len(testTuples)) diff --git a/pkg/triggers/executor_test.go b/pkg/triggers/executor_test.go index dd795361600..e08793a2008 100644 --- a/pkg/triggers/executor_test.go +++ b/pkg/triggers/executor_test.go @@ -80,17 +80,18 @@ func TestExecute(t *testing.T) { TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Namespace: "testkube", Name: "cypress"}, Spec: v1.ExecutorSpec{ - Types: []string{mockExecutorTypes}, - ExecutorType: "job", - URI: "", - Image: "cypress", - Args: nil, - Command: []string{"run"}, - ImagePullSecrets: nil, - Features: nil, - ContentTypes: nil, - JobTemplate: "", - Meta: nil, + Types: []string{mockExecutorTypes}, + ExecutorType: "job", + URI: "", + Image: "cypress", + Args: nil, + Command: []string{"run"}, + ImagePullSecrets: nil, + Features: nil, + ContentTypes: nil, + JobTemplate: "", + JobTemplateReference: "", + Meta: nil, }, } mockExecutorsClient.EXPECT().GetByType(mockExecutorTypes).Return(&mockExecutorV1, nil).AnyTimes() diff --git a/pkg/triggers/service_test.go b/pkg/triggers/service_test.go index 4d8ca820f4b..df9a9f47c39 100644 --- a/pkg/triggers/service_test.go +++ b/pkg/triggers/service_test.go @@ -86,17 +86,18 @@ func TestService_Run(t *testing.T) { TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Namespace: "testkube", Name: "cypress"}, Spec: executorv1.ExecutorSpec{ - Types: []string{mockExecutorTypes}, - ExecutorType: "job", - URI: "", - Image: "cypress", - Args: nil, - Command: []string{"run"}, - ImagePullSecrets: nil, - Features: nil, - ContentTypes: nil, - JobTemplate: "", - Meta: nil, + Types: []string{mockExecutorTypes}, + ExecutorType: "job", + URI: "", + Image: "cypress", + Args: nil, + Command: []string{"run"}, + ImagePullSecrets: nil, + Features: nil, + ContentTypes: nil, + JobTemplate: "", + JobTemplateReference: "", + Meta: nil, }, } mockExecutorsClient.EXPECT().GetByType(mockExecutorTypes).Return(&mockExecutorV1, nil).AnyTimes() From 7b846d498d01928bad074213554e05b6bd1e2d5d Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 31 Aug 2023 16:20:41 +0300 Subject: [PATCH 14/59] feat: using template reference --- internal/app/api/v1/server.go | 9 +++++++++ pkg/executor/client/job.go | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go index 4d5f2ec4ad7..dc31b409b5d 100644 --- a/internal/app/api/v1/server.go +++ b/internal/app/api/v1/server.go @@ -349,6 +349,15 @@ func (s *TestkubeAPI) InitRoutes() { testsources.Delete("/:name", s.DeleteTestSourceHandler()) testsources.Delete("/", s.DeleteTestSourcesHandler()) + templates := s.Routes.Group("/templates") + + templates.Post("/", s.CreateTemplateHandler()) + templates.Patch("/:name", s.UpdateTemplateHandler()) + templates.Get("/", s.ListTemplatesHandler()) + templates.Get("/:name", s.GetTemplateHandler()) + templates.Delete("/:name", s.DeleteTemplateHandler()) + templates.Delete("/", s.DeleteTemplatesHandler()) + labels := s.Routes.Group("/labels") labels.Get("/", s.ListLabelsHandler()) diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go index d8926fd2aa2..96e578fe257 100644 --- a/pkg/executor/client/job.go +++ b/pkg/executor/client/job.go @@ -801,7 +801,9 @@ func NewJobOptions(templatesClient templatesv1.Interface, initImage, jobTemplate return jobOptions, err } - jobOptions.JobTemplate = template.Spec.Body + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { + jobOptions.JobTemplate = template.Spec.Body + } } jobOptions.Variables = execution.Variables From c640ce060eb47f0f4f625c68ec70b78d814b00f5 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 31 Aug 2023 17:55:55 +0300 Subject: [PATCH 15/59] feat: template cli --- cmd/kubectl-testkube/commands/delete.go | 2 + cmd/kubectl-testkube/commands/get.go | 2 + .../commands/templates/common.go | 104 ++++++++++++++++++ .../commands/templates/create.go | 75 +++++++++++++ .../commands/templates/delete.go | 48 ++++++++ .../commands/templates/get.go | 74 +++++++++++++ .../commands/templates/update.go | 52 +++++++++ cmd/kubectl-testkube/commands/update.go | 2 + docs/docs/cli/testkube_create_executor.md | 1 + docs/docs/cli/testkube_create_test.md | 5 + docs/docs/cli/testkube_create_testsuite.md | 7 ++ docs/docs/cli/testkube_delete.md | 1 + docs/docs/cli/testkube_delete_template.md | 34 ++++++ docs/docs/cli/testkube_generate_tests-crds.md | 5 + docs/docs/cli/testkube_get.md | 1 + docs/docs/cli/testkube_get_template.md | 37 +++++++ docs/docs/cli/testkube_run_test.md | 4 + docs/docs/cli/testkube_run_testsuite.md | 6 + docs/docs/cli/testkube_update.md | 1 + docs/docs/cli/testkube_update_executor.md | 1 + docs/docs/cli/testkube_update_template.md | 36 ++++++ docs/docs/cli/testkube_update_test.md | 5 + docs/docs/cli/testkube_update_testsuite.md | 7 ++ pkg/api/v1/client/api.go | 3 + pkg/api/v1/client/interface.go | 19 +++- pkg/api/v1/client/template.go | 74 +++++++++++++ .../v1/testkube/model_template_extended.go | 12 ++ 27 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 cmd/kubectl-testkube/commands/templates/common.go create mode 100644 cmd/kubectl-testkube/commands/templates/create.go create mode 100644 cmd/kubectl-testkube/commands/templates/delete.go create mode 100644 cmd/kubectl-testkube/commands/templates/get.go create mode 100644 cmd/kubectl-testkube/commands/templates/update.go create mode 100644 docs/docs/cli/testkube_delete_template.md create mode 100644 docs/docs/cli/testkube_get_template.md create mode 100644 docs/docs/cli/testkube_update_template.md create mode 100644 pkg/api/v1/client/template.go create mode 100644 pkg/api/v1/testkube/model_template_extended.go diff --git a/cmd/kubectl-testkube/commands/delete.go b/cmd/kubectl-testkube/commands/delete.go index cb6385dec90..8f8cfa87727 100644 --- a/cmd/kubectl-testkube/commands/delete.go +++ b/cmd/kubectl-testkube/commands/delete.go @@ -6,6 +6,7 @@ import ( "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/executors" + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/templates" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsources" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites" @@ -40,6 +41,7 @@ func NewDeleteCmd() *cobra.Command { cmd.AddCommand(webhooks.NewDeleteWebhookCmd()) cmd.AddCommand(executors.NewDeleteExecutorCmd()) cmd.AddCommand(testsources.NewDeleteTestSourceCmd()) + cmd.AddCommand(templates.NewDeleteTemplateCmd()) return cmd } diff --git a/cmd/kubectl-testkube/commands/get.go b/cmd/kubectl-testkube/commands/get.go index c7cfb17dfad..b0355e79623 100644 --- a/cmd/kubectl-testkube/commands/get.go +++ b/cmd/kubectl-testkube/commands/get.go @@ -8,6 +8,7 @@ import ( "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/context" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/executors" + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/templates" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsources" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites" @@ -46,6 +47,7 @@ func NewGetCmd() *cobra.Command { cmd.AddCommand(testsuites.NewTestSuiteExecutionCmd()) cmd.AddCommand(testsources.NewGetTestSourceCmd()) cmd.AddCommand(context.NewGetContextCmd()) + cmd.AddCommand(templates.NewGetTemplateCmd()) cmd.PersistentFlags().StringP("output", "o", "pretty", "output type can be one of json|yaml|pretty|go-template") cmd.PersistentFlags().StringP("go-template", "", "{{.}}", "go template to render") diff --git a/cmd/kubectl-testkube/commands/templates/common.go b/cmd/kubectl-testkube/commands/templates/common.go new file mode 100644 index 00000000000..12209d3567b --- /dev/null +++ b/cmd/kubectl-testkube/commands/templates/common.go @@ -0,0 +1,104 @@ +package templates + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + apiv1 "github.com/kubeshop/testkube/pkg/api/v1/client" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" + "github.com/kubeshop/testkube/pkg/ui" +) + +// NewCreateTemplateOptionsFromFlags creates create template options from command flags +func NewCreateTemplateOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateTemplateOptions, err error) { + name := cmd.Flag("name").Value.String() + namespace := cmd.Flag("namespace").Value.String() + if err != nil { + return options, err + } + + templateType := testkube.TemplateType(cmd.Flag("template-type").Value.String()) + + if templateType != testkube.JOB_TemplateType && templateType != testkube.CRONJOB_TemplateType && + templateType != testkube.SCRAPER_TemplateType && templateType != testkube.PVC_TemplateType && + templateType != testkube.WEBHOOK_TemplateType { + ui.Failf("invalid template type: %s. use one of job|container|cronnjob|scraper|pvc|webhook", templateType) + } + + body := cmd.Flag("body").Value.String() + bodyContent := "" + if body != "" { + b, err := os.ReadFile(body) + ui.ExitOnError("reading template body", err) + bodyContent = string(b) + } + + labels, err := cmd.Flags().GetStringToString("label") + if err != nil { + return options, err + } + + options = apiv1.CreateTemplateOptions{ + Name: name, + Namespace: namespace, + Type_: &templateType, + Labels: labels, + Body: bodyContent, + } + + return options, nil +} + +// NewUpdateTemplateOptionsFromFlags creates update template options from command flags +func NewUpdateTemplateOptionsFromFlags(cmd *cobra.Command) (options apiv1.UpdateTemplateOptions, err error) { + var fields = []struct { + name string + destination **string + }{ + { + "name", + &options.Name, + }, + } + + for _, field := range fields { + if cmd.Flag(field.name).Changed { + value := cmd.Flag(field.name).Value.String() + *field.destination = &value + } + } + + if cmd.Flag("template-type").Changed { + templateType := testkube.TemplateType(cmd.Flag("template-type").Value.String()) + if templateType != testkube.JOB_TemplateType && templateType != testkube.CRONJOB_TemplateType && + templateType != testkube.SCRAPER_TemplateType && templateType != testkube.PVC_TemplateType && + templateType != testkube.WEBHOOK_TemplateType { + ui.Failf("invalid template type: %s. use one of job|container|cronnjob|scraper|pvc|webhook", templateType) + } + options.Type_ = &templateType + } + + if cmd.Flag("body").Changed { + body := cmd.Flag("body").Value.String() + b, err := os.ReadFile(body) + if err != nil { + return options, fmt.Errorf("reading template body %w", err) + } + + value := string(b) + options.Body = &value + } + + if cmd.Flag("label").Changed { + labels, err := cmd.Flags().GetStringToString("label") + if err != nil { + return options, err + } + + options.Labels = &labels + } + + return options, nil +} diff --git a/cmd/kubectl-testkube/commands/templates/create.go b/cmd/kubectl-testkube/commands/templates/create.go new file mode 100644 index 00000000000..8a8100c36fa --- /dev/null +++ b/cmd/kubectl-testkube/commands/templates/create.go @@ -0,0 +1,75 @@ +package templates + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" + apiv1 "github.com/kubeshop/testkube/pkg/api/v1/client" + "github.com/kubeshop/testkube/pkg/crd" + "github.com/kubeshop/testkube/pkg/ui" +) + +func NewCreateTemplateCmd() *cobra.Command { + var ( + name string + templateType string + labels map[string]string + body string + ) + + cmd := &cobra.Command{ + Use: "template", + Aliases: []string{"tp"}, + Short: "Create new Template", + Long: `Create new Template Custom Resource`, + Run: func(cmd *cobra.Command, args []string) { + crdOnly, err := strconv.ParseBool(cmd.Flag("crd-only").Value.String()) + ui.ExitOnError("parsing flag value", err) + + if name == "" { + ui.Failf("pass valid name (in '--name' flag)") + } + + namespace := cmd.Flag("namespace").Value.String() + var client apiv1.Client + if !crdOnly { + client, namespace, err = common.GetClient(cmd) + ui.ExitOnError("getting client", err) + + template, _ := client.GetTemplate(name) + if name == template.Name { + ui.Failf("Template with name '%s' already exists in namespace %s", name, namespace) + } + } + + options, err := NewCreateTemplateOptionsFromFlags(cmd) + ui.ExitOnError("getting template options", err) + + if !crdOnly { + _, err := client.CreateTemplate(options) + ui.ExitOnError("creating template "+name+" in namespace "+namespace, err) + + ui.Success("Template created", name) + } else { + if options.Body != "" { + options.Body = fmt.Sprintf("%q", options.Body) + } + + data, err := crd.ExecuteTemplate(crd.TemplateTemplate, options) + ui.ExitOnError("executing crd template", err) + + ui.Info(data) + } + }, + } + + cmd.Flags().StringVarP(&name, "name", "n", "", "unique template name - mandatory") + cmd.Flags().StringVarP(&templateType, "template-type", "", "", "template type one of job|container|cronnjob|scraper|pvc|webhook") + cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") + cmd.Flags().StringVarP(&body, "body", "", "", "a path to template file to use as template body") + + return cmd +} diff --git a/cmd/kubectl-testkube/commands/templates/delete.go b/cmd/kubectl-testkube/commands/templates/delete.go new file mode 100644 index 00000000000..ff8b53b2d39 --- /dev/null +++ b/cmd/kubectl-testkube/commands/templates/delete.go @@ -0,0 +1,48 @@ +package templates + +import ( + "strings" + + "github.com/spf13/cobra" + + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" + "github.com/kubeshop/testkube/pkg/ui" +) + +func NewDeleteTemplateCmd() *cobra.Command { + var name string + var selectors []string + + cmd := &cobra.Command{ + + Use: "template ", + Aliases: []string{"wh"}, + Short: "Delete template", + Long: `Delete template, pass template name which should be deleted`, + Run: func(cmd *cobra.Command, args []string) { + client, _, err := common.GetClient(cmd) + ui.ExitOnError("getting client", err) + + if len(args) > 0 { + name = args[0] + err := client.DeleteTemplate(name) + ui.ExitOnError("deleting template: "+name, err) + ui.SuccessAndExit("Succesfully deleted template", name) + } + + if len(selectors) != 0 { + selector := strings.Join(selectors, ",") + err := client.DeleteTemplates(selector) + ui.ExitOnError("deleting templates by labels: "+selector, err) + ui.SuccessAndExit("Succesfully deleted templates by labels", selector) + } + + ui.Failf("Pass Template name or labels to delete by labels") + }, + } + + cmd.Flags().StringVarP(&name, "name", "n", "", "unique template name, you can also pass it as first argument") + cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1") + + return cmd +} diff --git a/cmd/kubectl-testkube/commands/templates/get.go b/cmd/kubectl-testkube/commands/templates/get.go new file mode 100644 index 00000000000..881663bff66 --- /dev/null +++ b/cmd/kubectl-testkube/commands/templates/get.go @@ -0,0 +1,74 @@ +package templates + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render" + "github.com/kubeshop/testkube/pkg/crd" + "github.com/kubeshop/testkube/pkg/ui" +) + +func NewGetTemplateCmd() *cobra.Command { + var name string + var selectors []string + var crdOnly bool + + cmd := &cobra.Command{ + Use: "template ", + Aliases: []string{"templates", "tp"}, + Short: "Get template details", + Long: `Get template, you can change output format, to get single details pass name as first arg`, + Run: func(cmd *cobra.Command, args []string) { + client, _, err := common.GetClient(cmd) + ui.ExitOnError("getting client", err) + + firstEntry := true + if len(args) > 0 { + name := args[0] + template, err := client.GetTemplate(name) + ui.ExitOnError("getting template: "+name, err) + + if crdOnly { + if template.Body != "" { + template.Body = fmt.Sprintf("%q", template.Body) + } + + common.UIPrintCRD(crd.TemplateTemplate, template, &firstEntry) + return + } + + err = render.Obj(cmd, template, os.Stdout) + ui.ExitOnError("rendering obj", err) + } else { + templates, err := client.ListTemplates(strings.Join(selectors, ",")) + ui.ExitOnError("getting templates", err) + + if crdOnly { + for _, template := range templates { + if template.Body != "" { + template.Body = fmt.Sprintf("%q", template.Body) + } + + common.UIPrintCRD(crd.TemplateTemplate, template, &firstEntry) + } + + return + } + + err = render.List(cmd, templates, os.Stdout) + ui.ExitOnError("rendering list", err) + } + }, + } + + cmd.Flags().StringVarP(&name, "name", "n", "", "unique template name, you can also pass it as argument") + cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1") + cmd.Flags().BoolVar(&crdOnly, "crd-only", false, "show only test crd") + + return cmd +} diff --git a/cmd/kubectl-testkube/commands/templates/update.go b/cmd/kubectl-testkube/commands/templates/update.go new file mode 100644 index 00000000000..dd7605176c8 --- /dev/null +++ b/cmd/kubectl-testkube/commands/templates/update.go @@ -0,0 +1,52 @@ +package templates + +import ( + "github.com/spf13/cobra" + + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" + "github.com/kubeshop/testkube/pkg/ui" +) + +func UpdateTemplateCmd() *cobra.Command { + var ( + name string + templateType string + labels map[string]string + body string + ) + + cmd := &cobra.Command{ + Use: "template", + Aliases: []string{"templates", "tp"}, + Short: "Update Template", + Long: `Update Template Custom Resource`, + Run: func(cmd *cobra.Command, args []string) { + if name == "" { + ui.Failf("pass valid name (in '--name' flag)") + } + + client, namespace, err := common.GetClient(cmd) + ui.ExitOnError("getting client", err) + + template, _ := client.GetTemplate(name) + if name != template.Name { + ui.Failf("Template with name '%s' not exists in namespace %s", name, namespace) + } + + options, err := NewUpdateTemplateOptionsFromFlags(cmd) + ui.ExitOnError("getting template options", err) + + _, err = client.UpdateTemplate(options) + ui.ExitOnError("updating template "+name+" in namespace "+namespace, err) + + ui.Success("Template updated", name) + }, + } + + cmd.Flags().StringVarP(&name, "name", "n", "", "unique template name - mandatory") + cmd.Flags().StringVarP(&templateType, "template-type", "", "", "template type one of job|container|cronnjob|scraper|pvc|webhook") + cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") + cmd.Flags().StringVarP(&body, "body", "", "", "a path to template file to use as template body") + + return cmd +} diff --git a/cmd/kubectl-testkube/commands/update.go b/cmd/kubectl-testkube/commands/update.go index 157a5eb0f38..cd1b77ff3d7 100644 --- a/cmd/kubectl-testkube/commands/update.go +++ b/cmd/kubectl-testkube/commands/update.go @@ -6,6 +6,7 @@ import ( "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/executors" + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/templates" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsources" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites" @@ -37,6 +38,7 @@ func NewUpdateCmd() *cobra.Command { cmd.AddCommand(testsources.UpdateTestSourceCmd()) cmd.AddCommand(executors.UpdateExecutorCmd()) cmd.AddCommand(webhooks.UpdateWebhookCmd()) + cmd.AddCommand(templates.UpdateTemplateCmd()) return cmd } diff --git a/docs/docs/cli/testkube_create_executor.md b/docs/docs/cli/testkube_create_executor.md index 673c1351fc2..4001cb14bcb 100644 --- a/docs/docs/cli/testkube_create_executor.md +++ b/docs/docs/cli/testkube_create_executor.md @@ -24,6 +24,7 @@ testkube create executor [flags] --image string image used for executor --image-pull-secrets stringArray secret name used to pull the image in executor -j, --job-template string if executor needs to be launched using custom job specification, then a path to template file should be provided + --job-template-reference string reference to job template for using with executor -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique executor name - mandatory --tooltip stringToString tooltip key value pair: --tooltip key1=value1 (default []) diff --git a/docs/docs/cli/testkube_create_test.md b/docs/docs/cli/testkube_create_test.md index 454a25719f1..a1afc178623 100644 --- a/docs/docs/cli/testkube_create_test.md +++ b/docs/docs/cli/testkube_create_test.md @@ -22,6 +22,7 @@ testkube create test [flags] --command stringArray command passed to image in executor --copy-files stringArray file path mappings from host to pod of form source:destination --cronjob-template string cron job template file path for extensions to cron job template + --cronjob-template-reference string reference to cron job template to use for the test --description string test description --env stringToString envs in a form of name1=val1 passed to executor (default []) --execution-name string execution name, if empty will be autogenerated @@ -44,6 +45,7 @@ testkube create test [flags] --image string image for container executor --image-pull-secrets stringArray secret name used to pull the image in container executor --job-template string job template file path for extensions to job template + --job-template-reference string reference to job template to use for the test -l, --label stringToString label key value pair: --label key1=value1 (default []) --mount-configmap stringToString config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath (default []) --mount-secret stringToString secret value pair for mounting it to executor pod: --mount-secret secret_name=secret_mountpath (default []) @@ -51,8 +53,11 @@ testkube create test [flags] --negative-test negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa --postrun-script string path to script to be run after test execution --prerun-script string path to script to be run before test execution + --pvc-template string pvc template file path for extensions to pvc template + --pvc-template-reference string reference to pvc template to use for the test --schedule string test schedule in a cron job form: * * * * * --scraper-template string scraper template file path for extensions to scraper template + --scraper-template-reference string reference to scraper template to use for the test --secret-env stringToString secret envs in a form of secret_key1=secret_name1 passed to executor (default []) -s, --secret-variable stringToString secret variable key value pair: --secret-variable key1=value1 (default []) --secret-variable-reference stringToString secret variable references in a form name1=secret_name1=secret_key1 (default []) diff --git a/docs/docs/cli/testkube_create_testsuite.md b/docs/docs/cli/testkube_create_testsuite.md index 98a83ff2a95..54e848fd3da 100644 --- a/docs/docs/cli/testkube_create_testsuite.md +++ b/docs/docs/cli/testkube_create_testsuite.md @@ -14,14 +14,21 @@ testkube create testsuite [flags] ``` --cronjob-template string cron job template file path for extensions to cron job template + --cronjob-template-reference string reference to cron job template to use for the test --execution-name string execution name, if empty will be autogenerated -f, --file string JSON test suite file - will be read from stdin if not specified, look at testkube.TestUpsertRequest -h, --help help for testsuite --http-proxy string http proxy for executor containers --https-proxy string https proxy for executor containers + --job-template string job template file path for extensions to job template + --job-template-reference string reference to job template to use for the test -l, --label stringToString label key value pair: --label key1=value1 (default []) --name string Set/Override test suite name + --pvc-template string pvc template file path for extensions to pvc template + --pvc-template-reference string reference to pvc template to use for the test --schedule string test suite schedule in a cron job form: * * * * * + --scraper-template string scraper template file path for extensions to scraper template + --scraper-template-reference string reference to scraper template to use for the test -s, --secret-variable stringToString secret variable key value pair: --secret-variable key1=value1 (default []) --secret-variable-reference stringToString secret variable references in a form name1=secret_name1=secret_key1 (default []) --timeout int32 duration in seconds for test suite to timeout. 0 disables timeout. diff --git a/docs/docs/cli/testkube_delete.md b/docs/docs/cli/testkube_delete.md index 9c214cd836f..85fc7ee182d 100644 --- a/docs/docs/cli/testkube_delete.md +++ b/docs/docs/cli/testkube_delete.md @@ -26,6 +26,7 @@ testkube delete [flags] * [testkube](testkube.md) - Testkube entrypoint for kubectl plugin * [testkube delete executor](testkube_delete_executor.md) - Delete Executor +* [testkube delete template](testkube_delete_template.md) - Delete template * [testkube delete test](testkube_delete_test.md) - Delete Test * [testkube delete testsource](testkube_delete_testsource.md) - Delete test source * [testkube delete testsuite](testkube_delete_testsuite.md) - Delete test suite diff --git a/docs/docs/cli/testkube_delete_template.md b/docs/docs/cli/testkube_delete_template.md new file mode 100644 index 00000000000..64839c17255 --- /dev/null +++ b/docs/docs/cli/testkube_delete_template.md @@ -0,0 +1,34 @@ +## testkube delete template + +Delete template + +### Synopsis + +Delete template, pass template name which should be deleted + +``` +testkube delete template [flags] +``` + +### Options + +``` + -h, --help help for template + -l, --label strings label key value pair: --label key1=value1 + -n, --name string unique template name, you can also pass it as first argument +``` + +### Options inherited from parent commands + +``` + -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results/v1") + -c, --client string Client used for connecting to testkube API one of proxy|direct (default "proxy") + --namespace string Kubernetes namespace, default value read from config if set (default "testkube") + --oauth-enabled enable oauth + --verbose should I show additional debug messages +``` + +### SEE ALSO + +* [testkube delete](testkube_delete.md) - Delete resources + diff --git a/docs/docs/cli/testkube_generate_tests-crds.md b/docs/docs/cli/testkube_generate_tests-crds.md index 748c4f80c6d..a7e463cfd88 100644 --- a/docs/docs/cli/testkube_generate_tests-crds.md +++ b/docs/docs/cli/testkube_generate_tests-crds.md @@ -22,6 +22,7 @@ testkube generate tests-crds [flags] --command stringArray command passed to image in executor --copy-files stringArray file path mappings from host to pod of form source:destination --cronjob-template string cron job template file path for extensions to cron job template + --cronjob-template-reference string reference to cron job template to use for the test --description string test description --env stringToString envs in a form of name1=val1 passed to executor (default []) --execution-name string execution name, if empty will be autogenerated @@ -32,14 +33,18 @@ testkube generate tests-crds [flags] --image string image for container executor --image-pull-secrets stringArray secret name used to pull the image in container executor --job-template string job template file path for extensions to job template + --job-template-reference string reference to job template to use for the test -l, --label stringToString label key value pair: --label key1=value1 (default []) --mount-configmap stringToString config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath (default []) --mount-secret stringToString secret value pair for mounting it to executor pod: --mount-secret secret_name=secret_mountpath (default []) --negative-test negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa --postrun-script string path to script to be run after test execution --prerun-script string path to script to be run before test execution + --pvc-template string pvc template file path for extensions to pvc template + --pvc-template-reference string reference to pvc template to use for the test --schedule string test schedule in a cron job form: * * * * * --scraper-template string scraper template file path for extensions to scraper template + --scraper-template-reference string reference to scraper template to use for the test --secret-env stringToString secret envs in a form of secret_key1=secret_name1 passed to executor (default []) -s, --secret-variable stringToString secret variable key value pair: --secret-variable key1=value1 (default []) --secret-variable-reference stringToString secret variable references in a form name1=secret_name1=secret_key1 (default []) diff --git a/docs/docs/cli/testkube_get.md b/docs/docs/cli/testkube_get.md index d98c5523897..6b1172b18a5 100644 --- a/docs/docs/cli/testkube_get.md +++ b/docs/docs/cli/testkube_get.md @@ -35,6 +35,7 @@ testkube get [flags] * [testkube get context](testkube_get_context.md) - Set context for Testkube Cloud * [testkube get execution](testkube_get_execution.md) - Lists or gets test executions * [testkube get executor](testkube_get_executor.md) - Gets executor details +* [testkube get template](testkube_get_template.md) - Get template details * [testkube get test](testkube_get_test.md) - Get all available tests * [testkube get testsource](testkube_get_testsource.md) - Get test source details * [testkube get testsuite](testkube_get_testsuite.md) - Get test suite by name diff --git a/docs/docs/cli/testkube_get_template.md b/docs/docs/cli/testkube_get_template.md new file mode 100644 index 00000000000..334ebdc316c --- /dev/null +++ b/docs/docs/cli/testkube_get_template.md @@ -0,0 +1,37 @@ +## testkube get template + +Get template details + +### Synopsis + +Get template, you can change output format, to get single details pass name as first arg + +``` +testkube get template [flags] +``` + +### Options + +``` + --crd-only show only test crd + -h, --help help for template + -l, --label strings label key value pair: --label key1=value1 + -n, --name string unique template name, you can also pass it as argument +``` + +### Options inherited from parent commands + +``` + -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results/v1") + -c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy") + --go-template string go template to render (default "{{.}}") + --namespace string Kubernetes namespace, default value read from config if set (default "testkube") + --oauth-enabled enable oauth + -o, --output string output type can be one of json|yaml|pretty|go-template (default "pretty") + --verbose show additional debug messages +``` + +### SEE ALSO + +* [testkube get](testkube_get.md) - Get resources + diff --git a/docs/docs/cli/testkube_run_test.md b/docs/docs/cli/testkube_run_test.md index e2a16f8514b..d298f1ab3e3 100644 --- a/docs/docs/cli/testkube_run_test.md +++ b/docs/docs/cli/testkube_run_test.md @@ -38,6 +38,7 @@ testkube run test [flags] --image string execution variable passed to executor --iterations int how many times to run the test (default 1) --job-template string job template file path for extensions to job template + --job-template-reference string reference to job template to use for the test -l, --label strings label key value pair: --label key1=value1 --mask stringArray regexp to filter downloaded files, single or comma separated, like report/.* or .*\.json,.*\.js$ --mount-configmap stringToString config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath (default []) @@ -46,7 +47,10 @@ testkube run test [flags] --negative-test negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa --postrun-script string path to script to be run after test execution --prerun-script string path to script to be run before test execution + --pvc-template string pvc template file path for extensions to pvc template + --pvc-template-reference string reference to pvc template to use for the test --scraper-template string scraper template file path for extensions to scraper template + --scraper-template-reference string reference to scraper template to use for the test -s, --secret-variable stringToString execution secret variable passed to executor (default []) --secret-variable-reference stringToString secret variable references in a form name1=secret_name1=secret_key1 (default []) --upload-timeout string timeout to use when uploading files, example: 30s diff --git a/docs/docs/cli/testkube_run_testsuite.md b/docs/docs/cli/testkube_run_testsuite.md index 487a76d331e..e1138fcf6aa 100644 --- a/docs/docs/cli/testkube_run_testsuite.md +++ b/docs/docs/cli/testkube_run_testsuite.md @@ -23,8 +23,14 @@ testkube run testsuite [flags] -h, --help help for testsuite --http-proxy string http proxy for executor containers --https-proxy string https proxy for executor containers + --job-template string job template file path for extensions to job template + --job-template-reference string reference to job template to use for the test -l, --label strings label key value pair: --label key1=value1 -n, --name string execution name, if empty will be autogenerated + --pvc-template string pvc template file path for extensions to pvc template + --pvc-template-reference string reference to pvc template to use for the test + --scraper-template string scraper template file path for extensions to scraper template + --scraper-template-reference string reference to scraper template to use for the test -s, --secret-variable stringToString execution variables passed to executor (default []) --secret-variable-reference stringToString secret variable references in a form name1=secret_name1=secret_key1 (default []) -v, --variable stringToString execution variables passed to executor (default []) diff --git a/docs/docs/cli/testkube_update.md b/docs/docs/cli/testkube_update.md index 7a3de7cc45b..9da43b9874f 100644 --- a/docs/docs/cli/testkube_update.md +++ b/docs/docs/cli/testkube_update.md @@ -26,6 +26,7 @@ testkube update [flags] * [testkube](testkube.md) - Testkube entrypoint for kubectl plugin * [testkube update executor](testkube_update_executor.md) - Update Executor +* [testkube update template](testkube_update_template.md) - Update Template * [testkube update test](testkube_update_test.md) - Update test * [testkube update testsource](testkube_update_testsource.md) - Update TestSource * [testkube update testsuite](testkube_update_testsuite.md) - Update Test Suite diff --git a/docs/docs/cli/testkube_update_executor.md b/docs/docs/cli/testkube_update_executor.md index 2f8cf5c90b9..c65aece5c9a 100644 --- a/docs/docs/cli/testkube_update_executor.md +++ b/docs/docs/cli/testkube_update_executor.md @@ -24,6 +24,7 @@ testkube update executor [flags] --image string image used for executor --image-pull-secrets stringArray secret name used to pull the image in executor -j, --job-template string if executor needs to be launched using custom job specification, then a path to template file should be provided + --job-template-reference string reference to job template for using with executor -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique executor name - mandatory --tooltip stringToString tooltip key value pair: --tooltip key1=value1 (default []) diff --git a/docs/docs/cli/testkube_update_template.md b/docs/docs/cli/testkube_update_template.md new file mode 100644 index 00000000000..db7e0bdff29 --- /dev/null +++ b/docs/docs/cli/testkube_update_template.md @@ -0,0 +1,36 @@ +## testkube update template + +Update Template + +### Synopsis + +Update Template Custom Resource + +``` +testkube update template [flags] +``` + +### Options + +``` + --body string a path to template file to use as template body + -h, --help help for template + -l, --label stringToString label key value pair: --label key1=value1 (default []) + -n, --name string unique template name - mandatory + --template-type string template type one of job|container|cronnjob|scraper|pvc|webhook +``` + +### Options inherited from parent commands + +``` + -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results/v1") + -c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy") + --namespace string Kubernetes namespace, default value read from config if set (default "testkube") + --oauth-enabled enable oauth + --verbose show additional debug messages +``` + +### SEE ALSO + +* [testkube update](testkube_update.md) - Update resource + diff --git a/docs/docs/cli/testkube_update_test.md b/docs/docs/cli/testkube_update_test.md index 8e761d16799..3bddc3448ee 100644 --- a/docs/docs/cli/testkube_update_test.md +++ b/docs/docs/cli/testkube_update_test.md @@ -22,6 +22,7 @@ testkube update test [flags] --command stringArray command passed to image in executor --copy-files stringArray file path mappings from host to pod of form source:destination --cronjob-template string cron job template file path for extensions to cron job template + --cronjob-template-reference string reference to cron job template to use for the test --description string test description --execution-name string execution name, if empty will be autogenerated --executor-args stringArray executor binary additional arguments @@ -43,6 +44,7 @@ testkube update test [flags] -i, --image string image for container executor --image-pull-secrets stringArray secret name used to pull the image in container executor --job-template string job template file path for extensions to job template + --job-template-reference string reference to job template to use for the test -l, --label stringToString label key value pair: --label key1=value1 (default []) --mount-configmap stringToString config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath (default []) --mount-secret stringToString secret value pair for mounting it to executor pod: --mount-secret secret_name=secret_mountpath (default []) @@ -50,8 +52,11 @@ testkube update test [flags] --negative-test negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa --postrun-script string path to script to be run after test execution --prerun-script string path to script to be run before test execution + --pvc-template string pvc template file path for extensions to pvc template + --pvc-template-reference string reference to pvc template to use for the test --schedule string test schedule in a cron job form: * * * * * --scraper-template string scraper template file path for extensions to scraper template + --scraper-template-reference string reference to scraper template to use for the test -s, --secret-variable stringToString secret variable key value pair: -s key1=value1 (default []) --secret-variable-reference stringToString secret variable references in a form name1=secret_name1=secret_key1 (default []) --source string source name - will be used together with content parameters diff --git a/docs/docs/cli/testkube_update_testsuite.md b/docs/docs/cli/testkube_update_testsuite.md index f8c6173602e..10253abae79 100644 --- a/docs/docs/cli/testkube_update_testsuite.md +++ b/docs/docs/cli/testkube_update_testsuite.md @@ -14,14 +14,21 @@ testkube update testsuite [flags] ``` --cronjob-template string cron job template file path for extensions to cron job template + --cronjob-template-reference string reference to cron job template to use for the test --execution-name string execution name, if empty will be autogenerated -f, --file string JSON test file - will be read from stdin if not specified, look at testkube.TestUpsertRequest -h, --help help for testsuite --http-proxy string http proxy for executor containers --https-proxy string https proxy for executor containers + --job-template string job template file path for extensions to job template + --job-template-reference string reference to job template to use for the test -l, --label stringToString label key value pair: --label key1=value1 (default []) --name string Set/Override test suite name + --pvc-template string pvc template file path for extensions to pvc template + --pvc-template-reference string reference to pvc template to use for the test --schedule string test suite schedule in a cron job form: * * * * * + --scraper-template string scraper template file path for extensions to scraper template + --scraper-template-reference string reference to scraper template to use for the test -s, --secret-variable stringToString secret variable key value pair: --secret-variable key1=value1 (default []) --secret-variable-reference stringToString secret variable references in a form name1=secret_name1=secret_key1 (default []) --timeout int32 duration in seconds for test suite to timeout. 0 disables timeout. diff --git a/pkg/api/v1/client/api.go b/pkg/api/v1/client/api.go index 7969bc80dcf..b89ba8860b6 100644 --- a/pkg/api/v1/client/api.go +++ b/pkg/api/v1/client/api.go @@ -37,6 +37,7 @@ func NewProxyAPIClient(client kubernetes.Interface, config APIConfig) APIClient ConfigClient: NewConfigClient(NewProxyClient[testkube.Config](client, config)), TestSourceClient: NewTestSourceClient(NewProxyClient[testkube.TestSource](client, config)), CopyFileClient: NewCopyFileProxyClient(client, config), + TemplateClient: NewTemplateClient(NewProxyClient[testkube.Template](client, config)), } } @@ -66,6 +67,7 @@ func NewDirectAPIClient(httpClient *http.Client, sseClient *http.Client, apiURI, ConfigClient: NewConfigClient(NewDirectClient[testkube.Config](httpClient, apiURI, apiPathPrefix)), TestSourceClient: NewTestSourceClient(NewDirectClient[testkube.TestSource](httpClient, apiURI, apiPathPrefix)), CopyFileClient: NewCopyFileDirectClient(httpClient, apiURI, apiPathPrefix), + TemplateClient: NewTemplateClient(NewDirectClient[testkube.Template](httpClient, apiURI, apiPathPrefix)), } } @@ -78,4 +80,5 @@ type APIClient struct { ConfigClient TestSourceClient CopyFileClient + TemplateClient } diff --git a/pkg/api/v1/client/interface.go b/pkg/api/v1/client/interface.go index adc0e283e02..bca67b3f5c6 100644 --- a/pkg/api/v1/client/interface.go +++ b/pkg/api/v1/client/interface.go @@ -19,6 +19,7 @@ type Client interface { ConfigAPI TestSourceAPI CopyFileAPI + TemplateAPI } // TestAPI describes test api methods @@ -91,6 +92,16 @@ type WebhookAPI interface { DeleteWebhooks(selector string) (err error) } +// TemplateAPI describes template api methods +type TemplateAPI interface { + CreateTemplate(options CreateTemplateOptions) (template testkube.Template, err error) + UpdateTemplate(options UpdateTemplateOptions) (template testkube.Template, err error) + GetTemplate(name string) (template testkube.Template, err error) + ListTemplates(selector string) (templates testkube.Templates, err error) + DeleteTemplate(name string) (err error) + DeleteTemplates(selector string) (err error) +} + // ConfigAPI describes config api methods type ConfigAPI interface { UpdateConfig(config testkube.Config) (outputConfig testkube.Config, err error) @@ -154,6 +165,12 @@ type UpsertTestSourceOptions testkube.TestSourceUpsertRequest // if needed can be extended to custom struct type UpdateTestSourceOptions testkube.TestSourceUpdateRequest +// CreateTemplateOptions - is mapping for now to OpenAPI schema for creating/changing template +type CreateTemplateOptions testkube.TemplateCreateRequest + +// UpdateTemplateOptions - is mapping for now to OpenAPI schema for changing template request +type UpdateTemplateOptions testkube.TemplateUpdateRequest + // TODO consider replacing it with testkube.ExecutionRequest - looks almost the samea and redundant // ExecuteTestOptions contains test run options type ExecuteTestOptions struct { @@ -210,7 +227,7 @@ type Gettable interface { testkube.Test | testkube.TestSuite | testkube.ExecutorDetails | testkube.Webhook | testkube.TestWithExecution | testkube.TestSuiteWithExecution | testkube.TestWithExecutionSummary | testkube.TestSuiteWithExecutionSummary | testkube.Artifact | testkube.ServerInfo | testkube.Config | testkube.DebugInfo | - testkube.TestSource + testkube.TestSource | testkube.Template } // Executable is an interface of executable objects diff --git a/pkg/api/v1/client/template.go b/pkg/api/v1/client/template.go new file mode 100644 index 00000000000..aec2f7a3b6c --- /dev/null +++ b/pkg/api/v1/client/template.go @@ -0,0 +1,74 @@ +package client + +import ( + "encoding/json" + "net/http" + + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +// NewTemplateClient creates new Template client +func NewTemplateClient(templateTransport Transport[testkube.Template]) TemplateClient { + return TemplateClient{ + templateTransport: templateTransport, + } +} + +// TemplateClient is a client for templates +type TemplateClient struct { + templateTransport Transport[testkube.Template] +} + +// GetTemplate gets template by name +func (c TemplateClient) GetTemplate(name string) (template testkube.Template, err error) { + uri := c.templateTransport.GetURI("/templates/%s", name) + return c.templateTransport.Execute(http.MethodGet, uri, nil, nil) +} + +// ListTemplates list all templates +func (c TemplateClient) ListTemplates(selector string) (templates testkube.Templates, err error) { + uri := c.templateTransport.GetURI("/templates") + params := map[string]string{ + "selector": selector, + } + + return c.templateTransport.ExecuteMultiple(http.MethodGet, uri, nil, params) +} + +// CreateTemplate creates new Template Custom Resource +func (c TemplateClient) CreateTemplate(options CreateTemplateOptions) (template testkube.Template, err error) { + uri := c.templateTransport.GetURI("/templates") + request := testkube.TemplateCreateRequest(options) + + body, err := json.Marshal(request) + if err != nil { + return template, err + } + + return c.templateTransport.Execute(http.MethodPost, uri, body, nil) +} + +// UpdateTemplate updates Template Custom Resource +func (c TemplateClient) UpdateTemplate(options UpdateTemplateOptions) (template testkube.Template, err error) { + uri := c.templateTransport.GetURI("/templates/%s", options.Name) + request := testkube.TemplateUpdateRequest(options) + + body, err := json.Marshal(request) + if err != nil { + return template, err + } + + return c.templateTransport.Execute(http.MethodPatch, uri, body, nil) +} + +// DeleteTemplates deletes all templates +func (c TemplateClient) DeleteTemplates(selector string) (err error) { + uri := c.templateTransport.GetURI("/templates") + return c.templateTransport.Delete(uri, selector, true) +} + +// DeleteTemplate deletes single template by name +func (c TemplateClient) DeleteTemplate(name string) (err error) { + uri := c.templateTransport.GetURI("/templates/%s", name) + return c.templateTransport.Delete(uri, "", true) +} diff --git a/pkg/api/v1/testkube/model_template_extended.go b/pkg/api/v1/testkube/model_template_extended.go new file mode 100644 index 00000000000..7a9f03f669f --- /dev/null +++ b/pkg/api/v1/testkube/model_template_extended.go @@ -0,0 +1,12 @@ +/* + * 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 Templates []Template From 23b36a51d741dd545d0423c9429a73cc5b59eea4 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 31 Aug 2023 20:17:26 +0300 Subject: [PATCH 16/59] fix: get executor template by reference --- pkg/executor/client/job.go | 11 +++++++++++ pkg/executor/containerexecutor/tmpl.go | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go index 96e578fe257..80af28e07a4 100644 --- a/pkg/executor/client/job.go +++ b/pkg/executor/client/job.go @@ -795,6 +795,17 @@ func NewJobOptions(templatesClient templatesv1.Interface, initImage, jobTemplate jobOptions.JobTemplate = jobTemplate } + if options.ExecutorSpec.JobTemplateReference != "" { + template, err := templatesClient.Get(options.ExecutorSpec.JobTemplateReference) + if err != nil { + return jobOptions, err + } + + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { + jobOptions.JobTemplate = template.Spec.Body + } + } + if options.Request.JobTemplateReference != "" { template, err := templatesClient.Get(options.Request.JobTemplateReference) if err != nil { diff --git a/pkg/executor/containerexecutor/tmpl.go b/pkg/executor/containerexecutor/tmpl.go index 1f0e6d98856..dffeb9ff940 100644 --- a/pkg/executor/containerexecutor/tmpl.go +++ b/pkg/executor/containerexecutor/tmpl.go @@ -306,6 +306,17 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface } } + if options.ExecutorSpec.JobTemplateReference != "" { + template, err := templatesClient.Get(options.ExecutorSpec.JobTemplateReference) + if err != nil { + return jobOptions, err + } + + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { + jobOptions.JobTemplate = template.Spec.Body + } + } + if options.Request.JobTemplateReference != "" { template, err := templatesClient.Get(options.Request.JobTemplateReference) if err != nil { From 78c4b151cff8e71bf55c85cb14497760846168bd Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 13:45:56 +0300 Subject: [PATCH 17/59] fix: rename alias --- cmd/kubectl-testkube/commands/templates/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubectl-testkube/commands/templates/delete.go b/cmd/kubectl-testkube/commands/templates/delete.go index ff8b53b2d39..a2312ebe86d 100644 --- a/cmd/kubectl-testkube/commands/templates/delete.go +++ b/cmd/kubectl-testkube/commands/templates/delete.go @@ -16,7 +16,7 @@ func NewDeleteTemplateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "template ", - Aliases: []string{"wh"}, + Aliases: []string{"tp"}, Short: "Delete template", Long: `Delete template, pass template name which should be deleted`, Run: func(cmd *cobra.Command, args []string) { From 86743f96cbdf97671fa369f2cea5e583c9c1cae1 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 13:52:55 +0300 Subject: [PATCH 18/59] fixL list templates --- .../v1/testkube/model_template_extended.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pkg/api/v1/testkube/model_template_extended.go b/pkg/api/v1/testkube/model_template_extended.go index 7a9f03f669f..b46eae50289 100644 --- a/pkg/api/v1/testkube/model_template_extended.go +++ b/pkg/api/v1/testkube/model_template_extended.go @@ -10,3 +10,22 @@ package testkube type Templates []Template + +func (list Templates) Table() (header []string, output [][]string) { + header = []string{"Name", "Type", "Labels"} + + for _, e := range list { + templateType := "" + if e.Type_ != nil { + templateType = string(*e.Type_) + } + + output = append(output, []string{ + e.Name, + templateType, + MapToString(e.Labels), + }) + } + + return +} From ab96b1835a0eb7509252d56bd1b7fa9e68dff65c Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 14:28:09 +0300 Subject: [PATCH 19/59] fix: refactor quoting --- .../model_test_suite_base_extended.go | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pkg/api/v1/testkube/model_test_suite_base_extended.go b/pkg/api/v1/testkube/model_test_suite_base_extended.go index 0626d5ff88c..298fdc6ac0f 100644 --- a/pkg/api/v1/testkube/model_test_suite_base_extended.go +++ b/pkg/api/v1/testkube/model_test_suite_base_extended.go @@ -66,20 +66,17 @@ func (t *TestSuite) QuoteTestSuiteTextFields() { } } - if t.ExecutionRequest.JobTemplate != "" { - t.ExecutionRequest.JobTemplate = fmt.Sprintf("%q", t.ExecutionRequest.JobTemplate) + var fields = []*string{ + &t.ExecutionRequest.JobTemplate, + &t.ExecutionRequest.CronJobTemplate, + &t.ExecutionRequest.PvcTemplate, + &t.ExecutionRequest.ScraperTemplate, } - if t.ExecutionRequest.CronJobTemplate != "" { - t.ExecutionRequest.CronJobTemplate = fmt.Sprintf("%q", t.ExecutionRequest.CronJobTemplate) - } - - if t.ExecutionRequest.PvcTemplate != "" { - t.ExecutionRequest.PvcTemplate = fmt.Sprintf("%q", t.ExecutionRequest.PvcTemplate) - } - - if t.ExecutionRequest.ScraperTemplate != "" { - t.ExecutionRequest.ScraperTemplate = fmt.Sprintf("%q", t.ExecutionRequest.ScraperTemplate) + for _, field := range fields { + if *field != "" { + *field = fmt.Sprintf("%q", *field) + } } } } From 7517459b0cf69c0f507315bd4afb195583d8499c Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 14:43:00 +0300 Subject: [PATCH 20/59] fix: refactor field quoting --- ...odel_test_suite_upsert_request_extended.go | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go b/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go index e484a997b59..2dc887ec12c 100644 --- a/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go +++ b/pkg/api/v1/testkube/model_test_suite_upsert_request_extended.go @@ -21,20 +21,17 @@ func (testSuite *TestSuiteUpsertRequest) QuoteTestSuiteTextFields() { } } - if testSuite.ExecutionRequest.JobTemplate != "" { - testSuite.ExecutionRequest.JobTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.JobTemplate) + var fields = []*string{ + &testSuite.ExecutionRequest.JobTemplate, + &testSuite.ExecutionRequest.CronJobTemplate, + &testSuite.ExecutionRequest.ScraperTemplate, + &testSuite.ExecutionRequest.PvcTemplate, } - if testSuite.ExecutionRequest.CronJobTemplate != "" { - testSuite.ExecutionRequest.CronJobTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.CronJobTemplate) - } - - if testSuite.ExecutionRequest.PvcTemplate != "" { - testSuite.ExecutionRequest.PvcTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.PvcTemplate) - } - - if testSuite.ExecutionRequest.ScraperTemplate != "" { - testSuite.ExecutionRequest.ScraperTemplate = fmt.Sprintf("%q", testSuite.ExecutionRequest.ScraperTemplate) + for _, field := range fields { + if *field != "" { + *field = fmt.Sprintf("%q", *field) + } } } } From b825ca2163fdb59dbb25ca46d2e699762df38484 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 15:51:04 +0300 Subject: [PATCH 21/59] fix: refactor field merging --- pkg/scheduler/test_scheduler.go | 134 +++++++++++++++++---------- pkg/scheduler/testsuite_scheduler.go | 68 +++++++------- 2 files changed, 121 insertions(+), 81 deletions(-) diff --git a/pkg/scheduler/test_scheduler.go b/pkg/scheduler/test_scheduler.go index b57839b9311..0fb857ea530 100644 --- a/pkg/scheduler/test_scheduler.go +++ b/pkg/scheduler/test_scheduler.go @@ -495,26 +495,6 @@ func mergeContents(test testsv3.TestSpec, testSource testsourcev1.TestSourceSpec test.Content.Repository = &testsv3.Repository{} } - if test.Content.Repository.Type_ == "" { - test.Content.Repository.Type_ = testSource.Repository.Type_ - } - - if test.Content.Repository.Uri == "" { - test.Content.Repository.Uri = testSource.Repository.Uri - } - - if test.Content.Repository.Branch == "" { - test.Content.Repository.Branch = testSource.Repository.Branch - } - - if test.Content.Repository.Commit == "" { - test.Content.Repository.Commit = testSource.Repository.Commit - } - - if test.Content.Repository.Path == "" { - test.Content.Repository.Path = testSource.Repository.Path - } - if test.Content.Repository.UsernameSecret == nil && testSource.Repository.UsernameSecret != nil { test.Content.Repository.UsernameSecret = &testsv3.SecretRef{ Name: testSource.Repository.UsernameSecret.Name, @@ -529,16 +509,48 @@ func mergeContents(test testsv3.TestSpec, testSource testsourcev1.TestSourceSpec } } - if test.Content.Repository.WorkingDir == "" { - test.Content.Repository.WorkingDir = testSource.Repository.WorkingDir + if test.Content.Repository.AuthType == "" { + test.Content.Repository.AuthType = testsv3.GitAuthType(testSource.Repository.AuthType) } - if test.Content.Repository.CertificateSecret == "" { - test.Content.Repository.CertificateSecret = testSource.Repository.CertificateSecret + var fields = []struct { + source string + destination *string + }{ + { + testSource.Repository.Type_, + &test.Content.Repository.Type_, + }, + { + testSource.Repository.Uri, + &test.Content.Repository.Uri, + }, + { + testSource.Repository.Branch, + &test.Content.Repository.Branch, + }, + { + testSource.Repository.Commit, + &test.Content.Repository.Commit, + }, + { + testSource.Repository.Path, + &test.Content.Repository.Path, + }, + { + testSource.Repository.WorkingDir, + &test.Content.Repository.WorkingDir, + }, + { + testSource.Repository.CertificateSecret, + &test.Content.Repository.CertificateSecret, + }, } - if test.Content.Repository.AuthType == "" { - test.Content.Repository.AuthType = testsv3.GitAuthType(testSource.Repository.AuthType) + for _, field := range fields { + if *field.destination == "" { + *field.destination = field.source + } } } @@ -573,22 +585,34 @@ func mergeArtifacts(artifactBase *testkube.ArtifactRequest, artifactAdjust *test case artifactBase != nil && artifactAdjust == nil: return artifactBase default: - if artifactBase.StorageClassName == "" && artifactAdjust.StorageClassName != "" { - artifactBase.StorageClassName = artifactAdjust.StorageClassName - } + artifactBase.Dirs = append(artifactBase.Dirs, artifactAdjust.Dirs...) - if artifactBase.VolumeMountPath == "" && artifactAdjust.VolumeMountPath != "" { - artifactBase.VolumeMountPath = artifactAdjust.VolumeMountPath + if !artifactBase.OmitFolderPerExecution && artifactAdjust.OmitFolderPerExecution { + artifactBase.OmitFolderPerExecution = artifactAdjust.OmitFolderPerExecution } - artifactBase.Dirs = append(artifactBase.Dirs, artifactAdjust.Dirs...) - - if artifactBase.StorageBucket == "" && artifactAdjust.StorageBucket != "" { - artifactBase.StorageBucket = artifactAdjust.StorageBucket + var fields = []struct { + source string + destination *string + }{ + { + artifactAdjust.StorageClassName, + &artifactBase.StorageClassName, + }, + { + artifactAdjust.VolumeMountPath, + &artifactBase.VolumeMountPath, + }, + { + artifactAdjust.StorageBucket, + &artifactBase.StorageBucket, + }, } - if !artifactBase.OmitFolderPerExecution && artifactAdjust.OmitFolderPerExecution { - artifactBase.OmitFolderPerExecution = artifactAdjust.OmitFolderPerExecution + for _, field := range fields { + if *field.destination == "" && field.source != "" { + *field.destination = field.source + } } } @@ -607,20 +631,32 @@ func adjustContent(test testsv3.TestSpec, content *testkube.TestContentRequest) } if content.Repository != nil { - if content.Repository.Branch != "" { - test.Content.Repository.Branch = content.Repository.Branch - } - - if content.Repository.Commit != "" { - test.Content.Repository.Commit = content.Repository.Commit + var fields = []struct { + source string + destination *string + }{ + { + content.Repository.Branch, + &test.Content.Repository.Branch, + }, + { + content.Repository.Commit, + &test.Content.Repository.Commit, + }, + { + content.Repository.Path, + &test.Content.Repository.Path, + }, + { + content.Repository.WorkingDir, + &test.Content.Repository.WorkingDir, + }, } - if content.Repository.Path != "" { - test.Content.Repository.Path = content.Repository.Path - } - - if content.Repository.WorkingDir != "" { - test.Content.Repository.WorkingDir = content.Repository.WorkingDir + for _, field := range fields { + if field.source != "" { + *field.destination = field.source + } } } } diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go index 3c860c7b927..d9b715bf3b9 100644 --- a/pkg/scheduler/testsuite_scheduler.go +++ b/pkg/scheduler/testsuite_scheduler.go @@ -56,44 +56,48 @@ func (s *Scheduler) executeTestSuite(ctx context.Context, testSuite testkube.Tes request.SecretUUID = secretUUID if testSuite.ExecutionRequest != nil { - if request.Name == "" && testSuite.ExecutionRequest.Name != "" { - request.Name = testSuite.ExecutionRequest.Name - } - - if request.HttpProxy == "" && testSuite.ExecutionRequest.HttpProxy != "" { - request.HttpProxy = testSuite.ExecutionRequest.HttpProxy - } - - if request.HttpsProxy == "" && testSuite.ExecutionRequest.HttpsProxy != "" { - request.HttpsProxy = testSuite.ExecutionRequest.HttpsProxy - } - if request.Timeout == 0 && testSuite.ExecutionRequest.Timeout != 0 { request.Timeout = testSuite.ExecutionRequest.Timeout } - if request.JobTemplate == "" && testSuite.ExecutionRequest.JobTemplate != "" { - request.JobTemplate = testSuite.ExecutionRequest.JobTemplate - } - - if request.JobTemplateReference == "" && testSuite.ExecutionRequest.JobTemplateReference != "" { - request.JobTemplateReference = testSuite.ExecutionRequest.JobTemplateReference - } - - if request.ScraperTemplate == "" && testSuite.ExecutionRequest.ScraperTemplate != "" { - request.ScraperTemplate = testSuite.ExecutionRequest.ScraperTemplate - } - - if request.ScraperTemplateReference == "" && testSuite.ExecutionRequest.ScraperTemplateReference != "" { - request.ScraperTemplateReference = testSuite.ExecutionRequest.ScraperTemplateReference - } - - if request.PvcTemplate == "" && testSuite.ExecutionRequest.PvcTemplate != "" { - request.PvcTemplate = testSuite.ExecutionRequest.PvcTemplate + var fields = []struct { + source string + destination *string + }{ + { + testSuite.ExecutionRequest.Name, + &request.Name, + }, + { + testSuite.ExecutionRequest.HttpProxy, + &request.HttpProxy, + }, + { + testSuite.ExecutionRequest.HttpsProxy, + &request.HttpsProxy, + }, + { + testSuite.ExecutionRequest.JobTemplate, + &request.JobTemplate, + }, + { + testSuite.ExecutionRequest.JobTemplateReference, + &request.JobTemplateReference, + }, + { + testSuite.ExecutionRequest.ScraperTemplate, + &request.ScraperTemplate, + }, + { + testSuite.ExecutionRequest.ScraperTemplateReference, + &request.ScraperTemplateReference, + }, } - if request.PvcTemplateReference == "" && testSuite.ExecutionRequest.PvcTemplateReference != "" { - request.PvcTemplateReference = testSuite.ExecutionRequest.PvcTemplateReference + for _, field := range fields { + if *field.destination == "" && field.source != "" { + *field.destination = field.source + } } } From 435458d9e4db1d7bd0d62a36d771ea143a150f07 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 15:54:32 +0300 Subject: [PATCH 22/59] fix: merge pvc template --- pkg/scheduler/testsuite_scheduler.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go index d9b715bf3b9..e3d9e3a519c 100644 --- a/pkg/scheduler/testsuite_scheduler.go +++ b/pkg/scheduler/testsuite_scheduler.go @@ -92,6 +92,14 @@ func (s *Scheduler) executeTestSuite(ctx context.Context, testSuite testkube.Tes testSuite.ExecutionRequest.ScraperTemplateReference, &request.ScraperTemplateReference, }, + { + testSuite.ExecutionRequest.PvcTemplate, + &request.PvcTemplate, + }, + { + testSuite.ExecutionRequest.PvcTemplateReference, + &request.PvcTemplateReference, + }, } for _, field := range fields { From 9b09f090e4c7defa546729e64adb786574f1fa4c Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 18:00:51 +0300 Subject: [PATCH 23/59] fix: refactor file reading --- cmd/kubectl-testkube/commands/tests/common.go | 267 +++++++----------- cmd/kubectl-testkube/commands/tests/run.go | 88 +++--- 2 files changed, 144 insertions(+), 211 deletions(-) diff --git a/cmd/kubectl-testkube/commands/tests/common.go b/cmd/kubectl-testkube/commands/tests/common.go index 059f92b7755..4f76191640e 100644 --- a/cmd/kubectl-testkube/commands/tests/common.go +++ b/cmd/kubectl-testkube/commands/tests/common.go @@ -390,80 +390,9 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi } jobTemplateReference := cmd.Flag("job-template-reference").Value.String() - jobTemplateContent := "" - jobTemplate := cmd.Flag("job-template").Value.String() - if jobTemplate != "" { - b, err := os.ReadFile(jobTemplate) - if err != nil { - return nil, err - } - - jobTemplateContent = string(b) - } - cronJobTemplateReference := cmd.Flag("cronjob-template-reference").Value.String() - cronJobTemplateContent := "" - cronJobTemplate := cmd.Flag("cronjob-template").Value.String() - if cronJobTemplate != "" { - b, err := os.ReadFile(cronJobTemplate) - if err != nil { - return nil, err - } - - cronJobTemplateContent = string(b) - } - - preRunScriptContent := "" - preRunScript := cmd.Flag("prerun-script").Value.String() - if preRunScript != "" { - b, err := os.ReadFile(preRunScript) - if err != nil { - return nil, err - } - - preRunScriptContent = string(b) - } - - postRunScriptContent := "" - postRunScript := cmd.Flag("postrun-script").Value.String() - if postRunScript != "" { - b, err := os.ReadFile(postRunScript) - if err != nil { - return nil, err - } - - postRunScriptContent = string(b) - } - scraperTemplateReference := cmd.Flag("scraper-template-reference").Value.String() - scraperTemplateContent := "" - scraperTemplate := cmd.Flag("scraper-template").Value.String() - if scraperTemplate != "" { - b, err := os.ReadFile(scraperTemplate) - if err != nil { - return nil, err - } - - scraperTemplateContent = string(b) - } - pvcTemplateReference := cmd.Flag("pvc-template-reference").Value.String() - pvcTemplateContent := "" - pvcTemplate := cmd.Flag("pvc-template").Value.String() - if pvcTemplate != "" { - b, err := os.ReadFile(scraperTemplate) - if err != nil { - return nil, err - } - - scraperTemplateContent = string(b) - } - - envConfigMaps, envSecrets, err := newEnvReferencesFromFlags(cmd) - if err != nil { - return nil, err - } - request = &testkube.ExecutionRequest{ Name: executionName, Variables: variables, @@ -477,19 +406,57 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi HttpProxy: httpProxy, HttpsProxy: httpsProxy, ActiveDeadlineSeconds: timeout, - JobTemplate: jobTemplateContent, JobTemplateReference: jobTemplateReference, - CronJobTemplate: cronJobTemplateContent, CronJobTemplateReference: cronJobTemplateReference, - PreRunScript: preRunScriptContent, - PostRunScript: postRunScriptContent, - ScraperTemplate: scraperTemplateContent, ScraperTemplateReference: scraperTemplateReference, - PvcTemplate: pvcTemplateContent, PvcTemplateReference: pvcTemplateReference, NegativeTest: negativeTest, - EnvConfigMaps: envConfigMaps, - EnvSecrets: envSecrets, + } + + var fields = []struct { + source string + destination *string + }{ + { + cmd.Flag("job-template").Value.String(), + &request.JobTemplate, + }, + { + cmd.Flag("cronjob-template").Value.String(), + &request.CronJobTemplate, + }, + { + cmd.Flag("prerun-script").Value.String(), + &request.PreRunScript, + }, + { + cmd.Flag("postrun-script").Value.String(), + &request.PostRunScript, + }, + { + cmd.Flag("scraper-template").Value.String(), + &request.ScraperTemplate, + }, + { + cmd.Flag("pvc-template").Value.String(), + &request.PvcTemplate, + }, + } + + for _, field := range fields { + if field.source != "" { + b, err := os.ReadFile(field.source) + if err != nil { + return nil, err + } + + *field.destination = string(b) + } + } + + request.EnvConfigMaps, request.EnvSecrets, err = newEnvReferencesFromFlags(cmd) + if err != nil { + return nil, err } request.ArtifactRequest, err = newArtifactRequestFromFlags(cmd) @@ -955,100 +922,52 @@ func newExecutionUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.E nonEmpty = true } - if cmd.Flag("job-template").Changed { - jobTemplateContent := "" - jobTemplate := cmd.Flag("job-template").Value.String() - if jobTemplate != "" { - b, err := os.ReadFile(jobTemplate) - if err != nil { - return nil, err - } - - jobTemplateContent = string(b) - } - - request.JobTemplate = &jobTemplateContent - nonEmpty = true - } - - if cmd.Flag("cronjob-template").Changed { - cronJobTemplateContent := "" - cronJobTemplate := cmd.Flag("cronjob-template").Value.String() - if cronJobTemplate != "" { - b, err := os.ReadFile(cronJobTemplate) - if err != nil { - return nil, err - } - - cronJobTemplateContent = string(b) - } - - request.CronJobTemplate = &cronJobTemplateContent - nonEmpty = true - } - - if cmd.Flag("prerun-script").Changed { - preRunScriptContent := "" - preRunScript := cmd.Flag("prerun-script").Value.String() - if preRunScript != "" { - b, err := os.ReadFile(preRunScript) - if err != nil { - return nil, err - } - - preRunScriptContent = string(b) - } - - request.PreRunScript = &preRunScriptContent - nonEmpty = true - } - - if cmd.Flag("postrun-script").Changed { - postRunScriptContent := "" - postRunScript := cmd.Flag("postrun-script").Value.String() - if postRunScript != "" { - b, err := os.ReadFile(postRunScript) - if err != nil { - return nil, err - } - - postRunScriptContent = string(b) - } - - request.PostRunScript = &postRunScriptContent - nonEmpty = true + var values = []struct { + source string + destination **string + }{ + { + "job-template", + &request.JobTemplate, + }, + { + "cronjob-template", + &request.CronJobTemplate, + }, + { + "prerun-script", + &request.PreRunScript, + }, + { + "postrun-script", + &request.PostRunScript, + }, + { + "scraper-template", + &request.ScraperTemplate, + }, + { + "pvc-template", + &request.PvcTemplate, + }, } - if cmd.Flag("scraper-template").Changed { - scraperTemplateContent := "" - scraperTemplate := cmd.Flag("scraper-template").Value.String() - if scraperTemplate != "" { - b, err := os.ReadFile(scraperTemplate) - if err != nil { - return nil, err - } - - scraperTemplateContent = string(b) - } - - request.ScraperTemplate = &scraperTemplateContent - nonEmpty = true - } + for _, value := range values { + if cmd.Flag(value.source).Changed { + data := "" + name := cmd.Flag(value.source).Value.String() + if data != "" { + b, err := os.ReadFile(name) + if err != nil { + return nil, err + } - if cmd.Flag("pvc-template").Changed { - pvcTemplateContent := "" - pvcTemplate := cmd.Flag("pvc-template").Value.String() - if pvcTemplate != "" { - b, err := os.ReadFile(pvcTemplate) - if err != nil { - return nil, err + data = string(b) } - pvcTemplateContent = string(b) + *value.destination = &data + nonEmpty = true } - - request.PvcTemplate = &pvcTemplateContent - nonEmpty = true } if cmd.Flag("mount-configmap").Changed || cmd.Flag("variable-configmap").Changed { @@ -1104,6 +1023,10 @@ func newArtifactUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.Ar "artifact-volume-mount-path", &request.VolumeMountPath, }, + { + "artifact-storage-bucket", + &request.StorageBucket, + }, } var nonEmpty bool @@ -1125,6 +1048,16 @@ func newArtifactUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.Ar nonEmpty = true } + if cmd.Flag("artifact-omit-folder-per-execution").Changed { + value, err := cmd.Flags().GetBool("artifact-omit-folder-per-execution") + if err != nil { + return nil, err + } + + request.OmitFolderPerExecution = &value + nonEmpty = true + } + if nonEmpty { return request, nil } diff --git a/cmd/kubectl-testkube/commands/tests/run.go b/cmd/kubectl-testkube/commands/tests/run.go index 9dda9d04ee0..1b03d41e2bc 100644 --- a/cmd/kubectl-testkube/commands/tests/run.go +++ b/cmd/kubectl-testkube/commands/tests/run.go @@ -86,50 +86,11 @@ func NewRunTestCmd() *cobra.Command { envConfigMaps, envSecrets, err := newEnvReferencesFromFlags(cmd) ui.WarnOnError("getting env config maps and secrets", err) - jobTemplateContent := "" - if jobTemplate != "" { - b, err := os.ReadFile(jobTemplate) - ui.ExitOnError("reading job template", err) - jobTemplateContent = string(b) - } - - preRunScriptContent := "" - if preRunScript != "" { - b, err := os.ReadFile(preRunScript) - ui.ExitOnError("reading pre run script", err) - preRunScriptContent = string(b) - } - - postRunScriptContent := "" - if postRunScript != "" { - b, err := os.ReadFile(postRunScript) - ui.ExitOnError("reading post run script", err) - postRunScriptContent = string(b) - } - - scraperTemplateContent := "" - if scraperTemplate != "" { - b, err := os.ReadFile(scraperTemplate) - ui.ExitOnError("reading scraper template", err) - scraperTemplateContent = string(b) - } - - pvcTemplateContent := "" - if pvcTemplate != "" { - b, err := os.ReadFile(pvcTemplate) - ui.ExitOnError("reading pvc template", err) - pvcTemplateContent = string(b) - } - mode := "" if cmd.Flag("args-mode").Changed { mode = argsMode } - var executions []testkube.Execution - client, namespace, err := common.GetClient(cmd) - ui.ExitOnError("getting client", err) - options := apiv1.ExecuteTestOptions{ ExecutionVariables: variables, ExecutionLabels: executionLabels, @@ -141,13 +102,8 @@ func NewRunTestCmd() *cobra.Command { HTTPSProxy: httpsProxy, Envs: envs, Image: image, - JobTemplate: jobTemplateContent, JobTemplateReference: jobTemplateReference, - PreRunScriptContent: preRunScriptContent, - PostRunScriptContent: postRunScriptContent, - ScraperTemplate: scraperTemplateContent, ScraperTemplateReference: scraperTemplateReference, - PvcTemplate: pvcTemplateContent, PvcTemplateReference: pvcTemplateReference, IsNegativeTestChangedOnRun: false, EnvConfigMaps: envConfigMaps, @@ -158,6 +114,50 @@ func NewRunTestCmd() *cobra.Command { }, } + var fields = []struct { + source string + title string + destination *string + }{ + { + jobTemplate, + "job template", + &options.JobTemplate, + }, + { + preRunScript, + "pre run script", + &options.PreRunScriptContent, + }, + { + postRunScript, + "post run script", + &options.PostRunScriptContent, + }, + { + scraperTemplate, + "scraper template", + &options.ScraperTemplate, + }, + { + pvcTemplate, + "pvc template", + &options.PvcTemplate, + }, + } + + for _, field := range fields { + if field.source != "" { + b, err := os.ReadFile(field.source) + ui.ExitOnError("reading "+field.title, err) + *field.destination = string(b) + } + } + + var executions []testkube.Execution + client, namespace, err := common.GetClient(cmd) + ui.ExitOnError("getting client", err) + if artifactStorageClassName != "" || artifactVolumeMountPath != "" || len(artifactDirs) != 0 || artifactStorageBucket != "" || artifactOmitFolderPerExecution { options.ArtifactRequest = &testkube.ArtifactRequest{ From 4f9e96a8b134b3641160cca5e8cc9ce8d3a333ce Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 18:06:09 +0300 Subject: [PATCH 24/59] fix: refacto run command --- .../commands/testsuites/run.go | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/cmd/kubectl-testkube/commands/testsuites/run.go b/cmd/kubectl-testkube/commands/testsuites/run.go index fba589a5376..33700e2595c 100644 --- a/cmd/kubectl-testkube/commands/testsuites/run.go +++ b/cmd/kubectl-testkube/commands/testsuites/run.go @@ -53,47 +53,53 @@ func NewRunTestSuiteCmd() *cobra.Command { var executions []testkube.TestSuiteExecution - jobTemplateContent := "" - if jobTemplate != "" { - b, err := os.ReadFile(jobTemplate) - ui.ExitOnError("reading job template", err) - jobTemplateContent = string(b) - } - - scraperTemplateContent := "" - if scraperTemplate != "" { - b, err := os.ReadFile(scraperTemplate) - ui.ExitOnError("reading scraper template", err) - scraperTemplateContent = string(b) - } - - pvcTemplateContent := "" - if pvcTemplate != "" { - b, err := os.ReadFile(pvcTemplate) - ui.ExitOnError("reading pvc template", err) - pvcTemplateContent = string(b) - } - - variables, err := common.CreateVariables(cmd, false) - ui.WarnOnError("getting variables", err) options := apiv1.ExecuteTestSuiteOptions{ - ExecutionVariables: variables, - HTTPProxy: httpProxy, - HTTPSProxy: httpsProxy, - ExecutionLabels: executionLabels, + HTTPProxy: httpProxy, + HTTPSProxy: httpsProxy, + ExecutionLabels: executionLabels, RunningContext: &testkube.RunningContext{ Type_: string(testkube.RunningContextTypeUserCLI), Context: runningContext, }, ConcurrencyLevel: int32(concurrencyLevel), - JobTemplate: jobTemplateContent, JobTemplateReference: jobTemplateReference, - ScraperTemplate: scraperTemplateContent, ScraperTemplateReference: scraperTemplateReference, - PvcTemplate: pvcTemplateContent, PvcTemplateReference: pvcTemplateReference, } + var fields = []struct { + source string + title string + destination *string + }{ + { + jobTemplate, + "job template", + &options.JobTemplate, + }, + { + scraperTemplate, + "scraper template", + &options.ScraperTemplate, + }, + { + pvcTemplate, + "pvc template", + &options.PvcTemplate, + }, + } + + for _, field := range fields { + if field.source != "" { + b, err := os.ReadFile(field.source) + ui.ExitOnError("reading "+field.title, err) + *field.destination = string(b) + } + } + + options.ExecutionVariables, err = common.CreateVariables(cmd, false) + ui.WarnOnError("getting variables", err) + if gitBranch != "" || gitCommit != "" || gitPath != "" || gitWorkingDir != "" { options.ContentRequest = &testkube.TestContentRequest{ Repository: &testkube.RepositoryParameters{ From c88f30378876dd23a594ab9ea89b02ee593920a4 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 1 Sep 2023 18:19:18 +0300 Subject: [PATCH 25/59] fix: refactor common module --- .../commands/testsuites/common.go | 168 +++++++----------- 1 file changed, 65 insertions(+), 103 deletions(-) diff --git a/cmd/kubectl-testkube/commands/testsuites/common.go b/cmd/kubectl-testkube/commands/testsuites/common.go index 68f3aee9650..eefb13acd3c 100644 --- a/cmd/kubectl-testkube/commands/testsuites/common.go +++ b/cmd/kubectl-testkube/commands/testsuites/common.go @@ -150,52 +150,9 @@ func NewTestSuiteUpsertOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 } jobTemplateReference := cmd.Flag("job-template-reference").Value.String() - jobTemplateContent := "" - jobTemplate := cmd.Flag("job-template").Value.String() - if jobTemplate != "" { - b, err := os.ReadFile(jobTemplate) - if err != nil { - return options, err - } - - jobTemplateContent = string(b) - } - cronJobTemplateReference := cmd.Flag("cronjob-template-refeence").Value.String() - cronJobTemplateContent := "" - cronJobTemplate := cmd.Flag("cronjob-template").Value.String() - if cronJobTemplate != "" { - b, err := os.ReadFile(cronJobTemplate) - if err != nil { - return options, err - } - - cronJobTemplateContent = string(b) - } - scraperTemplateReference := cmd.Flag("scraper-template-reference").Value.String() - scraperTemplateContent := "" - scraperTemplate := cmd.Flag("scraper-template").Value.String() - if scraperTemplate != "" { - b, err := os.ReadFile(scraperTemplate) - if err != nil { - return options, err - } - - scraperTemplateContent = string(b) - } - pvcTemplateReference := cmd.Flag("pvc-template-reference").Value.String() - pvcTemplateContent := "" - pvcTemplate := cmd.Flag("pvc-template").Value.String() - if pvcTemplate != "" { - b, err := os.ReadFile(pvcTemplate) - if err != nil { - return options, err - } - - pvcTemplateContent = string(b) - } options.Schedule = schedule options.ExecutionRequest = &testkube.TestSuiteExecutionRequest{ @@ -204,16 +161,45 @@ func NewTestSuiteUpsertOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 HttpProxy: cmd.Flag("http-proxy").Value.String(), HttpsProxy: cmd.Flag("https-proxy").Value.String(), Timeout: timeout, - JobTemplate: jobTemplateContent, JobTemplateReference: jobTemplateReference, - CronJobTemplate: cronJobTemplateContent, CronJobTemplateReference: cronJobTemplateReference, - ScraperTemplate: scraperTemplateContent, ScraperTemplateReference: scraperTemplateReference, - PvcTemplate: pvcTemplateContent, PvcTemplateReference: pvcTemplateReference, } + var fields = []struct { + source string + destination *string + }{ + { + cmd.Flag("job-template").Value.String(), + &options.ExecutionRequest.JobTemplate, + }, + { + cmd.Flag("cronjob-template").Value.String(), + &options.ExecutionRequest.CronJobTemplate, + }, + { + cmd.Flag("scraper-template").Value.String(), + &options.ExecutionRequest.ScraperTemplate, + }, + { + cmd.Flag("pvc-template").Value.String(), + &options.ExecutionRequest.PvcTemplate, + }, + } + + for _, field := range fields { + if field.source != "" { + b, err := os.ReadFile(field.source) + if err != nil { + return options, err + } + + *field.destination = string(b) + } + } + return options, nil } @@ -312,68 +298,44 @@ func NewTestSuiteUpdateOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 nonEmpty = true } - if cmd.Flag("job-template").Changed { - jobTemplateContent := "" - jobTemplate := cmd.Flag("job-template").Value.String() - if jobTemplate != "" { - b, err := os.ReadFile(jobTemplate) - if err != nil { - return options, err - } - - jobTemplateContent = string(b) - } - - executionRequest.JobTemplate = &jobTemplateContent - nonEmpty = true - } - - if cmd.Flag("cronjob-template").Changed { - cronJobTemplateContent := "" - cronJobTemplate := cmd.Flag("cronjob-template").Value.String() - if cronJobTemplate != "" { - b, err := os.ReadFile(cronJobTemplate) - if err != nil { - return options, err - } - - cronJobTemplateContent = string(b) - } - - executionRequest.CronJobTemplate = &cronJobTemplateContent - nonEmpty = true + var values = []struct { + source string + destination **string + }{ + { + "job-template", + &executionRequest.JobTemplate, + }, + { + "cronjob-template", + &executionRequest.CronJobTemplate, + }, + { + "scraper-template", + &executionRequest.ScraperTemplate, + }, + { + "pvc-template", + &executionRequest.PvcTemplate, + }, } - if cmd.Flag("scraper-template").Changed { - scraperTemplateContent := "" - scraperTemplate := cmd.Flag("scraper-template").Value.String() - if scraperTemplate != "" { - b, err := os.ReadFile(scraperTemplate) - if err != nil { - return options, err - } - - scraperTemplateContent = string(b) - } - - executionRequest.ScraperTemplate = &scraperTemplateContent - nonEmpty = true - } + for _, value := range values { + if cmd.Flag(value.source).Changed { + data := "" + name := cmd.Flag(value.source).Value.String() + if data != "" { + b, err := os.ReadFile(name) + if err != nil { + return options, err + } - if cmd.Flag("pvc-template").Changed { - pvcTemplateContent := "" - pvcTemplate := cmd.Flag("pvc-template").Value.String() - if pvcTemplate != "" { - b, err := os.ReadFile(pvcTemplate) - if err != nil { - return options, err + data = string(b) } - pvcTemplateContent = string(b) + *value.destination = &data + nonEmpty = true } - - executionRequest.ScraperTemplate = &pvcTemplateContent - nonEmpty = true } var executionFields = []struct { From 9c0510880e607d17f8aa47e107302cac185bae36 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 4 Sep 2023 12:14:10 +0300 Subject: [PATCH 26/59] fix: sync docs --- cmd/kubectl-testkube/commands/create.go | 2 ++ docs/docs/cli/testkube_create.md | 1 + docs/docs/cli/testkube_create_template.md | 37 +++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 docs/docs/cli/testkube_create_template.md diff --git a/cmd/kubectl-testkube/commands/create.go b/cmd/kubectl-testkube/commands/create.go index 4e5e344afa0..46de9b05cc9 100644 --- a/cmd/kubectl-testkube/commands/create.go +++ b/cmd/kubectl-testkube/commands/create.go @@ -6,6 +6,7 @@ import ( "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/executors" + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/templates" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsources" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites" @@ -41,6 +42,7 @@ func NewCreateCmd() *cobra.Command { cmd.AddCommand(webhooks.NewCreateWebhookCmd()) cmd.AddCommand(executors.NewCreateExecutorCmd()) cmd.AddCommand(testsources.NewCreateTestSourceCmd()) + cmd.AddCommand(templates.NewCreateTemplateCmd()) cmd.PersistentFlags().BoolVar(&crdOnly, "crd-only", false, "generate only crd") diff --git a/docs/docs/cli/testkube_create.md b/docs/docs/cli/testkube_create.md index 63a7f60f50d..197297fa587 100644 --- a/docs/docs/cli/testkube_create.md +++ b/docs/docs/cli/testkube_create.md @@ -27,6 +27,7 @@ testkube create [flags] * [testkube](testkube.md) - Testkube entrypoint for kubectl plugin * [testkube create executor](testkube_create_executor.md) - Create new Executor +* [testkube create template](testkube_create_template.md) - Create new Template * [testkube create test](testkube_create_test.md) - Create new Test * [testkube create testsource](testkube_create_testsource.md) - Create new TestSource * [testkube create testsuite](testkube_create_testsuite.md) - Create new TestSuite diff --git a/docs/docs/cli/testkube_create_template.md b/docs/docs/cli/testkube_create_template.md new file mode 100644 index 00000000000..978d79cb367 --- /dev/null +++ b/docs/docs/cli/testkube_create_template.md @@ -0,0 +1,37 @@ +## testkube create template + +Create new Template + +### Synopsis + +Create new Template Custom Resource + +``` +testkube create template [flags] +``` + +### Options + +``` + --body string a path to template file to use as template body + -h, --help help for template + -l, --label stringToString label key value pair: --label key1=value1 (default []) + -n, --name string unique template name - mandatory + --template-type string template type one of job|container|cronnjob|scraper|pvc|webhook +``` + +### Options inherited from parent commands + +``` + -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results/v1") + -c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy") + --crd-only generate only crd + --namespace string Kubernetes namespace, default value read from config if set (default "testkube") + --oauth-enabled enable oauth + --verbose show additional debug messages +``` + +### SEE ALSO + +* [testkube create](testkube_create.md) - Create resource + From 672175c38b915114f552a8a59aed51ac7edbaff6 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 4 Sep 2023 17:12:39 +0300 Subject: [PATCH 27/59] fix: webhook payload template reference --- cmd/kubectl-testkube/commands/tests/common.go | 2 +- .../commands/tests/renderer/test_obj.go | 8 +-- .../commands/testsuites/common.go | 2 +- .../testsuites/renderer/testsuite_obj.go | 8 +-- .../commands/webhooks/common.go | 24 +++++--- .../commands/webhooks/create.go | 16 ++--- .../commands/webhooks/update.go | 16 ++--- docs/docs/cli/testkube_create_webhook.md | 19 +++--- docs/docs/cli/testkube_update_webhook.md | 19 +++--- pkg/crd/crd_test.go | 61 ++++++++++--------- 10 files changed, 95 insertions(+), 80 deletions(-) diff --git a/cmd/kubectl-testkube/commands/tests/common.go b/cmd/kubectl-testkube/commands/tests/common.go index 4f76191640e..76e6994b320 100644 --- a/cmd/kubectl-testkube/commands/tests/common.go +++ b/cmd/kubectl-testkube/commands/tests/common.go @@ -956,7 +956,7 @@ func newExecutionUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.E if cmd.Flag(value.source).Changed { data := "" name := cmd.Flag(value.source).Value.String() - if data != "" { + if name != "" { b, err := os.ReadFile(name) if err != nil { return nil, err diff --git a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go index 1e8dd8f1f77..76882ce7242 100644 --- a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go +++ b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go @@ -137,7 +137,7 @@ func TestRenderer(ui *ui.UI, obj interface{}) error { } if test.ExecutionRequest.JobTemplateReference != "" { - ui.Warn(" Job template reference: ", "\n", test.ExecutionRequest.JobTemplateReference) + ui.Warn(" Job template reference: ", test.ExecutionRequest.JobTemplateReference) } if test.ExecutionRequest.CronJobTemplate != "" { @@ -145,7 +145,7 @@ func TestRenderer(ui *ui.UI, obj interface{}) error { } if test.ExecutionRequest.CronJobTemplateReference != "" { - ui.Warn(" Cron job template reference: ", "\n", test.ExecutionRequest.CronJobTemplateReference) + ui.Warn(" Cron job template reference: ", test.ExecutionRequest.CronJobTemplateReference) } if test.ExecutionRequest.PreRunScript != "" { @@ -161,7 +161,7 @@ func TestRenderer(ui *ui.UI, obj interface{}) error { } if test.ExecutionRequest.ScraperTemplateReference != "" { - ui.Warn(" Scraper template reference: ", "\n", test.ExecutionRequest.ScraperTemplateReference) + ui.Warn(" Scraper template reference: ", test.ExecutionRequest.ScraperTemplateReference) } if test.ExecutionRequest.PvcTemplate != "" { @@ -169,7 +169,7 @@ func TestRenderer(ui *ui.UI, obj interface{}) error { } if test.ExecutionRequest.PvcTemplateReference != "" { - ui.Warn(" PVC template reference: ", "\n", test.ExecutionRequest.PvcTemplateReference) + ui.Warn(" PVC template reference: ", test.ExecutionRequest.PvcTemplateReference) } var mountConfigMaps, mountSecrets []mountParams diff --git a/cmd/kubectl-testkube/commands/testsuites/common.go b/cmd/kubectl-testkube/commands/testsuites/common.go index eefb13acd3c..59c58397709 100644 --- a/cmd/kubectl-testkube/commands/testsuites/common.go +++ b/cmd/kubectl-testkube/commands/testsuites/common.go @@ -324,7 +324,7 @@ func NewTestSuiteUpdateOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 if cmd.Flag(value.source).Changed { data := "" name := cmd.Flag(value.source).Value.String() - if data != "" { + if name != "" { b, err := os.ReadFile(name) if err != nil { return options, err diff --git a/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go b/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go index c12881e457c..c0540326f3d 100644 --- a/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go +++ b/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go @@ -49,7 +49,7 @@ func TestSuiteRenderer(ui *ui.UI, obj interface{}) error { } if ts.ExecutionRequest.JobTemplate != "" { - ui.Warn(" Job template: ", ts.ExecutionRequest.JobTemplate) + ui.Warn(" Job template: ", "\n", ts.ExecutionRequest.JobTemplate) } if ts.ExecutionRequest.JobTemplateReference != "" { @@ -57,7 +57,7 @@ func TestSuiteRenderer(ui *ui.UI, obj interface{}) error { } if ts.ExecutionRequest.CronJobTemplate != "" { - ui.Warn(" Cron job template: ", ts.ExecutionRequest.CronJobTemplate) + ui.Warn(" Cron job template: ", "\n", ts.ExecutionRequest.CronJobTemplate) } if ts.ExecutionRequest.CronJobTemplateReference != "" { @@ -65,7 +65,7 @@ func TestSuiteRenderer(ui *ui.UI, obj interface{}) error { } if ts.ExecutionRequest.ScraperTemplate != "" { - ui.Warn(" Scraper template: ", ts.ExecutionRequest.ScraperTemplate) + ui.Warn(" Scraper template: ", "\n", ts.ExecutionRequest.ScraperTemplate) } if ts.ExecutionRequest.ScraperTemplateReference != "" { @@ -73,7 +73,7 @@ func TestSuiteRenderer(ui *ui.UI, obj interface{}) error { } if ts.ExecutionRequest.PvcTemplate != "" { - ui.Warn(" PVC template: ", ts.ExecutionRequest.PvcTemplate) + ui.Warn(" PVC template: ", "\n", ts.ExecutionRequest.PvcTemplate) } if ts.ExecutionRequest.PvcTemplateReference != "" { diff --git a/cmd/kubectl-testkube/commands/webhooks/common.go b/cmd/kubectl-testkube/commands/webhooks/common.go index fbff807dd63..c818cc62035 100644 --- a/cmd/kubectl-testkube/commands/webhooks/common.go +++ b/cmd/kubectl-testkube/commands/webhooks/common.go @@ -42,16 +42,18 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW return options, err } + payloadTemplateReference := cmd.Flag("payload-template-reference").Value.String() options = apiv1.CreateWebhookOptions{ - Name: name, - Namespace: namespace, - Events: webhooksmapper.MapStringArrayToCRDEvents(events), - Uri: uri, - Selector: selector, - Labels: labels, - PayloadObjectField: payloadObjectField, - PayloadTemplate: payloadTemplateContent, - Headers: headers, + Name: name, + Namespace: namespace, + Events: webhooksmapper.MapStringArrayToCRDEvents(events), + Uri: uri, + Selector: selector, + Labels: labels, + PayloadObjectField: payloadObjectField, + PayloadTemplate: payloadTemplateContent, + Headers: headers, + PayloadTemplateReference: payloadTemplateReference, } return options, nil @@ -79,6 +81,10 @@ func NewUpdateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.UpdateW "payload-field", &options.PayloadObjectField, }, + { + "payload-template-reference", + &options.PayloadTemplateReference, + }, } for _, field := range fields { diff --git a/cmd/kubectl-testkube/commands/webhooks/create.go b/cmd/kubectl-testkube/commands/webhooks/create.go index b615e33eab5..01fb3cfba9a 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -14,13 +14,14 @@ import ( func NewCreateWebhookCmd() *cobra.Command { var ( - events []string - name, uri string - selector string - labels map[string]string - payloadObjectField string - payloadTemplate string - headers map[string]string + events []string + name, uri string + selector string + labels map[string]string + payloadObjectField string + payloadTemplate string + headers map[string]string + payloadTemplateReference string ) cmd := &cobra.Command{ @@ -77,6 +78,7 @@ func NewCreateWebhookCmd() *cobra.Command { cmd.Flags().StringVarP(&payloadObjectField, "payload-field", "", "", "field to use for notification object payload") cmd.Flags().StringVarP(&payloadTemplate, "payload-template", "", "", "if webhook needs to send a custom notification, then a path to template file should be provided") cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair: --header Content-Type=application/xml") + cmd.Flags().StringVar(&payloadTemplateReference, "payload-template-reference", "", "reference to payload template to use for the webhook") return cmd } diff --git a/cmd/kubectl-testkube/commands/webhooks/update.go b/cmd/kubectl-testkube/commands/webhooks/update.go index 81d2a469a33..a9edb0d80a1 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -9,13 +9,14 @@ import ( func UpdateWebhookCmd() *cobra.Command { var ( - events []string - name, uri string - selector string - labels map[string]string - payloadObjectField string - payloadTemplate string - headers map[string]string + events []string + name, uri string + selector string + labels map[string]string + payloadObjectField string + payloadTemplate string + headers map[string]string + payloadTemplateReference string ) cmd := &cobra.Command{ @@ -54,6 +55,7 @@ func UpdateWebhookCmd() *cobra.Command { cmd.Flags().StringVarP(&payloadObjectField, "payload-field", "", "", "field to use for notification object payload") cmd.Flags().StringVarP(&payloadTemplate, "payload-template", "", "", "if webhook needs to send a custom notification, then a path to template file should be provided") cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair: --header Content-Type=application/xml") + cmd.Flags().StringVar(&payloadTemplateReference, "payload-template-reference", "", "reference to payload template to use for the webhook") return cmd } diff --git a/docs/docs/cli/testkube_create_webhook.md b/docs/docs/cli/testkube_create_webhook.md index ae50d764a27..c504d3564bd 100644 --- a/docs/docs/cli/testkube_create_webhook.md +++ b/docs/docs/cli/testkube_create_webhook.md @@ -13,15 +13,16 @@ testkube create webhook [flags] ### Options ``` - -e, --events stringArray event types handled by webhook e.g. start-test|end-test - --header stringToString webhook header value pair: --header Content-Type=application/xml (default []) - -h, --help help for webhook - -l, --label stringToString label key value pair: --label key1=value1 (default []) - -n, --name string unique webhook name - mandatory - --payload-field string field to use for notification object payload - --payload-template string if webhook needs to send a custom notification, then a path to template file should be provided - --selector string expression to select tests and test suites for webhook events: --selector app=backend - -u, --uri string URI which should be called when given event occurs + -e, --events stringArray event types handled by webhook e.g. start-test|end-test + --header stringToString webhook header value pair: --header Content-Type=application/xml (default []) + -h, --help help for webhook + -l, --label stringToString label key value pair: --label key1=value1 (default []) + -n, --name string unique webhook name - mandatory + --payload-field string field to use for notification object payload + --payload-template string if webhook needs to send a custom notification, then a path to template file should be provided + --payload-template-reference string reference to payload template to use for the webhook + --selector string expression to select tests and test suites for webhook events: --selector app=backend + -u, --uri string URI which should be called when given event occurs ``` ### Options inherited from parent commands diff --git a/docs/docs/cli/testkube_update_webhook.md b/docs/docs/cli/testkube_update_webhook.md index 08772132bf6..64ce6abcfcf 100644 --- a/docs/docs/cli/testkube_update_webhook.md +++ b/docs/docs/cli/testkube_update_webhook.md @@ -13,15 +13,16 @@ testkube update webhook [flags] ### Options ``` - -e, --events stringArray event types handled by webhook e.g. start-test|end-test - --header stringToString webhook header value pair: --header Content-Type=application/xml (default []) - -h, --help help for webhook - -l, --label stringToString label key value pair: --label key1=value1 (default []) - -n, --name string unique webhook name - mandatory - --payload-field string field to use for notification object payload - --payload-template string if webhook needs to send a custom notification, then a path to template file should be provided - --selector string expression to select tests and test suites for webhook events: --selector app=backend - -u, --uri string URI which should be called when given event occurs + -e, --events stringArray event types handled by webhook e.g. start-test|end-test + --header stringToString webhook header value pair: --header Content-Type=application/xml (default []) + -h, --help help for webhook + -l, --label stringToString label key value pair: --label key1=value1 (default []) + -n, --name string unique webhook name - mandatory + --payload-field string field to use for notification object payload + --payload-template string if webhook needs to send a custom notification, then a path to template file should be provided + --payload-template-reference string reference to payload template to use for the webhook + --selector string expression to select tests and test suites for webhook events: --selector app=backend + -u, --uri string URI which should be called when given event occurs ``` ### Options inherited from parent commands diff --git a/pkg/crd/crd_test.go b/pkg/crd/crd_test.go index 93f0eab4fcf..4b9ef6812c0 100644 --- a/pkg/crd/crd_test.go +++ b/pkg/crd/crd_test.go @@ -12,18 +12,19 @@ func TestGenerateYAML(t *testing.T) { t.Run("generate single CRD yaml", func(t *testing.T) { // given - expected := "apiVersion: executor.testkube.io/v1\nkind: Webhook\nmetadata:\n name: name1\n namespace: namespace1\n labels:\n key1: value1\nspec:\n events:\n - start-test\n uri: http://localhost\n selector: app=backend\n payloadObjectField: text\n payloadTemplate: {{ .Id }}\n headers:\n Content-Type: appication/xml\n" + expected := "apiVersion: executor.testkube.io/v1\nkind: Webhook\nmetadata:\n name: name1\n namespace: namespace1\n labels:\n key1: value1\nspec:\n events:\n - start-test\n uri: http://localhost\n selector: app=backend\n payloadObjectField: text\n payloadTemplate: {{ .Id }}\n payloadTemplateReference: ref\n headers:\n Content-Type: appication/xml\n" webhooks := []testkube.Webhook{ { - Name: "name1", - Namespace: "namespace1", - Uri: "http://localhost", - Events: []testkube.EventType{*testkube.EventStartTest}, - Selector: "app=backend", - Labels: map[string]string{"key1": "value1"}, - PayloadObjectField: "text", - PayloadTemplate: "{{ .Id }}", - Headers: map[string]string{"Content-Type": "appication/xml"}, + Name: "name1", + Namespace: "namespace1", + Uri: "http://localhost", + Events: []testkube.EventType{*testkube.EventStartTest}, + Selector: "app=backend", + Labels: map[string]string{"key1": "value1"}, + PayloadObjectField: "text", + PayloadTemplate: "{{ .Id }}", + Headers: map[string]string{"Content-Type": "appication/xml"}, + PayloadTemplateReference: "ref", }, } @@ -37,29 +38,31 @@ func TestGenerateYAML(t *testing.T) { t.Run("generate multiple CRDs yaml", func(t *testing.T) { // given - expected := "apiVersion: executor.testkube.io/v1\nkind: Webhook\nmetadata:\n name: name1\n namespace: namespace1\n labels:\n key1: value1\nspec:\n events:\n - start-test\n uri: http://localhost\n selector: app=backend\n payloadObjectField: text\n payloadTemplate: {{ .Id }}\n headers:\n Content-Type: appication/xml\n\n---\napiVersion: executor.testkube.io/v1\nkind: Webhook\nmetadata:\n name: name2\n namespace: namespace2\n labels:\n key2: value2\nspec:\n events:\n - end-test-success\n uri: http://localhost\n selector: app=backend\n payloadObjectField: text\n payloadTemplate: {{ .Id }}\n headers:\n Content-Type: appication/xml\n" + expected := "apiVersion: executor.testkube.io/v1\nkind: Webhook\nmetadata:\n name: name1\n namespace: namespace1\n labels:\n key1: value1\nspec:\n events:\n - start-test\n uri: http://localhost\n selector: app=backend\n payloadObjectField: text\n payloadTemplate: {{ .Id }}\n payloadTemplateReference: ref\n headers:\n Content-Type: appication/xml\n\n---\napiVersion: executor.testkube.io/v1\nkind: Webhook\nmetadata:\n name: name2\n namespace: namespace2\n labels:\n key2: value2\nspec:\n events:\n - end-test-success\n uri: http://localhost\n selector: app=backend\n payloadObjectField: text\n payloadTemplate: {{ .Id }}\n payloadTemplateReference: ref\n headers:\n Content-Type: appication/xml\n" webhooks := []testkube.Webhook{ { - Name: "name1", - Namespace: "namespace1", - Uri: "http://localhost", - Events: []testkube.EventType{*testkube.EventStartTest}, - Selector: "app=backend", - Labels: map[string]string{"key1": "value1"}, - PayloadObjectField: "text", - PayloadTemplate: "{{ .Id }}", - Headers: map[string]string{"Content-Type": "appication/xml"}, + Name: "name1", + Namespace: "namespace1", + Uri: "http://localhost", + Events: []testkube.EventType{*testkube.EventStartTest}, + Selector: "app=backend", + Labels: map[string]string{"key1": "value1"}, + PayloadObjectField: "text", + PayloadTemplate: "{{ .Id }}", + Headers: map[string]string{"Content-Type": "appication/xml"}, + PayloadTemplateReference: "ref", }, { - Name: "name2", - Namespace: "namespace2", - Uri: "http://localhost", - Events: []testkube.EventType{*testkube.EventEndTestSuccess}, - Selector: "app=backend", - Labels: map[string]string{"key2": "value2"}, - PayloadObjectField: "text", - PayloadTemplate: "{{ .Id }}", - Headers: map[string]string{"Content-Type": "appication/xml"}, + Name: "name2", + Namespace: "namespace2", + Uri: "http://localhost", + Events: []testkube.EventType{*testkube.EventEndTestSuccess}, + Selector: "app=backend", + Labels: map[string]string{"key2": "value2"}, + PayloadObjectField: "text", + PayloadTemplate: "{{ .Id }}", + Headers: map[string]string{"Content-Type": "appication/xml"}, + PayloadTemplateReference: "ref", }, } From 4924c6234701e5c2c54e3ee5ecaa27a0a0fc65b9 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 4 Sep 2023 18:06:07 +0300 Subject: [PATCH 28/59] fix: var name typo --- cmd/kubectl-testkube/commands/testsuites/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubectl-testkube/commands/testsuites/common.go b/cmd/kubectl-testkube/commands/testsuites/common.go index 59c58397709..ca7d0b31445 100644 --- a/cmd/kubectl-testkube/commands/testsuites/common.go +++ b/cmd/kubectl-testkube/commands/testsuites/common.go @@ -150,7 +150,7 @@ func NewTestSuiteUpsertOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 } jobTemplateReference := cmd.Flag("job-template-reference").Value.String() - cronJobTemplateReference := cmd.Flag("cronjob-template-refeence").Value.String() + cronJobTemplateReference := cmd.Flag("cronjob-template-reference").Value.String() scraperTemplateReference := cmd.Flag("scraper-template-reference").Value.String() pvcTemplateReference := cmd.Flag("pvc-template-reference").Value.String() From 046b52036ea969b8d77240b9d28e2a2331407a32 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 4 Sep 2023 21:10:02 +0300 Subject: [PATCH 29/59] fix: add warning for template type --- internal/app/api/v1/server.go | 2 +- pkg/event/kind/webhook/loader.go | 8 +++++++- pkg/event/kind/webhook/loader_test.go | 3 ++- pkg/executor/client/job.go | 8 ++++++-- pkg/executor/containerexecutor/tmpl.go | 8 ++++++++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go index dc31b409b5d..1bf324558d3 100644 --- a/internal/app/api/v1/server.go +++ b/internal/app/api/v1/server.go @@ -132,7 +132,7 @@ func NewTestkubeAPI( // will be reused in websockets handler s.WebsocketLoader = ws.NewWebsocketLoader() - s.Events.Loader.Register(webhook.NewWebhookLoader(webhookClient, templatesClient)) + s.Events.Loader.Register(webhook.NewWebhookLoader(s.Log, webhookClient, templatesClient)) s.Events.Loader.Register(s.WebsocketLoader) s.Events.Loader.Register(s.slackLoader) diff --git a/pkg/event/kind/webhook/loader.go b/pkg/event/kind/webhook/loader.go index c465629e737..3f8bd77a33e 100644 --- a/pkg/event/kind/webhook/loader.go +++ b/pkg/event/kind/webhook/loader.go @@ -3,6 +3,8 @@ package webhook import ( "fmt" + "go.uber.org/zap" + executorsv1 "github.com/kubeshop/testkube-operator/apis/executor/v1" templatesclientv1 "github.com/kubeshop/testkube-operator/client/templates/v1" "github.com/kubeshop/testkube/pkg/api/v1/testkube" @@ -17,14 +19,16 @@ type WebhooksLister interface { List(selector string) (*executorsv1.WebhookList, error) } -func NewWebhookLoader(webhooksClient WebhooksLister, templatesClient templatesclientv1.Interface) *WebhooksLoader { +func NewWebhookLoader(log *zap.SugaredLogger, webhooksClient WebhooksLister, templatesClient templatesclientv1.Interface) *WebhooksLoader { return &WebhooksLoader{ + log: log, WebhooksClient: webhooksClient, templatesClient: templatesClient, } } type WebhooksLoader struct { + log *zap.SugaredLogger WebhooksClient WebhooksLister templatesClient templatesclientv1.Interface } @@ -51,6 +55,8 @@ func (r WebhooksLoader) Load() (listeners common.Listeners, err error) { if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.WEBHOOK_TemplateType { payloadTemplate = template.Spec.Body + } else { + r.log.Warnw("not matching template type", "template", webhook.Spec.PayloadTemplateReference) } } diff --git a/pkg/event/kind/webhook/loader_test.go b/pkg/event/kind/webhook/loader_test.go index ce1bf775892..4703f2410f7 100644 --- a/pkg/event/kind/webhook/loader_test.go +++ b/pkg/event/kind/webhook/loader_test.go @@ -5,6 +5,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "go.uber.org/zap" executorsv1 "github.com/kubeshop/testkube-operator/apis/executor/v1" templatesclientv1 "github.com/kubeshop/testkube-operator/client/templates/v1" @@ -28,7 +29,7 @@ func TestWebhookLoader(t *testing.T) { defer mockCtrl.Finish() mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl) - webhooksLoader := NewWebhookLoader(&DummyLoader{}, mockTemplatesClient) + webhooksLoader := NewWebhookLoader(zap.NewNop().Sugar(), &DummyLoader{}, mockTemplatesClient) listeners, err := webhooksLoader.Load() assert.Equal(t, 1, len(listeners)) diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go index 80af28e07a4..e9e694f8984 100644 --- a/pkg/executor/client/job.go +++ b/pkg/executor/client/job.go @@ -306,7 +306,7 @@ func (c *JobExecutor) MonitorJobForTimeout(ctx context.Context, jobName string) // CreateJob creates new Kubernetes job based on execution and execute options func (c *JobExecutor) CreateJob(ctx context.Context, execution testkube.Execution, options ExecuteOptions) error { jobs := c.ClientSet.BatchV1().Jobs(c.Namespace) - jobOptions, err := NewJobOptions(c.templatesClient, c.images.Init, c.jobTemplate, c.serviceAccountName, c.registry, + jobOptions, err := NewJobOptions(c.Log, c.templatesClient, c.images.Init, c.jobTemplate, c.serviceAccountName, c.registry, c.clusterID, execution, options) if err != nil { return err @@ -778,7 +778,7 @@ func NewJobSpec(log *zap.SugaredLogger, options JobOptions) (*batchv1.Job, error return &job, nil } -func NewJobOptions(templatesClient templatesv1.Interface, initImage, jobTemplate, serviceAccountName, registry, clusterID string, +func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface, initImage, jobTemplate, serviceAccountName, registry, clusterID string, execution testkube.Execution, options ExecuteOptions) (jobOptions JobOptions, err error) { jsn, err := json.Marshal(execution) if err != nil { @@ -803,6 +803,8 @@ func NewJobOptions(templatesClient templatesv1.Interface, initImage, jobTemplate if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { jobOptions.JobTemplate = template.Spec.Body + } else { + log.Warnw("Not matched template type", "template", options.ExecutorSpec.JobTemplateReference) } } @@ -814,6 +816,8 @@ func NewJobOptions(templatesClient templatesv1.Interface, initImage, jobTemplate if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { jobOptions.JobTemplate = template.Spec.Body + } else { + log.Warnw("Not matched template type", "template", options.Request.JobTemplateReference) } } diff --git a/pkg/executor/containerexecutor/tmpl.go b/pkg/executor/containerexecutor/tmpl.go index dffeb9ff940..b94f513ba93 100644 --- a/pkg/executor/containerexecutor/tmpl.go +++ b/pkg/executor/containerexecutor/tmpl.go @@ -314,6 +314,8 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { jobOptions.JobTemplate = template.Spec.Body + } else { + log.Warnw("Not matched template type", "template", options.ExecutorSpec.JobTemplateReference) } } @@ -325,6 +327,8 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { jobOptions.JobTemplate = template.Spec.Body + } else { + log.Warnw("Not matched template type", "template", options.Request.JobTemplateReference) } } @@ -337,6 +341,8 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.SCRAPER_TemplateType { jobOptions.ScraperTemplate = template.Spec.Body + } else { + log.Warnw("Not matched template type", "template", options.Request.ScraperTemplateReference) } } @@ -349,6 +355,8 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.PVC_TemplateType { jobOptions.PvcTemplate = template.Spec.Body + } else { + log.Warnw("Not matched template type", "template", options.Request.PvcTemplateReference) } } From 5121b13d5209b2fb490ff56aaec441e3e19d5d9e Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 16:59:52 +0300 Subject: [PATCH 30/59] Update docs/docs/cli/testkube_create_template.md Co-authored-by: Julianne Fermi --- docs/docs/cli/testkube_create_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cli/testkube_create_template.md b/docs/docs/cli/testkube_create_template.md index 978d79cb367..3f2a87de73a 100644 --- a/docs/docs/cli/testkube_create_template.md +++ b/docs/docs/cli/testkube_create_template.md @@ -4,7 +4,7 @@ Create new Template ### Synopsis -Create new Template Custom Resource +Create a new Template Custom Resource. ``` testkube create template [flags] From 4cf767ed4b827df4944d61e2e1ea66b99f1d7d43 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 17:00:01 +0300 Subject: [PATCH 31/59] Update docs/docs/cli/testkube_create_template.md Co-authored-by: Julianne Fermi --- docs/docs/cli/testkube_create_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cli/testkube_create_template.md b/docs/docs/cli/testkube_create_template.md index 3f2a87de73a..3aa13563645 100644 --- a/docs/docs/cli/testkube_create_template.md +++ b/docs/docs/cli/testkube_create_template.md @@ -1,6 +1,6 @@ ## testkube create template -Create new Template +Create a new Template. ### Synopsis From f6ef9b41cf2d317fcd8e6aee4c2dc3fb68522800 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 17:00:12 +0300 Subject: [PATCH 32/59] Update docs/docs/cli/testkube_delete_template.md Co-authored-by: Julianne Fermi --- docs/docs/cli/testkube_delete_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cli/testkube_delete_template.md b/docs/docs/cli/testkube_delete_template.md index 64839c17255..5fe05ab90ee 100644 --- a/docs/docs/cli/testkube_delete_template.md +++ b/docs/docs/cli/testkube_delete_template.md @@ -1,6 +1,6 @@ ## testkube delete template -Delete template +Delete a template. ### Synopsis From 9f560c10e7062484fd55c4c23db3d3e0425127b5 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 17:00:25 +0300 Subject: [PATCH 33/59] Update docs/docs/cli/testkube_get_template.md Co-authored-by: Julianne Fermi --- docs/docs/cli/testkube_get_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cli/testkube_get_template.md b/docs/docs/cli/testkube_get_template.md index 334ebdc316c..d38f89d60c3 100644 --- a/docs/docs/cli/testkube_get_template.md +++ b/docs/docs/cli/testkube_get_template.md @@ -1,6 +1,6 @@ ## testkube get template -Get template details +Get template details. ### Synopsis From 88120b27165030c1630f18e17da158c3328057d0 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 17:01:05 +0300 Subject: [PATCH 34/59] Update docs/docs/cli/testkube_delete_template.md Co-authored-by: Julianne Fermi --- docs/docs/cli/testkube_delete_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cli/testkube_delete_template.md b/docs/docs/cli/testkube_delete_template.md index 5fe05ab90ee..265a9ff458f 100644 --- a/docs/docs/cli/testkube_delete_template.md +++ b/docs/docs/cli/testkube_delete_template.md @@ -4,7 +4,7 @@ Delete a template. ### Synopsis -Delete template, pass template name which should be deleted +Delete a template and pass the template name to be deleted. ``` testkube delete template [flags] From 3fb6fda651ba0de244c20a3935834effe677c526 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 17:01:14 +0300 Subject: [PATCH 35/59] Update docs/docs/cli/testkube_get_template.md Co-authored-by: Julianne Fermi --- docs/docs/cli/testkube_get_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cli/testkube_get_template.md b/docs/docs/cli/testkube_get_template.md index d38f89d60c3..11b7e72a7c8 100644 --- a/docs/docs/cli/testkube_get_template.md +++ b/docs/docs/cli/testkube_get_template.md @@ -4,7 +4,7 @@ Get template details. ### Synopsis -Get template, you can change output format, to get single details pass name as first arg +Get template allows you to change the output format. To get single details, pass the template name as the first argument. ``` testkube get template [flags] From 19e322abfffc360377da96c6811c759c7d3bad85 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 17:01:59 +0300 Subject: [PATCH 36/59] Update docs/docs/cli/testkube_update_template.md Co-authored-by: Julianne Fermi --- docs/docs/cli/testkube_update_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cli/testkube_update_template.md b/docs/docs/cli/testkube_update_template.md index db7e0bdff29..09a0ec1d06a 100644 --- a/docs/docs/cli/testkube_update_template.md +++ b/docs/docs/cli/testkube_update_template.md @@ -4,7 +4,7 @@ Update Template ### Synopsis -Update Template Custom Resource +Update Template Custom Resource. ``` testkube update template [flags] From ae39373b741821f2c0d8a572e4f271b5450cd7e2 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 18:20:45 +0300 Subject: [PATCH 37/59] fix: templates doc --- docs/docs/articles/templates.mdx | 110 ++++++++++++++++++++++ docs/docs/cli/testkube_create_template.md | 2 +- docs/docs/cli/testkube_update_template.md | 2 +- docs/redirects.js | 4 + docs/sidebars.js | 1 + pkg/executor/containerexecutor/tmpl.go | 4 +- 6 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 docs/docs/articles/templates.mdx diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx new file mode 100644 index 00000000000..b79f2f3009f --- /dev/null +++ b/docs/docs/articles/templates.mdx @@ -0,0 +1,110 @@ +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Templates + +Templates allow you to store templates for other resources used in Testkube. We support such a list of templates job | container | cronjob | scraper | pvc | webhook. To define templates in Testkube, you'll need to provide a template body (in golang template format) and a type of the template. + +## Creating a Template +The template can be created using the API, CLI, or a Custom Resource. + + + +If you prefer to use the API for creating a template, you can check API spec for templates in below doc. + +![OpenAPI spec](../openapi.md) + + + + +Templates can be created with Testkube CLI using the `create template` command. + +```sh +kubectl testkube create template --name job-template --template-type job --body job-template.yaml +``` + +`--name` - Your template name (in this case `job-template`). +`--template-type` - Your template type (in this case `job` for prebuilt executors). +`--body` - A path to the file with job template content + + + + + +```yaml title="template.yaml" +apiVersion: tests.testkube.io/v1 +kind: Webhook +metadata: + name: example-webhook + namespace: testkube +spec: + type: job + body: +``` + +Where should be replaced with kubernetes job definition in golang template format. + +And then apply with: + +```sh +kubectl apply -f template.yaml +``` + + + + + +### Using template +You will need to refer a template in corresponding reference field of the resource. + + + + +Check templateReference fields in api spec, for example, Test -> executionRequest -> jobTemplateReference field +![OpenAPI spec](../openapi.md) + + + + + +Templates can be created with Testkube CLI using the `create template` command. + +```sh +kubectl testkube create test --name template-test --type k6/script --job-template-reference=job-template --test-content-type git --git-uri "https://github.com/kubeshop/testkube.git" --git-branch main --git-path test/k6/executor-tests/k6-smoke-test.js +``` + +`--name` - Your test name (in this case `template-test`). +`--type` - Your test type (in this case `k6/script`). +`--job-template-reference` - Job template reference (in this case `job-template`). +`--test-content-type` - Test content type (in this case `git`). +`--git-uri` - Git uri to repository (in this case `https://github.com/kubeshop/testkube.git`). +`--git-branch` - Git branch to use (in this case `main`). +`--git-path` - Git path to the test (in this case `test/k6/executor-tests/k6-smoke-test.js`). + + + + + +```yaml title="test.yaml" +apiVersion: tests.testkube.io/v3 +kind: Test +metadata: + name: template-test + namespace: testkube +spec: + type: k6/script + content: + type: git + repository: + type: git + uri: https://github.com/kubeshop/testkube.git + branch: main + path: test/k6/executor-tests/k6-smoke-test.js + authType: basic + executionRequest: + jobTemplateReference: job-template +``` + + + + diff --git a/docs/docs/cli/testkube_create_template.md b/docs/docs/cli/testkube_create_template.md index 3aa13563645..4cc7781a2ed 100644 --- a/docs/docs/cli/testkube_create_template.md +++ b/docs/docs/cli/testkube_create_template.md @@ -17,7 +17,7 @@ testkube create template [flags] -h, --help help for template -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique template name - mandatory - --template-type string template type one of job|container|cronnjob|scraper|pvc|webhook + --template-type string template type one of job|container|cronjob|scraper|pvc|webhook ``` ### Options inherited from parent commands diff --git a/docs/docs/cli/testkube_update_template.md b/docs/docs/cli/testkube_update_template.md index 09a0ec1d06a..aedaa7e8bdd 100644 --- a/docs/docs/cli/testkube_update_template.md +++ b/docs/docs/cli/testkube_update_template.md @@ -17,7 +17,7 @@ testkube update template [flags] -h, --help help for template -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique template name - mandatory - --template-type string template type one of job|container|cronnjob|scraper|pvc|webhook + --template-type string template type one of job|container|cronjob|scraper|pvc|webhook ``` ### Options inherited from parent commands diff --git a/docs/redirects.js b/docs/redirects.js index 274f137aafb..80daf150ce1 100644 --- a/docs/redirects.js +++ b/docs/redirects.js @@ -277,6 +277,10 @@ const redirects = [ from: "/guides/uninstall", to: "/articles/uninstall", }, + { + from: "/guides/templates", + to: "/articles/templates", + }, { from: ["/testkube-cloud/intro", "/testkube-cloud"], to: "/testkube-cloud/articles/intro", diff --git a/docs/sidebars.js b/docs/sidebars.js index 8461a87ba3c..cc575ed6e89 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -80,6 +80,7 @@ const sidebars = { "articles/webhooks", "articles/test-sources", "articles/test-executions", + "articles/templates", ], }, { diff --git a/pkg/executor/containerexecutor/tmpl.go b/pkg/executor/containerexecutor/tmpl.go index b94f513ba93..2e945379d87 100644 --- a/pkg/executor/containerexecutor/tmpl.go +++ b/pkg/executor/containerexecutor/tmpl.go @@ -312,7 +312,7 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface return jobOptions, err } - if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.CONTAINER_TemplateType { jobOptions.JobTemplate = template.Spec.Body } else { log.Warnw("Not matched template type", "template", options.ExecutorSpec.JobTemplateReference) @@ -325,7 +325,7 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface return nil, err } - if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.JOB_TemplateType { + if template.Spec.Type_ != nil && testkube.TemplateType(*template.Spec.Type_) == testkube.CONTAINER_TemplateType { jobOptions.JobTemplate = template.Spec.Body } else { log.Warnw("Not matched template type", "template", options.Request.JobTemplateReference) From fd7e098237a3b854532641d3e6098720da3e6ea5 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 20:01:34 +0300 Subject: [PATCH 38/59] fix: dep update --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4b788ba22de..81c6691928e 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.20230829170322-3e88740c6e7d + github.com/kubeshop/testkube-operator v1.10.8-0.20230905165843-5c2e3838fed8 github.com/minio/minio-go/v7 v7.0.47 github.com/montanaflynn/stats v0.6.6 github.com/moogar0880/problems v0.1.1 diff --git a/go.sum b/go.sum index c2e2001a0cb..c18a72d21d2 100644 --- a/go.sum +++ b/go.sum @@ -393,8 +393,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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.20230829170322-3e88740c6e7d h1:527VCINS75vdhlpxGsaL/c5TrEMftNtWNs0xGKFEfhA= -github.com/kubeshop/testkube-operator v1.10.8-0.20230829170322-3e88740c6e7d/go.mod h1:UmigDOKMVJa6Y/imKafWmhcvfLisOzr5X04kuOsH/B0= +github.com/kubeshop/testkube-operator v1.10.8-0.20230905165843-5c2e3838fed8 h1:GApLO59jzvfuwRxBoTdSHpIdt+OayuhjWuqjK25klJo= +github.com/kubeshop/testkube-operator v1.10.8-0.20230905165843-5c2e3838fed8/go.mod h1:UmigDOKMVJa6Y/imKafWmhcvfLisOzr5X04kuOsH/B0= 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= From 6d1304cabc2a37ef1ded8fa23203a002000b7943 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:46:39 +0300 Subject: [PATCH 39/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index b79f2f3009f..06c6a616bcc 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -3,7 +3,7 @@ import TabItem from "@theme/TabItem"; # Templates -Templates allow you to store templates for other resources used in Testkube. We support such a list of templates job | container | cronjob | scraper | pvc | webhook. To define templates in Testkube, you'll need to provide a template body (in golang template format) and a type of the template. +Templates allow you to store templates for other resources used in Testkube. We support a list of templates job | container | cronjob | scraper | pvc | webhook. To define templates in Testkube, you'll need to provide a template body (in Golang template format) and a type of the template. ## Creating a Template The template can be created using the API, CLI, or a Custom Resource. From 312e26611816ce94a3c5b3c4f7b31d55d766bdb3 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:46:54 +0300 Subject: [PATCH 40/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index 06c6a616bcc..12f189fc8c3 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -10,7 +10,7 @@ The template can be created using the API, CLI, or a Custom Resource. -If you prefer to use the API for creating a template, you can check API spec for templates in below doc. +If you prefer to use the API for creating a template, please visit the API spec for templates in the doc below. ![OpenAPI spec](../openapi.md) From c6c08de8bc1a35d55e0757b6b5c71f8d898d62da Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:47:05 +0300 Subject: [PATCH 41/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index 12f189fc8c3..dc41b74e45a 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -17,7 +17,7 @@ If you prefer to use the API for creating a template, please visit the API spec -Templates can be created with Testkube CLI using the `create template` command. +Templates can be created with the Testkube CLI using the `create template` command. ```sh kubectl testkube create template --name job-template --template-type job --body job-template.yaml From a561abc49f61151b7214b016853d827008dfb566 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:49:14 +0300 Subject: [PATCH 42/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index dc41b74e45a..6f8951ac44f 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -42,7 +42,7 @@ spec: body: ``` -Where should be replaced with kubernetes job definition in golang template format. +Where should be replaced with the Kubernetes job definition in Golang template format. And then apply with: From e64b6ec1b50dda67ca04f1c52085e15e3b22b5ba Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:49:30 +0300 Subject: [PATCH 43/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index 6f8951ac44f..91a78826dac 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -54,7 +54,7 @@ kubectl apply -f template.yaml -### Using template +### Using Templates You will need to refer a template in corresponding reference field of the resource. From 7853dd55e6966ce84257b13d34c0478972b5161c Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:49:41 +0300 Subject: [PATCH 44/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index 91a78826dac..8aa7251b46c 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -55,7 +55,7 @@ kubectl apply -f template.yaml ### Using Templates -You will need to refer a template in corresponding reference field of the resource. +You will need to refer to a template in the corresponding reference field of the resource. From d3ab32926fdfd5a33a02864d03e358acbfcd16aa Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:49:53 +0300 Subject: [PATCH 45/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index 8aa7251b46c..ee6bd7e89db 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -60,7 +60,7 @@ You will need to refer to a template in the corresponding reference field of the -Check templateReference fields in api spec, for example, Test -> executionRequest -> jobTemplateReference field +Check templateReference fields in API spec. For example, Test -> executionRequest -> jobTemplateReference field. ![OpenAPI spec](../openapi.md) From 7fe4d0f04670a859c6db28ab4fa410cd5e1cc590 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 5 Sep 2023 21:50:02 +0300 Subject: [PATCH 46/59] Update docs/docs/articles/templates.mdx Co-authored-by: Julianne Fermi --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index ee6bd7e89db..d0d5ad09a28 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -67,7 +67,7 @@ Check templateReference fields in API spec. For example, Test -> executionReques -Templates can be created with Testkube CLI using the `create template` command. +Templates can be created with the Testkube CLI using the `create template` command. ```sh kubectl testkube create test --name template-test --type k6/script --job-template-reference=job-template --test-content-type git --git-uri "https://github.com/kubeshop/testkube.git" --git-branch main --git-path test/k6/executor-tests/k6-smoke-test.js From d9a99912ca839d63815e7a8343371484fd2b2d6b Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Wed, 6 Sep 2023 14:36:06 +0300 Subject: [PATCH 47/59] feat: list secrets and keys --- api/v1/testkube.yaml | 43 +++++++++++++++++++++++++++++ internal/app/api/v1/secret.go | 34 +++++++++++++++++++++++ internal/app/api/v1/server.go | 3 ++ pkg/api/v1/testkube/model_secret.go | 18 ++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 internal/app/api/v1/secret.go create mode 100644 pkg/api/v1/testkube/model_secret.go diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index ab48f196336..309a70324a1 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -3188,6 +3188,32 @@ paths: items: $ref: "#/components/schemas/Problem" + /secrets: + get: + tags: + - secrets + - api + summary: "List secrets" + description: "List secrets available in cluster" + operationId: listSecrets + responses: + 200: + description: "successful operation" + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Secret" + 502: + description: "problem with communicating with kubernetes cluster or git server" + content: + application/problem+json: + schema: + type: array + items: + $ref: "#/components/schemas/Problem" + components: schemas: ExecutionsMetrics: @@ -5690,6 +5716,23 @@ components: allOf: - $ref: "#/components/schemas/Template" + Secret: + description: Secret with keys + type: object + required: + - name + properties: + name: + type: string + description: secret name + example: "git-secret" + keys: + type: array + description: secret keys + items: + type: string + example: ["key1", "key2", "key3"] + # # Errors # diff --git a/internal/app/api/v1/secret.go b/internal/app/api/v1/secret.go new file mode 100644 index 00000000000..25ee95a11df --- /dev/null +++ b/internal/app/api/v1/secret.go @@ -0,0 +1,34 @@ +package v1 + +import ( + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +// ListSecretsHandler list secrets and keys +func (s TestkubeAPI) ListSecretsHandler() fiber.Handler { + return func(c *fiber.Ctx) error { + errPrefix := "failed to list secrets" + + list, err := s.SecretClient.List() + if err != nil { + return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list secrets: %s", errPrefix, err)) + } + + results := make([]testkube.Secret, 0) + for name, values := range list { + keys := make([]string, 0) + for value := range values { + keys = append(keys, value) + } + + results = append(results, testkube.Secret{Name: name, Keys: keys}) + } + + return c.JSON(results) + } +} diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go index 1bf324558d3..74f5a9654ab 100644 --- a/internal/app/api/v1/server.go +++ b/internal/app/api/v1/server.go @@ -381,6 +381,9 @@ func (s *TestkubeAPI) InitRoutes() { repositories := s.Routes.Group("/repositories") repositories.Post("/", s.ValidateRepositoryHandler()) + secrets := s.Routes.Group("/secrets") + secrets.Get("/", s.ListSecretsHandler()) + // mount everything on results // TODO it should be named /api/ + dashboard refactor s.Mux.Mount("/results", s.Mux) diff --git a/pkg/api/v1/testkube/model_secret.go b/pkg/api/v1/testkube/model_secret.go new file mode 100644 index 00000000000..3c735eb96e6 --- /dev/null +++ b/pkg/api/v1/testkube/model_secret.go @@ -0,0 +1,18 @@ +/* + * 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 + +// Secret with keys +type Secret struct { + // secret name + Name string `json:"name"` + // secret keys + Keys []string `json:"keys,omitempty"` +} From 6f28553e5cc8256315d5a86aaf3aaf12ed63a3ad Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Wed, 6 Sep 2023 15:39:16 +0300 Subject: [PATCH 48/59] fix: list all secrets --- internal/app/api/v1/secret.go | 2 +- pkg/secret/client.go | 11 ++++++++--- pkg/secret/mock_client.go | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/internal/app/api/v1/secret.go b/internal/app/api/v1/secret.go index 25ee95a11df..9955ea572e4 100644 --- a/internal/app/api/v1/secret.go +++ b/internal/app/api/v1/secret.go @@ -14,7 +14,7 @@ func (s TestkubeAPI) ListSecretsHandler() fiber.Handler { return func(c *fiber.Ctx) error { errPrefix := "failed to list secrets" - list, err := s.SecretClient.List() + list, err := s.SecretClient.List(true) if err != nil { return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list secrets: %s", errPrefix, err)) } diff --git a/pkg/secret/client.go b/pkg/secret/client.go index fc863426ead..5b08339a131 100644 --- a/pkg/secret/client.go +++ b/pkg/secret/client.go @@ -20,7 +20,7 @@ const testkubeTestSecretLabel = "tests-secrets" type Interface interface { Get(id string) (map[string]string, error) GetObject(id string) (*v1.Secret, error) - List() (map[string]map[string]string, error) + List(all bool) (map[string]map[string]string, error) Create(id string, labels, stringData map[string]string) error Apply(id string, labels, stringData map[string]string) error Update(id string, labels, stringData map[string]string) error @@ -81,12 +81,17 @@ func (c *Client) GetObject(id string) (*v1.Secret, error) { } // List is a method to retrieve all existing secrets -func (c *Client) List() (map[string]map[string]string, error) { +func (c *Client) List(all bool) (map[string]map[string]string, error) { secretsClient := c.ClientSet.CoreV1().Secrets(c.Namespace) ctx := context.Background() + selector := "" + if !all { + selector = fmt.Sprintf("testkube=%s", testkubeTestSecretLabel) + } + secretList, err := secretsClient.List(ctx, metav1.ListOptions{ - LabelSelector: fmt.Sprintf("testkube=%s", testkubeTestSecretLabel)}) + LabelSelector: selector}) if err != nil { return nil, err } diff --git a/pkg/secret/mock_client.go b/pkg/secret/mock_client.go index f3e32086f73..41559e44496 100644 --- a/pkg/secret/mock_client.go +++ b/pkg/secret/mock_client.go @@ -121,18 +121,18 @@ func (mr *MockInterfaceMockRecorder) GetObject(arg0 interface{}) *gomock.Call { } // List mocks base method. -func (m *MockInterface) List() (map[string]map[string]string, error) { +func (m *MockInterface) List(arg0 bool) (map[string]map[string]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List") + ret := m.ctrl.Call(m, "List", arg0) ret0, _ := ret[0].(map[string]map[string]string) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. -func (mr *MockInterfaceMockRecorder) List() *gomock.Call { +func (mr *MockInterfaceMockRecorder) List(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List), arg0) } // Update mocks base method. From 7d0ee0b1f5b807adf76063688b17b0c3e6e7137a Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 7 Sep 2023 14:37:31 +0300 Subject: [PATCH 49/59] fix: doc syntax --- docs/docs/articles/templates.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index d0d5ad09a28..96ee5899f41 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -12,7 +12,7 @@ The template can be created using the API, CLI, or a Custom Resource. If you prefer to use the API for creating a template, please visit the API spec for templates in the doc below. -![OpenAPI spec](../openapi.md) +[OpenAPI spec](../openapi.md) @@ -61,7 +61,7 @@ You will need to refer to a template in the corresponding reference field of the Check templateReference fields in API spec. For example, Test -> executionRequest -> jobTemplateReference field. -![OpenAPI spec](../openapi.md) +[OpenAPI spec](../openapi.md) From 1c4df5942051766c01b730873fbcf5746be8a746 Mon Sep 17 00:00:00 2001 From: Dejan Zele Pejchev Date: Thu, 7 Sep 2023 14:06:03 +0200 Subject: [PATCH 50/59] feat: add jmeter docker image build which will be used by the jmeter executors (#4308) * add jmeter docker image build which will be used by the jmeter executors * update jmeter with latest requirements * update readme for jmeter docker --- contrib/docker/jmeter/Makefile | 19 +++++++++++ contrib/docker/jmeter/README.md | 25 ++++++++++++++ .../docker/jmeter/jmeter5.5.ubi8.8.Dockerfile | 33 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 contrib/docker/jmeter/Makefile create mode 100644 contrib/docker/jmeter/README.md create mode 100644 contrib/docker/jmeter/jmeter5.5.ubi8.8.Dockerfile diff --git a/contrib/docker/jmeter/Makefile b/contrib/docker/jmeter/Makefile new file mode 100644 index 00000000000..db7d0c99833 --- /dev/null +++ b/contrib/docker/jmeter/Makefile @@ -0,0 +1,19 @@ +# Variables +DOCKER_REPOSITORY = kubeshop +DOCKER_IMAGE_NAME = jmeter +DOCKER_TAG = 5.5 + +# Build the Docker image +.PHONY: build +build: + @echo "Building Docker image..." + @docker buildx build --platform linux/amd64,linux/arm64 -f jmeter5.5.ubi8.8.Dockerfile -t $(DOCKER_REPOSITORY)/$(DOCKER_IMAGE_NAME):$(DOCKER_TAG) . + +.PHONY: push +push: build + @echo "Pushing Docker image..." + @docker buildx build --push --platform linux/amd64,linux/arm64 -f jmeter5.5.ubi8.8.Dockerfile -t $(DOCKER_REPOSITORY)/$(DOCKER_IMAGE_NAME):$(DOCKER_TAG) . + +test: build + @echo "Testing Docker image..." + @docker run --rm -it $(DOCKER_REPOSITORY)/$(DOCKER_IMAGE_NAME):$(DOCKER_TAG) --version \ No newline at end of file diff --git a/contrib/docker/jmeter/README.md b/contrib/docker/jmeter/README.md new file mode 100644 index 00000000000..c1c386061ef --- /dev/null +++ b/contrib/docker/jmeter/README.md @@ -0,0 +1,25 @@ +# JMeter + +This repository contains Dockerfiles for JMeter builds which are used by the Testkube JMeter Executor. + +Currently supported builds: +* JMeter 5.5 with OpenJDK 17 built on RHEL UBI 8.8 (minimal) + +## Development + +Use the following `make` targets to build and push the images: + +To build the JMeter Docker image use: +```bash +make build +``` + +To do a quick test run of the JMeter Docker image use: +```bash +make test +``` + +To push the JMeter Docker image to the registry use: +```bash +make push +``` \ No newline at end of file diff --git a/contrib/docker/jmeter/jmeter5.5.ubi8.8.Dockerfile b/contrib/docker/jmeter/jmeter5.5.ubi8.8.Dockerfile new file mode 100644 index 00000000000..a84d1ebf2ac --- /dev/null +++ b/contrib/docker/jmeter/jmeter5.5.ubi8.8.Dockerfile @@ -0,0 +1,33 @@ +# Use Red Hat's Universal Base Image 8 +FROM redhat/ubi8-minimal:8.8 + +ENV JAVA_VERSION=17 +ENV JMETER_VERSION=5.5 + +# Labels and authorship +LABEL org.opencontainers.image.title="JMeter" \ + org.opencontainers.image.description="Red Hat UBI with Java $JAVA_VERSION and JMeter $JMETER_VERSION" \ + org.opencontainers.image.version="$JMETER_VERSION" \ + org.opencontainers.image.maintainer="support@testkube.io" \ + org.opencontainers.image.vendor="testkube" \ + org.opencontainers.image.url="https://cloud.testkube.io" \ + org.opencontainers.image.source="https://github.com/kubeshop/testkube/tree/develop/contrib/docker/jmeter" + +# Update the system and install required libraries +RUN microdnf update -y && \ + microdnf install curl unzip java-$JAVA_VERSION-openjdk tar && \ + microdnf clean all + +# Install JMeter +RUN curl -L https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz | tar xz -C /opt/ && \ + mv /opt/apache-jmeter-$JMETER_VERSION /opt/jmeter + +# Set JMeter Home and add JMeter bin directory to the PATH +ENV JMETER_HOME /opt/jmeter +ENV PATH $JMETER_HOME/bin:$PATH + +# Expose the required JMeter ports +EXPOSE 60000 + +# Command to run JMeter tests +ENTRYPOINT [ "jmeter" ] From a36e3669000673bad6d01dae988737992d85a4eb Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Wed, 6 Sep 2023 20:24:03 +0300 Subject: [PATCH 51/59] feat: suuport golang temolate for webhook uri --- .../commands/templates/common.go | 4 ++-- .../commands/templates/create.go | 6 +++--- .../commands/templates/delete.go | 4 ++-- cmd/kubectl-testkube/commands/templates/get.go | 4 ++-- .../commands/templates/update.go | 4 ++-- .../commands/testsources/create.go | 2 +- .../commands/testsources/update.go | 2 +- cmd/kubectl-testkube/commands/webhooks/create.go | 2 +- cmd/kubectl-testkube/commands/webhooks/update.go | 2 +- docs/docs/cli/testkube_create.md | 2 +- docs/docs/cli/testkube_create_testsource.md | 2 +- docs/docs/cli/testkube_create_webhook.md | 2 +- docs/docs/cli/testkube_delete.md | 2 +- docs/docs/cli/testkube_get.md | 2 +- docs/docs/cli/testkube_update_testsource.md | 2 +- docs/docs/cli/testkube_update_webhook.md | 2 +- pkg/event/kind/webhook/listener.go | 16 +++++++++++++++- 17 files changed, 37 insertions(+), 23 deletions(-) diff --git a/cmd/kubectl-testkube/commands/templates/common.go b/cmd/kubectl-testkube/commands/templates/common.go index 12209d3567b..e2ca1f283c2 100644 --- a/cmd/kubectl-testkube/commands/templates/common.go +++ b/cmd/kubectl-testkube/commands/templates/common.go @@ -24,7 +24,7 @@ func NewCreateTemplateOptionsFromFlags(cmd *cobra.Command) (options apiv1.Create if templateType != testkube.JOB_TemplateType && templateType != testkube.CRONJOB_TemplateType && templateType != testkube.SCRAPER_TemplateType && templateType != testkube.PVC_TemplateType && templateType != testkube.WEBHOOK_TemplateType { - ui.Failf("invalid template type: %s. use one of job|container|cronnjob|scraper|pvc|webhook", templateType) + ui.Failf("invalid template type: %s. use one of job|container|cronjob|scraper|pvc|webhook", templateType) } body := cmd.Flag("body").Value.String() @@ -75,7 +75,7 @@ func NewUpdateTemplateOptionsFromFlags(cmd *cobra.Command) (options apiv1.Update if templateType != testkube.JOB_TemplateType && templateType != testkube.CRONJOB_TemplateType && templateType != testkube.SCRAPER_TemplateType && templateType != testkube.PVC_TemplateType && templateType != testkube.WEBHOOK_TemplateType { - ui.Failf("invalid template type: %s. use one of job|container|cronnjob|scraper|pvc|webhook", templateType) + ui.Failf("invalid template type: %s. use one of job|container|cronjob|scraper|pvc|webhook", templateType) } options.Type_ = &templateType } diff --git a/cmd/kubectl-testkube/commands/templates/create.go b/cmd/kubectl-testkube/commands/templates/create.go index 8a8100c36fa..7ce82b90dae 100644 --- a/cmd/kubectl-testkube/commands/templates/create.go +++ b/cmd/kubectl-testkube/commands/templates/create.go @@ -23,8 +23,8 @@ func NewCreateTemplateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "template", Aliases: []string{"tp"}, - Short: "Create new Template", - Long: `Create new Template Custom Resource`, + Short: "Create a new Template.", + Long: `Create a new Template Custom Resource.`, Run: func(cmd *cobra.Command, args []string) { crdOnly, err := strconv.ParseBool(cmd.Flag("crd-only").Value.String()) ui.ExitOnError("parsing flag value", err) @@ -67,7 +67,7 @@ func NewCreateTemplateCmd() *cobra.Command { } cmd.Flags().StringVarP(&name, "name", "n", "", "unique template name - mandatory") - cmd.Flags().StringVarP(&templateType, "template-type", "", "", "template type one of job|container|cronnjob|scraper|pvc|webhook") + cmd.Flags().StringVarP(&templateType, "template-type", "", "", "template type one of job|container|cronjob|scraper|pvc|webhook") cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&body, "body", "", "", "a path to template file to use as template body") diff --git a/cmd/kubectl-testkube/commands/templates/delete.go b/cmd/kubectl-testkube/commands/templates/delete.go index a2312ebe86d..f6caef2981c 100644 --- a/cmd/kubectl-testkube/commands/templates/delete.go +++ b/cmd/kubectl-testkube/commands/templates/delete.go @@ -17,8 +17,8 @@ func NewDeleteTemplateCmd() *cobra.Command { Use: "template ", Aliases: []string{"tp"}, - Short: "Delete template", - Long: `Delete template, pass template name which should be deleted`, + Short: "Delete a template.", + Long: `Delete a template and pass the template name to be deleted.`, Run: func(cmd *cobra.Command, args []string) { client, _, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) diff --git a/cmd/kubectl-testkube/commands/templates/get.go b/cmd/kubectl-testkube/commands/templates/get.go index 881663bff66..cd5e3c60155 100644 --- a/cmd/kubectl-testkube/commands/templates/get.go +++ b/cmd/kubectl-testkube/commands/templates/get.go @@ -21,8 +21,8 @@ func NewGetTemplateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "template ", Aliases: []string{"templates", "tp"}, - Short: "Get template details", - Long: `Get template, you can change output format, to get single details pass name as first arg`, + Short: "Get template details.", + Long: `Get template allows you to change the output format. To get single details, pass the template name as the first argument.`, Run: func(cmd *cobra.Command, args []string) { client, _, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) diff --git a/cmd/kubectl-testkube/commands/templates/update.go b/cmd/kubectl-testkube/commands/templates/update.go index dd7605176c8..571f89e8749 100644 --- a/cmd/kubectl-testkube/commands/templates/update.go +++ b/cmd/kubectl-testkube/commands/templates/update.go @@ -19,7 +19,7 @@ func UpdateTemplateCmd() *cobra.Command { Use: "template", Aliases: []string{"templates", "tp"}, Short: "Update Template", - Long: `Update Template Custom Resource`, + Long: `Update Template Custom Resource.`, Run: func(cmd *cobra.Command, args []string) { if name == "" { ui.Failf("pass valid name (in '--name' flag)") @@ -44,7 +44,7 @@ func UpdateTemplateCmd() *cobra.Command { } cmd.Flags().StringVarP(&name, "name", "n", "", "unique template name - mandatory") - cmd.Flags().StringVarP(&templateType, "template-type", "", "", "template type one of job|container|cronnjob|scraper|pvc|webhook") + cmd.Flags().StringVarP(&templateType, "template-type", "", "", "template type one of job|container|cronjob|scraper|pvc|webhook") cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&body, "body", "", "", "a path to template file to use as template body") diff --git a/cmd/kubectl-testkube/commands/testsources/create.go b/cmd/kubectl-testkube/commands/testsources/create.go index c451fddd83a..a3a6e3e7e65 100644 --- a/cmd/kubectl-testkube/commands/testsources/create.go +++ b/cmd/kubectl-testkube/commands/testsources/create.go @@ -84,7 +84,7 @@ func NewCreateTestSourceCmd() *cobra.Command { cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&sourceType, "source-type", "", "", "source type of test one of string|file-uri|git") cmd.Flags().StringVarP(&file, "file", "f", "", "source file - will be read from stdin if not specified") - cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called when given event occurs") + cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called to get test content") cmd.Flags().StringVarP(&gitUri, "git-uri", "", "", "Git repository uri") cmd.Flags().StringVarP(&gitBranch, "git-branch", "", "", "if uri is git repository we can set additional branch parameter") cmd.Flags().StringVarP(&gitCommit, "git-commit", "", "", "if uri is git repository we can use commit id (sha) parameter") diff --git a/cmd/kubectl-testkube/commands/testsources/update.go b/cmd/kubectl-testkube/commands/testsources/update.go index 4d460ad067a..a5df73be47e 100644 --- a/cmd/kubectl-testkube/commands/testsources/update.go +++ b/cmd/kubectl-testkube/commands/testsources/update.go @@ -58,7 +58,7 @@ func UpdateTestSourceCmd() *cobra.Command { cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&sourceType, "source-type", "", "", "source type of test one of string|file-uri|git") cmd.Flags().StringVarP(&file, "file", "f", "", "source file - will be read from stdin if not specified") - cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called when given event occurs") + cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called to get test content") cmd.Flags().StringVarP(&gitUri, "git-uri", "", "", "Git repository uri") cmd.Flags().StringVarP(&gitBranch, "git-branch", "", "", "if uri is git repository we can set additional branch parameter") cmd.Flags().StringVarP(&gitCommit, "git-commit", "", "", "if uri is git repository we can use commit id (sha) parameter") diff --git a/cmd/kubectl-testkube/commands/webhooks/create.go b/cmd/kubectl-testkube/commands/webhooks/create.go index 01fb3cfba9a..ba4a22aa144 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -72,7 +72,7 @@ func NewCreateWebhookCmd() *cobra.Command { cmd.Flags().StringVarP(&name, "name", "n", "", "unique webhook name - mandatory") cmd.Flags().StringArrayVarP(&events, "events", "e", []string{}, "event types handled by webhook e.g. start-test|end-test") - cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called when given event occurs") + cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called when given event occurs (golang template supported)") cmd.Flags().StringVarP(&selector, "selector", "", "", "expression to select tests and test suites for webhook events: --selector app=backend") cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&payloadObjectField, "payload-field", "", "", "field to use for notification object payload") diff --git a/cmd/kubectl-testkube/commands/webhooks/update.go b/cmd/kubectl-testkube/commands/webhooks/update.go index a9edb0d80a1..15185146825 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -49,7 +49,7 @@ func UpdateWebhookCmd() *cobra.Command { cmd.Flags().StringVarP(&name, "name", "n", "", "unique webhook name - mandatory") cmd.Flags().StringArrayVarP(&events, "events", "e", []string{}, "event types handled by webhook e.g. start-test|end-test") - cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called when given event occurs") + cmd.Flags().StringVarP(&uri, "uri", "u", "", "URI which should be called when given event occurs (golang template supported)") cmd.Flags().StringVarP(&selector, "selector", "", "", "expression to select tests and test suites for webhook events: --selector app=backend") cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&payloadObjectField, "payload-field", "", "", "field to use for notification object payload") diff --git a/docs/docs/cli/testkube_create.md b/docs/docs/cli/testkube_create.md index 197297fa587..87bcdc9163f 100644 --- a/docs/docs/cli/testkube_create.md +++ b/docs/docs/cli/testkube_create.md @@ -27,7 +27,7 @@ testkube create [flags] * [testkube](testkube.md) - Testkube entrypoint for kubectl plugin * [testkube create executor](testkube_create_executor.md) - Create new Executor -* [testkube create template](testkube_create_template.md) - Create new Template +* [testkube create template](testkube_create_template.md) - Create a new Template. * [testkube create test](testkube_create_test.md) - Create new Test * [testkube create testsource](testkube_create_testsource.md) - Create new TestSource * [testkube create testsuite](testkube_create_testsuite.md) - Create new TestSuite diff --git a/docs/docs/cli/testkube_create_testsource.md b/docs/docs/cli/testkube_create_testsource.md index d7df2179afa..5a376c6ee7c 100644 --- a/docs/docs/cli/testkube_create_testsource.md +++ b/docs/docs/cli/testkube_create_testsource.md @@ -29,7 +29,7 @@ testkube create testsource [flags] -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique test source name - mandatory --source-type string source type of test one of string|file-uri|git - -u, --uri string URI which should be called when given event occurs + -u, --uri string URI which should be called to get test content ``` ### Options inherited from parent commands diff --git a/docs/docs/cli/testkube_create_webhook.md b/docs/docs/cli/testkube_create_webhook.md index c504d3564bd..38247f0996b 100644 --- a/docs/docs/cli/testkube_create_webhook.md +++ b/docs/docs/cli/testkube_create_webhook.md @@ -22,7 +22,7 @@ testkube create webhook [flags] --payload-template string if webhook needs to send a custom notification, then a path to template file should be provided --payload-template-reference string reference to payload template to use for the webhook --selector string expression to select tests and test suites for webhook events: --selector app=backend - -u, --uri string URI which should be called when given event occurs + -u, --uri string URI which should be called when given event occurs (golang template supported) ``` ### Options inherited from parent commands diff --git a/docs/docs/cli/testkube_delete.md b/docs/docs/cli/testkube_delete.md index 85fc7ee182d..1512aa3bc7b 100644 --- a/docs/docs/cli/testkube_delete.md +++ b/docs/docs/cli/testkube_delete.md @@ -26,7 +26,7 @@ testkube delete [flags] * [testkube](testkube.md) - Testkube entrypoint for kubectl plugin * [testkube delete executor](testkube_delete_executor.md) - Delete Executor -* [testkube delete template](testkube_delete_template.md) - Delete template +* [testkube delete template](testkube_delete_template.md) - Delete a template. * [testkube delete test](testkube_delete_test.md) - Delete Test * [testkube delete testsource](testkube_delete_testsource.md) - Delete test source * [testkube delete testsuite](testkube_delete_testsuite.md) - Delete test suite diff --git a/docs/docs/cli/testkube_get.md b/docs/docs/cli/testkube_get.md index 6b1172b18a5..010e0d0899f 100644 --- a/docs/docs/cli/testkube_get.md +++ b/docs/docs/cli/testkube_get.md @@ -35,7 +35,7 @@ testkube get [flags] * [testkube get context](testkube_get_context.md) - Set context for Testkube Cloud * [testkube get execution](testkube_get_execution.md) - Lists or gets test executions * [testkube get executor](testkube_get_executor.md) - Gets executor details -* [testkube get template](testkube_get_template.md) - Get template details +* [testkube get template](testkube_get_template.md) - Get template details. * [testkube get test](testkube_get_test.md) - Get all available tests * [testkube get testsource](testkube_get_testsource.md) - Get test source details * [testkube get testsuite](testkube_get_testsuite.md) - Get test suite by name diff --git a/docs/docs/cli/testkube_update_testsource.md b/docs/docs/cli/testkube_update_testsource.md index c2f8f68e4b9..3a3ec27d03b 100644 --- a/docs/docs/cli/testkube_update_testsource.md +++ b/docs/docs/cli/testkube_update_testsource.md @@ -29,7 +29,7 @@ testkube update testsource [flags] -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique test source name - mandatory --source-type string source type of test one of string|file-uri|git - -u, --uri string URI which should be called when given event occurs + -u, --uri string URI which should be called to get test content ``` ### Options inherited from parent commands diff --git a/docs/docs/cli/testkube_update_webhook.md b/docs/docs/cli/testkube_update_webhook.md index 64ce6abcfcf..7be51a48f18 100644 --- a/docs/docs/cli/testkube_update_webhook.md +++ b/docs/docs/cli/testkube_update_webhook.md @@ -22,7 +22,7 @@ testkube update webhook [flags] --payload-template string if webhook needs to send a custom notification, then a path to template file should be provided --payload-template-reference string reference to payload template to use for the webhook --selector string expression to select tests and test suites for webhook events: --selector app=backend - -u, --uri string URI which should be called when given event occurs + -u, --uri string URI which should be called when given event occurs (golang template supported) ``` ### Options inherited from parent commands diff --git a/pkg/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 30f5faef644..0a41d8de1e8 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -110,13 +110,27 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes } } + var tmpl *template.Template + tmpl, err = template.New("uri").Parse(l.Uri) + if err != nil { + log.Errorw("creating webhook uri error", "error", err) + return testkube.NewFailedEventResult(event.Id, err) + } + + var buffer bytes.Buffer + if err = tmpl.ExecuteTemplate(&buffer, "uri", event); err != nil { + log.Errorw("executing webhook uri error", "error", err) + return testkube.NewFailedEventResult(event.Id, err) + } + + uri := buffer.Bytes() if err != nil { err = errors.Wrap(err, "webhook send json encode error") log.Errorw("webhook send json encode error", "error", err) return testkube.NewFailedEventResult(event.Id, err) } - request, err := http.NewRequest(http.MethodPost, l.Uri, body) + request, err := http.NewRequest(http.MethodPost, string(uri), body) if err != nil { log.Errorw("webhook request creating error", "error", err) return testkube.NewFailedEventResult(event.Id, err) From 0981c37241a3a02aef1ec4406ac0fff1e9aa8145 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 7 Sep 2023 14:23:28 +0300 Subject: [PATCH 52/59] fix: golang template for headers --- api/v1/testkube.yaml | 2 +- .../commands/webhooks/create.go | 2 +- .../commands/webhooks/update.go | 2 +- docs/docs/articles/webhooks.mdx | 3 +- pkg/api/v1/testkube/model_webhook.go | 2 +- .../testkube/model_webhook_create_request.go | 2 +- .../testkube/model_webhook_update_request.go | 2 +- pkg/event/kind/webhook/listener.go | 63 ++++++++++--------- 8 files changed, 42 insertions(+), 36 deletions(-) diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index 309a70324a1..a461e3d4d73 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -5178,7 +5178,7 @@ components: description: name of the template resource headers: type: object - description: "webhook headers" + description: "webhook headers (golang template supported)" additionalProperties: type: string example: diff --git a/cmd/kubectl-testkube/commands/webhooks/create.go b/cmd/kubectl-testkube/commands/webhooks/create.go index ba4a22aa144..141e3e55855 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -77,7 +77,7 @@ func NewCreateWebhookCmd() *cobra.Command { cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&payloadObjectField, "payload-field", "", "", "field to use for notification object payload") cmd.Flags().StringVarP(&payloadTemplate, "payload-template", "", "", "if webhook needs to send a custom notification, then a path to template file should be provided") - cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair: --header Content-Type=application/xml") + cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair (golang template supported): --header Content-Type=application/xml") cmd.Flags().StringVar(&payloadTemplateReference, "payload-template-reference", "", "reference to payload template to use for the webhook") return cmd diff --git a/cmd/kubectl-testkube/commands/webhooks/update.go b/cmd/kubectl-testkube/commands/webhooks/update.go index 15185146825..8f0c524d026 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -54,7 +54,7 @@ func UpdateWebhookCmd() *cobra.Command { cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1") cmd.Flags().StringVarP(&payloadObjectField, "payload-field", "", "", "field to use for notification object payload") cmd.Flags().StringVarP(&payloadTemplate, "payload-template", "", "", "if webhook needs to send a custom notification, then a path to template file should be provided") - cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair: --header Content-Type=application/xml") + cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair (golang template supported): --header Content-Type=application/xml") cmd.Flags().StringVar(&payloadTemplateReference, "payload-template-reference", "", "reference to payload template to use for the webhook") return cmd diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index 5b099133724..ec695b3ee69 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -194,8 +194,9 @@ It's possible to get access to env variables of testkube-api-server pod in webho TESTKUBE_CLOUD_URL: {{ index .Envs "TESTKUBE_CLOUD_URL" }} ``` -### HTTP Headers +### URI andHTTP Headers You can add additional HTTP headers like `Authorization` or `x-api-key` to have a secret token. +It's possible to use golang based template string as header or uri value. diff --git a/pkg/api/v1/testkube/model_webhook.go b/pkg/api/v1/testkube/model_webhook.go index 46c46d0966b..92685ccc19e 100644 --- a/pkg/api/v1/testkube/model_webhook.go +++ b/pkg/api/v1/testkube/model_webhook.go @@ -23,7 +23,7 @@ type Webhook struct { PayloadTemplate string `json:"payloadTemplate,omitempty"` // name of the template resource PayloadTemplateReference string `json:"payloadTemplateReference,omitempty"` - // webhook headers + // webhook headers (golang template supported) Headers map[string]string `json:"headers,omitempty"` // webhook labels Labels map[string]string `json:"labels,omitempty"` diff --git a/pkg/api/v1/testkube/model_webhook_create_request.go b/pkg/api/v1/testkube/model_webhook_create_request.go index 8ed5a43869a..ed8387ced36 100644 --- a/pkg/api/v1/testkube/model_webhook_create_request.go +++ b/pkg/api/v1/testkube/model_webhook_create_request.go @@ -23,7 +23,7 @@ type WebhookCreateRequest struct { PayloadTemplate string `json:"payloadTemplate,omitempty"` // name of the template resource PayloadTemplateReference string `json:"payloadTemplateReference,omitempty"` - // webhook headers + // webhook headers (golang template supported) Headers map[string]string `json:"headers,omitempty"` // webhook labels Labels map[string]string `json:"labels,omitempty"` diff --git a/pkg/api/v1/testkube/model_webhook_update_request.go b/pkg/api/v1/testkube/model_webhook_update_request.go index c63b3fd46d5..22e2ec886a7 100644 --- a/pkg/api/v1/testkube/model_webhook_update_request.go +++ b/pkg/api/v1/testkube/model_webhook_update_request.go @@ -23,7 +23,7 @@ type WebhookUpdateRequest struct { PayloadTemplate *string `json:"payloadTemplate,omitempty"` // name of the template resource PayloadTemplateReference *string `json:"payloadTemplateReference,omitempty"` - // webhook headers + // webhook headers (golang template supported) Headers *map[string]string `json:"headers,omitempty"` // webhook labels Labels *map[string]string `json:"labels,omitempty"` diff --git a/pkg/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 0a41d8de1e8..774ee9c7c21 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -8,7 +8,6 @@ import ( "net/http" "text/template" - "github.com/pkg/errors" "go.uber.org/zap" "github.com/kubeshop/testkube/pkg/api/v1/testkube" @@ -87,20 +86,12 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes var err error if l.payloadTemplate != "" { - var tmpl *template.Template - tmpl, err = template.New("webhook").Parse(l.payloadTemplate) + data, err := l.processTemplate("payload", l.payloadTemplate, event) if err != nil { - log.Errorw("creating webhook template error", "error", err) return testkube.NewFailedEventResult(event.Id, err) } - var buffer bytes.Buffer - if err = tmpl.ExecuteTemplate(&buffer, "webhook", event); err != nil { - log.Errorw("executing webhook template error", "error", err) - return testkube.NewFailedEventResult(event.Id, err) - } - - _, err = body.Write(buffer.Bytes()) + _, err = body.Write(data) } else { err = json.NewEncoder(body).Encode(event) if err == nil && l.payloadObjectField != "" { @@ -110,27 +101,12 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes } } - var tmpl *template.Template - tmpl, err = template.New("uri").Parse(l.Uri) + data, err := l.processTemplate("uri", l.Uri, event) if err != nil { - log.Errorw("creating webhook uri error", "error", err) return testkube.NewFailedEventResult(event.Id, err) } - var buffer bytes.Buffer - if err = tmpl.ExecuteTemplate(&buffer, "uri", event); err != nil { - log.Errorw("executing webhook uri error", "error", err) - return testkube.NewFailedEventResult(event.Id, err) - } - - uri := buffer.Bytes() - if err != nil { - err = errors.Wrap(err, "webhook send json encode error") - log.Errorw("webhook send json encode error", "error", err) - return testkube.NewFailedEventResult(event.Id, err) - } - - request, err := http.NewRequest(http.MethodPost, string(uri), body) + request, err := http.NewRequest(http.MethodPost, string(data), body) if err != nil { log.Errorw("webhook request creating error", "error", err) return testkube.NewFailedEventResult(event.Id, err) @@ -138,6 +114,16 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes request.Header.Set("Content-Type", "application/json") for key, value := range l.headers { + values := []*string{&key, &value} + for i := range values { + data, err = l.processTemplate("header", *values[i], event) + if err != nil { + return testkube.NewFailedEventResult(event.Id, err) + } + + *values[i] = string(data) + } + request.Header.Set(key, value) } @@ -148,7 +134,7 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes } defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) + data, err = io.ReadAll(resp.Body) if err != nil { log.Errorw("webhook read response error", "error", err) return testkube.NewFailedEventResult(event.Id, err) @@ -169,3 +155,22 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes func (l *WebhookListener) Kind() string { return "webhook" } + +func (l *WebhookListener) processTemplate(field, body string, event testkube.Event) ([]byte, error) { + log := l.Log.With(event.Log()...) + + var tmpl *template.Template + tmpl, err := template.New(field).Parse(body) + if err != nil { + log.Errorw(fmt.Sprintf("creating webhook %s error", field), "error", err) + return nil, err + } + + var buffer bytes.Buffer + if err = tmpl.ExecuteTemplate(&buffer, field, event); err != nil { + log.Errorw(fmt.Sprintf("executing webhook %s error", field), "error", err) + return nil, err + } + + return buffer.Bytes(), nil +} From 7143d2b386aa78b6313cdc3508ae859e4b4c8685 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 7 Sep 2023 14:48:00 +0300 Subject: [PATCH 53/59] fix: golint --- docs/docs/articles/webhooks.mdx | 2 +- pkg/event/kind/webhook/listener.go | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index ec695b3ee69..14395c7f05e 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -194,7 +194,7 @@ It's possible to get access to env variables of testkube-api-server pod in webho TESTKUBE_CLOUD_URL: {{ index .Envs "TESTKUBE_CLOUD_URL" }} ``` -### URI andHTTP Headers +### URI and HTTP Headers You can add additional HTTP headers like `Authorization` or `x-api-key` to have a secret token. It's possible to use golang based template string as header or uri value. diff --git a/pkg/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 774ee9c7c21..b5dcea11e33 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -8,6 +8,7 @@ import ( "net/http" "text/template" + "github.com/pkg/errors" "go.uber.org/zap" "github.com/kubeshop/testkube/pkg/api/v1/testkube" @@ -86,7 +87,8 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes var err error if l.payloadTemplate != "" { - data, err := l.processTemplate("payload", l.payloadTemplate, event) + var data []byte + data, err = l.processTemplate("payload", l.payloadTemplate, event) if err != nil { return testkube.NewFailedEventResult(event.Id, err) } @@ -101,6 +103,12 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes } } + if err != nil { + err = errors.Wrap(err, "webhook send encode error") + log.Errorw("webhook send encode error", "error", err) + return testkube.NewFailedEventResult(event.Id, err) + } + data, err := l.processTemplate("uri", l.Uri, event) if err != nil { return testkube.NewFailedEventResult(event.Id, err) From f71967aedb35d9932e5bf2058ad489436652cf0b Mon Sep 17 00:00:00 2001 From: guoguangwu Date: Thu, 7 Sep 2023 19:17:21 +0800 Subject: [PATCH 54/59] chore: remove refs to deprecated io/ioutil Signed-off-by: guoguangwu --- cmd/kubectl-testkube/config/config_test.go | 3 +-- contrib/executor/zap/pkg/runner/runner_test.go | 15 +++++++-------- pkg/cloud/client/client_mock_test.go | 4 ++-- pkg/repository/storage/mongo.go | 3 +-- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/cmd/kubectl-testkube/config/config_test.go b/cmd/kubectl-testkube/config/config_test.go index c75069dfebb..52988e3cdf2 100644 --- a/cmd/kubectl-testkube/config/config_test.go +++ b/cmd/kubectl-testkube/config/config_test.go @@ -1,7 +1,6 @@ package config import ( - "io/ioutil" "os" "testing" @@ -10,7 +9,7 @@ import ( func TestSave(t *testing.T) { // override default directory - dir, err := ioutil.TempDir("", "test-config-save") + dir, err := os.MkdirTemp("", "test-config-save") assert.NoError(t, err) defaultDirectory = dir diff --git a/contrib/executor/zap/pkg/runner/runner_test.go b/contrib/executor/zap/pkg/runner/runner_test.go index e99fc77ef95..3aeb02053fd 100644 --- a/contrib/executor/zap/pkg/runner/runner_test.go +++ b/contrib/executor/zap/pkg/runner/runner_test.go @@ -2,7 +2,6 @@ package runner import ( "context" - "io/ioutil" "os" "path/filepath" "testing" @@ -19,7 +18,7 @@ func TestRun(t *testing.T) { t.Run("Run successful API scan", func(t *testing.T) { // given - tempDir, err := ioutil.TempDir(os.TempDir(), "") + tempDir, err := os.MkdirTemp(os.TempDir(), "") assert.NoError(t, err) runner, err := NewRunner(context.TODO(), envs.Params{ DataDir: tempDir, @@ -50,7 +49,7 @@ func TestRun(t *testing.T) { t.Run("Run API scan with PASS and WARN", func(t *testing.T) { // given - tempDir, err := ioutil.TempDir(os.TempDir(), "") + tempDir, err := os.MkdirTemp(os.TempDir(), "") assert.NoError(t, err) runner, err := NewRunner(context.TODO(), envs.Params{ DataDir: tempDir, @@ -81,7 +80,7 @@ func TestRun(t *testing.T) { t.Run("Run API scan with WARN and FailOnWarn", func(t *testing.T) { // given - tempDir, err := ioutil.TempDir(os.TempDir(), "") + tempDir, err := os.MkdirTemp(os.TempDir(), "") assert.NoError(t, err) runner, err := NewRunner(context.TODO(), envs.Params{ DataDir: tempDir, @@ -112,7 +111,7 @@ func TestRun(t *testing.T) { t.Run("Run API scan with FAIL", func(t *testing.T) { // given - tempDir, err := ioutil.TempDir(os.TempDir(), "") + tempDir, err := os.MkdirTemp(os.TempDir(), "") assert.NoError(t, err) runner, err := NewRunner(context.TODO(), envs.Params{ DataDir: tempDir, @@ -143,7 +142,7 @@ func TestRun(t *testing.T) { t.Run("Run Baseline scan with PASS", func(t *testing.T) { // given - tempDir, err := ioutil.TempDir(os.TempDir(), "") + tempDir, err := os.MkdirTemp(os.TempDir(), "") assert.NoError(t, err) runner, err := NewRunner(context.TODO(), envs.Params{ DataDir: tempDir, @@ -172,7 +171,7 @@ func TestRun(t *testing.T) { t.Run("Run Baseline scan with WARN", func(t *testing.T) { // given - tempDir, err := ioutil.TempDir(os.TempDir(), "") + tempDir, err := os.MkdirTemp(os.TempDir(), "") assert.NoError(t, err) runner, err := NewRunner(context.TODO(), envs.Params{ DataDir: tempDir, @@ -202,7 +201,7 @@ func TestRun(t *testing.T) { t.Run("Run Full scan with FAIL", func(t *testing.T) { // given - tempDir, err := ioutil.TempDir(os.TempDir(), "") + tempDir, err := os.MkdirTemp(os.TempDir(), "") assert.NoError(t, err) runner, err := NewRunner(context.TODO(), envs.Params{ DataDir: tempDir, diff --git a/pkg/cloud/client/client_mock_test.go b/pkg/cloud/client/client_mock_test.go index 1a2b172a5db..2f6ec40b414 100644 --- a/pkg/cloud/client/client_mock_test.go +++ b/pkg/cloud/client/client_mock_test.go @@ -2,7 +2,7 @@ package client import ( "bytes" - "io/ioutil" + "io" "net/http" ) @@ -18,6 +18,6 @@ func (c ClientMock) Do(req *http.Request) (*http.Response, error) { return nil, err } return &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader(c.body)), + Body: io.NopCloser(bytes.NewReader(c.body)), }, c.err } diff --git a/pkg/repository/storage/mongo.go b/pkg/repository/storage/mongo.go index 3c5ac512c55..94c754ad296 100644 --- a/pkg/repository/storage/mongo.go +++ b/pkg/repository/storage/mongo.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "time" @@ -81,7 +80,7 @@ func getDocDBTLSConfig() (*tls.Config, error) { }() tlsConfig := new(tls.Config) - certs, err := ioutil.ReadFile(caFilePath) + certs, err := os.ReadFile(caFilePath) if err != nil { return nil, fmt.Errorf("could not read CA file: %s", err) } From a74028eb832ceed643be4657c1547a0337451930 Mon Sep 17 00:00:00 2001 From: nicufk <89570185+nicufk@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:21:00 +0300 Subject: [PATCH 55/59] fix: improve abortion check to use nats events (#4345) * fix: improve abortion check to use nats events * fix: remove global bus var * fix: use interfaces instead of bus object * fix: synchronize using channels instead of atomic and move logic from event handler --- cmd/api-server/main.go | 2 + internal/app/api/v1/server.go | 4 + internal/app/api/v1/testsuites.go | 14 +- pkg/event/bus/nats.go | 10 +- pkg/scheduler/service.go | 4 + pkg/scheduler/testsuite_scheduler.go | 212 +++++++++++++++------------ pkg/triggers/executor_test.go | 2 + pkg/triggers/service_test.go | 2 + 8 files changed, 147 insertions(+), 103 deletions(-) diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index e1d17a3f790..f1a8b7efdab 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -395,6 +395,7 @@ func main() { configMapConfig, configMapClient, testsuiteExecutionsClient, + eventBus, ) slackLoader, err := newSlackLoader(cfg, envs) @@ -431,6 +432,7 @@ func main() { cfg.TestkubeDashboardURI, cfg.TestkubeHelmchartVersion, mode, + eventBus, ) if mode == common.ModeAgent { diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go index 74f5a9654ab..12ec6bb72c2 100644 --- a/internal/app/api/v1/server.go +++ b/internal/app/api/v1/server.go @@ -37,6 +37,7 @@ import ( testkubeclientset "github.com/kubeshop/testkube-operator/pkg/clientset/versioned" "github.com/kubeshop/testkube/internal/app/api/metrics" "github.com/kubeshop/testkube/pkg/event" + "github.com/kubeshop/testkube/pkg/event/bus" "github.com/kubeshop/testkube/pkg/event/kind/cdevent" "github.com/kubeshop/testkube/pkg/event/kind/slack" "github.com/kubeshop/testkube/pkg/event/kind/webhook" @@ -85,6 +86,7 @@ func NewTestkubeAPI( dashboardURI string, helmchartVersion string, mode string, + eventsBus bus.Bus, ) TestkubeAPI { var httpConfig server.Config @@ -127,6 +129,7 @@ func NewTestkubeAPI( TemplatesClient: templatesClient, helmchartVersion: helmchartVersion, mode: mode, + eventsBus: eventsBus, } // will be reused in websockets handler @@ -181,6 +184,7 @@ type TestkubeAPI struct { TemplatesClient *templatesclientv1.TemplatesClient helmchartVersion string mode string + eventsBus bus.Bus } type storageParams struct { diff --git a/internal/app/api/v1/testsuites.go b/internal/app/api/v1/testsuites.go index 88cfbadb8a4..3b547c5175e 100644 --- a/internal/app/api/v1/testsuites.go +++ b/internal/app/api/v1/testsuites.go @@ -19,6 +19,7 @@ import ( "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/crd" "github.com/kubeshop/testkube/pkg/datefilter" + "github.com/kubeshop/testkube/pkg/event/bus" testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests" testsuiteexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testsuiteexecutions" testsuitesmapper "github.com/kubeshop/testkube/pkg/mapper/testsuites" @@ -803,11 +804,14 @@ func (s TestkubeAPI) AbortTestSuiteHandler() fiber.Handler { for _, execution := range executions { execution.Status = testkube.TestSuiteExecutionStatusAborting - err = s.TestExecutionResults.Update(c.Context(), execution) + s.Log.Infow("aborting test suite execution", "executionID", execution.Id) + err := s.eventsBus.PublishTopic(bus.InternalPublishTopic, testkube.NewEventEndTestSuiteAborted(&execution)) if err != nil { - return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not update test suite execution: %w", errPrefix, err)) + return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not sent test suite abortion event: %w", errPrefix, err)) } + + s.Log.Infow("test suite execution aborted, event sent", "executionID", c.Params("executionID")) } return c.Status(http.StatusNoContent).SendString("") @@ -828,11 +832,13 @@ func (s TestkubeAPI) AbortTestSuiteExecutionHandler() fiber.Handler { } execution.Status = testkube.TestSuiteExecutionStatusAborting - err = s.TestExecutionResults.Update(c.Context(), execution) + + err = s.eventsBus.PublishTopic(bus.InternalPublishTopic, testkube.NewEventEndTestSuiteAborted(&execution)) if err != nil { - return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not update test suite execution: %w", errPrefix, err)) + return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not sent test suite abortion event: %w", errPrefix, err)) } + s.Log.Infow("test suite execution aborted, event sent", "executionID", c.Params("executionID")) return c.Status(http.StatusNoContent).SendString("") } diff --git a/pkg/event/bus/nats.go b/pkg/event/bus/nats.go index 9ef11429c70..a35f1aff615 100644 --- a/pkg/event/bus/nats.go +++ b/pkg/event/bus/nats.go @@ -11,11 +11,15 @@ import ( "github.com/kubeshop/testkube/pkg/log" ) -var _ Bus = (*NATSBus)(nil) +var ( + _ Bus = (*NATSBus)(nil) +) const ( - SubscribeBuffer = 1 - SubscriptionName = "events" + SubscribeBuffer = 1 + SubscriptionName = "events" + InternalPublishTopic = "internal.all" + InternalSubscribeTopic = "internal.>" ) func NewNATSConnection(uri string) (*nats.EncodedConn, error) { diff --git a/pkg/scheduler/service.go b/pkg/scheduler/service.go index 2952a8b654c..6939ab373c9 100644 --- a/pkg/scheduler/service.go +++ b/pkg/scheduler/service.go @@ -3,6 +3,7 @@ package scheduler import ( "go.uber.org/zap" + "github.com/kubeshop/testkube/pkg/event/bus" "github.com/kubeshop/testkube/pkg/repository/config" executorsv1 "github.com/kubeshop/testkube-operator/client/executors/v1" @@ -35,6 +36,7 @@ type Scheduler struct { configMap config.Repository configMapClient configmap.Interface testSuiteExecutionsClient testsuiteexecutionsclientv1.Interface + eventsBus bus.Bus } func NewScheduler( @@ -53,6 +55,7 @@ func NewScheduler( configMap config.Repository, configMapClient configmap.Interface, testSuiteExecutionsClient testsuiteexecutionsclientv1.Interface, + eventsBus bus.Bus, ) *Scheduler { return &Scheduler{ metrics: metrics, @@ -70,5 +73,6 @@ func NewScheduler( configMap: configMap, configMapClient: configMapClient, testSuiteExecutionsClient: testSuiteExecutionsClient, + eventsBus: eventsBus, } } diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go index e3d9e3a519c..d2123b6cdf2 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" + "github.com/kubeshop/testkube/pkg/event/bus" testsuiteexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testsuiteexecutions" testsuitesmapper "github.com/kubeshop/testkube/pkg/mapper/testsuites" "github.com/kubeshop/testkube/pkg/telemetry" @@ -141,22 +142,49 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE s.logger.Infow("Running steps", "test", testsuiteExecution.Name) + statusChan := make(chan *testkube.TestSuiteExecutionStatus) hasFailedSteps := false cancelSteps := false var batchStepResult *testkube.TestSuiteBatchStepExecutionResult var abortionStatus *testkube.TestSuiteExecutionStatus - abortChan := make(chan *testkube.TestSuiteExecutionStatus) - go s.abortionCheck(ctx, testsuiteExecution, request.Timeout, abortChan) + go s.timeoutCheck(ctx, testsuiteExecution, request.Timeout) + + err := s.eventsBus.SubscribeTopic(bus.InternalSubscribeTopic, testsuiteExecution.Name, func(event testkube.Event) error { + s.logger.Infow("test suite abortion event in runSteps", "event", event) + if event.TestSuiteExecution != nil && + event.TestSuiteExecution.Id == testsuiteExecution.Id && + event.Type_ != nil && + (*event.Type_ == testkube.END_TESTSUITE_ABORTED_EventType || *event.Type_ == testkube.END_TESTSUITE_TIMEOUT_EventType) { + s.logger.Infow("Aborting test suite execution", "execution", testsuiteExecution.Id) + + status := testkube.TestSuiteExecutionStatusAborting + if *event.Type_ == testkube.END_TESTSUITE_TIMEOUT_EventType { + status = testkube.TestSuiteExecutionStatusTimeout + } + statusChan <- status + } + return nil + }) + + if err != nil { + s.logger.Errorw("error subscribing to event", "error", err) + } for i := range testsuiteExecution.ExecuteStepResults { batchStepResult = &testsuiteExecution.ExecuteStepResults[i] - select { - case abortionStatus = <-abortChan: - s.logger.Infow("Aborting test suite execution", "execution", testsuiteExecution.Id, "i", i) + s.logger.Debugw("Running batch step", "step", batchStepResult.Execute, "i", i) + select { + case status := <-statusChan: + abortionStatus = status cancelSteps = true + default: + } + + if cancelSteps { + s.logger.Infow("Aborting batch step", "step", batchStepResult.Execute, "i", i) for j := range batchStepResult.Execute { if batchStepResult.Execute[j].Execution != nil && batchStepResult.Execute[j].Execution.ExecutionResult != nil { batchStepResult.Execute[j].Execution.ExecutionResult.Abort() @@ -164,63 +192,58 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE } testsuiteExecution.Status = testkube.TestSuiteExecutionStatusAborting - default: - s.logger.Debugw("Running batch step", "step", batchStepResult.Execute, "i", i) - - if cancelSteps { - s.logger.Debugw("Aborting batch step", "step", batchStepResult.Execute, "i", i) - for j := range batchStepResult.Execute { - if batchStepResult.Execute[j].Execution != nil && batchStepResult.Execute[j].Execution.ExecutionResult != nil { - batchStepResult.Execute[j].Execution.ExecutionResult.Abort() - } - } - - continue - } - - // start execution of given step for j := range batchStepResult.Execute { if batchStepResult.Execute[j].Execution != nil && batchStepResult.Execute[j].Execution.ExecutionResult != nil { - batchStepResult.Execute[j].Execution.ExecutionResult.InProgress() + batchStepResult.Execute[j].Execution.ExecutionResult.Abort() } } - err := s.testExecutionResults.Update(ctx, *testsuiteExecution) - if err != nil { - s.logger.Infow("Updating test execution", "error", err) + continue + } + + // start execution of given step + for j := range batchStepResult.Execute { + if batchStepResult.Execute[j].Execution != nil && batchStepResult.Execute[j].Execution.ExecutionResult != nil { + batchStepResult.Execute[j].Execution.ExecutionResult.InProgress() } + } + + err := s.testExecutionResults.Update(ctx, *testsuiteExecution) + if err != nil { + s.logger.Infow("Updating test execution", "error", err) + } - s.executeTestStep(ctx, *testsuiteExecution, request, batchStepResult) + s.executeTestStep(ctx, *testsuiteExecution, request, batchStepResult) - var results []*testkube.ExecutionResult - for j := range batchStepResult.Execute { - if batchStepResult.Execute[j].Execution != nil && batchStepResult.Execute[j].Execution.ExecutionResult != nil { - results = append(results, batchStepResult.Execute[j].Execution.ExecutionResult) - } + var results []*testkube.ExecutionResult + for j := range batchStepResult.Execute { + if batchStepResult.Execute[j].Execution != nil && batchStepResult.Execute[j].Execution.ExecutionResult != nil { + results = append(results, batchStepResult.Execute[j].Execution.ExecutionResult) } + } - s.logger.Debugw("Batch step execution result", "step", batchStepResult.Execute, "results", results) + s.logger.Debugw("Batch step execution result", "step", batchStepResult.Execute, "results", results) - err = s.testExecutionResults.Update(ctx, *testsuiteExecution) - if err != nil { - s.logger.Errorw("saving test suite execution results error", "error", err) + err = s.testExecutionResults.Update(ctx, *testsuiteExecution) + if err != nil { + s.logger.Errorw("saving test suite execution results error", "error", err) - hasFailedSteps = true - continue - } + hasFailedSteps = true + continue + } - for j := range batchStepResult.Execute { - if batchStepResult.Execute[j].IsFailed() { - hasFailedSteps = true - if batchStepResult.Step != nil && batchStepResult.Step.StopOnFailure { - cancelSteps = true - break - } + for j := range batchStepResult.Execute { + if batchStepResult.Execute[j].IsFailed() { + hasFailedSteps = true + if batchStepResult.Step != nil && batchStepResult.Step.StopOnFailure { + cancelSteps = true + break } } } } + s.logger.Infow("Finished running steps", "test", testsuiteExecution.Name, "hasFailedSteps", hasFailedSteps, "cancelSteps", cancelSteps, "status", testsuiteExecution.Status) if testsuiteExecution.Status != nil && *testsuiteExecution.Status == testkube.ABORTING_TestSuiteExecutionStatus { if abortionStatus != nil && *abortionStatus == testkube.TIMEOUT_TestSuiteExecutionStatus { @@ -240,10 +263,12 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE s.metrics.IncExecuteTestSuite(*testsuiteExecution) - err := s.testExecutionResults.Update(ctx, *testsuiteExecution) + err = s.testExecutionResults.Update(ctx, *testsuiteExecution) if err != nil { s.logger.Errorw("saving final test suite execution result error", "error", err) } + + s.eventsBus.Unsubscribe(testsuiteExecution.Name) } func (s *Scheduler) runAfterEachStep(ctx context.Context, execution *testkube.TestSuiteExecution, wg *sync.WaitGroup) { @@ -322,52 +347,34 @@ func (s *Scheduler) runAfterEachStep(ctx context.Context, execution *testkube.Te } } -// abortionCheck is polling database to see if the user aborted the test suite execution -func (s *Scheduler) abortionCheck(ctx context.Context, testsuiteExecution *testkube.TestSuiteExecution, timeout int32, abortChan chan *testkube.TestSuiteExecutionStatus) { - s.logger.Infow("Abortion check started", "test", testsuiteExecution.Name, "timeout", timeout) +// timeoutCheck is checking if the testsuite has timed out +func (s *Scheduler) timeoutCheck(ctx context.Context, testsuiteExecution *testkube.TestSuiteExecution, timeout int32) { + s.logger.Infow("timeout check started", "test", testsuiteExecution.Name, "timeout", timeout) - ticker := time.NewTicker(abortionPollingInterval) timer := time.NewTimer(time.Duration(timeout) * time.Second) defer func() { timer.Stop() - ticker.Stop() }() for testsuiteExecution.Status == testkube.TestSuiteExecutionStatusRunning { select { case <-timer.C: - s.logger.Debugw("Abortion check timeout", "test", testsuiteExecution.Name) + s.logger.Debugw("testsuite timeout occured", "test suite", testsuiteExecution.Name) if timeout > 0 { - s.logger.Debugw("Aborting test suite execution due to timeout", "execution", testsuiteExecution.Id) + s.logger.Debugw("aborting test suite execution due to timeout", "execution", testsuiteExecution.Id) - abortChan <- testkube.TestSuiteExecutionStatusTimeout - return - } - case <-ticker.C: - if s.wasTestSuiteAborted(ctx, testsuiteExecution.Id) { - s.logger.Debugw("Aborting test suite execution", "execution", testsuiteExecution.Id) - - abortChan <- testkube.TestSuiteExecutionStatusAborted + err := s.eventsBus.PublishTopic(bus.InternalPublishTopic, testkube.NewEventEndTestSuiteTimeout(testsuiteExecution)) + if err != nil { + s.logger.Errorw("error publishing event", "error", err) + } return } } } - s.logger.Debugw("Abortion check, finished checking", "test", testsuiteExecution.Name) -} - -func (s *Scheduler) wasTestSuiteAborted(ctx context.Context, id string) bool { - execution, err := s.testExecutionResults.Get(ctx, id) - if err != nil { - s.logger.Errorw("getting test execution", "error", err) - return false - } - - s.logger.Debugw("Checking if test suite execution was aborted", "id", id, "status", execution.Status) - - return execution.Status != nil && *execution.Status == testkube.ABORTING_TestSuiteExecutionStatus + s.logger.Debugw("Timeout check, finished checking", "test", testsuiteExecution.Name) } func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution testkube.TestSuiteExecution, @@ -502,13 +509,30 @@ func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution test func (s *Scheduler) delayWithAbortionCheck(duration time.Duration, testSuiteId string, result *testkube.TestSuiteBatchStepExecutionResult) { timer := time.NewTimer(duration) - ticker := time.NewTicker(abortionPollingInterval) defer func() { timer.Stop() - ticker.Stop() }() + abortChan := make(chan bool) + + err := s.eventsBus.SubscribeTopic(bus.InternalSubscribeTopic, testSuiteId, func(event testkube.Event) error { + s.logger.Infow("test suite abortion event in delay handling", "event", event) + if event.TestSuiteExecution != nil && + event.TestSuiteExecution.Id == testSuiteId && + event.Type_ != nil && + *event.Type_ == testkube.END_TESTSUITE_ABORTED_EventType { + + s.logger.Infow("delay aborted", "testSuiteId", testSuiteId, "duration", duration) + abortChan <- true + } + return nil + }) + + if err != nil { + s.logger.Errorw("error subscribing to event", "error", err) + } + for { select { case <-timer.C: @@ -522,30 +546,26 @@ func (s *Scheduler) delayWithAbortionCheck(duration time.Duration, testSuiteId s } return - case <-ticker.C: - if s.wasTestSuiteAborted(context.Background(), testSuiteId) { - s.logger.Infow("delay aborted", "testSuiteId", testSuiteId, "duration", duration) - - for i := range result.Execute { - if result.Execute[i].Step != nil && result.Execute[i].Step.Delay != "" && - result.Execute[i].Execution != nil && result.Execute[i].Execution.ExecutionResult != nil { - delay, err := time.ParseDuration(result.Execute[i].Step.Delay) - if err != nil { - result.Execute[i].Err(err) - continue - } - - if delay < duration { - result.Execute[i].Execution.ExecutionResult.Success() - continue - } - - result.Execute[i].Execution.ExecutionResult.Abort() + case <-abortChan: + + for i := range result.Execute { + if result.Execute[i].Step != nil && result.Execute[i].Step.Delay != "" && + result.Execute[i].Execution != nil && result.Execute[i].Execution.ExecutionResult != nil { + delay, err := time.ParseDuration(result.Execute[i].Step.Delay) + if err != nil { + result.Execute[i].Err(err) + continue } - } - return + if delay < duration { + result.Execute[i].Execution.ExecutionResult.Success() + continue + } + + result.Execute[i].Execution.ExecutionResult.Abort() + } } + return } } } diff --git a/pkg/triggers/executor_test.go b/pkg/triggers/executor_test.go index e08793a2008..73a8321fb58 100644 --- a/pkg/triggers/executor_test.go +++ b/pkg/triggers/executor_test.go @@ -39,6 +39,7 @@ func TestExecute(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() + mockBus := bus.NewEventBusMock() mockResultRepository := result.NewMockRepository(mockCtrl) mockTestResultRepository := testresult.NewMockRepository(mockCtrl) @@ -116,6 +117,7 @@ func TestExecute(t *testing.T) { configMapConfig, mockConfigMapClient, mockTestSuiteExecutionsClient, + mockBus, ) s := &Service{ triggerStatus: make(map[statusKey]*triggerStatus), diff --git a/pkg/triggers/service_test.go b/pkg/triggers/service_test.go index df9a9f47c39..608421a790c 100644 --- a/pkg/triggers/service_test.go +++ b/pkg/triggers/service_test.go @@ -45,6 +45,7 @@ func TestService_Run(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() + mockBus := bus.NewEventBusMock() mockResultRepository := result.NewMockRepository(mockCtrl) mockTestResultRepository := testresult.NewMockRepository(mockCtrl) @@ -130,6 +131,7 @@ func TestService_Run(t *testing.T) { configMapConfig, mockConfigMapClient, mockTestSuiteExecutionsClient, + mockBus, ) mockLeaseBackend := NewMockLeaseBackend(mockCtrl) From 2caa7c8c6ec4b22922195211aa95576a960733c6 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 8 Sep 2023 13:54:19 +0300 Subject: [PATCH 56/59] fix: show cli context --- cmd/kubectl-testkube/commands/common/cloudcontext.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubectl-testkube/commands/common/cloudcontext.go b/cmd/kubectl-testkube/commands/common/cloudcontext.go index fb989c4cb16..178a2813616 100644 --- a/cmd/kubectl-testkube/commands/common/cloudcontext.go +++ b/cmd/kubectl-testkube/commands/common/cloudcontext.go @@ -75,7 +75,7 @@ func UiContextHeader(cmd *cobra.Command, cfg config.Data) { header += ui.DarkGray("Env: ") + ui.White(envName) } else { header += ui.DarkGray("Context: ") + ui.White(cfg.ContextType) + ui.DarkGray(" ("+Version+")") + separator - header += ui.DarkGray("Namespace: ") + ui.White(cfg.Namespace) + header += ui.DarkGray("Namespace: ") + ui.White(cmd.Flag("namespace").Value.String()) } fmt.Println(header) From 2d7133088efba43cc496e5cb03062f316a9efcb5 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 8 Sep 2023 16:44:47 +0300 Subject: [PATCH 57/59] fix: doc typo --- docs/docs/articles/templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index 96ee5899f41..a705866ede2 100644 --- a/docs/docs/articles/templates.mdx +++ b/docs/docs/articles/templates.mdx @@ -33,7 +33,7 @@ kubectl testkube create template --name job-template --template-type job --body ```yaml title="template.yaml" apiVersion: tests.testkube.io/v1 -kind: Webhook +kind: Template metadata: name: example-webhook namespace: testkube From 7279abcbc666b471f1dd7e5485a71b65158617f8 Mon Sep 17 00:00:00 2001 From: Tomasz Konieczny Date: Fri, 8 Sep 2023 18:59:08 +0200 Subject: [PATCH 58/59] =?UTF-8?q?docs:=20Webhooks=20-=20docs=20extended=20?= =?UTF-8?q?with=20supported=20event,=20testexecution=20and=20test=E2=80=A6?= =?UTF-8?q?=20(#4338)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Webhooks - docs extended with supported event, testexecution and testsuiteexecution variables, example for microsoft teams added * docs - templates.mdx links fixed * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi * Update docs/docs/articles/webhooks.mdx Co-authored-by: Julianne Fermi --------- Co-authored-by: Julianne Fermi --- docs/docs/articles/webhooks.mdx | 103 +++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 3 deletions(-) diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index 14395c7f05e..2a04d701275 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -185,10 +185,21 @@ spec: -#### Customizing Webhook Payload -The payload template can be customized to include additional information. In the above example, only the event `Id` is being sent. The template's variables will be replaced with data when events occur. +### Webhook Payload Variables +Webhook payload can contain **event-specific** variables - they will be replaced with actual data when the events occurs. In the above examples, only the event `Id` is being sent. +However, any of these [supported Event Variables](#supported-event-variables) can be used. -It's possible to get access to env variables of testkube-api-server pod in webhook template: +For example, the following payload: +``` +{"text": "Event {{ .Type_ }} - Test '{{ .TestExecution.TestName }}' execution ({{ .TestExecution.Number }}) finished with '{{ .TestExecution.ExecutionResult.Status }}' status"} +``` +will result in: +``` +{"text": "Event end-test-success - Test 'postman-executor-smoke' execution (948) finished with 'passed' status"} +``` + +#### testkube-api-server ENV variables +In addition to event-specific variables, it's also possible to pass testkube-api-server ENV variables: ```sh title="template.txt" TESTKUBE_CLOUD_URL: {{ index .Envs "TESTKUBE_CLOUD_URL" }} @@ -269,6 +280,92 @@ They can be triggered by the following resources: - testexecution - testsuiteexecution +## Supported Event Variables + +### Event-specific variables: +- `Id` - event ID (for example, `2a20c7da-3b77-4ea9-a33d-403187d3e9e6`) +- `Resource` +- `ResourceId` +- `Type_` - event Type (for example, `start-test`, `end-test,success`, etc. All available trigger events can be found in the [Supported Event types](#supported-event-types) section). +- `TestExecution` - test execution details (example: [TestExecution (Execution)](#testexecution-execution) section) +- `TestSuiteExecution` - test suite execution details (example: [TestSuiteExecution](#testsuiteexecution) section) +- `ClusterName` - cluster name +- `Envs` (API-server ENV variables) - list of Testkube API-Server ENV variables + +The full Event Data Model can be found [here](https://github.com/kubeshop/testkube/blob/main/pkg/api/v1/testkube/model_event.go). + +### TestExecution (Execution): +- `Id` - Execution ID (for example, `64f8cf3c712890925aea51ce`) +- `TestName` - Test Name (for example, `postman-executor-smoke`) +- `TestSuiteName` - Test Suite name (if run as a part of a Test Suite) +- `TestNamespace` - Execution namespace, where testkube is installed (for example, `testkube`) +- `TestType` - Test type (for example, `postman/collection`) +- `Name` - Execution name (for example, `postman-executor-smoke-937) +- `Number` - Execution number (for example, `937`) +- `Envs` - List of ENV variables for specific Test (if defined) +- `Command` - Command executed inside the Pod (for example, `newman`) +- `Args` - Command arguments (for example, `run -e --reporters cli,json --reporter-json-export `) +- `Variables` - List of variables +- `Content` - Test content +- `StartTime` - Test start time (for example, `2023-09-06 19:23:34.543433547 +0000 UTC`) +- `EndTime` - Time when the test execution finished (for example, `2023-09-06 19:23:42.221493031 +0000 UTC`) +- `Duration` - Test duration in seconds (for example, `7.68s`) +- `DurationMs` - Test duration in miliseconds (for example, `7678`) +- `ExecutionResult` - Execution result (https://github.com/kubeshop/testkube/blob/main/pkg/api/v1/testkube/model_event.go) +- `Labels` Test labels (for example, `[core-tests:executors executor:postman-executor test-type:postman-collection],`) +- `RunningContext` - Running context - how the test has been triggered (for example, `user-ui`) + +The full Execution data model can be found [here](https://github.com/kubeshop/testkube/blob/main/pkg/api/v1/testkube/model_execution.go). + +### TestSuiteExecution: + +- `Id` - TestSuiteExecution ID (for example, `64f8d5b2712890925aea51dc`) +- `Name` - TestSuite name (for example, `ts-executor-postman-smoke-tests-472`) +- `Status` - TestSuite execution status (for example, `running` or `passed`) +- `Envs` - List of ENV variables +- `Variables` - List of variables +- `StartTime` - Test start time (for example, `2023-09-06 19:23:34.543433547 +0000 UTC`) +- `EndTime` - Time when the test execution finished (for example, `2023-09-06 19:23:42.221493031 +0000 UTC`) +- `Duration` - Test duration in seconds (for example, `7.68s`) +- `DurationMs` - Test duration in miliseconds (for example, `7678`) +- `StepResults` +- `Labels` - TestSuite labels (for example, `[app:testkube testsuite:executor-postman-smoke-tests]`) +- `RunningContext` - Running context - how the TestSuite has been triggered (for example, `user-ui`) + +The full TestSuiteExecution data model can be found [here](https://github.com/kubeshop/testkube/blob/main/pkg/api/v1/testkube/model_test_suite_execution.go). + +## Additional Examples + +### Microsoft Teams +Webhooks can also be used to send messages to Microsoft Teams channels. +First, you need to create an incoming webhook in Teams for a specific channel. You can see how to do it in the Teams Docs [here](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet#create-incoming-webhooks-1). After your Teams incoming webhook is created, you can use it with Testkube webhooks - just use the URL provided (it will probably look like this: `https://xxxxx.webhook.office.com/xxxxxxxxx`). + +In order to send the message when test execution finishes, the following Webhook can be used: +``` +apiVersion: executor.testkube.io/v1 +kind: Webhook +metadata: + name: example-webhook-teams + namespace: testkube +spec: + events: + - end-test-success + - end-test-failed + - end-test-aborted + - end-test-timeout + uri: https://xxxxx.webhook.office.com/xxxxxxxxx + payloadTemplate: "{\"text\": \"Test '{{ .TestExecution.TestName }}' execution ({{ .TestExecution.Number }}) finished with '{{ .TestExecution.ExecutionResult.Status }}' status\"}\n" +``` + +It will result in: + +``` +{"text": "Test 'postman-executor-smoke' execution (949) finished with 'passed' status"} +``` + +and the message: +`Test 'postman-executor-smoke' execution (949) finished with 'passed' status"` being displayed. + ## Testing Webhooks If you are just getting started and want to test your webhook configuration, you can use public and free services that act as HTTP catch-all apps. Here are a couple of options: From aac7795c53bdcc1bf5840518a7d82413ccfd9508 Mon Sep 17 00:00:00 2001 From: Dejan Zele Pejchev Date: Sun, 10 Sep 2023 18:14:05 +0200 Subject: [PATCH 59/59] Create scorecard.yml --- .github/workflows/scorecard.yml | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000000..ec9953b5f24 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,72 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '35 11 * * 1' + push: + branches: [ "develop", "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 + with: + sarif_file: results.sarif