From 25f31753b4042ca606524fb27da34eb0ae7ecfa5 Mon Sep 17 00:00:00 2001 From: Eugene McArdle Date: Fri, 30 Jul 2021 13:30:25 +1000 Subject: [PATCH] Add script mode support to windows tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps and sidecars can contain a script field. In linux tasks these scripts are copied into files which are made executable and then steps are added to the task to execute those files. This commit adds comparable functionality if scripts are used in a task which will run on a windows node. On a windows node the mechanics are different, due to how windows handles executable files. The key difference is that Tekton needs to know that a script will run on windows, and how to run the file (which interpreter to use). This is done through a ‘windows shebang’ line at the start of the script. The line must begin with ‘#!win’. After that the user needs to provide the interpreter to use, as well as any necessary arguments. The line must be written such that the name of the file containing the script to execute can be appended to the end. For example, to run the script in the file ‘test.ps1’ with powershell, the command would usually be ‘powershell -File test.ps1’ and so the shebang line must be ‘#!win powershell -File’. If no interpreter is provided (i.e. the shebang line is only ‘#!win’) then the script contents will be stored in a .cmd file and executed. Finally, since a pod cannot contain a mix of windows and linux containers a windows shell image has been added to the Images structure, which will be used in the place-scripts step when needed on a windows node. To maintain parity with other alpha features, task validation for tasks containing windows scripts will now require the 'enable-api-fields' flag to be 'alpha'. TaskRuns/Tasks that contain windows scripts will be rejected if this flag is not set, giving the user immediate feedback. The integration tests for windows scripts have been updated to reflect this alpha flag requirement. --- cmd/controller/main.go | 2 + config/controller.yaml | 5 +- docs/tasks.md | 46 ++++ pkg/apis/pipeline/images.go | 3 + pkg/apis/pipeline/images_test.go | 2 + pkg/apis/pipeline/v1beta1/task_validation.go | 7 + .../pipeline/v1beta1/task_validation_test.go | 13 + pkg/pod/pod.go | 4 +- pkg/pod/script.go | 106 ++++++-- pkg/pod/script_test.go | 237 +++++++++++++++++- test/windows_script_test.go | 192 ++++++++++++++ 11 files changed, 591 insertions(+), 26 deletions(-) create mode 100644 test/windows_script_test.go diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 691bd00e99d..64bca8f2b5e 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -46,6 +46,7 @@ var ( gitImage = flag.String("git-image", "", "The container image containing our Git binary.") kubeconfigWriterImage = flag.String("kubeconfig-writer-image", "", "The container image containing our kubeconfig writer binary.") shellImage = flag.String("shell-image", "", "The container image containing a shell") + shellImageWin = flag.String("shell-image-win", "", "The container image containing a windows shell") gsutilImage = flag.String("gsutil-image", "", "The container image containing gsutil") prImage = flag.String("pr-image", "", "The container image containing our PR binary.") imageDigestExporterImage = flag.String("imagedigest-exporter-image", "", "The container image containing our image digest exporter binary.") @@ -65,6 +66,7 @@ func main() { GitImage: *gitImage, KubeconfigWriterImage: *kubeconfigWriterImage, ShellImage: *shellImage, + ShellImageWin: *shellImageWin, GsutilImage: *gsutilImage, PRImage: *prImage, ImageDigestExporterImage: *imageDigestExporterImage, diff --git a/config/controller.yaml b/config/controller.yaml index 2ba64fd4182..183da5b422a 100644 --- a/config/controller.yaml +++ b/config/controller.yaml @@ -77,7 +77,10 @@ spec: # The shell image must be root in order to create directories and copy files to PVCs. # gcr.io/distroless/base:debug as of Apirl 17, 2021 # image shall not contains tag, so it will be supported on a runtime like cri-o - "-shell-image", "gcr.io/distroless/base@sha256:aa4fd987555ea10e1a4ec8765da8158b5ffdfef1e72da512c7ede509bc9966c4" + "-shell-image", "gcr.io/distroless/base@sha256:aa4fd987555ea10e1a4ec8765da8158b5ffdfef1e72da512c7ede509bc9966c4", + # for script mode to work with windows we need a powershell image + # pinning to nanoserver tag as of July 15 2021 + "-shell-image-win", "mcr.microsoft.com/powershell:nanoserver@sha256:b6d5ff841b78bdf2dfed7550000fd4f3437385b8fa686ec0f010be24777654d6" ] volumeMounts: - name: config-logging diff --git a/docs/tasks.md b/docs/tasks.md index 790f1633dbf..b39e0c7d245 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -12,6 +12,7 @@ weight: 200 - [Defining `Steps`](#defining-steps) - [Reserved directories](#reserved-directories) - [Running scripts within `Steps`](#running-scripts-within-steps) + - [Windows Scripts](#windows-scripts) - [Specifying a timeout](#specifying-a-timeout) - [Specifying `onError` for a `step`](#specifying-onerror-for-a-step) - [Accessing Step's `exitCode` in subsequent `Steps`](#accessing-steps-exitcode-in-subsequent-steps) @@ -265,6 +266,51 @@ steps: #!/usr/bin/env bash /bin/my-binary ``` + +##### Windows scripts + +Scripts in tasks that will eventually run on windows nodes need a custom shebang line, so that Tekton knows how to run the script. The format of the shebang line is: + +`#!win ` + +Unlike linux, we need to specify how to interpret the script file which is generated by Tekton. The example below shows how to execute a powershell script: + +```yaml +steps: + - image: mcr.microsoft.com/windows/servercore:1809 + script: | + #!win powershell.exe -File + echo 'Hello from PowerShell' +``` + +Microsoft provide `powershell` images, which contain Powershell Core (which is slightly different from powershell found in standard windows images). The example below shows how to use these images: +```yaml +steps: + - image: mcr.microsoft.com/powershell:nanoserver + script: | + #!win pwsh.exe -File + echo 'Hello from PowerShell Core' +``` + +As can be seen the command is different. The windows shebang can be used for any interpreter, as long as it exists in the image and can interpret commands from a file. The example below executes a Python script: +```yaml + steps: + - image: python + script: | + #!win python + print("Hello from Python!") +``` +Note that other than the `#!win` shebang the example is identical to the earlier linux example. + +Finally, if no interpreter is specified on the `#!win` line then the script will be treated as a windows `.cmd` file which will be excecuted. The example below shows this: +```yaml + steps: + - image: mcr.microsoft.com/powershell:lts-nanoserver-1809 + script: | + #!win + echo Hello from the default cmd file +``` + #### Specifying a timeout A `Step` can specify a `timeout` field. diff --git a/pkg/apis/pipeline/images.go b/pkg/apis/pipeline/images.go index 19de1291920..e0c81c9869c 100644 --- a/pkg/apis/pipeline/images.go +++ b/pkg/apis/pipeline/images.go @@ -34,6 +34,8 @@ type Images struct { KubeconfigWriterImage string // ShellImage is the container image containing bash shell. ShellImage string + // ShellImageWin is the container image containing powershell. + ShellImageWin string // GsutilImage is the container image containing gsutil. GsutilImage string // PRImage is the container image that we use to implement the PR source step. @@ -55,6 +57,7 @@ func (i Images) Validate() error { {i.GitImage, "git"}, {i.KubeconfigWriterImage, "kubeconfig-writer"}, {i.ShellImage, "shell"}, + {i.ShellImageWin, "windows-shell"}, {i.GsutilImage, "gsutil"}, {i.PRImage, "pr"}, {i.ImageDigestExporterImage, "imagedigest-exporter"}, diff --git a/pkg/apis/pipeline/images_test.go b/pkg/apis/pipeline/images_test.go index 0e78b3ecfd1..c4c4e766321 100644 --- a/pkg/apis/pipeline/images_test.go +++ b/pkg/apis/pipeline/images_test.go @@ -13,6 +13,7 @@ func TestValidate(t *testing.T) { GitImage: "set", KubeconfigWriterImage: "set", ShellImage: "set", + ShellImageWin: "set", GsutilImage: "set", PRImage: "set", ImageDigestExporterImage: "set", @@ -27,6 +28,7 @@ func TestValidate(t *testing.T) { GitImage: "", // unset! KubeconfigWriterImage: "set", ShellImage: "", // unset! + ShellImageWin: "set", GsutilImage: "set", PRImage: "", // unset! ImageDigestExporterImage: "set", diff --git a/pkg/apis/pipeline/v1beta1/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go index 72606e70711..0f9223f6355 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation.go +++ b/pkg/apis/pipeline/v1beta1/task_validation.go @@ -231,6 +231,13 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi }) } } + + if s.Script != "" { + cleaned := strings.TrimSpace(s.Script) + if strings.HasPrefix(cleaned, "#!win") { + errs = errs.Also(ValidateEnabledAPIFields(ctx, "windows script support", config.AlphaAPIFields).ViaField("script")) + } + } return errs } diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go index cb2dc74aaa2..fc56a9286a8 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go @@ -1253,6 +1253,19 @@ func TestIncompatibleAPIVersions(t *testing.T) { }, }}, }, + }, { + name: "windows script support requires alpha", + requiredVersion: "alpha", + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "my-image", + }, + Script: ` + #!win powershell -File + script-1`, + }}, + }, }} versions := []string{"alpha", "stable"} for _, tt := range tests { diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 8f57b3b04f6..104b5e08cca 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -136,9 +136,9 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec // Convert any steps with Script to command+args. // If any are found, append an init container to initialize scripts. if alphaAPIEnabled { - scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, steps, taskSpec.Sidecars, taskRun.Spec.Debug) + scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, b.Images.ShellImageWin, steps, taskSpec.Sidecars, taskRun.Spec.Debug) } else { - scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, steps, taskSpec.Sidecars, nil) + scriptsInit, stepContainers, sidecarContainers = convertScripts(b.Images.ShellImage, "", steps, taskSpec.Sidecars, nil) } if scriptsInit != nil { initContainers = append(initContainers, *scriptsInit) diff --git a/pkg/pod/script.go b/pkg/pod/script.go index 4d76b21398a..31efc9e63ef 100644 --- a/pkg/pod/script.go +++ b/pkg/pod/script.go @@ -70,20 +70,28 @@ var ( ) // convertScripts converts any steps and sidecars that specify a Script field into a normal Container. -// -// It does this by prepending a container that writes specified Script bodies -// to executable files in a shared volumeMount, then produces Containers that -// simply run those executable files. -func convertScripts(shellImage string, steps []v1beta1.Step, sidecars []v1beta1.Sidecar, debugConfig *v1beta1.TaskRunDebug) (*corev1.Container, []corev1.Container, []corev1.Container) { +func convertScripts(shellImageLinux string, shellImageWin string, steps []v1beta1.Step, sidecars []v1beta1.Sidecar, debugConfig *v1beta1.TaskRunDebug) (*corev1.Container, []corev1.Container, []corev1.Container) { placeScripts := false // Place scripts is an init container used for creating scripts in the // /tekton/scripts directory which would be later used by the step containers // as a Command + requiresWindows := checkWindowsRequirement(steps, sidecars) + + shellImage := shellImageLinux + shellCommand := "sh" + shellArg := "-c" + // Set windows variants for Image, Command and Args + if requiresWindows { + shellImage = shellImageWin + shellCommand = "pwsh" + shellArg = "-Command" + } + placeScriptsInit := corev1.Container{ Name: "place-scripts", Image: shellImage, - Command: []string{"sh"}, - Args: []string{"-c", ""}, + Command: []string{shellCommand}, + Args: []string{shellArg, ""}, VolumeMounts: []corev1.VolumeMount{writeScriptsVolumeMount, toolsMount}, } @@ -131,6 +139,7 @@ func convertListOfSteps(steps []v1beta1.Step, initContainer *corev1.Container, p // The shebang must be the first non-empty line. cleaned := strings.TrimSpace(s.Script) hasShebang := strings.HasPrefix(cleaned, "#!") + requiresWindows := strings.HasPrefix(cleaned, "#!win") script := s.Script if !hasShebang { @@ -141,13 +150,26 @@ func convertListOfSteps(steps []v1beta1.Step, initContainer *corev1.Container, p // non-nil init container. *placeScripts = true - script = encodeScript(script) - // Append to the place-scripts script to place the // script file in a known location in the scripts volume. scriptFile := filepath.Join(scriptsDir, names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("%s-%d", namePrefix, i))) - heredoc := "_EOF_" // underscores because base64 doesnt include them in its alphabet - initContainer.Args[1] += fmt.Sprintf(`scriptfile="%s" + if requiresWindows { + command, args, script, scriptFile := extractWindowsScriptComponents(script, scriptFile) + initContainer.Args[1] += fmt.Sprintf(`@" +%s +"@ | Out-File -FilePath %s +`, script, scriptFile) + + steps[i].Command = command + // Append existing args field to end of derived args + args = append(args, steps[i].Args...) + steps[i].Args = args + } else { + // Only encode the script for linux scripts + // The decode-script subcommand of the entrypoint does not work under windows + script = encodeScript(script) + heredoc := "_EOF_" // underscores because base64 doesnt include them in its alphabet + initContainer.Args[1] += fmt.Sprintf(`scriptfile="%s" touch ${scriptfile} && chmod +x ${scriptfile} cat > ${scriptfile} << '%s' %s @@ -155,12 +177,13 @@ cat > ${scriptfile} << '%s' /tekton/tools/entrypoint decode-script "${scriptfile}" `, scriptFile, heredoc, script, heredoc) - // Set the command to execute the correct script in the mounted - // volume. - // A previous merge with stepTemplate may have populated - // Command and Args, even though this is not normally valid, so - // we'll clear out the Args and overwrite Command. - steps[i].Command = []string{scriptFile} + // Set the command to execute the correct script in the mounted + // volume. + // A previous merge with stepTemplate may have populated + // Command and Args, even though this is not normally valid, so + // we'll clear out the Args and overwrite Command. + steps[i].Command = []string{scriptFile} + } steps[i].VolumeMounts = append(steps[i].VolumeMounts, scriptsVolumeMount) // Add debug mounts if breakpoints are present @@ -207,3 +230,52 @@ cat > ${scriptfile} << '%s' func encodeScript(script string) string { return base64.StdEncoding.EncodeToString([]byte(script)) } + +func checkWindowsRequirement(steps []v1beta1.Step, sidecars []v1beta1.Sidecar) bool { + // Detect windows shebangs + for _, step := range steps { + cleaned := strings.TrimSpace(step.Script) + if strings.HasPrefix(cleaned, "#!win") { + return true + } + } + // If no step needs windows, then check sidecars to be sure + for _, sidecar := range sidecars { + cleaned := strings.TrimSpace(sidecar.Script) + if strings.HasPrefix(cleaned, "#!win") { + return true + } + } + return false +} + +func extractWindowsScriptComponents(script string, fileName string) ([]string, []string, string, string) { + // Set the command to execute the correct script in the mounted volume. + shebangLine := strings.Split(script, "\n")[0] + splitLine := strings.Split(shebangLine, " ") + var command, args []string + if len(splitLine) > 1 { + strippedCommand := splitLine[1:] + command = strippedCommand[0:1] + // Handle legacy powershell limitation + if strings.HasPrefix(command[0], "powershell") { + fileName += ".ps1" + } + if len(strippedCommand) > 1 { + args = strippedCommand[1:] + args = append(args, fileName) + } else { + args = []string{fileName} + } + } else { + // If no interpreter is specified then strip the shebang and + // create a .cmd file + fileName += ".cmd" + commandLines := strings.Split(script, "\n")[1:] + script = strings.Join(commandLines, "\n") + command = []string{fileName} + args = []string{} + } + + return command, args, script, fileName +} diff --git a/pkg/pod/script_test.go b/pkg/pod/script_test.go index a0099bc20f8..344fb4ba983 100644 --- a/pkg/pod/script_test.go +++ b/pkg/pod/script_test.go @@ -27,7 +27,7 @@ import ( ) func TestConvertScripts_NothingToConvert_EmptySidecars(t *testing.T) { - gotInit, gotScripts, gotSidecars := convertScripts(images.ShellImage, []v1beta1.Step{{ + gotInit, gotScripts, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ Container: corev1.Container{ Image: "step-1", }, @@ -54,7 +54,7 @@ func TestConvertScripts_NothingToConvert_EmptySidecars(t *testing.T) { } func TestConvertScripts_NothingToConvert_NilSidecars(t *testing.T) { - gotInit, gotScripts, gotSidecars := convertScripts(images.ShellImage, []v1beta1.Step{{ + gotInit, gotScripts, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ Container: corev1.Container{ Image: "step-1", }, @@ -81,7 +81,7 @@ func TestConvertScripts_NothingToConvert_NilSidecars(t *testing.T) { } func TestConvertScripts_NothingToConvert_WithSidecar(t *testing.T) { - gotInit, gotScripts, gotSidecars := convertScripts(images.ShellImage, []v1beta1.Step{{ + gotInit, gotScripts, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ Container: corev1.Container{ Image: "step-1", }, @@ -130,7 +130,7 @@ func TestConvertScripts(t *testing.T) { MountPath: "/another/one", }} - gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, []v1beta1.Step{{ + gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ Script: `#!/bin/sh script-1`, Container: corev1.Container{Image: "step-1"}, @@ -223,7 +223,7 @@ func TestConvertScripts_WithBreakpoint_OnFailure(t *testing.T) { MountPath: "/another/one", }} - gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, []v1beta1.Step{{ + gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ Script: `#!/bin/sh script-1`, Container: corev1.Container{Image: "step-1"}, @@ -365,7 +365,7 @@ func TestConvertScripts_WithSidecar(t *testing.T) { MountPath: "/another/one", }} - gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, []v1beta1.Step{{ + gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ Script: `#!/bin/sh script-1`, Container: corev1.Container{Image: "step-1"}, @@ -447,3 +447,228 @@ _EOF_ } } + +func TestConvertScripts_Windows(t *testing.T) { + names.TestingSeed() + + preExistingVolumeMounts := []corev1.VolumeMount{{ + Name: "pre-existing-volume-mount", + MountPath: "/mount/path", + }, { + Name: "another-one", + MountPath: "/another/one", + }} + + gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ + Script: `#!win pwsh -File +script-1`, + Container: corev1.Container{Image: "step-1"}, + }, { + // No script to convert here. + Container: corev1.Container{Image: "step-2"}, + }, { + Script: `#!win powershell -File +script-3`, + Container: corev1.Container{ + Image: "step-3", + VolumeMounts: preExistingVolumeMounts, + Args: []string{"my", "args"}, + }, + }, { + Script: `#!win +no-shebang`, + Container: corev1.Container{ + Image: "step-3", + VolumeMounts: preExistingVolumeMounts, + Args: []string{"my", "args"}, + }, + }}, []v1beta1.Sidecar{}, nil) + wantInit := &corev1.Container{ + Name: "place-scripts", + Image: images.ShellImageWin, + Command: []string{"pwsh"}, + Args: []string{"-Command", `@" +#!win pwsh -File +script-1 +"@ | Out-File -FilePath /tekton/scripts/script-0-9l9zj +@" +#!win powershell -File +script-3 +"@ | Out-File -FilePath /tekton/scripts/script-2-mz4c7.ps1 +@" +no-shebang +"@ | Out-File -FilePath /tekton/scripts/script-3-mssqb.cmd +`}, + VolumeMounts: []corev1.VolumeMount{writeScriptsVolumeMount, toolsMount}, + } + want := []corev1.Container{{ + Image: "step-1", + Command: []string{"pwsh"}, + Args: []string{"-File", "/tekton/scripts/script-0-9l9zj"}, + VolumeMounts: []corev1.VolumeMount{scriptsVolumeMount}, + }, { + Image: "step-2", + }, { + Image: "step-3", + Command: []string{"powershell"}, + Args: []string{"-File", "/tekton/scripts/script-2-mz4c7.ps1", "my", "args"}, + VolumeMounts: append(preExistingVolumeMounts, scriptsVolumeMount), + }, { + Image: "step-3", + Command: []string{"/tekton/scripts/script-3-mssqb.cmd"}, + Args: []string{"my", "args"}, + VolumeMounts: []corev1.VolumeMount{ + {Name: "pre-existing-volume-mount", MountPath: "/mount/path"}, + {Name: "another-one", MountPath: "/another/one"}, + scriptsVolumeMount, + }, + }} + if d := cmp.Diff(wantInit, gotInit); d != "" { + t.Errorf("Init Container Diff %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(want, gotSteps); d != "" { + t.Errorf("Containers Diff %s", diff.PrintWantGot(d)) + } + + if len(gotSidecars) != 0 { + t.Errorf("Expected zero sidecars, got %v", len(gotSidecars)) + } +} + +func TestConvertScripts_Windows_WithSidecar(t *testing.T) { + names.TestingSeed() + + preExistingVolumeMounts := []corev1.VolumeMount{{ + Name: "pre-existing-volume-mount", + MountPath: "/mount/path", + }, { + Name: "another-one", + MountPath: "/another/one", + }} + + gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ + Script: `#!win pwsh -File +script-1`, + Container: corev1.Container{Image: "step-1"}, + }, { + // No script to convert here.: + Container: corev1.Container{Image: "step-2"}, + }, { + Script: `#!win powershell -File +script-3`, + Container: corev1.Container{ + Image: "step-3", + VolumeMounts: preExistingVolumeMounts, + Args: []string{"my", "args"}, + }, + }}, []v1beta1.Sidecar{{ + Script: `#!win pwsh -File +sidecar-1`, + Container: corev1.Container{Image: "sidecar-1"}, + }}, nil) + wantInit := &corev1.Container{ + Name: "place-scripts", + Image: images.ShellImageWin, + Command: []string{"pwsh"}, + Args: []string{"-Command", `@" +#!win pwsh -File +script-1 +"@ | Out-File -FilePath /tekton/scripts/script-0-9l9zj +@" +#!win powershell -File +script-3 +"@ | Out-File -FilePath /tekton/scripts/script-2-mz4c7.ps1 +@" +#!win pwsh -File +sidecar-1 +"@ | Out-File -FilePath /tekton/scripts/sidecar-script-0-mssqb +`}, + VolumeMounts: []corev1.VolumeMount{writeScriptsVolumeMount, toolsMount}, + } + want := []corev1.Container{{ + Image: "step-1", + Command: []string{"pwsh"}, + Args: []string{"-File", "/tekton/scripts/script-0-9l9zj"}, + VolumeMounts: []corev1.VolumeMount{scriptsVolumeMount}, + }, { + Image: "step-2", + }, { + Image: "step-3", + Command: []string{"powershell"}, + Args: []string{"-File", "/tekton/scripts/script-2-mz4c7.ps1", "my", "args"}, + VolumeMounts: []corev1.VolumeMount{ + {Name: "pre-existing-volume-mount", MountPath: "/mount/path"}, + {Name: "another-one", MountPath: "/another/one"}, + scriptsVolumeMount, + }, + }} + + wantSidecars := []corev1.Container{{ + Image: "sidecar-1", + Command: []string{"pwsh"}, + Args: []string{"-File", "/tekton/scripts/sidecar-script-0-mssqb"}, + VolumeMounts: []corev1.VolumeMount{scriptsVolumeMount}, + }} + if d := cmp.Diff(wantInit, gotInit); d != "" { + t.Errorf("Init Container Diff %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(want, gotSteps); d != "" { + t.Errorf("Step Containers Diff %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(wantSidecars, gotSidecars); d != "" { + t.Errorf("Sidecar Containers Diff %s", diff.PrintWantGot(d)) + } + + if len(gotSidecars) != 1 { + t.Errorf("Wanted 1 sidecar, got %v", len(gotSidecars)) + } + +} + +func TestConvertScripts_Windows_SidecarOnly(t *testing.T) { + names.TestingSeed() + + gotInit, gotSteps, gotSidecars := convertScripts(images.ShellImage, images.ShellImageWin, []v1beta1.Step{{ + // No script to convert here.: + Container: corev1.Container{Image: "step-1"}, + }}, []v1beta1.Sidecar{{ + Script: `#!win python +sidecar-1`, + Container: corev1.Container{Image: "sidecar-1"}, + }}, nil) + wantInit := &corev1.Container{ + Name: "place-scripts", + Image: images.ShellImageWin, + Command: []string{"pwsh"}, + Args: []string{"-Command", `@" +#!win python +sidecar-1 +"@ | Out-File -FilePath /tekton/scripts/sidecar-script-0-9l9zj +`}, + VolumeMounts: []corev1.VolumeMount{writeScriptsVolumeMount, toolsMount}, + } + want := []corev1.Container{{ + Image: "step-1", + }} + + wantSidecars := []corev1.Container{{ + Image: "sidecar-1", + Command: []string{"python"}, + Args: []string{"/tekton/scripts/sidecar-script-0-9l9zj"}, + VolumeMounts: []corev1.VolumeMount{scriptsVolumeMount}, + }} + if d := cmp.Diff(wantInit, gotInit); d != "" { + t.Errorf("Init Container Diff %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(want, gotSteps); d != "" { + t.Errorf("Step Containers Diff %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(wantSidecars, gotSidecars); d != "" { + t.Errorf("Sidecar Containers Diff %s", diff.PrintWantGot(d)) + } + + if len(gotSidecars) != 1 { + t.Errorf("Wanted 1 sidecar, got %v", len(gotSidecars)) + } + +} diff --git a/test/windows_script_test.go b/test/windows_script_test.go new file mode 100644 index 00000000000..542c1f26147 --- /dev/null +++ b/test/windows_script_test.go @@ -0,0 +1,192 @@ +// +build e2e,windows_e2e + +/* +Copyright 2021 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + knativetest "knative.dev/pkg/test" +) + +func TestWindowsScript(t *testing.T) { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + c, namespace := setup(ctx, t, requireAnyGate(map[string]string{"enable-api-fields": "alpha"})) + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + taskRunName := "windows-script-taskrun" + t.Logf("Creating TaskRun in namespace %s", namespace) + + taskRun := &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: taskRunName, Namespace: namespace}, + Spec: v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "mcr.microsoft.com/powershell:nanoserver", + }, + Script: `#!win pwsh.exe -File +echo Hello`, + }, { + Container: corev1.Container{ + Image: "mcr.microsoft.com/powershell:nanoserver", + }, + Script: `#!win +echo Hello`, + }}, + }, + PodTemplate: &v1beta1.PodTemplate{ + NodeSelector: map[string]string{"kubernetes.io/os": "windows"}, + }, + }, + } + if _, err := c.TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + t.Logf("Waiting for TaskRun in namespace %s to finish", namespace) + if err := WaitForTaskRunState(ctx, c, taskRunName, TaskRunSucceed(taskRunName), "TaskRunSucceeded"); err != nil { + t.Errorf("Error waiting for TaskRun to finish: %s", err) + } + + taskrun, err := c.TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + + expectedStepState := []v1beta1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + Name: "unnamed-0", + ContainerName: "step-unnamed-0", + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + Name: "unnamed-1", + ContainerName: "step-unnamed-1", + }} + if d := cmp.Diff(expectedStepState, taskrun.Status.Steps, ignoreTerminatedFields, ignoreStepFields); d != "" { + t.Fatalf("-want, +got: %v", d) + } +} + +func TestWindowsScriptFailure(t *testing.T) { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + c, namespace := setup(ctx, t, requireAnyGate(map[string]string{"enable-api-fields": "alpha"})) + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + taskRunName := "failing-windows-taskrun" + t.Logf("Creating TaskRun in namespace %s", namespace) + + taskRun := &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Name: taskRunName, Namespace: namespace}, + Spec: v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Image: "mcr.microsoft.com/powershell:nanoserver", + }, + Script: `#!win pwsh.exe -File +echo Hello`, + }, { + Container: corev1.Container{ + Image: "mcr.microsoft.com/powershell:nanoserver", + }, + Script: `#!win pwsh.exe -File +exit 42`, + }, { + Container: corev1.Container{ + Image: "mcr.microsoft.com/powershell:nanoserver", + }, + Script: `#!win pwsh.exe -File +echo Hello`, + }}, + }, + PodTemplate: &v1beta1.PodTemplate{ + NodeSelector: map[string]string{"kubernetes.io/os": "windows"}, + }, + }, + } + if _, err := c.TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + t.Logf("Waiting for TaskRun in namespace %s to fail", namespace) + if err := WaitForTaskRunState(ctx, c, taskRunName, TaskRunFailed(taskRunName), "TaskRunFailed"); err != nil { + t.Errorf("Error waiting for TaskRun to finish: %s", err) + } + + taskrun, err := c.TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + + expectedStepState := []v1beta1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 0, + Reason: "Completed", + }, + }, + Name: "unnamed-0", + ContainerName: "step-unnamed-0", + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 42, + Reason: "Error", + }, + }, + Name: "unnamed-1", + ContainerName: "step-unnamed-1", + }, { + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "Error", + }, + }, + Name: "unnamed-2", + ContainerName: "step-unnamed-2", + }} + if d := cmp.Diff(expectedStepState, taskrun.Status.Steps, ignoreTerminatedFields, ignoreStepFields); d != "" { + t.Fatalf("-want, +got: %v", d) + } +}