From 492446e82d2b2b2e1cb1d8461a095f4b1f40d945 Mon Sep 17 00:00:00 2001 From: Povilas Versockas Date: Wed, 10 Jul 2024 01:47:30 -0700 Subject: [PATCH 01/16] feat: [TKC-2194] improve workflow execution telemetry (#5648) --- pkg/telemetry/payload.go | 25 +- .../testworkflowexecutor/executor.go | 43 +--- .../testworkflowmetrics.go | 234 ++++++++++++++++++ 3 files changed, 259 insertions(+), 43 deletions(-) create mode 100644 pkg/testworkflows/testworkflowexecutor/testworkflowmetrics.go diff --git a/pkg/telemetry/payload.go b/pkg/telemetry/payload.go index ece02f9a3a1..84b113212c3 100644 --- a/pkg/telemetry/payload.go +++ b/pkg/telemetry/payload.go @@ -34,9 +34,16 @@ type Params struct { ErrorType string `json:"error_type,omitempty"` ErrorStackTrace string `json:"error_stacktrace,omitempty"` TestWorkflowSteps int32 `json:"test_workflow_steps,omitempty"` + TestWorkflowExecuteCount int32 `json:"test_workflow_execute_count,omitempty"` + TestWorkflowParallelUsed bool `json:"test_workflow_parallel_used,omitempty"` + TestWorkflowMatrixUsed bool `json:"test_workflow_matrix_used,omitempty"` + TestWorkflowServicesUsed bool `json:"test_workflow_services_used,omitempty"` + TestWorkflowIsSample bool `json:"test_workflow_is_sample,omitempty"` + TestWorkflowTemplates []string `json:"testWorkflowTemplates"` + TestWorkflowImages []string `json:"testWorkflowImages"` TestWorkflowTemplateUsed bool `json:"test_workflow_template_used,omitempty"` - TestWorkflowImage string `json:"test_workflow_image,omitempty"` TestWorkflowArtifactUsed bool `json:"test_workflow_artifact_used,omitempty"` + TestWorkflowImage string `json:"test_workflow_image,omitempty"` TestWorkflowKubeshopGitURI bool `json:"test_workflow_kubeshop_git_uri,omitempty"` License string `json:"license,omitempty"` Step string `json:"step,omitempty"` @@ -84,9 +91,16 @@ type RunContext struct { type WorkflowParams struct { TestWorkflowSteps int32 - TestWorkflowTemplateUsed bool + TestWorkflowExecuteCount int32 TestWorkflowImage string TestWorkflowArtifactUsed bool + TestWorkflowParallelUsed bool + TestWorkflowMatrixUsed bool + TestWorkflowServicesUsed bool + TestWorkflowTemplateUsed bool + TestWorkflowIsSample bool + TestWorkflowTemplates []string + TestWorkflowImages []string TestWorkflowKubeshopGitURI bool } @@ -290,7 +304,14 @@ func NewRunWorkflowPayload(name, clusterType string, params RunWorkflowParams) P ClusterType: clusterType, Context: getAgentContext(), TestWorkflowSteps: params.TestWorkflowSteps, + TestWorkflowExecuteCount: params.TestWorkflowExecuteCount, + TestWorkflowParallelUsed: params.TestWorkflowParallelUsed, TestWorkflowTemplateUsed: params.TestWorkflowTemplateUsed, + TestWorkflowMatrixUsed: params.TestWorkflowMatrixUsed, + TestWorkflowServicesUsed: params.TestWorkflowServicesUsed, + TestWorkflowIsSample: params.TestWorkflowIsSample, + TestWorkflowTemplates: params.TestWorkflowTemplates, + TestWorkflowImages: params.TestWorkflowImages, TestWorkflowImage: params.TestWorkflowImage, TestWorkflowArtifactUsed: params.TestWorkflowArtifactUsed, TestWorkflowKubeshopGitURI: params.TestWorkflowKubeshopGitURI, diff --git a/pkg/testworkflows/testworkflowexecutor/executor.go b/pkg/testworkflows/testworkflowexecutor/executor.go index 3dea3b6d0f8..d3f00502c13 100644 --- a/pkg/testworkflows/testworkflowexecutor/executor.go +++ b/pkg/testworkflows/testworkflowexecutor/executor.go @@ -29,13 +29,10 @@ import ( configRepo "github.com/kubeshop/testkube/pkg/repository/config" "github.com/kubeshop/testkube/pkg/repository/result" "github.com/kubeshop/testkube/pkg/repository/testworkflow" - "github.com/kubeshop/testkube/pkg/telemetry" - "github.com/kubeshop/testkube/pkg/testworkflows" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowcontroller" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowresolver" - "github.com/kubeshop/testkube/pkg/version" ) //go:generate mockgen -destination=./mock_executor.go -package=testworkflowexecutor "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowexecutor" TestWorkflowExecutor @@ -294,6 +291,8 @@ func (e *executor) Control(ctx context.Context, testWorkflow *testworkflowsv1.Te // TODO: Consider AppendOutput ($push) instead _ = e.repository.UpdateOutput(ctx, execution.Id, execution.Output) if execution.Result.IsFinished() { + e.sendRunWorkflowTelemetry(ctx, testWorkflow, execution) + if execution.Result.IsPassed() { e.emitter.Notify(testkube.NewEventEndTestWorkflowSuccess(execution)) } else if execution.Result.IsAborted() { @@ -537,8 +536,6 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor return execution, errors.Wrap(err, "deploying required resources") } - e.sendRunWorkflowTelemetry(ctx, &workflow) - // Start to control the results go func() { err = e.Control(context.Background(), initialWorkflow, &execution) @@ -550,39 +547,3 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor return execution, nil } - -func (e *executor) sendRunWorkflowTelemetry(ctx context.Context, workflow *testworkflowsv1.TestWorkflow) { - if workflow == nil { - log.DefaultLogger.Debug("empty workflow passed to telemetry event") - return - } - telemetryEnabled, err := e.configMap.GetTelemetryEnabled(ctx) - if err != nil { - log.DefaultLogger.Debugf("getting telemetry enabled error", "error", err) - } - if !telemetryEnabled { - return - } - - out, err := telemetry.SendRunWorkflowEvent("testkube_api_run_test_workflow", telemetry.RunWorkflowParams{ - RunParams: telemetry.RunParams{ - AppVersion: version.Version, - DataSource: testworkflows.GetDataSource(workflow.Spec.Content), - Host: testworkflows.GetHostname(), - ClusterID: testworkflows.GetClusterID(ctx, e.configMap), - }, - WorkflowParams: telemetry.WorkflowParams{ - TestWorkflowSteps: int32(len(workflow.Spec.Setup) + len(workflow.Spec.Steps) + len(workflow.Spec.After)), - TestWorkflowImage: testworkflows.GetImage(workflow.Spec.Container), - TestWorkflowArtifactUsed: testworkflows.HasWorkflowStepLike(workflow.Spec, testworkflows.HasArtifacts), - TestWorkflowKubeshopGitURI: testworkflows.IsKubeshopGitURI(workflow.Spec.Content) || - testworkflows.HasWorkflowStepLike(workflow.Spec, testworkflows.HasKubeshopGitURI), - }, - }) - - if err != nil { - log.DefaultLogger.Debugf("sending run test workflow telemetry event error", "error", err) - } else { - log.DefaultLogger.Debugf("sending run test workflow telemetry event", "output", out) - } -} diff --git a/pkg/testworkflows/testworkflowexecutor/testworkflowmetrics.go b/pkg/testworkflows/testworkflowexecutor/testworkflowmetrics.go new file mode 100644 index 00000000000..dae361f581b --- /dev/null +++ b/pkg/testworkflows/testworkflowexecutor/testworkflowmetrics.go @@ -0,0 +1,234 @@ +package testworkflowexecutor + +import ( + "context" + "strings" + + testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" + + "github.com/kubeshop/testkube/pkg/api/v1/testkube" + "github.com/kubeshop/testkube/pkg/log" + "github.com/kubeshop/testkube/pkg/telemetry" + "github.com/kubeshop/testkube/pkg/testworkflows" + "github.com/kubeshop/testkube/pkg/version" +) + +type stepStats struct { + numSteps int + numExecute int + hasArtifacts bool + hasMatrix bool + hasParallel bool + hasTemplate bool + hasServices bool + imagesUsed map[string]struct{} + templatesUsed map[string]struct{} +} + +func (ss *stepStats) Merge(stats *stepStats) { + ss.numSteps += stats.numSteps + ss.numExecute += stats.numExecute + + if stats.hasArtifacts { + ss.hasArtifacts = true + } + if stats.hasMatrix { + ss.hasMatrix = true + } + if stats.hasParallel { + ss.hasParallel = true + } + if stats.hasServices { + ss.hasServices = true + } + if stats.hasTemplate { + ss.hasTemplate = true + } + for image := range stats.imagesUsed { + ss.imagesUsed[image] = struct{}{} + } + for tmpl := range stats.templatesUsed { + ss.templatesUsed[tmpl] = struct{}{} + } +} + +func getStepInfo(step testworkflowsv1.Step) *stepStats { + res := &stepStats{ + imagesUsed: make(map[string]struct{}), + templatesUsed: make(map[string]struct{}), + } + if step.Execute != nil { + res.numExecute++ + } + if step.Artifacts != nil { + res.hasArtifacts = true + } + if len(step.Use) > 0 { + res.hasTemplate = true + for _, tmpl := range step.Use { + res.templatesUsed[tmpl.Name] = struct{}{} + } + } + if step.Template != nil { + res.hasTemplate = true + res.templatesUsed[step.Template.Name] = struct{}{} + } + if len(step.Services) > 0 { + res.hasServices = true + } + + if step.Run != nil && step.Run.Image != "" { + res.imagesUsed[step.Run.Image] = struct{}{} + } + if step.Container != nil && step.Container.Image != "" { + res.imagesUsed[step.Container.Image] = struct{}{} + } + + for _, step := range step.Steps { + res.Merge(getStepInfo(step)) + } + + if step.Parallel != nil { + res.hasParallel = true + + if len(step.Parallel.Matrix) != 0 { + res.hasMatrix = true + } + if step.Parallel.Artifacts != nil { + res.hasArtifacts = true + } + if step.Parallel.Execute != nil { + res.numExecute++ + } + if len(step.Parallel.Use) > 0 { + res.hasTemplate = true + for _, tmpl := range step.Parallel.Use { + res.templatesUsed[tmpl.Name] = struct{}{} + } + } + if step.Parallel.Template != nil { + res.hasTemplate = true + res.templatesUsed[step.Parallel.Template.Name] = struct{}{} + } + + if len(step.Parallel.Services) > 0 { + res.hasServices = true + } + + if step.Parallel.Run != nil && step.Parallel.Run.Image != "" { + res.imagesUsed[step.Parallel.Run.Image] = struct{}{} + } + if step.Parallel.Container != nil && step.Parallel.Container.Image != "" { + res.imagesUsed[step.Parallel.Container.Image] = struct{}{} + } + + for _, step := range step.Parallel.Steps { + res.Merge(getStepInfo(step)) + } + } + + return res +} + +func (e *executor) sendRunWorkflowTelemetry(ctx context.Context, workflow *testworkflowsv1.TestWorkflow, execution *testkube.TestWorkflowExecution) { + if workflow == nil { + log.DefaultLogger.Debug("empty workflow passed to telemetry event") + return + } + telemetryEnabled, err := e.configMap.GetTelemetryEnabled(ctx) + if err != nil { + log.DefaultLogger.Debugf("getting telemetry enabled error", "error", err) + } + if !telemetryEnabled { + return + } + + properties := make(map[string]any) + properties["name"] = workflow.Name + stats := stepStats{ + imagesUsed: make(map[string]struct{}), + templatesUsed: make(map[string]struct{}), + } + + var isSample bool + if workflow.Labels != nil && workflow.Labels["docs"] == "example" && strings.HasSuffix(workflow.Name, "-sample") { + isSample = true + } else { + isSample = false + } + + spec := workflow.Spec + for _, step := range spec.Steps { + stats.Merge(getStepInfo(step)) + } + if spec.Container != nil { + stats.imagesUsed[spec.Container.Image] = struct{}{} + } + if len(spec.Services) != 0 { + stats.hasServices = true + } + if len(spec.Use) > 0 { + stats.hasTemplate = true + for _, tmpl := range spec.Use { + stats.templatesUsed[tmpl.Name] = struct{}{} + } + } + + var images []string + for image := range stats.imagesUsed { + if image == "" { + continue + } + images = append(images, image) + } + + var templates []string + for t := range stats.templatesUsed { + if t == "" { + continue + } + templates = append(templates, t) + } + var ( + status string + durationMs int32 + ) + if execution.Result != nil { + if execution.Result.Status != nil { + status = string(*execution.Result.Status) + } + durationMs = execution.Result.DurationMs + } + + out, err := telemetry.SendRunWorkflowEvent("testkube_api_run_test_workflow", telemetry.RunWorkflowParams{ + RunParams: telemetry.RunParams{ + AppVersion: version.Version, + DataSource: testworkflows.GetDataSource(workflow.Spec.Content), + Host: testworkflows.GetHostname(), + ClusterID: testworkflows.GetClusterID(ctx, e.configMap), + DurationMs: durationMs, + Status: status, + }, + WorkflowParams: telemetry.WorkflowParams{ + TestWorkflowSteps: int32(stats.numSteps), + TestWorkflowExecuteCount: int32(stats.numExecute), + TestWorkflowImage: testworkflows.GetImage(workflow.Spec.Container), + TestWorkflowArtifactUsed: stats.hasArtifacts, + TestWorkflowParallelUsed: stats.hasParallel, + TestWorkflowMatrixUsed: stats.hasMatrix, + TestWorkflowServicesUsed: stats.hasServices, + TestWorkflowTemplateUsed: stats.hasTemplate, + TestWorkflowIsSample: isSample, + TestWorkflowTemplates: templates, + TestWorkflowImages: images, + TestWorkflowKubeshopGitURI: testworkflows.IsKubeshopGitURI(workflow.Spec.Content) || + testworkflows.HasWorkflowStepLike(workflow.Spec, testworkflows.HasKubeshopGitURI), + }, + }) + + if err != nil { + log.DefaultLogger.Debugf("sending run test workflow telemetry event error", "error", err) + } else { + log.DefaultLogger.Debugf("sending run test workflow telemetry event", "output", out) + } +} From 40e4ee96658f8cbe5057526800c734b3edb1d3ff Mon Sep 17 00:00:00 2001 From: Dejan Zele Pejchev Date: Wed, 10 Jul 2024 11:02:34 +0200 Subject: [PATCH 02/16] TKC-2161: skip tls verification when saving logs if STORAGE_SKIP_VERIFY is true (#5627) --- cmd/api-server/main.go | 6 +++++- pkg/cloud/data/testworkflow/output.go | 25 ++++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 69eb4a57c87..73bbf58d5aa 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -273,7 +273,11 @@ func main() { configRepository = cloudconfig.NewCloudResultRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey) // Pro edition only (tcl protected code) testWorkflowResultsRepository = cloudtestworkflow.NewCloudRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey) - testWorkflowOutputRepository = cloudtestworkflow.NewCloudOutputRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey) + var opts []cloudtestworkflow.Option + if cfg.StorageSkipVerify { + opts = append(opts, cloudtestworkflow.WithSkipVerify()) + } + testWorkflowOutputRepository = cloudtestworkflow.NewCloudOutputRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey, opts...) triggerLeaseBackend = triggers.NewAcquireAlwaysLeaseBackend() artifactStorage = cloudartifacts.NewCloudArtifactsStorage(grpcClient, grpcConn, cfg.TestkubeProAPIKey) } else { diff --git a/pkg/cloud/data/testworkflow/output.go b/pkg/cloud/data/testworkflow/output.go index 55102d42eac..5848b3e0e08 100644 --- a/pkg/cloud/data/testworkflow/output.go +++ b/pkg/cloud/data/testworkflow/output.go @@ -3,6 +3,7 @@ package testworkflow import ( "bytes" "context" + "crypto/tls" "io" "net/http" @@ -18,11 +19,25 @@ import ( var _ testworkflow.OutputRepository = (*CloudOutputRepository)(nil) type CloudOutputRepository struct { - executor executor.Executor + executor executor.Executor + httpClient *http.Client } -func NewCloudOutputRepository(client cloud.TestKubeCloudAPIClient, grpcConn *grpc.ClientConn, apiKey string) *CloudOutputRepository { - return &CloudOutputRepository{executor: executor.NewCloudGRPCExecutor(client, grpcConn, apiKey)} +type Option func(*CloudOutputRepository) + +func WithSkipVerify() Option { + return func(r *CloudOutputRepository) { + transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + r.httpClient.Transport = transport + } +} + +func NewCloudOutputRepository(client cloud.TestKubeCloudAPIClient, grpcConn *grpc.ClientConn, apiKey string, opts ...Option) *CloudOutputRepository { + r := &CloudOutputRepository{executor: executor.NewCloudGRPCExecutor(client, grpcConn, apiKey), httpClient: http.DefaultClient} + for _, opt := range opts { + opt(r) + } + return r } // PresignSaveLog builds presigned storage URL to save the output in Cloud @@ -59,7 +74,7 @@ func (r *CloudOutputRepository) SaveLog(ctx context.Context, id, workflowName st if err != nil { return err } - res, err := http.DefaultClient.Do(req) + res, err := r.httpClient.Do(req) if err != nil { return errors.Wrap(err, "failed to save file in cloud storage") } @@ -79,7 +94,7 @@ func (r *CloudOutputRepository) ReadLog(ctx context.Context, id, workflowName st if err != nil { return nil, err } - res, err := http.DefaultClient.Do(req) + res, err := r.httpClient.Do(req) if err != nil { return nil, errors.Wrap(err, "failed to get file from cloud storage") } From b7f81186c5fe98b49dde56807c68ae0a2f8ea3db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:11:51 +0300 Subject: [PATCH 03/16] build(deps): bump anchore/sbom-action from 0.16.0 to 0.16.1 (#5649) Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.16.0 to 0.16.1. - [Release notes](https://github.com/anchore/sbom-action/releases) - [Commits](https://github.com/anchore/sbom-action/compare/v0.16.0...v0.16.1) --- updated-dependencies: - dependency-name: anchore/sbom-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../docker-build-api-executors-tag.yaml | 22 +++++++++---------- .github/workflows/release-dev.yaml | 2 +- .github/workflows/release.yaml | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docker-build-api-executors-tag.yaml b/.github/workflows/docker-build-api-executors-tag.yaml index f933db83cb6..863162ab79c 100644 --- a/.github/workflows/docker-build-api-executors-tag.yaml +++ b/.github/workflows/docker-build-api-executors-tag.yaml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up Docker Buildx id: buildx @@ -94,7 +94,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up Docker Buildx id: buildx @@ -178,7 +178,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -249,7 +249,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -317,7 +317,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -442,7 +442,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -510,7 +510,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -669,7 +669,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -741,7 +741,7 @@ jobs: uses: docker/setup-qemu-action@v3 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up Docker Buildx id: buildx @@ -790,7 +790,7 @@ jobs: uses: actions/checkout@v4 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -863,7 +863,7 @@ jobs: fetch-depth: 0 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 596b21e2ea0..f51de24cecd 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -131,7 +131,7 @@ jobs: with: fetch-depth: 0 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Download Artifacts for Linux uses: actions/download-artifact@master with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0e539be9518..1682a69ee20 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -121,7 +121,7 @@ jobs: with: fetch-depth: 0 - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.16.0 + - uses: anchore/sbom-action/download-syft@v0.16.1 - name: Download Artifacts for Linux uses: actions/download-artifact@master with: From 14437375c69d328bfbc37bb5791cd688e09c5868 Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Wed, 10 Jul 2024 15:24:20 +0200 Subject: [PATCH 04/16] feat(performance): improve Log Processing performance (#5647) * feat(performance): buffer the logs sent from the container, to avoid sending message for each line * feat(performance): batch Test Workflow's result updates * fix(testworkflows): handle getting long container logs after the log rotation happens @see {@link https://stackoverflow.com/a/68673451} * feat(testworkflows): optimize reading timestamp from Kubernetes logs * feat(testworkflows): optimize buffering logs * feat(testworkflows): use native channel instead of heavier Channels for WatchInstrumentedPod * feat(testworkflows): increase buffer size for logs buffering --- .../testworkflowcontroller/channel.go | 5 + .../testworkflowcontroller/controller.go | 8 +- .../testworkflowcontroller/logs.go | 361 +++++++++++++++--- .../testworkflowcontroller/logs_test.go | 48 ++- .../testworkflowcontroller/notifier.go | 106 ++++- .../testworkflowcontroller/utils.go | 2 +- .../watchinstrumentedpod.go | 11 +- .../testworkflowexecutor/executor.go | 2 +- 8 files changed, 463 insertions(+), 80 deletions(-) diff --git a/pkg/testworkflows/testworkflowcontroller/channel.go b/pkg/testworkflows/testworkflowcontroller/channel.go index e9da67db0f9..00702e80c00 100644 --- a/pkg/testworkflows/testworkflowcontroller/channel.go +++ b/pkg/testworkflows/testworkflowcontroller/channel.go @@ -20,6 +20,7 @@ type Channel[T any] interface { Channel() <-chan ChannelMessage[T] Close() Done() <-chan struct{} + CtxErr() error } type channel[T any] struct { @@ -168,3 +169,7 @@ func (c *channel[T]) Close() { func (c *channel[T]) Done() <-chan struct{} { return c.ctx.Done() } + +func (c *channel[T]) CtxErr() error { + return c.ctx.Err() +} diff --git a/pkg/testworkflows/testworkflowcontroller/controller.go b/pkg/testworkflows/testworkflowcontroller/controller.go index bc074e216e6..ef7b2cbc52c 100644 --- a/pkg/testworkflows/testworkflowcontroller/controller.go +++ b/pkg/testworkflows/testworkflowcontroller/controller.go @@ -212,7 +212,7 @@ func (c *controller) StopController() { } func (c *controller) Watch(parentCtx context.Context) <-chan ChannelMessage[Notification] { - w, err := WatchInstrumentedPod(parentCtx, c.clientSet, c.signature, c.scheduledAt, c.pod, c.podEvents, WatchInstrumentedPodOptions{ + ch, err := WatchInstrumentedPod(parentCtx, c.clientSet, c.signature, c.scheduledAt, c.pod, c.podEvents, WatchInstrumentedPodOptions{ JobEvents: c.jobEvents, Job: c.job, }) @@ -222,7 +222,7 @@ func (c *controller) Watch(parentCtx context.Context) <-chan ChannelMessage[Noti v.Close() return v.Channel() } - return w.Channel() + return ch } // TODO: Make it actually light @@ -281,7 +281,7 @@ func (c *controller) Logs(parentCtx context.Context, follow bool) io.Reader { case <-c.podEvents.Peek(parentCtx): case <-alignTimeoutCh: } - w, err := WatchInstrumentedPod(parentCtx, c.clientSet, c.signature, c.scheduledAt, c.pod, c.podEvents, WatchInstrumentedPodOptions{ + ch, err := WatchInstrumentedPod(parentCtx, c.clientSet, c.signature, c.scheduledAt, c.pod, c.podEvents, WatchInstrumentedPodOptions{ JobEvents: c.jobEvents, Job: c.job, Follow: common.Ptr(follow), @@ -289,7 +289,7 @@ func (c *controller) Logs(parentCtx context.Context, follow bool) io.Reader { if err != nil { return } - for v := range w.Channel() { + for v := range ch { if v.Error == nil && v.Value.Log != "" && !v.Value.Temporary { if ref != v.Value.Ref && v.Value.Ref != "" { ref = v.Value.Ref diff --git a/pkg/testworkflows/testworkflowcontroller/logs.go b/pkg/testworkflows/testworkflowcontroller/logs.go index 15d9072d191..7fd97ef4bdc 100644 --- a/pkg/testworkflows/testworkflowcontroller/logs.go +++ b/pkg/testworkflows/testworkflowcontroller/logs.go @@ -2,20 +2,31 @@ package testworkflowcontroller import ( "bufio" + "bytes" "context" - "errors" "io" "strings" + "sync" "time" + "unsafe" - errors2 "github.com/pkg/errors" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "github.com/kubeshop/testkube/cmd/testworkflow-init/data" + "github.com/kubeshop/testkube/internal/common" + "github.com/kubeshop/testkube/pkg/log" "github.com/kubeshop/testkube/pkg/utils" ) +const ( + FlushLogMaxSize = 100_000 + FlushBufferSize = 65_536 + FlushLogTime = 100 * time.Millisecond +) + type Comment struct { Time time.Time Hint *data.Instruction @@ -29,63 +40,220 @@ type ContainerLog struct { Output *data.Instruction } -func WatchContainerLogs(ctx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, bufferSize int, follow bool, pod Channel[*corev1.Pod]) Channel[ContainerLog] { +// getContainerLogsStream is getting logs stream, and tries to reinitialize the stream on EOF. +// EOF may happen not only on the actual container end, but also in case of the log rotation. +// @see {@link https://stackoverflow.com/a/68673451} +func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, pod Channel[*corev1.Pod], since *time.Time) (io.Reader, error) { + // Fail immediately if the context is finished + if ctx.Err() != nil { + return nil, ctx.Err() + } + + // Build Kubernetes structure for time + var sinceTime *metav1.Time + if since != nil { + sinceTime = &metav1.Time{Time: *since} + } + + // Create logs stream request + req := clientSet.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ + Container: containerName, + Follow: true, + Timestamps: true, + SinceTime: sinceTime, + }) + var err error + var stream io.ReadCloser + for { + stream, err = req.Stream(ctx) + if err != nil { + // The container is not necessarily already started when Started event is received + if !strings.Contains(err.Error(), "is waiting to start") { + return nil, err + } + p := <-pod.Peek(ctx) + if p == nil { + return bytes.NewReader(nil), io.EOF + } + containerDone := IsPodDone(p) + for i := range p.Status.InitContainerStatuses { + if p.Status.InitContainerStatuses[i].Name == containerName { + if p.Status.InitContainerStatuses[i].State.Terminated != nil { + containerDone = true + break + } + } + } + for i := range p.Status.ContainerStatuses { + if p.Status.ContainerStatuses[i].Name == containerName { + if p.Status.ContainerStatuses[i].State.Terminated != nil { + containerDone = true + break + } + } + } + + if containerDone { + return bytes.NewReader(nil), io.EOF + } + continue + } + break + } + return stream, nil +} + +func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, bufferSize int, pod Channel[*corev1.Pod]) Channel[ContainerLog] { + ctx, ctxCancel := context.WithCancel(parentCtx) w := newChannel[ContainerLog](ctx, bufferSize) go func() { - defer w.Close() + <-w.Done() + ctxCancel() + }() + + go func() { + defer ctxCancel() var err error + var since *time.Time + // Create logs stream request - req := clientSet.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ - Follow: follow, - Timestamps: true, - Container: containerName, - }) - var stream io.ReadCloser - for { - stream, err = req.Stream(ctx) + stream, err := getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, pod, since) + hadAnyContent := false + if err == io.EOF { + return + } else if err != nil { + w.Error(err) + return + } + + // Build a buffer for logs to avoid scheduling Log notification for each write + var logBufferLog bytes.Buffer + var logBufferTs time.Time + var logBufferMu sync.Mutex + var logBufferCh = make(chan struct{}, 1) + unsafeFlushLogBuffer := func() { + if logBufferLog.Len() == 0 || w.CtxErr() != nil { + return + } + message := make([]byte, logBufferLog.Len()) + _, err := logBufferLog.Read(message) if err != nil { - // The container is not necessarily already started when Started event is received - if !strings.Contains(err.Error(), "is waiting to start") { - w.Error(err) - return - } - p := <-pod.Peek(ctx) - if p != nil && IsPodDone(p) { - w.Error(errors.New("pod is finished and there are no logs for this container")) + log.DefaultLogger.Errorf("failed to read log buffer: %s/%s", podName, containerName) + return + } + w.Send(ContainerLog{Time: logBufferTs, Log: message}) + } + flushLogBuffer := func() { + logBufferMu.Lock() + defer logBufferMu.Unlock() + unsafeFlushLogBuffer() + } + appendLog := func(ts time.Time, log ...[]byte) { + logBufferMu.Lock() + defer logBufferMu.Unlock() + + initialLogLen := logBufferLog.Len() + if initialLogLen == 0 { + logBufferTs = ts + } + for i := range log { + logBufferLog.Write(log[i]) + } + + finalLogLen := logBufferLog.Len() + flushable := finalLogLen > FlushLogMaxSize + if flushable { + unsafeFlushLogBuffer() + } + + // Inform the flushing worker about a new log to flush. + // Do it only when it's not scheduled + if initialLogLen == 0 || flushable { + select { + case logBufferCh <- struct{}{}: + default: } - continue } - break } + // Flush the log automatically after 100ms + bufferCtx, bufferCtxCancel := context.WithCancel(ctx) + defer bufferCtxCancel() go func() { - <-w.Done() - _ = stream.Close() + t := time.NewTimer(FlushLogTime) + for { + t.Stop() + + if bufferCtx.Err() != nil { + return + } + + logLen := logBufferLog.Len() + if logLen == 0 { + select { + case <-bufferCtx.Done(): + return + case <-logBufferCh: + continue + } + } + + t.Reset(FlushLogTime) + select { + case <-bufferCtx.Done(): + if !t.Stop() { + <-t.C + } + return + case <-t.C: + flushLogBuffer() + case <-logBufferCh: + continue + } + } }() + // Flush the rest of logs if it is closed + defer flushLogBuffer() + // Parse and return the logs - reader := bufio.NewReader(stream) - var tsPrefix, tmpTsPrefix []byte + reader := bufio.NewReaderSize(stream, FlushBufferSize) + tsReader := newTimestampReader() isNewLine := false isStarted := false - var ts, tmpTs time.Time for { var prepend []byte // Read next timestamp - tmpTs, tmpTsPrefix, err = ReadTimestamp(reader) + err = tsReader.Read(reader) if err == nil { - ts = tmpTs - tsPrefix = tmpTsPrefix + // Strip older logs - SinceTime in Kubernetes logs is ignoring milliseconds precision + if since != nil && since.After(tsReader.ts) { + _, _ = utils.ReadLongLine(reader) + continue + } + hadAnyContent = true } else if err == io.EOF { - return + if !hadAnyContent { + return + } + // Reinitialize logs stream + since = common.Ptr(tsReader.ts.Add(1)) + stream, err = getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, pod, since) + if err != nil { + return + } + reader.Reset(stream) + hadAnyContent = false + continue } else { // Edge case: Kubernetes may send critical errors without timestamp (like ionotify) - if len(tmpTsPrefix) > 0 { - prepend = tmpTsPrefix + if len(tsReader.Prefix()) > 0 { + prepend = bytes.Clone(tsReader.Prefix()) } + flushLogBuffer() w.Error(err) } @@ -104,33 +272,34 @@ func WatchContainerLogs(ctx context.Context, clientSet kubernetes.Interface, nam if err == nil && instruction != nil { isNewLine = false hadComment = true - log := ContainerLog{Time: ts} + log := ContainerLog{Time: tsReader.ts} if isHint { log.Hint = instruction } else { log.Output = instruction } + flushLogBuffer() w.Send(log) } // Append as regular log if expected if !hadComment { if !isStarted { - line = append(tsPrefix, line...) + appendLog(tsReader.ts, tsReader.Prefix(), line) isStarted = true } else if isNewLine { - line = append(append([]byte("\n"), tsPrefix...), line...) + appendLog(tsReader.ts, []byte("\n"), tsReader.Prefix(), line) } - w.Send(ContainerLog{Time: ts, Log: line}) isNewLine = true } } else if isStarted { - w.Send(ContainerLog{Time: ts, Log: append([]byte("\n"), tsPrefix...)}) + appendLog(tsReader.ts, []byte("\n"), tsReader.Prefix()) } // Handle the error if err != nil { if err != io.EOF { + flushLogBuffer() w.Error(err) } return @@ -141,31 +310,111 @@ func WatchContainerLogs(ctx context.Context, clientSet kubernetes.Interface, nam return w } -func ReadTimestamp(reader *bufio.Reader) (time.Time, []byte, error) { - tsPrefix := make([]byte, 31, 35) // 30 bytes for timestamp + 1 byte for space + 4 additional bytes for non-UTC timezone - count, err := io.ReadFull(reader, tsPrefix) +var ( + ErrInvalidTimestamp = errors.New("invalid timestamp") +) + +type timestampReader struct { + buffer []byte + bytes int + ts time.Time + utc *bool +} + +func newTimestampReader() *timestampReader { + return ×tampReader{ + buffer: make([]byte, 31, 36), // 30 bytes for timestamp + 1 byte for space + 5 additional bytes for non-UTC timezone + } +} + +func (t *timestampReader) Prefix() []byte { + return t.buffer[:t.bytes] +} + +// read is initial operation for reading the timestamp, +// that is the slowest one, but also detects the timestamp format. +// It's meant to be executed just once, for performance reasons. +func (t *timestampReader) read(reader *bufio.Reader) error { + // Read the possible timestamp slice + read, err := io.ReadFull(reader, t.buffer[:31]) + t.bytes = read if err != nil { - return time.Time{}, nil, err + return err } - if count < 31 { - return time.Time{}, nil, io.EOF + + // Detect the timezone format and adjust the reader if needed + utc := t.buffer[29] == 'Z' + t.utc = &utc + if !utc && len(t.buffer) < 35 { + // Increase capacity to store the +00:00 time + t.buffer = append(t.buffer, make([]byte, 5)...) + + // Read the missing part + read, err = io.ReadFull(reader, t.buffer[31:]) + t.bytes += read + if err != nil { + return err + } } - var ts time.Time - // Handle non-UTC timezones - if tsPrefix[29] == '+' { - tsSuffix := make([]byte, 5) - count, err = io.ReadFull(reader, tsSuffix) + + // Compute the timestamp + if utc { + ts, err := time.Parse(time.RFC3339Nano, unsafe.String(&t.buffer[0], 30)) if err != nil { - return time.Time{}, nil, err + return ErrInvalidTimestamp } - if count < 5 { - return time.Time{}, nil, io.EOF + t.ts = ts + } else { + ts, err := time.Parse(time.RFC3339Nano, unsafe.String(&t.buffer[0], 35)) + if err != nil { + return ErrInvalidTimestamp } - tsPrefix = append(tsPrefix, tsSuffix...) + t.ts = ts.UTC() } - ts, err = time.Parse(KubernetesTimezoneLogTimeFormat, string(tsPrefix[0:len(tsPrefix)-1])) + return nil +} + +// readUTC is optimized operation for reading the UTC timestamp (Z). +func (t *timestampReader) readUTC(reader *bufio.Reader) error { + // Read the possible timestamp slice + read, err := io.ReadFull(reader, t.buffer) + t.bytes = read if err != nil { - return time.Time{}, tsPrefix, errors2.Wrap(err, "parsing timestamp") + return err + } + + // Compute the timestamp + ts, err := time.Parse(time.RFC3339Nano, unsafe.String(&t.buffer[0], 30)) + if err != nil { + return ErrInvalidTimestamp + } + t.ts = ts + return nil +} + +// readNonUTC is optimized operation for reading the non-UTC timestamp (+00:00). +func (t *timestampReader) readNonUTC(reader *bufio.Reader) error { + // Read the possible timestamp slice + read, err := io.ReadFull(reader, t.buffer) + t.bytes = read + if err != nil { + return err + } + + // Compute the timestamp + ts, err := time.Parse(time.RFC3339Nano, unsafe.String(&t.buffer[0], 35)) + if err != nil { + return ErrInvalidTimestamp + } + t.ts = ts.UTC() + return nil +} + +func (t *timestampReader) Read(reader *bufio.Reader) error { + if t.utc == nil { + return t.read(reader) + } else if *t.utc { + return t.readUTC(reader) } - return ts.UTC(), tsPrefix, nil + return t.readNonUTC(reader) } diff --git a/pkg/testworkflows/testworkflowcontroller/logs_test.go b/pkg/testworkflows/testworkflowcontroller/logs_test.go index f181034773b..f9e4f6c64e9 100644 --- a/pkg/testworkflows/testworkflowcontroller/logs_test.go +++ b/pkg/testworkflows/testworkflowcontroller/logs_test.go @@ -10,26 +10,58 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_ReadTimestamp_UTC(t *testing.T) { +func Test_ReadTimestamp_UTC_Initial(t *testing.T) { + reader := newTimestampReader() prefix := "2024-06-07T12:41:49.037275300Z " message := "some-message" buf := bufio.NewReader(bytes.NewBufferString(prefix + message)) - ts, byt, err := ReadTimestamp(buf) + err := reader.Read(buf) rest, _ := io.ReadAll(buf) assert.NoError(t, err) - assert.Equal(t, []byte(prefix), byt) + assert.Equal(t, []byte(prefix), reader.Prefix()) assert.Equal(t, []byte(message), rest) - assert.Equal(t, time.Date(2024, 6, 7, 12, 41, 49, 37275300, time.UTC), ts) + assert.Equal(t, time.Date(2024, 6, 7, 12, 41, 49, 37275300, time.UTC), reader.ts) } -func Test_ReadTimestamp_NonUTC(t *testing.T) { +func Test_ReadTimestamp_NonUTC_Initial(t *testing.T) { + reader := newTimestampReader() prefix := "2024-06-07T15:41:49.037275300+03:00 " message := "some-message" buf := bufio.NewReader(bytes.NewBufferString(prefix + message)) - ts, byt, err := ReadTimestamp(buf) + err := reader.Read(buf) rest, _ := io.ReadAll(buf) assert.NoError(t, err) - assert.Equal(t, []byte(prefix), byt) + assert.Equal(t, []byte(prefix), reader.Prefix()) assert.Equal(t, []byte(message), rest) - assert.Equal(t, time.Date(2024, 6, 7, 12, 41, 49, 37275300, time.UTC), ts) + assert.Equal(t, time.Date(2024, 6, 7, 12, 41, 49, 37275300, time.UTC), reader.ts) +} + +func Test_ReadTimestamp_UTC_Recurring(t *testing.T) { + reader := newTimestampReader() + prefix := "2024-06-07T12:41:49.037275300Z " + message := "some-message" + buf := bufio.NewReader(bytes.NewBufferString(prefix + prefix + message)) + err1 := reader.Read(buf) + err2 := reader.Read(buf) + rest, _ := io.ReadAll(buf) + assert.NoError(t, err1) + assert.NoError(t, err2) + assert.Equal(t, []byte(prefix), reader.Prefix()) + assert.Equal(t, []byte(message), rest) + assert.Equal(t, time.Date(2024, 6, 7, 12, 41, 49, 37275300, time.UTC), reader.ts) +} + +func Test_ReadTimestamp_NonUTC_Recurring(t *testing.T) { + reader := newTimestampReader() + prefix := "2024-06-07T15:41:49.037275300+03:00 " + message := "some-message" + buf := bufio.NewReader(bytes.NewBufferString(prefix + prefix + message)) + err1 := reader.Read(buf) + err2 := reader.Read(buf) + rest, _ := io.ReadAll(buf) + assert.NoError(t, err1) + assert.NoError(t, err2) + assert.Equal(t, []byte(prefix), reader.Prefix()) + assert.Equal(t, []byte(message), rest) + assert.Equal(t, time.Date(2024, 6, 7, 12, 41, 49, 37275300, time.UTC), reader.ts) } diff --git a/pkg/testworkflows/testworkflowcontroller/notifier.go b/pkg/testworkflows/testworkflowcontroller/notifier.go index f5b6f145669..bb86d9cdb23 100644 --- a/pkg/testworkflows/testworkflowcontroller/notifier.go +++ b/pkg/testworkflows/testworkflowcontroller/notifier.go @@ -3,6 +3,7 @@ package testworkflowcontroller import ( "context" "fmt" + "sync" "time" "github.com/kubeshop/testkube/cmd/testworkflow-init/data" @@ -12,12 +13,38 @@ import ( "github.com/kubeshop/testkube/pkg/ui" ) +const ( + FlushResultTime = 50 * time.Millisecond + FlushResultMaxTime = 100 * time.Millisecond +) + type notifier struct { - watcher *channel[Notification] + ctx context.Context + ch chan ChannelMessage[Notification] result testkube.TestWorkflowResult sig []testkube.TestWorkflowSignature scheduledAt time.Time lastTs map[string]time.Time + + resultMu sync.Mutex + resultCh chan struct{} + resultScheduled bool +} + +func (n *notifier) send(value Notification) { + // Ignore when the channel is already closed + defer func() { + recover() + }() + n.ch <- ChannelMessage[Notification]{Value: value} +} + +func (n *notifier) error(err error) { + // Ignore when the channel is already closed + defer func() { + recover() + }() + n.ch <- ChannelMessage[Notification]{Error: err} } func (n *notifier) GetLastTimestamp(ref string) time.Time { @@ -40,13 +67,69 @@ func (n *notifier) RegisterTimestamp(ref string, t time.Time) { } } +func (n *notifier) Flush() { + n.resultMu.Lock() + defer n.resultMu.Unlock() + if !n.resultScheduled { + return + } + n.send(Notification{Timestamp: n.result.LatestTimestamp(), Result: n.result.Clone()}) + n.resultScheduled = false +} + +func (n *notifier) scheduleFlush() { + n.resultMu.Lock() + defer n.resultMu.Unlock() + + // Inform existing scheduler about the next result + if n.resultScheduled { + select { + case n.resultCh <- struct{}{}: + default: + } + return + } + + // Run the scheduler + n.resultScheduled = true + go func() { + flushTimer := time.NewTimer(FlushResultMaxTime) + flushTimerEnabled := false + + for { + if n.ctx.Err() != nil { + return + } + + select { + case <-n.ctx.Done(): + n.Flush() + return + case <-flushTimer.C: + n.Flush() + flushTimerEnabled = false + case <-time.After(FlushResultTime): + n.Flush() + flushTimerEnabled = false + case <-n.resultCh: + if !flushTimerEnabled { + flushTimerEnabled = true + flushTimer.Reset(FlushResultMaxTime) + } + continue + } + } + }() +} + func (n *notifier) Raw(ref string, ts time.Time, message string, temporary bool) { if message != "" { if ref == InitContainerName { ref = "" } // TODO: use timestamp from the message too for lastTs? - n.watcher.Send(Notification{ + n.Flush() + n.send(Notification{ Timestamp: ts.UTC(), Log: message, Ref: ref, @@ -63,7 +146,7 @@ func (n *notifier) Log(ref string, ts time.Time, message string) { } func (n *notifier) Error(err error) { - n.watcher.Error(err) + n.error(err) } func (n *notifier) Event(ref string, ts time.Time, level, reason, message string) { @@ -92,7 +175,7 @@ func (n *notifier) recompute() { func (n *notifier) emit() { n.recompute() - n.watcher.Send(Notification{Timestamp: n.result.LatestTimestamp(), Result: n.result.Clone()}) + n.scheduleFlush() } func (n *notifier) queue(ts time.Time) { @@ -184,7 +267,8 @@ func (n *notifier) Output(ref string, ts time.Time, output *data.Instruction) { return } n.RegisterTimestamp(ref, ts) - n.watcher.Send(Notification{Timestamp: ts.UTC(), Ref: ref, Output: output}) + n.Flush() + n.send(Notification{Timestamp: ts.UTC(), Ref: ref, Output: output}) } func (n *notifier) Finish(ts time.Time) { @@ -270,11 +354,21 @@ func newNotifier(ctx context.Context, signature []testworkflowprocessor.Signatur } result.Recompute(sig, scheduledAt) + ch := make(chan ChannelMessage[Notification]) + + go func() { + <-ctx.Done() + close(ch) + }() + return ¬ifier{ - watcher: newChannel[Notification](ctx, 0), + ch: ch, + ctx: ctx, sig: sig, scheduledAt: scheduledAt, result: result, lastTs: make(map[string]time.Time), + + resultCh: make(chan struct{}, 1), } } diff --git a/pkg/testworkflows/testworkflowcontroller/utils.go b/pkg/testworkflows/testworkflowcontroller/utils.go index 77bb22bc4ef..ad54220b0ca 100644 --- a/pkg/testworkflows/testworkflowcontroller/utils.go +++ b/pkg/testworkflows/testworkflowcontroller/utils.go @@ -12,7 +12,7 @@ import ( ) const ( - KubernetesLogTimeFormat = "2006-01-02T15:04:05.000000000Z" + KubernetesLogTimeFormat = "2006-01-02T15:04:05.999999999Z" KubernetesTimezoneLogTimeFormat = KubernetesLogTimeFormat + "07:00" ) diff --git a/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go b/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go index 7e7feafeda7..c98dd7b0678 100644 --- a/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go +++ b/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go @@ -27,7 +27,7 @@ type WatchInstrumentedPodOptions struct { Follow *bool } -func WatchInstrumentedPod(parentCtx context.Context, clientSet kubernetes.Interface, signature []testworkflowprocessor.Signature, scheduledAt time.Time, pod Channel[*corev1.Pod], podEvents Channel[*corev1.Event], opts WatchInstrumentedPodOptions) (Channel[Notification], error) { +func WatchInstrumentedPod(parentCtx context.Context, clientSet kubernetes.Interface, signature []testworkflowprocessor.Signature, scheduledAt time.Time, pod Channel[*corev1.Pod], podEvents Channel[*corev1.Event], opts WatchInstrumentedPodOptions) (<-chan ChannelMessage[Notification], error) { // Avoid missing data if pod == nil { return nil, errors.New("pod watcher is required") @@ -42,7 +42,10 @@ func WatchInstrumentedPod(parentCtx context.Context, clientSet kubernetes.Interf // Start watching go func() { - defer ctxCancel() + defer func() { + s.Flush() + ctxCancel() + }() // Watch for the basic initialization warnings for v := range state.PreStart("") { @@ -99,7 +102,7 @@ func WatchInstrumentedPod(parentCtx context.Context, clientSet kubernetes.Interf // Watch the container logs follow := common.ResolvePtr(opts.Follow, true) && !state.IsFinished(ref) - for v := range WatchContainerLogs(ctx, clientSet, podObj.Namespace, podObj.Name, ref, 10, follow, pod).Channel() { + for v := range WatchContainerLogs(ctx, clientSet, podObj.Namespace, podObj.Name, ref, 10, pod).Channel() { if v.Error != nil { s.Error(v.Error) } else if v.Value.Output != nil { @@ -177,7 +180,7 @@ func WatchInstrumentedPod(parentCtx context.Context, clientSet kubernetes.Interf } }() - return s.watcher, nil + return s.ch, nil } func maxTime(times ...time.Time) time.Time { diff --git a/pkg/testworkflows/testworkflowexecutor/executor.go b/pkg/testworkflows/testworkflowexecutor/executor.go index d3f00502c13..f7c02dff686 100644 --- a/pkg/testworkflows/testworkflowexecutor/executor.go +++ b/pkg/testworkflows/testworkflowexecutor/executor.go @@ -313,7 +313,7 @@ func (e *executor) Control(ctx context.Context, testWorkflow *testworkflowsv1.Te e.metrics.IncAndObserveExecuteTestWorkflow(*execution, e.dashboardURI) - e.updateStatus(testWorkflow, execution, testWorkflowExecution) + e.updateStatus(testWorkflow, execution, testWorkflowExecution) // TODO: Consider if it is needed err = testworkflowcontroller.Cleanup(ctx, e.clientSet, execution.GetNamespace(e.namespace), execution.Id) if err != nil { log.DefaultLogger.Errorw("failed to cleanup TestWorkflow resources", "id", execution.Id, "error", err) From f44d150a29ae6439c7943bfe3480cc40696e54b4 Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Thu, 11 Jul 2024 10:59:18 +0200 Subject: [PATCH 05/16] fix(testworkflows): displaying events (#5652) --- pkg/testworkflows/testworkflowcontroller/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/testworkflows/testworkflowcontroller/utils.go b/pkg/testworkflows/testworkflowcontroller/utils.go index ad54220b0ca..77bb22bc4ef 100644 --- a/pkg/testworkflows/testworkflowcontroller/utils.go +++ b/pkg/testworkflows/testworkflowcontroller/utils.go @@ -12,7 +12,7 @@ import ( ) const ( - KubernetesLogTimeFormat = "2006-01-02T15:04:05.999999999Z" + KubernetesLogTimeFormat = "2006-01-02T15:04:05.000000000Z" KubernetesTimezoneLogTimeFormat = KubernetesLogTimeFormat + "07:00" ) From aa1326ba6f9611e92a98772e994f92944cc382c9 Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Thu, 11 Jul 2024 14:21:17 +0200 Subject: [PATCH 06/16] fix(testworkflows): storing logs for the services (#5656) --- pkg/testworkflows/testworkflowcontroller/logs.go | 10 +++++----- pkg/testworkflows/testworkflowcontroller/utils.go | 2 +- .../testworkflowcontroller/watchinstrumentedpod.go | 2 +- pkg/testworkflows/testworkflowprocessor/container.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/testworkflows/testworkflowcontroller/logs.go b/pkg/testworkflows/testworkflowcontroller/logs.go index 7fd97ef4bdc..b72b8b36807 100644 --- a/pkg/testworkflows/testworkflowcontroller/logs.go +++ b/pkg/testworkflows/testworkflowcontroller/logs.go @@ -43,7 +43,7 @@ type ContainerLog struct { // getContainerLogsStream is getting logs stream, and tries to reinitialize the stream on EOF. // EOF may happen not only on the actual container end, but also in case of the log rotation. // @see {@link https://stackoverflow.com/a/68673451} -func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, pod Channel[*corev1.Pod], since *time.Time) (io.Reader, error) { +func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, follow bool, pod Channel[*corev1.Pod], since *time.Time) (io.Reader, error) { // Fail immediately if the context is finished if ctx.Err() != nil { return nil, ctx.Err() @@ -58,7 +58,7 @@ func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, // Create logs stream request req := clientSet.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ Container: containerName, - Follow: true, + Follow: follow, Timestamps: true, SinceTime: sinceTime, }) @@ -103,7 +103,7 @@ func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, return stream, nil } -func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, bufferSize int, pod Channel[*corev1.Pod]) Channel[ContainerLog] { +func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, follow bool, bufferSize int, pod Channel[*corev1.Pod]) Channel[ContainerLog] { ctx, ctxCancel := context.WithCancel(parentCtx) w := newChannel[ContainerLog](ctx, bufferSize) @@ -119,7 +119,7 @@ func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interfac var since *time.Time // Create logs stream request - stream, err := getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, pod, since) + stream, err := getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, follow, pod, since) hadAnyContent := false if err == io.EOF { return @@ -241,7 +241,7 @@ func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interfac } // Reinitialize logs stream since = common.Ptr(tsReader.ts.Add(1)) - stream, err = getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, pod, since) + stream, err = getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, follow, pod, since) if err != nil { return } diff --git a/pkg/testworkflows/testworkflowcontroller/utils.go b/pkg/testworkflows/testworkflowcontroller/utils.go index ad54220b0ca..77bb22bc4ef 100644 --- a/pkg/testworkflows/testworkflowcontroller/utils.go +++ b/pkg/testworkflows/testworkflowcontroller/utils.go @@ -12,7 +12,7 @@ import ( ) const ( - KubernetesLogTimeFormat = "2006-01-02T15:04:05.999999999Z" + KubernetesLogTimeFormat = "2006-01-02T15:04:05.000000000Z" KubernetesTimezoneLogTimeFormat = KubernetesLogTimeFormat + "07:00" ) diff --git a/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go b/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go index c98dd7b0678..1ae190e2f08 100644 --- a/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go +++ b/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go @@ -102,7 +102,7 @@ func WatchInstrumentedPod(parentCtx context.Context, clientSet kubernetes.Interf // Watch the container logs follow := common.ResolvePtr(opts.Follow, true) && !state.IsFinished(ref) - for v := range WatchContainerLogs(ctx, clientSet, podObj.Namespace, podObj.Name, ref, 10, pod).Channel() { + for v := range WatchContainerLogs(ctx, clientSet, podObj.Namespace, podObj.Name, ref, follow, 10, pod).Channel() { if v.Error != nil { s.Error(v.Error) } else if v.Value.Output != nil { diff --git a/pkg/testworkflows/testworkflowprocessor/container.go b/pkg/testworkflows/testworkflowprocessor/container.go index 7566a3a1061..1546965341b 100644 --- a/pkg/testworkflows/testworkflowprocessor/container.go +++ b/pkg/testworkflows/testworkflowprocessor/container.go @@ -468,7 +468,7 @@ func (c *container) Resolve(m ...expressions.Machine) error { } env := c.Env() name = name[4:] - for i := range env { + for i := len(env) - 1; i >= 0; i-- { if env[i].Name == name && env[i].ValueFrom == nil { value, err := expressions.EvalTemplate(env[i].Value) if err == nil { From 1db9d5652eed2558424d4f1491e4353f78da3655 Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Thu, 11 Jul 2024 14:21:17 +0200 Subject: [PATCH 07/16] fix(testworkflows): storing logs for the services (#5656) --- pkg/testworkflows/testworkflowcontroller/logs.go | 10 +++++----- .../testworkflowcontroller/watchinstrumentedpod.go | 2 +- pkg/testworkflows/testworkflowprocessor/container.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/testworkflows/testworkflowcontroller/logs.go b/pkg/testworkflows/testworkflowcontroller/logs.go index 7fd97ef4bdc..b72b8b36807 100644 --- a/pkg/testworkflows/testworkflowcontroller/logs.go +++ b/pkg/testworkflows/testworkflowcontroller/logs.go @@ -43,7 +43,7 @@ type ContainerLog struct { // getContainerLogsStream is getting logs stream, and tries to reinitialize the stream on EOF. // EOF may happen not only on the actual container end, but also in case of the log rotation. // @see {@link https://stackoverflow.com/a/68673451} -func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, pod Channel[*corev1.Pod], since *time.Time) (io.Reader, error) { +func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, follow bool, pod Channel[*corev1.Pod], since *time.Time) (io.Reader, error) { // Fail immediately if the context is finished if ctx.Err() != nil { return nil, ctx.Err() @@ -58,7 +58,7 @@ func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, // Create logs stream request req := clientSet.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{ Container: containerName, - Follow: true, + Follow: follow, Timestamps: true, SinceTime: sinceTime, }) @@ -103,7 +103,7 @@ func getContainerLogsStream(ctx context.Context, clientSet kubernetes.Interface, return stream, nil } -func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, bufferSize int, pod Channel[*corev1.Pod]) Channel[ContainerLog] { +func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interface, namespace, podName, containerName string, follow bool, bufferSize int, pod Channel[*corev1.Pod]) Channel[ContainerLog] { ctx, ctxCancel := context.WithCancel(parentCtx) w := newChannel[ContainerLog](ctx, bufferSize) @@ -119,7 +119,7 @@ func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interfac var since *time.Time // Create logs stream request - stream, err := getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, pod, since) + stream, err := getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, follow, pod, since) hadAnyContent := false if err == io.EOF { return @@ -241,7 +241,7 @@ func WatchContainerLogs(parentCtx context.Context, clientSet kubernetes.Interfac } // Reinitialize logs stream since = common.Ptr(tsReader.ts.Add(1)) - stream, err = getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, pod, since) + stream, err = getContainerLogsStream(ctx, clientSet, namespace, podName, containerName, follow, pod, since) if err != nil { return } diff --git a/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go b/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go index c98dd7b0678..1ae190e2f08 100644 --- a/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go +++ b/pkg/testworkflows/testworkflowcontroller/watchinstrumentedpod.go @@ -102,7 +102,7 @@ func WatchInstrumentedPod(parentCtx context.Context, clientSet kubernetes.Interf // Watch the container logs follow := common.ResolvePtr(opts.Follow, true) && !state.IsFinished(ref) - for v := range WatchContainerLogs(ctx, clientSet, podObj.Namespace, podObj.Name, ref, 10, pod).Channel() { + for v := range WatchContainerLogs(ctx, clientSet, podObj.Namespace, podObj.Name, ref, follow, 10, pod).Channel() { if v.Error != nil { s.Error(v.Error) } else if v.Value.Output != nil { diff --git a/pkg/testworkflows/testworkflowprocessor/container.go b/pkg/testworkflows/testworkflowprocessor/container.go index 7566a3a1061..1546965341b 100644 --- a/pkg/testworkflows/testworkflowprocessor/container.go +++ b/pkg/testworkflows/testworkflowprocessor/container.go @@ -468,7 +468,7 @@ func (c *container) Resolve(m ...expressions.Machine) error { } env := c.Env() name = name[4:] - for i := range env { + for i := len(env) - 1; i >= 0; i-- { if env[i].Name == name && env[i].ValueFrom == nil { value, err := expressions.EvalTemplate(env[i].Value) if err == nil { From 4665fbd0bf00fd84cafefaa2a65674376f4d840c Mon Sep 17 00:00:00 2001 From: Povilas Versockas Date: Thu, 11 Jul 2024 23:01:37 -0700 Subject: [PATCH 08/16] feat: [TKC-2194] fix workflow execution telemetry (#5659) --- pkg/telemetry/sender_sio.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/telemetry/sender_sio.go b/pkg/telemetry/sender_sio.go index 516f6668ea5..909da094b7c 100644 --- a/pkg/telemetry/sender_sio.go +++ b/pkg/telemetry/sender_sio.go @@ -70,7 +70,7 @@ func mapEvent(userID string, event Event) analytics.Track { return analytics.Track{ Event: event.Name, UserId: userID, - Properties: mapProperties(event.Params), + Properties: mapProperties(event.Name, event.Params), Context: &analytics.Context{ App: analytics.AppInfo{ Name: event.Params.AppName, @@ -81,7 +81,7 @@ func mapEvent(userID string, event Event) analytics.Track { } } -func mapProperties(params Params) analytics.Properties { +func mapProperties(name string, params Params) analytics.Properties { properties := analytics.NewProperties(). Set("name", params.AppName). Set("version", params.AppVersion). @@ -132,6 +132,19 @@ func mapProperties(params Params) analytics.Properties { if params.TestSuiteSteps != 0 { properties = properties.Set("testSuiteSteps", params.TestSuiteSteps) } + if name == "testkube_api_run_test_workflow" { + properties = properties.Set("testWorkflowSteps", params.TestWorkflowSteps) + properties = properties.Set("testWorkflowExecuteCount", params.TestWorkflowExecuteCount) + properties = properties.Set("testWorkflowParallelUsed", params.TestWorkflowParallelUsed) + properties = properties.Set("testWorkflowMatrixUsed", params.TestWorkflowMatrixUsed) + properties = properties.Set("testWorkflowServicesUsed", params.TestWorkflowServicesUsed) + properties = properties.Set("testWorkflowIsSample", params.TestWorkflowIsSample) + properties = properties.Set("testWorkflowTemplates", params.TestWorkflowTemplates) + properties = properties.Set("testWorkflowImages", params.TestWorkflowImages) + properties = properties.Set("testWorkflowTemplateUsed", params.TestWorkflowTemplateUsed) + properties = properties.Set("testWorkflowArtifactUsed", params.TestWorkflowArtifactUsed) + properties = properties.Set("testWorkflowImage", params.TestWorkflowImage) + } return properties } From bb5f7bd0e5786b2954c856058be30ce536875b9d Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 16 Jul 2024 14:50:43 +0400 Subject: [PATCH 09/16] feat: [TKC-2208] refactor webhook disabling (#5658) * fix(testworkflows): displaying events (#5652) * feat: remove test workflow notifications Signed-off-by: Vladislav Sukhin * fix: remove duplicated param Signed-off-by: Vladislav Sukhin * featL refactor disable webhooks Signed-off-by: Vladislav Sukhin * fix: dep update Signed-off-by: Vladislav Sukhin * fix: add deprecated cli param Signed-off-by: Vladislav Sukhin * fix: update docs Signed-off-by: Vladislav Sukhin * fix: remove enable flag Signed-off-by: Vladislav Sukhin * fix: update webhooks docs Signed-off-by: Vladislav Sukhin * fix: add disable webhooks to execution Signed-off-by: Vladislav Sukhin * fix: show more fields in twe Signed-off-by: Vladislav Sukhin * fix: pass disable webhooks for test suites Signed-off-by: Vladislav Sukhin * fixL format execution output Signed-off-by: Vladislav Sukhin * fix: pass disable webhooks flag to execute operation Signed-off-by: Vladislav Sukhin * fix: rename var Signed-off-by: Vladislav Sukhin * fix: dep update Signed-off-by: Vladislav Sukhin --------- Signed-off-by: Vladislav Sukhin Co-authored-by: Dawid Rusnak --- api/v1/testkube.yaml | 23 +- cmd/kubectl-testkube/commands/common/flags.go | 4 - cmd/kubectl-testkube/commands/tests/common.go | 18 -- cmd/kubectl-testkube/commands/tests/create.go | 8 +- .../commands/tests/renderer/execution_obj.go | 1 + .../commands/tests/renderer/test_obj.go | 2 - cmd/kubectl-testkube/commands/tests/run.go | 17 +- cmd/kubectl-testkube/commands/tests/update.go | 4 - .../commands/testsuites/common.go | 27 -- .../commands/testsuites/create.go | 4 +- .../testsuites/renderer/execution_obj.go | 1 + .../testsuites/renderer/testsuite_obj.go | 1 - .../commands/testsuites/run.go | 17 +- .../commands/testsuites/update.go | 4 +- .../commands/testworkflows/create.go | 8 +- .../renderer/testworkflowexecution_obj.go | 1 + .../commands/testworkflows/run.go | 23 +- .../commands/webhooks/common.go | 28 +- .../commands/webhooks/create.go | 16 +- .../commands/webhooks/update.go | 7 +- .../testworkflow-toolkit/commands/execute.go | 21 +- cmd/tcl/testworkflow-toolkit/spawn/utils.go | 9 +- cmd/testworkflow-toolkit/env/config.go | 21 +- docs/docs/articles/webhooks.mdx | 278 +----------------- docs/docs/cli/testkube.md | 2 +- docs/docs/cli/testkube_agent.md | 1 - docs/docs/cli/testkube_create_test.md | 2 - docs/docs/cli/testkube_create_testsuite.md | 2 - docs/docs/cli/testkube_create_testworkflow.md | 9 +- docs/docs/cli/testkube_create_webhook.md | 2 +- docs/docs/cli/testkube_debug.md | 11 +- docs/docs/cli/testkube_debug_agent.md | 35 +++ docs/docs/cli/testkube_debug_controlplane.md | 11 +- docs/docs/cli/testkube_debug_oss.md | 30 ++ docs/docs/cli/testkube_generate_tests-crds.md | 2 - docs/docs/cli/testkube_run_test.md | 1 - docs/docs/cli/testkube_run_testsuite.md | 1 - docs/docs/cli/testkube_run_testworkflow.md | 1 - docs/docs/cli/testkube_update_test.md | 2 - docs/docs/cli/testkube_update_testsuite.md | 2 - docs/docs/cli/testkube_update_webhook.md | 2 +- go.mod | 2 +- go.sum | 4 +- pkg/api/v1/client/testsuite.go | 2 + .../model_test_suite_execution_extended.go | 2 +- .../model_test_suite_execution_request.go | 2 +- ...del_test_suite_execution_update_request.go | 2 +- ...model_test_suite_step_execution_request.go | 2 +- .../testkube/model_test_workflow_execution.go | 2 +- .../model_test_workflow_execution_cr.go | 2 - .../model_test_workflow_execution_request.go | 2 +- ...odel_test_workflow_notifications_config.go | 15 - .../v1/testkube/model_test_workflow_spec.go | 23 +- .../model_test_workflow_step_parallel.go | 31 +- pkg/mapper/testexecutions/mapper.go | 1 + pkg/mapper/tests/kube_openapi.go | 2 - pkg/mapper/tests/openapi_kube.go | 6 - pkg/mapper/testsuiteexecutions/mapper.go | 1 + pkg/mapper/testsuites/kube_openapi.go | 3 - pkg/mapper/testsuites/openapi_kube.go | 7 - pkg/mapper/testworkflows/kube_openapi.go | 29 +- pkg/mapper/testworkflows/openapi_kube.go | 18 +- pkg/scheduler/test_scheduler.go | 4 - pkg/scheduler/testsuite_scheduler.go | 4 - .../testworkflowexecutor/executor.go | 25 +- .../testworkflowprocessor/container.go | 1 + 66 files changed, 229 insertions(+), 622 deletions(-) create mode 100644 docs/docs/cli/testkube_debug_agent.md create mode 100644 docs/docs/cli/testkube_debug_oss.md delete mode 100644 pkg/api/v1/testkube/model_test_workflow_notifications_config.go diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index c46d8fa6348..d5074d950ab 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -6595,7 +6595,7 @@ components: description: running context for the test execution disableWebhooks: type: boolean - description: whether webhooks on the executions of this step are disabled + description: whether webhooks on the execution of this step are disabled default: false example: - true @@ -6703,7 +6703,7 @@ components: description: test suite execution name started the test suite execution disableWebhooks: type: boolean - description: whether webhooks on the executions of this test suite are disabled + description: whether webhooks on the execution of this test suite are disabled default: false example: - true @@ -7872,7 +7872,7 @@ components: description: test workflow execution name started the test workflow execution disableWebhooks: type: boolean - description: whether webhooks on the executions of this test workflow are disabled + description: whether webhooks on the execution of this test workflow are disabled default: false TestWorkflowWithExecution: @@ -7960,7 +7960,7 @@ components: description: test workflow execution name started the test workflow execution disableWebhooks: type: boolean - description: whether webhooks on the executions of this test workflow are disabled + description: whether webhooks on the execution of this test workflow are disabled default: false example: - true @@ -8500,9 +8500,6 @@ components: type: array items: $ref: "#/components/schemas/TestWorkflowEvent" - notifications: - $ref: "#/components/schemas/TestWorkflowNotificationsConfig" - TestWorkflowTemplateSpec: type: object @@ -9175,14 +9172,6 @@ components: items: $ref: "#/components/schemas/VolumeMount" - TestWorkflowNotificationsConfig: - type: object - properties: - disableWebhooks: - type: boolean - description: disable webhooks for this test workflow - default: false - TestWorkflowStepRun: type: object properties: @@ -9350,10 +9339,6 @@ components: status: $ref: "#/components/schemas/TestWorkflowExecutionStatusCR" description: test workflow execution status - disableWebhooks: - type: boolean - description: disable webhooks for this execution - default: false TestWorkflowExecutionStatusCR: type: object diff --git a/cmd/kubectl-testkube/commands/common/flags.go b/cmd/kubectl-testkube/commands/common/flags.go index 0363c96d009..d4fa0764983 100644 --- a/cmd/kubectl-testkube/commands/common/flags.go +++ b/cmd/kubectl-testkube/commands/common/flags.go @@ -185,10 +185,6 @@ func ProcessMasterFlags(cmd *cobra.Command, opts *HelmOptions, cfg *config.Data) } -func IsBothEnabledAndDisabledSet(cmd *cobra.Command) bool { - return cmd.Flag("enable-webhooks").Changed && cmd.Flag("disable-webhooks").Changed -} - // CommaList is a custom flag type for features type CommaList []string diff --git a/cmd/kubectl-testkube/commands/tests/common.go b/cmd/kubectl-testkube/commands/tests/common.go index e436977e09f..ed136379aa8 100644 --- a/cmd/kubectl-testkube/commands/tests/common.go +++ b/cmd/kubectl-testkube/commands/tests/common.go @@ -671,13 +671,6 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi return nil, err } - if cmd.Flag("enable-webhooks").Changed { - request.DisableWebhooks = false - } - if cmd.Flag("disable-webhooks").Changed { - request.DisableWebhooks = true - } - return request, nil } @@ -1255,17 +1248,6 @@ func newExecutionUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.E request.SlavePodRequest = &emptyPodRequest } - disableWebhooks := false - if cmd.Flag("enable-webhooks").Changed { - request.DisableWebhooks = &disableWebhooks - nonEmpty = true - } - if cmd.Flag("disable-webhooks").Changed { - disableWebhooks = true - request.DisableWebhooks = &disableWebhooks - nonEmpty = true - } - if nonEmpty { return request, nil } diff --git a/cmd/kubectl-testkube/commands/tests/create.go b/cmd/kubectl-testkube/commands/tests/create.go index 69c2c427bf0..e5ada8fad2d 100644 --- a/cmd/kubectl-testkube/commands/tests/create.go +++ b/cmd/kubectl-testkube/commands/tests/create.go @@ -110,10 +110,6 @@ func NewCreateTestsCmd() *cobra.Command { ui.Failf("pass valid test name (in '--name' flag)") } - if common.IsBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable-webhooks and --disable-webhooks flags are set, please use only one") - } - namespace := cmd.Flag("namespace").Value.String() var client client.Client if !crdOnly { @@ -288,8 +284,8 @@ func AddCreateFlags(cmd *cobra.Command, flags *CreateCommonFlags) { cmd.Flags().StringVar(&flags.SlavePodTemplate, "slave-pod-template", "", "slave pod template file path for extensions to slave pod template") cmd.Flags().StringVar(&flags.SlavePodTemplateReference, "slave-pod-template-reference", "", "reference to slave pod template to use for the test") cmd.Flags().StringVar(&flags.ExecutionNamespace, "execution-namespace", "", "namespace for test execution (Pro edition only)") - cmd.Flags().Bool("disable-webhooks", false, "disable webhooks") - cmd.Flags().Bool("enable-webhooks", false, "enable webhooks") + cmd.Flags().MarkDeprecated("disable-webhooks", "disable-webhooks is deprecated") + cmd.Flags().MarkDeprecated("enable-webhooks", "enable-webhooks is deprecated") } func validateExecutorTypeAndContent(executorType, contentType string, executors testkube.ExecutorsDetails) error { diff --git a/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go b/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go index b4a6c895a1e..baa81015b21 100644 --- a/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go +++ b/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go @@ -60,6 +60,7 @@ func ExecutionRenderer(client client.Client, ui *ui.UI, obj interface{}) error { ui.Warn(" Auth type: ", execution.Content.Repository.AuthType) } + ui.Warn("Disabled webhooks:", fmt.Sprint(execution.DisableWebhooks)) if err := render.RenderExecutionResult(client, &execution, false, true); err != nil { return err } diff --git a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go index 4da43ad066b..33f6a88aa11 100644 --- a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go +++ b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go @@ -278,8 +278,6 @@ func TestRenderer(client client.Client, ui *ui.UI, obj interface{}) error { ui.NL() ui.Warn(" Variable secrets: ", variableSecrets...) } - - ui.Warn(" Disable webhooks: ", fmt.Sprint(test.ExecutionRequest.DisableWebhooks)) } return nil diff --git a/cmd/kubectl-testkube/commands/tests/run.go b/cmd/kubectl-testkube/commands/tests/run.go index 3343cb32e90..3409b5aa57a 100644 --- a/cmd/kubectl-testkube/commands/tests/run.go +++ b/cmd/kubectl-testkube/commands/tests/run.go @@ -83,6 +83,7 @@ func NewRunTestCmd() *cobra.Command { executionNamespace string attachDebugger bool debugFile string + disableWebhooks bool ) cmd := &cobra.Command{ @@ -95,10 +96,6 @@ func NewRunTestCmd() *cobra.Command { watchEnabled = true } - if common.IsBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable-webhooks and --disable-webhooks flags are set, please use only one") - } - outputFlag := cmd.Flag("output") outputType := render.OutputPretty if outputFlag != nil { @@ -150,13 +147,7 @@ func NewRunTestCmd() *cobra.Command { ExecutePostRunScriptBeforeScraping: executePostRunScriptBeforeScraping, SourceScripts: sourceScripts, ExecutionNamespace: executionNamespace, - } - - if cmd.Flag("enable-webhooks").Changed { - options.DisableWebhooks = false - } - if cmd.Flag("disable-webhooks").Changed { - options.DisableWebhooks = true + DisableWebhooks: disableWebhooks, } var fields = []struct { @@ -480,8 +471,8 @@ func NewRunTestCmd() *cobra.Command { cmd.Flags().StringVar(&executionNamespace, "execution-namespace", "", "namespace for test execution (Pro edition only)") cmd.Flags().StringVar(&debugFile, "debugger-file", "", "store debug info into file, stdout by default") cmd.Flags().BoolVar(&attachDebugger, "attach-debugger", false, "attach simple debugger for job, need KUBECONFIG for the agent to be active") - cmd.Flags().Bool("disable-webhooks", false, "disable webhooks") - cmd.Flags().Bool("enable-webhooks", false, "enable webhooks") + cmd.Flags().BoolVar(&disableWebhooks, "disable-webhooks", false, "disable webhooks") + cmd.Flags().MarkDeprecated("enable-webhooks", "enable-webhooks is deprecated") return cmd } diff --git a/cmd/kubectl-testkube/commands/tests/update.go b/cmd/kubectl-testkube/commands/tests/update.go index 6a719d99689..890b1b604cb 100644 --- a/cmd/kubectl-testkube/commands/tests/update.go +++ b/cmd/kubectl-testkube/commands/tests/update.go @@ -39,10 +39,6 @@ func NewUpdateTestsCmd() *cobra.Command { ui.Failf("pass valid test name (in '--name' flag)") } - if common.IsBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable-webhooks and --disable-webhooks flags are set, please use only one") - } - client, namespace, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) diff --git a/cmd/kubectl-testkube/commands/testsuites/common.go b/cmd/kubectl-testkube/commands/testsuites/common.go index ff332d0a2ef..f8087cf39d8 100644 --- a/cmd/kubectl-testkube/commands/testsuites/common.go +++ b/cmd/kubectl-testkube/commands/testsuites/common.go @@ -117,10 +117,6 @@ func uiShellTestSuiteWatchCommandBlock(id string) { // NewTestSuiteUpsertOptionsFromFlags creates test suite upsert options from command flags func NewTestSuiteUpsertOptionsFromFlags(cmd *cobra.Command) (options apiclientv1.UpsertTestSuiteOptions, err error) { - if common.IsBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable-webhooks and --disable-webhooks flags are set, please use only one") - } - data, err := common.NewDataFromFlags(cmd) if err != nil { return options, err @@ -260,22 +256,11 @@ func NewTestSuiteUpsertOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 } } - if cmd.Flag("enable-webhooks").Changed { - options.ExecutionRequest.DisableWebhooks = false - } - if cmd.Flag("disable-webhooks").Changed { - options.ExecutionRequest.DisableWebhooks = true - } - return options, nil } // NewTestSuiteUpdateOptionsFromFlags creates test suite update options from command flags func NewTestSuiteUpdateOptionsFromFlags(cmd *cobra.Command) (options apiclientv1.UpdateTestSuiteOptions, err error) { - if common.IsBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable-webhooks and --disable-webhooks flags are set, please use only one") - } - data, err := common.NewDataFromFlags(cmd) if err != nil { return options, err @@ -379,18 +364,6 @@ func NewTestSuiteUpdateOptionsFromFlags(cmd *cobra.Command) (options apiclientv1 nonEmpty = true } - var disableWebhook bool - if cmd.Flag("enable-webhooks").Changed { - nonEmpty = true - disableWebhook = false - executionRequest.DisableWebhooks = &disableWebhook - } - if cmd.Flag("disable-webhooks").Changed { - nonEmpty = true - disableWebhook = true - executionRequest.DisableWebhooks = &disableWebhook - } - var values = []struct { source string destination **string diff --git a/cmd/kubectl-testkube/commands/testsuites/create.go b/cmd/kubectl-testkube/commands/testsuites/create.go index f23ab92831a..63084f9ec68 100644 --- a/cmd/kubectl-testkube/commands/testsuites/create.go +++ b/cmd/kubectl-testkube/commands/testsuites/create.go @@ -121,8 +121,8 @@ func NewCreateTestSuitesCmd() *cobra.Command { 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") cmd.Flags().BoolVar(&update, "update", false, "update, if test suite already exists") - cmd.Flags().Bool("disable-webhooks", false, "disable webhooks") - cmd.Flags().Bool("enable-webhooks", false, "enable webhooks") + cmd.Flags().MarkDeprecated("disable-webhooks", "disable-webhooks is deprecated") + cmd.Flags().MarkDeprecated("enable-webhooks", "enable-webhooks is deprecated") return cmd } diff --git a/cmd/kubectl-testkube/commands/testsuites/renderer/execution_obj.go b/cmd/kubectl-testkube/commands/testsuites/renderer/execution_obj.go index 24e910010d6..6b404ddd403 100644 --- a/cmd/kubectl-testkube/commands/testsuites/renderer/execution_obj.go +++ b/cmd/kubectl-testkube/commands/testsuites/renderer/execution_obj.go @@ -29,6 +29,7 @@ func TestSuiteExecutionRenderer(client client.Client, ui *ui.UI, obj interface{} ui.Warn("Context:", execution.RunningContext.Context) } + ui.Warn("Disabled webhooks:", fmt.Sprint(execution.DisableWebhooks)) info, err := client.GetServerInfo() ui.ExitOnError("getting server info", err) diff --git a/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go b/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go index ecfe25f63e1..3a4926f6ef7 100644 --- a/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go +++ b/cmd/kubectl-testkube/commands/testsuites/renderer/testsuite_obj.go @@ -80,7 +80,6 @@ func TestSuiteRenderer(client client.Client, ui *ui.UI, obj interface{}) error { if ts.ExecutionRequest.PvcTemplateReference != "" { ui.Warn(" PVC template reference: ", ts.ExecutionRequest.PvcTemplateReference) } - ui.Warn(" Disable webhooks: ", fmt.Sprint(ts.ExecutionRequest.DisableWebhooks)) } batches := append(ts.Before, ts.Steps...) diff --git a/cmd/kubectl-testkube/commands/testsuites/run.go b/cmd/kubectl-testkube/commands/testsuites/run.go index 73ea3a33ec5..36f2a3620ee 100644 --- a/cmd/kubectl-testkube/commands/testsuites/run.go +++ b/cmd/kubectl-testkube/commands/testsuites/run.go @@ -48,6 +48,7 @@ func NewRunTestSuiteCmd() *cobra.Command { format string masks []string silentMode bool + disableWebhooks bool ) cmd := &cobra.Command{ @@ -67,10 +68,6 @@ func NewRunTestSuiteCmd() *cobra.Command { client, namespace, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) - if common.IsBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable-webhooks and --disable-webhooks flags are set, please use only one") - } - var executions []testkube.TestSuiteExecution options := apiv1.ExecuteTestSuiteOptions{ @@ -85,13 +82,7 @@ func NewRunTestSuiteCmd() *cobra.Command { JobTemplateReference: jobTemplateReference, ScraperTemplateReference: scraperTemplateReference, PvcTemplateReference: pvcTemplateReference, - } - - if cmd.Flag("enable-webhooks").Changed { - options.DisableWebhooks = false - } - if cmd.Flag("disable-webhooks").Changed { - options.DisableWebhooks = true + DisableWebhooks: disableWebhooks, } var fields = []struct { @@ -245,8 +236,8 @@ func NewRunTestSuiteCmd() *cobra.Command { cmd.Flags().StringVar(&format, "format", "folder", "data format for storing files, one of folder|archive") cmd.Flags().StringArrayVarP(&masks, "mask", "", []string{}, "regexp to filter downloaded files, single or comma separated, like report/.* or .*\\.json,.*\\.js$") cmd.Flags().BoolVarP(&silentMode, "silent", "", false, "don't print intermediate test suite execution") - cmd.Flags().Bool("disable-webhooks", false, "disable webhooks") - cmd.Flags().Bool("enable-webhooks", false, "enable webhooks") + cmd.Flags().BoolVar(&disableWebhooks, "disable-webhooks", false, "disable webhooks") + cmd.Flags().MarkDeprecated("enable-webhooks", "enable-webhooks is deprecated") return cmd } diff --git a/cmd/kubectl-testkube/commands/testsuites/update.go b/cmd/kubectl-testkube/commands/testsuites/update.go index f6122584923..ad63ad0c073 100644 --- a/cmd/kubectl-testkube/commands/testsuites/update.go +++ b/cmd/kubectl-testkube/commands/testsuites/update.go @@ -80,8 +80,8 @@ func UpdateTestSuitesCmd() *cobra.Command { 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") - cmd.Flags().Bool("disable-webhooks", false, "disable webhooks") - cmd.Flags().Bool("enable-webhooks", false, "enable webhooks") + cmd.Flags().MarkDeprecated("disable-webhooks", "disable-webhooks is deprecated") + cmd.Flags().MarkDeprecated("enable-webhooks", "enable-webhooks is deprecated") return cmd } diff --git a/cmd/kubectl-testkube/commands/testworkflows/create.go b/cmd/kubectl-testkube/commands/testworkflows/create.go index 47e0db5c8c0..d843c4a5953 100644 --- a/cmd/kubectl-testkube/commands/testworkflows/create.go +++ b/cmd/kubectl-testkube/commands/testworkflows/create.go @@ -61,12 +61,6 @@ func NewCreateTestWorkflowCmd() *cobra.Command { client, _, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) - if cmd.Flag("disable-webhooks").Changed { - obj.Spec.Notifications = &testworkflowsv1.NotificationsConfig{ - DisableWebhooks: true, - } - } - workflow, err := client.GetTestWorkflow(obj.Name) if err != nil { if update { @@ -94,7 +88,7 @@ func NewCreateTestWorkflowCmd() *cobra.Command { cmd.Flags().StringVar(&name, "name", "", "test workflow name") cmd.Flags().BoolVar(&update, "update", false, "update, if test workflow already exists") cmd.Flags().StringVarP(&filePath, "file", "f", "", "file path to get the test workflow specification") - cmd.Flags().Bool("disable-webhooks", false, "disable webhooks for this test workflow") + cmd.Flags().MarkDeprecated("disable-webhooks", "disable-webhooks is deprecated") return cmd } diff --git a/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflowexecution_obj.go b/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflowexecution_obj.go index 05076da5af9..d00f1734fd3 100644 --- a/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflowexecution_obj.go +++ b/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflowexecution_obj.go @@ -58,6 +58,7 @@ func printPrettyOutput(ui *ui.UI, execution testkube.TestWorkflowExecution) { ui.Warn("Execution number: ", fmt.Sprintf("%d", execution.Number)) } ui.Warn("Requested at: ", execution.ScheduledAt.String()) + ui.Warn("Disabled webhooks: ", fmt.Sprint(execution.DisableWebhooks)) if execution.Result != nil && execution.Result.Status != nil { ui.Warn("Status: ", string(*execution.Result.Status)) if !execution.Result.QueuedAt.IsZero() { diff --git a/cmd/kubectl-testkube/commands/testworkflows/run.go b/cmd/kubectl-testkube/commands/testworkflows/run.go index 7a42b731e6e..afc648b2214 100644 --- a/cmd/kubectl-testkube/commands/testworkflows/run.go +++ b/cmd/kubectl-testkube/commands/testworkflows/run.go @@ -31,9 +31,10 @@ var ( func NewRunTestWorkflowCmd() *cobra.Command { var ( - executionName string - config map[string]string - watchEnabled bool + executionName string + config map[string]string + watchEnabled bool + disableWebhooks bool ) cmd := &cobra.Command{ @@ -43,10 +44,6 @@ func NewRunTestWorkflowCmd() *cobra.Command { Short: "Starts test workflow execution", Run: func(cmd *cobra.Command, args []string) { - if common.IsBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable-webhooks and --disable-webhooks flags are set, please use only one") - } - outputFlag := cmd.Flag("output") outputType := render.OutputPretty if outputFlag != nil { @@ -58,14 +55,6 @@ func NewRunTestWorkflowCmd() *cobra.Command { client, _, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) - var disableWebhooks bool - if cmd.Flag("enable-webhooks").Changed { - disableWebhooks = false - } - if cmd.Flag("disable-webhooks").Changed { - disableWebhooks = true - } - name := args[0] execution, err := client.ExecuteTestWorkflow(name, testkube.TestWorkflowExecutionRequest{ Name: executionName, @@ -114,8 +103,8 @@ func NewRunTestWorkflowCmd() *cobra.Command { cmd.Flags().StringVarP(&executionName, "name", "n", "", "execution name, if empty will be autogenerated") cmd.Flags().StringToStringVarP(&config, "config", "", map[string]string{}, "configuration variables in a form of name1=val1 passed to executor") cmd.Flags().BoolVarP(&watchEnabled, "watch", "f", false, "watch for changes after start") - cmd.Flags().Bool("disable-webhooks", false, "disable webhooks for this execution") - cmd.Flags().Bool("enable-webhooks", false, "enable webhooks for this execution") + cmd.Flags().BoolVar(&disableWebhooks, "disable-webhooks", false, "disable webhooks for this execution") + cmd.Flags().MarkDeprecated("enable-webhooks", "enable-webhooks is deprecated") return cmd } diff --git a/cmd/kubectl-testkube/commands/webhooks/common.go b/cmd/kubectl-testkube/commands/webhooks/common.go index 561355417d7..15c4262b460 100644 --- a/cmd/kubectl-testkube/commands/webhooks/common.go +++ b/cmd/kubectl-testkube/commands/webhooks/common.go @@ -47,6 +47,11 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW return options, err } + disabled, err := cmd.Flags().GetBool("disable") + if err != nil { + return options, err + } + payloadTemplateReference := cmd.Flag("payload-template-reference").Value.String() options = apiv1.CreateWebhookOptions{ Name: name, @@ -60,13 +65,7 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW Headers: headers, PayloadTemplateReference: payloadTemplateReference, OnStateChange: onStateChange, - } - - if cmd.Flag("enable").Changed { - options.Disabled = false - } - if cmd.Flag("disable").Changed { - options.Disabled = true + Disabled: disabled, } return options, nil @@ -150,13 +149,12 @@ func NewUpdateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.UpdateW options.Headers = &headers } - if cmd.Flag("enable").Changed { - options.Disabled = new(bool) - *options.Disabled = false - } if cmd.Flag("disable").Changed { - options.Disabled = new(bool) - *options.Disabled = true + disabled, err := cmd.Flags().GetBool("disable") + if err != nil { + return options, err + } + options.Disabled = &disabled } if cmd.Flag("on-state-change").Changed { @@ -169,7 +167,3 @@ func NewUpdateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.UpdateW return options, nil } - -func isBothEnabledAndDisabledSet(cmd *cobra.Command) bool { - return cmd.Flag("enable").Changed && cmd.Flag("disable").Changed -} diff --git a/cmd/kubectl-testkube/commands/webhooks/create.go b/cmd/kubectl-testkube/commands/webhooks/create.go index 5727c23cfdc..4bf8d798e2b 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -23,6 +23,7 @@ func NewCreateWebhookCmd() *cobra.Command { headers map[string]string payloadTemplateReference string update bool + disable bool onStateChange bool ) @@ -39,10 +40,6 @@ func NewCreateWebhookCmd() *cobra.Command { ui.Failf("pass valid name (in '--name' flag)") } - if isBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable and --disable flags are set, please use only one") - } - namespace := cmd.Flag("namespace").Value.String() var client apiv1.Client if !crdOnly { @@ -50,13 +47,6 @@ func NewCreateWebhookCmd() *cobra.Command { ui.ExitOnError("getting client", err) webhook, _ := client.GetWebhook(name) - if cmd.Flag("enable").Changed { - webhook.Disabled = false - } - if cmd.Flag("disable").Changed { - webhook.Disabled = true - } - if name == webhook.Name { if cmd.Flag("update").Changed { if !update { @@ -111,8 +101,8 @@ func NewCreateWebhookCmd() *cobra.Command { 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") cmd.Flags().BoolVar(&update, "update", false, "update, if webhook already exists") - cmd.Flags().Bool("disable", false, "disable webhook") - cmd.Flags().Bool("enable", false, "enable webhook") + cmd.Flags().BoolVar(&disable, "disable", false, "disable webhook") + cmd.Flags().MarkDeprecated("enable", "enable webhook is deprecated") cmd.Flags().BoolVar(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change") return cmd diff --git a/cmd/kubectl-testkube/commands/webhooks/update.go b/cmd/kubectl-testkube/commands/webhooks/update.go index 2f3f9405f5f..90ff196ea66 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -17,7 +17,6 @@ func UpdateWebhookCmd() *cobra.Command { payloadTemplate string headers map[string]string payloadTemplateReference string - enable bool disable bool onStateChange bool ) @@ -32,10 +31,6 @@ func UpdateWebhookCmd() *cobra.Command { ui.Failf("pass valid name (in '--name' flag)") } - if isBothEnabledAndDisabledSet(cmd) { - ui.Failf("both --enable and --disable flags are set, please use only one") - } - client, namespace, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) @@ -64,7 +59,7 @@ func UpdateWebhookCmd() *cobra.Command { 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") cmd.Flags().BoolVar(&disable, "disable", false, "disable webhook") - cmd.Flags().BoolVar(&enable, "enable", false, "enable webhook") + cmd.Flags().MarkDeprecated("enable", "enable webhook is depecated") cmd.Flags().BoolVar(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change") return cmd diff --git a/cmd/tcl/testworkflow-toolkit/commands/execute.go b/cmd/tcl/testworkflow-toolkit/commands/execute.go index 8d8aa468ffd..b146ae54621 100644 --- a/cmd/tcl/testworkflow-toolkit/commands/execute.go +++ b/cmd/tcl/testworkflow-toolkit/commands/execute.go @@ -52,7 +52,7 @@ type executionResult struct { Status string `json:"status"` } -func buildTestExecution(test testworkflowsv1.StepExecuteTest, async, disableWebhooks bool) (func() error, error) { +func buildTestExecution(test testworkflowsv1.StepExecuteTest, async bool) (func() error, error) { return func() (err error) { c := env.Testkube() @@ -84,7 +84,7 @@ func buildTestExecution(test testworkflowsv1.StepExecuteTest, async, disableWebh EnvConfigMaps: common.MapSlice(test.ExecutionRequest.EnvConfigMaps, testworkflows.MapTestEnvReferenceKubeToAPI), EnvSecrets: common.MapSlice(test.ExecutionRequest.EnvSecrets, testworkflows.MapTestEnvReferenceKubeToAPI), ExecutionNamespace: test.ExecutionRequest.ExecutionNamespace, - DisableWebhooks: disableWebhooks, + DisableWebhooks: env.ExecutionDisableWebhooks(), }) execName := exec.Name if err != nil { @@ -151,8 +151,9 @@ func buildWorkflowExecution(workflow testworkflowsv1.StepExecuteWorkflow, async c := env.Testkube() exec, err := c.ExecuteTestWorkflow(workflow.Name, testkube.TestWorkflowExecutionRequest{ - Name: workflow.ExecutionName, - Config: testworkflows.MapConfigValueKubeToAPI(workflow.Config), + Name: workflow.ExecutionName, + Config: testworkflows.MapConfigValueKubeToAPI(workflow.Config), + DisableWebhooks: env.ExecutionDisableWebhooks(), }) execName := exec.Name if err != nil { @@ -256,11 +257,10 @@ func registerTransfer(transferSrv transfer.Server, request map[string]testworkfl func NewExecuteCmd() *cobra.Command { var ( - tests []string - workflows []string - parallelism int - async bool - disableWebhooks bool + tests []string + workflows []string + parallelism int + async bool ) cmd := &cobra.Command{ @@ -308,7 +308,7 @@ func NewExecuteCmd() *cobra.Command { if err != nil { ui.Fail(errors.Wrapf(err, "'%s' test: computing execution", spec.Name)) } - fn, err := buildTestExecution(*spec, async, disableWebhooks) + fn, err := buildTestExecution(*spec, async) if err != nil { ui.Fail(err) } @@ -409,7 +409,6 @@ func NewExecuteCmd() *cobra.Command { cmd.Flags().StringArrayVarP(&workflows, "workflow", "w", nil, "workflows to run") cmd.Flags().IntVarP(¶llelism, "parallelism", "p", 0, "how many items could be executed at once") cmd.Flags().BoolVar(&async, "async", false, "should it wait for results") - cmd.Flags().BoolVar(&disableWebhooks, "disableWebhooks", false, "should it disable webhooks") return cmd } diff --git a/cmd/tcl/testworkflow-toolkit/spawn/utils.go b/cmd/tcl/testworkflow-toolkit/spawn/utils.go index 5f50fbfe107..acfe2b137eb 100644 --- a/cmd/tcl/testworkflow-toolkit/spawn/utils.go +++ b/cmd/tcl/testworkflow-toolkit/spawn/utils.go @@ -199,10 +199,11 @@ func CreateExecutionMachine(prefix string, index int64) (string, expressions.Mac "fsPrefix": fsPrefix, }). Register("execution", map[string]interface{}{ - "id": env.ExecutionId(), - "name": env.ExecutionName(), - "number": env.ExecutionNumber(), - "scheduledAt": env.ExecutionScheduledAt().UTC().Format(constants.RFC3339Millis), + "id": env.ExecutionId(), + "name": env.ExecutionName(), + "number": env.ExecutionNumber(), + "scheduledAt": env.ExecutionScheduledAt().UTC().Format(constants.RFC3339Millis), + "disableWebhooks": env.ExecutionDisableWebhooks(), }) } diff --git a/cmd/testworkflow-toolkit/env/config.go b/cmd/testworkflow-toolkit/env/config.go index c7fd6207610..2d301a16a93 100644 --- a/cmd/testworkflow-toolkit/env/config.go +++ b/cmd/testworkflow-toolkit/env/config.go @@ -35,14 +35,15 @@ type envCloudConfig struct { } type envExecutionConfig struct { - WorkflowName string `envconfig:"TK_WF"` - Id string `envconfig:"TK_EX"` - Name string `envconfig:"TK_EXN"` - Number int64 `envconfig:"TK_EXC"` - ScheduledAt string `envconfig:"TK_EXS"` - ResourceId string `envconfig:"TK_EXI"` - RootResourceId string `envconfig:"TK_EXR"` - FSPrefix string `envconfig:"TK_FS"` + WorkflowName string `envconfig:"TK_WF"` + Id string `envconfig:"TK_EX"` + Name string `envconfig:"TK_EXN"` + Number int64 `envconfig:"TK_EXC"` + ScheduledAt string `envconfig:"TK_EXS"` + ResourceId string `envconfig:"TK_EXI"` + RootResourceId string `envconfig:"TK_EXR"` + FSPrefix string `envconfig:"TK_FS"` + DisableWebhooks bool `envconfig:"TK_DWH"` } type envSystemConfig struct { @@ -145,6 +146,10 @@ func ExecutionScheduledAt() time.Time { return t } +func ExecutionDisableWebhooks() bool { + return Config().Execution.DisableWebhooks +} + func JUnitParserEnabled() bool { return Config().Features.EnableJUnitParser } diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index ea0830839e3..25f21d09bed 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -491,7 +491,7 @@ testkube create webhook --disable --name "${WEBHOOK_NAME}" ... To enable it explicitly: ```bash -testkube create webhook --enable --name "${WEBHOOK_NAME}" ... +testkube create webhook --disable=false --name "${WEBHOOK_NAME}" ... ``` #### Set on Update @@ -505,7 +505,7 @@ testkube update webhook --disable --name "${WEBHOOK_NAME}" To enable: ```bash -testkube update webhook --enable --name "${WEBHOOK_NAME}" +testkube update webhook --disable=false --name "${WEBHOOK_NAME}" ``` @@ -552,7 +552,7 @@ To enable a webhook again, you can either set the field `disabled` to `"false"` ### Disabling Webhooks on Other Resources -Disabling webhooks can also be done directly on an execution, a test, a test suite and a test workflow. This way the notifications can be disabled temporarily during creation or when they need to be fixed or updated. The CLI offers the flag `--disable-webhooks`, the API the parameter `disableWebhooks`, and the CRDs `disableWebhooks`. When not set explicitly, webhooks will be enabled by default. +Disabling webhooks can also be done directly on an execution of a test, a test suite and a test workflow. This way the notifications can be disabled temporarily during running the execution. The CLI offers the flag `--disable-webhooks`, the API the parameter `disableWebhooks`. When not set explicitly, webhooks will be enabled by default. #### Disabling Webhooks on Tests @@ -563,38 +563,7 @@ Disabling webhooks can also be done directly on an execution, a test, a test sui Use the flag `--disable-webhooks` on the CLI: ```bash -testkube create test --name k6-test --type k6/script --test-content-type file-uri --file k6-test.js --disable-webhooks -``` - - - - - -Use the attribute `disableWebhooks` in the `executionRequest` on CRD level. - -```yaml -apiVersion: tests.testkube.io/v3 -kind: Test -metadata: - name: k6-test - namespace: testkube -spec: - content: - data: | - import http from 'k6/http'; - import { check } from 'k6'; - - export default function () { - const baseURI = `${__ENV.API_URI || 'http://google.com'}`; - - check(http.get(`${baseURI}/joke`), { - 'joke should be about Chuck': r => r.body.includes("Chuck") // this should fail - }); - } - type: file-uri - executionRequest: - disableWebhooks: true - type: k6/script +testkube run test k6-test --disable-webhooks ``` @@ -604,47 +573,9 @@ spec: The API expects a boolean value of the form: ```json - "executionRequest": { - "disableWebhooks": true - } -``` - -Inside a test this would look like the following: - -```json -{ - "name": "test-from-api", - "namespace": "testkube", - "type": "k6/script", - "content": { - "type": "string", - "data": "import http from 'k6/http';\n import { sleep } from 'k6';\n export default function () {\n const res = http.get(`http://${__ENV.MY_HOSTNAME}/`);\n sleep(1);\n}" - }, - "labels": { - "env": "prod", - "app": "backend" - }, - "variables": { - "var1": { - "name": "var1", - "value": "1", - "type": "basic" - }, - "var2": { - "name": "var2", - "value": "2", - "type": "secret", - "secretRef": { - "namespace": "testkube", - "name": "var2name", - "key": "var2key" - } - } - }, - "executionRequest": { + { "disableWebhooks": true } -} ``` @@ -662,44 +593,7 @@ The examples show only the `create` operations, however these instructions work Use the flag `--disable-webhooks` on the CLI: ```bash -cat ts.json | /Users/lilla/bin/kubectl-testkube create testsuite --disable-webhooks -``` - - - - - -Use the attribute `disableWebhooks` in the `executionRequest` on CRD level: - -```yaml -apiVersion: tests.testkube.io/v3 -kind: TestSuite -metadata: - name: testkube-suite - namespace: testkube -spec: - description: Testkube test suite, api, dashboard and performance - executionRequest: { disableWebhooks: true } - steps: - - execute: - - delay: 0s - test: testkube-api - - delay: 0s - test: testkube-dashboard - stopOnFailure: false - - execute: - - delay: 1s - stopOnFailure: false - - downloadArtifacts: - previousTestNames: - - testkube-api - execute: - - delay: 0s - test: testkube-dashboard - - delay: 1s - - delay: 0s - test: testkube-homepage - stopOnFailure: false +testkube run testsuite test-suite-1 --disable-webhooks ``` @@ -709,33 +603,9 @@ spec: The API expects a boolean value of the form: ```json - "executionRequest": { - "disableWebhooks": true - } -``` - -Inside a test suite this would look like the following: - -```json -{ - "name": "test-suite-1", - "namespace": "testkube", - "description": "Example Test Suite", - "steps": [ - { - "stopOnFailure": false, - "execute": [ - { - "test": "existing-test" - } - ] - } - ], - "created": "2024-06-03T10:40:44Z", - "executionRequest": { + { "disableWebhooks": true } -} ``` @@ -753,66 +623,7 @@ The examples show only the `create` operations, however these instructions work Use the flag `--disable-webhooks` on the CLI: ```bash -testkube create testworkflow --file wf.yaml --name test-workflow --disable-webhooks -``` - - - - - -For disabling webhooks on the test workflows, set the following on the workflow spec: - -```yaml -notifications: - disableWebhooks: true -``` - -Example usage: - -```yaml -apiVersion: testworkflows.testkube.io/v1 -kind: TestWorkflow -metadata: - name: test-workflow - namespace: testkube -spec: - container: - resources: - requests: - cpu: 128m - memory: 128Mi - workingDir: /data/repo/test/k6/executor-tests - content: - git: - paths: - - test/k6/executor-tests/k6-smoke-test.js - revision: main - uri: https://github.com/kubeshop/testkube - notifications: - disableWebhooks: true - steps: - - artifacts: - paths: - - "*" - workingDir: /data/artifacts - container: - image: grafana/k6:0.49.0 - name: Run test - run: - args: - - run - - k6-smoke-test.js - - -e - - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value - env: - - name: K6_SYSTEM_ENV - value: K6_SYSTEM_ENV_value - - name: K6_WEB_DASHBOARD - value: "true" - - name: K6_WEB_DASHBOARD_EXPORT - value: /data/artifacts/k6-test-report.html - setup: - - shell: mkdir /data/artifacts +testkube run testworkflow test-workflow --disable-webhooks ``` @@ -822,82 +633,11 @@ spec: The API expects the following object set under the `spec` attribute: ```json - "notifications": { + { "disableWebhooks": true } ``` -Example usage: - -```json -{ - "name": "wf1", - "namespace": "testkube", - "created": "2024-04-04T09:15:12Z", - "spec": { - "content": { - "git": { - "uri": "https://github.com/kubeshop/testkube", - "revision": "main", - "paths": ["test/k6/executor-tests/k6-smoke-test.js"] - } - }, - "container": { - "workingDir": { - "value": "/data/repo/test/k6/executor-tests" - }, - "resources": { - "requests": { - "cpu": "128m", - "memory": "128Mi" - } - } - }, - "steps": [ - { - "name": "Run test", - "run": { - "env": [ - { - "name": "K6_SYSTEM_ENV", - "value": "K6_SYSTEM_ENV_value" - }, - { - "name": "K6_WEB_DASHBOARD", - "value": "true" - }, - { - "name": "K6_WEB_DASHBOARD_EXPORT", - "value": "/data/artifacts/k6-test-report.html" - } - ], - "args": { - "value": ["run", "k6-smoke-test.js", "-e", "K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value"] - } - }, - "container": { - "image": "grafana/k6:0.49.0" - }, - "artifacts": { - "workingDir": { - "value": "/data/artifacts" - }, - "paths": ["*"] - }, - "setup": [ - { - "shell": "mkdir /data/artifacts" - } - ] - } - ], - "notifications": { - "disableWebhooks": true - } - } -} -``` - diff --git a/docs/docs/cli/testkube.md b/docs/docs/cli/testkube.md index 2c52219ccf3..2a6926f608e 100644 --- a/docs/docs/cli/testkube.md +++ b/docs/docs/cli/testkube.md @@ -28,7 +28,7 @@ testkube [flags] * [testkube create](testkube_create.md) - Create resource * [testkube create-ticket](testkube_create-ticket.md) - Create bug ticket * [testkube dashboard](testkube_dashboard.md) - Open Testkube dashboard -* [testkube debug](testkube_debug.md) - Print environment information for debugging +* [testkube debug](testkube_debug.md) - Print debugging info * [testkube delete](testkube_delete.md) - Delete resources * [testkube disable](testkube_disable.md) - Disable feature * [testkube download](testkube_download.md) - Artifacts management commands diff --git a/docs/docs/cli/testkube_agent.md b/docs/docs/cli/testkube_agent.md index c592f851ff3..0727833a389 100644 --- a/docs/docs/cli/testkube_agent.md +++ b/docs/docs/cli/testkube_agent.md @@ -27,5 +27,4 @@ testkube agent [flags] ### SEE ALSO * [testkube](testkube.md) - Testkube entrypoint for kubectl plugin -* [testkube agent debug](testkube_agent_debug.md) - Debug Agent info diff --git a/docs/docs/cli/testkube_create_test.md b/docs/docs/cli/testkube_create_test.md index 4599ae2819d..b063de7fe87 100644 --- a/docs/docs/cli/testkube_create_test.md +++ b/docs/docs/cli/testkube_create_test.md @@ -28,8 +28,6 @@ testkube create test [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 --description string test description - --disable-webhooks disable webhooks - --enable-webhooks enable webhooks --env stringToString envs in a form of name1=val1 passed to executor (default []) --execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only) --execution-name string execution name, if empty will be autogenerated diff --git a/docs/docs/cli/testkube_create_testsuite.md b/docs/docs/cli/testkube_create_testsuite.md index 0982a691dde..b18294d128e 100644 --- a/docs/docs/cli/testkube_create_testsuite.md +++ b/docs/docs/cli/testkube_create_testsuite.md @@ -15,8 +15,6 @@ 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 - --disable-webhooks disable webhooks - --enable-webhooks enable webhooks --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 diff --git a/docs/docs/cli/testkube_create_testworkflow.md b/docs/docs/cli/testkube_create_testworkflow.md index bdd9e7d8d2e..c39b20d642a 100644 --- a/docs/docs/cli/testkube_create_testworkflow.md +++ b/docs/docs/cli/testkube_create_testworkflow.md @@ -9,11 +9,10 @@ testkube create testworkflow [flags] ### Options ``` - --disable-webhooks disable webhooks for this test workflow - -f, --file string file path to get the test workflow specification - -h, --help help for testworkflow - --name string test workflow name - --update update, if test workflow already exists + -f, --file string file path to get the test workflow specification + -h, --help help for testworkflow + --name string test workflow name + --update update, if test workflow already exists ``` ### Options inherited from parent commands diff --git a/docs/docs/cli/testkube_create_webhook.md b/docs/docs/cli/testkube_create_webhook.md index 94c6b85e24d..08897f7f0d4 100644 --- a/docs/docs/cli/testkube_create_webhook.md +++ b/docs/docs/cli/testkube_create_webhook.md @@ -14,12 +14,12 @@ testkube create webhook [flags] ``` --disable disable webhook - --enable enable webhook -e, --events stringArray event types handled by webhook e.g. start-test|end-test --header stringToString webhook header value pair (golang template supported): --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 + --on-state-change specify whether webhook should be triggered only on a state change --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 diff --git a/docs/docs/cli/testkube_debug.md b/docs/docs/cli/testkube_debug.md index 93522827c6d..1e9f9473c9f 100644 --- a/docs/docs/cli/testkube_debug.md +++ b/docs/docs/cli/testkube_debug.md @@ -1,10 +1,6 @@ ## testkube debug -Print environment information for debugging - -``` -testkube debug [flags] -``` +Print debugging info ### Options @@ -27,6 +23,7 @@ testkube debug [flags] ### SEE ALSO * [testkube](testkube.md) - Testkube entrypoint for kubectl plugin -* [testkube debug controlplane](testkube_debug_controlplane.md) - Show debug info -* [testkube debug info](testkube_debug_info.md) - Show debug info +* [testkube debug agent](testkube_debug_agent.md) - Show Agent debug information +* [testkube debug controlplane](testkube_debug_controlplane.md) - Show Control Plane debug information +* [testkube debug oss](testkube_debug_oss.md) - Show OSS installation debug info diff --git a/docs/docs/cli/testkube_debug_agent.md b/docs/docs/cli/testkube_debug_agent.md new file mode 100644 index 00000000000..f1c08ffba9e --- /dev/null +++ b/docs/docs/cli/testkube_debug_agent.md @@ -0,0 +1,35 @@ +## testkube debug agent + +Show Agent debug information + +### Synopsis + +Get all the necessary information to debug an issue in Testkube Agent you can fiter through comma separated list of items to show with additional flag `--show pods,services,ingresses,events,nats,connection,roundtrip` + +``` +testkube debug agent [flags] +``` + +### Options + +``` + -h, --help help for agent + -s, --show []string Comma-separated list of features to show, one of: pods,services,ingresses,events,nats,connection,roundtrip, defaults to all +``` + +### Options inherited from parent commands + +``` + -a, --api-uri string api uri, default value read from config if set (default "http://localhost:8088") + -c, --client string client used for connecting to Testkube API one of proxy|direct|cluster (default "proxy") + --header stringToString headers for direct client key value pair: --header name=value (default []) + --insecure insecure connection for direct client + --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 debug](testkube_debug.md) - Print debugging info + diff --git a/docs/docs/cli/testkube_debug_controlplane.md b/docs/docs/cli/testkube_debug_controlplane.md index d513f4f5dd4..4cd6f43b561 100644 --- a/docs/docs/cli/testkube_debug_controlplane.md +++ b/docs/docs/cli/testkube_debug_controlplane.md @@ -1,10 +1,10 @@ ## testkube debug controlplane -Show debug info +Show Control Plane debug information ### Synopsis -Get all the necessary information to debug an issue in Testkube Control Plane +Get all the necessary information to debug an issue in Testkube Control Plane you can fiter through comma separated list of items to show with additional flag `--show pods,services,ingresses,storageclasses,events,nats,connection,api,nats,mongo,dex,ui,worker` ``` testkube debug controlplane [flags] @@ -13,9 +13,8 @@ testkube debug controlplane [flags] ### Options ``` - --attach-agent-log Attach agent log to the output keep in mind to configure valid agent first in the Testkube CLI - -h, --help help for controlplane - --labels stringToString Labels to filter logs by (default []) + -h, --help help for controlplane + -s, --show []string Comma-separated list of features to show, one of: pods,services,ingresses,storageclasses,events,nats,connection,api,nats,mongo,dex,ui,worker, defaults to all ``` ### Options inherited from parent commands @@ -32,5 +31,5 @@ testkube debug controlplane [flags] ### SEE ALSO -* [testkube debug](testkube_debug.md) - Print environment information for debugging +* [testkube debug](testkube_debug.md) - Print debugging info diff --git a/docs/docs/cli/testkube_debug_oss.md b/docs/docs/cli/testkube_debug_oss.md new file mode 100644 index 00000000000..a80ade10577 --- /dev/null +++ b/docs/docs/cli/testkube_debug_oss.md @@ -0,0 +1,30 @@ +## testkube debug oss + +Show OSS installation debug info + +``` +testkube debug oss [flags] +``` + +### Options + +``` + -h, --help help for oss +``` + +### Options inherited from parent commands + +``` + -a, --api-uri string api uri, default value read from config if set (default "http://localhost:8088") + -c, --client string client used for connecting to Testkube API one of proxy|direct|cluster (default "proxy") + --header stringToString headers for direct client key value pair: --header name=value (default []) + --insecure insecure connection for direct client + --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 debug](testkube_debug.md) - Print debugging info + diff --git a/docs/docs/cli/testkube_generate_tests-crds.md b/docs/docs/cli/testkube_generate_tests-crds.md index a6566e42d86..79fa5113b2c 100644 --- a/docs/docs/cli/testkube_generate_tests-crds.md +++ b/docs/docs/cli/testkube_generate_tests-crds.md @@ -28,8 +28,6 @@ testkube generate tests-crds [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 --description string test description - --disable-webhooks disable webhooks - --enable-webhooks enable webhooks --env stringToString envs in a form of name1=val1 passed to executor (default []) --execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only) --execution-name string execution name, if empty will be autogenerated diff --git a/docs/docs/cli/testkube_run_test.md b/docs/docs/cli/testkube_run_test.md index b893b8ad0ff..bb8c3fd8b86 100644 --- a/docs/docs/cli/testkube_run_test.md +++ b/docs/docs/cli/testkube_run_test.md @@ -33,7 +33,6 @@ testkube run test [flags] --disable-webhooks disable webhooks -d, --download-artifacts download artifacts automatically --download-dir string download dir (default "artifacts") - --enable-webhooks enable webhooks --execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only) --execution-label stringToString execution-label key value pair: --execution-label key1=value1 (default []) --execution-namespace string namespace for test execution (Pro edition only) diff --git a/docs/docs/cli/testkube_run_testsuite.md b/docs/docs/cli/testkube_run_testsuite.md index f96d69df2c6..0172faaaa58 100644 --- a/docs/docs/cli/testkube_run_testsuite.md +++ b/docs/docs/cli/testkube_run_testsuite.md @@ -18,7 +18,6 @@ testkube run testsuite [flags] --disable-webhooks disable webhooks -d, --download-artifacts download artifacts automatically --download-dir string download dir (default "artifacts") - --enable-webhooks enable webhooks --execution-label stringToString execution-label adds a label to execution in form of key value pair: --execution-label key1=value1 (default []) --format string data format for storing files, one of folder|archive (default "folder") --git-branch string if uri is git repository we can set additional branch parameter diff --git a/docs/docs/cli/testkube_run_testworkflow.md b/docs/docs/cli/testkube_run_testworkflow.md index 1d7658dd59b..7c6d5caf83b 100644 --- a/docs/docs/cli/testkube_run_testworkflow.md +++ b/docs/docs/cli/testkube_run_testworkflow.md @@ -11,7 +11,6 @@ testkube run testworkflow [name] [flags] ``` --config stringToString configuration variables in a form of name1=val1 passed to executor (default []) --disable-webhooks disable webhooks for this execution - --enable-webhooks enable webhooks for this execution -h, --help help for testworkflow -n, --name string execution name, if empty will be autogenerated -f, --watch watch for changes after start diff --git a/docs/docs/cli/testkube_update_test.md b/docs/docs/cli/testkube_update_test.md index 73761b86ef4..70836df9110 100644 --- a/docs/docs/cli/testkube_update_test.md +++ b/docs/docs/cli/testkube_update_test.md @@ -28,8 +28,6 @@ testkube update test [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 --description string test description - --disable-webhooks disable webhooks - --enable-webhooks enable webhooks --env stringToString envs in a form of name1=val1 passed to executor (default []) --execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only) --execution-name string execution name, if empty will be autogenerated diff --git a/docs/docs/cli/testkube_update_testsuite.md b/docs/docs/cli/testkube_update_testsuite.md index a0b74cf4cb6..aba8477f0d3 100644 --- a/docs/docs/cli/testkube_update_testsuite.md +++ b/docs/docs/cli/testkube_update_testsuite.md @@ -15,8 +15,6 @@ 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 - --disable-webhooks disable webhooks - --enable-webhooks enable webhooks --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 diff --git a/docs/docs/cli/testkube_update_webhook.md b/docs/docs/cli/testkube_update_webhook.md index f21fe9d5894..1bb3168c87e 100644 --- a/docs/docs/cli/testkube_update_webhook.md +++ b/docs/docs/cli/testkube_update_webhook.md @@ -14,12 +14,12 @@ testkube update webhook [flags] ``` --disable disable webhook - --enable enable webhook -e, --events stringArray event types handled by webhook e.g. start-test|end-test --header stringToString webhook header value pair (golang template supported): --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 + --on-state-change specify whether webhook should be triggered only on a state change --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 diff --git a/go.mod b/go.mod index 18be0b41937..f268deb8dc9 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kelseyhightower/envconfig v1.4.0 github.com/kubepug/kubepug v1.7.1 - github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240625102049-77e0cc1374e9 + github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240716095745-a1fa1f1a8d25 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 c824cf603b9..8a9506cbf60 100644 --- a/go.sum +++ b/go.sum @@ -358,8 +358,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubepug/kubepug v1.7.1 h1:LKhfSxS8Y5mXs50v+3Lpyec+cogErDLcV7CMUuiaisw= github.com/kubepug/kubepug v1.7.1/go.mod h1:lv+HxD0oTFL7ZWjj0u6HKhMbbTIId3eG7aWIW0gyF8g= -github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240625102049-77e0cc1374e9 h1:zqGRFwBshCRZ+FdHl2Ty5E6NrwfzNUpuDL+BwKe2L7M= -github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240625102049-77e0cc1374e9/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk= +github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240716095745-a1fa1f1a8d25 h1:U0IaIRvYnx3THAvKc2Y2P9Xp4aKUj5E3cpTmA6VGLU8= +github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240716095745-a1fa1f1a8d25/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk= 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/pkg/api/v1/client/testsuite.go b/pkg/api/v1/client/testsuite.go index 8578a2c057d..d798bcf3b83 100644 --- a/pkg/api/v1/client/testsuite.go +++ b/pkg/api/v1/client/testsuite.go @@ -162,6 +162,7 @@ func (c TestSuiteClient) ExecuteTestSuite(id, executionName string, options Exec ScraperTemplateReference: options.ScraperTemplateReference, PvcTemplate: options.PvcTemplate, PvcTemplateReference: options.PvcTemplateReference, + DisableWebhooks: options.DisableWebhooks, } body, err := json.Marshal(executionRequest) @@ -190,6 +191,7 @@ func (c TestSuiteClient) ExecuteTestSuites(selector string, concurrencyLevel int ScraperTemplateReference: options.ScraperTemplateReference, PvcTemplate: options.PvcTemplate, PvcTemplateReference: options.PvcTemplateReference, + DisableWebhooks: options.DisableWebhooks, } body, err := json.Marshal(executionRequest) diff --git a/pkg/api/v1/testkube/model_test_suite_execution_extended.go b/pkg/api/v1/testkube/model_test_suite_execution_extended.go index 2f7e45d70a7..7d5c3dff2b0 100644 --- a/pkg/api/v1/testkube/model_test_suite_execution_extended.go +++ b/pkg/api/v1/testkube/model_test_suite_execution_extended.go @@ -39,11 +39,11 @@ func NewStartedTestSuiteExecution(testSuite TestSuite, request TestSuiteExecutio Variables: map[string]Variable{}, RunningContext: request.RunningContext, TestSuiteExecutionName: request.TestSuiteExecutionName, + DisableWebhooks: request.DisableWebhooks, } if testSuite.ExecutionRequest != nil { testExecution.Variables = testSuite.ExecutionRequest.Variables - testExecution.DisableWebhooks = testSuite.ExecutionRequest.DisableWebhooks } // override variables from request 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 a99b2a7f663..fe4c874b0e1 100644 --- a/pkg/api/v1/testkube/model_test_suite_execution_request.go +++ b/pkg/api/v1/testkube/model_test_suite_execution_request.go @@ -54,6 +54,6 @@ type TestSuiteExecutionRequest struct { ConcurrencyLevel int32 `json:"concurrencyLevel,omitempty"` // test suite execution name started the test suite execution TestSuiteExecutionName string `json:"testSuiteExecutionName,omitempty"` - // whether webhooks on the executions of this test suite are disabled + // whether webhooks on the execution of this test suite are disabled DisableWebhooks bool `json:"disableWebhooks,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_suite_execution_update_request.go b/pkg/api/v1/testkube/model_test_suite_execution_update_request.go index c844f5c48b5..59c41e7d075 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 @@ -54,6 +54,6 @@ type TestSuiteExecutionUpdateRequest struct { ConcurrencyLevel *int32 `json:"concurrencyLevel,omitempty"` // test suite execution name started the test suite execution TestSuiteExecutionName *string `json:"testSuiteExecutionName,omitempty"` - // whether webhooks on the executions of this test suite are disabled + // whether webhooks on the execution of this test suite are disabled DisableWebhooks *bool `json:"disableWebhooks,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_suite_step_execution_request.go b/pkg/api/v1/testkube/model_test_suite_step_execution_request.go index 053366e55ce..ca7734c0762 100644 --- a/pkg/api/v1/testkube/model_test_suite_step_execution_request.go +++ b/pkg/api/v1/testkube/model_test_suite_step_execution_request.go @@ -45,6 +45,6 @@ type TestSuiteStepExecutionRequest struct { // name of the template resource PvcTemplateReference string `json:"pvcTemplateReference,omitempty"` RunningContext *RunningContext `json:"runningContext,omitempty"` - // whether webhooks on the executions of this step are disabled + // whether webhooks on the execution of this step are disabled DisableWebhooks bool `json:"disableWebhooks,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_workflow_execution.go b/pkg/api/v1/testkube/model_test_workflow_execution.go index 0c06aca5e27..ff7774c6aa6 100644 --- a/pkg/api/v1/testkube/model_test_workflow_execution.go +++ b/pkg/api/v1/testkube/model_test_workflow_execution.go @@ -37,6 +37,6 @@ type TestWorkflowExecution struct { ResolvedWorkflow *TestWorkflow `json:"resolvedWorkflow,omitempty"` // test workflow execution name started the test workflow execution TestWorkflowExecutionName string `json:"testWorkflowExecutionName,omitempty"` - // whether webhooks on the executions of this test workflow are disabled + // whether webhooks on the execution of this test workflow are disabled DisableWebhooks bool `json:"disableWebhooks,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_cr.go b/pkg/api/v1/testkube/model_test_workflow_execution_cr.go index 347122569aa..a80bad3d99a 100644 --- a/pkg/api/v1/testkube/model_test_workflow_execution_cr.go +++ b/pkg/api/v1/testkube/model_test_workflow_execution_cr.go @@ -13,6 +13,4 @@ type TestWorkflowExecutionCr struct { TestWorkflow *ObjectRef `json:"testWorkflow"` ExecutionRequest *TestWorkflowExecutionRequest `json:"executionRequest,omitempty"` Status *TestWorkflowExecutionStatusCr `json:"status,omitempty"` - // disable webhooks for this execution - DisableWebhooks bool `json:"disableWebhooks,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_request.go b/pkg/api/v1/testkube/model_test_workflow_execution_request.go index d3ed46b6979..1bf75035185 100644 --- a/pkg/api/v1/testkube/model_test_workflow_execution_request.go +++ b/pkg/api/v1/testkube/model_test_workflow_execution_request.go @@ -15,6 +15,6 @@ type TestWorkflowExecutionRequest struct { Config map[string]string `json:"config,omitempty"` // test workflow execution name started the test workflow execution TestWorkflowExecutionName string `json:"testWorkflowExecutionName,omitempty"` - // whether webhooks on the executions of this test workflow are disabled + // whether webhooks on the execution of this test workflow are disabled DisableWebhooks bool `json:"disableWebhooks,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_workflow_notifications_config.go b/pkg/api/v1/testkube/model_test_workflow_notifications_config.go deleted file mode 100644 index 14f34fa8dc5..00000000000 --- a/pkg/api/v1/testkube/model_test_workflow_notifications_config.go +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 TestWorkflowNotificationsConfig struct { - // disable webhooks for this test workflow - DisableWebhooks bool `json:"disableWebhooks,omitempty"` -} diff --git a/pkg/api/v1/testkube/model_test_workflow_spec.go b/pkg/api/v1/testkube/model_test_workflow_spec.go index e61d0fe83a5..dace346a30d 100644 --- a/pkg/api/v1/testkube/model_test_workflow_spec.go +++ b/pkg/api/v1/testkube/model_test_workflow_spec.go @@ -10,16 +10,15 @@ package testkube type TestWorkflowSpec struct { - Use []TestWorkflowTemplateRef `json:"use,omitempty"` - Config map[string]TestWorkflowParameterSchema `json:"config,omitempty"` - Content *TestWorkflowContent `json:"content,omitempty"` - Services map[string]TestWorkflowServiceSpec `json:"services,omitempty"` - Container *TestWorkflowContainerConfig `json:"container,omitempty"` - Job *TestWorkflowJobConfig `json:"job,omitempty"` - Pod *TestWorkflowPodConfig `json:"pod,omitempty"` - Setup []TestWorkflowStep `json:"setup,omitempty"` - Steps []TestWorkflowStep `json:"steps,omitempty"` - After []TestWorkflowStep `json:"after,omitempty"` - Events []TestWorkflowEvent `json:"events,omitempty"` - Notifications *TestWorkflowNotificationsConfig `json:"notifications,omitempty"` + Use []TestWorkflowTemplateRef `json:"use,omitempty"` + Config map[string]TestWorkflowParameterSchema `json:"config,omitempty"` + Content *TestWorkflowContent `json:"content,omitempty"` + Services map[string]TestWorkflowServiceSpec `json:"services,omitempty"` + Container *TestWorkflowContainerConfig `json:"container,omitempty"` + Job *TestWorkflowJobConfig `json:"job,omitempty"` + Pod *TestWorkflowPodConfig `json:"pod,omitempty"` + Setup []TestWorkflowStep `json:"setup,omitempty"` + Steps []TestWorkflowStep `json:"steps,omitempty"` + After []TestWorkflowStep `json:"after,omitempty"` + Events []TestWorkflowEvent `json:"events,omitempty"` } diff --git a/pkg/api/v1/testkube/model_test_workflow_step_parallel.go b/pkg/api/v1/testkube/model_test_workflow_step_parallel.go index b296a1e92dc..fbea0caff01 100644 --- a/pkg/api/v1/testkube/model_test_workflow_step_parallel.go +++ b/pkg/api/v1/testkube/model_test_workflow_step_parallel.go @@ -28,22 +28,21 @@ type TestWorkflowStepParallel struct { // delay before the step Delay string `json:"delay,omitempty"` // script to run in a default shell for the container - Shell string `json:"shell,omitempty"` - Run *TestWorkflowStepRun `json:"run,omitempty"` - Execute *TestWorkflowStepExecute `json:"execute,omitempty"` - Artifacts *TestWorkflowStepArtifacts `json:"artifacts,omitempty"` - Use []TestWorkflowTemplateRef `json:"use,omitempty"` - Config map[string]TestWorkflowParameterSchema `json:"config,omitempty"` - Content *TestWorkflowContent `json:"content,omitempty"` - Services map[string]TestWorkflowServiceSpec `json:"services,omitempty"` - Container *TestWorkflowContainerConfig `json:"container,omitempty"` - Job *TestWorkflowJobConfig `json:"job,omitempty"` - Pod *TestWorkflowPodConfig `json:"pod,omitempty"` - Setup []TestWorkflowStep `json:"setup,omitempty"` - Steps []TestWorkflowStep `json:"steps,omitempty"` - After []TestWorkflowStep `json:"after,omitempty"` - Events []TestWorkflowEvent `json:"events,omitempty"` - Notifications *TestWorkflowNotificationsConfig `json:"notifications,omitempty"` + Shell string `json:"shell,omitempty"` + Run *TestWorkflowStepRun `json:"run,omitempty"` + Execute *TestWorkflowStepExecute `json:"execute,omitempty"` + Artifacts *TestWorkflowStepArtifacts `json:"artifacts,omitempty"` + Use []TestWorkflowTemplateRef `json:"use,omitempty"` + Config map[string]TestWorkflowParameterSchema `json:"config,omitempty"` + Content *TestWorkflowContent `json:"content,omitempty"` + Services map[string]TestWorkflowServiceSpec `json:"services,omitempty"` + Container *TestWorkflowContainerConfig `json:"container,omitempty"` + Job *TestWorkflowJobConfig `json:"job,omitempty"` + Pod *TestWorkflowPodConfig `json:"pod,omitempty"` + Setup []TestWorkflowStep `json:"setup,omitempty"` + Steps []TestWorkflowStep `json:"steps,omitempty"` + After []TestWorkflowStep `json:"after,omitempty"` + Events []TestWorkflowEvent `json:"events,omitempty"` // how many resources could be scheduled in parallel Parallelism int32 `json:"parallelism,omitempty"` // worker description to display diff --git a/pkg/mapper/testexecutions/mapper.go b/pkg/mapper/testexecutions/mapper.go index b89aac2cd9f..78a06fe2346 100644 --- a/pkg/mapper/testexecutions/mapper.go +++ b/pkg/mapper/testexecutions/mapper.go @@ -215,6 +215,7 @@ func MapAPIToCRD(request *testkube.Execution, generation int64) testexecutionv1. RunningContext: runningContext, ContainerShell: request.ContainerShell, SlavePodRequest: podRequest, + DisableWebhooks: request.DisableWebhooks, }, } diff --git a/pkg/mapper/tests/kube_openapi.go b/pkg/mapper/tests/kube_openapi.go index 767e746ff0d..14b4e90e33c 100644 --- a/pkg/mapper/tests/kube_openapi.go +++ b/pkg/mapper/tests/kube_openapi.go @@ -195,7 +195,6 @@ func MapExecutionRequestFromSpec(specExecutionRequest *testsv3.ExecutionRequest) EnvConfigMaps: MapEnvReferences(specExecutionRequest.EnvConfigMaps), EnvSecrets: MapEnvReferences(specExecutionRequest.EnvSecrets), SlavePodRequest: podRequest, - DisableWebhooks: specExecutionRequest.DisableWebhooks, } // Pro edition only (tcl protected code) @@ -527,7 +526,6 @@ func MapSpecExecutionRequestToExecutionUpdateRequest( executionRequest.EnvSecrets = &envSecrets executionRequest.ExecutePostRunScriptBeforeScraping = &request.ExecutePostRunScriptBeforeScraping executionRequest.SourceScripts = &request.SourceScripts - executionRequest.DisableWebhooks = &request.DisableWebhooks // Pro edition only (tcl protected code) mappertcl.MapSpecExecutionRequestToExecutionUpdateRequest(request, executionRequest) diff --git a/pkg/mapper/tests/openapi_kube.go b/pkg/mapper/tests/openapi_kube.go index 80f73ed3912..00c6b36e96f 100644 --- a/pkg/mapper/tests/openapi_kube.go +++ b/pkg/mapper/tests/openapi_kube.go @@ -207,7 +207,6 @@ func MapExecutionRequestToSpecExecutionRequest(executionRequest *testkube.Execut EnvConfigMaps: mapEnvReferences(executionRequest.EnvConfigMaps), EnvSecrets: mapEnvReferences(executionRequest.EnvSecrets), SlavePodRequest: podRequest, - DisableWebhooks: executionRequest.DisableWebhooks, } // Pro edition only (tcl protected code) @@ -635,11 +634,6 @@ func MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest *testkube. emptyExecution = false } - if executionRequest.DisableWebhooks != nil { - request.DisableWebhooks = *executionRequest.DisableWebhooks - emptyExecution = false - } - // Pro edition only (tcl protected code) if !mappertcl.MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest, request) { emptyExecution = false diff --git a/pkg/mapper/testsuiteexecutions/mapper.go b/pkg/mapper/testsuiteexecutions/mapper.go index 8e545cd3c70..34f5748c4b9 100644 --- a/pkg/mapper/testsuiteexecutions/mapper.go +++ b/pkg/mapper/testsuiteexecutions/mapper.go @@ -368,6 +368,7 @@ func MapAPIToCRD(request *testkube.TestSuiteExecution, generation int64) testsui ExecuteStepResults: executeStepResults, Labels: request.Labels, RunningContext: runningContext, + DisableWebhooks: request.DisableWebhooks, }, } diff --git a/pkg/mapper/testsuites/kube_openapi.go b/pkg/mapper/testsuites/kube_openapi.go index 7b5549289b0..4f384847350 100644 --- a/pkg/mapper/testsuites/kube_openapi.go +++ b/pkg/mapper/testsuites/kube_openapi.go @@ -192,7 +192,6 @@ func MapExecutionRequestFromSpec(specExecutionRequest *testsuitesv3.TestSuiteExe PvcTemplateReference: specExecutionRequest.PvcTemplateReference, ScraperTemplate: specExecutionRequest.ScraperTemplate, ScraperTemplateReference: specExecutionRequest.ScraperTemplateReference, - DisableWebhooks: specExecutionRequest.DisableWebhooks, } } @@ -354,7 +353,6 @@ func MapSpecExecutionRequestToExecutionUpdateRequest(request *testsuitesv3.TestS vars := MergeVariablesAndParams(request.Variables, nil) executionRequest.Variables = &vars - executionRequest.DisableWebhooks = &request.DisableWebhooks return executionRequest } @@ -406,6 +404,5 @@ func MapTestStepExecutionRequestCRDToAPI(request *testsuitesv3.TestSuiteStepExec PvcTemplate: request.PvcTemplate, PvcTemplateReference: request.PvcTemplateReference, RunningContext: runningContext, - DisableWebhooks: request.DisableWebhooks, } } diff --git a/pkg/mapper/testsuites/openapi_kube.go b/pkg/mapper/testsuites/openapi_kube.go index 6ea10a9c95f..6195612252b 100644 --- a/pkg/mapper/testsuites/openapi_kube.go +++ b/pkg/mapper/testsuites/openapi_kube.go @@ -230,7 +230,6 @@ func MapExecutionRequestToSpecExecutionRequest(executionRequest *testkube.TestSu ScraperTemplateReference: executionRequest.ScraperTemplateReference, PvcTemplate: executionRequest.PvcTemplate, PvcTemplateReference: executionRequest.PvcTemplateReference, - DisableWebhooks: executionRequest.DisableWebhooks, } } @@ -404,11 +403,6 @@ func MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest *testkube. empty = false } - if executionRequest.DisableWebhooks != nil { - request.DisableWebhooks = *executionRequest.DisableWebhooks - empty = false - } - if empty { return nil } @@ -487,7 +481,6 @@ func MapTestStepExecutionRequestCRD(request *testkube.TestSuiteStepExecutionRequ PvcTemplate: request.PvcTemplate, PvcTemplateReference: request.PvcTemplateReference, RunningContext: runningContext, - DisableWebhooks: request.DisableWebhooks, } } diff --git a/pkg/mapper/testworkflows/kube_openapi.go b/pkg/mapper/testworkflows/kube_openapi.go index 3c38f805984..b87ef748650 100644 --- a/pkg/mapper/testworkflows/kube_openapi.go +++ b/pkg/mapper/testworkflows/kube_openapi.go @@ -1081,18 +1081,17 @@ func MapIndependentStepKubeToAPI(v testworkflowsv1.IndependentStep) testkube.Tes func MapSpecKubeToAPI(v testworkflowsv1.TestWorkflowSpec) testkube.TestWorkflowSpec { return testkube.TestWorkflowSpec{ - Use: common.MapSlice(v.Use, MapTemplateRefKubeToAPI), - Config: common.MapMap(v.Config, MapParameterSchemaKubeToAPI), - Content: common.MapPtr(v.Content, MapContentKubeToAPI), - Services: common.MapMap(v.Services, MapServiceSpecKubeToAPI), - Container: common.MapPtr(v.Container, MapContainerConfigKubeToAPI), - Job: common.MapPtr(v.Job, MapJobConfigKubeToAPI), - Pod: common.MapPtr(v.Pod, MapPodConfigKubeToAPI), - Setup: common.MapSlice(v.Setup, MapStepKubeToAPI), - Steps: common.MapSlice(v.Steps, MapStepKubeToAPI), - After: common.MapSlice(v.After, MapStepKubeToAPI), - Events: common.MapSlice(v.Events, MapEventKubeToAPI), - Notifications: common.MapPtr(v.Notifications, MapNotificationsConfigKubeToAPI), + Use: common.MapSlice(v.Use, MapTemplateRefKubeToAPI), + Config: common.MapMap(v.Config, MapParameterSchemaKubeToAPI), + Content: common.MapPtr(v.Content, MapContentKubeToAPI), + Services: common.MapMap(v.Services, MapServiceSpecKubeToAPI), + Container: common.MapPtr(v.Container, MapContainerConfigKubeToAPI), + Job: common.MapPtr(v.Job, MapJobConfigKubeToAPI), + Pod: common.MapPtr(v.Pod, MapPodConfigKubeToAPI), + Setup: common.MapSlice(v.Setup, MapStepKubeToAPI), + Steps: common.MapSlice(v.Steps, MapStepKubeToAPI), + After: common.MapSlice(v.After, MapStepKubeToAPI), + Events: common.MapSlice(v.Events, MapEventKubeToAPI), } } @@ -1157,9 +1156,3 @@ func MapTemplateListKubeToAPI(v *testworkflowsv1.TestWorkflowTemplateList) []tes } return workflows } - -func MapNotificationsConfigKubeToAPI(v testworkflowsv1.NotificationsConfig) testkube.TestWorkflowNotificationsConfig { - return testkube.TestWorkflowNotificationsConfig{ - DisableWebhooks: v.DisableWebhooks, - } -} diff --git a/pkg/mapper/testworkflows/openapi_kube.go b/pkg/mapper/testworkflows/openapi_kube.go index d7a328bf039..8992de1fa13 100644 --- a/pkg/mapper/testworkflows/openapi_kube.go +++ b/pkg/mapper/testworkflows/openapi_kube.go @@ -908,12 +908,11 @@ func MapStepParallelAPIToKube(v testkube.TestWorkflowStepParallel) testworkflows Fetch: common.MapSlice(v.Fetch, MapStepParallelFetchAPIToKube), TestWorkflowSpec: testworkflowsv1.TestWorkflowSpec{ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{ - Config: common.MapMap(v.Config, MapParameterSchemaAPIToKube), - Content: common.MapPtr(v.Content, MapContentAPIToKube), - Container: common.MapPtr(v.Container, MapContainerConfigAPIToKube), - Job: common.MapPtr(v.Job, MapJobConfigAPIToKube), - Pod: common.MapPtr(v.Pod, MapPodConfigAPIToKube), - Notifications: common.MapPtr(v.Notifications, MapNotificationsAPIToKube), + Config: common.MapMap(v.Config, MapParameterSchemaAPIToKube), + Content: common.MapPtr(v.Content, MapContentAPIToKube), + Container: common.MapPtr(v.Container, MapContainerConfigAPIToKube), + Job: common.MapPtr(v.Job, MapJobConfigAPIToKube), + Pod: common.MapPtr(v.Pod, MapPodConfigAPIToKube), }, Use: common.MapSlice(v.Use, MapTemplateRefAPIToKube), Setup: common.MapSlice(v.Setup, MapStepAPIToKube), @@ -1378,6 +1377,7 @@ func MapTestWorkflowExecutionAPIToKube(v *testkube.TestWorkflowExecution) *testw Workflow: common.MapPtr(v.Workflow, MapTestWorkflowAPIToKube), ResolvedWorkflow: common.MapPtr(v.ResolvedWorkflow, MapTestWorkflowAPIToKube), TestWorkflowExecutionName: v.TestWorkflowExecutionName, + DisableWebhooks: v.DisableWebhooks, } } @@ -1425,9 +1425,3 @@ func MapTestWorkflowAPIToKubeTestWorkflowSummary(v testkube.TestWorkflow) testwo Annotations: v.Annotations, } } - -func MapNotificationsAPIToKube(v testkube.TestWorkflowNotificationsConfig) testworkflowsv1.NotificationsConfig { - return testworkflowsv1.NotificationsConfig{ - DisableWebhooks: v.DisableWebhooks, - } -} diff --git a/pkg/scheduler/test_scheduler.go b/pkg/scheduler/test_scheduler.go index e7f374c5183..9a654c49d55 100644 --- a/pkg/scheduler/test_scheduler.go +++ b/pkg/scheduler/test_scheduler.go @@ -59,10 +59,6 @@ func (s *Scheduler) executeTest(ctx context.Context, test testkube.Test, request request.Name = fmt.Sprintf("%s-%d", request.Name, request.Number) } - if !request.DisableWebhooks && test.ExecutionRequest != nil { - request.DisableWebhooks = test.ExecutionRequest.DisableWebhooks - } - // test name + test execution name should be unique execution, _ = s.testResults.GetByNameAndTest(ctx, request.Name, test.Name) diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go index edff555e354..66f7bbd56f2 100644 --- a/pkg/scheduler/testsuite_scheduler.go +++ b/pkg/scheduler/testsuite_scheduler.go @@ -118,10 +118,6 @@ func (s *Scheduler) executeTestSuite(ctx context.Context, testSuite testkube.Tes request.Name = fmt.Sprintf("ts-%s-%d", testSuite.Name, request.Number) } - if testSuite.ExecutionRequest != nil && testSuite.ExecutionRequest.DisableWebhooks { - request.DisableWebhooks = testSuite.ExecutionRequest.DisableWebhooks - } - testsuiteExecution = testkube.NewStartedTestSuiteExecution(testSuite, request) err = s.testsuiteResults.Insert(ctx, testsuiteExecution) if err != nil { diff --git a/pkg/testworkflows/testworkflowexecutor/executor.go b/pkg/testworkflows/testworkflowexecutor/executor.go index f7c02dff686..46e89e5e536 100644 --- a/pkg/testworkflows/testworkflowexecutor/executor.go +++ b/pkg/testworkflows/testworkflowexecutor/executor.go @@ -392,11 +392,6 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor return execution, fmt.Errorf("not supported execution namespace %s", namespace) } - disableWebhooks := request.DisableWebhooks - if !disableWebhooks && workflow.Spec.Notifications != nil { - disableWebhooks = workflow.Spec.Notifications.DisableWebhooks - } - // Build the basic Execution data id := primitive.NewObjectID().Hex() now := time.Now() @@ -444,10 +439,11 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor "fsPrefix": "", }) mockExecutionMachine := expressions.NewMachine().Register("execution", map[string]interface{}{ - "id": id, - "name": "", - "number": "1", - "scheduledAt": now.UTC().Format(constants.RFC3339Millis), + "id": id, + "name": "", + "number": "1", + "scheduledAt": now.UTC().Format(constants.RFC3339Millis), + "disableWebhooks": request.DisableWebhooks, }) // Preserve resolved TestWorkflow @@ -485,10 +481,11 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor // Build machine with actual execution data executionMachine := expressions.NewMachine().Register("execution", map[string]interface{}{ - "id": id, - "name": executionName, - "number": number, - "scheduledAt": now.UTC().Format(constants.RFC3339Millis), + "id": id, + "name": executionName, + "number": number, + "scheduledAt": now.UTC().Format(constants.RFC3339Millis), + "disableWebhooks": request.DisableWebhooks, }) // Process the TestWorkflow @@ -519,7 +516,7 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor Workflow: testworkflowmappers.MapKubeToAPI(initialWorkflow), ResolvedWorkflow: testworkflowmappers.MapKubeToAPI(resolvedWorkflow), TestWorkflowExecutionName: testWorkflowExecutionName, - DisableWebhooks: disableWebhooks, + DisableWebhooks: request.DisableWebhooks, } err = e.repository.Insert(ctx, execution) if err != nil { diff --git a/pkg/testworkflows/testworkflowprocessor/container.go b/pkg/testworkflows/testworkflowprocessor/container.go index 1546965341b..fba2a2d4950 100644 --- a/pkg/testworkflows/testworkflowprocessor/container.go +++ b/pkg/testworkflows/testworkflowprocessor/container.go @@ -429,6 +429,7 @@ func (c *container) EnableToolkit(ref string) Container { "TK_EXN": "{{execution.name}}", "TK_EXC": "{{execution.number}}", "TK_EXS": "{{execution.scheduledAt}}", + "TK_DWH": "{{execution.disableWebhooks}}", "TK_EXI": "{{resource.id}}", "TK_EXR": "{{resource.root}}", "TK_FS": "{{resource.fsPrefix}}", From 4e3477f6e1880c2835fed9597255d41762dfe385 Mon Sep 17 00:00:00 2001 From: Dejan Zele Pejchev Date: Wed, 17 Jul 2024 16:53:47 +0200 Subject: [PATCH 10/16] add support for defining TTL for image credentials cache (#5660) --- cmd/api-server/main.go | 7 +- cmd/tcl/testworkflow-toolkit/spawn/utils.go | 1 + cmd/testworkflow-toolkit/env/client.go | 6 +- cmd/testworkflow-toolkit/env/config.go | 9 +- internal/config/config.go | 59 ++--- pkg/cache/cache.go | 33 +++ pkg/cache/inmem.go | 71 ++++++ pkg/cache/inmem_test.go | 223 ++++++++++++++++++ .../containerexecutor/containerexecutor.go | 62 +++-- pkg/imageinspector/secretfetcher.go | 66 ++++-- pkg/imageinspector/secretfetcher_test.go | 42 +++- .../testworkflowexecutor/executor.go | 1 + .../testworkflowprocessor/container.go | 1 + 13 files changed, 502 insertions(+), 79 deletions(-) create mode 100644 pkg/cache/cache.go create mode 100644 pkg/cache/inmem.go create mode 100644 pkg/cache/inmem_test.go diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 73bbf58d5aa..97222a56e8f 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -13,6 +13,10 @@ import ( "syscall" "time" + corev1 "k8s.io/api/core/v1" + + "github.com/kubeshop/testkube/pkg/cache" + "github.com/nats-io/nats.go" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" @@ -487,7 +491,7 @@ func main() { inspector := imageinspector.NewInspector( cfg.TestkubeRegistry, imageinspector.NewSkopeoFetcher(), - imageinspector.NewSecretFetcher(secretClient), + imageinspector.NewSecretFetcher(secretClient, cache.NewInMemoryCache[*corev1.Secret](), imageinspector.WithSecretCacheTTL(cfg.TestkubeImageCredentialsCacheTTL)), inspectorStorages..., ) @@ -515,6 +519,7 @@ func main() { features, cfg.TestkubeDefaultStorageClassName, cfg.WhitelistedContainers, + cfg.TestkubeImageCredentialsCacheTTL, ) if err != nil { exitOnError("Creating container executor", err) diff --git a/cmd/tcl/testworkflow-toolkit/spawn/utils.go b/cmd/tcl/testworkflow-toolkit/spawn/utils.go index acfe2b137eb..6d6db65b967 100644 --- a/cmd/tcl/testworkflow-toolkit/spawn/utils.go +++ b/cmd/tcl/testworkflow-toolkit/spawn/utils.go @@ -299,6 +299,7 @@ func CreateBaseMachine() expressions.Machine { "images.toolkit": env.Config().Images.Toolkit, "images.persistence.enabled": strconv.FormatBool(env.Config().Images.InspectorPersistenceEnabled), "images.persistence.key": env.Config().Images.InspectorPersistenceCacheKey, + "images.cache.ttl": env.Config().Images.ImageCredentialsCacheTTL.String(), }), ) } diff --git a/cmd/testworkflow-toolkit/env/client.go b/cmd/testworkflow-toolkit/env/client.go index d59cb72c1a5..ab6ca5528d8 100644 --- a/cmd/testworkflow-toolkit/env/client.go +++ b/cmd/testworkflow-toolkit/env/client.go @@ -5,6 +5,10 @@ import ( "fmt" "math" + corev1 "k8s.io/api/core/v1" + + "github.com/kubeshop/testkube/pkg/cache" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -58,7 +62,7 @@ func ImageInspector() imageinspector.Inspector { return imageinspector.NewInspector( Config().System.DefaultRegistry, imageinspector.NewSkopeoFetcher(), - imageinspector.NewSecretFetcher(secretClient), + imageinspector.NewSecretFetcher(secretClient, cache.NewInMemoryCache[*corev1.Secret](), imageinspector.WithSecretCacheTTL(Config().Images.ImageCredentialsCacheTTL)), inspectorStorages..., ) } diff --git a/cmd/testworkflow-toolkit/env/config.go b/cmd/testworkflow-toolkit/env/config.go index 2d301a16a93..23862f9d799 100644 --- a/cmd/testworkflow-toolkit/env/config.go +++ b/cmd/testworkflow-toolkit/env/config.go @@ -60,10 +60,11 @@ type envSystemConfig struct { } type envImagesConfig struct { - Init string `envconfig:"TESTKUBE_TW_INIT_IMAGE"` - Toolkit string `envconfig:"TESTKUBE_TW_TOOLKIT_IMAGE"` - InspectorPersistenceEnabled bool `envconfig:"TK_IMG_P" default:"false"` - InspectorPersistenceCacheKey string `envconfig:"TK_IMG_PK"` + Init string `envconfig:"TESTKUBE_TW_INIT_IMAGE"` + Toolkit string `envconfig:"TESTKUBE_TW_TOOLKIT_IMAGE"` + InspectorPersistenceEnabled bool `envconfig:"TK_IMG_P" default:"false"` + InspectorPersistenceCacheKey string `envconfig:"TK_IMG_PK"` + ImageCredentialsCacheTTL time.Duration `envconfig:"TK_IMG_CRED_TTL" default:"0"` } type featuresConfig struct { diff --git a/internal/config/config.go b/internal/config/config.go index b07e70ace13..7f48a517350 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -82,36 +82,39 @@ type Config struct { TestkubeProTLSSecret string `envconfig:"TESTKUBE_PRO_TLS_SECRET" default:""` TestkubeProRunnerCustomCASecret string `envconfig:"TESTKUBE_PRO_RUNNER_CUSTOM_CA_SECRET" default:""` TestkubeWatcherNamespaces string `envconfig:"TESTKUBE_WATCHER_NAMESPACES" default:""` - GraphqlPort string `envconfig:"TESTKUBE_GRAPHQL_PORT" default:"8070"` TestkubeRegistry string `envconfig:"TESTKUBE_REGISTRY" default:""` TestkubePodStartTimeout time.Duration `envconfig:"TESTKUBE_POD_START_TIMEOUT" default:"30m"` - CDEventsTarget string `envconfig:"CDEVENTS_TARGET" default:""` - TestkubeDashboardURI string `envconfig:"TESTKUBE_DASHBOARD_URI" default:""` - DisableReconciler bool `envconfig:"DISABLE_RECONCILER" default:"false"` - 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"` - EnableSecretsEndpoint bool `envconfig:"ENABLE_SECRETS_ENDPOINT" default:"false"` - EnableListingAllSecrets bool `envconfig:"ENABLE_LISTING_ALL_SECRETS" default:"false"` - SecretCreationPrefix string `envconfig:"SECRET_CREATION_PREFIX" default:"testkube-"` - DisableMongoMigrations bool `envconfig:"DISABLE_MONGO_MIGRATIONS" default:"false"` - Debug bool `envconfig:"DEBUG" default:"false"` - Trace bool `envconfig:"TRACE" default:"false"` - EnableImageDataPersistentCache bool `envconfig:"TESTKUBE_ENABLE_IMAGE_DATA_PERSISTENT_CACHE" default:"false"` - ImageDataPersistentCacheKey string `envconfig:"TESTKUBE_IMAGE_DATA_PERSISTENT_CACHE_KEY" default:"testkube-image-cache"` - LogServerGrpcAddress string `envconfig:"LOG_SERVER_GRPC_ADDRESS" default:":9090"` - LogServerSecure bool `envconfig:"LOG_SERVER_SECURE" default:"false"` - LogServerSkipVerify bool `envconfig:"LOG_SERVER_SKIP_VERIFY" default:"false"` - LogServerCertFile string `envconfig:"LOG_SERVER_CERT_FILE" default:""` - LogServerKeyFile string `envconfig:"LOG_SERVER_KEY_FILE" default:""` - LogServerCAFile string `envconfig:"LOG_SERVER_CA_FILE" default:""` - DisableSecretCreation bool `envconfig:"DISABLE_SECRET_CREATION" default:"false"` - TestkubeExecutionNamespaces string `envconfig:"TESTKUBE_EXECUTION_NAMESPACES" default:""` - TestkubeDefaultStorageClassName string `envconfig:"TESTKUBE_DEFAULT_STORAGE_CLASS_NAME" default:""` - GlobalWorkflowTemplateName string `envconfig:"TESTKUBE_GLOBAL_WORKFLOW_TEMPLATE_NAME" default:""` - EnableK8sEvents bool `envconfig:"ENABLE_K8S_EVENTS" default:"true"` + // TestkubeImageCredentialsCacheTTL is the duration for which the image pull credentials should be cached provided as a Go duration string. + // If set to 0, the cache is disabled. + TestkubeImageCredentialsCacheTTL time.Duration `envconfig:"TESTKUBE_IMAGE_CREDENTIALS_CACHE_TTL" default:"30m"` + GraphqlPort string `envconfig:"TESTKUBE_GRAPHQL_PORT" default:"8070"` + CDEventsTarget string `envconfig:"CDEVENTS_TARGET" default:""` + TestkubeDashboardURI string `envconfig:"TESTKUBE_DASHBOARD_URI" default:""` + DisableReconciler bool `envconfig:"DISABLE_RECONCILER" default:"false"` + 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"` + EnableSecretsEndpoint bool `envconfig:"ENABLE_SECRETS_ENDPOINT" default:"false"` + EnableListingAllSecrets bool `envconfig:"ENABLE_LISTING_ALL_SECRETS" default:"false"` + SecretCreationPrefix string `envconfig:"SECRET_CREATION_PREFIX" default:"testkube-"` + DisableMongoMigrations bool `envconfig:"DISABLE_MONGO_MIGRATIONS" default:"false"` + Debug bool `envconfig:"DEBUG" default:"false"` + Trace bool `envconfig:"TRACE" default:"false"` + EnableImageDataPersistentCache bool `envconfig:"TESTKUBE_ENABLE_IMAGE_DATA_PERSISTENT_CACHE" default:"false"` + ImageDataPersistentCacheKey string `envconfig:"TESTKUBE_IMAGE_DATA_PERSISTENT_CACHE_KEY" default:"testkube-image-cache"` + LogServerGrpcAddress string `envconfig:"LOG_SERVER_GRPC_ADDRESS" default:":9090"` + LogServerSecure bool `envconfig:"LOG_SERVER_SECURE" default:"false"` + LogServerSkipVerify bool `envconfig:"LOG_SERVER_SKIP_VERIFY" default:"false"` + LogServerCertFile string `envconfig:"LOG_SERVER_CERT_FILE" default:""` + LogServerKeyFile string `envconfig:"LOG_SERVER_KEY_FILE" default:""` + LogServerCAFile string `envconfig:"LOG_SERVER_CA_FILE" default:""` + DisableSecretCreation bool `envconfig:"DISABLE_SECRET_CREATION" default:"false"` + TestkubeExecutionNamespaces string `envconfig:"TESTKUBE_EXECUTION_NAMESPACES" default:""` + TestkubeDefaultStorageClassName string `envconfig:"TESTKUBE_DEFAULT_STORAGE_CLASS_NAME" default:""` + GlobalWorkflowTemplateName string `envconfig:"TESTKUBE_GLOBAL_WORKFLOW_TEMPLATE_NAME" default:""` + EnableK8sEvents bool `envconfig:"ENABLE_K8S_EVENTS" default:"true"` // DEPRECATED: Use TestkubeProAPIKey instead TestkubeCloudAPIKey string `envconfig:"TESTKUBE_CLOUD_API_KEY" default:""` diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 00000000000..0d84f8b4603 --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,33 @@ +package cache + +import ( + "context" + "math" + "time" + + "github.com/pkg/errors" +) + +var ( + ErrNotFound = errors.New("item not found") +) + +type Cache[T any] interface { + // Get retrieves the cached value for the given key. + // If the key is not found or expired, the method should return ErrNotFound. + Get(ctx context.Context, key string) (T, error) + // Set stores the value in the cache with the given key. + // If ttl is 0, the item should not be cached and this method should return no error. + Set(ctx context.Context, key string, value T, ttl time.Duration) error +} + +// IsCacheMiss returns true if the error is a cache miss error. +// This is a helper function to determine so users don't have to compare errors manually. +func IsCacheMiss(err error) bool { + return errors.Is(err, ErrNotFound) +} + +// InfiniteTTL returns a time.Duration that represents an infinite TTL. +func InfiniteTTL() time.Duration { + return math.MaxInt64 +} diff --git a/pkg/cache/inmem.go b/pkg/cache/inmem.go new file mode 100644 index 00000000000..22b9138cb59 --- /dev/null +++ b/pkg/cache/inmem.go @@ -0,0 +1,71 @@ +package cache + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" +) + +type item[T any] struct { + value T + expiresAt *time.Time +} + +// timeGetter is a function that returns the current time. +type timeGetter func() time.Time + +type InMemoryCache[T any] struct { + cache sync.Map + timeGetter timeGetter +} + +// NewInMemoryCache creates a new in-memory cache. +// The underlying cache implementation uses a sync.Map so it is thread-safe. +func NewInMemoryCache[T any]() *InMemoryCache[T] { + return &InMemoryCache[T]{ + timeGetter: time.Now, + } +} + +func (c *InMemoryCache[T]) Get(ctx context.Context, key string) (T, error) { + var defaultVal T + rawItem, ok := c.cache.Load(key) + if !ok { + return defaultVal, ErrNotFound + } + i, ok := rawItem.(*item[T]) + if !ok { + return defaultVal, errors.New("unexpected item type found in cache") + } + + if i.expiresAt != nil && i.expiresAt.Before(time.Now()) { + c.cache.Delete(key) + return defaultVal, ErrNotFound + } + + return i.value, nil +} + +func (c *InMemoryCache[T]) Set(ctx context.Context, key string, value T, ttl time.Duration) error { + if ttl < 0 { + return errors.New("ttl must be greater than 0") + } + if ttl == 0 { + return nil + } + + i := &item[T]{ + value: value, + } + if ttl > 0 { + expiresAt := c.timeGetter().Add(ttl) + i.expiresAt = &expiresAt + } + c.cache.Store(key, i) + + return nil +} + +var _ Cache[any] = &InMemoryCache[any]{} diff --git a/pkg/cache/inmem_test.go b/pkg/cache/inmem_test.go new file mode 100644 index 00000000000..e98d19836af --- /dev/null +++ b/pkg/cache/inmem_test.go @@ -0,0 +1,223 @@ +package cache + +import ( + "context" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestInMemoryCache_Get(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + tests := []struct { + name string + setup func(cache *InMemoryCache[string]) + key string + want any + wantError error + }{ + { + name: "Get existing item without TTL", + setup: func(c *InMemoryCache[string]) { + i := &item[string]{ + value: "value", + } + c.cache.Store("existing", i) + }, + key: "existing", + want: "value", + wantError: nil, + }, + { + name: "Get existing item with expired TTL", + setup: func(cache *InMemoryCache[string]) { + expiresAt := time.Now().Add(-1 * time.Hour) + i := &item[string]{ + value: "value", + expiresAt: &expiresAt, + } + cache.cache.Store("stale", i) + }, + key: "stale", + want: nil, + wantError: ErrNotFound, + }, + { + name: "Get non-existing item", + setup: func(cache *InMemoryCache[string]) {}, + key: "non-existing", + want: nil, + wantError: ErrNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cache := NewInMemoryCache[string]() + tt.setup(cache) + got, err := cache.Get(ctx, tt.key) + if tt.wantError != nil { + assert.EqualError(t, err, tt.wantError.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestInMemoryCache_Set(t *testing.T) { + t.Parallel() + + ctx := context.Background() + staticTimeGetter := func() time.Time { + return time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + } + + tests := []struct { + name string + key string + value string + ttl time.Duration + wantErr error + }{ + { + name: "Set item without TTL", + key: "key", + value: "value", + wantErr: nil, + }, + { + name: "Set item with TTL", + key: "key", + value: "value", + ttl: 1 * time.Hour, + wantErr: nil, + }, + { + name: "Set item with infinite TTL", + key: "key", + value: "value", + ttl: InfiniteTTL(), + wantErr: nil, + }, + { + name: "Set item with invalid TTL", + key: "key", + value: "value", + ttl: -1 * time.Minute, + wantErr: errors.New("ttl must be greater than 0"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := InMemoryCache[string]{ + timeGetter: staticTimeGetter, + } + err := c.Set(ctx, tt.key, tt.value, tt.ttl) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + if tt.ttl == 0 { + // Assert that the item is not expired + _, err := c.Get(ctx, tt.key) + assert.ErrorIs(t, err, ErrNotFound) + return + } + rawItem, ok := c.cache.Load(tt.key) + if !ok { + t.Fatalf("expected item to be set in cache") + } + i, ok := rawItem.(*item[string]) + if !ok { + t.Fatalf("unexpected item type found in cache") + } + assert.Equal(t, tt.value, i.value) + if tt.ttl > 0 { + if i.expiresAt == nil { + t.Fatalf("expected item to have an expiry time") + } + assert.Equal(t, staticTimeGetter().Add(tt.ttl), *i.expiresAt) + } else { + assert.Nil(t, i.expiresAt) + } + } + }) + } +} + +func TestInMemoryCache(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + tests := []struct { + name string + key string + value string + ttl time.Duration + waitForExpiration bool + want any + }{ + { + name: "Set and Get existing item without TTL", + key: "existing", + value: "value", + ttl: 0, + }, + { + name: "Set and Get existing item with TTL", + key: "existingWithTTL", + value: "value", + ttl: 1 * time.Hour, + want: "value", + }, + { + name: "Set and Get item which expired", + key: "existingWithTTL", + value: "value", + ttl: 100 * time.Millisecond, + waitForExpiration: true, + want: "value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cache := NewInMemoryCache[string]() + + err := cache.Set(ctx, tt.key, tt.value, tt.ttl) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tt.ttl == 0 { + // Set should not have cached the item if TTL is 0 + _, err := cache.Get(ctx, tt.key) + assert.ErrorIs(t, err, ErrNotFound) + return + } + if tt.waitForExpiration { + // Assert that a not found error eventually gets returned + assert.Eventually(t, func() bool { + _, err := cache.Get(ctx, tt.key) + return errors.Is(err, ErrNotFound) + }, 300*time.Millisecond, 30*time.Millisecond) + // Assert that any subsequent Get calls return a not found error + _, err := cache.Get(ctx, tt.key) + assert.Equal(t, ErrNotFound, err) + + return + } + got, err := cache.Get(ctx, tt.key) + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/executor/containerexecutor/containerexecutor.go b/pkg/executor/containerexecutor/containerexecutor.go index 0da5513b1e7..83a476f7916 100644 --- a/pkg/executor/containerexecutor/containerexecutor.go +++ b/pkg/executor/containerexecutor/containerexecutor.go @@ -6,6 +6,8 @@ import ( "path/filepath" "time" + "github.com/kubeshop/testkube/pkg/cache" + "github.com/pkg/errors" "github.com/kubeshop/testkube/pkg/featureflags" @@ -85,6 +87,7 @@ func NewContainerExecutor( features featureflags.FeatureFlags, defaultStorageClassName string, whitelistedContainers []string, + imageCredentialsCacheTTL time.Duration, ) (client *ContainerExecutor, err error) { clientSet, err := k8sclient.ConnectToK8s() if err != nil { @@ -96,31 +99,32 @@ func NewContainerExecutor( } return &ContainerExecutor{ - clientSet: clientSet, - repository: repo, - log: log.DefaultLogger, - images: images, - templates: templates, - imageInspector: imageInspector, - configMap: configMap, - serviceAccountNames: serviceAccountNames, - metrics: metrics, - emitter: emiter, - testsClient: testsClient, - executorsClient: executorsClient, - testExecutionsClient: testExecutionsClient, - templatesClient: templatesClient, - registry: registry, - podStartTimeout: podStartTimeout, - clusterID: clusterID, - dashboardURI: dashboardURI, - apiURI: apiURI, - natsURI: natsUri, - debug: debug, - logsStream: logsStream, - features: features, - defaultStorageClassName: defaultStorageClassName, - whitelistedContainers: whitelistedContainers, + clientSet: clientSet, + repository: repo, + log: log.DefaultLogger, + images: images, + templates: templates, + imageInspector: imageInspector, + configMap: configMap, + serviceAccountNames: serviceAccountNames, + metrics: metrics, + emitter: emiter, + testsClient: testsClient, + executorsClient: executorsClient, + testExecutionsClient: testExecutionsClient, + templatesClient: templatesClient, + registry: registry, + podStartTimeout: podStartTimeout, + clusterID: clusterID, + dashboardURI: dashboardURI, + apiURI: apiURI, + natsURI: natsUri, + debug: debug, + logsStream: logsStream, + features: features, + defaultStorageClassName: defaultStorageClassName, + whitelistedContainers: whitelistedContainers, + imageCredentialsCacheTTL: imageCredentialsCacheTTL, }, nil } @@ -156,6 +160,8 @@ type ContainerExecutor struct { defaultStorageClassName string // whitelistedContainers is a list of containers from which logs are allowed to be streamed. whitelistedContainers []string + // imageCredentialsCacheTTL defines the ttl for image credentials cache. + imageCredentialsCacheTTL time.Duration } type JobOptions struct { @@ -323,7 +329,11 @@ func (c *ContainerExecutor) createJob(ctx context.Context, execution testkube.Ex if err != nil { return nil, errors.Wrap(err, "failed to build secrets client") } - inspector = imageinspector.NewInspector(c.registry, imageinspector.NewSkopeoFetcher(), imageinspector.NewSecretFetcher(secretClient)) + inspector = imageinspector.NewInspector( + c.registry, + imageinspector.NewSkopeoFetcher(), + imageinspector.NewSecretFetcher(secretClient, cache.NewInMemoryCache[*corev1.Secret](), imageinspector.WithSecretCacheTTL(c.imageCredentialsCacheTTL)), + ) } jobOptions, err := NewJobOptions(c.log, c.templatesClient, c.images, c.templates, inspector, diff --git a/pkg/imageinspector/secretfetcher.go b/pkg/imageinspector/secretfetcher.go index 44b81f51159..e03b6d84567 100644 --- a/pkg/imageinspector/secretfetcher.go +++ b/pkg/imageinspector/secretfetcher.go @@ -2,35 +2,54 @@ package imageinspector import ( "context" - "sync" + "time" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + "github.com/kubeshop/testkube/pkg/cache" + "github.com/kubeshop/testkube/pkg/log" + "github.com/kubeshop/testkube/pkg/secret" ) +type SecretFetcherOption func(*secretFetcher) + +// WithSecretCacheTTL sets the time to live for the cached secrets. +func WithSecretCacheTTL(ttl time.Duration) SecretFetcherOption { + return func(s *secretFetcher) { + s.ttl = ttl + } +} + type secretFetcher struct { client secret.Interface - cache map[string]*corev1.Secret - mu sync.RWMutex + cache cache.Cache[*corev1.Secret] + ttl time.Duration } -func NewSecretFetcher(client secret.Interface) SecretFetcher { - return &secretFetcher{ +func NewSecretFetcher(client secret.Interface, cache cache.Cache[*corev1.Secret], opts ...SecretFetcherOption) SecretFetcher { + s := &secretFetcher{ client: client, - cache: make(map[string]*corev1.Secret), + cache: cache, + } + for _, opt := range opts { + opt(s) } + return s } func (s *secretFetcher) Get(ctx context.Context, name string) (*corev1.Secret, error) { - // Get cached secret - s.mu.RLock() - if v, ok := s.cache[name]; ok { - s.mu.RUnlock() - return v, nil + if s.ttl > 0 { + // Get cached secret + cached, err := s.getFromCache(ctx, name) + if err != nil { + return nil, err + } + if cached != nil { + return cached, nil + } } - s.mu.RUnlock() // Load secret from the Kubernetes obj, err := s.client.GetObject(name) @@ -38,13 +57,28 @@ func (s *secretFetcher) Get(ctx context.Context, name string) (*corev1.Secret, e return nil, errors.Wrap(err, "fetching image pull secret") } - // Save in cache - s.mu.Lock() - s.cache[name] = obj - s.mu.Unlock() + if s.ttl > 0 { + // Save in cache + if err := s.cache.Set(ctx, name, obj, s.ttl); err != nil { + log.DefaultLogger.Warnw("error while saving secret in cache", "name", name, "error", err) + } + } if ctx.Err() != nil { return nil, ctx.Err() } return obj, nil } + +func (s *secretFetcher) getFromCache(ctx context.Context, name string) (*corev1.Secret, error) { + cached, err := s.cache.Get(ctx, name) + if err != nil { + if cache.IsCacheMiss(err) { + return nil, nil + } + log.DefaultLogger.Warnw("error while getting secret from cache", "name", name, "error", err) + return nil, err + } + + return cached, nil +} diff --git a/pkg/imageinspector/secretfetcher_test.go b/pkg/imageinspector/secretfetcher_test.go index e60e7cd5ee6..b9828d45ac1 100644 --- a/pkg/imageinspector/secretfetcher_test.go +++ b/pkg/imageinspector/secretfetcher_test.go @@ -3,6 +3,9 @@ package imageinspector import ( "context" "testing" + "time" + + "github.com/kubeshop/testkube/pkg/cache" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -16,7 +19,7 @@ import ( func TestSecretFetcherGetExisting(t *testing.T) { ctrl := gomock.NewController(t) client := secret.NewMockInterface(ctrl) - fetcher := NewSecretFetcher(client) + fetcher := NewSecretFetcher(client, cache.NewInMemoryCache[*corev1.Secret]()) expected := corev1.Secret{ StringData: map[string]string{"key": "value"}, @@ -31,7 +34,7 @@ func TestSecretFetcherGetExisting(t *testing.T) { func TestSecretFetcherGetCache(t *testing.T) { ctrl := gomock.NewController(t) client := secret.NewMockInterface(ctrl) - fetcher := NewSecretFetcher(client) + fetcher := NewSecretFetcher(client, cache.NewInMemoryCache[*corev1.Secret](), WithSecretCacheTTL(1*time.Minute)) expected := corev1.Secret{ StringData: map[string]string{"key": "value"}, @@ -46,10 +49,25 @@ func TestSecretFetcherGetCache(t *testing.T) { assert.Equal(t, &expected, result2) } +func TestSecretFetcherGetDisabledCache(t *testing.T) { + ctrl := gomock.NewController(t) + client := secret.NewMockInterface(ctrl) + fetcher := NewSecretFetcher(client, newNoCache(t), WithSecretCacheTTL(0)) + + expected := corev1.Secret{ + StringData: map[string]string{"key": "value"}, + } + client.EXPECT().GetObject("dummy").Return(&expected, nil) + + result1, err1 := fetcher.Get(context.Background(), "dummy") + assert.NoError(t, err1) + assert.Equal(t, &expected, result1) +} + func TestSecretFetcherGetError(t *testing.T) { ctrl := gomock.NewController(t) client := secret.NewMockInterface(ctrl) - fetcher := NewSecretFetcher(client) + fetcher := NewSecretFetcher(client, cache.NewInMemoryCache[*corev1.Secret]()) client.EXPECT().GetObject("dummy").Return(nil, k8serrors.NewNotFound(schema.GroupResource{}, "dummy")) client.EXPECT().GetObject("dummy").Return(nil, k8serrors.NewNotFound(schema.GroupResource{}, "dummy")) @@ -64,3 +82,21 @@ func TestSecretFetcherGetError(t *testing.T) { assert.Equal(t, noSecret, result1) assert.Equal(t, noSecret, result2) } + +type noCache struct { + t *testing.T +} + +func newNoCache(t *testing.T) *noCache { + return &noCache{t: t} +} + +func (n *noCache) Set(ctx context.Context, key string, value *corev1.Secret, ttl time.Duration) error { + n.t.Fatalf("set method should not be invoked when cache is disabled") + return nil +} + +func (n *noCache) Get(ctx context.Context, key string) (*corev1.Secret, error) { + n.t.Fatalf("get method should not be invoked when cache is disabled") + return nil, nil +} diff --git a/pkg/testworkflows/testworkflowexecutor/executor.go b/pkg/testworkflows/testworkflowexecutor/executor.go index 46e89e5e536..b30f359c6d6 100644 --- a/pkg/testworkflows/testworkflowexecutor/executor.go +++ b/pkg/testworkflows/testworkflowexecutor/executor.go @@ -429,6 +429,7 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor "images.toolkit": constants.DefaultToolkitImage, "images.persistence.enabled": strconv.FormatBool(e.enableImageDataPersistentCache), "images.persistence.key": e.imageDataPersistentCacheKey, + "images.cache.ttl": common.GetOr(os.Getenv("TESTKUBE_IMAGE_CREDENTIALS_CACHE_TTL"), "30m"), }). Register("workflow", map[string]string{ "name": workflow.Name, diff --git a/pkg/testworkflows/testworkflowprocessor/container.go b/pkg/testworkflows/testworkflowprocessor/container.go index fba2a2d4950..0b2b7b95370 100644 --- a/pkg/testworkflows/testworkflowprocessor/container.go +++ b/pkg/testworkflows/testworkflowprocessor/container.go @@ -458,6 +458,7 @@ func (c *container) EnableToolkit(ref string) Container { "TESTKUBE_TW_INIT_IMAGE": "{{internal.images.init}}", "TK_IMG_P": "{{internal.images.persistence.enabled}}", "TK_IMG_PK": "{{internal.images.persistence.key}}", + "TK_IMG_CRED_TTL": "{{internal.images.cache.ttl}}", }) } From 565b4e73db67d5b389c3d899c46da58f501f2710 Mon Sep 17 00:00:00 2001 From: ypoplavs <45286051+ypoplavs@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:53:17 +0300 Subject: [PATCH 11/16] add dispatch (#5667) --- .github/workflows/sandbox.yaml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/sandbox.yaml b/.github/workflows/sandbox.yaml index 77fc711e9c2..bc6f6990201 100644 --- a/.github/workflows/sandbox.yaml +++ b/.github/workflows/sandbox.yaml @@ -776,3 +776,34 @@ jobs: - name: Push Docker images run: | docker push kubeshop/testkube-sandbox:${{ matrix.service }}-${{ env.branch_identifier }}-${{ steps.commit.outputs.short }} + + dispatch: + needs: + [ + api, + single_executor, + executor_jmeter, + executor_maven, + executor_gradle, + executor_cypress, + executor_cypress_manifest, + executor_playwright, + log_server_sidecar, + testworkflow + ] + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Repository dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ steps.app-token.outputs.token }} + repository: kubeshop/testkube-deployment + event-type: sandbox_image_update + client-payload: '{"ref_name": "${{ github.ref_name }}"}' From 1d1cbbe7915aaca06acc2ec28b9e5e777c485766 Mon Sep 17 00:00:00 2001 From: ypoplavs <45286051+ypoplavs@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:53:17 +0300 Subject: [PATCH 12/16] add dispatch (#5667) --- .github/workflows/sandbox.yaml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/sandbox.yaml b/.github/workflows/sandbox.yaml index 77fc711e9c2..bc6f6990201 100644 --- a/.github/workflows/sandbox.yaml +++ b/.github/workflows/sandbox.yaml @@ -776,3 +776,34 @@ jobs: - name: Push Docker images run: | docker push kubeshop/testkube-sandbox:${{ matrix.service }}-${{ env.branch_identifier }}-${{ steps.commit.outputs.short }} + + dispatch: + needs: + [ + api, + single_executor, + executor_jmeter, + executor_maven, + executor_gradle, + executor_cypress, + executor_cypress_manifest, + executor_playwright, + log_server_sidecar, + testworkflow + ] + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Repository dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ steps.app-token.outputs.token }} + repository: kubeshop/testkube-deployment + event-type: sandbox_image_update + client-payload: '{"ref_name": "${{ github.ref_name }}"}' From ab73b19cb5f4ff6ae41f87fcfdf23d17773128e3 Mon Sep 17 00:00:00 2001 From: ypoplavs <45286051+ypoplavs@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:27:55 +0300 Subject: [PATCH 13/16] fix a typo in CI for sandbox (#5671) --- .github/workflows/sandbox.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/sandbox.yaml b/.github/workflows/sandbox.yaml index bc6f6990201..8ea32b57a27 100644 --- a/.github/workflows/sandbox.yaml +++ b/.github/workflows/sandbox.yaml @@ -786,7 +786,6 @@ jobs: executor_maven, executor_gradle, executor_cypress, - executor_cypress_manifest, executor_playwright, log_server_sidecar, testworkflow From b614f25d203fd064f75abe092cf3937884fbdb16 Mon Sep 17 00:00:00 2001 From: ypoplavs <45286051+ypoplavs@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:23:54 +0300 Subject: [PATCH 14/16] add sandbox deletion (#5674) --- .github/workflows/sandbox-deletion.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/sandbox-deletion.yaml diff --git a/.github/workflows/sandbox-deletion.yaml b/.github/workflows/sandbox-deletion.yaml new file mode 100644 index 00000000000..a17e5eabf71 --- /dev/null +++ b/.github/workflows/sandbox-deletion.yaml @@ -0,0 +1,25 @@ +name: Remove sandbox environment + +on: + pull_request: + types: [closed] + +jobs: + delete: + if: startsWith(github.event.pull_request.head.ref, 'sandbox/') + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Repository dispatch + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ steps.app-token.outputs.token }} + repository: kubeshop/testkube-deployment + event-type: sandbox_env_delete + client-payload: '{"ref_name": "${{ github.event.pull_request.head.ref }}"}' From af6ac025bd05f067274ae0df44663ea14733d414 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 19 Jul 2024 16:36:04 +0400 Subject: [PATCH 15/16] fix: [TKC-2209] add webhooks events (#5666) * fix: sync docs Signed-off-by: Vladislav Sukhin * fix: remove on state changed Signed-off-by: Vladislav Sukhin * fix: unit tests Signed-off-by: Vladislav Sukhin * fix: add new webhook events Signed-off-by: Vladislav Sukhin * fix: remove link Signed-off-by: Vladislav Sukhin * fix: remove slack events Signed-off-by: Vladislav Sukhin * fix: support new evenys in webhooks Signed-off-by: Vladislav Sukhin * docs: new event types Signed-off-by: Vladislav Sukhin * fix: unit test for event types Signed-off-by: Vladislav Sukhin * fix: unit tests for events Signed-off-by: Vladislav Sukhin * fix: unit tests for exetended event type Signed-off-by: Vladislav Sukhin * fix: add parallel unit tests Signed-off-by: Vladislav Sukhin * fix: unit tests for emitter Signed-off-by: Vladislav Sukhin * fix: add break Signed-off-by: Vladislav Sukhin * fix: add nil check Signed-off-by: Vladislav Sukhin * fix: test suite event end time Signed-off-by: Vladislav Sukhin * fix: dep update Signed-off-by: Vladislav Sukhin * fix: doc typo Signed-off-by: Vladislav Sukhin --------- Signed-off-by: Vladislav Sukhin --- api/v1/testkube.yaml | 21 +- .../commands/webhooks/common.go | 14 -- .../commands/webhooks/create.go | 2 - .../commands/webhooks/update.go | 2 - docs/docs/articles/webhooks.mdx | 54 ++++- docs/docs/cli/testkube_create_webhook.md | 1 - docs/docs/cli/testkube_update_webhook.md | 1 - go.mod | 2 +- go.sum | 4 +- pkg/api/v1/testkube/model_event_extended.go | 20 +- .../v1/testkube/model_event_extended_test.go | 195 +++++++++++++++++- pkg/api/v1/testkube/model_event_type.go | 50 +++-- .../v1/testkube/model_event_type_extended.go | 163 +++++++++++++++ .../model_event_type_extended_test.go | 191 +++++++++++++++++ pkg/api/v1/testkube/model_webhook.go | 2 - .../testkube/model_webhook_create_request.go | 2 - pkg/api/v1/testkube/model_webhook_extended.go | 3 +- .../testkube/model_webhook_update_request.go | 2 - pkg/event/emitter.go | 13 +- pkg/event/emitter_test.go | 44 ++++ pkg/event/kind/dummy/listener.go | 5 + pkg/event/kind/webhook/listener.go | 35 ++-- pkg/event/kind/webhook/listener_test.go | 12 +- pkg/event/kind/webhook/loader.go | 2 +- pkg/mapper/webhooks/mapper.go | 7 - pkg/scheduler/testsuite_scheduler.go | 17 +- 26 files changed, 744 insertions(+), 120 deletions(-) create mode 100644 pkg/api/v1/testkube/model_event_type_extended_test.go diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index d5074d950ab..eed82a971e0 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -7096,13 +7096,6 @@ components: example: - true - false - onStateChange: - type: boolean - description: whether webhook is triggered on state change only - default: false - example: - - true - - false Event: description: Event data @@ -7165,16 +7158,30 @@ components: - end-test-failed - end-test-aborted - end-test-timeout + - become-test-up + - become-test-down + - become-test-failed + - become-test-aborted + - become-test-timeout - start-testsuite - end-testsuite-success - end-testsuite-failed - end-testsuite-aborted - end-testsuite-timeout + - become-testsuite-up + - become-testsuite-down + - become-testsuite-failed + - become-testsuite-aborted + - become-testsuite-timeout - queue-testworkflow - start-testworkflow - end-testworkflow-success - end-testworkflow-failed - end-testworkflow-aborted + - become-testworkflow-up + - become-testworkflow-down + - become-testworkflow-failed + - become-testworkflow-aborted - created - updated - deleted diff --git a/cmd/kubectl-testkube/commands/webhooks/common.go b/cmd/kubectl-testkube/commands/webhooks/common.go index 15c4262b460..bc3cac9269f 100644 --- a/cmd/kubectl-testkube/commands/webhooks/common.go +++ b/cmd/kubectl-testkube/commands/webhooks/common.go @@ -42,11 +42,6 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW return options, err } - onStateChange, err := cmd.Flags().GetBool("on-state-change") - if err != nil { - return options, err - } - disabled, err := cmd.Flags().GetBool("disable") if err != nil { return options, err @@ -64,7 +59,6 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW PayloadTemplate: payloadTemplateContent, Headers: headers, PayloadTemplateReference: payloadTemplateReference, - OnStateChange: onStateChange, Disabled: disabled, } @@ -157,13 +151,5 @@ func NewUpdateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.UpdateW options.Disabled = &disabled } - if cmd.Flag("on-state-change").Changed { - onStateChange, err := cmd.Flags().GetBool("on-state-change") - if err != nil { - return options, err - } - options.OnStateChange = &onStateChange - } - return options, nil } diff --git a/cmd/kubectl-testkube/commands/webhooks/create.go b/cmd/kubectl-testkube/commands/webhooks/create.go index 4bf8d798e2b..c7f3b80478c 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -24,7 +24,6 @@ func NewCreateWebhookCmd() *cobra.Command { payloadTemplateReference string update bool disable bool - onStateChange bool ) cmd := &cobra.Command{ @@ -103,7 +102,6 @@ func NewCreateWebhookCmd() *cobra.Command { cmd.Flags().BoolVar(&update, "update", false, "update, if webhook already exists") cmd.Flags().BoolVar(&disable, "disable", false, "disable webhook") cmd.Flags().MarkDeprecated("enable", "enable webhook is deprecated") - cmd.Flags().BoolVar(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change") return cmd } diff --git a/cmd/kubectl-testkube/commands/webhooks/update.go b/cmd/kubectl-testkube/commands/webhooks/update.go index 90ff196ea66..cbc93070d7d 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -18,7 +18,6 @@ func UpdateWebhookCmd() *cobra.Command { headers map[string]string payloadTemplateReference string disable bool - onStateChange bool ) cmd := &cobra.Command{ @@ -60,7 +59,6 @@ func UpdateWebhookCmd() *cobra.Command { cmd.Flags().StringVar(&payloadTemplateReference, "payload-template-reference", "", "reference to payload template to use for the webhook") cmd.Flags().BoolVar(&disable, "disable", false, "disable webhook") cmd.Flags().MarkDeprecated("enable", "enable webhook is depecated") - cmd.Flags().BoolVar(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change") return cmd } diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index 25f21d09bed..a01787cbc18 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -80,7 +80,7 @@ spec: selector: "" ``` -Where should be replaced with the HTTPS endpoint URL where you want to receive the webhook events. +Where `` should be replaced with the HTTPS endpoint URL where you want to receive the webhook events. And then apply with: @@ -170,7 +170,7 @@ And set it with `--payload-template template.json`. 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. +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 🥇 @@ -310,16 +310,30 @@ Webhooks can be triggered on any of the following events: - end-test-failed - end-test-aborted - end-test-timeout +- become-test-up +- become-test-down +- become-test-failed +- become-test-aborted +- become-test-timeout - start-testsuite - end-testsuite-success - end-testsuite-failed - end-testsuite-aborted - end-testsuite-timeout -- start-testworkflow +- become-testsuite-up +- become-testsuite-down +- become-testsuite-failed +- become-testsuite-aborted +- become-testsuite-timeout +- queue-testworkflow - start-testworkflow - end-testworkflow-success - end-testworkflow-failed - end-testworkflow-aborted +- become-testworkflow-up +- become-testworkflow-down +- become-testworkflow-failed +- become-testworkflow-aborted - created - updated - deleted @@ -353,6 +367,10 @@ The full Event Data Model can be found [here](https://github.com/kubeshop/testku ### TestExecution (Execution): +:::warning +Test and Test Suite webhooks are being deprecated from Testkube. +::: + - `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) @@ -377,6 +395,10 @@ The full Execution data model can be found [here](https://github.com/kubeshop/te ### TestSuiteExecution: +:::warning +Test and Test Suite webhooks are being deprecated from Testkube. +::: + - `Id` - TestSuiteExecution ID (for example, `64f8d5b2712890925aea51dc`) - `Name` - TestSuite execution name (for example, `ts-executor-postman-smoke-tests-472`) - `Status` - TestSuite execution status (for example, `running` or `passed`) @@ -646,14 +668,34 @@ The examples show only the `create` operations, however, these instructions work ## Enable Webhooks on State Changes -It is possible to get notified by webhooks only when the latest execution's outcome differs from the previous one. This way you can see instantly when one of your scheduled tests was healed or when they got broken. It could also help prevent alert fatigue by deduplicating the alerts and therefore decreasing the load on your monitoring systems. Enable this by setting `onStateChange` to true directly on the webhook. +It is possible to get notified by webhooks only when the latest execution's outcome differs from the previous one. This way you can see instantly when one of your scheduled tests was healed or when they got broken. It could also help prevent alert fatigue by deduplicating the alerts and therefore decreasing the load on your monitoring systems. +Testkube suppots special webhook event types to track changes between current and previous execution states. +For Tests: +- become-test-up (from any error state to succeed one) +- become-test-down (from succeed state to any error one) +- become-test-failed (from any state to failed one) +- become-test-aborted (from any state to aborted one) +- become-test-timeout (from any state to timeout one) + +For Test Suites: +- become-testsuite-up (from any error state to succeed one) +- become-testsuite-down (from succeed state to any error one) +- become-testsuite-failed (from any state to failed one) +- become-testsuite-aborted (from any state to aborted one) +- become-testsuite-timeout (from any state to timeout one) + +For Test Workflows: +- become-testworkflow-up (from any error state to succeed one) +- become-testworkflow-down (from succeed state to any error one) +- become-testworkflow-failed (from any state to failed one) +- become-testworkflow-aborted (from any state to aborted one) ```bash -testkube update webhook --name example-webhook --on-state-change +testkube update webhook --name example-webhook ``` @@ -671,7 +713,6 @@ testkube update webhook --name example-webhook --on-state-change "end-test-failed" ], "disabled": false, - "onStateChange": true, } ``` @@ -690,7 +731,6 @@ spec: - start-test - end-test-success - end-test-failed - onStateChange: true uri: https://webhook.url/ ``` diff --git a/docs/docs/cli/testkube_create_webhook.md b/docs/docs/cli/testkube_create_webhook.md index 08897f7f0d4..b34aeed934b 100644 --- a/docs/docs/cli/testkube_create_webhook.md +++ b/docs/docs/cli/testkube_create_webhook.md @@ -19,7 +19,6 @@ testkube create webhook [flags] -h, --help help for webhook -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique webhook name - mandatory - --on-state-change specify whether webhook should be triggered only on a state change --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 diff --git a/docs/docs/cli/testkube_update_webhook.md b/docs/docs/cli/testkube_update_webhook.md index 1bb3168c87e..d5528f3968c 100644 --- a/docs/docs/cli/testkube_update_webhook.md +++ b/docs/docs/cli/testkube_update_webhook.md @@ -19,7 +19,6 @@ testkube update webhook [flags] -h, --help help for webhook -l, --label stringToString label key value pair: --label key1=value1 (default []) -n, --name string unique webhook name - mandatory - --on-state-change specify whether webhook should be triggered only on a state change --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 diff --git a/go.mod b/go.mod index f268deb8dc9..0c7ce5572ad 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kelseyhightower/envconfig v1.4.0 github.com/kubepug/kubepug v1.7.1 - github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240716095745-a1fa1f1a8d25 + github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240719112653-d677ef7c3437 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 8a9506cbf60..6d131d1642c 100644 --- a/go.sum +++ b/go.sum @@ -358,8 +358,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubepug/kubepug v1.7.1 h1:LKhfSxS8Y5mXs50v+3Lpyec+cogErDLcV7CMUuiaisw= github.com/kubepug/kubepug v1.7.1/go.mod h1:lv+HxD0oTFL7ZWjj0u6HKhMbbTIId3eG7aWIW0gyF8g= -github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240716095745-a1fa1f1a8d25 h1:U0IaIRvYnx3THAvKc2Y2P9Xp4aKUj5E3cpTmA6VGLU8= -github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240716095745-a1fa1f1a8d25/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk= +github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240719112653-d677ef7c3437 h1:Ov4w/ozhVUwdOekhD3YzFQ3IYwKkmILGRAy3LdRFGck= +github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240719112653-d677ef7c3437/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk= 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/pkg/api/v1/testkube/model_event_extended.go b/pkg/api/v1/testkube/model_event_extended.go index e16d860dfd8..e9415a386e2 100644 --- a/pkg/api/v1/testkube/model_event_extended.go +++ b/pkg/api/v1/testkube/model_event_extended.go @@ -215,7 +215,7 @@ func (e Event) Log() []any { } } -func (e Event) Valid(selector string, types []EventType) (valid bool) { +func (e Event) Valid(selector string, types []EventType) (matchedTypes []EventType, valid bool) { var executionLabels map[string]string // load labels from event test execution or test-suite execution or test workflow execution @@ -231,21 +231,29 @@ func (e Event) Valid(selector string, types []EventType) (valid bool) { typesMatch := false for _, t := range types { - if t == e.Type() { - typesMatch = true - break + ts := []EventType{t} + if t.IsBecome() { + ts = t.MapBecomeToRegular() + } + + for _, et := range ts { + if et == e.Type() { + typesMatch = true + matchedTypes = append(matchedTypes, t) + break + } } } if !typesMatch { - return false + return nil, false } valid = selector == "" if !valid { selector, err := labels.Parse(selector) if err != nil { - return false + return nil, false } valid = selector.Matches(labels.Set(executionLabels)) diff --git a/pkg/api/v1/testkube/model_event_extended_test.go b/pkg/api/v1/testkube/model_event_extended_test.go index 811776cd212..cf0da3bb5cb 100644 --- a/pkg/api/v1/testkube/model_event_extended_test.go +++ b/pkg/api/v1/testkube/model_event_extended_test.go @@ -7,131 +7,299 @@ import ( ) func TestEmitter_IsValidEvent_ForTest(t *testing.T) { + t.Parallel() t.Run("should pass only events with given selector", func(t *testing.T) { + t.Parallel() + // given execution := NewQueuedExecution() execution.Labels = map[string]string{"test": "1"} e := Event{Type_: EventStartTest, TestExecution: execution} // when - valid := e.Valid("test=1", AllEventTypes) + types, valid := e.Valid("test=1", AllEventTypes) // then + assert.Equal(t, []EventType{START_TEST_EventType}, types) assert.True(t, valid) }) t.Run("should not pass events with not matching selector", func(t *testing.T) { + t.Parallel() + // given execution := NewQueuedExecution() execution.Labels = map[string]string{"test": "2"} e := Event{Type_: EventStartTest, TestExecution: execution} // when - valid := e.Valid("test=1", AllEventTypes) + types, valid := e.Valid("test=1", AllEventTypes) // then + assert.Equal(t, []EventType{START_TEST_EventType}, types) assert.False(t, valid) }) t.Run("should pass events without selector", func(t *testing.T) { + t.Parallel() + // given execution := NewQueuedExecution() e := Event{Type_: EventStartTest, TestExecution: execution} // when - valid := e.Valid("", AllEventTypes) + types, valid := e.Valid("", AllEventTypes) + + // then + assert.Equal(t, []EventType{START_TEST_EventType}, types) + assert.True(t, valid) + }) + + t.Run("should pass events with become events", func(t *testing.T) { + t.Parallel() + + // given + execution := NewQueuedExecution() + e := Event{Type_: EventEndTestFailed, TestExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TEST_DOWN_EventType, BECOME_TEST_FAILED_EventType}) // then + assert.Equal(t, []EventType{BECOME_TEST_DOWN_EventType, BECOME_TEST_FAILED_EventType}, types) assert.True(t, valid) }) + + t.Run("should pass events with become and regular events", func(t *testing.T) { + t.Parallel() + + // given + execution := NewQueuedExecution() + e := Event{Type_: EventEndTestFailed, TestExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TEST_DOWN_EventType, END_TEST_FAILED_EventType}) + + // then + assert.Equal(t, []EventType{BECOME_TEST_DOWN_EventType, END_TEST_FAILED_EventType}, types) + assert.True(t, valid) + }) + + t.Run("should not pass events with wrong become events", func(t *testing.T) { + t.Parallel() + + // given + execution := NewQueuedExecution() + e := Event{Type_: EventEndTestFailed, TestExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TEST_UP_EventType}) + + // then + assert.Nil(t, types) + assert.False(t, valid) + }) } func TestEmitter_IsValidEvent_ForTestSuite(t *testing.T) { + t.Parallel() t.Run("should pass only events with given selector", func(t *testing.T) { + t.Parallel() + // given execution := NewQueuedTestSuiteExecution("", "") execution.Labels = map[string]string{"test": "1"} e := Event{Type_: EventStartTestSuite, TestSuiteExecution: execution} // when - valid := e.Valid("test=1", AllEventTypes) + types, valid := e.Valid("test=1", AllEventTypes) // then + assert.Equal(t, []EventType{START_TESTSUITE_EventType}, types) assert.True(t, valid) }) t.Run("should not pass events with not matching selector", func(t *testing.T) { + t.Parallel() + // given execution := NewQueuedTestSuiteExecution("", "") execution.Labels = map[string]string{"test": "2"} e := Event{Type_: EventStartTestSuite, TestSuiteExecution: execution} // when - valid := e.Valid("test=1", AllEventTypes) + types, valid := e.Valid("test=1", AllEventTypes) // then + assert.Equal(t, []EventType{START_TESTSUITE_EventType}, types) assert.False(t, valid) }) t.Run("should pass events without selector", func(t *testing.T) { + t.Parallel() + // given execution := NewQueuedTestSuiteExecution("", "") e := Event{Type_: EventStartTestSuite, TestSuiteExecution: execution} // when - valid := e.Valid("", AllEventTypes) + types, valid := e.Valid("", AllEventTypes) // then + assert.Equal(t, []EventType{START_TESTSUITE_EventType}, types) assert.True(t, valid) }) + + t.Run("should pass events with become events", func(t *testing.T) { + t.Parallel() + + // given + execution := NewQueuedTestSuiteExecution("", "") + e := Event{Type_: EventEndTestSuiteFailed, TestSuiteExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TESTSUITE_DOWN_EventType, BECOME_TESTSUITE_FAILED_EventType}) + + // then + assert.Equal(t, []EventType{BECOME_TESTSUITE_DOWN_EventType, BECOME_TESTSUITE_FAILED_EventType}, types) + assert.True(t, valid) + }) + + t.Run("should pass events with become and regular events", func(t *testing.T) { + t.Parallel() + + // given + execution := NewQueuedTestSuiteExecution("", "") + e := Event{Type_: EventEndTestSuiteFailed, TestSuiteExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TESTSUITE_DOWN_EventType, END_TESTSUITE_FAILED_EventType}) + + // then + assert.Equal(t, []EventType{BECOME_TESTSUITE_DOWN_EventType, END_TESTSUITE_FAILED_EventType}, types) + assert.True(t, valid) + }) + + t.Run("should not pass events with wrong become events", func(t *testing.T) { + t.Parallel() + + // given + execution := NewQueuedTestSuiteExecution("", "") + e := Event{Type_: EventEndTestSuiteFailed, TestSuiteExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TESTSUITE_UP_EventType}) + + // then + assert.Nil(t, types) + assert.False(t, valid) + }) } func TestEmitter_IsValidEvent_ForTestWorkflow(t *testing.T) { + t.Parallel() t.Run("should pass only events with given selector", func(t *testing.T) { + t.Parallel() + // given execution := &TestWorkflowExecution{Workflow: &TestWorkflow{}} execution.Workflow.Labels = map[string]string{"test": "1"} e := Event{Type_: EventStartTestWorkflow, TestWorkflowExecution: execution} // when - valid := e.Valid("test=1", AllEventTypes) + types, valid := e.Valid("test=1", AllEventTypes) // then + assert.Equal(t, []EventType{START_TESTWORKFLOW_EventType}, types) assert.True(t, valid) }) t.Run("should not pass events with not matching selector", func(t *testing.T) { + t.Parallel() + // given execution := &TestWorkflowExecution{Workflow: &TestWorkflow{}} execution.Workflow.Labels = map[string]string{"test": "2"} e := Event{Type_: EventStartTestWorkflow, TestWorkflowExecution: execution} // when - valid := e.Valid("test=1", AllEventTypes) + types, valid := e.Valid("test=1", AllEventTypes) // then + assert.Equal(t, []EventType{START_TESTWORKFLOW_EventType}, types) assert.False(t, valid) }) t.Run("should pass events without selector", func(t *testing.T) { + t.Parallel() + // given execution := &TestWorkflowExecution{Workflow: &TestWorkflow{}} e := Event{Type_: EventStartTestWorkflow, TestWorkflowExecution: execution} // when - valid := e.Valid("", AllEventTypes) + types, valid := e.Valid("", AllEventTypes) + + // then + assert.Equal(t, []EventType{START_TESTWORKFLOW_EventType}, types) + assert.True(t, valid) + }) + + t.Run("should pass events with become events", func(t *testing.T) { + t.Parallel() + + // given + execution := &TestWorkflowExecution{} + e := Event{Type_: EventEndTestWorkflowFailed, TestWorkflowExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TESTWORKFLOW_DOWN_EventType, BECOME_TESTWORKFLOW_FAILED_EventType}) // then + assert.Equal(t, []EventType{BECOME_TESTWORKFLOW_DOWN_EventType, BECOME_TESTWORKFLOW_FAILED_EventType}, types) assert.True(t, valid) }) + + t.Run("should pass events with become and regular events", func(t *testing.T) { + t.Parallel() + + // given + execution := &TestWorkflowExecution{} + e := Event{Type_: EventEndTestWorkflowFailed, TestWorkflowExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TESTWORKFLOW_DOWN_EventType, END_TESTWORKFLOW_FAILED_EventType}) + + // then + assert.Equal(t, []EventType{BECOME_TESTWORKFLOW_DOWN_EventType, END_TESTWORKFLOW_FAILED_EventType}, types) + assert.True(t, valid) + }) + + t.Run("should not pass events with wrong become events", func(t *testing.T) { + t.Parallel() + + // given + execution := &TestWorkflowExecution{} + e := Event{Type_: EventEndTestWorkflowFailed, TestWorkflowExecution: execution} + + // when + types, valid := e.Valid("", []EventType{BECOME_TESTWORKFLOW_UP_EventType}) + + // then + assert.Nil(t, types) + assert.False(t, valid) + }) } func TestEvent_IsSuccess(t *testing.T) { + t.Parallel() t.Run("should return true for success events", func(t *testing.T) { + t.Parallel() + events := map[EventType]bool{ START_TEST_EventType: false, START_TESTSUITE_EventType: false, @@ -158,23 +326,32 @@ func TestEvent_IsSuccess(t *testing.T) { } func TestEvent_Topic(t *testing.T) { + t.Parallel() t.Run("should return events topic if explicitly set", func(t *testing.T) { + t.Parallel() + evt := Event{Type_: EventStartTest, StreamTopic: "topic"} assert.Equal(t, "topic", evt.Topic()) }) t.Run("should return events topic if not resource set", func(t *testing.T) { + t.Parallel() + evt := Event{Type_: EventStartTest, Resource: nil} assert.Equal(t, "events.all", evt.Topic()) }) t.Run("should return event topic with resource name and id if set", func(t *testing.T) { + t.Parallel() + evt := Event{Type_: EventStartTest, Resource: EventResourceExecutor, ResourceId: "a12"} assert.Equal(t, "events.executor.a12", evt.Topic()) }) t.Run("should return event topic with resource name when id not set", func(t *testing.T) { + t.Parallel() + evt := Event{Type_: EventStartTest, Resource: EventResourceExecutor} assert.Equal(t, "events.executor", evt.Topic()) }) diff --git a/pkg/api/v1/testkube/model_event_type.go b/pkg/api/v1/testkube/model_event_type.go index 1fcd825675d..de89ff6837a 100644 --- a/pkg/api/v1/testkube/model_event_type.go +++ b/pkg/api/v1/testkube/model_event_type.go @@ -13,22 +13,36 @@ type EventType string // List of EventType const ( - START_TEST_EventType EventType = "start-test" - END_TEST_SUCCESS_EventType EventType = "end-test-success" - END_TEST_FAILED_EventType EventType = "end-test-failed" - END_TEST_ABORTED_EventType EventType = "end-test-aborted" - END_TEST_TIMEOUT_EventType EventType = "end-test-timeout" - START_TESTSUITE_EventType EventType = "start-testsuite" - END_TESTSUITE_SUCCESS_EventType EventType = "end-testsuite-success" - END_TESTSUITE_FAILED_EventType EventType = "end-testsuite-failed" - END_TESTSUITE_ABORTED_EventType EventType = "end-testsuite-aborted" - END_TESTSUITE_TIMEOUT_EventType EventType = "end-testsuite-timeout" - QUEUE_TESTWORKFLOW_EventType EventType = "queue-testworkflow" - START_TESTWORKFLOW_EventType EventType = "start-testworkflow" - END_TESTWORKFLOW_SUCCESS_EventType EventType = "end-testworkflow-success" - END_TESTWORKFLOW_FAILED_EventType EventType = "end-testworkflow-failed" - END_TESTWORKFLOW_ABORTED_EventType EventType = "end-testworkflow-aborted" - CREATED_EventType EventType = "created" - UPDATED_EventType EventType = "updated" - DELETED_EventType EventType = "deleted" + START_TEST_EventType EventType = "start-test" + END_TEST_SUCCESS_EventType EventType = "end-test-success" + END_TEST_FAILED_EventType EventType = "end-test-failed" + END_TEST_ABORTED_EventType EventType = "end-test-aborted" + END_TEST_TIMEOUT_EventType EventType = "end-test-timeout" + BECOME_TEST_UP_EventType EventType = "become-test-up" + BECOME_TEST_DOWN_EventType EventType = "become-test-down" + BECOME_TEST_FAILED_EventType EventType = "become-test-failed" + BECOME_TEST_ABORTED_EventType EventType = "become-test-aborted" + BECOME_TEST_TIMEOUT_EventType EventType = "become-test-timeout" + START_TESTSUITE_EventType EventType = "start-testsuite" + END_TESTSUITE_SUCCESS_EventType EventType = "end-testsuite-success" + END_TESTSUITE_FAILED_EventType EventType = "end-testsuite-failed" + END_TESTSUITE_ABORTED_EventType EventType = "end-testsuite-aborted" + END_TESTSUITE_TIMEOUT_EventType EventType = "end-testsuite-timeout" + BECOME_TESTSUITE_UP_EventType EventType = "become-testsuite-up" + BECOME_TESTSUITE_DOWN_EventType EventType = "become-testsuite-down" + BECOME_TESTSUITE_FAILED_EventType EventType = "become-testsuite-failed" + BECOME_TESTSUITE_ABORTED_EventType EventType = "become-testsuite-aborted" + BECOME_TESTSUITE_TIMEOUT_EventType EventType = "become-testsuite-timeout" + QUEUE_TESTWORKFLOW_EventType EventType = "queue-testworkflow" + START_TESTWORKFLOW_EventType EventType = "start-testworkflow" + END_TESTWORKFLOW_SUCCESS_EventType EventType = "end-testworkflow-success" + END_TESTWORKFLOW_FAILED_EventType EventType = "end-testworkflow-failed" + END_TESTWORKFLOW_ABORTED_EventType EventType = "end-testworkflow-aborted" + BECOME_TESTWORKFLOW_UP_EventType EventType = "become-testworkflow-up" + BECOME_TESTWORKFLOW_DOWN_EventType EventType = "become-testworkflow-down" + BECOME_TESTWORKFLOW_FAILED_EventType EventType = "become-testworkflow-failed" + BECOME_TESTWORKFLOW_ABORTED_EventType EventType = "become-testworkflow-aborted" + CREATED_EventType EventType = "created" + UPDATED_EventType EventType = "updated" + DELETED_EventType EventType = "deleted" ) diff --git a/pkg/api/v1/testkube/model_event_type_extended.go b/pkg/api/v1/testkube/model_event_type_extended.go index 8d4e1733890..f438c848f16 100644 --- a/pkg/api/v1/testkube/model_event_type_extended.go +++ b/pkg/api/v1/testkube/model_event_type_extended.go @@ -57,3 +57,166 @@ func EventTypesFromSlice(types []string) []EventType { } return t } + +func (t EventType) IsBecome() bool { + types := []EventType{ + BECOME_TEST_UP_EventType, + BECOME_TEST_DOWN_EventType, + BECOME_TEST_FAILED_EventType, + BECOME_TEST_ABORTED_EventType, + BECOME_TEST_TIMEOUT_EventType, + + BECOME_TESTSUITE_UP_EventType, + BECOME_TESTSUITE_DOWN_EventType, + BECOME_TESTSUITE_FAILED_EventType, + BECOME_TESTSUITE_ABORTED_EventType, + BECOME_TESTSUITE_TIMEOUT_EventType, + + BECOME_TESTWORKFLOW_UP_EventType, + BECOME_TESTWORKFLOW_DOWN_EventType, + BECOME_TESTWORKFLOW_FAILED_EventType, + BECOME_TESTWORKFLOW_ABORTED_EventType, + } + + for _, tp := range types { + if tp == t { + return true + } + } + + return false +} + +func (t EventType) MapBecomeToRegular() []EventType { + eventMap := map[EventType][]EventType{ + BECOME_TEST_UP_EventType: {END_TEST_SUCCESS_EventType}, + BECOME_TEST_DOWN_EventType: {END_TEST_FAILED_EventType, END_TEST_ABORTED_EventType, END_TEST_TIMEOUT_EventType}, + BECOME_TEST_FAILED_EventType: {END_TEST_FAILED_EventType}, + BECOME_TEST_ABORTED_EventType: {END_TEST_ABORTED_EventType}, + BECOME_TEST_TIMEOUT_EventType: {END_TEST_TIMEOUT_EventType}, + + BECOME_TESTSUITE_UP_EventType: {END_TESTSUITE_SUCCESS_EventType}, + BECOME_TESTSUITE_DOWN_EventType: {END_TESTSUITE_FAILED_EventType, END_TESTSUITE_ABORTED_EventType, END_TESTSUITE_TIMEOUT_EventType}, + BECOME_TESTSUITE_FAILED_EventType: {END_TESTSUITE_FAILED_EventType}, + BECOME_TESTSUITE_ABORTED_EventType: {END_TESTSUITE_ABORTED_EventType}, + BECOME_TESTSUITE_TIMEOUT_EventType: {END_TESTSUITE_TIMEOUT_EventType}, + + BECOME_TESTWORKFLOW_UP_EventType: {END_TESTWORKFLOW_SUCCESS_EventType}, + BECOME_TESTWORKFLOW_DOWN_EventType: {END_TESTWORKFLOW_FAILED_EventType, END_TESTWORKFLOW_ABORTED_EventType}, + BECOME_TESTWORKFLOW_FAILED_EventType: {END_TESTWORKFLOW_FAILED_EventType}, + BECOME_TESTWORKFLOW_ABORTED_EventType: {END_TESTWORKFLOW_ABORTED_EventType}, + } + + return eventMap[t] +} + +func (t EventType) IsBecomeExecutionStatus(previousStatus ExecutionStatus) bool { + eventMap := map[EventType]map[ExecutionStatus]struct{}{ + BECOME_TEST_UP_EventType: { + FAILED_ExecutionStatus: {}, + ABORTED_ExecutionStatus: {}, + TIMEOUT_ExecutionStatus: {}, + }, + + BECOME_TEST_DOWN_EventType: { + PASSED_ExecutionStatus: {}, + }, + + BECOME_TEST_FAILED_EventType: { + PASSED_ExecutionStatus: {}, + ABORTED_ExecutionStatus: {}, + TIMEOUT_ExecutionStatus: {}, + }, + + BECOME_TEST_ABORTED_EventType: { + PASSED_ExecutionStatus: {}, + FAILED_ExecutionStatus: {}, + TIMEOUT_ExecutionStatus: {}, + }, + + BECOME_TEST_TIMEOUT_EventType: { + PASSED_ExecutionStatus: {}, + FAILED_ExecutionStatus: {}, + ABORTED_ExecutionStatus: {}, + }, + } + + if statusMap, ok := eventMap[t]; ok { + if _, ok := statusMap[previousStatus]; ok { + return true + } + } + + return false +} + +func (t EventType) IsBecomeTestSuiteExecutionStatus(previousStatus TestSuiteExecutionStatus) bool { + eventMap := map[EventType]map[TestSuiteExecutionStatus]struct{}{ + BECOME_TESTSUITE_UP_EventType: { + FAILED_TestSuiteExecutionStatus: {}, + ABORTED_TestSuiteExecutionStatus: {}, + TIMEOUT_TestSuiteExecutionStatus: {}, + }, + + BECOME_TESTSUITE_DOWN_EventType: { + PASSED_TestSuiteExecutionStatus: {}, + }, + + BECOME_TESTSUITE_FAILED_EventType: { + PASSED_TestSuiteExecutionStatus: {}, + ABORTED_TestSuiteExecutionStatus: {}, + TIMEOUT_TestSuiteExecutionStatus: {}, + }, + + BECOME_TESTSUITE_ABORTED_EventType: { + PASSED_TestSuiteExecutionStatus: {}, + FAILED_TestSuiteExecutionStatus: {}, + TIMEOUT_TestSuiteExecutionStatus: {}, + }, + + BECOME_TESTSUITE_TIMEOUT_EventType: { + PASSED_TestSuiteExecutionStatus: {}, + FAILED_TestSuiteExecutionStatus: {}, + ABORTED_TestSuiteExecutionStatus: {}, + }, + } + + if statusMap, ok := eventMap[t]; ok { + if _, ok := statusMap[previousStatus]; ok { + return true + } + } + + return false +} + +func (t EventType) IsBecomeTestWorkflowExecutionStatus(previousStatus TestWorkflowStatus) bool { + eventMap := map[EventType]map[TestWorkflowStatus]struct{}{ + BECOME_TESTWORKFLOW_UP_EventType: { + FAILED_TestWorkflowStatus: {}, + ABORTED_TestWorkflowStatus: {}, + }, + + BECOME_TESTWORKFLOW_DOWN_EventType: { + PASSED_TestWorkflowStatus: {}, + }, + + BECOME_TESTWORKFLOW_FAILED_EventType: { + PASSED_TestWorkflowStatus: {}, + ABORTED_TestWorkflowStatus: {}, + }, + + BECOME_TESTWORKFLOW_ABORTED_EventType: { + PASSED_TestWorkflowStatus: {}, + FAILED_TestWorkflowStatus: {}, + }, + } + + if statusMap, ok := eventMap[t]; ok { + if _, ok := statusMap[previousStatus]; ok { + return true + } + } + + return false +} diff --git a/pkg/api/v1/testkube/model_event_type_extended_test.go b/pkg/api/v1/testkube/model_event_type_extended_test.go new file mode 100644 index 00000000000..10d8af1617f --- /dev/null +++ b/pkg/api/v1/testkube/model_event_type_extended_test.go @@ -0,0 +1,191 @@ +package testkube + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEventType_IsBecome(t *testing.T) { + t.Parallel() + + t.Run("should return true for become events", func(t *testing.T) { + t.Parallel() + + events := map[EventType]bool{ + START_TEST_EventType: false, + BECOME_TEST_UP_EventType: true, + START_TESTSUITE_EventType: false, + BECOME_TESTSUITE_UP_EventType: true, + START_TESTWORKFLOW_EventType: false, + BECOME_TESTWORKFLOW_UP_EventType: true, + } + + for eventType, expected := range events { + // given + e := Event{Type_: &eventType} + + // when + become := e.Type_.IsBecome() + + // then + assert.Equal(t, expected, become) + } + }) + +} + +func TestEventType_MapBecomeToRegular(t *testing.T) { + t.Parallel() + + t.Run("should return event types for become events", func(t *testing.T) { + t.Parallel() + + events := map[EventType][]EventType{ + START_TEST_EventType: nil, + BECOME_TEST_UP_EventType: {END_TEST_SUCCESS_EventType}, + START_TESTSUITE_EventType: nil, + BECOME_TESTSUITE_UP_EventType: {END_TESTSUITE_SUCCESS_EventType}, + START_TESTWORKFLOW_EventType: nil, + BECOME_TESTWORKFLOW_UP_EventType: {END_TESTWORKFLOW_SUCCESS_EventType}, + } + + for eventType, expected := range events { + // given + e := Event{Type_: &eventType} + + // when + types := e.Type_.MapBecomeToRegular() + + // then + assert.Equal(t, expected, types) + } + }) + +} + +func TestEventType_IsBecomeExecutionStatus(t *testing.T) { + t.Parallel() + + t.Run("should return true for become execution status", func(t *testing.T) { + t.Parallel() + + events := []struct { + eventType EventType + status ExecutionStatus + result bool + }{ + { + eventType: BECOME_TEST_UP_EventType, + status: FAILED_ExecutionStatus, + result: true, + }, + { + eventType: END_TEST_SUCCESS_EventType, + status: FAILED_ExecutionStatus, + result: false, + }, + { + eventType: BECOME_TEST_UP_EventType, + status: PASSED_ExecutionStatus, + result: false, + }, + } + + for _, expected := range events { + // given + e := Event{Type_: &expected.eventType} + + // when + become := e.Type_.IsBecomeExecutionStatus(expected.status) + + // then + assert.Equal(t, expected.result, become) + } + }) + +} + +func TestEventType_IsBecomeTestSuiteExecutionStatus(t *testing.T) { + t.Parallel() + + t.Run("should return true for become test suite execution status", func(t *testing.T) { + t.Parallel() + + events := []struct { + eventType EventType + status TestSuiteExecutionStatus + result bool + }{ + { + eventType: BECOME_TESTSUITE_UP_EventType, + status: FAILED_TestSuiteExecutionStatus, + result: true, + }, + { + eventType: END_TESTSUITE_SUCCESS_EventType, + status: FAILED_TestSuiteExecutionStatus, + result: false, + }, + { + eventType: BECOME_TESTSUITE_UP_EventType, + status: PASSED_TestSuiteExecutionStatus, + result: false, + }, + } + + for _, expected := range events { + // given + e := Event{Type_: &expected.eventType} + + // when + become := e.Type_.IsBecomeTestSuiteExecutionStatus(expected.status) + + // then + assert.Equal(t, expected.result, become) + } + }) + +} + +func TestEventType_IsBecomeTestWorkflowExecutionStatus(t *testing.T) { + t.Parallel() + + t.Run("should return true for become test workflow execution status", func(t *testing.T) { + t.Parallel() + + events := []struct { + eventType EventType + status TestWorkflowStatus + result bool + }{ + { + eventType: BECOME_TESTWORKFLOW_UP_EventType, + status: FAILED_TestWorkflowStatus, + result: true, + }, + { + eventType: END_TESTWORKFLOW_SUCCESS_EventType, + status: FAILED_TestWorkflowStatus, + result: false, + }, + { + eventType: BECOME_TESTWORKFLOW_UP_EventType, + status: PASSED_TestWorkflowStatus, + result: false, + }, + } + + for _, expected := range events { + // given + e := Event{Type_: &expected.eventType} + + // when + become := e.Type_.IsBecomeTestWorkflowExecutionStatus(expected.status) + + // then + assert.Equal(t, expected.result, become) + } + }) + +} diff --git a/pkg/api/v1/testkube/model_webhook.go b/pkg/api/v1/testkube/model_webhook.go index 34700b1b69f..1302ed9b2f0 100644 --- a/pkg/api/v1/testkube/model_webhook.go +++ b/pkg/api/v1/testkube/model_webhook.go @@ -29,6 +29,4 @@ type Webhook struct { Labels map[string]string `json:"labels,omitempty"` // whether webhook is disabled Disabled bool `json:"disabled,omitempty"` - // whether webhook is triggered on state change only - OnStateChange bool `json:"onStateChange,omitempty"` } diff --git a/pkg/api/v1/testkube/model_webhook_create_request.go b/pkg/api/v1/testkube/model_webhook_create_request.go index ebbcb8f0989..d98e35a0710 100644 --- a/pkg/api/v1/testkube/model_webhook_create_request.go +++ b/pkg/api/v1/testkube/model_webhook_create_request.go @@ -29,6 +29,4 @@ type WebhookCreateRequest struct { Labels map[string]string `json:"labels,omitempty"` // whether webhook is disabled Disabled bool `json:"disabled,omitempty"` - // whether webhook is triggered on state change only - OnStateChange bool `json:"onStateChange,omitempty"` } diff --git a/pkg/api/v1/testkube/model_webhook_extended.go b/pkg/api/v1/testkube/model_webhook_extended.go index f723fd10b64..c937aca7f5e 100644 --- a/pkg/api/v1/testkube/model_webhook_extended.go +++ b/pkg/api/v1/testkube/model_webhook_extended.go @@ -14,7 +14,7 @@ import "fmt" type Webhooks []Webhook func (list Webhooks) Table() (header []string, output [][]string) { - header = []string{"Name", "URI", "Events", "Selector", "Labels", "Disabled", "On State Change"} + header = []string{"Name", "URI", "Events", "Selector", "Labels", "Disabled"} for _, e := range list { output = append(output, []string{ @@ -24,7 +24,6 @@ func (list Webhooks) Table() (header []string, output [][]string) { e.Selector, MapToString(e.Labels), fmt.Sprint(e.Disabled), - fmt.Sprint(e.OnStateChange), }) } diff --git a/pkg/api/v1/testkube/model_webhook_update_request.go b/pkg/api/v1/testkube/model_webhook_update_request.go index 111ed24d956..8ec6795be9e 100644 --- a/pkg/api/v1/testkube/model_webhook_update_request.go +++ b/pkg/api/v1/testkube/model_webhook_update_request.go @@ -29,6 +29,4 @@ type WebhookUpdateRequest struct { Labels *map[string]string `json:"labels,omitempty"` // whether webhook is disabled Disabled *bool `json:"disabled,omitempty"` - // whether webhook is triggered on state change only - OnStateChange *bool `json:"onStateChange,omitempty"` } diff --git a/pkg/event/emitter.go b/pkg/event/emitter.go index 52fd65fd4e7..c2445aef6f8 100644 --- a/pkg/event/emitter.go +++ b/pkg/event/emitter.go @@ -170,15 +170,18 @@ func (e *Emitter) stopListener(name string) { if err != nil { e.Log.Errorw("error while stopping listener", "error", err) } - e.Log.Infow("stopped listener", name) + e.Log.Info("stopped listener", name) } func (e *Emitter) notifyHandler(l common.Listener) bus.Handler { logger := e.Log.With("listen-on", l.Events(), "queue-group", l.Name(), "selector", l.Selector(), "metadata", l.Metadata()) return func(event testkube.Event) error { - if event.Valid(l.Selector(), l.Events()) { - result := l.Notify(event) - log.Tracew(logger, "listener notified", append(event.Log(), "result", result)...) + if types, valid := event.Valid(l.Selector(), l.Events()); valid { + for i := range types { + event.Type_ = &types[i] + result := l.Notify(event) + log.Tracew(logger, "listener notified", append(event.Log(), "result", result)...) + } } else { log.Tracew(logger, "dropping event not matching selector or type", event.Log()...) } @@ -191,7 +194,7 @@ func (e *Emitter) Reconcile(ctx context.Context) { for { select { case <-ctx.Done(): - e.Log.Infow("stopping reconciler") + e.Log.Info("stopping reconciler") return default: listeners := e.Loader.Reconcile() diff --git a/pkg/event/emitter_test.go b/pkg/event/emitter_test.go index 52198f5f2a9..8d4ba6126fc 100644 --- a/pkg/event/emitter_test.go +++ b/pkg/event/emitter_test.go @@ -124,6 +124,42 @@ func TestEmitter_Notify(t *testing.T) { }) } +func TestEmitter_NotifyBecome(t *testing.T) { + t.Parallel() + + t.Run("notifies listeners in queue groups for become events", func(t *testing.T) { + t.Parallel() + // given + eventBus := bus.NewEventBusMock() + emitter := NewEmitter(eventBus, "", nil) + // and 2 listeners subscribed to the same queue + // * first on pod1 + listener1 := &dummy.DummyListener{Id: "l5", Types: []testkube.EventType{ + testkube.BECOME_TEST_FAILED_EventType, testkube.BECOME_TEST_DOWN_EventType, testkube.END_TEST_FAILED_EventType}} + // * second on pod2 + listener2 := &dummy.DummyListener{Id: "l5", Types: []testkube.EventType{ + testkube.BECOME_TEST_FAILED_EventType, testkube.BECOME_TEST_DOWN_EventType, testkube.END_TEST_FAILED_EventType}} + + emitter.Register(listener1) + emitter.Register(listener2) + + // and listening emitter + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + emitter.Listen(ctx) + + time.Sleep(time.Millisecond * 50) + + // when event sent to queue group + emitter.Notify(newExampleTestEvent5()) + + time.Sleep(time.Millisecond * 50) + + // then only one listener should be notified + assert.Equal(t, 3, listener2.GetNotificationCount()+listener1.GetNotificationCount()) + }) +} + func TestEmitter_Reconcile(t *testing.T) { t.Parallel() @@ -178,6 +214,14 @@ func newExampleTestEvent2() testkube.Event { } } +func newExampleTestEvent5() testkube.Event { + return testkube.Event{ + Id: "eventID5", + Type_: testkube.EventEndTestFailed, + TestExecution: testkube.NewExecutionWithID("executionID5", "test/test", "test"), + } +} + func TestEmitter_UpdateListeners(t *testing.T) { t.Parallel() diff --git a/pkg/event/kind/dummy/listener.go b/pkg/event/kind/dummy/listener.go index df84804dd1e..107624f3856 100644 --- a/pkg/event/kind/dummy/listener.go +++ b/pkg/event/kind/dummy/listener.go @@ -14,6 +14,7 @@ type DummyListener struct { Id string NotificationCount int32 SelectorString string + Types []testkube.EventType } func (l *DummyListener) GetNotificationCount() int { @@ -35,6 +36,10 @@ func (l *DummyListener) Name() string { } func (l *DummyListener) Events() []testkube.EventType { + if l.Types != nil { + return l.Types + } + return testkube.AllEventTypes } diff --git a/pkg/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 66403f267d8..d2ad5379bdb 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -29,7 +29,6 @@ var _ common.Listener = (*WebhookListener)(nil) func NewWebhookListener(name, uri, selector string, events []testkube.EventType, payloadObjectField, payloadTemplate string, headers map[string]string, disabled bool, - onStateChange bool, testExecutionResults result.Repository, testSuiteExecutionResults testresult.Repository, testWorkflowExecutionResults testworkflow.Repository, @@ -46,7 +45,6 @@ func NewWebhookListener(name, uri, selector string, events []testkube.EventType, payloadTemplate: payloadTemplate, headers: headers, disabled: disabled, - onStateChange: onStateChange, testExecutionResults: testExecutionResults, testSuiteExecutionResults: testSuiteExecutionResults, testWorkflowExecutionResults: testWorkflowExecutionResults, @@ -66,7 +64,6 @@ type WebhookListener struct { payloadTemplate string headers map[string]string disabled bool - onStateChange bool testExecutionResults result.Repository testSuiteExecutionResults testresult.Repository testWorkflowExecutionResults testworkflow.Repository @@ -95,7 +92,6 @@ func (l *WebhookListener) Metadata() map[string]string { "payloadTemplate": l.payloadTemplate, "headers": fmt.Sprintf("%v", l.headers), "disabled": fmt.Sprint(l.disabled), - "onStateChange": fmt.Sprint(l.onStateChange), } } @@ -149,13 +145,13 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes return } - if l.onStateChange { - changed, err := l.hasStateChanges(event) + if event.Type_ != nil && event.Type_.IsBecome() { + became, err := l.hasBecomeState(event) if err != nil { - l.Log.With(event.Log()...).Errorw(fmt.Sprintf("could not get previous finished state for test %s", event.TestExecution.TestName), "error", err) + l.Log.With(event.Log()...).Errorw("could not get previous finished state", "error", err) } - if !changed { - return testkube.NewSuccessEventResult(event.Id, "webhook set to state change only; state has not changed") + if !became { + return testkube.NewSuccessEventResult(event.Id, "webhook is set to become state only; state has not become") } } @@ -274,48 +270,49 @@ func (l *WebhookListener) processTemplate(field, body string, event testkube.Eve return buffer.Bytes(), nil } -func (l *WebhookListener) hasStateChanges(event testkube.Event) (bool, error) { +func (l *WebhookListener) hasBecomeState(event testkube.Event) (bool, error) { log := l.Log.With(event.Log()...) - if event.TestExecution != nil && event.TestExecution.ExecutionResult != nil { + if event.TestExecution != nil && event.Type_ != nil { prevStatus, err := l.testExecutionResults.GetPreviousFinishedState(context.Background(), event.TestExecution.TestName, event.TestExecution.EndTime) if err != nil { return false, err } + if prevStatus == "" { log.Debugw(fmt.Sprintf("no previous finished state for test %s", event.TestExecution.TestName)) return true, nil } - return *event.TestExecution.ExecutionResult.Status != prevStatus, nil + return event.Type_.IsBecomeExecutionStatus(prevStatus), nil } - if event.TestSuiteExecution != nil && event.TestSuiteExecution.Status != nil { + if event.TestSuiteExecution != nil && event.TestSuiteExecution.TestSuite != nil && event.Type_ != nil { prevStatus, err := l.testSuiteExecutionResults.GetPreviousFinishedState(context.Background(), event.TestSuiteExecution.TestSuite.Name, event.TestSuiteExecution.EndTime) if err != nil { - log.Errorw(fmt.Sprintf("could not get previous finished state for test suite %s", event.TestSuiteExecution.TestSuite.Name), "error", err) return false, err } + if prevStatus == "" { log.Debugw(fmt.Sprintf("no previous finished state for test suite %s", event.TestSuiteExecution.TestSuite.Name)) return true, nil } - return *event.TestSuiteExecution.Status != prevStatus, nil + return event.Type_.IsBecomeTestSuiteExecutionStatus(prevStatus), nil } - if event.TestWorkflowExecution != nil && event.TestWorkflowExecution.Result != nil { + if event.TestWorkflowExecution != nil && event.TestWorkflowExecution.Workflow != nil && event.Type_ != nil { prevStatus, err := l.testWorkflowExecutionResults.GetPreviousFinishedState(context.Background(), event.TestWorkflowExecution.Workflow.Name, event.TestWorkflowExecution.StatusAt) - if err != nil { - log.Errorw(fmt.Sprintf("could not get previous finished state for test workflow %s", event.TestWorkflowExecution.Workflow.Name), "error", err) return false, err } + if prevStatus == "" { log.Debugw(fmt.Sprintf("no previous finished state for test workflow %s", event.TestWorkflowExecution.Workflow.Name)) return true, nil } - return *event.TestWorkflowExecution.Result.Status != prevStatus, nil + + return event.Type_.IsBecomeTestWorkflowExecutionStatus(prevStatus), nil } return false, nil diff --git a/pkg/event/kind/webhook/listener_test.go b/pkg/event/kind/webhook/listener_test.go index e0ba0fa7d27..4d34276523a 100644 --- a/pkg/event/kind/webhook/listener_test.go +++ b/pkg/event/kind/webhook/listener_test.go @@ -34,7 +34,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -56,7 +56,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -73,7 +73,7 @@ func TestWebhookListener_Notify(t *testing.T) { t.Parallel() // given - s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) + s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := s.Notify(testkube.Event{ @@ -106,7 +106,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -133,7 +133,7 @@ func TestWebhookListener_Notify(t *testing.T) { defer svr.Close() l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "{\"id\": \"{{ .Id }}\"}", - map[string]string{"Content-Type": "application/json"}, false, false, nil, nil, nil, v1.NewMetrics(), nil) + map[string]string{"Content-Type": "application/json"}, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -150,7 +150,7 @@ func TestWebhookListener_Notify(t *testing.T) { t.Parallel() // given - s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, true, false, nil, nil, nil, v1.NewMetrics(), nil) + s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, true, nil, nil, nil, v1.NewMetrics(), nil) // when r := s.Notify(testkube.Event{ diff --git a/pkg/event/kind/webhook/loader.go b/pkg/event/kind/webhook/loader.go index fb78fca9479..b5a3c28d8a6 100644 --- a/pkg/event/kind/webhook/loader.go +++ b/pkg/event/kind/webhook/loader.go @@ -86,7 +86,7 @@ func (r WebhooksLoader) Load() (listeners common.Listeners, err error) { 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, payloadTemplate, webhook.Spec.Headers, webhook.Spec.Disabled, - webhook.Spec.OnStateChange, r.testExecutionResults, r.testSuiteExecutionResults, r.testWorkflowExecutionResults, + r.testExecutionResults, r.testSuiteExecutionResults, r.testWorkflowExecutionResults, r.metrics, r.proContext)) } diff --git a/pkg/mapper/webhooks/mapper.go b/pkg/mapper/webhooks/mapper.go index 5febf357095..36da188012d 100644 --- a/pkg/mapper/webhooks/mapper.go +++ b/pkg/mapper/webhooks/mapper.go @@ -21,7 +21,6 @@ func MapCRDToAPI(item executorv1.Webhook) testkube.Webhook { PayloadTemplateReference: item.Spec.PayloadTemplateReference, Headers: item.Spec.Headers, Disabled: item.Spec.Disabled, - OnStateChange: item.Spec.OnStateChange, } } @@ -58,7 +57,6 @@ func MapAPIToCRD(request testkube.WebhookCreateRequest) executorv1.Webhook { PayloadTemplateReference: request.PayloadTemplateReference, Headers: request.Headers, Disabled: request.Disabled, - OnStateChange: request.OnStateChange, }, } } @@ -129,10 +127,6 @@ func MapUpdateToSpec(request testkube.WebhookUpdateRequest, webhook *executorv1. webhook.Spec.Disabled = *request.Disabled } - if request.OnStateChange != nil { - webhook.Spec.OnStateChange = *request.OnStateChange - } - return webhook } @@ -182,7 +176,6 @@ func MapSpecToUpdate(webhook *executorv1.Webhook) (request testkube.WebhookUpdat request.Labels = &webhook.Labels request.Headers = &webhook.Spec.Headers request.Disabled = &webhook.Spec.Disabled - request.OnStateChange = &webhook.Spec.OnStateChange return request } diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go index 66f7bbd56f2..6d93ad9d94c 100644 --- a/pkg/scheduler/testsuite_scheduler.go +++ b/pkg/scheduler/testsuite_scheduler.go @@ -248,18 +248,14 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE if testsuiteExecution.Status != nil && *testsuiteExecution.Status == testkube.ABORTING_TestSuiteExecutionStatus { if abortionStatus != nil && *abortionStatus == testkube.TIMEOUT_TestSuiteExecutionStatus { - s.events.Notify(testkube.NewEventEndTestSuiteTimeout(testsuiteExecution)) testsuiteExecution.Status = testkube.TestSuiteExecutionStatusTimeout } else { - s.events.Notify(testkube.NewEventEndTestSuiteAborted(testsuiteExecution)) testsuiteExecution.Status = testkube.TestSuiteExecutionStatusAborted } } else if hasFailedSteps { testsuiteExecution.Status = testkube.TestSuiteExecutionStatusFailed - s.events.Notify(testkube.NewEventEndTestSuiteFailed(testsuiteExecution)) } else { testsuiteExecution.Status = testkube.TestSuiteExecutionStatusPassed - s.events.Notify(testkube.NewEventEndTestSuiteSuccess(testsuiteExecution)) } s.metrics.IncAndObserveExecuteTestSuite(*testsuiteExecution, s.dashboardURI) @@ -281,6 +277,19 @@ func (s *Scheduler) runAfterEachStep(ctx context.Context, execution *testkube.Te wg.Done() + if execution.Status != nil { + switch *execution.Status { + case *testkube.TestSuiteExecutionStatusTimeout: + s.events.Notify(testkube.NewEventEndTestSuiteTimeout(execution)) + case *testkube.TestSuiteExecutionStatusAborted: + s.events.Notify(testkube.NewEventEndTestSuiteAborted(execution)) + case *testkube.TestSuiteExecutionStatusFailed: + s.events.Notify(testkube.NewEventEndTestSuiteFailed(execution)) + case *testkube.TestSuiteExecutionStatusPassed: + s.events.Notify(testkube.NewEventEndTestSuiteSuccess(execution)) + } + } + if execution.TestSuite != nil { testSuite, err := s.testSuitesClient.Get(execution.TestSuite.Name) if err != nil { From 31ad16827b5821c56cd84bb9d576ee45b0d89757 Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Tue, 23 Jul 2024 07:09:36 +0200 Subject: [PATCH 16/16] fix: read correctly parallel template (#5678) --- pkg/testworkflows/testworkflowresolver/analyze.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/testworkflows/testworkflowresolver/analyze.go b/pkg/testworkflows/testworkflowresolver/analyze.go index d79165d8a78..3a089ba0bb7 100644 --- a/pkg/testworkflows/testworkflowresolver/analyze.go +++ b/pkg/testworkflows/testworkflowresolver/analyze.go @@ -39,7 +39,7 @@ func listStepTemplates(cr testworkflowsv1.Step) map[string]struct{} { if cr.Parallel != nil { maps.Copy(v, listSpecTemplates(cr.Parallel.TestWorkflowSpec)) if cr.Parallel.Template != nil { - v[GetInternalTemplateName(cr.Template.Name)] = struct{}{} + v[GetInternalTemplateName(cr.Parallel.Template.Name)] = struct{}{} } } for i := range cr.Setup {