From 0bf01718b9bb8e1d3c452f1758be9e3679f79b5d Mon Sep 17 00:00:00 2001 From: Chuang Wang Date: Mon, 17 Oct 2022 11:59:37 -0400 Subject: [PATCH] Remove unsupported tekton-provenance storage in config (#594) tekton-provenance storage option is not supported anymore. Signed-off-by: Chuang Wang Signed-off-by: Chuang Wang --- .../formats/intotoite6/extract/extract.go | 26 +- .../intotoite6/extract/extract_test.go | 165 +++++ .../intotoite6/pipelinerun/pipelinerun.go | 3 +- .../formats/intotoite6/taskrun/taskrun.go | 6 +- pkg/chains/objects/objects.go | 25 +- pkg/chains/signing.go | 4 +- pkg/chains/storage/grafeas/grafeas.go | 161 +++-- pkg/chains/storage/grafeas/grafeas_test.go | 681 ++++++++++++------ pkg/chains/storage/oci/oci.go | 4 +- pkg/chains/storage/tekton/tekton.go | 8 +- pkg/config/config.go | 2 +- 11 files changed, 760 insertions(+), 325 deletions(-) create mode 100644 pkg/chains/formats/intotoite6/extract/extract_test.go diff --git a/pkg/chains/formats/intotoite6/extract/extract.go b/pkg/chains/formats/intotoite6/extract/extract.go index bdf8e1901c..366a336f32 100644 --- a/pkg/chains/formats/intotoite6/extract/extract.go +++ b/pkg/chains/formats/intotoite6/extract/extract.go @@ -17,6 +17,7 @@ limitations under the License. package extract import ( + "fmt" "sort" "strings" @@ -30,8 +31,13 @@ import ( "go.uber.org/zap" ) -// SubjectDigests extracts OCI images and other structured results from the TaskRun and PipelineRun based on standard hinting set up -// It also goes through looking for any PipelineResources of Image type +// SubjectDigests returns software artifacts produced from the TaskRun/PipelineRun object +// in the form of standard subject field of intoto statement. +// The type hinting fields expected in results help identify the generated software artifacts. +// Valid type hinting fields must: +// - have suffix `IMAGE_URL` & `IMAGE_DIGEST` or `ARTIFACT_URI` & `ARTIFACT_DIGEST` pair. +// - the `*_DIGEST` field must be in the format of ":" where the algorithm must be "sha256" and actual sha must be valid per https://github.com/opencontainers/image-spec/blob/main/descriptor.md#sha-256. +// - the `*_URL` or `*_URI` fields cannot be empty. func SubjectDigests(obj objects.TektonObject, logger *zap.SugaredLogger) []intoto.Subject { var subjects []intoto.Subject @@ -118,3 +124,19 @@ func SubjectDigests(obj objects.TektonObject, logger *zap.SugaredLogger) []intot }) return subjects } + +// RetrieveAllArtifactURIs returns all the URIs of the software artifacts produced from the run object. +// - It first extracts intoto subjects from run object results and converts the subjects +// to a slice of string URIs in the format of "NAME" + "@" + "ALGORITHM" + ":" + "DIGEST". +// - If no subjects could be extracted from results, then an empty slice is returned. +func RetrieveAllArtifactURIs(obj objects.TektonObject, logger *zap.SugaredLogger) []string { + result := []string{} + subjects := SubjectDigests(obj, logger) + + for _, s := range subjects { + for algo, digest := range s.Digest { + result = append(result, fmt.Sprintf("%s@%s:%s", s.Name, algo, digest)) + } + } + return result +} diff --git a/pkg/chains/formats/intotoite6/extract/extract_test.go b/pkg/chains/formats/intotoite6/extract/extract_test.go new file mode 100644 index 0000000000..80a0cf402d --- /dev/null +++ b/pkg/chains/formats/intotoite6/extract/extract_test.go @@ -0,0 +1,165 @@ +/* +Copyright 2022 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 extract_test + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + intoto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + logtesting "knative.dev/pkg/logging/testing" +) + +const ( + artifactURL1 = "gcr.io/test/kaniko-chains1" + artifactDigest1 = "a2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9" + artifactURL2 = "us-central1-maven.pkg.dev/test/java" + artifactDigest2 = "b2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9" +) + +func TestSubjectDigests(t *testing.T) { + var tests = []struct { + name string + // a map of url:digest pairs for type hinting results + results map[string]string + wantSubjects []intoto.Subject + wantFullURLs []string + }{ + { + name: "valid type hinting result fields", + results: map[string]string{ + artifactURL1: "sha256:" + artifactDigest1, + artifactURL2: "sha256:" + artifactDigest2, + }, + wantSubjects: []intoto.Subject{ + { + Name: artifactURL1, + Digest: map[string]string{ + "sha256": artifactDigest1, + }, + }, + { + Name: artifactURL2, + Digest: map[string]string{ + "sha256": artifactDigest2, + }, + }, + }, + wantFullURLs: []string{ + fmt.Sprintf("%s@sha256:%s", artifactURL1, artifactDigest1), + fmt.Sprintf("%s@sha256:%s", artifactURL2, artifactDigest2), + }, + }, + { + name: "invalid/missing digest algorithm name", + results: map[string]string{ + artifactURL1: "sha1:" + artifactDigest1, + artifactURL2: artifactDigest2, + }, + wantSubjects: nil, + wantFullURLs: []string{}, + }, + { + name: "invalid digest sha", + results: map[string]string{ + artifactURL1: "sha256:a123", + }, + wantSubjects: nil, + wantFullURLs: []string{}, + }, + { + name: "invalid url value", + results: map[string]string{ + "": "sha256:" + artifactDigest1, + }, + wantSubjects: nil, + wantFullURLs: []string{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // test both taskrun object and pipelinerun object + runObjects := []objects.TektonObject{ + createTaskRunObjectWithResults(tc.results), + createPipelineRunObjectWithResults(tc.results), + } + + for _, o := range runObjects { + gotSubjects := extract.SubjectDigests(o, logtesting.TestLogger(t)) + if diff := cmp.Diff(tc.wantSubjects, gotSubjects, cmpopts.SortSlices(func(x, y intoto.Subject) bool { return x.Name < y.Name })); diff != "" { + t.Errorf("Wrong subjects extracted, diff=%s", diff) + } + + gotURIs := extract.RetrieveAllArtifactURIs(o, logtesting.TestLogger(t)) + if diff := cmp.Diff(tc.wantFullURLs, gotURIs, cmpopts.SortSlices(func(x, y string) bool { return x < y })); diff != "" { + t.Errorf("Wrong URIs extracted, diff=%s", diff) + } + } + + }) + } +} + +func createTaskRunObjectWithResults(results map[string]string) objects.TektonObject { + trResults := []v1beta1.TaskRunResult{} + prefix := 0 + for url, digest := range results { + trResults = append(trResults, + v1beta1.TaskRunResult{Name: fmt.Sprintf("%v_IMAGE_DIGEST", prefix), Value: *v1beta1.NewStructuredValues(digest)}, + v1beta1.TaskRunResult{Name: fmt.Sprintf("%v_IMAGE_URL", prefix), Value: *v1beta1.NewStructuredValues(url)}, + ) + prefix++ + } + + return objects.NewTaskRunObject( + &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: trResults, + }, + }, + }, + ) +} + +func createPipelineRunObjectWithResults(results map[string]string) objects.TektonObject { + prResults := []v1beta1.PipelineRunResult{} + prefix := 0 + for url, digest := range results { + prResults = append(prResults, + v1beta1.PipelineRunResult{Name: fmt.Sprintf("%v_IMAGE_DIGEST", prefix), Value: *v1beta1.NewStructuredValues(digest)}, + v1beta1.PipelineRunResult{Name: fmt.Sprintf("%v_IMAGE_URL", prefix), Value: *v1beta1.NewStructuredValues(url)}, + ) + prefix++ + } + + return objects.NewPipelineRunObject( + &v1beta1.PipelineRun{ + Status: v1beta1.PipelineRunStatus{ + PipelineRunStatusFields: v1beta1.PipelineRunStatusFields{ + PipelineResults: prResults, + }, + }, + }, + ) +} diff --git a/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go b/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go index f9f97207f9..7e8b5b8c40 100644 --- a/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go +++ b/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go @@ -14,7 +14,6 @@ limitations under the License. package pipelinerun import ( - "fmt" "time" intoto "github.com/in-toto/in-toto-golang/in_toto" @@ -58,7 +57,7 @@ func GenerateAttestation(builderID string, pro *objects.PipelineRunObject, logge Builder: slsa.ProvenanceBuilder{ ID: builderID, }, - BuildType: fmt.Sprintf("%s/%s", pro.GetGroupVersionKind().GroupVersion().String(), pro.GetGroupVersionKind().Kind), + BuildType: pro.GetGVK(), Invocation: invocation(pro), BuildConfig: buildConfig(pro, logger), Metadata: metadata(pro), diff --git a/pkg/chains/formats/intotoite6/taskrun/taskrun.go b/pkg/chains/formats/intotoite6/taskrun/taskrun.go index 1a52749c95..0c7639f6ed 100644 --- a/pkg/chains/formats/intotoite6/taskrun/taskrun.go +++ b/pkg/chains/formats/intotoite6/taskrun/taskrun.go @@ -14,8 +14,6 @@ limitations under the License. package taskrun import ( - "fmt" - intoto "github.com/in-toto/in-toto-golang/in_toto" slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" "github.com/tektoncd/chains/pkg/artifacts" @@ -30,8 +28,6 @@ import ( func GenerateAttestation(builderID string, tro *objects.TaskRunObject, logger *zap.SugaredLogger) (interface{}, error) { subjects := extract.SubjectDigests(tro, logger) - tr := tro.GetObject().(*v1beta1.TaskRun) - att := intoto.ProvenanceStatement{ StatementHeader: intoto.StatementHeader{ Type: intoto.StatementInTotoV01, @@ -42,7 +38,7 @@ func GenerateAttestation(builderID string, tro *objects.TaskRunObject, logger *z Builder: slsa.ProvenanceBuilder{ ID: builderID, }, - BuildType: fmt.Sprintf("%s/%s", tr.GetGroupVersionKind().GroupVersion().String(), tr.GetGroupVersionKind().Kind), + BuildType: tro.GetGVK(), Invocation: invocation(tro), BuildConfig: buildConfig(tro), Metadata: metadata(tro), diff --git a/pkg/chains/objects/objects.go b/pkg/chains/objects/objects.go index 0217fce0ff..9e9828d434 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -16,6 +16,7 @@ package objects import ( "context" "errors" + "fmt" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -51,7 +52,7 @@ type Result struct { // to Tekton objects. type TektonObject interface { Object - GetKind() string + GetGVK() string GetObject() interface{} GetLatestAnnotations(ctx context.Context, clientSet versioned.Interface) (map[string]string, error) Patch(ctx context.Context, clientSet versioned.Interface, patchBytes []byte) error @@ -78,17 +79,17 @@ type TaskRunObject struct { *v1beta1.TaskRun } +var _ TektonObject = &TaskRunObject{} + func NewTaskRunObject(tr *v1beta1.TaskRun) *TaskRunObject { return &TaskRunObject{ tr, } } -// Get the TaskRun kind -func (tro *TaskRunObject) GetKind() string { - // TODO: Want to use tro.GetObjectKind().GroupVersionKind().Kind but - // never seems to be populated - return "taskrun" +// Get the TaskRun GroupVersionKind +func (tro *TaskRunObject) GetGVK() string { + return fmt.Sprintf("%s/%s", tro.GetGroupVersionKind().GroupVersion().String(), tro.GetGroupVersionKind().Kind) } // Get the latest annotations on the TaskRun @@ -135,21 +136,21 @@ func (tro *TaskRunObject) GetPullSecrets() []string { type PipelineRunObject struct { // The base PipelineRun *v1beta1.PipelineRun - // TaskRuns that were apart of this PipelineRun + // taskRuns that were apart of this PipelineRun taskRuns []*v1beta1.TaskRun } +var _ TektonObject = &PipelineRunObject{} + func NewPipelineRunObject(pr *v1beta1.PipelineRun) *PipelineRunObject { return &PipelineRunObject{ PipelineRun: pr, } } -// Get the PipelineRun kind -func (pro *PipelineRunObject) GetKind() string { - // TODO: Want to use tro.GetObjectKind().GroupVersionKind().Kind but - // never seems to be populated - return "pipelinerun" +// Get the PipelineRun GroupVersionKind +func (pro *PipelineRunObject) GetGVK() string { + return fmt.Sprintf("%s/%s", pro.GetGroupVersionKind().GroupVersion().String(), pro.GetGroupVersionKind().Kind) } // Request the current annotations on the PipelineRun object diff --git a/pkg/chains/signing.go b/pkg/chains/signing.go index dfb54f12e2..53daa3a465 100644 --- a/pkg/chains/signing.go +++ b/pkg/chains/signing.go @@ -165,7 +165,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) payloader, ok := o.Formatters[payloadFormat] if !ok { - logger.Warnf("Format %s configured for %s: %v was not found", payloadFormat, tektonObj.GetKind(), signableType.Type()) + logger.Warnf("Format %s configured for %s: %v was not found", payloadFormat, tektonObj.GetGVK(), signableType.Type()) continue } @@ -181,7 +181,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) logger.Error(err) continue } - logger.Infof("Created payload of type %s for %s %s/%s", string(payloadFormat), tektonObj.GetKind(), tektonObj.GetNamespace(), tektonObj.GetName()) + logger.Infof("Created payload of type %s for %s %s/%s", string(payloadFormat), tektonObj.GetGVK(), tektonObj.GetNamespace(), tektonObj.GetName()) // Sign it! signerType := signableType.Signer(cfg) diff --git a/pkg/chains/storage/grafeas/grafeas.go b/pkg/chains/storage/grafeas/grafeas.go index 4f9469c4b8..e317590776 100644 --- a/pkg/chains/storage/grafeas/grafeas.go +++ b/pkg/chains/storage/grafeas/grafeas.go @@ -20,14 +20,13 @@ import ( "fmt" "strings" - "github.com/google/go-containerregistry/pkg/name" grafeasutil "github.com/grafeas/grafeas/go/utils/intoto" pb "github.com/grafeas/grafeas/proto/v1/grafeas_go_proto" intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/types" - "github.com/tektoncd/chains/pkg/artifacts" "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -40,11 +39,12 @@ import ( ) const ( - StorageBackendGrafeas = "grafeas" - projectPath = "projects/%s" - notePath = "projects/%s/notes/%s" - attestationNoteNameFormat = "%s-simplesigning" - buildNoteNameFormat = "%s-intoto" + StorageBackendGrafeas = "grafeas" + projectPathFormat = "projects/%s" + notePathFormat = "projects/%s/notes/%s" + attestationNoteNameFormat = "%s-simplesigning" + pipelinerunBuildNoteNameFormat = "%s-pipelinerun-intoto" + taskrunBuildNoteNameFormat = "%s-taskrun-intoto" ) // Backend is a storage backend that stores signed payloads in the storage that @@ -89,9 +89,7 @@ func NewStorageBackend(ctx context.Context, logger *zap.SugaredLogger, cfg confi // StorePayload implements the storage.Backend interface. func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, rawPayload []byte, signature string, opts config.StorageOpts) error { - // TODO: Gracefully handle unexpected type - tr := obj.GetObject().(*v1beta1.TaskRun) - // We only support simplesigning for OCI images, and in-toto for taskrun. + // We only support simplesigning for OCI images, and in-toto for taskrun & pipelinerun. if opts.PayloadFormat != formats.PayloadTypeInTotoIte6 && opts.PayloadFormat != formats.PayloadTypeSimpleSigning { return errors.New("Grafeas storage backend only supports simplesigning and intoto payload format.") } @@ -103,40 +101,43 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra // check if noteID is configured. If not, we give it a name as `tekton-` if b.cfg.Storage.Grafeas.NoteID == "" { - generatedNoteID := fmt.Sprintf("tekton-%s", tr.GetNamespace()) + generatedNoteID := fmt.Sprintf("tekton-%s", obj.GetNamespace()) b.cfg.Storage.Grafeas.NoteID = generatedNoteID } // step1: create note // If the note already exists, just move to the next step of creating occurrence. - if _, err := b.createNote(ctx, tr, opts); err != nil && status.Code(err) != codes.AlreadyExists { + if _, err := b.createNote(ctx, obj, opts); err != nil && status.Code(err) != codes.AlreadyExists { return err } // step2: create occurrences - occurrences, err := b.createOccurrence(ctx, tr, rawPayload, signature, opts) + occurrences, err := b.createOccurrence(ctx, obj, rawPayload, signature, opts) if err != nil { return err } - names := []string{} + occNames := []string{} for _, occ := range occurrences { - names = append(names, occ.GetName()) + occNames = append(occNames, occ.GetName()) + } + + if len(occNames) == 0 { + b.logger.Infof("No occurrences created for payload of type %s for %s %s/%s", string(opts.PayloadFormat), obj.GetGVK(), obj.GetNamespace(), obj.GetName()) + } else { + b.logger.Infof("Successfully created grafeas occurrences %v for %s %s/%s", occNames, obj.GetGVK(), obj.GetNamespace(), obj.GetName()) } - b.logger.Infof("Successfully stored signature in the grafeas occurrences %s.", names) return nil } // Retrieve payloads from grafeas server and store it in a map func (b *Backend) RetrievePayloads(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string]string, error) { - // TODO: Gracefully handle unexpected type - tr := obj.GetObject().(*v1beta1.TaskRun) // initialize an empty map for result result := make(map[string]string) // get all occurrences created using this backend - occurrences, err := b.getAllOccurrences(ctx, tr, opts) + occurrences, err := b.getAllOccurrences(ctx, obj, opts) if err != nil { return nil, err } @@ -156,13 +157,11 @@ func (b *Backend) RetrievePayloads(ctx context.Context, obj objects.TektonObject // Retrieve signatures from grafeas server and store it in a map func (b *Backend) RetrieveSignatures(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string][]string, error) { - // TODO: Gracefully handle unexpected type - tr := obj.GetObject().(*v1beta1.TaskRun) // initialize an empty map for result result := make(map[string][]string) // get all occurrences created using this backend - occurrences, err := b.getAllOccurrences(ctx, tr, opts) + occurrences, err := b.getAllOccurrences(ctx, obj, opts) if err != nil { return nil, err } @@ -191,15 +190,15 @@ func (b *Backend) Type() string { // ----------------------------- Helper Functions ---------------------------- // createNote creates grafeas note that will be linked to grafeas occurrences -func (b *Backend) createNote(ctx context.Context, tr *v1beta1.TaskRun, opts config.StorageOpts) (*pb.Note, error) { - noteID := b.cfg.Storage.Grafeas.NoteID +func (b *Backend) createNote(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (*pb.Note, error) { + notePrefix := b.cfg.Storage.Grafeas.NoteID // for oci image: AttestationNote if opts.PayloadFormat == formats.PayloadTypeSimpleSigning { return b.client.CreateNote(ctx, &pb.CreateNoteRequest{ Parent: b.getProjectPath(), - NoteId: fmt.Sprintf(attestationNoteNameFormat, noteID), + NoteId: fmt.Sprintf(attestationNoteNameFormat, b.cfg.Storage.Grafeas.NoteID), Note: &pb.Note{ ShortDescription: "OCI Artifact Attestation Note", Type: &pb.Note_Attestation{ @@ -214,16 +213,28 @@ func (b *Backend) createNote(ctx context.Context, tr *v1beta1.TaskRun, opts conf ) } - // for taskrun: BuildNote + switch obj.GetObject().(type) { + case *v1beta1.PipelineRun: + // create the build note for pipelinerun attestations + return b.createBuildNote(ctx, fmt.Sprintf(pipelinerunBuildNoteNameFormat, notePrefix), obj) + case *v1beta1.TaskRun: + // create the build note for taskrun attestations + return b.createBuildNote(ctx, fmt.Sprintf(taskrunBuildNoteNameFormat, notePrefix), obj) + default: + return nil, errors.New("Tried to create build note for unsupported type of object") + } +} + +func (b *Backend) createBuildNote(ctx context.Context, noteid string, obj objects.TektonObject) (*pb.Note, error) { return b.client.CreateNote(ctx, &pb.CreateNoteRequest{ Parent: b.getProjectPath(), - NoteId: fmt.Sprintf(buildNoteNameFormat, noteID), + NoteId: noteid, Note: &pb.Note{ - ShortDescription: "Build Provenance Note", + ShortDescription: "Build Provenance Note for TaskRun", Type: &pb.Note_Build{ Build: &pb.BuildNote{ - BuilderVersion: tr.GetGroupVersionKind().GroupVersion().String(), + BuilderVersion: obj.GetGVK(), }, }, }, @@ -236,16 +247,16 @@ func (b *Backend) createNote(ctx context.Context, tr *v1beta1.TaskRun, opts conf // - its simplesigning payload and signature will be stored in ATTESTATION occurrence // - the identifier/ResourceUri is IMAGE_URL@IMAGE_DIGEST // -// for a taskrun object -// - its intoto payload and signature will be stored in a BUILD occurrences for each image artifact generated from the taskrun +// for a taskrun/pipelinerun object +// - its intoto payload and signature will be stored in a BUILD occurrences for each image artifact generated from the taskrun/pipelinerun // - each BUILD occurrence will have the same data but differ in the ResourceUri field // - the identifier/ResourceUri is IMAGE_URL@IMAGE_DIGEST -func (b *Backend) createOccurrence(ctx context.Context, tr *v1beta1.TaskRun, payload []byte, signature string, opts config.StorageOpts) ([]*pb.Occurrence, error) { +func (b *Backend) createOccurrence(ctx context.Context, obj objects.TektonObject, payload []byte, signature string, opts config.StorageOpts) ([]*pb.Occurrence, error) { occs := []*pb.Occurrence{} // create Occurrence_Attestation for OCI if opts.PayloadFormat == formats.PayloadTypeSimpleSigning { - occ, err := b.createAttestationOccurrence(ctx, tr, payload, signature, opts.FullKey) + occ, err := b.createAttestationOccurrence(ctx, payload, signature, opts.FullKey) if err != nil { return nil, err } @@ -254,9 +265,9 @@ func (b *Backend) createOccurrence(ctx context.Context, tr *v1beta1.TaskRun, pay } // create Occurrence_Build for TaskRun - allURIs := b.retrieveAllArtifactIdentifiers(tr) + allURIs := extract.RetrieveAllArtifactURIs(obj, b.logger) for _, uri := range allURIs { - occ, err := b.createBuildOccurrence(ctx, tr, payload, signature, uri) + occ, err := b.createBuildOccurrence(ctx, obj, payload, signature, uri) if err != nil { return nil, err } @@ -265,7 +276,7 @@ func (b *Backend) createOccurrence(ctx context.Context, tr *v1beta1.TaskRun, pay return occs, nil } -func (b *Backend) createAttestationOccurrence(ctx context.Context, tr *v1beta1.TaskRun, payload []byte, signature string, uri string) (*pb.Occurrence, error) { +func (b *Backend) createAttestationOccurrence(ctx context.Context, payload []byte, signature string, uri string) (*pb.Occurrence, error) { occurrenceDetails := &pb.Occurrence_Attestation{ Attestation: &pb.AttestationOccurrence{ SerializedPayload: payload, @@ -303,7 +314,7 @@ func (b *Backend) createAttestationOccurrence(ctx context.Context, tr *v1beta1.T ) } -func (b *Backend) createBuildOccurrence(ctx context.Context, tr *v1beta1.TaskRun, payload []byte, signature string, uri string) (*pb.Occurrence, error) { +func (b *Backend) createBuildOccurrence(ctx context.Context, obj objects.TektonObject, payload []byte, signature string, uri string) (*pb.Occurrence, error) { in := intoto.ProvenanceStatement{} if err := json.Unmarshal(payload, &in); err != nil { return nil, err @@ -336,7 +347,7 @@ func (b *Backend) createBuildOccurrence(ctx context.Context, tr *v1beta1.TaskRun Parent: b.getProjectPath(), Occurrence: &pb.Occurrence{ ResourceUri: uri, - NoteName: b.getBuildNotePath(), + NoteName: b.getBuildNotePath(obj), Details: occurrenceDetails, Envelope: envelope, }, @@ -346,44 +357,66 @@ func (b *Backend) createBuildOccurrence(ctx context.Context, tr *v1beta1.TaskRun func (b *Backend) getProjectPath() string { projectID := b.cfg.Storage.Grafeas.ProjectID - return fmt.Sprintf(projectPath, projectID) + return fmt.Sprintf(projectPathFormat, projectID) } func (b *Backend) getAttestationNotePath() string { projectID := b.cfg.Storage.Grafeas.ProjectID noteID := b.cfg.Storage.Grafeas.NoteID - return fmt.Sprintf(notePath, projectID, fmt.Sprintf(attestationNoteNameFormat, noteID)) + return fmt.Sprintf(notePathFormat, projectID, fmt.Sprintf(attestationNoteNameFormat, noteID)) } -func (b *Backend) getBuildNotePath() string { +func (b *Backend) getBuildNotePath(obj objects.TektonObject) string { projectID := b.cfg.Storage.Grafeas.ProjectID noteID := b.cfg.Storage.Grafeas.NoteID - return fmt.Sprintf(notePath, projectID, fmt.Sprintf(buildNoteNameFormat, noteID)) + + switch obj.GetObject().(type) { + case *v1beta1.PipelineRun: + return fmt.Sprintf(notePathFormat, projectID, fmt.Sprintf(pipelinerunBuildNoteNameFormat, noteID)) + case *v1beta1.TaskRun: + return fmt.Sprintf(notePathFormat, projectID, fmt.Sprintf(taskrunBuildNoteNameFormat, noteID)) + } + return "" } // getAllOccurrences retrieves back all occurrences created for a taskrun -func (b *Backend) getAllOccurrences(ctx context.Context, tr *v1beta1.TaskRun, opts config.StorageOpts) ([]*pb.Occurrence, error) { +func (b *Backend) getAllOccurrences(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) ([]*pb.Occurrence, error) { + result := []*pb.Occurrence{} // step 1: get all resource URIs created under the taskrun - uriFilters := b.retrieveAllArtifactIdentifiers(tr) + uriFilters := extract.RetrieveAllArtifactURIs(obj, b.logger) - // step 2: find all occurrences by using ListOccurrences filters - occs, err := b.findOccurrencesForCriteria(ctx, b.getProjectPath(), uriFilters) - if err != nil { - return nil, err + // step 2: find all build occurrences + if opts.PayloadFormat == formats.PayloadTypeInTotoIte6 { + occs, err := b.findOccurrencesForCriteria(ctx, b.getBuildNotePath(obj), uriFilters) + if err != nil { + return nil, err + } + result = append(result, occs...) } - return occs, nil + + // step 3: find all attestation occurrences + if opts.PayloadFormat == formats.PayloadTypeSimpleSigning { + occs, err := b.findOccurrencesForCriteria(ctx, b.getAttestationNotePath(), uriFilters) + if err != nil { + return nil, err + } + result = append(result, occs...) + } + + return result, nil } // findOccurrencesForCriteria lookups a project's occurrences by the resource uri -func (b *Backend) findOccurrencesForCriteria(ctx context.Context, projectPath string, resourceURIs []string) ([]*pb.Occurrence, error) { +func (b *Backend) findOccurrencesForCriteria(ctx context.Context, noteName string, resourceURIs []string) ([]*pb.Occurrence, error) { + var uriFilters []string for _, url := range resourceURIs { uriFilters = append(uriFilters, fmt.Sprintf("resourceUrl=%q", url)) } - occurences, err := b.client.ListOccurrences(ctx, - &pb.ListOccurrencesRequest{ - Parent: projectPath, + occurences, err := b.client.ListNoteOccurrences(ctx, + &pb.ListNoteOccurrencesRequest{ + Name: noteName, Filter: strings.Join(uriFilters, " OR "), }, ) @@ -393,25 +426,3 @@ func (b *Backend) findOccurrencesForCriteria(ctx context.Context, projectPath st } return occurences.GetOccurrences(), nil } - -// retrieve the uri of all images generated from a specific taskrun in the format of `IMAGE_URL@IMAGE_DIGEST` -func (b *Backend) retrieveAllArtifactIdentifiers(tr *v1beta1.TaskRun) []string { - result := []string{} - // for image artifacts - trObj := objects.NewTaskRunObject(tr) - images := artifacts.ExtractOCIImagesFromResults(trObj, b.logger) - for _, image := range images { - ref, ok := image.(name.Digest) - if !ok { - continue - } - result = append(result, ref.Name()) - } - - // for other signable artifacts - artifacts := artifacts.ExtractSignableTargetFromResults(trObj, b.logger) - for _, a := range artifacts { - result = append(result, a.FullRef()) - } - return result -} diff --git a/pkg/chains/storage/grafeas/grafeas_test.go b/pkg/chains/storage/grafeas/grafeas_test.go index f085320ff9..1ce8a60333 100644 --- a/pkg/chains/storage/grafeas/grafeas_test.go +++ b/pkg/chains/storage/grafeas/grafeas_test.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Tekton Authors +Copyright 2022 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 @@ -15,6 +15,7 @@ package grafeas import ( "context" + "encoding/json" "fmt" "net" "sort" @@ -22,40 +23,190 @@ import ( "testing" "github.com/google/go-cmp/cmp" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" pb "github.com/grafeas/grafeas/proto/v1/grafeas_go_proto" "github.com/tektoncd/chains/pkg/config" - "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" gstatus "google.golang.org/grpc/status" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" logtesting "knative.dev/pkg/logging/testing" rtesting "knative.dev/pkg/reconciler/testing" ) +const ( + ProjectID = "test-project" + NoteID = "test-note" + // repo information for clone taskrun + repoURL = "https://github.com/test/tekton-test.git" + commitSHA = "e02ae3576b4e621bd6798ccbfb89358121d896d7" + // image information for artifact build task + artifactURL1 = "gcr.io/test/kaniko-chains1" + artifactDigest1 = "a2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9" + artifactURL2 = "us-central1-maven.pkg.dev/test/java" + artifactDigest2 = "b2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9" +) + +// Those variables are +// - the clone taskrun and its provenance +// - the artifact build taskrun and its provenance +// - the CI pipelinerun that mocks as the owner of the two taskruns, and its provenance +var ( + // clone taskrun + // -------------- + cloneTaskRun = &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "git-clone", + UID: types.UID("uid-task1"), + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + {Name: "CHAINS-GIT_COMMIT", Value: *v1beta1.NewStructuredValues(commitSHA)}, + {Name: "CHAINS-GIT_URL", Value: *v1beta1.NewStructuredValues(repoURL)}, + }, + }, + }, + } + + // clone taskrun provenance + cloneTaskRunProvenance = intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Materials: []slsa.ProvenanceMaterial{ + { + URI: repoURL, + Digest: slsa.DigestSet{ + "sha1": commitSHA, + }, + }, + }, + }, + } + + artifactIdentifier1 = fmt.Sprintf("%s@sha256:%s", artifactURL1, artifactDigest1) + artifactIdentifier2 = fmt.Sprintf("%s@sha256:%s", artifactURL2, artifactDigest2) + + // artifact build taskrun + buildTaskRun = &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "artifact-build", + UID: types.UID("uid-task2"), + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + {Name: "IMAGE_DIGEST", Value: *v1beta1.NewStructuredValues("sha256:" + artifactDigest1)}, + {Name: "IMAGE_URL", Value: *v1beta1.NewStructuredValues(artifactURL1)}, + {Name: "x_ARTIFACT_DIGEST", Value: *v1beta1.NewStructuredValues("sha256:" + artifactDigest2)}, + {Name: "x_ARTIFACT_URI", Value: *v1beta1.NewStructuredValues(artifactURL2)}, + }, + }, + }, + } + + // artifact built taskrun provenance + buildTaskRunProvenance = intoto.ProvenanceStatement{ + StatementHeader: intoto.StatementHeader{ + Subject: []intoto.Subject{ + { + Name: artifactURL1, + Digest: slsa.DigestSet{ + "sha256": artifactDigest1, + }, + }, + { + Name: artifactURL2, + Digest: slsa.DigestSet{ + "sha256": artifactDigest2, + }, + }, + }, + }, + } + + // ci pipelinerun + ciPipeline = &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "ci-pipeline", + UID: types.UID("uid-pipeline"), + }, + Status: v1beta1.PipelineRunStatus{ + PipelineRunStatusFields: v1beta1.PipelineRunStatusFields{ + PipelineResults: []v1beta1.PipelineRunResult{ + // the results from task 1 - clone + {Name: "CHAINS-GIT_COMMIT", Value: *v1beta1.NewStructuredValues(commitSHA)}, + {Name: "CHAINS-GIT_URL", Value: *v1beta1.NewStructuredValues(repoURL)}, + // the results from task 2 - build + {Name: "IMAGE_DIGEST", Value: *v1beta1.NewStructuredValues("sha256:" + artifactDigest1)}, + {Name: "IMAGE_URL", Value: *v1beta1.NewStructuredValues(artifactURL1)}, + {Name: "x_ARTIFACT_DIGEST", Value: *v1beta1.NewStructuredValues("sha256:" + artifactDigest2)}, + {Name: "x_ARTIFACT_URI", Value: *v1beta1.NewStructuredValues(artifactURL2)}, + }, + }, + }, + } + + // ci pipelinerun provenance + ciPipelineRunProvenance = intoto.ProvenanceStatement{ + StatementHeader: buildTaskRunProvenance.StatementHeader, + Predicate: slsa.ProvenancePredicate{ + Materials: cloneTaskRunProvenance.Predicate.Materials, + }, + } +) + type args struct { - tr *v1beta1.TaskRun + runObject objects.TektonObject payload []byte signature string opts config.StorageOpts } type testConfig struct { - name string - args args - wantErr bool + name string + args args + wantOccurrences []*pb.Occurrence + wantErr bool } // This function is to test the implementation of the fake server's ListOccurrences function. // As the filter logic is implemented, we want to make sure it can be trusted before testing store & retrieve. func TestBackend_ListOccurrences(t *testing.T) { - // get grafeas client + var tests = []struct { + name string + filter string + wantOccurrences []*pb.Occurrence + }{ + { + name: "empty filter", + wantOccurrences: []*pb.Occurrence{getPipelineRunBuildOcc(t, artifactIdentifier1), getPipelineRunBuildOcc(t, artifactIdentifier2)}, + }, + { + name: "multiple filters", + filter: fmt.Sprintf(`resourceUrl="%s" OR resourceUrl="%s"`, artifactIdentifier1, artifactIdentifier2), + wantOccurrences: []*pb.Occurrence{getPipelineRunBuildOcc(t, artifactIdentifier1), getPipelineRunBuildOcc(t, artifactIdentifier2)}, + }, + { + name: "a single filter", + filter: fmt.Sprintf(`resourceUrl="%s"`, artifactIdentifier1), + wantOccurrences: []*pb.Occurrence{getPipelineRunBuildOcc(t, artifactIdentifier1)}, + }, + } + + // setup ctx, _ := rtesting.SetupFakeContext(t) conn, client, err := setupConnection() @@ -64,136 +215,115 @@ func TestBackend_ListOccurrences(t *testing.T) { } defer conn.Close() - // two sample occurrences used for testing - occs := getExpectedOccurrences() + // store two sample occurrences into the fake server + occs := []*pb.Occurrence{getPipelineRunBuildOcc(t, artifactIdentifier1), getPipelineRunBuildOcc(t, artifactIdentifier2)} - // store occurrences into the fake server for _, occ := range occs { - _, err := client.CreateOccurrence(ctx, &pb.CreateOccurrenceRequest{Occurrence: occ}) - if err != nil { - t.Fatal("Failed to create occurrence in the server") + // create note + _, err := client.CreateNote(ctx, &pb.CreateNoteRequest{ + Parent: "projects/" + ProjectID, + NoteId: fmt.Sprintf("%s-pipelinerun-intoto", NoteID), + Note: &pb.Note{ + Name: occ.NoteName, + }, + }) + if err != nil && status.Code(err) != codes.AlreadyExists { + t.Fatal("Failed to create notes in the server, err:", err) } - } - - // construct expected ListOccurrencesResponse - wantedResponse := &pb.ListOccurrencesResponse{Occurrences: occs} - // 1. test empty filter string - return all occurrences - got, err := client.ListOccurrences(ctx, &pb.ListOccurrencesRequest{}) - if err != nil { - t.Fatalf("Failed to call ListOccurrences: %v", err) - } - if diff := cmp.Diff(got, wantedResponse, protocmp.Transform()); diff != "" { - t.Errorf("Wrong list of occurrences received for empty filter, diff=%s", diff) - } - - // 2. test multiple chained filter - return multiple occurrences - got, err = client.ListOccurrences(ctx, &pb.ListOccurrencesRequest{ - Filter: `resourceUrl="gcr.io/test/kaniko-chains1@sha256:cfe4f0bf41c80609214f9b8ec0408b1afb28b3ced343b944aaa05d47caba3e00" OR resourceUrl="gcr.io/test/kaniko-chains1@sha256:a2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9"`, - }) - if err != nil { - t.Fatalf("Failed to call ListOccurrences: %v", err) - } - - wantedResponse = &pb.ListOccurrencesResponse{Occurrences: occs[:2]} - if diff := cmp.Diff(got, wantedResponse, protocmp.Transform()); diff != "" { - t.Errorf("Wrong list of occurrences received for multiple filters, diff=%s", diff) + // create occurrence + _, err = client.CreateOccurrence(ctx, &pb.CreateOccurrenceRequest{Occurrence: occ}) + if err != nil { + t.Fatal("Failed to create occurrence in the server, err:", err) + } } - // 3. test a single filter - return one occurrence - got, err = client.ListOccurrences(ctx, &pb.ListOccurrencesRequest{ - Filter: `resourceUrl="gcr.io/test/kaniko-chains1@sha256:cfe4f0bf41c80609214f9b8ec0408b1afb28b3ced343b944aaa05d47caba3e00"`, - }) - if err != nil { - t.Fatalf("Failed to call ListOccurrences: %v", err) - } + for _, tc := range tests { + got, err := client.ListOccurrences(ctx, &pb.ListOccurrencesRequest{Filter: tc.filter}) + if err != nil { + t.Fatalf("Failed to call ListOccurrences: %v", err) + } - wantedResponse = &pb.ListOccurrencesResponse{Occurrences: occs[1:2]} - if diff := cmp.Diff(got, wantedResponse, protocmp.Transform()); diff != "" { - t.Errorf("Wrong list of occurrences received for a single filter, diff=%s", diff) + want := &pb.ListOccurrencesResponse{Occurrences: tc.wantOccurrences} + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { + t.Errorf("Wrong list of occurrences received, diff=%s", diff) + } } } -/* - This function is to test - -- if the StorePayload function can create correct occurrences and store them into grafeas server -- if the RetrievePayloads and RetrieveSignatures functions work properly to fetch correct payloads and signatures -*/ +// This function is to test +// - if the StorePayload function can create correct occurrences and store them into grafeas server +// - if the RetrievePayloads and RetrieveSignatures functions work properly to fetch correct payloads and signatures func TestGrafeasBackend_StoreAndRetrieve(t *testing.T) { tests := []testConfig{ { - name: "intoto for taskrun, no error", + name: "intoto for clone taskrun, no error, no occurrences created because no artifacts were built.", args: args{ - tr: &v1beta1.TaskRun{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo1", - Name: "bar1", - UID: types.UID("uid1"), - }, - Status: v1beta1.TaskRunStatus{ - TaskRunStatusFields: v1beta1.TaskRunStatusFields{ - TaskRunResults: []v1beta1.TaskRunResult{ - // the image digest for test purpose also needs to follow image digest protocol to pass the check. - // i.e. only contain chars in "sh:0123456789abcdef", and the lenth is 7+64. See more details here: - // https://github.com/google/go-containerregistry/blob/d9bfbcb99e526b2a9417160e209b816e1b1fb6bd/pkg/name/digest.go#L63 - {Name: "IMAGE_DIGEST", Value: *v1beta1.NewArrayOrString("sha256:a2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9")}, - {Name: "IMAGE_URL", Value: *v1beta1.NewArrayOrString("gcr.io/test/kaniko-chains1")}, - {Name: "x_ARTIFACT_DIGEST", Value: *v1beta1.NewArrayOrString("sha256:b2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9")}, - {Name: "x_ARTIFACT_URI", Value: *v1beta1.NewArrayOrString("us-central1-maven.pkg.dev/test/java")}, - }, - }, - }, + runObject: &objects.TaskRunObject{ + TaskRun: cloneTaskRun, }, - payload: []byte("{}"), - signature: "taskrun signature", - opts: config.StorageOpts{FullKey: "tekton.dev-v1beta1-taskrun-uuid", PayloadFormat: formats.PayloadTypeInTotoIte6}, + payload: getRawPayload(t, cloneTaskRunProvenance), + signature: "clone taskrun signatures", + opts: config.StorageOpts{PayloadFormat: formats.PayloadTypeInTotoIte6}, }, - wantErr: false, + wantOccurrences: nil, + wantErr: false, }, { - name: "simplesigning for oci, no error", + name: "intoto for build taskrun, no error, 2 BUILD occurrences should be created for the 2 artifacts generated.", args: args{ - tr: &v1beta1.TaskRun{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo2", - Name: "bar2", - UID: types.UID("uid2"), - }, - Status: v1beta1.TaskRunStatus{ - TaskRunStatusFields: v1beta1.TaskRunStatusFields{ - TaskRunResults: []v1beta1.TaskRunResult{ - // the image digest for test purpose also needs to follow image digest protocol to pass the check. - // i.e. only contain chars in "sh:0123456789abcdef", and the lenth is 7+64. See more details here: - // https://github.com/google/go-containerregistry/blob/d9bfbcb99e526b2a9417160e209b816e1b1fb6bd/pkg/name/digest.go#L63 - {Name: "IMAGE_DIGEST", Value: *v1beta1.NewArrayOrString("sha256:cfe4f0bf41c80609214f9b8ec0408b1afb28b3ced343b944aaa05d47caba3e00")}, - {Name: "IMAGE_URL", Value: *v1beta1.NewArrayOrString("gcr.io/test/kaniko-chains1")}, - }, - }, - }, + runObject: &objects.TaskRunObject{ + TaskRun: buildTaskRun, }, - payload: []byte("oci payload"), - signature: "oci signature", - opts: config.StorageOpts{FullKey: "gcr.io/test/kaniko-chains1@sha256:cfe4f0bf41c80609214f9b8ec0408b1afb28b3ced343b944aaa05d47caba3e00", PayloadFormat: formats.PayloadTypeSimpleSigning}, + payload: getRawPayload(t, buildTaskRunProvenance), + signature: "build taskrun signature", + opts: config.StorageOpts{PayloadFormat: formats.PayloadTypeInTotoIte6}, }, - wantErr: false, + wantOccurrences: []*pb.Occurrence{getTaskRunBuildOcc(t, artifactIdentifier1), getTaskRunBuildOcc(t, artifactIdentifier2)}, + wantErr: false, }, { - name: "tekton format for taskrun, error", + name: "simplesigning for the build taskrun, no error, 1 ATTESTATION occurrence should be created for the artifact specified in storageopts.key", args: args{ - tr: &v1beta1.TaskRun{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo3", - Name: "bar3", - UID: types.UID("uid3"), - }, + runObject: &objects.TaskRunObject{ + TaskRun: buildTaskRun, }, - opts: config.StorageOpts{FullKey: "tekton.dev-v1beta1-taskrun2-uuid", PayloadFormat: formats.PayloadTypeTekton}, + payload: []byte("attestation payload"), + signature: "build taskrun image signature", + opts: config.StorageOpts{FullKey: artifactIdentifier1, PayloadFormat: formats.PayloadTypeSimpleSigning}, }, - wantErr: true, + wantOccurrences: []*pb.Occurrence{getTaskRunAttestationOcc(t, artifactIdentifier1)}, + wantErr: false, + }, + { + name: "intoto for the ci pipeline, no error, 2 occurences should be created for the pipelinerun for the 2 artifact generated.", + args: args{ + runObject: &objects.PipelineRunObject{ + PipelineRun: ciPipeline, + }, + payload: getRawPayload(t, ciPipelineRunProvenance), + signature: "ci pipelinerun signature", + opts: config.StorageOpts{PayloadFormat: formats.PayloadTypeInTotoIte6}, + }, + wantOccurrences: []*pb.Occurrence{getPipelineRunBuildOcc(t, artifactIdentifier1), getPipelineRunBuildOcc(t, artifactIdentifier2)}, + wantErr: false, + }, + { + name: "tekton format for a taskrun, error, only simplesigning and intoto are supported", + args: args{ + runObject: &objects.TaskRunObject{ + TaskRun: buildTaskRun, + }, + payload: []byte("foo"), + signature: "bar", + opts: config.StorageOpts{PayloadFormat: formats.PayloadTypeTekton}, + }, + wantOccurrences: nil, + wantErr: true, }, } + // setup connection ctx, _ := rtesting.SetupFakeContext(t) conn, client, err := setupConnection() if err != nil { @@ -202,7 +332,12 @@ func TestGrafeasBackend_StoreAndRetrieve(t *testing.T) { defer conn.Close() + // collect all the occurences expected to be created in the server + allOccurrencesInServer := []*pb.Occurrence{} + for _, test := range tests { + + // run the test t.Run(test.name, func(t *testing.T) { backend := Backend{ logger: logtesting.TestLogger(t), @@ -210,38 +345,52 @@ func TestGrafeasBackend_StoreAndRetrieve(t *testing.T) { cfg: config.Config{ Storage: config.StorageConfigs{ Grafeas: config.GrafeasConfig{ - ProjectID: "test-project", - NoteID: "test-note", + ProjectID: ProjectID, + NoteID: NoteID, }, }, }, } // test if the attestation of the taskrun/oci artifact can be successfully stored into grafeas server // and test if payloads and signatures inside the attestation can be retrieved. - testStoreAndRetrieve(ctx, t, test, backend) + testStoreAndRetrieveHelper(ctx, t, test, backend) + + // accumulate the expected occurrences from each call. + allOccurrencesInServer = append(allOccurrencesInServer, test.wantOccurrences...) }) } - // test if occurrences are created correctly from the StorePayload function + // test if all occurrences are created correctly from multiple requests // - ProjectID field in ListOccurrencesRequest doesn't matter here because we assume there is only one project in the mocked server. // - ListOccurrencesRequest with empty filter should be able to fetch all occurrences. got, err := client.ListOccurrences(ctx, &pb.ListOccurrencesRequest{}) if err != nil { t.Fatal("Failed to call ListOccurrences. error ", err) } - wantedResponse := &pb.ListOccurrencesResponse{Occurrences: getExpectedOccurrences()} - if diff := cmp.Diff(got, wantedResponse, protocmp.Transform()); diff != "" { + + sort.Slice(allOccurrencesInServer, func(i, j int) bool { + return allOccurrencesInServer[i].ResourceUri+allOccurrencesInServer[i].NoteName < allOccurrencesInServer[j].ResourceUri+allOccurrencesInServer[j].NoteName + }) + want := &pb.ListOccurrencesResponse{ + Occurrences: allOccurrencesInServer, + } + + if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" { t.Errorf("Wrong list of occurrences received for empty filter, diff=%s", diff) } } // test attestation storage and retrieval -func testStoreAndRetrieve(ctx context.Context, t *testing.T, test testConfig, backend Backend) { - trObj := objects.NewTaskRunObject(test.args.tr) - if err := backend.StorePayload(ctx, trObj, test.args.payload, test.args.signature, test.args.opts); (err != nil) != test.wantErr { +func testStoreAndRetrieveHelper(ctx context.Context, t *testing.T, test testConfig, backend Backend) { + if err := backend.StorePayload(ctx, test.args.runObject, test.args.payload, test.args.signature, test.args.opts); (err != nil) != test.wantErr { t.Fatalf("Backend.StorePayload() failed. error:%v, wantErr:%v", err, test.wantErr) } + // if occurrence is not expected to be created, then stop and no point to retrieve sig & payload + if len(test.wantOccurrences) == 0 { + return + } + // check signature // ---------------- expectSignature := map[string][]string{} @@ -249,13 +398,13 @@ func testStoreAndRetrieve(ctx context.Context, t *testing.T, test testConfig, ba expectSignature[test.args.opts.FullKey] = []string{test.args.signature} } if test.args.opts.PayloadFormat == formats.PayloadTypeInTotoIte6 { - allURIs := backend.retrieveAllArtifactIdentifiers(test.args.tr) + allURIs := extract.RetrieveAllArtifactURIs(test.args.runObject, backend.logger) for _, u := range allURIs { expectSignature[u] = []string{test.args.signature} } } - gotSignature, err := backend.RetrieveSignatures(ctx, trObj, test.args.opts) + gotSignature, err := backend.RetrieveSignatures(ctx, test.args.runObject, test.args.opts) if err != nil { t.Fatal("Backend.RetrieveSignatures() failed: ", err) } @@ -271,13 +420,13 @@ func testStoreAndRetrieve(ctx context.Context, t *testing.T, test testConfig, ba expectPayload[test.args.opts.FullKey] = string(test.args.payload) } if test.args.opts.PayloadFormat == formats.PayloadTypeInTotoIte6 { - allURIs := backend.retrieveAllArtifactIdentifiers(test.args.tr) + allURIs := extract.RetrieveAllArtifactURIs(test.args.runObject, backend.logger) for _, u := range allURIs { expectPayload[u] = string(test.args.payload) } } - gotPayload, err := backend.RetrievePayloads(ctx, trObj, test.args.opts) + gotPayload, err := backend.RetrievePayloads(ctx, test.args.runObject, test.args.opts) if err != nil { t.Fatal("RetrievePayloads.RetrievePayloads() failed: ", err) } @@ -287,87 +436,122 @@ func testStoreAndRetrieve(ctx context.Context, t *testing.T, test testConfig, ba } } -// Two occurrences that will be used for testing ListOccurrences -func getExpectedOccurrences() []*pb.Occurrence { - return []*pb.Occurrence{ - // build occurrence for taskrun that is associated with image artifact - { - // Occurrence ID will be randomly generated by grafeas server. - // In this fake grafeas server, we mock this behaviour by just using resource URI as the auto-generated occurrence name. - Name: "gcr.io/test/kaniko-chains1@sha256:a2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9", - ResourceUri: "gcr.io/test/kaniko-chains1@sha256:a2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9", - NoteName: "projects/test-project/notes/test-note-intoto", - Details: &pb.Occurrence_Build{ - Build: &pb.BuildOccurrence{ - IntotoStatement: &pb.InTotoStatement{ - Predicate: &pb.InTotoStatement_SlsaProvenanceZeroTwo{ - SlsaProvenanceZeroTwo: &pb.SlsaProvenanceZeroTwo{ - Builder: &pb.SlsaProvenanceZeroTwo_SlsaBuilder{}, - Invocation: &pb.SlsaProvenanceZeroTwo_SlsaInvocation{ - ConfigSource: &pb.SlsaProvenanceZeroTwo_SlsaConfigSource{}, - }, +// ------------------ occurrences for taskruns and pipelineruns -------------- +// BUILD Occurrence for the build taskrun that stores the slsa provenance +func getTaskRunBuildOcc(t *testing.T, identifier string) *pb.Occurrence { + return &pb.Occurrence{ + Name: identifier, + ResourceUri: identifier, + NoteName: fmt.Sprintf("projects/%s/notes/%s-taskrun-intoto", ProjectID, NoteID), + Details: &pb.Occurrence_Build{ + Build: &pb.BuildOccurrence{ + IntotoStatement: &pb.InTotoStatement{ + Subject: []*pb.Subject{ + { + Name: artifactURL1, + Digest: map[string]string{"sha256": artifactDigest1}, + }, + { + Name: artifactURL2, + Digest: map[string]string{"sha256": artifactDigest2}, + }, + }, + Predicate: &pb.InTotoStatement_SlsaProvenanceZeroTwo{ + SlsaProvenanceZeroTwo: &pb.SlsaProvenanceZeroTwo{ + Builder: &pb.SlsaProvenanceZeroTwo_SlsaBuilder{}, + Invocation: &pb.SlsaProvenanceZeroTwo_SlsaInvocation{ + ConfigSource: &pb.SlsaProvenanceZeroTwo_SlsaConfigSource{}, }, - }}, - }, + }, + }}, }, - Envelope: &pb.Envelope{ - Payload: []byte("{}"), - PayloadType: "application/vnd.in-toto+json", - Signatures: []*pb.EnvelopeSignature{ - {Sig: []byte("taskrun signature")}, - }, + }, + Envelope: &pb.Envelope{ + Payload: getRawPayload(t, buildTaskRunProvenance), + PayloadType: "application/vnd.in-toto+json", + Signatures: []*pb.EnvelopeSignature{ + {Sig: []byte("build taskrun signature")}, }, }, - // attestation occurrence for OCI image - { - Name: "gcr.io/test/kaniko-chains1@sha256:cfe4f0bf41c80609214f9b8ec0408b1afb28b3ced343b944aaa05d47caba3e00", - ResourceUri: "gcr.io/test/kaniko-chains1@sha256:cfe4f0bf41c80609214f9b8ec0408b1afb28b3ced343b944aaa05d47caba3e00", - NoteName: "projects/test-project/notes/test-note-simplesigning", - Details: &pb.Occurrence_Attestation{ - Attestation: &pb.AttestationOccurrence{ - SerializedPayload: []byte("oci payload"), - Signatures: []*pb.Signature{ - {Signature: []byte("oci signature")}, - }, + } +} + +// ATTESTATION Occurrence for the build taskrun that stores the image attestation +func getTaskRunAttestationOcc(t *testing.T, identifier string) *pb.Occurrence { + return &pb.Occurrence{ + Name: identifier, + ResourceUri: identifier, + NoteName: fmt.Sprintf("projects/%s/notes/%s-simplesigning", ProjectID, NoteID), + Details: &pb.Occurrence_Attestation{ + Attestation: &pb.AttestationOccurrence{ + SerializedPayload: []byte("attestation payload"), + Signatures: []*pb.Signature{ + {Signature: []byte("build taskrun image signature")}, }, }, - Envelope: &pb.Envelope{ - Payload: []byte("oci payload"), - PayloadType: "application/vnd.dev.cosign.simplesigning.v1+json", - Signatures: []*pb.EnvelopeSignature{ - {Sig: []byte("oci signature")}, - }, + }, + Envelope: &pb.Envelope{ + Payload: []byte("attestation payload"), + PayloadType: "application/vnd.dev.cosign.simplesigning.v1+json", + Signatures: []*pb.EnvelopeSignature{ + {Sig: []byte("build taskrun image signature")}, }, }, - // build occurrence for taskrun that is associated with maven artifact - { - Name: "us-central1-maven.pkg.dev/test/java@sha256:b2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9", - ResourceUri: "us-central1-maven.pkg.dev/test/java@sha256:b2e500bebfe16cf12fc56316ba72c645e1d29054541dc1ab6c286197434170a9", - NoteName: "projects/test-project/notes/test-note-intoto", - Details: &pb.Occurrence_Build{ - Build: &pb.BuildOccurrence{ - IntotoStatement: &pb.InTotoStatement{ - Predicate: &pb.InTotoStatement_SlsaProvenanceZeroTwo{ - SlsaProvenanceZeroTwo: &pb.SlsaProvenanceZeroTwo{ - Builder: &pb.SlsaProvenanceZeroTwo_SlsaBuilder{}, - Invocation: &pb.SlsaProvenanceZeroTwo_SlsaInvocation{ - ConfigSource: &pb.SlsaProvenanceZeroTwo_SlsaConfigSource{}, + } +} + +func getPipelineRunBuildOcc(t *testing.T, identifier string) *pb.Occurrence { + return &pb.Occurrence{ + Name: identifier, + ResourceUri: identifier, + NoteName: fmt.Sprintf("projects/%s/notes/%s-pipelinerun-intoto", ProjectID, NoteID), + Details: &pb.Occurrence_Build{ + Build: &pb.BuildOccurrence{ + IntotoStatement: &pb.InTotoStatement{ + Subject: []*pb.Subject{ + { + Name: artifactURL1, + Digest: map[string]string{"sha256": artifactDigest1}, + }, + { + Name: artifactURL2, + Digest: map[string]string{"sha256": artifactDigest2}, + }, + }, + Predicate: &pb.InTotoStatement_SlsaProvenanceZeroTwo{ + SlsaProvenanceZeroTwo: &pb.SlsaProvenanceZeroTwo{ + Builder: &pb.SlsaProvenanceZeroTwo_SlsaBuilder{}, + Invocation: &pb.SlsaProvenanceZeroTwo_SlsaInvocation{ + ConfigSource: &pb.SlsaProvenanceZeroTwo_SlsaConfigSource{}, + }, + Materials: []*pb.SlsaProvenanceZeroTwo_SlsaMaterial{ + { + Uri: repoURL, + Digest: map[string]string{"sha1": commitSHA}, }, }, - }}, - }, + }, + }}, }, - Envelope: &pb.Envelope{ - Payload: []byte("{}"), - PayloadType: "application/vnd.in-toto+json", - Signatures: []*pb.EnvelopeSignature{ - {Sig: []byte("taskrun signature")}, - }, + }, + Envelope: &pb.Envelope{ + Payload: getRawPayload(t, ciPipelineRunProvenance), + PayloadType: "application/vnd.in-toto+json", + Signatures: []*pb.EnvelopeSignature{ + {Sig: []byte("ci pipelinerun signature")}, }, }, } } +func getRawPayload(t *testing.T, in interface{}) []byte { + rawPayload, err := json.Marshal(in) + if err != nil { + t.Errorf("Unable to marshal the provenance: %v", in) + } + return rawPayload +} + // set up the connection between grafeas server and client // and return the client object to the caller func setupConnection() (*grpc.ClientConn, pb.GrafeasClient, error) { @@ -401,71 +585,128 @@ type mockGrafeasServer struct { // Tests will keep working if more methods are added in the future. pb.UnimplementedGrafeasServer - // Assume there is only one project for storing notes and occurences - occurences map[string]*pb.Occurrence - notes map[string]*pb.Note + // entries mocks the storage of notes and occurrences. + // Assume there is only one grafeas project + entries map[string]*noteOccurrences +} + +// noteOccurrences mocks that the behaviour of one note linking multiple occurrences +type noteOccurrences struct { + // mocks a grafeas note instance + note *pb.Note + // mocks occurrences under a note + // - key is artifact uri (ResourceUri) that represents the occurrence. + // - value is the actual occurrence instance + occurrences map[string]*pb.Occurrence } func (s *mockGrafeasServer) CreateOccurrence(ctx context.Context, req *pb.CreateOccurrenceRequest) (*pb.Occurrence, error) { - if s.occurences == nil { - s.occurences = make(map[string]*pb.Occurrence) + if s.entries == nil { + s.entries = make(map[string]*noteOccurrences) } - occID := req.GetOccurrence().GetResourceUri() - expectedResponse := req.GetOccurrence() - expectedResponse.Name = occID // mock auto-generated id + occ := req.GetOccurrence() + noteName := req.GetOccurrence().NoteName + resourceUri := req.GetOccurrence().ResourceUri + occ.Name = resourceUri // mock how the occurrence ID (name) is outputed. + + if note, ok := s.entries[noteName]; ok { + if _, ok := note.occurrences[resourceUri]; ok { + return nil, gstatus.Error(codes.AlreadyExists, "Occurrence ID already exists") + } + if note.occurrences == nil { + note.occurrences = make(map[string]*pb.Occurrence) + } + note.occurrences[resourceUri] = occ + return occ, nil + } - s.occurences[occID] = expectedResponse - return expectedResponse, nil + return nil, gstatus.Error(codes.FailedPrecondition, "The note for that occurrences does not exist.") } func (s *mockGrafeasServer) CreateNote(ctx context.Context, req *pb.CreateNoteRequest) (*pb.Note, error) { - noteID := fmt.Sprintf("%s/notes/%s", req.GetParent(), req.GetNoteId()) - expectedResponse := req.GetNote() - if s.notes == nil { - s.notes = make(map[string]*pb.Note) + notePath := fmt.Sprintf("%s/notes/%s", req.GetParent(), req.GetNoteId()) + noteRequested := req.GetNote() + + if s.entries == nil { + s.entries = make(map[string]*noteOccurrences) } - if _, exists := s.notes[noteID]; exists { + if _, ok := s.entries[notePath]; ok { return nil, gstatus.Error(codes.AlreadyExists, "note ID already exists") } - s.notes[noteID] = expectedResponse - return expectedResponse, nil + + s.entries[notePath] = ¬eOccurrences{ + note: noteRequested, + } + return noteRequested, nil +} + +func (s *mockGrafeasServer) ListNotes(ctx context.Context, req *pb.ListNotesRequest) (*pb.ListNotesResponse, error) { + notes := []*pb.Note{} + for _, n := range s.entries { + notes = append(notes, n.note) + } + return &pb.ListNotesResponse{Notes: notes}, nil } func (s *mockGrafeasServer) ListOccurrences(ctx context.Context, req *pb.ListOccurrencesRequest) (*pb.ListOccurrencesResponse, error) { // to make sure the occurrences we get are in order. - sortedOccurrencesInServer := []*pb.Occurrence{} - keys := []string{} - for k := range s.occurences { - keys = append(keys, k) + allOccurrencesInServer := []*pb.Occurrence{} + for _, note := range s.entries { + for _, occ := range note.occurrences { + allOccurrencesInServer = append(allOccurrencesInServer, occ) + } } - sort.Strings(keys) - for _, k := range keys { - sortedOccurrencesInServer = append(sortedOccurrencesInServer, s.occurences[k]) + + filteredOccs := s.getOccurrencesByFilter(req.GetFilter(), allOccurrencesInServer) + sort.Slice(filteredOccs, func(i, j int) bool { + return filteredOccs[i].ResourceUri+filteredOccs[i].NoteName < filteredOccs[j].ResourceUri+filteredOccs[j].NoteName + }) + + return &pb.ListOccurrencesResponse{Occurrences: filteredOccs}, nil +} + +func (s *mockGrafeasServer) ListNoteOccurrences(ctx context.Context, req *pb.ListNoteOccurrencesRequest) (*pb.ListNoteOccurrencesResponse, error) { + noteName := req.Name + if _, ok := s.entries[noteName]; !ok { + return nil, nil } + allOccurrences := []*pb.Occurrence{} + for _, o := range s.entries[noteName].occurrences { + allOccurrences = append(allOccurrences, o) + } + + filteredOccs := s.getOccurrencesByFilter(req.GetFilter(), allOccurrences) + sort.Slice(filteredOccs, func(i, j int) bool { + return filteredOccs[i].ResourceUri+filteredOccs[i].NoteName < filteredOccs[j].ResourceUri+filteredOccs[j].NoteName + }) + return &pb.ListNoteOccurrencesResponse{Occurrences: filteredOccs}, nil +} + +func (s *mockGrafeasServer) getOccurrencesByFilter(filter string, occurrences []*pb.Occurrence) []*pb.Occurrence { // if filter string is empty, the expected behaviour will be to return all. - if len(req.GetFilter()) == 0 { - return &pb.ListOccurrencesResponse{Occurrences: sortedOccurrencesInServer}, nil + if len(filter) == 0 { + return occurrences } // if the filter string is not empty, do the filtering. // mock how uri filter works - uris := parseURIFilterString(req.GetFilter()) + uris := parseURIFilterString(filter) - // result occurrences - occurrences := []*pb.Occurrence{} + // result result + result := []*pb.Occurrence{} - for _, occ := range sortedOccurrencesInServer { + for _, occ := range occurrences { for _, uri := range uris { if uri == occ.GetResourceUri() { - occurrences = append(occurrences, occ) + result = append(result, occ) } } } - return &pb.ListOccurrencesResponse{Occurrences: occurrences}, nil + return result } // parse a chained uri filter string to a list of uris diff --git a/pkg/chains/storage/oci/oci.go b/pkg/chains/storage/oci/oci.go index 723bde8a3a..e11517f31a 100644 --- a/pkg/chains/storage/oci/oci.go +++ b/pkg/chains/storage/oci/oci.go @@ -80,7 +80,7 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra return err } - b.logger.Infof("Storing payload on %s/%s/%s", obj.GetKind(), obj.GetNamespace(), obj.GetName()) + b.logger.Infof("Storing payload on %s/%s/%s", obj.GetGVK(), obj.GetNamespace(), obj.GetName()) if storageOpts.PayloadFormat == formats.PayloadTypeSimpleSigning { format := simple.SimpleContainerImage{} @@ -101,7 +101,7 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra // that is not intended to produce an image, e.g. git-clone. if len(attestation.Subject) == 0 { b.logger.Infof( - "No image subject to attest for %s/%s/%s. Skipping upload to registry", obj.GetKind(), obj.GetNamespace(), obj.GetName()) + "No image subject to attest for %s/%s/%s. Skipping upload to registry", obj.GetGVK(), obj.GetNamespace(), obj.GetName()) return nil } diff --git a/pkg/chains/storage/tekton/tekton.go b/pkg/chains/storage/tekton/tekton.go index aa1d254572..938ffb195b 100644 --- a/pkg/chains/storage/tekton/tekton.go +++ b/pkg/chains/storage/tekton/tekton.go @@ -51,7 +51,7 @@ func NewStorageBackend(ps versioned.Interface, logger *zap.SugaredLogger) *Backe // StorePayload implements the Payloader interface. func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, rawPayload []byte, signature string, opts config.StorageOpts) error { - b.logger.Infof("Storing payload on %s/%s/%s", obj.GetKind(), obj.GetNamespace(), obj.GetName()) + b.logger.Infof("Storing payload on %s/%s/%s", obj.GetGVK(), obj.GetNamespace(), obj.GetName()) // Use patch instead of update to prevent race conditions. patchBytes, err := patch.GetAnnotationsPatch(map[string]string{ @@ -78,7 +78,7 @@ func (b *Backend) Type() string { // retrieveAnnotationValue retrieve the value of an annotation and base64 decode it if needed. func (b *Backend) retrieveAnnotationValue(ctx context.Context, obj objects.TektonObject, annotationKey string, decode bool) (string, error) { - b.logger.Infof("Retrieving annotation %q on %s/%s/%s", annotationKey, obj.GetKind(), obj.GetNamespace(), obj.GetName()) + b.logger.Infof("Retrieving annotation %q on %s/%s/%s", annotationKey, obj.GetGVK(), obj.GetNamespace(), obj.GetName()) var annotationValue string annotations, err := obj.GetLatestAnnotations(ctx, b.pipelineclientset) @@ -106,7 +106,7 @@ func (b *Backend) retrieveAnnotationValue(ctx context.Context, obj objects.Tekto // RetrieveSignature retrieve the signature stored in the taskrun. func (b *Backend) RetrieveSignatures(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string][]string, error) { - b.logger.Infof("Retrieving signature on %s/%s/%s", obj.GetKind(), obj.GetNamespace(), obj.GetName()) + b.logger.Infof("Retrieving signature on %s/%s/%s", obj.GetGVK(), obj.GetNamespace(), obj.GetName()) signatureAnnotation := sigName(opts) signature, err := b.retrieveAnnotationValue(ctx, obj, signatureAnnotation, true) if err != nil { @@ -119,7 +119,7 @@ func (b *Backend) RetrieveSignatures(ctx context.Context, obj objects.TektonObje // RetrievePayload retrieve the payload stored in the taskrun. func (b *Backend) RetrievePayloads(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string]string, error) { - b.logger.Infof("Retrieving payload on %s/%s/%s", obj.GetKind(), obj.GetNamespace(), obj.GetName()) + b.logger.Infof("Retrieving payload on %s/%s/%s", obj.GetGVK(), obj.GetNamespace(), obj.GetName()) payloadAnnotation := payloadName(opts) payload, err := b.retrieveAnnotationValue(ctx, obj, payloadAnnotation, true) if err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index 5246519ff9..b6351f4ae4 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -256,7 +256,7 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { // PipelineRuns asString(pipelinerunFormatKey, &cfg.Artifacts.PipelineRuns.Format, "tekton", "in-toto"), - asStringSet(pipelinerunStorageKey, &cfg.Artifacts.PipelineRuns.StorageBackend, sets.NewString("tekton", "oci")), + asStringSet(pipelinerunStorageKey, &cfg.Artifacts.PipelineRuns.StorageBackend, sets.NewString("tekton", "oci", "grafeas")), asString(pipelinerunSignerKey, &cfg.Artifacts.PipelineRuns.Signer, "x509", "kms"), // OCI