diff --git a/docs/artifacts.md b/docs/artifacts.md new file mode 100644 index 00000000000..233dec1c53a --- /dev/null +++ b/docs/artifacts.md @@ -0,0 +1,243 @@ + + +# Artifacts + +- [Overview](#overview) +- [Artifact Provenance Data](#passing-artifacts-between-steps) + - [Passing Artifacts between Steps](#passing-artifacts-between-steps) + + + +## Overview +> :seedling: **`Artifacts` is an [alpha](additional-configs.md#alpha-features) feature.** +> The `enable-artifacts` feature flag must be set to `"true"` to read or write artifacts in a step. + +Artifacts provide a way to track the origin of data produced and consumed within your Tekton Tasks. + +## Artifact Provenance Data +Artifacts fall into two categories: + + - Inputs: Artifacts downloaded and used by the Step/Task. + - Outputs: Artifacts created and uploaded by the Step/Task. +Example Structure: +```json +{ + "inputs":[ + { + "name": "", + "values": [ + { + "uri": "pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", + "digest": { "sha256": "b35caccc..." } + } + ] + } + ], + "outputs": [ + { + "name": "", + "values": [ + { + "uri": "pkg:oci/nginx:stable-alpine3.17-slim?repository_url=docker.io/library", + "digest": { + "sha256": "df85b9e3...", + "sha1": "95588b8f..." + } + } + ] + } + ] +} + +``` + +The content is written by the `Step` to a file `$(step.artifacts.path)`: + +```yaml +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + generateName: step-artifacts- +spec: + taskSpec: + description: | + A simple task that populates artifacts to TaskRun stepState + steps: + - name: artifacts-producer + image: bash:latest + script: | + cat > $(step.artifacts.path) << EOF + { + "inputs":[ + { + "name":"source", + "values":[ + { + "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", + "digest":{ + "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0" + } + } + ] + } + ], + "outputs":[ + { + "name":"image", + "values":[ + { + "uri":"pkg:oci/nginx:stable-alpine3.17-slim?repository_url=docker.io/library", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } + ] + } + ] + } + EOF +``` + +It is recommended to use [purl format](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst) for artifacts uri as shown in the example. + +### Passing Artifacts between Steps +You can pass artifacts from one step to the next using: + +- Specific Artifact: `$(steps..inputs.)` or `$(steps..outputs.)` +- Default (First) Artifact: `$(steps..inputs)` or `$(steps..outputs)` (if is omitted) + +The example below shows how to access the previous' step artifacts from another step in the same task + +```yaml +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + generateName: step-artifacts- +spec: + taskSpec: + description: | + A simple task that populates artifacts to TaskRun stepState + steps: + - name: artifacts-producer + image: bash:latest + script: | + # the script is for creating the output artifacts + cat > $(step.artifacts.path) << EOF + { + "inputs":[ + { + "name":"source", + "values":[ + { + "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", + "digest":{ + "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0" + } + } + ] + } + ], + "outputs":[ + { + "name":"image", + "values":[ + { + "uri":"pkg:oci/nginx:stable-alpine3.17-slim?repository_url=docker.io/library", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } + ] + } + ] + } + EOF + - name: artifacts-consumer + image: bash:latest + script: | + echo $(steps.artifacts-producer.outputs) +``` + + +The resolved value of `$(steps..outputs.)` or `$(steps..outputs)` is the values of an artifact. For this example, +`$(steps.artifacts-producer.outputs)` is resolved to +```json +[ + { + "uri":"pkg:oci/nginx:stable-alpine3.17-slim?repository_url=docker.io/library", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } +] +``` + + +Upon resolution and execution of the `TaskRun`, the `Status` will look something like: +```yaml +"steps": [ + { + "container": "step-artifacts-producer", + "imageID": "docker.io/library/bash@sha256:5353512b79d2963e92a2b97d9cb52df72d32f94661aa825fcfa0aede73304743", + "inputs": [ + { + "name": "source", + "values": [ + { + "digest": { + "sha256": "b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0" + }, + "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", + } + ] + } + ], + "name": "artifacts-producer", + "outputs": [ + { + "name": "image", + "values": [ + { + "digest": { + "sha1": "95588b8f34c31eb7d62c92aaa4e6506639b06ef2", + "sha256": "df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48" + }, + "uri":"pkg:oci/nginx:stable-alpine3.17-slim?repository_url=docker.io/library", + } + ] + } + ], + "terminated": { + "containerID": "containerd://010f02d103d1db48531327a1fe09797c87c1d50b6a216892319b3af93e0f56e7", + "exitCode": 0, + "finishedAt": "2024-03-18T17:05:06Z", + "message": "...", + "reason": "Completed", + "startedAt": "2024-03-18T17:05:06Z" + }, + "terminationReason": "Completed" + }, + { + "container": "step-artifacts-consumer", + "imageID": "docker.io/library/bash@sha256:5353512b79d2963e92a2b97d9cb52df72d32f94661aa825fcfa0aede73304743", + "name": "artifacts-consumer", + "terminated": { + "containerID": "containerd://42428aa7e5a507eba924239f213d185dd4bc0882b6f217a79e6792f7fec3586e", + "exitCode": 0, + "finishedAt": "2024-03-18T17:05:06Z", + "reason": "Completed", + "startedAt": "2024-03-18T17:05:06Z" + }, + "terminationReason": "Completed" + } + ], + +``` \ No newline at end of file diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index 5a07522b52b..e0b52e34073 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -1312,7 +1312,7 @@ TaskRunStatus

Artifact

-(Appears on:StepState) +(Appears on:Artifacts, StepState)

TaskRunStepArtifact represents an artifact produced or used by a step within a task run. @@ -1390,6 +1390,47 @@ string +

Artifacts +

+
+

Artifacts represents the collection of input and output artifacts associated with +a task run or a similar process. Artifacts in this context are units of data or resources +that the process either consumes as input or produces as output.

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+inputs
+ + +[]Artifact + + +
+
+outputs
+ + +[]Artifact + + +
+

ChildStatusReference

@@ -9640,7 +9681,7 @@ TaskRunStatus

Artifact

-(Appears on:StepState) +(Appears on:Artifacts, StepState)

TaskRunStepArtifact represents an artifact produced or used by a step within a task run. @@ -9718,6 +9759,47 @@ string +

Artifacts +

+
+

Artifacts represents the collection of input and output artifacts associated with +a task run or a similar process. Artifacts in this context are units of data or resources +that the process either consumes as input or produces as output.

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+inputs
+ + +[]Artifact + + +
+
+outputs
+ + +[]Artifact + + +
+

ChildStatusReference

diff --git a/examples/v1/taskruns/alpha/produce-consume-artifacts.yaml b/examples/v1/taskruns/alpha/produce-consume-artifacts.yaml new file mode 100644 index 00000000000..e270b1b85ff --- /dev/null +++ b/examples/v1/taskruns/alpha/produce-consume-artifacts.yaml @@ -0,0 +1,55 @@ +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + generateName: step-artifacts- +spec: + taskSpec: + description: | + A simple task that populates artifacts to TaskRun stepState + steps: + - name: artifacts-producer + image: bash:latest + script: | + cat > $(step.artifacts.path) << EOF + { + "inputs":[ + { + "name":"input-artifacts", + "values":[ + { + "uri":"git:jjjsss", + "digest":{ + "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0" + } + } + ] + } + ], + "outputs":[ + { + "name":"image", + "values":[ + { + "uri":"pkg:balba", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } + ] + } + ] + } + EOF + - name: artifacts-consumer + image: bash:latest + script: | + echo $(steps.artifacts-producer.outputs) + echo $(steps.artifacts-producer.inputs.input-artifacts) + - name: artifacts-consumer-python + image: python:latest + script: | + #!/usr/bin/env python3 + import json + data = json.loads('$(steps.artifacts-producer.outputs)') + print(data[0]['uri']) diff --git a/hack/ignored-openapi-violations.list b/hack/ignored-openapi-violations.list index 445aecc6365..b9f7edf52e2 100644 --- a/hack/ignored-openapi-violations.list +++ b/hack/ignored-openapi-violations.list @@ -57,3 +57,7 @@ API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipe API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1,Artifact,Values API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1,StepState,Inputs API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1,StepState,Outputs +API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1,Artifacts,Inputs +API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1,Artifacts,Outputs +API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1,Artifacts,Inputs +API rule violation: list_type_missing,github.com/tektoncd/pipeline/pkg/apis/pipeline/v1,Artifacts,Outputs diff --git a/internal/artifactref/artifactref.go b/internal/artifactref/artifactref.go new file mode 100644 index 00000000000..b5a46a73023 --- /dev/null +++ b/internal/artifactref/artifactref.go @@ -0,0 +1,11 @@ +package artifactref + +import "regexp" + +// case 1: steps..inputs +// case 2: steps..outputs +// case 3: steps..inputs. +// case 4: steps..outputs. +const stepArtifactUsagePattern = `\$\(steps\.([^.]+)\.(?:inputs|outputs)(?:\.([^.^\)]+))?\)` + +var StepArtifactRegex = regexp.MustCompile(stepArtifactUsagePattern) diff --git a/pkg/apis/pipeline/paths.go b/pkg/apis/pipeline/paths.go index 5522cb0741c..fb2b3bcf87c 100644 --- a/pkg/apis/pipeline/paths.go +++ b/pkg/apis/pipeline/paths.go @@ -28,4 +28,6 @@ const ( CredsDir = "/tekton/creds" // #nosec // StepsDir is the directory used for a step to store any metadata related to the step StepsDir = "/tekton/steps" + + ScriptDir = "/tekton/scripts" ) diff --git a/pkg/apis/pipeline/v1/artifact_types.go b/pkg/apis/pipeline/v1/artifact_types.go index c19cd4f94e0..07e43ebe171 100644 --- a/pkg/apis/pipeline/v1/artifact_types.go +++ b/pkg/apis/pipeline/v1/artifact_types.go @@ -19,3 +19,11 @@ type ArtifactValue struct { // TaskRunStepArtifact represents an artifact produced or used by a step within a task run. // It directly uses the Artifact type for its structure. type TaskRunStepArtifact = Artifact + +// Artifacts represents the collection of input and output artifacts associated with +// a task run or a similar process. Artifacts in this context are units of data or resources +// that the process either consumes as input or produces as output. +type Artifacts struct { + Inputs []Artifact `json:"inputs,omitempty"` + Outputs []Artifact `json:"outputs,omitempty"` +} diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go index b5e5f19fa0a..67a9508011b 100644 --- a/pkg/apis/pipeline/v1/openapi_generated.go +++ b/pkg/apis/pipeline/v1/openapi_generated.go @@ -34,6 +34,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod.Template": schema_pkg_apis_pipeline_pod_Template(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifact": schema_pkg_apis_pipeline_v1_Artifact(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.ArtifactValue": schema_pkg_apis_pipeline_v1_ArtifactValue(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifacts": schema_pkg_apis_pipeline_v1_Artifacts(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.ChildStatusReference": schema_pkg_apis_pipeline_v1_ChildStatusReference(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.EmbeddedTask": schema_pkg_apis_pipeline_v1_EmbeddedTask(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.IncludeParams": schema_pkg_apis_pipeline_v1_IncludeParams(ref), @@ -455,6 +456,47 @@ func schema_pkg_apis_pipeline_v1_ArtifactValue(ref common.ReferenceCallback) com } } +func schema_pkg_apis_pipeline_v1_Artifacts(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Artifacts represents the collection of input and output artifacts associated with a task run or a similar process. Artifacts in this context are units of data or resources that the process either consumes as input or produces as output.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "inputs": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifact"), + }, + }, + }, + }, + }, + "outputs": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifact"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.Artifact"}, + } +} + func schema_pkg_apis_pipeline_v1_ChildStatusReference(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json index 04538db544b..e014703014c 100644 --- a/pkg/apis/pipeline/v1/swagger.json +++ b/pkg/apis/pipeline/v1/swagger.json @@ -185,6 +185,26 @@ } } }, + "v1.Artifacts": { + "description": "Artifacts represents the collection of input and output artifacts associated with a task run or a similar process. Artifacts in this context are units of data or resources that the process either consumes as input or produces as output.", + "type": "object", + "properties": { + "inputs": { + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1.Artifact" + } + }, + "outputs": { + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1.Artifact" + } + } + } + }, "v1.ChildStatusReference": { "description": "ChildStatusReference is used to point to the statuses of individual TaskRuns and Runs within this PipelineRun.", "type": "object", diff --git a/pkg/apis/pipeline/v1/task_validation.go b/pkg/apis/pipeline/v1/task_validation.go index 5c7addf54fb..95708574409 100644 --- a/pkg/apis/pipeline/v1/task_validation.go +++ b/pkg/apis/pipeline/v1/task_validation.go @@ -25,11 +25,14 @@ import ( "strings" "time" + "github.com/tektoncd/pipeline/internal/artifactref" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/validate" "github.com/tektoncd/pipeline/pkg/internal/resultref" "github.com/tektoncd/pipeline/pkg/substitution" + + "golang.org/x/exp/slices" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" @@ -308,6 +311,45 @@ func errorIfStepResultReferenceinField(value, fieldName string) (errs *apis.Fiel return errs } +func stepArtifactReferenceExists(src string) bool { + return len(artifactref.StepArtifactRegex.FindAllStringSubmatch(src, -1)) > 0 || strings.Contains(src, "$(step.artifacts.path)") +} +func errorIfStepArtifactReferencedInField(value, fieldName string) (errs *apis.FieldError) { + if stepArtifactReferenceExists(value) { + errs = errs.Also(&apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{fieldName}, + }) + } + return errs +} + +func validateStepArtifactsReference(s Step) (errs *apis.FieldError) { + errs = errs.Also(errorIfStepArtifactReferencedInField(s.Name, "name")) + errs = errs.Also(errorIfStepArtifactReferencedInField(s.Image, "image")) + errs = errs.Also(errorIfStepArtifactReferencedInField(string(s.ImagePullPolicy), "imagePullPoliicy")) + errs = errs.Also(errorIfStepArtifactReferencedInField(s.WorkingDir, "workingDir")) + for _, e := range s.EnvFrom { + errs = errs.Also(errorIfStepArtifactReferencedInField(e.Prefix, "envFrom.prefix")) + if e.ConfigMapRef != nil { + errs = errs.Also(errorIfStepArtifactReferencedInField(e.ConfigMapRef.LocalObjectReference.Name, "envFrom.configMapRef")) + } + if e.SecretRef != nil { + errs = errs.Also(errorIfStepArtifactReferencedInField(e.SecretRef.LocalObjectReference.Name, "envFrom.secretRef")) + } + } + for _, v := range s.VolumeMounts { + errs = errs.Also(errorIfStepArtifactReferencedInField(v.Name, "volumeMounts.name")) + errs = errs.Also(errorIfStepArtifactReferencedInField(v.MountPath, "volumeMounts.mountPath")) + errs = errs.Also(errorIfStepArtifactReferencedInField(v.SubPath, "volumeMounts.subPath")) + } + for _, v := range s.VolumeDevices { + errs = errs.Also(errorIfStepArtifactReferencedInField(v.Name, "volumeDevices.name")) + errs = errs.Also(errorIfStepArtifactReferencedInField(v.DevicePath, "volumeDevices.devicePath")) + } + return errs +} + func validateStepResultReference(s Step) (errs *apis.FieldError) { errs = errs.Also(errorIfStepResultReferenceinField(s.Name, "name")) errs = errs.Also(errorIfStepResultReferenceinField(s.Image, "image")) @@ -336,6 +378,19 @@ func validateStepResultReference(s Step) (errs *apis.FieldError) { } func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.FieldError) { + if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableArtifacts { + var t []string + t = append(t, s.Script) + t = append(t, s.Command...) + t = append(t, s.Args...) + for _, e := range s.Env { + t = append(t, e.Value) + } + if slices.ContainsFunc(t, stepArtifactReferenceExists) { + return errs.Also(apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), "")) + } + } + if s.Ref != nil { if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdateAndDiverged(ctx, s) { return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to reference StepActions in Steps.", config.EnableStepActions), "") @@ -476,6 +531,10 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi // Validate usage of step result reference. // Referencing previous step's results are only allowed in `env`, `command` and `args`. errs = errs.Also(validateStepResultReference(s)) + + // Validate usage of step artifacts output reference + // Referencing previous step's results are only allowed in `env`, `command` and `args`, `script`. + errs = errs.Also(validateStepArtifactsReference(s)) return errs } diff --git a/pkg/apis/pipeline/v1/task_validation_test.go b/pkg/apis/pipeline/v1/task_validation_test.go index 77942ca1751..8f3c02b221a 100644 --- a/pkg/apis/pipeline/v1/task_validation_test.go +++ b/pkg/apis/pipeline/v1/task_validation_test.go @@ -1741,7 +1741,297 @@ func TestTaskSpecValidateErrorWithStepResultRef(t *testing.T) { }) } } +func TestTaskSpecValidateSuccessWithArtifactsRefFlagEnabled(t *testing.T) { + tests := []struct { + name string + Steps []v1.Step + }{ + { + name: "reference step artifacts in Env", + Steps: []v1.Step{{ + Image: "busybox", + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(steps.aaa.outputs)"}}, + }}, + }, + { + name: "reference step artifacts path in Env", + Steps: []v1.Step{{ + Image: "busybox", + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(step.artifacts.path)"}}, + }}, + }, + { + name: "reference step artifacts in Script", + Steps: []v1.Step{{ + Image: "busybox", + Script: "echo $(steps.aaa.inputs.bbb)", + }}, + }, + { + name: "reference step artifacts path in Script", + Steps: []v1.Step{{ + Image: "busybox", + Script: "echo 123 >> $(step.artifacts.path)", + }}, + }, + { + name: "reference step artifacts in Command", + Steps: []v1.Step{{ + Image: "busybox", + Command: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + }, + { + name: "reference step artifacts path in Command", + Steps: []v1.Step{{ + Image: "busybox", + Command: []string{"echo", "$(step.artifacts.path)"}, + }}, + }, + { + name: "reference step artifacts in Args", + Steps: []v1.Step{{ + Image: "busybox", + Args: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + }, + { + name: "reference step artifacts path in Args", + Steps: []v1.Step{{ + Image: "busybox", + Args: []string{"echo", "$(step.artifacts.path)"}, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := v1.TaskSpec{ + Steps: tt.Steps, + } + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableStepActions: true, + EnableArtifacts: true, + }, + }) + ctx = apis.WithinCreate(ctx) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if err != nil { + t.Fatalf("Expected no errors, got err for %v", err) + } + }) + } +} +func TestTaskSpecValidateErrorWithArtifactsRefFlagNotEnabled(t *testing.T) { + tests := []struct { + name string + Steps []v1.Step + expectedError apis.FieldError + }{ + { + name: "Cannot reference step artifacts in Env without setting enable-artifacts to true", + Steps: []v1.Step{{ + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(steps.aaa.outputs)"}}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in Env without setting enable-artifacts to true", + Steps: []v1.Step{{ + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(step.artifacts.path)"}}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts in Script without setting enable-artifacts to true", + Steps: []v1.Step{{ + Script: "echo $(steps.aaa.inputs.bbb)", + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in Script without setting enable-artifacts to true", + Steps: []v1.Step{{ + Script: "echo 123 >> $(step.artifacts.path)", + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts in Command without setting enable-artifacts to true", + Steps: []v1.Step{{ + Command: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in Command without setting enable-artifacts to true", + Steps: []v1.Step{{ + Command: []string{"echo", "$(step.artifacts.path)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts in Args without setting enable-artifacts to true", + Steps: []v1.Step{{ + Args: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in Args without setting enable-artifacts to true", + Steps: []v1.Step{{ + Args: []string{"echo", "$(step.artifacts.path)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := v1.TaskSpec{ + Steps: tt.Steps, + } + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableStepActions: true, + }, + }) + ctx = apis.WithinCreate(ctx) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if err == nil { + t.Fatalf("Expected an error, got nothing for %v", ts) + } + if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" { + t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d)) + } + }) + } +} +func TestTaskSpecValidateErrorWithArtifactsRef(t *testing.T) { + tests := []struct { + name string + Steps []v1.Step + expectedError apis.FieldError + }{{ + name: "Cannot reference step artifacts in image", + Steps: []v1.Step{{ + Image: "$(steps.prevStep.outputs.aaa)", + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].image"}, + }, + }, + { + name: "Cannot reference step artifacts in workingDir", + Steps: []v1.Step{{ + Image: "my-img", + WorkingDir: "$(steps.prevStep.outputs.aaa)", + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].workingDir"}, + }, + }, + { + name: "Cannot reference step artifacts in envFrom", + Steps: []v1.Step{{ + Image: "my-img", + EnvFrom: []corev1.EnvFromSource{{ + Prefix: "$(steps.prevStep.outputs.aaa)", + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(steps.prevStep.outputs.aaa)", + }, + }, + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(steps.prevStep.outputs.aaa)", + }, + }, + }}, + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].envFrom.configMapRef", "steps[0].envFrom.prefix", "steps[0].envFrom.secretRef"}, + }, + }, { + name: "Cannot reference step artifacts in VolumeMounts", + Steps: []v1.Step{{ + Image: "my-img", + VolumeMounts: []corev1.VolumeMount{{ + Name: "$(steps.prevStep.outputs.aaa)", + MountPath: "$(steps.prevStep.outputs.aaa)", + SubPath: "$(steps.prevStep.outputs.aaa)", + }}, + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].volumeMounts.name", "steps[0].volumeMounts.mountPath", "steps[0].volumeMounts.subPath"}, + }, + }, { + name: "Cannot reference step artifacts in VolumeDevices", + Steps: []v1.Step{{ + Image: "my-img", + VolumeDevices: []corev1.VolumeDevice{{ + Name: "$(steps.prevStep.outputs.aaa)", + DevicePath: "$(steps.prevStep.outputs.aaa)", + }}, + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].volumeDevices.name", "steps[0].volumeDevices.devicePath"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := v1.TaskSpec{ + Steps: tt.Steps, + } + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableStepActions: true, + }, + }) + ctx = apis.WithinCreate(ctx) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if err == nil { + t.Fatalf("Expected an error, got nothing for %v", ts) + } + if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" { + t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d)) + } + }) + } +} func TestTaskSpecValidateErrorSidecars(t *testing.T) { tests := []struct { name string diff --git a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go index 50bbf2ba4dd..16e483fcec1 100644 --- a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go @@ -76,6 +76,36 @@ func (in *ArtifactValue) DeepCopy() *ArtifactValue { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Artifacts) DeepCopyInto(out *Artifacts) { + *out = *in + if in.Inputs != nil { + in, out := &in.Inputs, &out.Inputs + *out = make([]Artifact, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Outputs != nil { + in, out := &in.Outputs, &out.Outputs + *out = make([]Artifact, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Artifacts. +func (in *Artifacts) DeepCopy() *Artifacts { + if in == nil { + return nil + } + out := new(Artifacts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ChildStatusReference) DeepCopyInto(out *ChildStatusReference) { *out = *in diff --git a/pkg/apis/pipeline/v1beta1/artifact_types.go b/pkg/apis/pipeline/v1beta1/artifact_types.go index c10ed98e5fe..ec50bde8a16 100644 --- a/pkg/apis/pipeline/v1beta1/artifact_types.go +++ b/pkg/apis/pipeline/v1beta1/artifact_types.go @@ -19,3 +19,11 @@ type ArtifactValue struct { // TaskRunStepArtifact represents an artifact produced or used by a step within a task run. // It directly uses the Artifact type for its structure. type TaskRunStepArtifact = Artifact + +// Artifacts represents the collection of input and output artifacts associated with +// a task run or a similar process. Artifacts in this context are units of data or resources +// that the process either consumes as input or produces as output. +type Artifacts struct { + Inputs []Artifact `json:"inputs,omitempty"` + Outputs []Artifact `json:"outputs,omitempty"` +} diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go index 75817a84c3a..72277eb914e 100644 --- a/pkg/apis/pipeline/v1beta1/openapi_generated.go +++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go @@ -34,6 +34,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod.Template": schema_pkg_apis_pipeline_pod_Template(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Artifact": schema_pkg_apis_pipeline_v1beta1_Artifact(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.ArtifactValue": schema_pkg_apis_pipeline_v1beta1_ArtifactValue(ref), + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Artifacts": schema_pkg_apis_pipeline_v1beta1_Artifacts(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.ChildStatusReference": schema_pkg_apis_pipeline_v1beta1_ChildStatusReference(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.CloudEventDelivery": schema_pkg_apis_pipeline_v1beta1_CloudEventDelivery(ref), "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.CloudEventDeliveryState": schema_pkg_apis_pipeline_v1beta1_CloudEventDeliveryState(ref), @@ -479,6 +480,47 @@ func schema_pkg_apis_pipeline_v1beta1_ArtifactValue(ref common.ReferenceCallback } } +func schema_pkg_apis_pipeline_v1beta1_Artifacts(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Artifacts represents the collection of input and output artifacts associated with a task run or a similar process. Artifacts in this context are units of data or resources that the process either consumes as input or produces as output.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "inputs": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Artifact"), + }, + }, + }, + }, + }, + "outputs": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Artifact"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1.Artifact"}, + } +} + func schema_pkg_apis_pipeline_v1beta1_ChildStatusReference(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json index 00322afb0e1..4fd2b41fd3f 100644 --- a/pkg/apis/pipeline/v1beta1/swagger.json +++ b/pkg/apis/pipeline/v1beta1/swagger.json @@ -185,6 +185,26 @@ } } }, + "v1beta1.Artifacts": { + "description": "Artifacts represents the collection of input and output artifacts associated with a task run or a similar process. Artifacts in this context are units of data or resources that the process either consumes as input or produces as output.", + "type": "object", + "properties": { + "inputs": { + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1beta1.Artifact" + } + }, + "outputs": { + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/v1beta1.Artifact" + } + } + } + }, "v1beta1.ChildStatusReference": { "description": "ChildStatusReference is used to point to the statuses of individual TaskRuns and Runs within this PipelineRun.", "type": "object", diff --git a/pkg/apis/pipeline/v1beta1/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go index 3b1fd1f7b31..9ef0db6909a 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation.go +++ b/pkg/apis/pipeline/v1beta1/task_validation.go @@ -25,12 +25,15 @@ import ( "strings" "time" + "github.com/tektoncd/pipeline/internal/artifactref" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/validate" "github.com/tektoncd/pipeline/pkg/internal/resultref" "github.com/tektoncd/pipeline/pkg/substitution" + + "golang.org/x/exp/slices" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" @@ -297,6 +300,46 @@ func errorIfStepResultReferenceinField(value, fieldName string) (errs *apis.Fiel return errs } +func stepArtifactReferenceExists(src string) bool { + return len(artifactref.StepArtifactRegex.FindAllStringSubmatch(src, -1)) > 0 || strings.Contains(src, "$(step.artifacts.path)") +} + +func errorIfStepArtifactReferencedInField(value, fieldName string) (errs *apis.FieldError) { + if stepArtifactReferenceExists(value) { + errs = errs.Also(&apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{fieldName}, + }) + } + return errs +} + +func validateStepArtifactsReference(s Step) (errs *apis.FieldError) { + errs = errs.Also(errorIfStepArtifactReferencedInField(s.Name, "name")) + errs = errs.Also(errorIfStepArtifactReferencedInField(s.Image, "image")) + errs = errs.Also(errorIfStepArtifactReferencedInField(string(s.ImagePullPolicy), "imagePullPoliicy")) + errs = errs.Also(errorIfStepArtifactReferencedInField(s.WorkingDir, "workingDir")) + for _, e := range s.EnvFrom { + errs = errs.Also(errorIfStepArtifactReferencedInField(e.Prefix, "envFrom.prefix")) + if e.ConfigMapRef != nil { + errs = errs.Also(errorIfStepArtifactReferencedInField(e.ConfigMapRef.LocalObjectReference.Name, "envFrom.configMapRef")) + } + if e.SecretRef != nil { + errs = errs.Also(errorIfStepArtifactReferencedInField(e.SecretRef.LocalObjectReference.Name, "envFrom.secretRef")) + } + } + for _, v := range s.VolumeMounts { + errs = errs.Also(errorIfStepArtifactReferencedInField(v.Name, "volumeMounts.name")) + errs = errs.Also(errorIfStepArtifactReferencedInField(v.MountPath, "volumeMounts.mountPath")) + errs = errs.Also(errorIfStepArtifactReferencedInField(v.SubPath, "volumeMounts.subPath")) + } + for _, v := range s.VolumeDevices { + errs = errs.Also(errorIfStepArtifactReferencedInField(v.Name, "volumeDevices.name")) + errs = errs.Also(errorIfStepArtifactReferencedInField(v.DevicePath, "volumeDevices.devicePath")) + } + return errs +} + func validateStepResultReference(s Step) (errs *apis.FieldError) { errs = errs.Also(errorIfStepResultReferenceinField(s.Name, "name")) errs = errs.Also(errorIfStepResultReferenceinField(s.Image, "image")) @@ -325,6 +368,19 @@ func validateStepResultReference(s Step) (errs *apis.FieldError) { } func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.FieldError) { + if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableArtifacts { + var t []string + t = append(t, s.Script) + t = append(t, s.Command...) + t = append(t, s.Args...) + for _, e := range s.Env { + t = append(t, e.Value) + } + if slices.ContainsFunc(t, stepArtifactReferenceExists) { + return errs.Also(apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), "")) + } + } + if s.Ref != nil { if !config.FromContextOrDefaults(ctx).FeatureFlags.EnableStepActions && isCreateOrUpdateAndDiverged(ctx, s) { return apis.ErrGeneric(fmt.Sprintf("feature flag %s should be set to true to reference StepActions in Steps.", config.EnableStepActions), "") @@ -465,6 +521,11 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi // Validate usage of step result reference. // Referencing previous step's results are only allowed in `env`, `command` and `args`. errs = errs.Also(validateStepResultReference(s)) + + // Validate usage of step artifacts output reference + // Referencing previous step's results are only allowed in `env`, `command` and `args`, `script`. + errs = errs.Also(validateStepArtifactsReference(s)) + return errs } diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go index 02694dc5418..4f36224b78e 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go @@ -2586,3 +2586,295 @@ func TestTaskSpecValidate_StepResults_Error(t *testing.T) { }) } } + +func TestTaskSpecValidateSuccessWithArtifactsRefFlagEnabled(t *testing.T) { + tests := []struct { + name string + Steps []v1beta1.Step + }{ + { + name: "reference step artifacts in Env", + Steps: []v1beta1.Step{{ + Image: "busybox", + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(steps.aaa.outputs)"}}, + }}, + }, + { + name: "reference step artifacts path in Env", + Steps: []v1beta1.Step{{ + Image: "busybox", + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(step.artifacts.path)"}}, + }}, + }, + { + name: "reference step artifacts in Script", + Steps: []v1beta1.Step{{ + Image: "busybox", + Script: "echo $(steps.aaa.inputs.bbb)", + }}, + }, + { + name: "reference step artifacts path in Script", + Steps: []v1beta1.Step{{ + Image: "busybox", + Script: "echo 123 >> $(step.artifacts.path)", + }}, + }, + { + name: "reference step artifacts in Command", + Steps: []v1beta1.Step{{ + Image: "busybox", + Command: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + }, + { + name: "reference step artifacts path in Command", + Steps: []v1beta1.Step{{ + Image: "busybox", + Command: []string{"echo", "$(step.artifacts.path)"}, + }}, + }, + { + name: "reference step artifacts in Args", + Steps: []v1beta1.Step{{ + Image: "busybox", + Args: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + }, + { + name: "reference step artifacts path in Args", + Steps: []v1beta1.Step{{ + Image: "busybox", + Args: []string{"echo", "$(step.artifacts.path)"}, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := v1beta1.TaskSpec{ + Steps: tt.Steps, + } + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableStepActions: true, + EnableArtifacts: true, + }, + }) + ctx = apis.WithinCreate(ctx) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if err != nil { + t.Fatalf("Expected no errors, got err for %v", err) + } + }) + } +} +func TestTaskSpecValidateErrorWithArtifactsRefFlagNotEnabled(t *testing.T) { + tests := []struct { + name string + Steps []v1beta1.Step + expectedError apis.FieldError + }{ + { + name: "Cannot reference step artifacts in Env without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(steps.aaa.outputs)"}}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in Env without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Env: []corev1.EnvVar{{Name: "AAA", Value: "$(step.artifacts.path)"}}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts in Script without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Script: "echo $(steps.aaa.inputs.bbb)", + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in Script without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Script: "echo 123 >> $(step.artifacts.path)", + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts in Command without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Command: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in Command without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Command: []string{"echo", "$(step.artifacts.path)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts in Args without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Args: []string{"echo", "$(steps.aaa.outputs.bbbb)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + { + name: "Cannot reference step artifacts path in CoArgsmmand without setting enable-artifacts to true", + Steps: []v1beta1.Step{{ + Args: []string{"echo", "$(step.artifacts.path)"}, + }}, + expectedError: apis.FieldError{ + Message: fmt.Sprintf("feature flag %s should be set to true to use artifacts feature.", config.EnableArtifacts), + Paths: []string{"steps[0]"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := v1beta1.TaskSpec{ + Steps: tt.Steps, + } + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableStepActions: true, + }, + }) + ctx = apis.WithinCreate(ctx) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if err == nil { + t.Fatalf("Expected an error, got nothing for %v", ts) + } + if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" { + t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestTaskSpecValidateErrorWithArtifactsRef(t *testing.T) { + tests := []struct { + name string + Steps []v1beta1.Step + expectedError apis.FieldError + }{{ + name: "Cannot reference step artifacts in image", + Steps: []v1beta1.Step{{ + Image: "$(steps.prevStep.outputs.aaa)", + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].image"}, + }, + }, + { + name: "Cannot reference step artifacts in workingDir", + Steps: []v1beta1.Step{{ + Image: "my-img", + WorkingDir: "$(steps.prevStep.outputs.aaa)", + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].workingDir"}, + }, + }, + { + name: "Cannot reference step artifacts in envFrom", + Steps: []v1beta1.Step{{ + Image: "my-img", + EnvFrom: []corev1.EnvFromSource{{ + Prefix: "$(steps.prevStep.outputs.aaa)", + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(steps.prevStep.outputs.aaa)", + }, + }, + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "$(steps.prevStep.outputs.aaa)", + }, + }, + }}, + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].envFrom.configMapRef", "steps[0].envFrom.prefix", "steps[0].envFrom.secretRef"}, + }, + }, { + name: "Cannot reference step artifacts in VolumeMounts", + Steps: []v1beta1.Step{{ + Image: "my-img", + VolumeMounts: []corev1.VolumeMount{{ + Name: "$(steps.prevStep.outputs.aaa)", + MountPath: "$(steps.prevStep.outputs.aaa)", + SubPath: "$(steps.prevStep.outputs.aaa)", + }}, + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].volumeMounts.name", "steps[0].volumeMounts.mountPath", "steps[0].volumeMounts.subPath"}, + }, + }, { + name: "Cannot reference step artifacts in VolumeDevices", + Steps: []v1beta1.Step{{ + Image: "my-img", + VolumeDevices: []corev1.VolumeDevice{{ + Name: "$(steps.prevStep.outputs.aaa)", + DevicePath: "$(steps.prevStep.outputs.aaa)", + }}, + }}, + expectedError: apis.FieldError{ + Message: "stepArtifact substitutions are only allowed in env, command, args and script. Found usage in", + Paths: []string{"steps[0].volumeDevices.name", "steps[0].volumeDevices.devicePath"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := v1beta1.TaskSpec{ + Steps: tt.Steps, + } + ctx := config.ToContext(context.Background(), &config.Config{ + FeatureFlags: &config.FeatureFlags{ + EnableStepActions: true, + }, + }) + ctx = apis.WithinCreate(ctx) + ts.SetDefaults(ctx) + err := ts.Validate(ctx) + if err == nil { + t.Fatalf("Expected an error, got nothing for %v", ts) + } + if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" { + t.Errorf("TaskSpec.Validate() errors diff %s", diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go index a1f56b93e11..807595a1925 100644 --- a/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go @@ -79,6 +79,36 @@ func (in *ArtifactValue) DeepCopy() *ArtifactValue { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Artifacts) DeepCopyInto(out *Artifacts) { + *out = *in + if in.Inputs != nil { + in, out := &in.Inputs, &out.Inputs + *out = make([]Artifact, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Outputs != nil { + in, out := &in.Outputs, &out.Outputs + *out = make([]Artifact, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Artifacts. +func (in *Artifacts) DeepCopy() *Artifacts { + if in == nil { + return nil + } + out := new(Artifacts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ChildStatusReference) DeepCopyInto(out *ChildStatusReference) { *out = *in diff --git a/pkg/entrypoint/entrypointer.go b/pkg/entrypoint/entrypointer.go index 0be35256b77..4eaf6b07c45 100644 --- a/pkg/entrypoint/entrypointer.go +++ b/pkg/entrypoint/entrypointer.go @@ -18,17 +18,20 @@ package entrypoint import ( "context" + "encoding/json" "errors" "fmt" "log" "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "syscall" "time" + "github.com/tektoncd/pipeline/internal/artifactref" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" @@ -47,6 +50,9 @@ const ( FailOnError = "stopAndFail" ) +// ScriptDir for testing +var ScriptDir = pipeline.ScriptDir + // ContextError context error type type ContextError string @@ -162,6 +168,9 @@ func (e Entrypointer) Go() error { if err := os.MkdirAll(filepath.Join(e.StepMetadataDir, "results"), os.ModePerm); err != nil { return err } + if err := os.MkdirAll(filepath.Join(e.StepMetadataDir, "artifacts"), os.ModePerm); err != nil { + return err + } for _, f := range e.WaitFiles { if err := e.Waiter.Wait(context.Background(), f, e.WaitFileContent, e.BreakpointOnFailure); err != nil { // An error happened while waiting, so we bail @@ -200,6 +209,10 @@ func (e Entrypointer) Go() error { if err := e.applyStepResultSubstitutions(pipeline.StepsDir); err != nil { logger.Error("Error while substituting step results: ", err) } + if err := e.applyStepArtifactSubstitutions(pipeline.StepsDir); err != nil { + logger.Error("Error while substituting step artifacts: ", err) + } + ctx, cancel = context.WithCancel(ctx) if e.Timeout != nil && *e.Timeout > time.Duration(0) { ctx, cancel = context.WithTimeout(ctx, *e.Timeout) @@ -266,9 +279,30 @@ func (e Entrypointer) Go() error { } } + if e.ResultExtractionMethod == config.ResultExtractionMethodTerminationMessage { + fp := filepath.Join(e.StepMetadataDir, "artifacts", "provenance.json") + + artifacts, err := readArtifacts(fp) + if err != nil { + logger.Fatalf("Error while handling artifacts: %s", err) + } + output = append(output, artifacts...) + } + return err } +func readArtifacts(fp string) ([]result.RunResult, error) { + file, err := os.ReadFile(fp) + if os.IsNotExist(err) { + return []result.RunResult{}, nil + } + if err != nil { + return nil, err + } + return []result.RunResult{{Key: fp, Value: string(file), ResultType: result.ArtifactsResultType}}, nil +} + func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string, resultType result.ResultType) error { output := []result.RunResult{} results := e.Results @@ -467,3 +501,189 @@ func (e Entrypointer) outputRunResult(terminationReason string) result.RunResult ResultType: result.InternalTektonResultType, } } + +// getStepArtifactsPath gets the path to the step artifacts +func getStepArtifactsPath(stepDir string, containerName string) string { + return filepath.Join(stepDir, containerName, "artifacts", "provenance.json") +} + +// loadStepArtifacts loads and parses the artifacts file for a specified step. +func loadStepArtifacts(stepDir string, containerName string) (v1.Artifacts, error) { + v := v1.Artifacts{} + fp := getStepArtifactsPath(stepDir, containerName) + + fileContents, err := os.ReadFile(fp) + if err != nil { + return v, err + } + err = json.Unmarshal(fileContents, &v) + if err != nil { + return v, err + } + return v, nil +} + +// getArtifactValues retrieves the values associated with a specified artifact reference. +// It parses the provided artifact template, loads the corresponding step's artifacts, and extracts the relevant values. +// If the artifact name is not specified in the template, the values of the first output are returned. +func getArtifactValues(dir string, template string) (string, error) { + artifactTemplate, err := parseArtifactTemplate(template) + + if err != nil { + return "", err + } + + artifacts, err := loadStepArtifacts(dir, artifactTemplate.ContainerName) + if err != nil { + return "", err + } + + // $(steps.stepName.outputs.artifactName) <- artifacts.Output[artifactName].Values + // $(steps.stepName.outputs) <- artifacts.Output[0].Values + var t []v1.Artifact + if artifactTemplate.Type == "outputs" { + t = artifacts.Outputs + } else { + t = artifacts.Inputs + } + + if artifactTemplate.ArtifactName == "" { + marshal, err := json.Marshal(t[0].Values) + if err != nil { + return "", err + } + return string(marshal), err + } + for _, ar := range t { + if ar.Name == artifactTemplate.ArtifactName { + marshal, err := json.Marshal(ar.Values) + if err != nil { + return "", err + } + return string(marshal), err + } + } + return "", fmt.Errorf("values for template %s not found", template) +} + +// parseArtifactTemplate parses an artifact template string and extracts relevant information into an ArtifactTemplate struct. +// +// The artifact template is expected to be in the format "$(steps.{step-name}.outputs.{artifact-name})" or "$(steps.{step-name}.outputs)". +func parseArtifactTemplate(template string) (ArtifactTemplate, error) { + if template == "" { + return ArtifactTemplate{}, fmt.Errorf("template is empty") + } + if artifactref.StepArtifactRegex.FindString(template) != template { + return ArtifactTemplate{}, fmt.Errorf("invalid artifact template %s", template) + } + template = strings.TrimSuffix(strings.TrimPrefix(template, "$("), ")") + split := strings.Split(template, ".") + at := ArtifactTemplate{ + ContainerName: fmt.Sprintf("step-%s", split[1]), + Type: split[2], + } + if len(split) == 4 { + at.ArtifactName = split[3] + } + return at, nil +} + +// ArtifactTemplate holds steps artifacts metadata parsed from step artifacts interpolation +type ArtifactTemplate struct { + ContainerName string + Type string // inputs or outputs + ArtifactName string +} + +// applyStepArtifactSubstitutions replaces artifact references within a step's command and environment variables with their corresponding values. +// +// This function is designed to handle artifact substitutions in a script file, inline command, or environment variables. +// +// Args: +// +// stepDir: The directory of the executing step. +// +// Returns: +// +// An error object if any issues occur during substitution. +func (e *Entrypointer) applyStepArtifactSubstitutions(stepDir string) error { + // Script was re-written into a file, we need to read the file to and substitute the content + // and re-write the command. + // While param substitution cannot be used in Script from StepAction, allowing artifact substitution doesn't seem bad as + // artifacts are unmarshalled, should be safe. + if len(e.Command) == 1 && filepath.Dir(e.Command[0]) == filepath.Clean(ScriptDir) { + dataBytes, err := os.ReadFile(e.Command[0]) + if err != nil { + return err + } + fileContent := string(dataBytes) + v, err := replaceValue(artifactref.StepArtifactRegex, fileContent, stepDir, getArtifactValues) + if err != nil { + return err + } + if v != fileContent { + temp, err := writeToTempFile(v) + if err != nil { + return err + } + e.Command = []string{temp.Name()} + } + } else { + command := e.Command + var newCmd []string + for _, c := range command { + v, err := replaceValue(artifactref.StepArtifactRegex, c, stepDir, getArtifactValues) + if err != nil { + return err + } + newCmd = append(newCmd, v) + } + e.Command = newCmd + } + + // substitute env + for _, e := range os.Environ() { + pair := strings.SplitN(e, "=", 2) + v, err := replaceValue(artifactref.StepArtifactRegex, pair[1], stepDir, getArtifactValues) + + if err != nil { + return err + } + os.Setenv(pair[0], v) + } + + return nil +} + +func writeToTempFile(v string) (*os.File, error) { + tmp, err := os.CreateTemp("", "script-*") + if err != nil { + return nil, err + } + err = os.Chmod(tmp.Name(), 0o755) + if err != nil { + return nil, err + } + _, err = tmp.WriteString(v) + if err != nil { + return nil, err + } + err = tmp.Close() + if err != nil { + return nil, err + } + return tmp, nil +} + +func replaceValue(regex *regexp.Regexp, src string, stepDir string, getValue func(string, string) (string, error)) (string, error) { + matches := regex.FindAllStringSubmatch(src, -1) + t := src + for _, m := range matches { + v, err := getValue(stepDir, m[0]) + if err != nil { + return "", err + } + t = strings.ReplaceAll(t, m[0], v) + } + return t, nil +} diff --git a/pkg/entrypoint/entrypointer_test.go b/pkg/entrypoint/entrypointer_test.go index 36eb014e8d5..a417238dd7f 100644 --- a/pkg/entrypoint/entrypointer_test.go +++ b/pkg/entrypoint/entrypointer_test.go @@ -33,6 +33,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tektoncd/pipeline/pkg/apis/config" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/pod" "github.com/tektoncd/pipeline/pkg/result" @@ -195,6 +196,10 @@ func TestEntrypointer(t *testing.T) { if err != nil { t.Fatalf("Entrypointer failed: %v", err) } + _, err = os.Stat(filepath.Join(c.stepDir, "artifacts")) + if err != nil { + t.Fatalf("fail to stat artifacts dir: %v", err) + } if len(c.waitFiles) > 0 { if fw.waited == nil { @@ -1066,6 +1071,604 @@ func TestTerminationReason(t *testing.T) { } } +func TestReadArtifactsFileDoesNotExist(t *testing.T) { + t.Run("readArtifact file doesn't exist, empty result, no error.", func(t *testing.T) { + dir := createTmpDir(t, "") + fp := filepath.Join(dir, "provenance.json") + got, err := readArtifacts(fp) + + if err != nil { + t.Fatalf("Did not expect and error but got: %v", err) + } + + want := []result.RunResult{} + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("artifacts don't match %s", diff.PrintWantGot(d)) + } + }) +} + +func TestReadArtifactsFileExistNoError(t *testing.T) { + t.Run("readArtifact file exist", func(t *testing.T) { + dir := createTmpDir(t, "") + fp := filepath.Join(dir, "provenance.json") + err := os.WriteFile(fp, []byte{}, 0755) + if err != nil { + t.Fatalf("Did not expect and error but got: %v", err) + } + got, err := readArtifacts(fp) + + if err != nil { + t.Fatalf("Did not expect and error but got: %v", err) + } + + want := []result.RunResult{{Key: fp, Value: "", ResultType: 5}} + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("artifacts don't match %s", diff.PrintWantGot(d)) + } + }) +} + +func TestReadArtifactsFileExistReadError(t *testing.T) { + t.Run("readArtifact file exist", func(t *testing.T) { + if os.Getuid() == 0 { + t.Skipf("Test doesn't work when running with root") + } + dir := createTmpDir(t, "") + fp := filepath.Join(dir, "provenance.json") + err := os.WriteFile(fp, []byte{}, 0000) + if err != nil { + t.Fatalf("Did not expect and error but got: %v", err) + } + got, err := readArtifacts(fp) + + if err == nil { + t.Fatalf("expecting error but got nil") + } + + var want []result.RunResult + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("artifacts don't match %s", diff.PrintWantGot(d)) + } + }) +} + +func TestGetStepArtifactsPath(t *testing.T) { + t.Run("test get step artifacts path", func(t *testing.T) { + got := getStepArtifactsPath("a", "b") + want := "a/b/artifacts/provenance.json" + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("path doesn't match %s", diff.PrintWantGot(d)) + } + }) +} + +func TestLoadStepArtifacts(t *testing.T) { + tests := []struct { + desc string + wantErr bool + want v1.Artifacts + fileContent string + mode os.FileMode + }{ + { + desc: "read artifact success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: v1.Artifacts{ + Inputs: []v1.Artifact{{Name: "inputs", Values: []v1.ArtifactValue{{ + Digest: map[v1.Algorithm]string{"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"}, + Uri: "pkg:example.github.com/inputs", + }}}}, + Outputs: []v1.Artifact{{Name: "output", Values: []v1.ArtifactValue{{ + Digest: map[v1.Algorithm]string{"sha256": "64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"}, + Uri: "docker:example.registry.com/outputs", + }}}}, + }, + mode: 0755, + }, + { + desc: "read artifact file doesn't exist, error", + want: v1.Artifacts{}, + wantErr: true, + }, + { + desc: "read artifact, mal-formatted json, error", + fileContent: `{\\`, + mode: 0755, + wantErr: true, + }, + { + desc: "read artifact, file cannot be read, error", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + mode: 0000, + wantErr: true, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + if tc.mode == 0000 && os.Getuid() == 0 { + t.Skipf("Test doesn't work when running with root") + } + dir := createTmpDir(t, "") + name := "step-name" + artifactsPath := getStepArtifactsPath(dir, name) + if tc.fileContent != "" { + err := os.MkdirAll(filepath.Dir(artifactsPath), 0755) + if err != nil { + t.Fatalf("fail to create dir %v", err) + } + err = os.WriteFile(artifactsPath, []byte(tc.fileContent), tc.mode) + if err != nil { + return + } + } + got, err := loadStepArtifacts(dir, name) + + if tc.wantErr != (err != nil) { + t.Fatalf("Error checking failed %v", err) + } + if d := cmp.Diff(tc.want, got); d != "" { + t.Fatalf("artifacts don't match %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestParseArtifactTemplate(t *testing.T) { + tests := []struct { + desc string + input string + want ArtifactTemplate + wantErr bool + }{ + { + desc: "valid outputs template with artifact name", + input: "$(steps.name.outputs.aaa)", + want: ArtifactTemplate{ + ContainerName: "step-name", + Type: "outputs", + ArtifactName: "aaa", + }, + }, + { + desc: "valid outputs template without artifact name", + input: "$(steps.name.outputs)", + want: ArtifactTemplate{ + Type: "outputs", + ContainerName: "step-name", + }, + }, + { + desc: "valid inputs template with artifact name", + input: "$(steps.name.inputs.aaa)", + want: ArtifactTemplate{ + ContainerName: "step-name", + Type: "inputs", + ArtifactName: "aaa", + }, + }, + { + desc: "valid outputs template without artifact name", + input: "$(steps.name.inputs)", + want: ArtifactTemplate{ + Type: "inputs", + ContainerName: "step-name", + }, + }, + { + desc: "invalid template without artifact name, no prefix and suffix", + input: "steps.name.outputs", + wantErr: true, + }, + { + desc: "invalid template with artifact name, no prefix and suffix", + input: "steps.name.outputs.aaa", + wantErr: true, + }, + { + desc: "invalid template with 5 segments", + input: "$(steps.name.outputs.aaa.sss)", + wantErr: true, + }, + { + desc: "invalid template with 2 segments", + input: "$(steps.name)", + wantErr: true, + }, + { + desc: "invalid template concatenated with valid template", + input: "aaa$(steps.name.outputs.aaa)", + wantErr: true, + }, + { + desc: "invalid template segment 3 is not correct", + input: "$(steps.name.xxxx.aaa)", + wantErr: true, + }, + { + desc: "invalid template -- two valid template concatenation", + input: "$(steps.name.outputs.aaa)$(steps.name.outputs.aaa)", + wantErr: true, + }, + { + desc: "invalid template -- empty", + input: "", + wantErr: true, + }, + { + desc: "invalid template -- extra )", + input: "$(steps.name.outputs.aaa))", + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + got, err := parseArtifactTemplate(tc.input) + if tc.wantErr != (err != nil) { + t.Fatalf("Error checking failed %v", err) + } + if d := cmp.Diff(tc.want, got); d != "" { + t.Fatalf("ArtifactTemplate doesn't match %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestGetArtifactValues(t *testing.T) { + name := "name" + + tests := []struct { + desc string + wantErr bool + want string + fileContent string + mode os.FileMode + template string + }{ + { + desc: "read outputs artifact without artifact name, success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: `[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.outputs)", name), + }, + { + desc: "read inputs artifact without artifact name, success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: `[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.inputs)", name), + }, + { + desc: "read outputs artifact without artifact name, multiple outputs, default to first", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]},{"name":"output2","values":[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/outputs"}]}]}`, + want: `[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.outputs)", name), + }, + { + desc: "read inputs artifact without artifact name, multiple outputs, default to first", + fileContent: `{"outputs":[{"name":"out","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"inputs":[{"name":"in","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/inputs"}]},{"name":"in2","values":[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/inputs"}]}]}`, + want: `[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/inputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.inputs)", name), + }, + { + desc: "read outputs artifact with artifact name, success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: `[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.outputs.output)", name), + }, + { + desc: "read inputs artifact with artifact name, success", + fileContent: `{"outputs":[{"name":"outputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/outputs"}]}],"inputs":[{"name":"input","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/inputs"}]}]}`, + want: `[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/inputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.inputs.input)", name), + }, + { + desc: "read outputs artifact with artifact name, multiple outputs, success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]},{"name":"output2","values":[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/outputs"}]}]}`, + want: `[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/outputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.outputs.output2)", name), + }, + { + desc: "read inputs artifact with artifact name, multiple inputs, success", + fileContent: `{"outputs":[{"name":"outputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/outputs"}]}],"inputs":[{"name":"input","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/inputs"}]},{"name":"input2","values":[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/inputs"}]}]}`, + want: `[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/inputs"}]`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.inputs.input2)", name), + }, + { + desc: "invalid template", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]},{"name":"output2","values":[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/outputs"}]}]}`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.outputs.output2.333)", name), + wantErr: true, + }, + { + desc: "fail to load artifacts", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]},{"name":"output2","values":[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/outputs"}]}]}`, + mode: 0000, + template: fmt.Sprintf("$(steps.%s.outputs.output2.333)", name), + wantErr: true, + }, + { + desc: "template not found", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]},{"name":"output2","values":[{"digest":{"sha256":"22222157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f13402222"},"uri":"docker2:example.registry.com/outputs"}]}]}`, + mode: 0755, + template: fmt.Sprintf("$(steps.%s.outputs.output3)", name), + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + if tc.mode == 0000 && os.Getuid() == 0 { + t.Skipf("Test doesn't work when running with root") + } + dir := createTmpDir(t, "") + artifactsPath := getStepArtifactsPath(dir, "step-"+name) + if tc.fileContent != "" { + err := os.MkdirAll(filepath.Dir(artifactsPath), 0755) + if err != nil { + t.Fatalf("fail to create dir %v", err) + } + err = os.WriteFile(artifactsPath, []byte(tc.fileContent), tc.mode) + if err != nil { + t.Fatalf("fail to write to file %v", err) + } + } + + got, err := getArtifactValues(dir, tc.template) + if tc.wantErr != (err != nil) { + t.Fatalf("Error checking failed %v", err) + } + + if d := cmp.Diff(tc.want, got); d != "" { + t.Fatalf("artifactValues don't match %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestApplyStepArtifactSubstitutionsCommandSuccess(t *testing.T) { + stepName := "name" + scriptDir := createTmpDir(t, "script") + cur := ScriptDir + ScriptDir = scriptDir + t.Cleanup(func() { + ScriptDir = cur + }) + + tests := []struct { + desc string + wantErr bool + want string + fileContent string + mode os.FileMode + scriptContent string + scriptFile string + command []string + }{ + { + desc: "apply substitution to command from script file, success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: `echo [{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]`, + mode: 0755, + scriptContent: fmt.Sprintf("echo $(steps.%s.outputs)", stepName), + scriptFile: filepath.Join(scriptDir, "foo.sh"), + command: []string{filepath.Join(scriptDir, "foo.sh")}, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + stepDir := createTmpDir(t, "") + artifactsPath := getStepArtifactsPath(stepDir, "step-"+stepName) + if tc.fileContent != "" { + err := os.MkdirAll(filepath.Dir(artifactsPath), 0755) + if err != nil { + t.Fatalf("fail to create stepDir %v", err) + } + err = os.WriteFile(artifactsPath, []byte(tc.fileContent), tc.mode) + if err != nil { + t.Fatalf("fail to write to file %v", err) + } + } + if tc.scriptContent != "" { + err := os.WriteFile(tc.scriptFile, []byte(tc.scriptContent), 0755) + if err != nil { + t.Fatalf("failed to write script to scriptFile %v", err) + } + } + e := Entrypointer{Command: tc.command} + err := e.applyStepArtifactSubstitutions(stepDir) + if tc.wantErr != (err != nil) { + t.Fatalf("Error checking failed %v", err) + } + got, err := os.ReadFile(e.Command[0]) + if err != nil { + t.Fatalf("faile to read replaced script file %v", err) + } + + if d := cmp.Diff(tc.want, string(got)); d != "" { + t.Fatalf("command doesn't match %s", diff.PrintWantGot(d)) + } + }) + } +} +func TestApplyStepArtifactSubstitutionsCommand(t *testing.T) { + stepName := "name" + scriptDir := createTmpDir(t, "script") + cur := ScriptDir + ScriptDir = scriptDir + t.Cleanup(func() { + ScriptDir = cur + }) + + tests := []struct { + desc string + wantErr bool + want []string + fileContent string + mode os.FileMode + scriptContent string + scriptFile string + command []string + }{ + { + desc: "apply substitution script, fail to read artifacts", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: []string{filepath.Join(scriptDir, "foo2.sh")}, + mode: 0000, + wantErr: true, + scriptContent: fmt.Sprintf("echo $(steps.%s.outputs)", stepName), + scriptFile: filepath.Join(scriptDir, "foo2.sh"), + command: []string{filepath.Join(scriptDir, "foo2.sh")}, + }, + { + desc: "apply substitution to command from script file , no matches success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: []string{filepath.Join(scriptDir, "bar.sh")}, + mode: 0755, + scriptContent: "echo 123", + scriptFile: filepath.Join(scriptDir, "bar.sh"), + command: []string{filepath.Join(scriptDir, "bar.sh")}, + }, + { + desc: "apply substitution to inline command, success", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: []string{"echo", `[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]`, "|", "jq", "."}, + mode: 0755, + command: []string{"echo", fmt.Sprintf("$(steps.%s.outputs)", stepName), "|", "jq", "."}, + }, + { + desc: "apply substitution to inline command, fail to read, command no change", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + want: []string{"echo", fmt.Sprintf("$(steps.%s.outputs)", stepName), "|", "jq", "."}, + mode: 0000, + wantErr: true, + command: []string{"echo", fmt.Sprintf("$(steps.%s.outputs)", stepName), "|", "jq", "."}, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + if tc.mode == 0000 && os.Getuid() == 0 { + t.Skipf("Test doesn't work when running with root") + } + stepDir := createTmpDir(t, "") + artifactsPath := getStepArtifactsPath(stepDir, "step-"+stepName) + if tc.fileContent != "" { + err := os.MkdirAll(filepath.Dir(artifactsPath), 0755) + if err != nil { + t.Fatalf("fail to create stepDir %v", err) + } + err = os.WriteFile(artifactsPath, []byte(tc.fileContent), tc.mode) + if err != nil { + t.Fatalf("fail to write to file %v", err) + } + } + if tc.scriptContent != "" { + err := os.WriteFile(tc.scriptFile, []byte(tc.scriptContent), 0755) + if err != nil { + t.Fatalf("failed to write script to scriptFile %v", err) + } + } + e := Entrypointer{Command: tc.command} + err := e.applyStepArtifactSubstitutions(stepDir) + if tc.wantErr != (err != nil) { + t.Fatalf("Error checking failed %v", err) + } + got := e.Command + + if d := cmp.Diff(tc.want, got); d != "" { + t.Fatalf("command doesn't match %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestApplyStepArtifactSubstitutionsEnv(t *testing.T) { + stepName := "name" + scriptDir := createTmpDir(t, "script") + cur := ScriptDir + ScriptDir = scriptDir + t.Cleanup(func() { + ScriptDir = cur + }) + tests := []struct { + desc string + wantErr bool + want string + fileContent string + mode os.FileMode + envKey string + envValue string + }{ + { + desc: "apply substitution to env, no matches, no changes", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + mode: 0755, + envKey: "aaa", + envValue: "bbb", + want: "bbb", + }, + { + desc: "apply substitution to env, matches found, has change", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + mode: 0755, + envKey: "aaa", + envValue: fmt.Sprintf("abc-$(steps.%s.outputs)", stepName), + want: `abc-[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]`, + }, + { + desc: "apply substitution to env, matches found, read artifacts failed.", + fileContent: `{"inputs":[{"name":"inputs","values":[{"digest":{"sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},"uri":"pkg:example.github.com/inputs"}]}],"outputs":[{"name":"output","values":[{"digest":{"sha256":"64d0b157fdf2d7f6548836dd82085fd8401c9481a9f59e554f1b337f134074b0"},"uri":"docker:example.registry.com/outputs"}]}]}`, + mode: 0000, + envKey: "aaa", + envValue: fmt.Sprintf("abc-$(steps.%s.outputs)", stepName), + want: fmt.Sprintf("abc-$(steps.%s.outputs)", stepName), + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + if tc.mode == 0000 && os.Getuid() == 0 { + t.Skipf("Test doesn't work when running with root") + } + stepDir := createTmpDir(t, "") + artifactsPath := getStepArtifactsPath(stepDir, "step-"+stepName) + if tc.fileContent != "" { + err := os.MkdirAll(filepath.Dir(artifactsPath), 0755) + if err != nil { + t.Fatalf("fail to create stepDir %v", err) + } + err = os.WriteFile(artifactsPath, []byte(tc.fileContent), tc.mode) + if err != nil { + t.Fatalf("fail to write to file %v", err) + } + } + e := Entrypointer{} + t.Setenv(tc.envKey, tc.envValue) + err := e.applyStepArtifactSubstitutions(stepDir) + + if tc.wantErr != (err != nil) { + t.Fatalf("Error checking failed %v", err) + } + got := os.Getenv(tc.envKey) + + if d := cmp.Diff(tc.want, got); d != "" { + t.Fatalf("env doesn't match %s", diff.PrintWantGot(d)) + } + }) + } +} + func getTermination(t *testing.T, terminationFile string) ([]result.RunResult, error) { t.Helper() fileContents, err := os.ReadFile(terminationFile) @@ -1084,7 +1687,6 @@ func getTermination(t *testing.T, terminationFile string) ([]result.RunResult, e terminationStatus[i].Value = "" } } - return terminationStatus, nil } diff --git a/pkg/pod/status.go b/pkg/pod/status.go index 40674df6542..9ab226530a7 100644 --- a/pkg/pod/status.go +++ b/pkg/pod/status.go @@ -273,6 +273,7 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL // Parse termination messages terminationReason := "" + var as v1.Artifacts if state.Terminated != nil && len(state.Terminated.Message) != 0 { msg := state.Terminated.Message @@ -281,6 +282,15 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL logger.Errorf("termination message could not be parsed as JSON: %v", err) merr = multierror.Append(merr, err) } else { + for _, r := range results { + if r.ResultType == result.ArtifactsResultType { + if err := json.Unmarshal([]byte(r.Value), &as); err != nil { + merr = multierror.Append(merr, err) + } + // there should be only one ArtifactsResult + break + } + } time, err := extractStartedAtTimeFromResults(results) if err != nil { logger.Errorf("error setting the start time of step %q in taskrun %q: %v", s.Name, tr.Name, err) @@ -324,6 +334,8 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL ImageID: s.ImageID, Results: taskRunStepResults, TerminationReason: terminationReason, + Inputs: as.Inputs, + Outputs: as.Outputs, }) } @@ -435,6 +447,9 @@ func filterResults(results []result.RunResult, specResults []v1.TaskResult, step } taskRunStepResults = append(taskRunStepResults, taskRunStepResult) filteredResults = append(filteredResults, r) + case result.ArtifactsResultType: + filteredResults = append(filteredResults, r) + continue case result.InternalTektonResultType: // Internal messages are ignored because they're not used as external result continue diff --git a/pkg/pod/status_test.go b/pkg/pod/status_test.go index 5686acbf0e0..93796e0759c 100644 --- a/pkg/pod/status_test.go +++ b/pkg/pod/status_test.go @@ -433,6 +433,118 @@ func TestMakeTaskRunStatus_StepResults(t *testing.T) { } } +func TestMakeTaskRunStatus_StepArtifacts(t *testing.T) { + for _, c := range []struct { + desc string + podStatus corev1.PodStatus + pod corev1.Pod + tr v1.TaskRun + want v1.TaskRunStatus + }{ + { + desc: "step artifacts result type", + podStatus: corev1.PodStatus{ + Phase: corev1.PodSucceeded, + ContainerStatuses: []corev1.ContainerStatus{{ + Name: "step-one", + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"/tekton/run/0/status/artifacts/provenance.json","value":"{\n \"inputs\":[\n {\n \"name\":\"input-artifacts\",\n \"values\":[\n {\n \"uri\":\"git:jjjsss\",\n \"digest\":{\n \"sha256\":\"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0\"\n }\n }\n ]\n }\n ],\n \"outputs\":[\n {\n \"name\":\"build-results\",\n \"values\":[\n {\n \"uri\":\"pkg:balba\",\n \"digest\":{\n \"sha256\":\"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48\",\n \"sha1\":\"95588b8f34c31eb7d62c92aaa4e6506639b06ef2\"\n }\n }\n ]\n }\n ]\n}\n","type":5}]`, + }, + }, + }}, + }, + tr: v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "task-run", + Namespace: "foo", + }, + Spec: v1.TaskRunSpec{ + TaskSpec: &v1.TaskSpec{ + Steps: []v1.Step{{ + Name: "one", + }}, + }, + }, + }, + want: v1.TaskRunStatus{ + Status: statusSuccess(), + TaskRunStatusFields: v1.TaskRunStatusFields{ + Steps: []v1.StepState{{ + ContainerState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Message: `[{"key":"/tekton/run/0/status/artifacts/provenance.json","value":"{\n \"inputs\":[\n {\n \"name\":\"input-artifacts\",\n \"values\":[\n {\n \"uri\":\"git:jjjsss\",\n \"digest\":{\n \"sha256\":\"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0\"\n }\n }\n ]\n }\n ],\n \"outputs\":[\n {\n \"name\":\"build-results\",\n \"values\":[\n {\n \"uri\":\"pkg:balba\",\n \"digest\":{\n \"sha256\":\"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48\",\n \"sha1\":\"95588b8f34c31eb7d62c92aaa4e6506639b06ef2\"\n }\n }\n ]\n }\n ]\n}\n","type":5}]`, + }}, + Name: "one", + Container: "step-one", + Inputs: []v1.Artifact{ + { + Name: "input-artifacts", + Values: []v1.ArtifactValue{{ + Digest: map[v1.Algorithm]string{"sha256": "b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"}, + Uri: "git:jjjsss", + }, + }, + }, + }, + Outputs: []v1.Artifact{ + { + Name: "build-results", + Values: []v1.ArtifactValue{{ + Digest: map[v1.Algorithm]string{ + "sha1": "95588b8f34c31eb7d62c92aaa4e6506639b06ef2", + "sha256": "df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + }, + Uri: "pkg:balba", + }, + }, + }, + }, + Results: []v1.TaskRunResult{}, + }}, + Sidecars: []v1.SidecarState{}, + // We don't actually care about the time, just that it's not nil + CompletionTime: &metav1.Time{Time: time.Now()}, + }, + }, + }, + } { + t.Run(c.desc, func(t *testing.T) { + now := metav1.Now() + if cmp.Diff(c.pod, corev1.Pod{}) == "" { + c.pod = corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "foo", + CreationTimestamp: now, + }, + Status: c.podStatus, + } + } + + logger, _ := logging.NewLogger("", "status") + kubeclient := fakek8s.NewSimpleClientset() + got, err := MakeTaskRunStatus(context.Background(), logger, c.tr, &c.pod, kubeclient, c.tr.Spec.TaskSpec) + if err != nil { + t.Errorf("MakeTaskRunResult: %s", err) + } + + // Common traits, set for test case brevity. + c.want.PodName = "pod" + + ensureTimeNotNil := cmp.Comparer(func(x, y *metav1.Time) bool { + if x == nil { + return y == nil + } + return y != nil + }) + if d := cmp.Diff(c.want, got, ignoreVolatileTime, ensureTimeNotNil); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } + }) + } +} + func TestMakeTaskRunStatus(t *testing.T) { for _, c := range []struct { desc string diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go index b0d6d43b1d8..b54a618dba1 100644 --- a/pkg/reconciler/taskrun/resources/apply.go +++ b/pkg/reconciler/taskrun/resources/apply.go @@ -399,6 +399,23 @@ func getTaskResultReplacements(spec *v1.TaskSpec) map[string]string { return stringReplacements } +// ApplyArtifacts replaces the occurrences of step.artifacts.path with the absolute tekton internal path +func ApplyArtifacts(spec *v1.TaskSpec) *v1.TaskSpec { + for i := range spec.Steps { + stringReplacements := getStepArtifactReplacements(spec.Steps[i], i) + container.ApplyStepReplacements(&spec.Steps[i], stringReplacements, map[string][]string{}) + } + return spec +} + +func getStepArtifactReplacements(step v1.Step, idx int) map[string]string { + stringReplacements := map[string]string{} + stepName := pod.StepName(step.Name, idx) + stringReplacements["step.artifacts.path"] = filepath.Join(pipeline.StepsDir, stepName, "artifacts", "provenance.json") + + return stringReplacements +} + // ApplyStepExitCodePath replaces the occurrences of exitCode path with the absolute tekton internal path // Replace $(steps..exitCode.path) with pipeline.StepPath//exitCode func ApplyStepExitCodePath(spec *v1.TaskSpec) *v1.TaskSpec { diff --git a/pkg/reconciler/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go index 4d6f592dd0e..6d657d8c20f 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -2018,3 +2018,26 @@ func TestApplyParametersToWorkspaceBindings(t *testing.T) { }) } } + +func TestArtifacts(t *testing.T) { + ts := &v1.TaskSpec{ + Steps: []v1.Step{{ + Name: "name1", + Image: "bash:latest", + Args: []string{ + "$(step.artifacts.path)", + }, + Script: "#!/usr/bin/env bash\n echo -n $(step.artifacts.path)", + }, + }, + } + + want := applyMutation(ts, func(spec *v1.TaskSpec) { + spec.Steps[0].Args[0] = "/tekton/steps/step-name1/artifacts/provenance.json" + spec.Steps[0].Script = "#!/usr/bin/env bash\n echo -n /tekton/steps/step-name1/artifacts/provenance.json" + }) + got := resources.ApplyArtifacts(ts) + if d := cmp.Diff(want, got); d != "" { + t.Errorf("ApplyArtifacts() got diff %s", diff.PrintWantGot(d)) + } +} diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 4133b5a9165..2e818595fdd 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -918,6 +918,8 @@ func applyParamsContextsResultsAndWorkspaces(ctx context.Context, tr *v1.TaskRun // Apply task result substitution ts = resources.ApplyResults(ts) + // Apply step Artifacts substitution + ts = resources.ApplyArtifacts(ts) // Apply step exitCode path substitution ts = resources.ApplyStepExitCodePath(ts) diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index 2440624d563..be035d6b6b7 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -3768,6 +3768,34 @@ spec: Name: "step1", Results: []v1.StepResult{{Name: "result", Type: "string"}}, }}, + }, { + name: "step artifacts", + taskRun: parse.MustParseV1TaskRun(t, ` +metadata: + name: taskrun-with-step-artifacts + namespace: foo +spec: + taskSpec: + steps: + - ref: + name: stepAction + name: step1 +`), + stepAction: parse.MustParseV1alpha1StepAction(t, ` +metadata: + name: stepAction + namespace: foo +spec: + image: myImage + command: ["echo"] + args: ["hi", ">>", "$(step.artifacts.path)"] +`), + want: []v1.Step{{ + Image: "myImage", + Command: []string{"echo"}, + Args: []string{"hi", ">>", "/tekton/steps/step-step1/artifacts/provenance.json"}, + Name: "step1", + }}, }, { name: "params from taskspec", taskRun: parse.MustParseV1TaskRun(t, ` diff --git a/pkg/result/result.go b/pkg/result/result.go index 515fe9a6025..e3d66b596ff 100644 --- a/pkg/result/result.go +++ b/pkg/result/result.go @@ -35,6 +35,9 @@ const ( UnknownResultType = 10 // StepResultType default step result value StepResultType ResultType = 4 + + // ArtifactsResultType default artifacts result value + ArtifactsResultType ResultType = 5 ) // RunResult is used to write key/value pairs to TaskRun pod termination messages. @@ -88,6 +91,8 @@ func (r *ResultType) UnmarshalJSON(data []byte) error { *r = TaskRunResultType case "InternalTektonResult": *r = InternalTektonResultType + case "ArtifactsResult": + *r = ArtifactsResultType default: *r = UnknownResultType } diff --git a/pkg/result/result_test.go b/pkg/result/result_test.go index 41006aed079..5b96acb2041 100644 --- a/pkg/result/result_test.go +++ b/pkg/result/result_test.go @@ -42,6 +42,10 @@ func TestRunResult_UnmarshalJSON(t *testing.T) { name: "type defined as string - InternalTektonResult", data: "{\"key\":\"resultName\",\"value\":\"\", \"type\": \"InternalTektonResult\"}", pr: RunResult{Key: "resultName", Value: "", ResultType: InternalTektonResultType}, + }, { + name: "type defined as string - ArtifactsResult", + data: "{\"key\":\"resultName\",\"value\":\"\", \"type\": \"ArtifactsResult\"}", + pr: RunResult{Key: "resultName", Value: "", ResultType: ArtifactsResultType}, }, { name: "type defined as int", data: "{\"key\":\"resultName\",\"value\":\"\", \"type\": 1}", diff --git a/test/artifacts_test.go b/test/artifacts_test.go new file mode 100644 index 00000000000..39ca286b3aa --- /dev/null +++ b/test/artifacts_test.go @@ -0,0 +1,359 @@ +//go:build e2e +// +build e2e + +// /* +// Copyright 2024 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" + "fmt" + "testing" + + "github.com/tektoncd/pipeline/pkg/apis/config" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/test/parse" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/system" + knativetest "knative.dev/pkg/test" + "knative.dev/pkg/test/helpers" +) + +var ( + requireEnableStepArtifactsGate = map[string]string{ + "enable-artifacts": "true", + } +) + +func TestSurfaceArtifactsThroughTerminationMessage(t *testing.T) { + featureFlags := getFeatureFlagsBaseOnAPIFlag(t) + checkFlagsEnabled := requireAllGates(requireEnableStepArtifactsGate) + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + c, namespace := setup(ctx, t) + checkFlagsEnabled(ctx, t, c, "") + previous := featureFlags.ResultExtractionMethod + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": config.ResultExtractionMethodTerminationMessage, + }) + + knativetest.CleanupOnInterrupt(func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": previous, + }) + tearDown(ctx, t, c, namespace) + }, t.Logf) + defer func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": previous, + }) + tearDown(ctx, t, c, namespace) + }() + + taskRunName := helpers.ObjectNameForTest(t) + + fqImageName := getTestImage(busyboxImage) + + t.Logf("Creating Task and TaskRun in namespace %s", namespace) + task := simpleArtifactProducerTask(t, namespace, fqImageName) + if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create Task: %s", err) + } + taskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + taskRef: + name: %s +`, taskRunName, namespace, task.Name)) + if _, err := c.V1TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + if err := WaitForTaskRunState(ctx, c, taskRunName, TaskRunSucceed(taskRunName), "TaskRunSucceed", v1Version); err != nil { + t.Errorf("Error waiting for TaskRun to finish: %s", err) + } + + taskrun, err := c.V1TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + if d := cmp.Diff([]v1.TaskRunStepArtifact{{Name: "input-artifacts", + Values: []v1.ArtifactValue{{Digest: map[v1.Algorithm]string{"sha256": "b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"}, + Uri: "git:jjjsss", + }}, + }}, taskrun.Status.Steps[0].Inputs); d != "" { + t.Fatalf(`The expected stepState Inputs does not match created taskrun stepState Inputs. Here is the diff: %v`, d) + } + if d := cmp.Diff([]v1.TaskRunStepArtifact{{Name: "build-result", + Values: []v1.ArtifactValue{{Digest: map[v1.Algorithm]string{"sha1": "95588b8f34c31eb7d62c92aaa4e6506639b06ef2", "sha256": "df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48"}, + Uri: "pkg:balba", + }}, + }}, taskrun.Status.Steps[0].Outputs); d != "" { + t.Fatalf(`The expected stepState Outputs does not match created taskrun stepState Outputs. Here is the diff: %v`, d) + } +} + +func TestSurfaceArtifactsThroughTerminationMessageScriptProducesArtifacts(t *testing.T) { + featureFlags := getFeatureFlagsBaseOnAPIFlag(t) + checkFlagsEnabled := requireAllGates(requireEnableStepArtifactsGate) + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + c, namespace := setup(ctx, t) + checkFlagsEnabled(ctx, t, c, "") + previous := featureFlags.ResultExtractionMethod + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": config.ResultExtractionMethodTerminationMessage, + }) + + knativetest.CleanupOnInterrupt(func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": previous, + }) + tearDown(ctx, t, c, namespace) + }, t.Logf) + defer func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": previous, + }) + tearDown(ctx, t, c, namespace) + }() + + taskRunName := helpers.ObjectNameForTest(t) + + fqImageName := getTestImage(busyboxImage) + + t.Logf("Creating Task and TaskRun in namespace %s", namespace) + task := simpleArtifactScriptProducerTask(t, namespace, fqImageName) + if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create Task: %s", err) + } + taskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + taskRef: + name: %s +`, taskRunName, namespace, task.Name)) + if _, err := c.V1TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + if err := WaitForTaskRunState(ctx, c, taskRunName, TaskRunSucceed(taskRunName), "TaskRunSucceed", v1Version); err != nil { + t.Errorf("Error waiting for TaskRun to finish: %s", err) + } + + taskrun, err := c.V1TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + if d := cmp.Diff([]v1.TaskRunStepArtifact{{Name: "input-artifacts", + Values: []v1.ArtifactValue{{Digest: map[v1.Algorithm]string{"sha256": "b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"}, + Uri: "git:jjjsss", + }}, + }}, taskrun.Status.Steps[0].Inputs); d != "" { + t.Fatalf(`The expected stepState Inputs does not match created taskrun stepState Inputs. Here is the diff: %v`, d) + } + if d := cmp.Diff([]v1.TaskRunStepArtifact{{Name: "build-result", + Values: []v1.ArtifactValue{{Digest: map[v1.Algorithm]string{"sha1": "95588b8f34c31eb7d62c92aaa4e6506639b06ef2", "sha256": "df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48"}, + Uri: "pkg:balba", + }}, + }}, taskrun.Status.Steps[0].Outputs); d != "" { + t.Fatalf(`The expected stepState Outputs does not match created taskrun stepState Outputs. Here is the diff: %v`, d) + } +} + +func TestConsumeArtifacts(t *testing.T) { + featureFlags := getFeatureFlagsBaseOnAPIFlag(t) + checkFlagsEnabled := requireAllGates(map[string]string{ + "enable-artifacts": "true", + "enable-step-actions": "true", + }) + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + c, namespace := setup(ctx, t) + checkFlagsEnabled(ctx, t, c, "") + previous := featureFlags.ResultExtractionMethod + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": config.ResultExtractionMethodTerminationMessage, + }) + + knativetest.CleanupOnInterrupt(func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": previous, + }) + tearDown(ctx, t, c, namespace) + }, t.Logf) + + defer func() { + updateConfigMap(ctx, c.KubeClient, system.Namespace(), config.GetFeatureFlagsConfigName(), map[string]string{ + "results-from": previous, + }) + tearDown(ctx, t, c, namespace) + }() + taskRunName := helpers.ObjectNameForTest(t) + + fqImageName := getTestImage(busyboxImage) + + t.Logf("Creating Task and TaskRun in namespace %s", namespace) + task := simpleArtifactProducerTask(t, namespace, fqImageName) + task.Spec.Steps = append(task.Spec.Steps, + v1.Step{Name: "consume-outputs", Image: fqImageName, + Command: []string{"sh", "-c", "echo -n $(steps.hello.outputs) >> $(step.results.result1.path)"}, + Results: []v1.StepResult{{Name: "result1", Type: v1.ResultsTypeString}}}, + v1.Step{Name: "consume-inputs", Image: fqImageName, + Command: []string{"sh", "-c", "echo -n $(steps.hello.inputs) >> $(step.results.result2.path)"}, + Results: []v1.StepResult{{Name: "result2", Type: v1.ResultsTypeString}}}, + ) + if _, err := c.V1TaskClient.Create(ctx, task, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create Task: %s", err) + } + taskRun := parse.MustParseV1TaskRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + taskRef: + name: %s +`, taskRunName, namespace, task.Name)) + if _, err := c.V1TaskRunClient.Create(ctx, taskRun, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create TaskRun: %s", err) + } + + if err := WaitForTaskRunState(ctx, c, taskRunName, TaskRunSucceed(taskRunName), "TaskRunSucceed", v1Version); err != nil { + t.Errorf("Error waiting for TaskRun to finish: %s", err) + } + + taskrun, err := c.V1TaskRunClient.Get(ctx, taskRunName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Couldn't get expected TaskRun %s: %s", taskRunName, err) + } + wantOut := `[{digest:{sha1:95588b8f34c31eb7d62c92aaa4e6506639b06ef2,sha256:df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48},uri:pkg:balba}]` + gotOut := taskrun.Status.Steps[1].Results[0].Value.StringVal + if d := cmp.Diff(wantOut, gotOut); d != "" { + t.Fatalf(`The expected artifact outputs consumption result doesnot match expected. Here is the diff: %v`, d) + } + wantIn := `[{digest:{sha256:b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0},uri:git:jjjsss}]` + gotIn := taskrun.Status.Steps[2].Results[0].Value.StringVal + if d := cmp.Diff(wantIn, gotIn); d != "" { + t.Fatalf(`The expected artifact Inputs consumption result doesnot match expected. Here is the diff: %v`, d) + } +} + +func simpleArtifactProducerTask(t *testing.T, namespace string, fqImageName string) *v1.Task { + t.Helper() + task := parse.MustParseV1Task(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: hello + image: %s + command: ['/bin/sh'] + args: + - "-c" + - | + cat > $(step.artifacts.path) << EOF + { + "inputs":[ + { + "name":"input-artifacts", + "values":[ + { + "uri":"git:jjjsss", + "digest":{ + "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0" + } + } + ] + } + ], + "outputs":[ + { + "name":"build-result", + "values":[ + { + "uri":"pkg:balba", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } + ] + } + ] + } + EOF +`, helpers.ObjectNameForTest(t), namespace, fqImageName)) + return task +} + +func simpleArtifactScriptProducerTask(t *testing.T, namespace string, fqImageName string) *v1.Task { + t.Helper() + task := parse.MustParseV1Task(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: hello + image: %s + script: | + cat > $(step.artifacts.path) << EOF + { + "inputs":[ + { + "name":"input-artifacts", + "values":[ + { + "uri":"git:jjjsss", + "digest":{ + "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0" + } + } + ] + } + ], + "outputs":[ + { + "name":"build-result", + "values":[ + { + "uri":"pkg:balba", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } + ] + } + ] + } + EOF +`, helpers.ObjectNameForTest(t), namespace, fqImageName)) + return task +}