From 5555e6caf92ae0678a8d6d016efb1815d99e748f Mon Sep 17 00:00:00 2001 From: Chitrang Patel Date: Mon, 8 Jul 2024 11:57:39 -0400 Subject: [PATCH] Add isBuildArtifact field to Artifacts This PR adds isBuildArtifact field to Artifacts. This field will allow Tekton Chains to understand user's desire and appropriate add the artifact as a subject or a byProduct in the SLSA provenance. --- docs/artifacts.md | 76 +++++++++++++++++++ docs/pipeline-api.md | 22 ++++++ .../alpha/produce-consume-artifacts.yaml | 4 + go.mod | 3 +- pkg/apis/pipeline/v1/artifact_types.go | 31 +++++--- pkg/apis/pipeline/v1/artifact_types_test.go | 12 ++- pkg/apis/pipeline/v1/openapi_generated.go | 7 ++ pkg/apis/pipeline/v1/swagger.json | 4 + pkg/apis/pipeline/v1beta1/artifact_types.go | 5 +- .../pipeline/v1beta1/openapi_generated.go | 7 ++ pkg/apis/pipeline/v1beta1/swagger.json | 4 + test/artifacts_test.go | 6 +- 12 files changed, 162 insertions(+), 19 deletions(-) diff --git a/docs/artifacts.md b/docs/artifacts.md index 5b58d8b0471..f778b0caf30 100644 --- a/docs/artifacts.md +++ b/docs/artifacts.md @@ -155,6 +155,82 @@ spec: 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. +### Output Artifacts in SLSA Provenance +By default, Tekton Chains will consider all output artifacts as `byProducts` when generating in the [SLSA provenance](https://slsa.dev/spec/v1.0/provenance). In order to treat an artifact as a [subject](https://slsa.dev/spec/v1.0/provenance#schema) of the build, you must set a boolean field `"isBuildArtifact": true` for the output artifact. + +e.g. +```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 > $(artifacts.path) << EOF + { + "outputs":[ + { + "name":"image", + "isBuildArtifact": true, + "values":[ + { + "uri":"pkg:oci/nginx:stable-alpine3.17-slim?repository_url=docker.io/library", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } + ] + } + ] + } + EOF +``` +This informs Tekton Chains your desire to handle the artifact. + +When authoring a `StepAction` or a `Task`, you can even parametrize this field and let the user of the `StepAction`/`Task` indicate their desire. Generally, things like "gcs-upload" might want to set this value by default to `false`, however, a `StepAction/Task` like "kaniko" (since it is uploading images to container registries, which are normally build products) might want to set the default to `true`. + +e.g. + +```yaml +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: artifact-producer +spec: + params: + - name: isBuildArtifact + default: true + steps: + image: bash:latest + script: | + cat > $(artifacts.path) << EOF + { + "outputs":[ + { + "name":"image", + "isBuildArtifact": $(params.isBuildArtifact), + "values":[ + { + "uri":"pkg:oci/nginx:stable-alpine3.17-slim?repository_url=docker.io/library", + "digest":{ + "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48", + "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2" + } + } + ] + } + ] + } + EOF +``` + ### Passing Artifacts between Steps You can pass artifacts from one step to the next using: - Specific Artifact: `$(steps..inputs.)` or `$(steps..outputs.)` diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md index a2656ffaef2..b8cb221e16d 100644 --- a/docs/pipeline-api.md +++ b/docs/pipeline-api.md @@ -1385,6 +1385,17 @@ string

The artifact’s identifying category name

+ + +isBuildArtifact
+ +bool + + + +

A collection of values related to the artifact

+ +

ArtifactValue @@ -10033,6 +10044,17 @@ string

The artifact’s identifying category name

+ + +isBuildArtifact
+ +bool + + + +

A collection of values related to the artifact

+ +

ArtifactValue diff --git a/examples/v1/taskruns/alpha/produce-consume-artifacts.yaml b/examples/v1/taskruns/alpha/produce-consume-artifacts.yaml index cca1f84fd1e..bed55711682 100644 --- a/examples/v1/taskruns/alpha/produce-consume-artifacts.yaml +++ b/examples/v1/taskruns/alpha/produce-consume-artifacts.yaml @@ -6,6 +6,9 @@ spec: taskSpec: description: | A simple task that populates artifacts to TaskRun stepState + params: + - name: isBuildArtifact + default: true steps: - name: artifacts-producer image: docker.io/library/bash:latest @@ -28,6 +31,7 @@ spec: "outputs":[ { "name":"image", + "isBuildArtifact": $(params.isBuildArtifact), "values":[ { "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", diff --git a/go.mod b/go.mod index d3eec4c37eb..5af9341e3bd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/tektoncd/pipeline -go 1.22 +go 1.22.3 + toolchain go1.22.4 require ( diff --git a/pkg/apis/pipeline/v1/artifact_types.go b/pkg/apis/pipeline/v1/artifact_types.go index 1cd898d2ef5..eee578846f0 100644 --- a/pkg/apis/pipeline/v1/artifact_types.go +++ b/pkg/apis/pipeline/v1/artifact_types.go @@ -26,8 +26,9 @@ type Algorithm string // Artifact represents an artifact within a system, potentially containing multiple values // associated with it. type Artifact struct { - Name string `json:"name,omitempty"` // The artifact's identifying category name - Values []ArtifactValue `json:"values,omitempty"` // A collection of values related to the artifact + Name string `json:"name,omitempty"` // The artifact's identifying category name + Values []ArtifactValue `json:"values,omitempty"` // A collection of values related to the artifact + IsBuildArtifact bool `json:"isBuildArtifact,omitempty"` // Indicate if the artifact is a build artifact or a by product } // ArtifactValue represents a specific value or data element within an Artifact. @@ -82,35 +83,45 @@ func (a *Artifacts) Merge(another Artifacts) { }) } - outputMap := make(map[string][]ArtifactValue) + outputMap := make(map[string]Artifact) var newOutputs []Artifact for _, v := range a.Outputs { - outputMap[v.Name] = v.Values + outputMap[v.Name] = v } for _, v := range another.Outputs { _, ok := outputMap[v.Name] if !ok { - outputMap[v.Name] = []ArtifactValue{} + outputMap[v.Name] = Artifact{Name: v.Name, Values: []ArtifactValue{}, IsBuildArtifact: v.IsBuildArtifact} + } + // only update isBuildArtifact to true. + // Do not convert to false if it was true before. + if v.IsBuildArtifact { + art := outputMap[v.Name] + art.IsBuildArtifact = v.IsBuildArtifact + outputMap[v.Name] = art } for _, vv := range v.Values { exists := false - for _, av := range outputMap[v.Name] { + for _, av := range outputMap[v.Name].Values { if cmp.Equal(vv, av) { exists = true break } } if !exists { - outputMap[v.Name] = append(outputMap[v.Name], vv) + art := outputMap[v.Name] + art.Values = append(art.Values, vv) + outputMap[v.Name] = art } } } - for k, v := range outputMap { + for _, v := range outputMap { newOutputs = append(newOutputs, Artifact{ - Name: k, - Values: v, + Name: v.Name, + Values: v.Values, + IsBuildArtifact: v.IsBuildArtifact, }) } a.Inputs = newInputs diff --git a/pkg/apis/pipeline/v1/artifact_types_test.go b/pkg/apis/pipeline/v1/artifact_types_test.go index 895244d6363..fea82bc41e1 100644 --- a/pkg/apis/pipeline/v1/artifact_types_test.go +++ b/pkg/apis/pipeline/v1/artifact_types_test.go @@ -95,7 +95,8 @@ func TestArtifactsMerge(t *testing.T) { }, Outputs: []Artifact{ { - Name: "output1", + Name: "output1", + IsBuildArtifact: true, Values: []ArtifactValue{ { Digest: map[Algorithm]string{"sha256": "698c4539633943f7889f41605003d7fa63833722ebd2b37c7e75df1d3d06941a"}, @@ -104,7 +105,8 @@ func TestArtifactsMerge(t *testing.T) { }, }, { - Name: "output2", + Name: "output2", + IsBuildArtifact: true, Values: []ArtifactValue{ { Digest: map[Algorithm]string{"sha256": "7e406d83706c7193df3e38b66d350e55df6f13d2a28a1d35917a043533a70f5c"}, @@ -150,7 +152,8 @@ func TestArtifactsMerge(t *testing.T) { }, Outputs: []Artifact{ { - Name: "output1", + Name: "output1", + IsBuildArtifact: true, Values: []ArtifactValue{ { Digest: map[Algorithm]string{"sha256": "47de7a85905970a45132f48a9247879a15c483477e23a637504694e611135b40e"}, @@ -163,7 +166,8 @@ func TestArtifactsMerge(t *testing.T) { }, }, { - Name: "output2", + Name: "output2", + IsBuildArtifact: true, Values: []ArtifactValue{ { Digest: map[Algorithm]string{"sha256": "7e406d83706c7193df3e38b66d350e55df6f13d2a28a1d35917a043533a70f5c"}, diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go index 928f446c294..500d8034cfa 100644 --- a/pkg/apis/pipeline/v1/openapi_generated.go +++ b/pkg/apis/pipeline/v1/openapi_generated.go @@ -413,6 +413,13 @@ func schema_pkg_apis_pipeline_v1_Artifact(ref common.ReferenceCallback) common.O }, }, }, + "isBuildArtifact": { + SchemaProps: spec.SchemaProps{ + Description: "A collection of values related to the artifact", + Type: []string{"boolean"}, + Format: "", + }, + }, }, }, }, diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json index 4ece3c37804..07a371c01e8 100644 --- a/pkg/apis/pipeline/v1/swagger.json +++ b/pkg/apis/pipeline/v1/swagger.json @@ -155,6 +155,10 @@ "description": "TaskRunStepArtifact represents an artifact produced or used by a step within a task run. It directly uses the Artifact type for its structure.", "type": "object", "properties": { + "isBuildArtifact": { + "description": "A collection of values related to the artifact", + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/pkg/apis/pipeline/v1beta1/artifact_types.go b/pkg/apis/pipeline/v1beta1/artifact_types.go index ec50bde8a16..b7e11a0b7e5 100644 --- a/pkg/apis/pipeline/v1beta1/artifact_types.go +++ b/pkg/apis/pipeline/v1beta1/artifact_types.go @@ -6,8 +6,9 @@ type Algorithm string // Artifact represents an artifact within a system, potentially containing multiple values // associated with it. type Artifact struct { - Name string `json:"name,omitempty"` // The artifact's identifying category name - Values []ArtifactValue `json:"values,omitempty"` // A collection of values related to the artifact + Name string `json:"name,omitempty"` // The artifact's identifying category name + Values []ArtifactValue `json:"values,omitempty"` // A collection of values related to the artifact + IsBuildArtifact bool `json:"isBuildArtifact,omitempty"` // Indicate if the artifact is a build artifact or a by product } // ArtifactValue represents a specific value or data element within an Artifact. diff --git a/pkg/apis/pipeline/v1beta1/openapi_generated.go b/pkg/apis/pipeline/v1beta1/openapi_generated.go index dde2863832c..73870e14e18 100644 --- a/pkg/apis/pipeline/v1beta1/openapi_generated.go +++ b/pkg/apis/pipeline/v1beta1/openapi_generated.go @@ -440,6 +440,13 @@ func schema_pkg_apis_pipeline_v1beta1_Artifact(ref common.ReferenceCallback) com }, }, }, + "isBuildArtifact": { + SchemaProps: spec.SchemaProps{ + Description: "A collection of values related to the artifact", + Type: []string{"boolean"}, + Format: "", + }, + }, }, }, }, diff --git a/pkg/apis/pipeline/v1beta1/swagger.json b/pkg/apis/pipeline/v1beta1/swagger.json index e17ab8e0f33..25e14827d75 100644 --- a/pkg/apis/pipeline/v1beta1/swagger.json +++ b/pkg/apis/pipeline/v1beta1/swagger.json @@ -155,6 +155,10 @@ "description": "TaskRunStepArtifact represents an artifact produced or used by a step within a task run. It directly uses the Artifact type for its structure.", "type": "object", "properties": { + "isBuildArtifact": { + "description": "A collection of values related to the artifact", + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/test/artifacts_test.go b/test/artifacts_test.go index 1ae89116803..1ad0c853f73 100644 --- a/test/artifacts_test.go +++ b/test/artifacts_test.go @@ -117,7 +117,7 @@ spec: }}, 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: "image", + if d := cmp.Diff([]v1.TaskRunStepArtifact{{Name: "image", IsBuildArtifact: true, Values: []v1.ArtifactValue{{Digest: map[v1.Algorithm]string{"sha1": "95588b8f34c31eb7d62c92aaa4e6506639b06ef2", "sha256": "df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48"}, Uri: "pkg:balba", }}, @@ -191,7 +191,7 @@ spec: }}, 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", + if d := cmp.Diff([]v1.TaskRunStepArtifact{{Name: "build-result", IsBuildArtifact: false, Values: []v1.ArtifactValue{{Digest: map[v1.Algorithm]string{"sha1": "95588b8f34c31eb7d62c92aaa4e6506639b06ef2", "sha256": "df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48"}, Uri: "pkg:balba", }}, @@ -425,6 +425,7 @@ spec: "outputs":[ { "name":"image", + "isBuildArtifact":true, "values":[ { "uri":"pkg:balba", @@ -523,6 +524,7 @@ spec: "outputs":[ { "name":"build-result", + "isBuildArtifact":false, "values":[ { "uri":"pkg:balba",