diff --git a/pkg/artifacts/signable.go b/pkg/artifacts/signable.go index 2e489bc21b..799cdf9932 100644 --- a/pkg/artifacts/signable.go +++ b/pkg/artifacts/signable.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" "github.com/opencontainers/go-digest" "github.com/tektoncd/chains/internal/backport" "github.com/tektoncd/chains/pkg/chains/objects" @@ -323,6 +324,28 @@ func RetrieveMaterialsFromStructuredResults(ctx context.Context, obj objects.Tek return mats } +// RetrieveResolvedDependenciesFromStructuredResults retrieves structured results from Tekton Object, and convert them into materials. +func RetrieveResolvedDependenciesFromStructuredResults(ctx context.Context, obj objects.TektonObject, categoryMarker string) []v1.ResourceDescriptor { + logger := logging.FromContext(ctx) + // Retrieve structured provenance for inputs. + resolvedDependencies := []v1.ResourceDescriptor{} + ssts := ExtractStructuredTargetFromResults(ctx, obj, ArtifactsInputsResultName) + for _, s := range ssts { + if err := checkDigest(s.Digest); err != nil { + logger.Debugf("Digest for %s not in the right format: %s, %v", s.URI, s.Digest, err) + continue + } + splits := strings.Split(s.Digest, ":") + alg := splits[0] + digest := splits[1] + resolvedDependencies = append(resolvedDependencies, v1.ResourceDescriptor{ + URI: s.URI, + Digest: map[string]string{alg: digest}, + }) + } + return resolvedDependencies +} + // ExtractStructuredTargetFromResults extracts structured signable targets aim to generate intoto provenance as materials within TaskRun results and store them as StructuredSignable. // categoryMarker categorizes signable targets into inputs and outputs. func ExtractStructuredTargetFromResults(ctx context.Context, obj objects.TektonObject, categoryMarker string) []*StructuredSignable { diff --git a/pkg/chains/formats/slsa/internal/material/material.go b/pkg/chains/formats/slsa/internal/material/material.go index 51f718ce4a..b899922159 100644 --- a/pkg/chains/formats/slsa/internal/material/material.go +++ b/pkg/chains/formats/slsa/internal/material/material.go @@ -60,22 +60,29 @@ func AddImageIDToMaterials(imageID string, mats *[]common.ProvenanceMaterial) er m := common.ProvenanceMaterial{ Digest: common.DigestSet{}, } + uriDigest, err := ExtractUriDigestFromImageID(imageID) + if err != nil { + return err + } + m.URI = uriDigest["uri"] + m.Digest[uriDigest["digestAlgorithm"]] = uriDigest["digestValue"] + *mats = append(*mats, m) + return nil +} + +// ExtractUriDigestFromImageID extracts uri and digest from an imageID with format @sha256: +func ExtractUriDigestFromImageID(imageID string) (map[string]string, error) { uriDigest := strings.Split(imageID, uriSeparator) if len(uriDigest) == 2 { digest := strings.Split(uriDigest[1], digestSeparator) if len(digest) == 2 { - // no point in partially populating the material - // do it if both conditions are valid. - m.URI = uriDigest[0] - m.Digest[digest[0]] = digest[1] - *mats = append(*mats, m) + return map[string]string{"uri": uriDigest[0], "digestAlgorithm": digest[0], "digestValue": digest[1]}, nil } else { - return fmt.Errorf("expected imageID %s to be separable by @ and :", imageID) + return map[string]string{}, fmt.Errorf("expected imageID %s to be separable by @ and :", imageID) } } else { - return fmt.Errorf("expected imageID %s to be separable by @", imageID) + return map[string]string{}, fmt.Errorf("expected imageID %s to be separable by @", imageID) } - return nil } // Materials constructs `predicate.materials` section by collecting all the artifacts that influence a taskrun such as source code repo and step&sidecar base images. diff --git a/pkg/chains/formats/slsa/v2alpha2/internal/resolved_dependencies/resolved_dependencies.go b/pkg/chains/formats/slsa/v2alpha2/internal/resolved_dependencies/resolved_dependencies.go new file mode 100644 index 0000000000..1a952eea42 --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha2/internal/resolved_dependencies/resolved_dependencies.go @@ -0,0 +1,331 @@ +/* +Copyright 2023 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 resolvedDependencies + +import ( + "context" + "encoding/json" + + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/tektoncd/chains/internal/backport" + "github.com/tektoncd/chains/pkg/artifacts" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/attest" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/material" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "knative.dev/pkg/logging" +) + +// addStepImagesToResolvedDependencies adds step images to predicate.resolvedDepencencies +func addStepImagesToResolvedDependencies(steps []v1beta1.StepState, resolvedDependencies *[]v1.ResourceDescriptor) error { + for _, stepState := range steps { + if err := addImageIDToResolvedDependencies(stepState.ImageID, resolvedDependencies); err != nil { + return err + } + } + return nil +} + +// addSidecarImagesToResolvedDependencies adds sidecar images to predicate.resolvedDependencies +func addSidecarImagesToResolvedDependencies(sidecars []v1beta1.SidecarState, resolvedDependencies *[]v1.ResourceDescriptor) error { + for _, sidecarState := range sidecars { + if err := addImageIDToResolvedDependencies(sidecarState.ImageID, resolvedDependencies); err != nil { + return err + } + } + return nil +} + +// addImageIDToResolvedDependencies converts an imageId with format @sha256: and then adds it to a provenance resolvedDependencies. +func addImageIDToResolvedDependencies(imageID string, resolvedDependencies *[]v1.ResourceDescriptor) error { + rd := v1.ResourceDescriptor{ + Digest: common.DigestSet{}, + } + uriDigest, err := material.ExtractUriDigestFromImageID(imageID) + if err != nil { + return err + } + rd.URI = uriDigest["uri"] + rd.Digest[uriDigest["digestAlgorithm"]] = uriDigest["digestValue"] + rd.Name = "image" + *resolvedDependencies = append(*resolvedDependencies, rd) + return nil +} + +// gitInfo scans over the input parameters and looks for parameters +// with specified names. +func gitInfo(tro *objects.TaskRunObject) (commit string, url string) { + // Scan for git params to use for resolved dependencies + if tro.Status.TaskSpec != nil { + for _, p := range tro.Status.TaskSpec.Params { + if p.Default == nil { + continue + } + if p.Name == attest.CommitParam { + commit = p.Default.StringVal + continue + } + if p.Name == attest.URLParam { + url = p.Default.StringVal + } + } + } + + for _, p := range tro.Spec.Params { + if p.Name == attest.CommitParam { + commit = p.Value.StringVal + continue + } + if p.Name == attest.URLParam { + url = p.Value.StringVal + } + } + + for _, r := range tro.Status.TaskRunResults { + if r.Name == attest.CommitParam { + commit = r.Value.StringVal + } + if r.Name == attest.URLParam { + url = r.Value.StringVal + } + } + + url = attest.SPDXGit(url, "") + return +} + +// removeDuplicateResolvedDependencies removes duplicate resolved dependencies from the slice of resolved dependencies. +// Original order of resolved dependencies is retained. +func removeDuplicateResolvedDependencies(resolvedDependencies []v1.ResourceDescriptor) ([]v1.ResourceDescriptor, error) { + out := make([]v1.ResourceDescriptor, 0, len(resolvedDependencies)) + + // make map to store seen resolved dependencies + seen := map[string]bool{} + for _, resolvedDependency := range resolvedDependencies { + rDep := v1.ResourceDescriptor{} + rDep.URI = resolvedDependency.URI + rDep.Digest = resolvedDependency.Digest + rd, err := json.Marshal(rDep) + if err != nil { + return nil, err + } + if seen[string(rd)] { + // if the resource is a top level task or pipeline config then add it regardless. + if resolvedDependency.Name == "config/task" || resolvedDependency.Name == "config/pipeline" { + out = append(out, resolvedDependency) + } else { + continue + } + } + seen[string(rd)] = true + out = append(out, resolvedDependency) + } + return out, nil +} + +// TaskRunResolvedDependencies constructs `predicate.resolvedDependencies` section by collecting all the artifacts that influence a taskrun such as source code repo and step&sidecar base images. +func TaskRunResolvedDependencies(ctx context.Context, tro *objects.TaskRunObject) ([]v1.ResourceDescriptor, error) { + var resolvedDependencies []v1.ResourceDescriptor + + // add top level task config + if p := tro.Status.Provenance; p != nil && p.RefSource != nil { + rd := v1.ResourceDescriptor{ + Name: "config/task", + URI: p.RefSource.URI, + Digest: p.RefSource.Digest, + } + resolvedDependencies = append(resolvedDependencies, rd) + } + + // add step images + if err := addStepImagesToResolvedDependencies(tro.Status.Steps, &resolvedDependencies); err != nil { + return resolvedDependencies, nil + } + + // add sidecar images + if err := addSidecarImagesToResolvedDependencies(tro.Status.Sidecars, &resolvedDependencies); err != nil { + return resolvedDependencies, nil + } + + gitCommit, gitURL := gitInfo(tro) + + // Store git rev as ResolvedDependencies + if gitCommit != "" && gitURL != "" { + resolvedDependencies = append(resolvedDependencies, v1.ResourceDescriptor{ + Name: "inputs/result", + URI: gitURL, + Digest: map[string]string{"sha1": gitCommit}, + }) + return resolvedDependencies, nil + } + + // add input artifacts + sms := artifacts.RetrieveResolvedDependenciesFromStructuredResults(ctx, tro, artifacts.ArtifactsInputsResultName) + for i := range sms { + sms[i].Name = "inputs/result" + } + resolvedDependencies = append(resolvedDependencies, sms...) + + if tro.Spec.Resources != nil { //nolint:all //incompatible with pipelines v0.45 + // check for a Git PipelineResource + for _, input := range tro.Spec.Resources.Inputs { + if input.ResourceSpec == nil || input.ResourceSpec.Type != backport.PipelineResourceTypeGit { //nolint:all //incompatible with pipelines v0.45 + continue + } + + rd := v1.ResourceDescriptor{ + Name: "pipelineResource", + Digest: common.DigestSet{}, + } + + for _, rr := range tro.Status.ResourcesResult { + if rr.ResourceName != input.Name { + continue + } + if rr.Key == "url" { + rd.URI = attest.SPDXGit(rr.Value, "") + } else if rr.Key == "commit" { + rd.Digest["sha1"] = rr.Value + } + } + + var url string + var revision string + for _, param := range input.ResourceSpec.Params { + if param.Name == "url" { + url = param.Value + } + if param.Name == "revision" { + revision = param.Value + } + } + rd.URI = attest.SPDXGit(url, revision) + resolvedDependencies = append(resolvedDependencies, rd) + } + } + + // remove duplicate resolved dependencies + resolvedDependencies, err := removeDuplicateResolvedDependencies(resolvedDependencies) + if err != nil { + return resolvedDependencies, err + } + return resolvedDependencies, nil +} + +// PipelineRunResolvedDependencies constructs `predicate.resolvedDependencies` section by collecting all the artifacts that influence a taskrun such as source code repo and step&sidecar base images. +func PipelineRunResolvedDependencies(ctx context.Context, pro *objects.PipelineRunObject) ([]v1.ResourceDescriptor, error) { + logger := logging.FromContext(ctx) + var resolvedDependencies []v1.ResourceDescriptor + + if p := pro.Status.Provenance; p != nil && p.RefSource != nil { + rd := v1.ResourceDescriptor{ + Name: "config/pipeline", + URI: p.RefSource.URI, + Digest: p.RefSource.Digest, + } + resolvedDependencies = append(resolvedDependencies, rd) + } + pSpec := pro.Status.PipelineSpec + if pSpec != nil { + pipelineTasks := append(pSpec.Tasks, pSpec.Finally...) + for _, t := range pipelineTasks { + tr := pro.GetTaskRunFromTask(t.Name) + // Ignore Tasks that did not execute during the PipelineRun. + if tr == nil || tr.Status.CompletionTime == nil { + logger.Infof("taskrun status not found for task %s", t.Name) + continue + } + // add remote task configsource information in materials + if tr.Status.Provenance != nil && tr.Status.Provenance.RefSource != nil { + rd := v1.ResourceDescriptor{ + Name: "config/pipelineTask", + URI: tr.Status.Provenance.RefSource.URI, + Digest: tr.Status.Provenance.RefSource.Digest, + } + resolvedDependencies = append(resolvedDependencies, rd) + } + // add step images + if err := addStepImagesToResolvedDependencies(tr.Status.Steps, &resolvedDependencies); err != nil { + return resolvedDependencies, nil + } + + // add sidecar images + if err := addSidecarImagesToResolvedDependencies(tr.Status.Sidecars, &resolvedDependencies); err != nil { + return resolvedDependencies, nil + } + } + } + var commit, url string + // search spec.params + for _, p := range pro.Spec.Params { + if p.Name == attest.CommitParam { + commit = p.Value.StringVal + continue + } + if p.Name == attest.URLParam { + url = p.Value.StringVal + } + } + + // search status.PipelineSpec.params + if pro.Status.PipelineSpec != nil { + for _, p := range pro.Status.PipelineSpec.Params { + if p.Default == nil { + continue + } + if p.Name == attest.CommitParam { + commit = p.Default.StringVal + continue + } + if p.Name == attest.URLParam { + url = p.Default.StringVal + } + } + } + + // search status.PipelineRunResults + for _, r := range pro.Status.PipelineResults { + if r.Name == attest.CommitParam { + commit = r.Value.StringVal + } + if r.Name == attest.URLParam { + url = r.Value.StringVal + } + } + if len(commit) > 0 && len(url) > 0 { + url = attest.SPDXGit(url, "") + resolvedDependencies = append(resolvedDependencies, v1.ResourceDescriptor{ + Name: "inputs/result", + URI: url, + Digest: map[string]string{"sha1": commit}, + }) + } + + sms := artifacts.RetrieveResolvedDependenciesFromStructuredResults(ctx, pro, artifacts.ArtifactsInputsResultName) + for i := range sms { + sms[i].Name = "inputs/result" + } + resolvedDependencies = append(resolvedDependencies, sms...) + + // remove duplicate resolved dependencies + resolvedDependencies, err := removeDuplicateResolvedDependencies(resolvedDependencies) + if err != nil { + return resolvedDependencies, err + } + return resolvedDependencies, nil +} diff --git a/pkg/chains/formats/slsa/v2alpha2/internal/resolved_dependencies/resolved_dependencies_test.go b/pkg/chains/formats/slsa/v2alpha2/internal/resolved_dependencies/resolved_dependencies_test.go new file mode 100644 index 0000000000..2e0b0691e4 --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha2/internal/resolved_dependencies/resolved_dependencies_test.go @@ -0,0 +1,725 @@ +/* +Copyright 2023 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 resolvedDependencies + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/ghodss/yaml" + "github.com/google/go-cmp/cmp" + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/tektoncd/chains/internal/backport" + "github.com/tektoncd/chains/pkg/artifacts" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/internal/objectloader" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1" + logtesting "knative.dev/pkg/logging/testing" +) + +const digest = "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b7" + +var pro *objects.PipelineRunObject +var proStructuredResults *objects.PipelineRunObject + +func init() { + pro = createPro("../../../testdata/pipelinerun1.json") + proStructuredResults = createPro("../../../testdata/pipelinerun_structured_results.json") +} + +func createPro(path string) *objects.PipelineRunObject { + var err error + pr, err := objectloader.PipelineRunFromFile(path) + if err != nil { + panic(err) + } + tr1, err := objectloader.TaskRunFromFile("../../../testdata/taskrun1.json") + if err != nil { + panic(err) + } + tr2, err := objectloader.TaskRunFromFile("../../../testdata/taskrun2.json") + if err != nil { + panic(err) + } + p := objects.NewPipelineRunObject(pr) + p.AppendTaskRun(tr1) + p.AppendTaskRun(tr2) + return p +} + +func TestTaskRunResolvedDependenciesWithTaskRunResults(t *testing.T) { + // make sure this works with Git resources + taskrun := `apiVersion: tekton.dev/v1beta1 +kind: TaskRun +spec: + taskSpec: + resources: + inputs: + - name: repo + type: git +status: + taskResults: + - name: CHAINS-GIT_COMMIT + value: 50c56a48cfb3a5a80fa36ed91c739bdac8381cbe + - name: CHAINS-GIT_URL + value: https://github.com/GoogleContainerTools/distroless` + var taskRun *v1beta1.TaskRun + if err := yaml.Unmarshal([]byte(taskrun), &taskRun); err != nil { + t.Fatal(err) + } + + want := []v1.ResourceDescriptor{ + { + Name: "inputs/result", + URI: "git+https://github.com/GoogleContainerTools/distroless.git", + Digest: common.DigestSet{ + "sha1": "50c56a48cfb3a5a80fa36ed91c739bdac8381cbe", + }, + }, + } + + ctx := logtesting.TestContextWithLogger(t) + got, err := TaskRunResolvedDependencies(ctx, objects.NewTaskRunObject(taskRun)) + if err != nil { + t.Fatalf("Did not expect an error but got %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("want %v got %v", want, got) + } +} + +func TestTaskRunResolvedDependencies(t *testing.T) { + tests := []struct { + name string + taskRun *v1beta1.TaskRun + want []v1.ResourceDescriptor + }{{ + name: "resolvedDependencies from pipeline resources", + taskRun: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Resources: &v1beta1.TaskRunResources{ //nolint:all //incompatible with pipelines v0.45 + Inputs: []v1beta1.TaskResourceBinding{ //nolint:all //incompatible with pipelines v0.45 + { + PipelineResourceBinding: v1beta1.PipelineResourceBinding{ //nolint:all //incompatible with pipelines v0.45 + Name: "nil-resource-spec", + }, + }, { + PipelineResourceBinding: v1beta1.PipelineResourceBinding{ //nolint:all //incompatible with pipelines v0.45 + Name: "repo", + ResourceSpec: &v1alpha1.PipelineResourceSpec{ //nolint:all //incompatible with pipelines v0.45 + Params: []v1alpha1.ResourceParam{ + {Name: "url", Value: "https://github.com/GoogleContainerTools/distroless"}, + }, + Type: backport.PipelineResourceTypeGit, + }, + }, + }, + }, + }, + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "img1_input" + "-" + artifacts.ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest, + }), + }, + }, + ResourcesResult: []v1beta1.PipelineResourceResult{ + { + ResourceName: "repo", + Key: "commit", + Value: "50c56a48cfb3a5a80fa36ed91c739bdac8381cbe", + }, { + ResourceName: "repo", + Key: "url", + Value: "https://github.com/GoogleContainerTools/distroless", + }, + }, + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "inputs/result", + URI: "gcr.io/foo/bar", + Digest: common.DigestSet{ + "sha256": strings.TrimPrefix(digest, "sha256:"), + }, + }, + { + Name: "pipelineResource", + URI: "git+https://github.com/GoogleContainerTools/distroless.git", + Digest: common.DigestSet{ + "sha1": "50c56a48cfb3a5a80fa36ed91c739bdac8381cbe", + }, + }, + }, + }, { + name: "resolvedDependencies from remote task", + taskRun: &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + Provenance: &v1beta1.Provenance{ + RefSource: &v1beta1.RefSource{ + URI: "git+github.com/something.git", + Digest: map[string]string{ + "sha1": "abcd1234", + }, + }, + }, + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "config/task", + URI: "git+github.com/something.git", + Digest: common.DigestSet{ + "sha1": "abcd1234", + }, + }, + }, + }, { + name: "resolvedDependencies from git results", + taskRun: &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Params: []v1beta1.Param{{ + Name: "CHAINS-GIT_COMMIT", + Value: *v1beta1.NewStructuredValues("my-commit"), + }, { + Name: "CHAINS-GIT_URL", + Value: *v1beta1.NewStructuredValues("github.com/something"), + }}, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "inputs/result", + URI: "git+github.com/something.git", + Digest: common.DigestSet{ + "sha1": "my-commit", + }, + }, + }, + }, { + name: "resolvedDependencies from step images", + taskRun: &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + Steps: []v1beta1.StepState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "git-source-repo-repeat-again-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "build", + ImageID: "gcr.io/cloud-marketplace-containers/google/bazel@sha256:010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }}, + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + { + Name: "image", + URI: "gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, + }, + }, { + name: "resolvedDependencies from step and sidecar images", + taskRun: &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + Steps: []v1beta1.StepState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "git-source-repo-repeat-again-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "build", + ImageID: "gcr.io/cloud-marketplace-containers/google/bazel@sha256:010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }}, + Sidecars: []v1beta1.SidecarState{{ + Name: "sidecar-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init@sha256:a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567", + }}, + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, { + Name: "image", + URI: "gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init", + Digest: common.DigestSet{ + "sha256": "a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567", + }, + }, + }, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + mat, err := TaskRunResolvedDependencies(ctx, objects.NewTaskRunObject(tc.taskRun)) + if err != nil { + t.Fatalf("Did not expect an error but got %v", err) + } + if diff := cmp.Diff(tc.want, mat); diff != "" { + t.Errorf("ResolvedDependencies(): -want +got: %s", diff) + } + }) + } +} + +func TestAddStepImagesToResolvedDependencies(t *testing.T) { + tests := []struct { + name string + steps []v1beta1.StepState + want []v1.ResourceDescriptor + wantError error + }{{ + name: "steps with proper imageID", + steps: []v1beta1.StepState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "git-source-repo-repeat-again-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "build", + ImageID: "gcr.io/cloud-marketplace-containers/google/bazel@sha256:010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }}, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + { + Name: "image", + URI: "gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, + }, + }, { + name: "step with bad imageId - no uri", + steps: []v1beta1.StepState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init-sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }}, + want: []v1.ResourceDescriptor{{}}, + wantError: fmt.Errorf("expected imageID gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init-sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247 to be separable by @"), + }, { + name: "step with bad imageId - no digest", + steps: []v1beta1.StepState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256-b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }}, + want: []v1.ResourceDescriptor{{}}, + wantError: fmt.Errorf("expected imageID gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256-b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247 to be separable by @ and :"), + }} + for _, tc := range tests { + var mat []v1.ResourceDescriptor + if err := addStepImagesToResolvedDependencies(tc.steps, &mat); err != nil { + if err.Error() != tc.wantError.Error() { + t.Fatalf("Expected error %v but got %v", tc.wantError, err) + } + } + if tc.wantError == nil { + if diff := cmp.Diff(tc.want, mat); diff != "" { + t.Errorf("resolvedDependencies(): -want +got: %s", diff) + } + } + } +} + +func TestAddSidecarImagesToResolvedDependencies(t *testing.T) { + tests := []struct { + name string + sidecars []v1beta1.SidecarState + want []v1.ResourceDescriptor + wantError error + }{{ + name: "sidecars with proper imageID", + sidecars: []v1beta1.SidecarState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "git-source-repo-repeat-again-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "build", + ImageID: "gcr.io/cloud-marketplace-containers/google/bazel@sha256:010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }}, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + { + Name: "image", + URI: "gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, + }, + }, { + name: "sidecars with bad imageId - no uri", + sidecars: []v1beta1.SidecarState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init-sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }}, + want: []v1.ResourceDescriptor{{}}, + wantError: fmt.Errorf("expected imageID gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init-sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247 to be separable by @"), + }, { + name: "sidecars with bad imageId - no digest", + sidecars: []v1beta1.SidecarState{{ + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256-b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }}, + want: []v1.ResourceDescriptor{{}}, + wantError: fmt.Errorf("expected imageID gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256-b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247 to be separable by @ and :"), + }} + for _, tc := range tests { + var mat []v1.ResourceDescriptor + if err := addSidecarImagesToResolvedDependencies(tc.sidecars, &mat); err != nil { + if err.Error() != tc.wantError.Error() { + t.Fatalf("Expected error %v but got %v", tc.wantError, err) + } + } + if tc.wantError == nil { + if diff := cmp.Diff(tc.want, mat); diff != "" { + t.Errorf("resolvedDependencies(): -want +got: %s", diff) + } + } + } +} + +func TestAddImageIDToResolvedDependencies(t *testing.T) { + tests := []struct { + name string + imageID string + want []v1.ResourceDescriptor + wantError error + }{{ + name: "proper ImageID", + imageID: "gcr.io/cloud-marketplace-containers/google/bazel@sha256:010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, + }, + }, { + name: "bad ImageID", + imageID: "badImageId", + want: []v1.ResourceDescriptor{}, + wantError: fmt.Errorf("expected imageID badImageId to be separable by @"), + }} + for _, tc := range tests { + mat := []v1.ResourceDescriptor{} + if err := addImageIDToResolvedDependencies(tc.imageID, &mat); err != nil { + if err.Error() != tc.wantError.Error() { + t.Fatalf("Expected error %v but got %v", tc.wantError, err) + } + } + if tc.wantError == nil { + if diff := cmp.Diff(tc.want, mat); diff != "" { + t.Errorf("resolvedDependencies(): -want +got: %s", diff) + } + } + } +} + +func TestRemoveDuplicates(t *testing.T) { + tests := []struct { + name string + mats []v1.ResourceDescriptor + want []v1.ResourceDescriptor + }{{ + name: "no duplicate resolvedDependencies", + mats: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, { + Name: "image", + URI: "gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init", + Digest: common.DigestSet{ + "sha256": "a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567", + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, { + Name: "image", + URI: "gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init", + Digest: common.DigestSet{ + "sha256": "a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567", + }, + }, + }, + }, { + name: "same uri and digest", + mats: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + }, + }, { + name: "same uri but different digest", + mats: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01248", + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01248", + }, + }, + }, + }, { + name: "same uri but different digest, swap order", + mats: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01248", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + }, + want: []v1.ResourceDescriptor{ + { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01248", + }, + }, { + Name: "image", + URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, + }, + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mat, err := removeDuplicateResolvedDependencies(tc.mats) + if err != nil { + t.Fatalf("Did not expect an error but got %v", err) + } + if diff := cmp.Diff(tc.want, mat); diff != "" { + t.Errorf("resolvedDependencies(): -want +got: %s", diff) + } + }) + } +} + +func TestPipelineRunResolvedDependencies(t *testing.T) { + expected := []v1.ResourceDescriptor{ + {Name: "config/pipeline", URI: "github.com/test", Digest: common.DigestSet{"sha1": "28b123"}}, + {Name: "config/pipelineTask", URI: "github.com/catalog", Digest: common.DigestSet{"sha1": "x123"}}, + { + Name: "image", + URI: "docker-pullable://gcr.io/test1/test1", + Digest: common.DigestSet{"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"}, + }, + {Name: "config/pipelineTask", URI: "github.com/test", Digest: common.DigestSet{"sha1": "ab123"}}, + { + Name: "image", + URI: "docker-pullable://gcr.io/test2/test2", + Digest: common.DigestSet{"sha256": "4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac"}, + }, + { + Name: "image", + URI: "docker-pullable://gcr.io/test3/test3", + Digest: common.DigestSet{"sha256": "f1a8b8549c179f41e27ff3db0fe1a1793e4b109da46586501a8343637b1d0478"}, + }, + {Name: "inputs/result", URI: "git+https://git.test.com.git", Digest: common.DigestSet{"sha1": "abcd"}}, + {Name: "inputs/result", URI: "abc", Digest: common.DigestSet{"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}}, + } + ctx := logtesting.TestContextWithLogger(t) + got, err := PipelineRunResolvedDependencies(ctx, pro) + if err != nil { + t.Error(err) + } + if diff := cmp.Diff(expected, got); diff != "" { + t.Errorf("PipelineRunResolvedDependencies(): -want +got: %s", diff) + } +} + +func TestPipelineRunStructuredResultResolvedDependencies(t *testing.T) { + want := []v1.ResourceDescriptor{ + {Name: "config/pipeline", URI: "github.com/test", Digest: common.DigestSet{"sha1": "28b123"}}, + {Name: "config/pipelineTask", URI: "github.com/catalog", Digest: common.DigestSet{"sha1": "x123"}}, + { + Name: "image", + URI: "docker-pullable://gcr.io/test1/test1", + Digest: common.DigestSet{"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"}, + }, + {Name: "config/pipelineTask", URI: "github.com/test", Digest: common.DigestSet{"sha1": "ab123"}}, + { + Name: "image", + URI: "docker-pullable://gcr.io/test2/test2", + Digest: common.DigestSet{"sha256": "4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac"}, + }, + { + Name: "image", + URI: "docker-pullable://gcr.io/test3/test3", + Digest: common.DigestSet{"sha256": "f1a8b8549c179f41e27ff3db0fe1a1793e4b109da46586501a8343637b1d0478"}, + }, + { + Name: "inputs/result", + URI: "abcd", + Digest: common.DigestSet{ + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + } + ctx := logtesting.TestContextWithLogger(t) + got, err := PipelineRunResolvedDependencies(ctx, proStructuredResults) + if err != nil { + t.Errorf("error while extracting resolvedDependencies: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("resolvedDependencies(): -want +got: %s", diff) + } +}