From 7078f4f45a80e756a228ac646a742840b3d4e698 Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Wed, 7 Aug 2024 13:12:12 +0200 Subject: [PATCH] feat(TKC-2217): include Toolkit operations in other containers (#5740) * feat: include Init Process in the Toolkit image * feat: copy the Toolkit binary to the volume * feat: copy only the Busybox binaries from Toolkit image * chore: clean up actions optimization code a bit * chore: don't process setup step in action optimizations * feat: add `pure` flag for the ContainerStage to define if it can be combined with other container * feat: fit pure operations in TestWorkflow into other containers * fix: ensure timestamps for step results in case of aborting * feat: make `fetch` operation pure too * chore: delete obsolete comments * fix: unit tests and bugfixes * fix: use proper image for setup step --- .../docker-build-api-executors-tag.yaml | 2 +- .github/workflows/docker-build-develop.yaml | 2 +- .github/workflows/docker-build-release.yaml | 2 +- .github/workflows/sandbox.yaml | 2 +- build/testworkflow-init/Dockerfile | 1 + build/testworkflow-toolkit/Dockerfile | 5 + .../testworkflow-toolkit/commands/services.go | 2 +- cmd/tcl/testworkflow-toolkit/spawn/utils.go | 3 +- cmd/testworkflow-init/commands/setup.go | 16 +- cmd/testworkflow-init/data/constants.go | 1 + cmd/testworkflow-init/orchestration/setup.go | 2 +- ...leaser-docker-build-testworkflow-init.yml} | 8 +- ...aser-docker-build-testworkflow-toolkit.yml | 112 +++++++ .../model_test_workflow_result_extended.go | 15 + .../testworkflowprocessor/operations.go | 20 +- .../action/actiontypes/analysis.go | 104 ++++++ .../action/actiontypes/lite/action.go | 2 + .../action/actiontypes/lite/utils.go | 4 +- .../action/actiontypes/mutations.go | 275 +++++++++++++++ .../action/actiontypes/utils.go | 30 +- .../action/containerize.go | 53 +-- .../testworkflowprocessor/action/finalize.go | 110 ++++++ .../testworkflowprocessor/action/group.go | 120 ++++++- .../action/group_test.go | 12 +- .../testworkflowprocessor/action/optimize.go | 312 +----------------- .../testworkflowprocessor/action/process.go | 19 +- .../action/process_test.go | 55 +-- .../testworkflowprocessor/action/utils.go | 11 - .../constants/constants.go | 2 +- .../testworkflowprocessor/operations.go | 14 +- .../presets/processor_test.go | 190 +++-------- .../testworkflowprocessor/processor.go | 13 +- .../stage/containerstage.go | 20 ++ 33 files changed, 957 insertions(+), 582 deletions(-) rename goreleaser_files/{.goreleaser-docker-build-testworkflow.yml => .goreleaser-docker-build-testworkflow-init.yml} (96%) create mode 100644 goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml create mode 100644 pkg/testworkflows/testworkflowprocessor/action/actiontypes/analysis.go create mode 100644 pkg/testworkflows/testworkflowprocessor/action/actiontypes/mutations.go create mode 100644 pkg/testworkflows/testworkflowprocessor/action/finalize.go delete mode 100644 pkg/testworkflows/testworkflowprocessor/action/utils.go diff --git a/.github/workflows/docker-build-api-executors-tag.yaml b/.github/workflows/docker-build-api-executors-tag.yaml index 3738e48171e..f4992e84e5d 100644 --- a/.github/workflows/docker-build-api-executors-tag.yaml +++ b/.github/workflows/docker-build-api-executors-tag.yaml @@ -128,7 +128,7 @@ jobs: with: distribution: goreleaser version: latest - args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow.yml + args: release -f goreleaser_files/.goreleaser-docker-build-${{ matrix.service }}.yml env: GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }} ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}} diff --git a/.github/workflows/docker-build-develop.yaml b/.github/workflows/docker-build-develop.yaml index 4256104b181..7dd5b3201a8 100644 --- a/.github/workflows/docker-build-develop.yaml +++ b/.github/workflows/docker-build-develop.yaml @@ -114,7 +114,7 @@ jobs: with: distribution: goreleaser version: latest - args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow.yml --snapshot + args: release -f goreleaser_files/.goreleaser-docker-build-${{ matrix.service }}.yml --snapshot env: GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }} ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}} diff --git a/.github/workflows/docker-build-release.yaml b/.github/workflows/docker-build-release.yaml index 15ab7f70e7a..3e651f554d7 100644 --- a/.github/workflows/docker-build-release.yaml +++ b/.github/workflows/docker-build-release.yaml @@ -115,7 +115,7 @@ jobs: with: distribution: goreleaser version: latest - args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow.yml --snapshot + args: release -f goreleaser_files/.goreleaser-docker-build-${{ matrix.service }}.yml --snapshot env: GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }} ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}} diff --git a/.github/workflows/sandbox.yaml b/.github/workflows/sandbox.yaml index 8be21243722..e1c31c3478d 100644 --- a/.github/workflows/sandbox.yaml +++ b/.github/workflows/sandbox.yaml @@ -137,7 +137,7 @@ jobs: with: distribution: goreleaser version: latest - args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow.yml --snapshot + args: release -f goreleaser_files/.goreleaser-docker-build-${{ matrix.service }}.yml --snapshot env: GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }} ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}} diff --git a/build/testworkflow-init/Dockerfile b/build/testworkflow-init/Dockerfile index 60b14c150bd..633c502b061 100644 --- a/build/testworkflow-init/Dockerfile +++ b/build/testworkflow-init/Dockerfile @@ -1,6 +1,7 @@ # syntax=docker/dockerfile:1 ARG BUSYBOX_IMAGE FROM ${BUSYBOX_IMAGE} +RUN cp -rf /bin /.tktw-bin COPY testworkflow-init /init USER 1001 ENTRYPOINT ["/init"] diff --git a/build/testworkflow-toolkit/Dockerfile b/build/testworkflow-toolkit/Dockerfile index e45922ff9bc..99ff012e433 100644 --- a/build/testworkflow-toolkit/Dockerfile +++ b/build/testworkflow-toolkit/Dockerfile @@ -1,8 +1,13 @@ # syntax=docker/dockerfile:1 +ARG BUSYBOX_IMAGE ARG ALPINE_IMAGE + +FROM ${BUSYBOX_IMAGE} AS busybox FROM ${ALPINE_IMAGE} RUN apk --no-cache add ca-certificates libssl3 git openssh-client +COPY --from=busybox /bin /.tktw-bin COPY testworkflow-toolkit /toolkit +COPY testworkflow-init /init RUN adduser --disabled-password --home / --no-create-home --uid 1001 default USER 1001 ENTRYPOINT ["/toolkit"] diff --git a/cmd/tcl/testworkflow-toolkit/commands/services.go b/cmd/tcl/testworkflow-toolkit/commands/services.go index 07ce4f40e38..58fb1ec9232 100644 --- a/cmd/tcl/testworkflow-toolkit/commands/services.go +++ b/cmd/tcl/testworkflow-toolkit/commands/services.go @@ -179,7 +179,7 @@ func NewServicesCmd() *cobra.Command { v, err := expressions.EvalTemplate(svcSpec.Timeout, machines...) ui.ExitOnError(fmt.Sprintf("%s: %d: error: timeout expression", commontcl.ServiceLabel(name), index), err) d, err := time.ParseDuration(strings.ReplaceAll(v, " ", "")) - ui.ExitOnError(fmt.Sprintf("%s: %d: error: invalid timeout: %s:", commontcl.ServiceLabel(name), index, v), err) + ui.ExitOnError(fmt.Sprintf("%s: %d: error: invalid timeout: %s", commontcl.ServiceLabel(name), index, v), err) svcInstances[index].Timeout = &d } } diff --git a/cmd/tcl/testworkflow-toolkit/spawn/utils.go b/cmd/tcl/testworkflow-toolkit/spawn/utils.go index 83da8882b48..d2e71aed073 100644 --- a/cmd/tcl/testworkflow-toolkit/spawn/utils.go +++ b/cmd/tcl/testworkflow-toolkit/spawn/utils.go @@ -170,11 +170,12 @@ func ProcessFetch(transferSrv transfer.Server, fetch []testworkflowsv1.StepParal ContainerConfig: testworkflowsv1.ContainerConfig{ Image: env.Config().Images.Toolkit, ImagePullPolicy: corev1.PullIfNotPresent, - Command: common.Ptr([]string{"/toolkit", "transfer"}), + Command: common.Ptr([]string{constants.DefaultToolkitPath, "transfer"}), Env: []corev1.EnvVar{ {Name: "TK_NS", Value: env.Namespace()}, {Name: "TK_REF", Value: env.Ref()}, stage.BypassToolkitCheck, + stage.BypassPure, }, Args: &result, }, diff --git a/cmd/testworkflow-init/commands/setup.go b/cmd/testworkflow-init/commands/setup.go index ee78ded348e..7e57bc618a9 100644 --- a/cmd/testworkflow-init/commands/setup.go +++ b/cmd/testworkflow-init/commands/setup.go @@ -30,12 +30,26 @@ func Setup(config lite.ActionSetup) error { stdoutUnsafe.Print(" skipped\n") } + // Copy the toolkit + stdoutUnsafe.Print("Configuring toolkit...") + if config.CopyToolkit { + err := exec.Command("cp", "/toolkit", data.ToolkitPath).Run() + if err != nil { + stdoutUnsafe.Error(" error\n") + stdoutUnsafe.Errorf(" failed to copy the /toolkit utilities: %s\n", err.Error()) + return err + } + stdoutUnsafe.Print(" done\n") + } else { + stdoutUnsafe.Print(" skipped\n") + } + // Copy the shell and useful libraries stdoutUnsafe.Print("Configuring shell...") if config.CopyBinaries { // Use `cp` on the whole directory, as it has plenty of files, which lead to the same FS block. // Copying individual files will lead to high FS usage - err := exec.Command("cp", "-rf", "/bin", data.InternalBinPath).Run() + err := exec.Command("cp", "-rf", "/.tktw-bin", data.InternalBinPath).Run() if err != nil { stdoutUnsafe.Error(" error\n") stdoutUnsafe.Errorf(" failed to copy the binaries: %s\n", err.Error()) diff --git a/cmd/testworkflow-init/data/constants.go b/cmd/testworkflow-init/data/constants.go index 69ee95e0a5f..fff1490b399 100644 --- a/cmd/testworkflow-init/data/constants.go +++ b/cmd/testworkflow-init/data/constants.go @@ -11,6 +11,7 @@ const ( var ( InternalBinPath = filepath.Join(InternalPath, "bin") InitPath = filepath.Join(InternalPath, "init") + ToolkitPath = filepath.Join(InternalPath, "toolkit") StatePath = filepath.Join(InternalPath, "state") ) diff --git a/cmd/testworkflow-init/orchestration/setup.go b/cmd/testworkflow-init/orchestration/setup.go index c8057f94531..2df6e6ca23d 100644 --- a/cmd/testworkflow-init/orchestration/setup.go +++ b/cmd/testworkflow-init/orchestration/setup.go @@ -220,7 +220,7 @@ func (c *setup) SetWorkingDir(workingDir string) { wd = workingDir _ = os.MkdirAll(wd, 0755) } else { - err = os.MkdirAll(wd, 0755) + _ = os.MkdirAll(wd, 0755) } err = os.Chdir(wd) diff --git a/goreleaser_files/.goreleaser-docker-build-testworkflow.yml b/goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml similarity index 96% rename from goreleaser_files/.goreleaser-docker-build-testworkflow.yml rename to goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml index 90737ef5ea5..bf41b73b9e5 100644 --- a/goreleaser_files/.goreleaser-docker-build-testworkflow.yml +++ b/goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml @@ -19,8 +19,8 @@ env: - DOCKER_IMAGE_URL={{ if index .Env "SANDBOX_IMAGE" }}https://hub.docker.com/r/kubeshop/testkube-sandbox{{ else }}https://hub.docker.com/r/kubeshop/{{ .Env.REPOSITORY }}{{ end }} builds: - id: "linux" - main: "./cmd/{{ .Env.SERVICE }}" - binary: "{{ .Env.SERVICE }}" + main: "./cmd/testworkflow-init" + binary: "testworkflow-init" env: - CGO_ENABLED=0 goos: @@ -34,7 +34,7 @@ builds: -X github.com/kubeshop/testkube/pkg/version.Commit={{ .FullCommit }} -s -w dockers: - - dockerfile: ./build/{{ .Env.SERVICE }}/Dockerfile + - dockerfile: ./build/testworkflow-init/Dockerfile use: buildx goos: linux goarch: amd64 @@ -55,7 +55,7 @@ dockers: - "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}" - "--build-arg=BUSYBOX_IMAGE={{ .Env.BUSYBOX_IMAGE }}" - - dockerfile: ./build/{{ .Env.SERVICE }}/Dockerfile + - dockerfile: ./build/testworkflow-init/Dockerfile use: buildx goos: linux goarch: arm64 diff --git a/goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml b/goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml new file mode 100644 index 00000000000..5977c9bcf2b --- /dev/null +++ b/goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml @@ -0,0 +1,112 @@ +version: 2 +env: + # Goreleaser always uses the docker buildx builder with name "default"; see + # https://github.com/goreleaser/goreleaser/pull/3199 + # To use a builder other than "default", set this variable. + # Necessary for, e.g., GitHub actions cache integration. + - DOCKER_REPO={{ if index .Env "DOCKER_REPO" }}{{ .Env.DOCKER_REPO }}{{ else }}kubeshop{{ end }} + - DOCKER_BUILDX_BUILDER={{ if index .Env "DOCKER_BUILDX_BUILDER" }}{{ .Env.DOCKER_BUILDX_BUILDER }}{{ else }}default{{ end }} + # Setup to enable Docker to use, e.g., the GitHub actions cache; see + # https://docs.docker.com/build/building/cache/backends/ + # https://github.com/moby/buildkit#export-cache + - DOCKER_BUILDX_CACHE_FROM={{ if index .Env "DOCKER_BUILDX_CACHE_FROM" }}{{ .Env.DOCKER_BUILDX_CACHE_FROM }}{{ else }}type=registry{{ end }} + - DOCKER_BUILDX_CACHE_TO={{ if index .Env "DOCKER_BUILDX_CACHE_TO" }}{{ .Env.DOCKER_BUILDX_CACHE_TO }}{{ else }}type=inline{{ end }} + # Build image with commit sha tag + - IMAGE_TAG_SHA={{ if index .Env "IMAGE_TAG_SHA" }}{{ .Env.IMAGE_TAG_SHA }}{{ else }}{{ end }} + # Build Sandbox Image + - SANDBOX_IMAGE={{ if index .Env "SANDBOX_IMAGE" }}{{ .Env.SANDBOX_IMAGE }}{{ else }}{{ end }} + - DOCKER_IMAGE_TITLE={{ if index .Env "SANDBOX_IMAGE" }}testkube-sandbox-{{ .Env.SERVICE }}{{ else }}{{ .Env.REPOSITORY }}{{ end }} + - DOCKER_IMAGE_URL={{ if index .Env "SANDBOX_IMAGE" }}https://hub.docker.com/r/kubeshop/testkube-sandbox{{ else }}https://hub.docker.com/r/kubeshop/{{ .Env.REPOSITORY }}{{ end }} +builds: + - id: "linux-init" + main: "./cmd/testworkflow-init" + binary: "testworkflow-init" + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + mod_timestamp: "{{ .CommitTimestamp }}" + ldflags: + -X github.com/kubeshop/testkube/pkg/version.Version={{ .Version }} + -X github.com/kubeshop/testkube/pkg/version.Commit={{ .FullCommit }} + -s -w + - id: "linux-toolkit" + main: "./cmd/testworkflow-toolkit" + binary: "testworkflow-toolkit" + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + mod_timestamp: "{{ .CommitTimestamp }}" + ldflags: + -X github.com/kubeshop/testkube/pkg/version.Version={{ .Version }} + -X github.com/kubeshop/testkube/pkg/version.Commit={{ .FullCommit }} + -s -w +dockers: + - dockerfile: ./build/testworkflow-toolkit/Dockerfile + use: buildx + goos: linux + goarch: amd64 + image_templates: + - "{{ if .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .ShortCommit }}{{ end }}" + - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .Version }}-amd64{{ end }}" + - "{{ if .Env.SANDBOX_IMAGE }}{{ .Env.DOCKER_REPO }}/testkube-sandbox:{{ .Env.SERVICE }}-{{ .Env.BRANCH_IDENTIFIER }}-{{ .ShortCommit }}{{ end }}" + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.title={{ .Env.DOCKER_IMAGE_TITLE }}" + - "--label=org.opencontainers.image.url={{ .Env.DOCKER_IMAGE_URL }}" + - "--label=org.opencontainers.image.created={{ .Date}}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--builder={{ .Env.DOCKER_BUILDX_BUILDER }}" + - "--cache-to={{ .Env.DOCKER_BUILDX_CACHE_TO }}" + - "--cache-from={{ .Env.DOCKER_BUILDX_CACHE_FROM }}" + - "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}" + - "--build-arg=BUSYBOX_IMAGE={{ .Env.BUSYBOX_IMAGE }}" + + - dockerfile: ./build/testworkflow-toolkit/Dockerfile + use: buildx + goos: linux + goarch: arm64 + image_templates: + - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .Version }}-arm64v8{{ end }}" + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--builder={{ .Env.DOCKER_BUILDX_BUILDER }}" + - "--cache-to={{ .Env.DOCKER_BUILDX_CACHE_TO }}" + - "--cache-from={{ .Env.DOCKER_BUILDX_CACHE_FROM }}" + - "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}" + - "--build-arg=BUSYBOX_IMAGE={{ .Env.BUSYBOX_IMAGE }}" + +docker_manifests: + - name_template: "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .Version }}{{ end }}" + image_templates: + - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .Version }}-amd64{{ end }}" + - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .Version }}-arm64v8{{ end }}" + - name_template: "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:latest{{ end }}" + image_templates: + - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .Version }}-amd64{{ end }}" + - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/{{ .Env.REPOSITORY }}:{{ .Version }}-arm64v8{{ end }}" + + +release: + disable: true + +docker_signs: + - cmd: cosign + artifacts: all + output: true + args: + - "sign" + - "${artifact}" + - "--yes" \ No newline at end of file diff --git a/pkg/api/v1/testkube/model_test_workflow_result_extended.go b/pkg/api/v1/testkube/model_test_workflow_result_extended.go index 7dc4449ff32..52f451dd9e1 100644 --- a/pkg/api/v1/testkube/model_test_workflow_result_extended.go +++ b/pkg/api/v1/testkube/model_test_workflow_result_extended.go @@ -492,6 +492,21 @@ func predictTestWorkflowStepStatus(v TestWorkflowStepResult, sig TestWorkflowSig } func recomputeTestWorkflowStepResult(v TestWorkflowStepResult, sig TestWorkflowSignature, r *TestWorkflowResult) TestWorkflowStepResult { + // Ensure there is a queue time if the step is already started + if v.QueuedAt.IsZero() { + if !v.StartedAt.IsZero() { + v.QueuedAt = v.StartedAt + } else if !v.FinishedAt.IsZero() { + v.QueuedAt = v.FinishedAt + } + } + + // Ensure there is a start time if the step is already finished + if v.StartedAt.IsZero() && !v.FinishedAt.IsZero() { + v.StartedAt = v.QueuedAt + } + + // Compute children children := sig.Children if len(children) == 0 { return v diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.go index d10a179774e..84d35a0e2eb 100644 --- a/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.go +++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.go @@ -36,6 +36,9 @@ func ProcessExecute(_ testworkflowprocessor.InternalProcessor, layer testworkflo hasWorkflows := len(step.Execute.Workflows) > 0 hasTests := len(step.Execute.Tests) > 0 + // Allow to combine it within other containers + stage.SetPure(true) + // Fail if there is nothing to run if !hasTests && !hasWorkflows { return nil, errors.New("no test workflows and tests provided to the 'execute' step") @@ -44,7 +47,7 @@ func ProcessExecute(_ testworkflowprocessor.InternalProcessor, layer testworkflo container. SetImage(constants.DefaultToolkitImage). SetImagePullPolicy(corev1.PullIfNotPresent). - SetCommand("/toolkit", "execute"). + SetCommand(constants.DefaultToolkitPath, "execute"). EnableToolkit(stage.Ref()). AppendVolumeMounts(layer.AddEmptyDirVolume(nil, constants.DefaultTransferDirPath)) args := make([]string, 0) @@ -91,6 +94,9 @@ func ProcessParallel(_ testworkflowprocessor.InternalProcessor, layer testworkfl stage := stage.NewContainerStage(layer.NextRef(), container.CreateChild()) stage.SetCategory("Run in parallel") + // Allow to combine it within other containers + stage.SetPure(true) + // Inherit container defaults inherited := common.Ptr(stage.Container().ToContainerConfig()) inherited.VolumeMounts = nil @@ -99,7 +105,7 @@ func ProcessParallel(_ testworkflowprocessor.InternalProcessor, layer testworkfl stage.Container(). SetImage(constants.DefaultToolkitImage). SetImagePullPolicy(corev1.PullIfNotPresent). - SetCommand("/toolkit", "parallel"). + SetCommand(constants.DefaultToolkitPath, "parallel"). EnableToolkit(stage.Ref()). AppendVolumeMounts(layer.AddEmptyDirVolume(nil, constants.DefaultTransferDirPath)) @@ -124,10 +130,13 @@ func ProcessServicesStart(_ testworkflowprocessor.InternalProcessor, layer testw stage := stage.NewContainerStage(layer.NextRef(), container.CreateChild()) stage.SetCategory("Start services") + // Allow to combine it within other containers + stage.SetPure(true) + stage.Container(). SetImage(constants.DefaultToolkitImage). SetImagePullPolicy(corev1.PullIfNotPresent). - SetCommand("/toolkit", "services", "-g", "{{env.TK_SVC_REF}}"). + SetCommand(constants.DefaultToolkitPath, "services", "-g", "{{env.TK_SVC_REF}}"). EnableToolkit(stage.Ref()). AppendVolumeMounts(layer.AddEmptyDirVolume(nil, constants.DefaultTransferDirPath)) @@ -154,10 +163,13 @@ func ProcessServicesStop(_ testworkflowprocessor.InternalProcessor, layer testwo stage.SetOptional(true) stage.SetCategory("Stop services") + // Allow to combine it within other containers + stage.SetPure(true) + stage.Container(). SetImage(constants.DefaultToolkitImage). SetImagePullPolicy(corev1.PullIfNotPresent). - SetCommand("/toolkit", "kill", "{{env.TK_SVC_REF}}"). + SetCommand(constants.DefaultToolkitPath, "kill", "{{env.TK_SVC_REF}}"). EnableToolkit(stage.Ref()). AppendVolumeMounts(layer.AddEmptyDirVolume(nil, constants.DefaultTransferDirPath)) diff --git a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/analysis.go b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/analysis.go new file mode 100644 index 00000000000..97f527085c1 --- /dev/null +++ b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/analysis.go @@ -0,0 +1,104 @@ +package actiontypes + +import ( + "github.com/kubeshop/testkube/cmd/testworkflow-init/data" + "github.com/kubeshop/testkube/pkg/expressions" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite" +) + +// List + +func (a ActionList) GetLastRef() string { + for i := len(a) - 1; i >= 0; i-- { + switch a[i].Type() { + case lite.ActionTypeStart: + return *a[i].Start + case lite.ActionTypeSetup: + return data.InitStepName + } + } + return "" +} + +func (a ActionList) Refs() map[string]struct{} { + refs := make(map[string]struct{}) + for i := range a { + if a[i].Result != nil { + refs[a[i].Result.Ref] = struct{}{} + } else if a[i].Execute != nil { + refs[a[i].Execute.Ref] = struct{}{} + } else if a[i].End != nil { + refs[*a[i].End] = struct{}{} + } + } + return refs +} + +func (a ActionList) ExecutableRefs() map[string]struct{} { + refs := make(map[string]struct{}) + for i := range a { + if a[i].Execute != nil { + refs[a[i].Execute.Ref] = struct{}{} + } + } + return refs +} + +func (a ActionList) SkippedRefs() map[string]struct{} { + skipped := make(map[string]struct{}) + for i := range a { + if a[i].Declare != nil { + v, err := expressions.EvalExpressionPartial(a[i].Declare.Condition) + if err == nil && v.Static() != nil { + b, err := v.Static().BoolValue() + if err == nil && !b { + skipped[a[i].Declare.Ref] = struct{}{} + } + } + } + } + return skipped +} + +func (a ActionList) Results() (map[string]expressions.Expression, error) { + results := make(map[string]expressions.Expression) + for i := range a { + if a[i].Result != nil { + var err error + results[a[i].Result.Ref], err = expressions.EvalExpressionPartial(a[i].Result.Value) + if err != nil { + return results, err + } + } + } + return results, nil +} + +func (a ActionList) Conditions() (map[string]expressions.Expression, error) { + conditions := make(map[string]expressions.Expression) + for i := range a { + if a[i].Declare != nil { + var err error + conditions[a[i].Declare.Ref], err = expressions.EvalExpressionPartial(a[i].Declare.Condition) + if err != nil { + return conditions, err + } + } + } + return conditions, nil +} + +// Group + +func (a ActionGroups) GetLastRef() (ref string) { + for i := len(a) - 1; i >= 0; i-- { + + for j := len(a[i]) - 1; j >= 0; j-- { + ref = a[i].GetLastRef() + if ref != "" { + return + } + } + } + return +} diff --git a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/action.go b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/action.go index 04f763da5f2..ab77ec2a11d 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/action.go +++ b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/action.go @@ -20,6 +20,7 @@ type ActionExecute struct { Ref string `json:"r"` Negative bool `json:"n,omitempty"` Toolkit bool `json:"t,omitempty"` + Pure bool `json:"p,omitempty"` } type ActionPause struct { @@ -39,6 +40,7 @@ type ActionRetry struct { type ActionSetup struct { CopyInit bool `json:"i,omitempty"` + CopyToolkit bool `json:"t,omitempty"` CopyBinaries bool `json:"b,omitempty"` } diff --git a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/utils.go b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/utils.go index 0bfbb492f8e..ede5389b61e 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/utils.go +++ b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite/utils.go @@ -6,8 +6,8 @@ func NewLiteActionList() LiteActionList { return nil } -func (a LiteActionList) Setup(copyInit, copyBinaries bool) LiteActionList { - return append(a, LiteAction{Setup: &ActionSetup{CopyInit: copyInit, CopyBinaries: copyBinaries}}) +func (a LiteActionList) Setup(copyInit, copyToolkit, copyBinaries bool) LiteActionList { + return append(a, LiteAction{Setup: &ActionSetup{CopyInit: copyInit, CopyToolkit: copyToolkit, CopyBinaries: copyBinaries}}) } func (a LiteActionList) Declare(ref string, condition string, parents ...string) LiteActionList { diff --git a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/mutations.go b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/mutations.go new file mode 100644 index 00000000000..a4ee3c92573 --- /dev/null +++ b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/mutations.go @@ -0,0 +1,275 @@ +package actiontypes + +import ( + "fmt" + "maps" + "reflect" + "regexp" + "strings" + + "github.com/kubeshop/testkube/internal/common" + "github.com/kubeshop/testkube/pkg/expressions" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite" +) + +// List + +func (a ActionList) DeleteEmptyContainerMutations() ActionList { + for i := 0; i < len(a); i++ { + if a[i].Container != nil && reflect.ValueOf(a[i].Container.Config).IsZero() { + a = append(a[0:i], a[i+1:]...) + i-- + } + } + return a +} + +func (a ActionList) Skip(refs map[string]struct{}) ActionList { + skipped := make(map[string]struct{}) + maps.Copy(skipped, refs) + + // Skip children too + for i := range a { + if a[i].Declare != nil { + for j := range a[i].Declare.Parents { + if _, ok := skipped[a[i].Declare.Parents[j]]; ok { + skipped[a[i].Declare.Ref] = struct{}{} + } + } + if _, ok := skipped[a[i].Declare.Ref]; ok { + a[i].Declare.Condition = "false" + a[i].Declare.Parents = nil + } + } + } + + // Avoid executing skipped steps (Execute, Timeout, Retry, Result & End) + for i := 0; i < len(a); i++ { + if a[i].Execute != nil { + if _, ok := skipped[a[i].Execute.Ref]; ok { + a = append(a[:i], a[i+1:]...) + i-- + } + } + if a[i].Result != nil { + if _, ok := skipped[a[i].Result.Ref]; ok { + a = append(a[:i], a[i+1:]...) + i-- + } + } + if a[i].Timeout != nil { + if _, ok := skipped[a[i].Timeout.Ref]; ok { + a = append(a[:i], a[i+1:]...) + i-- + } + } + if a[i].Retry != nil { + if _, ok := skipped[a[i].Retry.Ref]; ok { + a = append(a[:i], a[i+1:]...) + i-- + } + } + if a[i].Pause != nil { + if _, ok := skipped[a[i].Pause.Ref]; ok { + a = append(a[:i], a[i+1:]...) + i-- + } + } + if a[i].Container != nil { + if _, ok := skipped[a[i].Container.Ref]; ok { + a = append(a[:i], a[i+1:]...) + i-- + } + } + } + + // Get rid of skipped steps from initial statuses and results + skipMachine := expressions.NewMachine(). + RegisterAccessor(func(name string) (interface{}, bool) { + if _, ok := skipped[name]; ok { + return true, true + } + return nil, false + }) + for i := range a { + if a[i].CurrentStatus != nil { + a[i].CurrentStatus = common.Ptr(simplifyExpression(*a[i].CurrentStatus, skipMachine)) + } + if a[i].Result != nil { + a[i].Result.Value = simplifyExpression(a[i].Result.Value, skipMachine) + } + } + + return a +} + +func (a ActionList) SimplifyIntermediateStatuses(currentStatus expressions.Expression) (ActionList, error) { + // Get all requirements + refs := a.Refs() + skipped := a.SkippedRefs() + results, err := a.Results() + if err != nil { + return nil, err + } + conditions, err := a.Conditions() + if err != nil { + return nil, err + } + + // Build current state + executed := make(map[string]struct{}) + machine := expressions.NewMachine().RegisterAccessor(func(name string) (interface{}, bool) { + if name == "never" { + return false, true + } else if name == "always" { + return true, true + } else if name == "passed" || name == "success" { + return currentStatus, true + } else if name == "failed" || name == "error" { + return expressions.MustCompile("!passed"), true + } else if _, ok := skipped[name]; ok { + return true, true + } else if v, ok := results[name]; ok { + // Ignore steps that didn't execute yet + if _, ok := executed[name]; !ok { + return true, true + } + + // Do not go deeper if the result is not determined yet + if v.Static() == nil { + return nil, false + } + c, ok2 := conditions[name] + if ok2 { + return expressions.MustCompile(fmt.Sprintf(`(%s) && (%s)`, c.String(), v.String())), true + } + return v, true + } else if _, ok := refs[name]; ok { + // Ignore steps that didn't execute yet + if _, ok := executed[name]; !ok { + return true, true + } + return nil, false + } + return nil, false + }) + + for i := range a { + // Update current status + if a[i].CurrentStatus != nil { + var err error + currentStatus, err = expressions.Compile(*a[i].CurrentStatus) + if err != nil { + return nil, err + } + } + + // Mark step as executed + if a[i].Execute != nil { + executed[a[i].Execute.Ref] = struct{}{} + } else if a[i].End != nil { + executed[*a[i].End] = struct{}{} + } + + // Simplify the condition + if a[i].Declare != nil { + a[i].Declare.Condition = simplifyExpression(a[i].Declare.Condition, machine) + conditions[a[i].Declare.Ref] = expressions.MustCompile(a[i].Declare.Condition) + for _, parentRef := range a[i].Declare.Parents { + if _, ok := skipped[parentRef]; ok { + a[i].Declare.Condition = "false" + break + } + } + } + } + + return a, nil +} + +func (a ActionList) CastRefStatusToBool() ActionList { + refs := a.Refs() + + // Wrap all the references with boolean function, and simplify values + refReplacements := make(map[string]string) + refResults := make(map[string]string) + wrapStartRef := expressions.NewMachine().RegisterAccessor(func(name string) (interface{}, bool) { + if _, ok := refs[name]; !ok { + return nil, false + } + if _, ok := refReplacements[name]; !ok { + refReplacements[name] = fmt.Sprintf("_WREF_%s_", name) + refResults[refReplacements[name]] = fmt.Sprintf("bool(%s)", name) + } + return expressions.MustCompile(refReplacements[name]), true + }) + wrapEndRef := expressions.NewMachine().RegisterAccessor(func(name string) (interface{}, bool) { + if result, ok := refResults[name]; ok { + return expressions.MustCompile(result), true + } + return nil, false + }) + for i := range a { + if a[i].CurrentStatus != nil { + a[i].CurrentStatus = common.Ptr(simplifyExpression(*a[i].CurrentStatus, wrapStartRef)) + a[i].CurrentStatus = common.Ptr(simplifyExpression(*a[i].CurrentStatus, wrapEndRef)) + } + if a[i].Declare != nil { + a[i].Declare.Condition = simplifyExpression(a[i].Declare.Condition, wrapStartRef) + a[i].Declare.Condition = simplifyExpression(a[i].Declare.Condition, wrapEndRef) + } + if a[i].Result != nil { + a[i].Result.Value = simplifyExpression(a[i].Result.Value, wrapStartRef) + a[i].Result.Value = simplifyExpression(a[i].Result.Value, wrapEndRef) + } + } + return a +} + +func (a ActionList) UncastRefStatusFromBool() ActionList { + refs := a.Refs() + + // Avoid unnecessary casting to boolean + uncastRegex := regexp.MustCompile(`bool\([^)]+\)`) + uncastBoolRefs := func(expr string) string { + return uncastRegex.ReplaceAllStringFunc(expr, func(s string) string { + ref := s[5 : len(s)-1] + if _, ok := refs[ref]; ok { + return ref + } + return s + }) + } + for i := range a { + if a[i].CurrentStatus != nil { + a[i].CurrentStatus = common.Ptr(uncastBoolRefs(*a[i].CurrentStatus)) + } + if a[i].Declare != nil { + a[i].Declare.Condition = uncastBoolRefs(a[i].Declare.Condition) + } + if a[i].Result != nil { + a[i].Result.Value = uncastBoolRefs(a[i].Result.Value) + } + } + return a +} + +func (a ActionList) RewireCommandDirectory(imageName string, src, dest string) ActionList { + for i := range a { + if a[i].Type() != lite.ActionTypeContainerTransition || a[i].Container.Config.Image != imageName { + continue + } + if a[i].Container.Config.Command != nil && len(*a[i].Container.Config.Command) > 0 && strings.HasPrefix((*a[i].Container.Config.Command)[0], src+"/") { + (*a[i].Container.Config.Command)[0] = dest + (*a[i].Container.Config.Command)[0][len(src):] + } + } + return a +} + +func simplifyExpression(expr string, machines ...expressions.Machine) string { + v, err := expressions.EvalExpressionPartial(expr, machines...) + if err == nil { + return v.String() + } + return expr +} diff --git a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/utils.go b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/utils.go index f46b8a054d1..84f7c163650 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/actiontypes/utils.go +++ b/pkg/testworkflows/testworkflowprocessor/action/actiontypes/utils.go @@ -6,7 +6,6 @@ import ( corev1 "k8s.io/api/core/v1" testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" - "github.com/kubeshop/testkube/cmd/testworkflow-init/data" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite" ) @@ -41,8 +40,8 @@ func NewActionList() ActionList { return nil } -func (a ActionList) Setup(copyInit, copyBinaries bool) ActionList { - return append(a, Action{Setup: &lite.ActionSetup{CopyInit: copyInit, CopyBinaries: copyBinaries}}) +func (a ActionList) Setup(copyInit, copyToolkit, copyBinaries bool) ActionList { + return append(a, Action{Setup: &lite.ActionSetup{CopyInit: copyInit, CopyToolkit: copyToolkit, CopyBinaries: copyBinaries}}) } func (a ActionList) Declare(ref string, condition string, parents ...string) ActionList { @@ -77,18 +76,6 @@ func (a ActionList) MutateContainer(ref string, config testworkflowsv1.Container return append(a, Action{Container: &ActionContainer{Ref: ref, Config: config}}) } -func (a ActionList) GetLastRef() string { - for i := len(a) - 1; i >= 0; i-- { - switch a[i].Type() { - case lite.ActionTypeStart: - return *a[i].Start - case lite.ActionTypeSetup: - return data.InitStepName - } - } - return "" -} - type ActionGroups []ActionList func (a ActionGroups) Append(fn func(list ActionList) ActionList) ActionGroups { @@ -98,16 +85,3 @@ func (a ActionGroups) Append(fn func(list ActionList) ActionList) ActionGroups { func NewActionGroups() ActionGroups { return nil } - -func (a ActionGroups) GetLastRef() (ref string) { - for i := len(a) - 1; i >= 0; i-- { - - for j := len(a[i]) - 1; j >= 0; j-- { - ref = a[i].GetLastRef() - if ref != "" { - return - } - } - } - return -} diff --git a/pkg/testworkflows/testworkflowprocessor/action/containerize.go b/pkg/testworkflows/testworkflowprocessor/action/containerize.go index 6f56eecd6a3..03f3d1a825d 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/containerize.go +++ b/pkg/testworkflows/testworkflowprocessor/action/containerize.go @@ -14,13 +14,14 @@ import ( stage2 "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/stage" ) -func CreateContainer(groupId int, defaultContainer stage2.Container, actions []actiontypes.Action) (cr corev1.Container, actionsCleanup []actiontypes.Action, err error) { +func CreateContainer(groupId int, defaultContainer stage2.Container, actions []actiontypes.Action, usesToolkit bool) (cr corev1.Container, actionsCleanup []actiontypes.Action, err error) { actions = slices.Clone(actions) actionsCleanup = actions // Find the container configurations and executable/setup steps var setup *actiontypes.Action executable := map[string]bool{} + toolkit := map[string]bool{} containerConfigs := make([]*actiontypes.Action, 0) for i := range actions { if actions[i].Container != nil { @@ -29,15 +30,21 @@ func CreateContainer(groupId int, defaultContainer stage2.Container, actions []a setup = &actions[i] } else if actions[i].Execute != nil { executable[actions[i].Execute.Ref] = true + if actions[i].Execute.Toolkit { + toolkit[actions[i].Execute.Ref] = true + } } } // Find the highest priority container configuration var bestContainerConfig *actiontypes.Action + var bestIsToolkit = false for i := range containerConfigs { if executable[containerConfigs[i].Container.Ref] { - bestContainerConfig = containerConfigs[i] - break + if bestContainerConfig == nil || bestIsToolkit { + bestContainerConfig = containerConfigs[i] + bestIsToolkit = toolkit[bestContainerConfig.Container.Ref] + } } } if bestContainerConfig == nil && len(containerConfigs) > 0 { @@ -47,9 +54,11 @@ func CreateContainer(groupId int, defaultContainer stage2.Container, actions []a bestContainerConfig = &actiontypes.Action{Container: &actiontypes.ActionContainer{Config: defaultContainer.ToContainerConfig()}} } - // Build the cr base - // TODO: Handle the case when there are multiple exclusive execution configurations - // TODO: Handle a case when that configuration should join multiple configurations (i.e. envs/volumeMounts) + // Build the CR base + cr, _ = defaultContainer.Detach().ToKubernetesTemplate() + cr.Image = "" + cr.Env = nil + cr.EnvFrom = nil if len(containerConfigs) > 0 { cr, err = stage2.NewContainer().ApplyCR(&bestContainerConfig.Container.Config).ToKubernetesTemplate() if err != nil { @@ -75,7 +84,19 @@ func CreateContainer(groupId int, defaultContainer stage2.Container, actions []a cr.EnvFrom = append(cr.EnvFrom, newEnvFrom) } } - // TODO: Combine the rest + + // Combine the volume mounts + for i := range containerConfigs { + loop: + for _, v := range containerConfigs[i].Container.Config.VolumeMounts { + for j := range cr.VolumeMounts { + if cr.VolumeMounts[j].MountPath == v.MountPath { + continue loop + } + } + cr.VolumeMounts = append(cr.VolumeMounts, v) + } + } } // Set up a default image when not specified @@ -86,6 +107,11 @@ func CreateContainer(groupId int, defaultContainer stage2.Container, actions []a cr.ImagePullPolicy = corev1.PullIfNotPresent } + // Use the Toolkit image instead of Init if it's anyway used + if usesToolkit && cr.Image == constants.DefaultInitImage { + cr.Image = constants.DefaultToolkitImage + } + // Provide the data required for setup step if setup != nil { cr.Env = append(cr.Env, @@ -104,20 +130,11 @@ func CreateContainer(groupId int, defaultContainer stage2.Container, actions []a corev1.EnvVar{Name: fmt.Sprintf("_%s_%s", constants2.EnvGroupActions, constants2.EnvActions), ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{FieldPath: constants.SpecAnnotationFieldPath}, }}) - - // Apply basic mounts, so there is a state provided - for _, volumeMount := range defaultContainer.VolumeMounts() { - if !slices.ContainsFunc(cr.VolumeMounts, func(mount corev1.VolumeMount) bool { - return mount.Name == volumeMount.Name - }) { - cr.VolumeMounts = append(cr.VolumeMounts, volumeMount) - } - } } // Avoid using /.tktw/init if there is Init Process Image - use /init then initPath := constants.DefaultInitPath - if cr.Image == constants.DefaultInitImage { + if cr.Image == constants.DefaultInitImage || cr.Image == constants.DefaultToolkitImage { initPath = "/init" } @@ -128,14 +145,12 @@ func CreateContainer(groupId int, defaultContainer stage2.Container, actions []a // Clean up the executions for i := range containerConfigs { - // TODO: Clean it up newConfig := testworkflowsv1.ContainerConfig{} if executable[containerConfigs[i].Container.Ref] { newConfig.Command = containerConfigs[i].Container.Config.Command newConfig.Args = containerConfigs[i].Container.Config.Args } newConfig.WorkingDir = containerConfigs[i].Container.Config.WorkingDir - // TODO: expose more? containerConfigs[i].Container = &actiontypes.ActionContainer{ Ref: containerConfigs[i].Container.Ref, diff --git a/pkg/testworkflows/testworkflowprocessor/action/finalize.go b/pkg/testworkflows/testworkflowprocessor/action/finalize.go new file mode 100644 index 00000000000..f052ce15f4e --- /dev/null +++ b/pkg/testworkflows/testworkflowprocessor/action/finalize.go @@ -0,0 +1,110 @@ +package action + +import ( + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" +) + +func Finalize(groups actiontypes.ActionGroups) actiontypes.ActionGroups { + if len(groups) == 0 { + return actiontypes.ActionGroups{{{Setup: &lite.ActionSetup{ + CopyInit: false, + CopyToolkit: false, + CopyBinaries: false, + }}}} + } + + // Determine if the Init Process should be copied + copyInit := false + copyBinaries := false + for i := range groups { + for j := range groups[i] { + if groups[i][j].Type() == lite.ActionTypeContainerTransition { + if groups[i][j].Container.Config.Image != constants.DefaultInitImage && groups[i][j].Container.Config.Image != constants.DefaultToolkitImage { + copyInit = true + copyBinaries = true + } + } + } + } + + // Determine if the Toolkit should be copied + copyToolkit := false + for i := range groups { + hadToolkit := false + hadOther := false + for j := range groups[i] { + if groups[i][j].Type() == lite.ActionTypeContainerTransition { + if groups[i][j].Container.Config.Image == constants.DefaultToolkitImage { + hadToolkit = true + } else if groups[i][j].Container.Config.Image != constants.DefaultInitImage { + hadOther = true + } + } + } + if hadToolkit && hadOther { + copyToolkit = true + } + } + + // Determine if the setup step can be combined with the further group + canMergeSetup := true + maybeCopyToolkit := false + for i := range groups[0] { + // Ignore non-transition actions + if groups[0][i].Type() != lite.ActionTypeContainerTransition { + continue + } + + // Disallow merging setup step for custom images + if groups[0][i].Container.Config.Image != constants.DefaultInitImage && groups[0][i].Container.Config.Image != constants.DefaultToolkitImage { + canMergeSetup = false + break + } + + // Allow merging setup step with toolkit image + if groups[0][i].Container.Config.Image == constants.DefaultToolkitImage { + maybeCopyToolkit = true + } + } + if maybeCopyToolkit && canMergeSetup { + copyToolkit = true + } + + // Avoid using /.tktw/toolkit when the toolkit is not copied + if !copyToolkit { + for i := range groups { + for j := range groups[i] { + if groups[i][j].Type() != lite.ActionTypeContainerTransition || groups[i][j].Container.Config.Image != constants.DefaultToolkitImage { + continue + } + if groups[i][j].Container.Config.Command == nil || len(*groups[i][j].Container.Config.Command) == 0 { + continue + } + if (*groups[i][j].Container.Config.Command)[0] == constants.DefaultToolkitPath { + (*groups[i][j].Container.Config.Command)[0] = "/toolkit" + } + } + } + } + + // Build the setup action + setup := actiontypes.ActionList{{Setup: &lite.ActionSetup{ + CopyInit: copyInit, + CopyToolkit: copyToolkit, + CopyBinaries: copyBinaries, + }}} + + // Inject into the first group if possible + if canMergeSetup { + return append(actiontypes.ActionGroups{append(setup, groups[0]...)}, groups[1:]...) + } + + // Move non-executable steps from the 2nd group into setup + for groups[0][0].Type() != lite.ActionTypeContainerTransition { + setup = append(setup, groups[0][0]) + groups[0] = groups[0][1:] + } + return append(actiontypes.ActionGroups{setup}, groups...) +} diff --git a/pkg/testworkflows/testworkflowprocessor/action/group.go b/pkg/testworkflows/testworkflowprocessor/action/group.go index 955e9ee1f54..908ce93d595 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/group.go +++ b/pkg/testworkflows/testworkflowprocessor/action/group.go @@ -1,12 +1,68 @@ package action import ( + "bytes" + "encoding/json" "slices" + testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" ) -func Group(actions []actiontypes.Action) (groups [][]actiontypes.Action) { +// TODO: Optimize +func isCompatibleContainerConfig(c1, c2 *testworkflowsv1.ContainerConfig) bool { + // Clean the safe parts of the container configs + c1 = c1.DeepCopy() + c2 = c2.DeepCopy() + c1.Env = nil + c1.EnvFrom = nil + c1.WorkingDir = nil + c1.Command = nil + c1.Args = nil + c2.Env = nil + c2.EnvFrom = nil + c2.WorkingDir = nil + c2.Command = nil + c2.Args = nil + + // Verify if the volume mounts are compatible + for i1 := range c1.VolumeMounts { + for i2 := range c2.VolumeMounts { + if c1.VolumeMounts[i1].MountPath != c2.VolumeMounts[i2].MountPath { + continue + } + if c1.VolumeMounts[i1].Name != c2.VolumeMounts[i2].Name || c1.VolumeMounts[i1].SubPath != c2.VolumeMounts[i2].SubPath || c1.VolumeMounts[i1].SubPathExpr != c2.VolumeMounts[i2].SubPathExpr { + return false + } + } + } + c1.VolumeMounts = nil + c2.VolumeMounts = nil + + // Convert to bytes and compare (ignores order) + v1, err1 := json.Marshal(c1) + v2, err2 := json.Marshal(c2) + return err1 == nil && err2 == nil && bytes.Equal(v1, v2) +} + +func getContainerConfigs(actions actiontypes.ActionList) (configs []testworkflowsv1.ContainerConfig, pure bool) { + pure = true + for i := range actions { + switch actions[i].Type() { + case lite.ActionTypeContainerTransition: + configs = append(configs, actions[i].Container.Config) + case lite.ActionTypeExecute: + if !actions[i].Execute.Pure { + pure = false + } + } + } + return +} + +func Group(actions actiontypes.ActionList) (groups actiontypes.ActionGroups) { // Detect "start" and "execute" instructions startIndexes := make([]int, 0) startInstructions := make(map[string]int) @@ -33,13 +89,13 @@ func Group(actions []actiontypes.Action) (groups [][]actiontypes.Action) { // Fast-track when there is only a single instruction to execute if len(executeIndexes) <= 1 { - return [][]actiontypes.Action{actions} + return actiontypes.ActionGroups{actions} } // Basic behavior: split based on each execute instruction for _, executeIndex := range executeIndexes { if actions[executeIndex].Setup != nil { - groups = append([][]actiontypes.Action{actions[executeIndex:]}, groups...) + groups = append(actiontypes.ActionGroups{actions[executeIndex:]}, groups...) actions = actions[:executeIndex] continue } @@ -49,18 +105,64 @@ func Group(actions []actiontypes.Action) (groups [][]actiontypes.Action) { startIndex = containerIndex } - // TODO: Combine multiple operations in a single container if it's possible - - groups = append([][]actiontypes.Action{actions[startIndex:]}, groups...) + groups = append(actiontypes.ActionGroups{actions[startIndex:]}, groups...) actions = actions[:startIndex] } if len(actions) > 0 { groups[0] = append(actions, groups[0]...) } - // TODO: Behavior: allow selected Toolkit actions to be executed in the same container - // TODO: Behavior: split based on the image used (use all mounts and variables altogether) - // TODO: Behavior: split based on the image used (isolate variables) + // Combine multiple operations in a single container if it's possible +merging: + for i := len(groups) - 2; i >= 0; i-- { + // Ignore case when there is last group available + // TODO: it shouldn't be needed, but it is + if i+1 >= len(groups) { + continue + } + + // Analyze consecutive groups + g1, p1 := getContainerConfigs(groups[i]) + g2, p2 := getContainerConfigs(groups[i+1]) + + // The groups are not pure + if !p1 && !p2 { + continue merging + } + + // One of the groups is not executing anything + if len(g1) == 0 || len(g2) == 0 { + groups[i] = append(groups[i], groups[i+1]...) + groups = append(groups[:i], groups[i+1:]...) + i++ + continue merging + } + + // The containers are compatible + for i1 := range g1 { + for i2 := range g2 { + // The pure init or toolkit container is used, so it can be copied + if (g1[i1].Image == constants.DefaultToolkitImage || g1[i1].Image == constants.DefaultInitImage) && p1 { + continue + } + if (g2[i2].Image == constants.DefaultToolkitImage || g2[i2].Image == constants.DefaultInitImage) && p2 { + continue + } + + // We are able to combine the containers + if isCompatibleContainerConfig(&g1[i1], &g2[i2]) { + continue + } + + // The groups cannot be merged together + continue merging + } + } + + groups[i+1] = append(groups[i], groups[i+1]...) + groups = append(groups[:i], groups[i+1:]...) + i++ + } return groups } diff --git a/pkg/testworkflows/testworkflowprocessor/action/group_test.go b/pkg/testworkflows/testworkflowprocessor/action/group_test.go index 464ec6a2291..52087c409fa 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/group_test.go +++ b/pkg/testworkflows/testworkflowprocessor/action/group_test.go @@ -13,7 +13,6 @@ import ( func TestGroup_Basic(t *testing.T) { input := actiontypes.NewActionList(). // Configure - Setup(true, true). Declare("init", "true"). Declare("step1", "false"). Declare("step2", "true", "init"). @@ -49,11 +48,12 @@ func TestGroup_Basic(t *testing.T) { End("init"). End("") - want := [][]actiontypes.Action{ - input[:13], // ends before containerConfig("step2") - input[13:18], // ends before containerConfig("step3") - input[18:], + setup := actiontypes.NewActionList().Setup(true, false, true) + want := actiontypes.ActionGroups{ + append(setup, input[:12]...), // ends before containerConfig("step2") + input[12:17], // ends before containerConfig("step3") + input[17:], } - got := Group(input) + got := Finalize(Group(input)) assert.Equal(t, want, got) } diff --git a/pkg/testworkflows/testworkflowprocessor/action/optimize.go b/pkg/testworkflows/testworkflowprocessor/action/optimize.go index d7b24fc3c5d..108d113ff45 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/optimize.go +++ b/pkg/testworkflows/testworkflowprocessor/action/optimize.go @@ -1,324 +1,36 @@ package action import ( - "fmt" - "reflect" - "regexp" - "strings" - - "k8s.io/apimachinery/pkg/util/rand" - - "github.com/kubeshop/testkube/internal/common" "github.com/kubeshop/testkube/pkg/expressions" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes" - "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" ) -func optimize(actions []actiontypes.Action) ([]actiontypes.Action, error) { - // Detect all the step references - refs := make(map[string]struct{}) - executableRefs := make(map[string]struct{}) - for i := range actions { - if actions[i].Result != nil { - refs[actions[i].Result.Ref] = struct{}{} - } - - if actions[i].Execute != nil { - refs[actions[i].Execute.Ref] = struct{}{} - executableRefs[actions[i].Execute.Ref] = struct{}{} - } - if actions[i].End != nil { - refs[*actions[i].End] = struct{}{} - executableRefs[*actions[i].End] = struct{}{} - } - } - +func optimize(actions actiontypes.ActionList) (actiontypes.ActionList, error) { // Delete empty `container` declarations - for i := 0; i < len(actions); i++ { - if actions[i].Container != nil && reflect.ValueOf(actions[i].Container.Config).IsZero() { - actions = append(actions[0:i], actions[i+1:]...) - i-- - } - } + actions = actions.DeleteEmptyContainerMutations() // Wrap all the references with boolean function, and simplify values - refReplacements := make(map[string]string) - refResults := make(map[string]string) - wrapStartRef := expressions.NewMachine().RegisterAccessor(func(name string) (interface{}, bool) { - if _, ok := executableRefs[name]; !ok { - return nil, false - } - if _, ok := refReplacements[name]; !ok { - hashStart := rand.String(10) - hashEnd := rand.String(10) - refReplacements[name] = fmt.Sprintf("_%s_%s_%s_", hashStart, name, hashEnd) - refResults[refReplacements[name]] = fmt.Sprintf("bool(%s)", name) - } - return expressions.MustCompile(refReplacements[name]), true - }) - wrapEndRef := expressions.NewMachine().RegisterAccessor(func(name string) (interface{}, bool) { - if result, ok := refResults[name]; ok { - return expressions.MustCompile(result), true - } - return nil, false - }) - for i := range actions { - if actions[i].CurrentStatus != nil { - actions[i].CurrentStatus = common.Ptr(simplifyExpression(*actions[i].CurrentStatus, wrapStartRef)) - actions[i].CurrentStatus = common.Ptr(simplifyExpression(*actions[i].CurrentStatus, wrapEndRef)) - } - if actions[i].Declare != nil { - actions[i].Declare.Condition = simplifyExpression(actions[i].Declare.Condition, wrapStartRef) - actions[i].Declare.Condition = simplifyExpression(actions[i].Declare.Condition, wrapEndRef) - } - if actions[i].Result != nil { - actions[i].Result.Value = simplifyExpression(actions[i].Result.Value, wrapStartRef) - actions[i].Result.Value = simplifyExpression(actions[i].Result.Value, wrapEndRef) - } - } - - // Detect immediately skipped steps - skipped := make(map[string]struct{}) - for i := range actions { - if actions[i].Declare != nil { - v, err := expressions.EvalExpressionPartial(actions[i].Declare.Condition) - if err == nil && v.Static() != nil { - b, err := v.Static().BoolValue() - if err == nil && !b { - skipped[actions[i].Declare.Ref] = struct{}{} - } - } - } - } - - // List all the results - results := make(map[string]expressions.Expression) - conditions := make(map[string]expressions.Expression) - for i := range actions { - if actions[i].Result != nil { - var err error - refs[actions[i].Result.Ref] = struct{}{} - results[actions[i].Result.Ref], err = expressions.EvalExpressionPartial(actions[i].Result.Value) - if err != nil { - return nil, err - } - } - - if actions[i].Declare != nil { - var err error - conditions[actions[i].Declare.Ref], err = expressions.EvalExpressionPartial(actions[i].Declare.Condition) - if err != nil { - return nil, err - } - } - - if actions[i].Execute != nil { - refs[actions[i].Execute.Ref] = struct{}{} - } - } + actions = actions.CastRefStatusToBool() // Pre-resolve conditions - currentStatus := expressions.MustCompile("true") - executed := make(map[string]struct{}) - for i := range actions { - // Update current status - if actions[i].CurrentStatus != nil { - var err error - currentStatus, err = expressions.Compile(*actions[i].CurrentStatus) - if err != nil { - return nil, err - } - } - - // Mark step as executed - if actions[i].Execute != nil { - executed[actions[i].Execute.Ref] = struct{}{} - } else if actions[i].End != nil { - executed[*actions[i].End] = struct{}{} - } - - // Simplify the condition - if actions[i].Declare != nil { - // TODO: Handle `never` and other aliases - machine := expressions.NewMachine().RegisterAccessor(func(name string) (interface{}, bool) { - if name == "passed" || name == "success" { - return currentStatus, true - } else if name == "failed" || name == "error" { - return expressions.MustCompile("!passed"), true - } else if _, ok := skipped[name]; ok { - return true, true - } else if v, ok := results[name]; ok { - // Ignore steps that didn't execute yet - if _, ok := executed[name]; !ok { - return true, true - } - // Do not go deeper if the result is not determined yet - if v.Static() == nil { - return nil, false - } - c, ok2 := conditions[name] - if ok2 { - return expressions.MustCompile(fmt.Sprintf(`(%s) && (%s)`, c.String(), v.String())), true - } - return v, true - } else if _, ok := refs[name]; ok { - // Ignore steps that didn't execute yet - if _, ok := executed[name]; !ok { - return true, true - } - return nil, false - } - return nil, false - }) - actions[i].Declare.Condition = simplifyExpression(actions[i].Declare.Condition, machine) - conditions[actions[i].Declare.Ref] = expressions.MustCompile(actions[i].Declare.Condition) - for _, parentRef := range actions[i].Declare.Parents { - if _, ok := skipped[parentRef]; ok { - actions[i].Declare.Condition = "false" - break - } - } - } + actions, err := actions.SimplifyIntermediateStatuses(expressions.MustCompile("true")) + if err != nil { + return nil, err } // Avoid unnecessary casting to boolean - uncastRegex := regexp.MustCompile(`bool\([^)]+\)`) - uncastBoolRefs := func(expr string) string { - return uncastRegex.ReplaceAllStringFunc(expr, func(s string) string { - ref := s[5 : len(s)-1] - if _, ok := refs[ref]; ok { - return ref - } - return s - }) - } - for i := range actions { - if actions[i].CurrentStatus != nil { - actions[i].CurrentStatus = common.Ptr(uncastBoolRefs(*actions[i].CurrentStatus)) - } - if actions[i].Declare != nil { - actions[i].Declare.Condition = uncastBoolRefs(actions[i].Declare.Condition) - } - if actions[i].Result != nil { - actions[i].Result.Value = uncastBoolRefs(actions[i].Result.Value) - } - } + actions = actions.UncastRefStatusFromBool() // Detect immediately skipped steps - skipped = make(map[string]struct{}) - for i := range actions { - if actions[i].Declare != nil { - v, err := expressions.EvalExpressionPartial(actions[i].Declare.Condition) - if err == nil && v.Static() != nil { - b, err := v.Static().BoolValue() - if err == nil && !b { - skipped[actions[i].Declare.Ref] = struct{}{} - } - } - } - } + skipped := actions.SkippedRefs() // Avoid executing skipped steps (Execute, Timeout, Retry, Result & End) - for i := 0; i < len(actions); i++ { - if actions[i].Execute != nil { - if _, ok := skipped[actions[i].Execute.Ref]; ok { - actions = append(actions[:i], actions[i+1:]...) - i-- - } - } - if actions[i].Result != nil { - if _, ok := skipped[actions[i].Result.Ref]; ok { - actions = append(actions[:i], actions[i+1:]...) - i-- - } - } - if actions[i].Timeout != nil { - if _, ok := skipped[actions[i].Timeout.Ref]; ok { - actions = append(actions[:i], actions[i+1:]...) - i-- - } - } - if actions[i].Retry != nil { - if _, ok := skipped[actions[i].Retry.Ref]; ok { - actions = append(actions[:i], actions[i+1:]...) - i-- - } - } - if actions[i].Pause != nil { - if _, ok := skipped[actions[i].Pause.Ref]; ok { - actions = append(actions[:i], actions[i+1:]...) - i-- - } - } - if actions[i].Container != nil { - if _, ok := skipped[actions[i].Container.Ref]; ok { - actions = append(actions[:i], actions[i+1:]...) - i-- - } - } - } - - // Ignore parents for already statically skipped conditions - for i := range actions { - if actions[i].Declare != nil { - if _, ok := skipped[actions[i].Declare.Ref]; ok { - actions[i].Declare.Parents = nil - } - } - } + actions = actions.Skip(skipped) - // TODO: Avoid using /.tktw/toolkit if there is Toolkit image - - // Avoid using /.tktw/bin/sh when it is internal image used, with binaries in /bin - for i := range actions { - if actions[i].Type() != lite.ActionTypeContainerTransition { - continue - } - if actions[i].Container.Config.Image != constants.DefaultInitImage && actions[i].Container.Config.Image != constants.DefaultToolkitImage { - continue - } - if actions[i].Container.Config.Command != nil && len(*actions[i].Container.Config.Command) > 0 && strings.HasPrefix((*actions[i].Container.Config.Command)[0], constants.InternalBinPath+"/") { - (*actions[i].Container.Config.Command)[0] = "/bin" + (*actions[i].Container.Config.Command)[0][len(constants.InternalBinPath):] - } - } - - // Avoid copying init process and common binaries, when it is not necessary - copyInit := false - copyBinaries := false - for i := range actions { - if actions[i].Type() == lite.ActionTypeContainerTransition { - if actions[i].Container.Config.Image != constants.DefaultInitImage { - copyInit = true - if actions[i].Container.Config.Image != constants.DefaultToolkitImage { - copyBinaries = true - } - } - } - } - for i := range actions { - if actions[i].Type() == lite.ActionTypeSetup { - actions[i].Setup.CopyInit = copyInit - actions[i].Setup.CopyBinaries = copyBinaries - } - } - - // Get rid of skipped steps from initial statuses and results - skipMachine := expressions.NewMachine(). - RegisterAccessor(func(name string) (interface{}, bool) { - if _, ok := skipped[name]; ok { - return true, true - } - return nil, false - }) - for i := range actions { - if actions[i].CurrentStatus != nil { - actions[i].CurrentStatus = common.Ptr(simplifyExpression(*actions[i].CurrentStatus, skipMachine)) - } - if actions[i].Result != nil { - actions[i].Result.Value = simplifyExpression(actions[i].Result.Value, skipMachine) - } - } + // Avoid using /.tktw/bin/sh when it is internal image used, with direct binaries + actions = actions.RewireCommandDirectory(constants.DefaultInitImage, constants.InternalBinPath, "/.tktw-bin") + actions = actions.RewireCommandDirectory(constants.DefaultToolkitImage, constants.InternalBinPath, "/.tktw-bin") return actions, nil } diff --git a/pkg/testworkflows/testworkflowprocessor/action/process.go b/pkg/testworkflows/testworkflowprocessor/action/process.go index 521b3c6a8b1..df7cf2d39c3 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/process.go +++ b/pkg/testworkflows/testworkflowprocessor/action/process.go @@ -13,7 +13,7 @@ import ( stage2 "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/stage" ) -func process(currentStatus string, parents []string, stage stage2.Stage, machines ...expressions.Machine) (actions []actiontypes.Action, err error) { +func process(currentStatus string, parents []string, stage stage2.Stage, machines ...expressions.Machine) (actions actiontypes.ActionList, err error) { // Store the init status actions = append(actions, actiontypes.Action{ CurrentStatus: common.Ptr(currentStatus), @@ -79,6 +79,7 @@ func process(currentStatus string, parents []string, stage stage2.Stage, machine Ref: exec.Ref(), Negative: exec.Negative(), Toolkit: exec.IsToolkit(), + Pure: exec.Pure(), }, }) } @@ -132,21 +133,27 @@ func process(currentStatus string, parents []string, stage stage2.Stage, machine return } -func Process(root stage2.Stage, machines ...expressions.Machine) ([]actiontypes.Action, error) { +func Process(root stage2.Stage, machines ...expressions.Machine) (actiontypes.ActionList, error) { actions, err := process("true", nil, root, machines...) if err != nil { return nil, err } - actions = append([]actiontypes.Action{{Setup: &lite.ActionSetup{CopyInit: true, CopyBinaries: true}}, {Start: common.Ptr("")}}, actions...) + actions = append([]actiontypes.Action{{Start: common.Ptr("")}}, actions...) actions = append(actions, actiontypes.Action{Result: &lite.ActionResult{Ref: "", Value: root.Ref()}}, actiontypes.Action{End: common.Ptr("")}) // Optimize until simplest list of operations for { prevLength := len(actions) actions, err = optimize(actions) - if err != nil || len(actions) == prevLength { - sort(actions) - return actions, errors.Wrap(err, "processing operations") + + // Continue until final optimizations are applied + if err == nil && len(actions) != prevLength { + continue } + + // Sort for easier reading + sort(actions) + + return actions, errors.Wrap(err, "processing operations") } } diff --git a/pkg/testworkflows/testworkflowprocessor/action/process_test.go b/pkg/testworkflows/testworkflowprocessor/action/process_test.go index 38ccf374531..992a8a881ee 100644 --- a/pkg/testworkflows/testworkflowprocessor/action/process_test.go +++ b/pkg/testworkflows/testworkflowprocessor/action/process_test.go @@ -19,9 +19,6 @@ func TestProcess_BasicSteps(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "true", "init"). @@ -63,7 +60,7 @@ func TestProcess_BasicSteps(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_Grouping(t *testing.T) { @@ -78,9 +75,6 @@ func TestProcess_Grouping(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "true", "init"). @@ -153,7 +147,7 @@ func TestProcess_Grouping(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_Pause(t *testing.T) { @@ -166,9 +160,6 @@ func TestProcess_Pause(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "true", "init"). @@ -213,7 +204,7 @@ func TestProcess_Pause(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_NegativeStep(t *testing.T) { @@ -226,9 +217,6 @@ func TestProcess_NegativeStep(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "true", "init"). @@ -270,7 +258,7 @@ func TestProcess_NegativeStep(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_NegativeGroup(t *testing.T) { @@ -282,9 +270,6 @@ func TestProcess_NegativeGroup(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "true", "init"). @@ -326,7 +311,7 @@ func TestProcess_NegativeGroup(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_OptionalStep(t *testing.T) { @@ -339,9 +324,6 @@ func TestProcess_OptionalStep(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "true", "init"). @@ -383,7 +365,7 @@ func TestProcess_OptionalStep(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_OptionalGroup(t *testing.T) { @@ -397,9 +379,6 @@ func TestProcess_OptionalGroup(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("inner", "true", "init"). @@ -446,7 +425,7 @@ func TestProcess_OptionalGroup(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_IgnoreExecutionOfStaticSkip(t *testing.T) { @@ -459,9 +438,6 @@ func TestProcess_IgnoreExecutionOfStaticSkip(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "false"). @@ -498,7 +474,7 @@ func TestProcess_IgnoreExecutionOfStaticSkip(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_IgnoreExecutionOfStaticSkipGroup(t *testing.T) { @@ -510,9 +486,6 @@ func TestProcess_IgnoreExecutionOfStaticSkipGroup(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(false, false). // don't copy as there is nothing to do - // Declare stage conditions Declare("init", "false"). Declare("step1", "false"). @@ -543,7 +516,7 @@ func TestProcess_IgnoreExecutionOfStaticSkipGroup(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_IgnoreExecutionOfStaticSkipGroup_Pause(t *testing.T) { @@ -556,9 +529,6 @@ func TestProcess_IgnoreExecutionOfStaticSkipGroup_Pause(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(false, false). // don't copy as there is nothing to do - // Declare stage conditions Declare("init", "false"). Declare("step1", "false"). @@ -591,7 +561,7 @@ func TestProcess_IgnoreExecutionOfStaticSkipGroup_Pause(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } func TestProcess_IgnoreExecutionOfStaticSkip_PauseGroup(t *testing.T) { @@ -605,9 +575,6 @@ func TestProcess_IgnoreExecutionOfStaticSkip_PauseGroup(t *testing.T) { // Build the expectations want := actiontypes.NewActionList(). - // Configure - Setup(true, true). - // Declare stage conditions Declare("init", "true"). Declare("step1", "false"). @@ -647,5 +614,5 @@ func TestProcess_IgnoreExecutionOfStaticSkip_PauseGroup(t *testing.T) { // Assert got, err := Process(root) assert.NoError(t, err) - assert.Equal(t, want, actiontypes.ActionList(got)) + assert.Equal(t, want, got) } diff --git a/pkg/testworkflows/testworkflowprocessor/action/utils.go b/pkg/testworkflows/testworkflowprocessor/action/utils.go deleted file mode 100644 index 6532947eee6..00000000000 --- a/pkg/testworkflows/testworkflowprocessor/action/utils.go +++ /dev/null @@ -1,11 +0,0 @@ -package action - -import "github.com/kubeshop/testkube/pkg/expressions" - -func simplifyExpression(expr string, machines ...expressions.Machine) string { - v, err := expressions.EvalExpressionPartial(expr, machines...) - if err == nil { - return v.String() - } - return expr -} diff --git a/pkg/testworkflows/testworkflowprocessor/constants/constants.go b/pkg/testworkflows/testworkflowprocessor/constants/constants.go index a0fc38c86cd..dd0f95dd31a 100644 --- a/pkg/testworkflows/testworkflowprocessor/constants/constants.go +++ b/pkg/testworkflows/testworkflowprocessor/constants/constants.go @@ -32,7 +32,7 @@ var ( InternalBinPath = filepath.Join(DefaultInternalPath, "bin") DefaultShellPath = filepath.Join(InternalBinPath, "sh") DefaultInitPath = filepath.Join(DefaultInternalPath, "init") - DefaultStatePath = filepath.Join(DefaultInternalPath, "state") + DefaultToolkitPath = filepath.Join(DefaultInternalPath, "toolkit") DefaultTransferDirPath = filepath.Join(DefaultInternalPath, "transfer") DefaultTmpDirPath = filepath.Join(DefaultInternalPath, "tmp") DefaultTransferPort = 60433 diff --git a/pkg/testworkflows/testworkflowprocessor/operations.go b/pkg/testworkflows/testworkflowprocessor/operations.go index a140f2db986..3c1769dd406 100644 --- a/pkg/testworkflows/testworkflowprocessor/operations.go +++ b/pkg/testworkflows/testworkflowprocessor/operations.go @@ -26,6 +26,10 @@ func ProcessDelay(_ InternalProcessor, layer Intermediate, container stage.Conta SetArgs(fmt.Sprintf("%g", t.Seconds())) stage := stage.NewContainerStage(layer.NextRef(), shell) stage.SetCategory(fmt.Sprintf("Delay: %s", step.Delay)) + + // Allow to combine it within other containers + stage.SetPure(true) + return stage, nil } @@ -241,10 +245,13 @@ func ProcessContentTarball(_ InternalProcessor, layer Intermediate, container st stage.SetRetryPolicy(step.Retry) stage.SetCategory("Fetch tarball") + // Allow to combine it within other containers + stage.SetPure(true) + selfContainer. SetImage(constants.DefaultToolkitImage). SetImagePullPolicy(corev1.PullIfNotPresent). - SetCommand("/toolkit", "tarball"). + SetCommand(constants.DefaultToolkitPath, "tarball"). EnableToolkit(stage.Ref()) // Build volume pair and share with all siblings @@ -286,10 +293,13 @@ func ProcessArtifacts(_ InternalProcessor, layer Intermediate, container stage.C stage.SetCondition("always") stage.SetCategory("Upload artifacts") + // Allow to combine it within other containers + stage.SetPure(true) + selfContainer. SetImage(constants.DefaultToolkitImage). SetImagePullPolicy(corev1.PullIfNotPresent). - SetCommand("/toolkit", "artifacts", "-m", constants.DefaultDataPath). + SetCommand(constants.DefaultToolkitPath, "artifacts", "-m", constants.DefaultDataPath). EnableToolkit(stage.Ref()) args := make([]string, 0) diff --git a/pkg/testworkflows/testworkflowprocessor/presets/processor_test.go b/pkg/testworkflows/testworkflowprocessor/presets/processor_test.go index 7a09ffef685..c12012d1cd6 100644 --- a/pkg/testworkflows/testworkflowprocessor/presets/processor_test.go +++ b/pkg/testworkflows/testworkflowprocessor/presets/processor_test.go @@ -120,12 +120,12 @@ func TestProcessBasic(t *testing.T) { sigSerialized, _ := json.Marshal(sig) volumes := res.Job.Spec.Template.Spec.Volumes - volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts + volumeMounts := res.Job.Spec.Template.Spec.Containers[0].VolumeMounts wantActions := actiontypes.NewActionGroups(). Append(func(list actiontypes.ActionList) actiontypes.ActionList { return list. - Setup(false, false). + Setup(false, false, false). Declare(constants.RootOperationName, "true"). Declare(sig[0].Ref(), "true", constants.RootOperationName). Result(constants.RootOperationName, sig[0].Ref()). @@ -133,12 +133,11 @@ func TestProcessBasic(t *testing.T) { Start(""). CurrentStatus("true"). Start(constants.RootOperationName). - CurrentStatus(constants.RootOperationName) - }). - Append(func(list actiontypes.ActionList) actiontypes.ActionList { - return list. + CurrentStatus(constants.RootOperationName). + + // Joined as default image is used MutateContainer(sig[0].Ref(), testworkflowsv1.ContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test"), }). Start(sig[0].Ref()). @@ -176,13 +175,15 @@ func TestProcessBasic(t *testing.T) { RestartPolicy: corev1.RestartPolicyNever, EnableServiceLinks: common.Ptr(false), Volumes: volumes, - InitContainers: []corev1.Container{ + InitContainers: []corev1.Container{}, + Containers: []corev1.Container{ { Name: "1", Image: constants.DefaultInitImage, ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"/init", "0"}, Env: []corev1.EnvVar{ + env(0, false, "CI", "1"), envDebugNode, envDebugPod, envDebugNamespace, @@ -195,21 +196,6 @@ func TestProcessBasic(t *testing.T) { }, }, }, - Containers: []corev1.Container{ - { - Name: "2", - Image: constants.DefaultInitImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "1"}, - Env: []corev1.EnvVar{ - env(0, false, "CI", "1"), - }, - VolumeMounts: volumeMounts, - SecurityContext: &corev1.SecurityContext{ - RunAsGroup: common.Ptr(constants.DefaultFsGroup), - }, - }, - }, SecurityContext: &corev1.PodSecurityContext{ FSGroup: common.Ptr(constants.DefaultFsGroup), }, @@ -252,7 +238,7 @@ func TestProcessShellWithNonStandardImage(t *testing.T) { wantActions := actiontypes.NewActionGroups(). Append(func(list actiontypes.ActionList) actiontypes.ActionList { return list. - Setup(true, true). + Setup(true, false, true). Declare(constants.RootOperationName, "true"). Declare(sig[0].Ref(), "true", constants.RootOperationName). Result(constants.RootOperationName, sig[0].Ref()). @@ -380,12 +366,12 @@ func TestProcessBasicEnvReference(t *testing.T) { sig := res.Signature volumes := res.Job.Spec.Template.Spec.Volumes - volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts + volumeMounts := res.Job.Spec.Template.Spec.Containers[0].VolumeMounts wantActions := lite.NewLiteActionGroups(). Append(func(list lite.LiteActionList) lite.LiteActionList { return list. - Setup(false, false). + Setup(false, false, false). Declare(constants.RootOperationName, "true"). Declare(sig[0].Ref(), "true", constants.RootOperationName). Result(constants.RootOperationName, sig[0].Ref()). @@ -393,12 +379,9 @@ func TestProcessBasicEnvReference(t *testing.T) { Start(""). CurrentStatus("true"). Start(constants.RootOperationName). - CurrentStatus(constants.RootOperationName) - }). - Append(func(list lite.LiteActionList) lite.LiteActionList { - return list. + CurrentStatus(constants.RootOperationName). MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test"), }). Start(sig[0].Ref()). @@ -412,31 +395,13 @@ func TestProcessBasicEnvReference(t *testing.T) { RestartPolicy: corev1.RestartPolicyNever, EnableServiceLinks: common.Ptr(false), Volumes: volumes, - InitContainers: []corev1.Container{ + InitContainers: []corev1.Container{}, + Containers: []corev1.Container{ { Name: "1", Image: constants.DefaultInitImage, ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"/init", "0"}, - Env: []corev1.EnvVar{ - envDebugNode, - envDebugPod, - envDebugNamespace, - envDebugServiceAccount, - envActions, - }, - VolumeMounts: volumeMounts, - SecurityContext: &corev1.SecurityContext{ - RunAsGroup: common.Ptr(constants.DefaultFsGroup), - }, - }, - }, - Containers: []corev1.Container{ - { - Name: "2", - Image: constants.DefaultInitImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "1"}, Env: []corev1.EnvVar{ env(0, false, "CI", "1"), env(0, false, "ZERO", "foo"), @@ -444,6 +409,11 @@ func TestProcessBasicEnvReference(t *testing.T) { env(0, false, "INPUT", "foobar"), env(0, true, "NEXT", "foo{{env.UNDETERMINED}}foofoobarbar"), env(0, false, "LAST", "foofoobarbar"), + envDebugNode, + envDebugPod, + envDebugNamespace, + envDebugServiceAccount, + envActions, }, VolumeMounts: volumeMounts, SecurityContext: &corev1.SecurityContext{ @@ -480,7 +450,7 @@ func TestProcessMultipleSteps(t *testing.T) { wantActions := lite.NewLiteActionGroups(). Append(func(list lite.LiteActionList) lite.LiteActionList { return list. - Setup(false, false). + Setup(false, false, false). Declare(constants.RootOperationName, "true"). Declare(sig[0].Ref(), "true", constants.RootOperationName). Declare(sig[1].Ref(), sig[0].Ref(), constants.RootOperationName). @@ -489,12 +459,11 @@ func TestProcessMultipleSteps(t *testing.T) { Start(""). CurrentStatus("true"). Start(constants.RootOperationName). - CurrentStatus(constants.RootOperationName) - }). - Append(func(list lite.LiteActionList) lite.LiteActionList { - return list. + CurrentStatus(constants.RootOperationName). + + // Joined as default container is used MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test"), }). Start(sig[0].Ref()). @@ -505,7 +474,7 @@ func TestProcessMultipleSteps(t *testing.T) { Append(func(list lite.LiteActionList) lite.LiteActionList { return list. MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test-2"), }). Start(sig[1].Ref()). @@ -526,6 +495,7 @@ func TestProcessMultipleSteps(t *testing.T) { ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"/init", "0"}, Env: []corev1.EnvVar{ + env(0, false, "CI", "1"), envDebugNode, envDebugPod, envDebugNamespace, @@ -537,26 +507,13 @@ func TestProcessMultipleSteps(t *testing.T) { RunAsGroup: common.Ptr(constants.DefaultFsGroup), }, }, - { - Name: "2", - Image: constants.DefaultInitImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "1"}, - Env: []corev1.EnvVar{ - env(0, false, "CI", "1"), - }, - VolumeMounts: volumeMounts, - SecurityContext: &corev1.SecurityContext{ - RunAsGroup: common.Ptr(constants.DefaultFsGroup), - }, - }, }, Containers: []corev1.Container{ { - Name: "3", + Name: "2", Image: constants.DefaultInitImage, ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "2"}, + Command: []string{"/init", "1"}, Env: []corev1.EnvVar{ env(0, false, "CI", "1"), }, @@ -602,7 +559,7 @@ func TestProcessNestedSteps(t *testing.T) { wantActions := lite.NewLiteActionGroups(). Append(func(list lite.LiteActionList) lite.LiteActionList { return list. - Setup(false, false). + Setup(false, false, false). Declare(constants.RootOperationName, "true"). Declare(sig[0].Ref(), "true", constants.RootOperationName). Declare(sig[1].Ref(), sig[0].Ref(), constants.RootOperationName). @@ -615,12 +572,11 @@ func TestProcessNestedSteps(t *testing.T) { Start(""). CurrentStatus("true"). Start(constants.RootOperationName). - CurrentStatus(constants.RootOperationName) - }). - Append(func(list lite.LiteActionList) lite.LiteActionList { - return list. + CurrentStatus(constants.RootOperationName). + + // Joined as default container is used MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test"), }). Start(sig[0].Ref()). @@ -633,7 +589,7 @@ func TestProcessNestedSteps(t *testing.T) { Append(func(list lite.LiteActionList) lite.LiteActionList { return list. MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test-2"), }). Start(sig[1].Children()[0].Ref()). @@ -644,7 +600,7 @@ func TestProcessNestedSteps(t *testing.T) { Append(func(list lite.LiteActionList) lite.LiteActionList { return list. MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test-3"), }). Start(sig[1].Children()[1].Ref()). @@ -656,7 +612,7 @@ func TestProcessNestedSteps(t *testing.T) { Append(func(list lite.LiteActionList) lite.LiteActionList { return list. MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test-4"), }). Start(sig[2].Ref()). @@ -677,6 +633,7 @@ func TestProcessNestedSteps(t *testing.T) { ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"/init", "0"}, Env: []corev1.EnvVar{ + env(0, false, "CI", "1"), envDebugNode, envDebugPod, envDebugNamespace, @@ -714,26 +671,13 @@ func TestProcessNestedSteps(t *testing.T) { RunAsGroup: common.Ptr(constants.DefaultFsGroup), }, }, - { - Name: "4", - Image: constants.DefaultInitImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "3"}, - Env: []corev1.EnvVar{ - env(0, false, "CI", "1"), - }, - VolumeMounts: volumeMounts, - SecurityContext: &corev1.SecurityContext{ - RunAsGroup: common.Ptr(constants.DefaultFsGroup), - }, - }, }, Containers: []corev1.Container{ { - Name: "5", + Name: "4", Image: constants.DefaultInitImage, ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "4"}, + Command: []string{"/init", "3"}, Env: []corev1.EnvVar{ env(0, false, "CI", "1"), }, @@ -776,8 +720,8 @@ func TestProcessLocalContent(t *testing.T) { assert.NoError(t, err) volumes := res.Job.Spec.Template.Spec.Volumes - volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts - volumeMountsWithContent := res.Job.Spec.Template.Spec.InitContainers[1].VolumeMounts + volumeMounts := res.Job.Spec.Template.Spec.Containers[0].VolumeMounts + volumeMountsWithContent := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts want := corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, @@ -790,25 +734,13 @@ func TestProcessLocalContent(t *testing.T) { ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"/init", "0"}, Env: []corev1.EnvVar{ + env(0, false, "CI", "1"), envDebugNode, envDebugPod, envDebugNamespace, envDebugServiceAccount, envActions, }, - VolumeMounts: volumeMounts, - SecurityContext: &corev1.SecurityContext{ - RunAsGroup: common.Ptr(constants.DefaultFsGroup), - }, - }, - { - Name: "2", - Image: constants.DefaultInitImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "1"}, - Env: []corev1.EnvVar{ - env(0, false, "CI", "1"), - }, VolumeMounts: volumeMountsWithContent, SecurityContext: &corev1.SecurityContext{ RunAsGroup: common.Ptr(constants.DefaultFsGroup), @@ -817,10 +749,10 @@ func TestProcessLocalContent(t *testing.T) { }, Containers: []corev1.Container{ { - Name: "3", + Name: "2", Image: constants.DefaultInitImage, ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "2"}, + Command: []string{"/init", "1"}, Env: []corev1.EnvVar{ env(0, false, "CI", "1"), }, @@ -881,6 +813,7 @@ func TestProcessGlobalContent(t *testing.T) { ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"/init", "0"}, Env: []corev1.EnvVar{ + env(0, false, "CI", "1"), envDebugNode, envDebugPod, envDebugNamespace, @@ -892,26 +825,13 @@ func TestProcessGlobalContent(t *testing.T) { RunAsGroup: common.Ptr(constants.DefaultFsGroup), }, }, - { - Name: "2", - Image: constants.DefaultInitImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "1"}, - Env: []corev1.EnvVar{ - env(0, false, "CI", "1"), - }, - VolumeMounts: volumeMounts, - SecurityContext: &corev1.SecurityContext{ - RunAsGroup: common.Ptr(constants.DefaultFsGroup), - }, - }, }, Containers: []corev1.Container{ { - Name: "3", + Name: "2", Image: constants.DefaultInitImage, ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"/init", "2"}, + Command: []string{"/init", "1"}, Env: []corev1.EnvVar{ env(0, false, "CI", "1"), }, @@ -928,6 +848,7 @@ func TestProcessGlobalContent(t *testing.T) { assert.Equal(t, want, res.Job.Spec.Template.Spec) assert.Equal(t, 3, len(volumeMounts)) + fmt.Println(volumeMounts) assert.Equal(t, "/some/path", volumeMounts[2].MountPath) assert.Equal(t, 1, len(res.ConfigMaps)) assert.Equal(t, volumeMounts[2].Name, volumes[2].Name) @@ -971,7 +892,7 @@ func TestProcessShell(t *testing.T) { want := lite.NewLiteActionGroups(). Append(func(list lite.LiteActionList) lite.LiteActionList { return list. - Setup(false, false). + Setup(false, false, false). Declare(constants.RootOperationName, "true"). Declare(sig[0].Ref(), "true", constants.RootOperationName). Result(constants.RootOperationName, sig[0].Ref()). @@ -979,12 +900,11 @@ func TestProcessShell(t *testing.T) { Start(""). CurrentStatus("true"). Start(constants.RootOperationName). - CurrentStatus(constants.RootOperationName) - }). - Append(func(list lite.LiteActionList) lite.LiteActionList { - return list. + CurrentStatus(constants.RootOperationName). + + // Joined together as default image is used MutateContainer(lite.LiteContainerConfig{ - Command: cmd("/bin/sh"), + Command: cmd("/.tktw-bin/sh"), Args: cmdShell("shell-test"), }). Start(sig[0].Ref()). diff --git a/pkg/testworkflows/testworkflowprocessor/processor.go b/pkg/testworkflows/testworkflowprocessor/processor.go index a38bc7895e8..ee59505b2a7 100644 --- a/pkg/testworkflows/testworkflowprocessor/processor.go +++ b/pkg/testworkflows/testworkflowprocessor/processor.go @@ -18,6 +18,7 @@ import ( "github.com/kubeshop/testkube/pkg/imageinspector" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/action/actiontypes/lite" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/stage" ) @@ -253,11 +254,18 @@ func (p *processor) Bundle(ctx context.Context, workflow *testworkflowsv1.TestWo if err != nil { return nil, errors.Wrap(err, "analyzing Kubernetes container operations") } - actionGroups := action.Group(actions) + usesToolkit := false + for _, a := range actions { + if a.Type() == lite.ActionTypeExecute && a.Execute.Toolkit { + usesToolkit = true + break + } + } + actionGroups := action.Finalize(action.Group(actions)) containers := make([]corev1.Container, len(actionGroups)) for i := range actionGroups { var bareActions []actiontypes.Action - containers[i], bareActions, err = action.CreateContainer(i, layer.ContainerDefaults(), actionGroups[i]) + containers[i], bareActions, err = action.CreateContainer(i, layer.ContainerDefaults(), actionGroups[i], usesToolkit) actionGroups[i] = bareActions if err != nil { return nil, errors.Wrap(err, "building Kubernetes containers") @@ -378,7 +386,6 @@ func (p *processor) Bundle(ctx context.Context, workflow *testworkflowsv1.TestWo jobSpec.Annotations = jobAnnotations // Build running instructions - // TODO: Get rid of the unnecessary ContainerConfig parts actionGroupsSerialized, _ := json.Marshal(actionGroups) podAnnotations := make(map[string]string) maps.Copy(podAnnotations, jobSpec.Spec.Template.Annotations) diff --git a/pkg/testworkflows/testworkflowprocessor/stage/containerstage.go b/pkg/testworkflows/testworkflowprocessor/stage/containerstage.go index 2481c72a2f4..328cfb906f5 100644 --- a/pkg/testworkflows/testworkflowprocessor/stage/containerstage.go +++ b/pkg/testworkflows/testworkflowprocessor/stage/containerstage.go @@ -1,6 +1,8 @@ package stage import ( + "slices" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/rand" @@ -14,16 +16,25 @@ var BypassToolkitCheck = corev1.EnvVar{ Value: rand.String(20), } +var BypassPure = corev1.EnvVar{ + Name: "TK_TC_PURE", + Value: rand.String(20), +} + type containerStage struct { stageMetadata stageLifecycle container Container + pure bool } type ContainerStage interface { Stage Container() Container IsToolkit() bool + + SetPure(pure bool) ContainerStage + Pure() bool // TODO: Consider purity level? } func NewContainerStage(ref string, container Container) ContainerStage { @@ -84,3 +95,12 @@ func (s *containerStage) HasPause() bool { func (s *containerStage) IsToolkit() bool { return s.container.IsToolkit() } + +func (s *containerStage) Pure() bool { + return s.pure || slices.Contains(s.container.Env(), BypassPure) +} + +func (s *containerStage) SetPure(pure bool) ContainerStage { + s.pure = pure + return s +}