diff --git a/.github/workflows/sandbox-deletion.yaml b/.github/workflows/sandbox-deletion.yaml new file mode 100644 index 0000000000..a17e5eabf7 --- /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 }}"}' diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index c46d8fa634..eed82a971e 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 @@ -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 @@ -7872,7 +7879,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 +7967,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 +8507,6 @@ components: type: array items: $ref: "#/components/schemas/TestWorkflowEvent" - notifications: - $ref: "#/components/schemas/TestWorkflowNotificationsConfig" - TestWorkflowTemplateSpec: type: object @@ -9175,14 +9179,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 +9346,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/api-server/main.go b/cmd/api-server/main.go index 73bbf58d5a..97222a56e8 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/kubectl-testkube/commands/common/flags.go b/cmd/kubectl-testkube/commands/common/flags.go index 0363c96d00..d4fa076498 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 e436977e09..ed136379aa 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 69c2c427bf..e5ada8fad2 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 b4a6c895a1..baa81015b2 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 4da43ad066..33f6a88aa1 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 3343cb32e9..3409b5aa57 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 6a719d9968..890b1b604c 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 ff332d0a2e..f8087cf39d 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 f23ab92831..63084f9ec6 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 24e910010d..6b404ddd40 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 ecfe25f63e..3a4926f6ef 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 73ea3a33ec..36f2a3620e 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 f612258492..ad63ad0c07 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 47e0db5c8c..d843c4a595 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 05076da5af..d00f1734fd 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 7a42b731e6..afc648b221 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 561355417d..bc3cac9269 100644 --- a/cmd/kubectl-testkube/commands/webhooks/common.go +++ b/cmd/kubectl-testkube/commands/webhooks/common.go @@ -42,7 +42,7 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW return options, err } - onStateChange, err := cmd.Flags().GetBool("on-state-change") + disabled, err := cmd.Flags().GetBool("disable") if err != nil { return options, err } @@ -59,14 +59,7 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW PayloadTemplate: payloadTemplateContent, 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,26 +143,13 @@ 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 - } - - if cmd.Flag("on-state-change").Changed { - onStateChange, err := cmd.Flags().GetBool("on-state-change") + disabled, err := cmd.Flags().GetBool("disable") if err != nil { return options, err } - options.OnStateChange = &onStateChange + options.Disabled = &disabled } 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 5727c23cfd..c7f3b80478 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -23,7 +23,7 @@ func NewCreateWebhookCmd() *cobra.Command { headers map[string]string payloadTemplateReference string update bool - onStateChange bool + disable bool ) cmd := &cobra.Command{ @@ -39,10 +39,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 +46,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,9 +100,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(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change") + cmd.Flags().BoolVar(&disable, "disable", false, "disable webhook") + cmd.Flags().MarkDeprecated("enable", "enable webhook is deprecated") return cmd } diff --git a/cmd/kubectl-testkube/commands/webhooks/update.go b/cmd/kubectl-testkube/commands/webhooks/update.go index 2f3f9405f5..cbc93070d7 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -17,9 +17,7 @@ func UpdateWebhookCmd() *cobra.Command { payloadTemplate string headers map[string]string payloadTemplateReference string - enable bool disable bool - onStateChange bool ) cmd := &cobra.Command{ @@ -32,10 +30,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,8 +58,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().BoolVar(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change") + cmd.Flags().MarkDeprecated("enable", "enable webhook is depecated") return cmd } diff --git a/cmd/tcl/testworkflow-toolkit/commands/execute.go b/cmd/tcl/testworkflow-toolkit/commands/execute.go index 8d8aa468ff..b146ae5462 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 5f50fbfe10..6d6db65b96 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(), }) } @@ -298,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 d59cb72c1a..ab6ca5528d 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 c7fd620761..23862f9d79 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 { @@ -59,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 { @@ -145,6 +147,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 ea0830839e..a01787cbc1 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`) @@ -491,7 +513,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 +527,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 +574,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 +585,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,49 +595,11 @@ 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 +615,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,35 +625,11 @@ 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 +645,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 +655,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 - } - } -} -``` - @@ -906,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 ``` @@ -931,7 +713,6 @@ testkube update webhook --name example-webhook --on-state-change "end-test-failed" ], "disabled": false, - "onStateChange": true, } ``` @@ -950,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.md b/docs/docs/cli/testkube.md index 2c52219ccf..2a6926f608 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 c592f851ff..0727833a38 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 4599ae2819..b063de7fe8 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 0982a691dd..b18294d128 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 bdd9e7d8d2..c39b20d642 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 94c6b85e24..b34aeed934 100644 --- a/docs/docs/cli/testkube_create_webhook.md +++ b/docs/docs/cli/testkube_create_webhook.md @@ -14,7 +14,6 @@ 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 diff --git a/docs/docs/cli/testkube_debug.md b/docs/docs/cli/testkube_debug.md index 93522827c6..1e9f9473c9 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 0000000000..f1c08ffba9 --- /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 d513f4f5dd..4cd6f43b56 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 0000000000..a80ade1057 --- /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 a6566e42d8..79fa5113b2 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 b893b8ad0f..bb8c3fd8b8 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 f96d69df2c..0172faaaa5 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 1d7658dd59..7c6d5caf83 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 73761b86ef..70836df911 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 a0b74cf4cb..aba8477f0d 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 f21fe9d589..d5528f3968 100644 --- a/docs/docs/cli/testkube_update_webhook.md +++ b/docs/docs/cli/testkube_update_webhook.md @@ -14,7 +14,6 @@ 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 diff --git a/go.mod b/go.mod index 18be0b4193..0c7ce5572a 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.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 c824cf603b..6d131d1642 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.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/internal/config/config.go b/internal/config/config.go index b07e70ace1..7f48a51735 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/api/v1/client/testsuite.go b/pkg/api/v1/client/testsuite.go index 8578a2c057..d798bcf3b8 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_event_extended.go b/pkg/api/v1/testkube/model_event_extended.go index e16d860dfd..e9415a386e 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 811776cd21..cf0da3bb5c 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 1fcd825675..de89ff6837 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 8d4e173389..f438c848f1 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 0000000000..10d8af1617 --- /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_test_suite_execution_extended.go b/pkg/api/v1/testkube/model_test_suite_execution_extended.go index 2f7e45d70a..7d5c3dff2b 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 a99b2a7f66..fe4c874b0e 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 c844f5c48b..59c41e7d07 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 053366e55c..ca7734c076 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 0c06aca5e2..ff7774c6aa 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 347122569a..a80bad3d99 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 d3ed46b697..1bf7503518 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 14f34fa8dc..0000000000 --- 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 e61d0fe83a..dace346a30 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 b296a1e92d..fbea0caff0 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/api/v1/testkube/model_webhook.go b/pkg/api/v1/testkube/model_webhook.go index 34700b1b69..1302ed9b2f 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 ebbcb8f098..d98e35a071 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 f723fd10b6..c937aca7f5 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 111ed24d95..8ec6795be9 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/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 0000000000..0d84f8b460 --- /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 0000000000..22b9138cb5 --- /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 0000000000..e98d19836a --- /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/event/emitter.go b/pkg/event/emitter.go index 52fd65fd4e..c2445aef6f 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 52198f5f2a..8d4ba6126f 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 df84804dd1..107624f385 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 66403f267d..d2ad5379bd 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 e0ba0fa7d2..4d34276523 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 fb78fca947..b5a3c28d8a 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/executor/containerexecutor/containerexecutor.go b/pkg/executor/containerexecutor/containerexecutor.go index 0da5513b1e..83a476f791 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 44b81f5115..e03b6d8456 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 e60e7cd5ee..b9828d45ac 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/mapper/testexecutions/mapper.go b/pkg/mapper/testexecutions/mapper.go index b89aac2cd9..78a06fe234 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 767e746ff0..14b4e90e33 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 80f73ed391..00c6b36e96 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 8e545cd3c7..34f5748c4b 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 7b5549289b..4f38484735 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 6ea10a9c95..6195612252 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 3c38f80598..b87ef74865 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 d7a328bf03..8992de1fa1 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/mapper/webhooks/mapper.go b/pkg/mapper/webhooks/mapper.go index 5febf35709..36da188012 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/test_scheduler.go b/pkg/scheduler/test_scheduler.go index e7f374c518..9a654c49d5 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 edff555e35..6d93ad9d94 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 { @@ -252,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) @@ -285,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 { diff --git a/pkg/testworkflows/testworkflowexecutor/executor.go b/pkg/testworkflows/testworkflowexecutor/executor.go index f7c02dff68..b30f359c6d 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() @@ -434,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, @@ -444,10 +440,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 +482,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 +517,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 1546965341..0b2b7b9537 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}}", @@ -457,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}}", }) } diff --git a/pkg/testworkflows/testworkflowresolver/analyze.go b/pkg/testworkflows/testworkflowresolver/analyze.go index d79165d8a7..3a089ba0bb 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 {