From f94913d52395fe561e067a3c2ef3587895ca26ed Mon Sep 17 00:00:00 2001 From: Aaron Prindle Date: Thu, 26 Oct 2023 21:23:41 +0000 Subject: [PATCH] feat: add PipelineRun support to storage/gcs Prior to this commit the storage/gcs client didn't support PipelineRuns, only TaskRuns which is not in parity with our other storage interfaces. This PR adds PipelineRuns support to storage/gcs. --- pkg/chains/storage/gcs/gcs.go | 171 ++++++++++++++++++++--------- pkg/chains/storage/gcs/gcs_test.go | 43 +++++++- 2 files changed, 160 insertions(+), 54 deletions(-) diff --git a/pkg/chains/storage/gcs/gcs.go b/pkg/chains/storage/gcs/gcs.go index 3038db68ca..2009cde5e2 100644 --- a/pkg/chains/storage/gcs/gcs.go +++ b/pkg/chains/storage/gcs/gcs.go @@ -32,8 +32,11 @@ import ( const ( StorageBackendGCS = "gcs" // taskrun-$namespace-$name/$key. - SignatureNameFormat = "taskrun-%s-%s/%s.signature" - PayloadNameFormat = "taskrun-%s-%s/%s.payload" + SignatureNameFormatTaskRun = "taskrun-%s-%s/%s.signature" + PayloadNameFormatTaskRun = "taskrun-%s-%s/%s.payload" + // pipelinerun-$namespace-$name/$key. + SignatureNameFormatPipelineRun = "pipelinerun-%s-%s/%s.signature" + PayloadNameFormatPipelineRun = "pipelinerun-%s-%s/%s.payload" ) // Backend is a storage backend that stores signed payloads in the TaskRun metadata as an annotation. @@ -63,30 +66,48 @@ func NewStorageBackend(ctx context.Context, cfg config.Config) (*Backend, error) func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, rawPayload []byte, signature string, opts config.StorageOpts) error { logger := logging.FromContext(ctx) - // TODO(https://github.com/tektoncd/chains/issues/852): Support PipelineRuns - tr, ok := obj.GetObject().(*v1beta1.TaskRun) - if !ok { - return fmt.Errorf("type %T not supported - supported types: [*v1beta1.TaskRun]", obj.GetObject()) - } - - store := &TaskRunStorer{ - writer: b.writer, - key: opts.ShortKey, - } - if _, err := store.Store(ctx, &api.StoreRequest[*v1beta1.TaskRun, *in_toto.Statement]{ - Object: obj, - Artifact: tr, - // We don't actually use payload - we store the raw bundle values directly. - Payload: nil, - Bundle: &signing.Bundle{ - Content: rawPayload, - Signature: []byte(signature), - Cert: []byte(opts.Cert), - Chain: []byte(opts.Chain), - }, - }); err != nil { - logger.Errorf("error writing to GCS: %w", err) - return err + if tr, isTaskRun := obj.GetObject().(*v1beta1.TaskRun); isTaskRun { + store := &TaskRunStorer{ + writer: b.writer, + key: opts.ShortKey, + } + if _, err := store.Store(ctx, &api.StoreRequest[*v1beta1.TaskRun, *in_toto.Statement]{ + Object: obj, + Artifact: tr, + // We don't actually use payload - we store the raw bundle values directly. + Payload: nil, + Bundle: &signing.Bundle{ + Content: rawPayload, + Signature: []byte(signature), + Cert: []byte(opts.Cert), + Chain: []byte(opts.Chain), + }, + }); err != nil { + logger.Errorf("error writing to GCS: %w", err) + return err + } + } else if pr, isPipelineRun := obj.GetObject().(*v1beta1.PipelineRun); isPipelineRun { + store := &PipelineRunStorer{ + writer: b.writer, + key: opts.ShortKey, + } + if _, err := store.Store(ctx, &api.StoreRequest[*v1beta1.PipelineRun, *in_toto.Statement]{ + Object: obj, + Artifact: pr, + // We don't actually use payload - we store the raw bundle values directly. + Payload: nil, + Bundle: &signing.Bundle{ + Content: rawPayload, + Signature: []byte(signature), + Cert: []byte(opts.Cert), + Chain: []byte(opts.Chain), + }, + }); err != nil { + logger.Errorf("error writing to GCS: %w", err) + return err + } + } else { + return fmt.Errorf("type %T not supported - supported types: [*v1beta1.TaskRun, *v1beta1.PipelineRun]", obj.GetObject()) } return nil } @@ -122,9 +143,17 @@ func (r *reader) GetReader(ctx context.Context, object string) (io.ReadCloser, e } func (b *Backend) RetrieveSignatures(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string][]string, error) { - // TODO: Handle unsupported type gracefully - tr := obj.GetObject().(*v1beta1.TaskRun) - object := sigName(tr, opts) + var object string + + switch t := obj.GetObject().(type) { + case *v1beta1.TaskRun: + object = taskRunSigName(t, opts) + case *v1beta1.PipelineRun: + object = pipelineRunSigname(t, opts) + default: + return nil, fmt.Errorf("unsupported TektonObject type: %T", t) + } + signature, err := b.retrieveObject(ctx, object) if err != nil { return nil, err @@ -136,15 +165,23 @@ func (b *Backend) RetrieveSignatures(ctx context.Context, obj objects.TektonObje } func (b *Backend) RetrievePayloads(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) (map[string]string, error) { - // TODO: Handle unsupported type gracefully - tr := obj.GetObject().(*v1beta1.TaskRun) - object := payloadName(tr, opts) - m := make(map[string]string) + var object string + + switch t := obj.GetObject().(type) { + case *v1beta1.TaskRun: + object = taskRunPayloadName(t, opts) + case *v1beta1.PipelineRun: + object = pipelineRunPayloadName(t, opts) + default: + return nil, fmt.Errorf("unsupported TektonObject type: %T", t) + } + payload, err := b.retrieveObject(ctx, object) if err != nil { return nil, err } + m := make(map[string]string) m[object] = payload return m, nil } @@ -163,12 +200,20 @@ func (b *Backend) retrieveObject(ctx context.Context, object string) (string, er return string(payload), nil } -func sigName(tr *v1beta1.TaskRun, opts config.StorageOpts) string { - return fmt.Sprintf(SignatureNameFormat, tr.Namespace, tr.Name, opts.ShortKey) +func taskRunSigName(tr *v1beta1.TaskRun, opts config.StorageOpts) string { + return fmt.Sprintf(SignatureNameFormatTaskRun, tr.Namespace, tr.Name, opts.ShortKey) +} + +func taskRunPayloadName(tr *v1beta1.TaskRun, opts config.StorageOpts) string { + return fmt.Sprintf(PayloadNameFormatTaskRun, tr.Namespace, tr.Name, opts.ShortKey) +} + +func pipelineRunSigname(pr *v1beta1.PipelineRun, opts config.StorageOpts) string { + return fmt.Sprintf(SignatureNameFormatPipelineRun, pr.Namespace, pr.Name, opts.ShortKey) } -func payloadName(tr *v1beta1.TaskRun, opts config.StorageOpts) string { - return fmt.Sprintf(PayloadNameFormat, tr.Namespace, tr.Name, opts.ShortKey) +func pipelineRunPayloadName(pr *v1beta1.PipelineRun, opts config.StorageOpts) string { + return fmt.Sprintf(PayloadNameFormatPipelineRun, pr.Namespace, pr.Name, opts.ShortKey) } var ( @@ -176,7 +221,6 @@ var ( ) // TaskRunStorer stores TaskRuns in GCS. -// TODO(https://github.com/tektoncd/chains/issues/852): implement PipelineRun support (nothing in here is particularly TaskRun specific, but needs tests). type TaskRunStorer struct { writer gcsWriter @@ -185,41 +229,64 @@ type TaskRunStorer struct { key string } -// Store stores the +// Store stores the TaskRun chains information in GCS func (s *TaskRunStorer) Store(ctx context.Context, req *api.StoreRequest[*v1beta1.TaskRun, *in_toto.Statement]) (*api.StoreResponse, error) { + pr := req.Artifact + return store(ctx, s.writer, "taskrun", s.key, + pr.GetNamespace(), pr.GetName(), string(pr.GetUID()), + req.Bundle.Signature, req.Bundle.Content, req.Bundle.Cert, req.Bundle.Chain) +} + +// PipelineRunStorer stores PipelineRuns in GCS. +type PipelineRunStorer struct { + writer gcsWriter + + // Optional key to store objects as. If not set, the object UID will be used. + // The resulting name will look like: $bucket/pipelinerun-$namespace-$name/$key.signature + key string +} + +// Store stores the PipelineRun chains information in GCS +func (s *PipelineRunStorer) Store(ctx context.Context, req *api.StoreRequest[*v1beta1.PipelineRun, *in_toto.Statement]) (*api.StoreResponse, error) { + pr := req.Artifact + return store(ctx, s.writer, "pipelinerun", s.key, + pr.GetNamespace(), pr.GetName(), string(pr.GetUID()), + req.Bundle.Signature, req.Bundle.Content, req.Bundle.Cert, req.Bundle.Chain) +} + +func store(ctx context.Context, writer gcsWriter, kind, key, namespace, name, uid string, + signature, content, cert, chain []byte) (*api.StoreResponse, error) { logger := logging.FromContext(ctx) - tr := req.Artifact - // We need multiple objects: the signature and the payload. We want to make these unique to the UID, but easy to find based on the - // name/namespace as well. - // $bucket/taskrun-$namespace-$name/$key.signature - // $bucket/taskrun-$namespace-$name/$key.payload - key := s.key if key == "" { - key = string(tr.GetUID()) + key = uid } - prefix := fmt.Sprintf("taskrun-%s-%s/%s", tr.GetNamespace(), tr.GetName(), key) + prefix := fmt.Sprintf("%s-%s-%s/%s", kind, namespace, name, key) // Write signature sigName := prefix + ".signature" logger.Infof("Storing signature at %s", sigName) - if _, err := write(ctx, s.writer, sigName, req.Bundle.Signature); err != nil { + if _, err := write(ctx, writer, sigName, signature); err != nil { return nil, err } // Write payload - if _, err := write(ctx, s.writer, prefix+".payload", req.Bundle.Content); err != nil { + payloadName := prefix + ".payload" + if _, err := write(ctx, writer, payloadName, content); err != nil { return nil, err } // Only write cert+chain if it is present. - if req.Bundle.Cert == nil { + if cert == nil { return nil, nil } - if _, err := write(ctx, s.writer, prefix+".cert", req.Bundle.Cert); err != nil { + certName := prefix + ".cert" + if _, err := write(ctx, writer, certName, cert); err != nil { return nil, err } - if _, err := write(ctx, s.writer, prefix+".chain", req.Bundle.Chain); err != nil { + + chainName := prefix + ".chain" + if _, err := write(ctx, writer, chainName, chain); err != nil { return nil, err } diff --git a/pkg/chains/storage/gcs/gcs_test.go b/pkg/chains/storage/gcs/gcs_test.go index 3bf08eb470..7b024db403 100644 --- a/pkg/chains/storage/gcs/gcs_test.go +++ b/pkg/chains/storage/gcs/gcs_test.go @@ -33,6 +33,7 @@ func TestBackend_StorePayload(t *testing.T) { type args struct { tr *v1beta1.TaskRun + pr *v1beta1.PipelineRun signed []byte signature string opts config.StorageOpts @@ -52,6 +53,13 @@ func TestBackend_StorePayload(t *testing.T) { UID: types.UID("uid"), }, }, + pr: &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "bar", + UID: types.UID("uid"), + }, + }, signed: []byte("signed"), signature: "signature", opts: config.StorageOpts{ShortKey: "foo.uuid", PayloadFormat: formats.PayloadTypeSlsav1}, @@ -67,6 +75,13 @@ func TestBackend_StorePayload(t *testing.T) { UID: types.UID("uid"), }, }, + pr: &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "bar", + UID: types.UID("uid"), + }, + }, signed: []byte("signed"), signature: "signature", opts: config.StorageOpts{ShortKey: "foo.uuid", PayloadFormat: formats.PayloadTypeTekton}, @@ -88,8 +103,8 @@ func TestBackend_StorePayload(t *testing.T) { t.Errorf("Backend.StorePayload() error = %v, wantErr %v", err, tt.wantErr) } - objectSig := sigName(tt.args.tr, tt.args.opts) - objectPayload := payloadName(tt.args.tr, tt.args.opts) + objectSig := taskRunSigName(tt.args.tr, tt.args.opts) + objectPayload := taskRunPayloadName(tt.args.tr, tt.args.opts) got, err := b.RetrieveSignatures(ctx, trObj, tt.args.opts) if err != nil { t.Fatal(err) @@ -105,6 +120,30 @@ func TestBackend_StorePayload(t *testing.T) { if gotPayload[objectPayload] != string(tt.args.signed) { t.Errorf("wrong signature, expected %s, got %s", tt.args.signed, gotPayload[objectPayload]) } + + prObj := objects.NewPipelineRunObject(tt.args.pr) + if err := b.StorePayload(ctx, prObj, tt.args.signed, tt.args.signature, tt.args.opts); (err != nil) != tt.wantErr { + t.Errorf("Backend.StorePayload() error = %v, wantErr %v", err, tt.wantErr) + } + + objectSig = pipelineRunSigname(tt.args.pr, tt.args.opts) + objectPayload = pipelineRunPayloadName(tt.args.pr, tt.args.opts) + got, err = b.RetrieveSignatures(ctx, prObj, tt.args.opts) + if err != nil { + t.Fatal(err) + } + + if got[objectSig][0] != tt.args.signature { + t.Errorf("wrong signature, expected %q, got %q", tt.args.signature, got[objectSig][0]) + } + + gotPayload, err = b.RetrievePayloads(ctx, prObj, tt.args.opts) + if err != nil { + t.Fatal(err) + } + if gotPayload[objectPayload] != string(tt.args.signed) { + t.Errorf("wrong signature, expected %s, got %s", tt.args.signed, gotPayload[objectPayload]) + } }) } }