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 diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index ab48f196336..a461e3d4d73 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: @@ -5152,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: @@ -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/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/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) 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..141e3e55855 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -72,12 +72,12 @@ 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") 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 a9edb0d80a1..8f0c524d026 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -49,12 +49,12 @@ 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") 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/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/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" ] 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/docs/docs/articles/templates.mdx b/docs/docs/articles/templates.mdx index d0d5ad09a28..a705866ede2 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) @@ -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 @@ -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) diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index 5b099133724..2a04d701275 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -185,17 +185,29 @@ 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" }} ``` -### HTTP 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. @@ -268,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: 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/internal/app/api/v1/secret.go b/internal/app/api/v1/secret.go new file mode 100644 index 00000000000..9955ea572e4 --- /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(true) + 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..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 { @@ -381,6 +385,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/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/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"` +} 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/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/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/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 30f5faef644..b5dcea11e33 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -87,20 +87,13 @@ 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) + var data []byte + 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 != "" { @@ -111,12 +104,17 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes } if err != nil { - err = errors.Wrap(err, "webhook send json encode error") - log.Errorw("webhook send json encode error", "error", err) + err = errors.Wrap(err, "webhook send encode error") + log.Errorw("webhook send encode error", "error", err) return testkube.NewFailedEventResult(event.Id, err) } - request, err := http.NewRequest(http.MethodPost, l.Uri, body) + data, err := l.processTemplate("uri", l.Uri, event) + if err != nil { + return testkube.NewFailedEventResult(event.Id, err) + } + + 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) @@ -124,6 +122,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) } @@ -134,7 +142,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) @@ -155,3 +163,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 +} 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) } 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/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. 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)