diff --git a/examples/pipelineruns/pipeline-output-image.yaml b/examples/pipelineruns/pipeline-output-image.yaml new file mode 100644 index 0000000000..74216a43a5 --- /dev/null +++ b/examples/pipelineruns/pipeline-output-image.yaml @@ -0,0 +1,40 @@ +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + generateName: image-pipelinerun +spec: + params: + - name: CHAINS-GIT_COMMIT + value: my-git-commit + - name: CHAINS-GIT_URL + value: https://my-git-url + pipelineSpec: + results: + - description: "" + name: IMAGE_URL + value: $(tasks.buildimage.results.IMAGE_URL) + - description: "" + name: IMAGE_DIGEST + value: $(tasks.buildimage.results.IMAGE_DIGEST) + tasks: + - name: buildimage + taskSpec: + results: + - name: IMAGE_URL + type: string + - name: IMAGE_DIGEST + type: string + steps: + - image: bash:latest + name: create-dockerfile + resources: {} + script: |- + #!/usr/bin/env bash + echo 'gcr.io/foo/bar' | tee $(results.IMAGE_URL.path) + echo 'sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5' | tee $(results.IMAGE_DIGEST.path) + volumeMounts: + - mountPath: /dockerfile + name: dockerfile + volumes: + - emptyDir: {} + name: dockerfile diff --git a/pkg/chains/annotations_test.go b/pkg/chains/annotations_test.go index 4bff628335..ccdc8f5668 100644 --- a/pkg/chains/annotations_test.go +++ b/pkg/chains/annotations_test.go @@ -17,7 +17,7 @@ import ( "testing" "github.com/tektoncd/chains/pkg/chains/objects" - "github.com/tektoncd/chains/pkg/internal/tekton" + "github.com/tektoncd/chains/pkg/test/tekton" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" fakepipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -119,9 +119,7 @@ func TestMarkSigned(t *testing.T) { ctx, _ := rtesting.SetupFakeContext(t) c := fakepipelineclient.Get(ctx) - if err := tekton.CreateObject(t, ctx, c, tt.object); err != nil { - t.Fatal(err) - } + tekton.CreateObject(t, ctx, c, tt.object) // Now mark it as signed. if err := MarkSigned(ctx, tt.object, c, nil); err != nil { @@ -202,9 +200,7 @@ func TestMarkFailed(t *testing.T) { ctx, _ := rtesting.SetupFakeContext(t) // Create a TR for testing c := fakepipelineclient.Get(ctx) - if err := tekton.CreateObject(t, ctx, c, tt.object); err != nil { - t.Fatal(err) - } + tekton.CreateObject(t, ctx, c, tt.object) // Test HandleRetry, should mark it as failed if err := HandleRetry(ctx, tt.object, c, nil); err != nil { @@ -302,9 +298,7 @@ func TestAddRetry(t *testing.T) { ctx, _ := rtesting.SetupFakeContext(t) c := fakepipelineclient.Get(ctx) - if err := tekton.CreateObject(t, ctx, c, tt.object); err != nil { - t.Fatal(err) - } + tekton.CreateObject(t, ctx, c, tt.object) // run it through AddRetry, make sure annotation is added if err := AddRetry(ctx, tt.object, c, nil); err != nil { diff --git a/pkg/chains/objects/objects.go b/pkg/chains/objects/objects.go index 0daee20d0d..0217fce0ff 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -15,6 +15,7 @@ package objects import ( "context" + "errors" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -23,6 +24,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "knative.dev/pkg/apis" ) // Label added to TaskRuns identifying the associated pipeline Task @@ -56,6 +58,19 @@ type TektonObject interface { GetResults() []Result GetServiceAccountName() string GetPullSecrets() []string + IsDone() bool + IsSuccessful() bool +} + +func NewTektonObject(i interface{}) (TektonObject, error) { + switch o := i.(type) { + case *v1beta1.PipelineRun: + return NewPipelineRunObject(o), nil + case *v1beta1.TaskRun: + return NewTaskRunObject(o), nil + default: + return nil, errors.New("unrecognized type when attempting to create tekton object") + } } // TaskRunObject extends v1beta1.TaskRun with additional functions. @@ -71,7 +86,9 @@ func NewTaskRunObject(tr *v1beta1.TaskRun) *TaskRunObject { // Get the TaskRun kind func (tro *TaskRunObject) GetKind() string { - return tro.GetObjectKind().GroupVersionKind().Kind + // TODO: Want to use tro.GetObjectKind().GroupVersionKind().Kind but + // never seems to be populated + return "taskrun" } // Get the latest annotations on the TaskRun @@ -130,7 +147,9 @@ func NewPipelineRunObject(pr *v1beta1.PipelineRun) *PipelineRunObject { // Get the PipelineRun kind func (pro *PipelineRunObject) GetKind() string { - return pro.GetObjectKind().GroupVersionKind().Kind + // TODO: Want to use tro.GetObjectKind().GroupVersionKind().Kind but + // never seems to be populated + return "pipelinerun" } // Request the current annotations on the PipelineRun object @@ -171,6 +190,11 @@ func (pro *PipelineRunObject) GetServiceAccountName() string { return pro.Spec.ServiceAccountName } +// Get the ServiceAccount declared in the PipelineRun +func (pro *PipelineRunObject) IsSuccessful() bool { + return pro.Status.GetCondition(apis.ConditionSucceeded).IsTrue() +} + // Append TaskRuns to this PipelineRun func (pro *PipelineRunObject) AppendTaskRun(tr *v1beta1.TaskRun) { pro.taskRuns = append(pro.taskRuns, tr) diff --git a/pkg/chains/signing_test.go b/pkg/chains/signing_test.go index a549ffe4b3..50924f6838 100644 --- a/pkg/chains/signing_test.go +++ b/pkg/chains/signing_test.go @@ -25,7 +25,7 @@ import ( "github.com/tektoncd/chains/pkg/chains/signing" "github.com/tektoncd/chains/pkg/chains/storage" "github.com/tektoncd/chains/pkg/config" - "github.com/tektoncd/chains/pkg/internal/tekton" + "github.com/tektoncd/chains/pkg/test/tekton" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" fakepipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client/fake" "go.uber.org/zap" @@ -152,9 +152,8 @@ func TestSigner_Sign(t *testing.T) { Pipelineclientset: ps, } - if err := tekton.CreateObject(t, ctx, ps, tt.object); err != nil { - t.Errorf("error creating fake object: %v", err) - } + tekton.CreateObject(t, ctx, ps, tt.object) + if err := ts.Sign(ctx, tt.object); (err != nil) != tt.wantErr { t.Errorf("Signer.Sign() error = %v", err) } @@ -312,9 +311,9 @@ func TestSigner_Transparency(t *testing.T) { } obj := tt.getNewObject("foo") - if err := tekton.CreateObject(t, ctx, ps, obj); err != nil { - t.Errorf("error creating fake object: %v", err) - } + + tekton.CreateObject(t, ctx, ps, obj) + if err := os.Sign(ctx, obj); err != nil { t.Errorf("Signer.Sign() error = %v", err) } @@ -328,9 +327,9 @@ func TestSigner_Transparency(t *testing.T) { ctx = config.ToContext(ctx, tt.cfg.DeepCopy()) obj = tt.getNewObject("foobar") - if err := tekton.CreateObject(t, ctx, ps, obj); err != nil { - t.Errorf("error creating fake object: %v", err) - } + + tekton.CreateObject(t, ctx, ps, obj) + if err := os.Sign(ctx, obj); err != nil { t.Errorf("Signer.Sign() error = %v", err) } @@ -344,9 +343,9 @@ func TestSigner_Transparency(t *testing.T) { ctx = config.ToContext(ctx, tt.cfg.DeepCopy()) obj = tt.getNewObject("mytektonobject") - if err := tekton.CreateObject(t, ctx, ps, obj); err != nil { - t.Errorf("error creating fake object: %v", err) - } + + tekton.CreateObject(t, ctx, ps, obj) + if err := os.Sign(ctx, obj); err != nil { t.Errorf("Signer.Sign() error = %v", err) } diff --git a/pkg/chains/storage/tekton/tekton_test.go b/pkg/chains/storage/tekton/tekton_test.go index 1574aea76b..42f46746ea 100644 --- a/pkg/chains/storage/tekton/tekton_test.go +++ b/pkg/chains/storage/tekton/tekton_test.go @@ -20,7 +20,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" - "github.com/tektoncd/chains/pkg/internal/tekton" + "github.com/tektoncd/chains/pkg/test/tekton" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" fakepipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -83,9 +83,7 @@ func TestBackend_StorePayload(t *testing.T) { ctx, _ := rtesting.SetupFakeContext(t) c := fakepipelineclient.Get(ctx) - if err := tekton.CreateObject(t, ctx, c, tt.object); err != nil { - t.Errorf("error setting up fake taskrun: %v", err) - } + tekton.CreateObject(t, ctx, c, tt.object) b := &Backend{ pipelineclientset: c, diff --git a/pkg/internal/tekton/tekton.go b/pkg/test/tekton/tekton.go similarity index 61% rename from pkg/internal/tekton/tekton.go rename to pkg/test/tekton/tekton.go index 6fe04df690..d74bf1461c 100644 --- a/pkg/internal/tekton/tekton.go +++ b/pkg/test/tekton/tekton.go @@ -22,18 +22,23 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" pipelineclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" ) -func CreateObject(t *testing.T, ctx context.Context, ps pipelineclientset.Interface, obj objects.TektonObject) error { +func CreateObject(t *testing.T, ctx context.Context, ps pipelineclientset.Interface, obj objects.TektonObject) objects.TektonObject { switch o := obj.GetObject().(type) { case *v1beta1.PipelineRun: - if _, err := ps.TektonV1beta1().PipelineRuns(obj.GetNamespace()).Create(ctx, o, metav1.CreateOptions{}); err != nil { - t.Errorf("error creating pipelinerun: %v", err) + pr, err := ps.TektonV1beta1().PipelineRuns(obj.GetNamespace()).Create(ctx, o, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("error creating pipelinerun: %v", err) } + return objects.NewPipelineRunObject(pr) case *v1beta1.TaskRun: - if _, err := ps.TektonV1beta1().TaskRuns(obj.GetNamespace()).Create(ctx, o, metav1.CreateOptions{}); err != nil { - t.Errorf("error creating taskrun: %v", err) + tr, err := ps.TektonV1beta1().TaskRuns(obj.GetNamespace()).Create(ctx, o, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("error creating taskrun: %v", err) } + return objects.NewTaskRunObject(tr) } return nil } @@ -46,14 +51,14 @@ func GetObject(t *testing.T, ctx context.Context, ps pipelineclientset.Interface case *v1beta1.TaskRun: return GetTaskRun(t, ctx, ps, obj.GetNamespace(), obj.GetName()) } - t.Errorf("unknown object type %T", obj.GetObject()) + t.Fatalf("unknown object type %T", obj.GetObject()) return nil, fmt.Errorf("unknown object type %T", obj.GetObject()) } func GetPipelineRun(t *testing.T, ctx context.Context, ps pipelineclientset.Interface, namespace, name string) (objects.TektonObject, error) { pr, err := ps.TektonV1beta1().PipelineRuns(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - t.Errorf("error getting pipelinerun: %v", err) + t.Fatalf("error getting pipelinerun: %v", err) } return objects.NewPipelineRunObject(pr), nil } @@ -61,7 +66,23 @@ func GetPipelineRun(t *testing.T, ctx context.Context, ps pipelineclientset.Inte func GetTaskRun(t *testing.T, ctx context.Context, ps pipelineclientset.Interface, namespace, name string) (objects.TektonObject, error) { tr, err := ps.TektonV1beta1().TaskRuns(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - t.Errorf("error getting taskrun: %v", err) + t.Fatalf("error getting taskrun: %v", err) } return objects.NewTaskRunObject(tr), nil } + +func WatchObject(t *testing.T, ctx context.Context, ps pipelineclientset.Interface, obj objects.TektonObject) (watch.Interface, error) { + switch o := obj.GetObject().(type) { + case *v1beta1.PipelineRun: + return ps.TektonV1beta1().PipelineRuns(obj.GetNamespace()).Watch(ctx, metav1.SingleObject(metav1.ObjectMeta{ + Name: o.GetName(), + Namespace: o.GetNamespace(), + })) + case *v1beta1.TaskRun: + return ps.TektonV1beta1().TaskRuns(obj.GetNamespace()).Watch(ctx, metav1.SingleObject(metav1.ObjectMeta{ + Name: o.GetName(), + Namespace: o.GetNamespace(), + })) + } + return nil, fmt.Errorf("unknown object type %T", obj.GetObject()) +} diff --git a/test/clients.go b/test/clients.go index 52d5e6873d..8a823de494 100644 --- a/test/clients.go +++ b/test/clients.go @@ -89,6 +89,7 @@ func newClients(t *testing.T, configPath, clusterName string) *clients { type setupOpts struct { useCosignSigner bool registry bool + kanikoTaskImage string ns string } @@ -107,6 +108,15 @@ func setup(ctx context.Context, t *testing.T, opts setupOpts) (*clients, string, c.internalRegistry = createRegistry(ctx, t, namespace, c.KubeClient) } + if opts.kanikoTaskImage != "" { + imageDest := fmt.Sprintf("%s/%s", c.internalRegistry, opts.kanikoTaskImage) + t.Logf("Creating Kaniko task referencing image %s", imageDest) + task := kanikoTask(t, namespace, imageDest) + if _, err := c.PipelineClient.TektonV1beta1().Tasks(namespace).Create(ctx, task, metav1.CreateOptions{}); err != nil { + t.Fatalf("error creating task: %s", err) + } + } + var cleanup = func() { if namespace == "default" { return diff --git a/test/e2e_test.go b/test/e2e_test.go index 3b7e9fe476..c396cc9eab 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -39,7 +39,9 @@ import ( "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/tektoncd/chains/pkg/chains" + "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/chains/provenance" + "github.com/tektoncd/chains/pkg/test/tekton" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -60,70 +62,121 @@ func TestInstall(t *testing.T) { } func TestTektonStorage(t *testing.T) { - ctx := logtesting.TestContextWithLogger(t) - c, ns, cleanup := setup(ctx, t, setupOpts{}) - defer cleanup() + tests := []struct { + name string + cm map[string]string + getObject func(ns string) objects.TektonObject + }{ + { + name: "taskrun", + cm: map[string]string{ + "artifacts.taskrun.format": "tekton", + "artifacts.taskrun.signer": "x509", + "artifacts.taskrun.storage": "tekton", + "artifacts.oci.format": "simplesigning", + "artifacts.oci.signer": "x509", + "artifacts.oci.storage": "tekton", + }, + getObject: getTaskRunObject, + }, + { + name: "pipelinerun", + cm: map[string]string{ + "artifacts.pipelinerun.format": "tekton", + "artifacts.pipelinerun.signer": "x509", + "artifacts.pipelinerun.storage": "tekton", + "artifacts.oci.format": "simplesigning", + "artifacts.oci.signer": "x509", + "artifacts.oci.storage": "tekton", + }, + getObject: getPipelineRunObject, + }, + } - // Setup the right config. - resetConfig := setConfigMap(ctx, t, c, map[string]string{ - "artifacts.taskrun.format": "tekton", - "artifacts.taskrun.signer": "x509", - "artifacts.taskrun.storage": "tekton", - "artifacts.oci.format": "simplesigning", - "artifacts.oci.signer": "x509", - "artifacts.oci.storage": "tekton", - }) - defer resetConfig() + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + c, ns, cleanup := setup(ctx, t, setupOpts{}) + defer cleanup() - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, &imageTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + // Setup the right config. + resetConfig := setConfigMap(ctx, t, c, test.cm) + defer resetConfig() - // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, done, 60*time.Second) + createdObj := tekton.CreateObject(t, ctx, c.PipelineClient, test.getObject(ns)) - // It can take up to a minute for the secret data to be updated! - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) + // Give it a minute to complete. + waitForCondition(ctx, t, c.PipelineClient, createdObj, done, 60*time.Second) - // Verify the payload signature. - verifySignature(ctx, t, c, tr) + // It can take up to a minute for the secret data to be updated! + updatedObj := waitForCondition(ctx, t, c.PipelineClient, createdObj, signed, 120*time.Second) + + // Verify the payload signature. + verifySignature(ctx, t, c, updatedObj) + }) + } } func TestRekor(t *testing.T) { - ctx := logtesting.TestContextWithLogger(t) - c, ns, cleanup := setup(ctx, t, setupOpts{}) - defer cleanup() + tests := []struct { + name string + cm map[string]string + getObject func(ns string) objects.TektonObject + }{ + { + name: "taskrun", + cm: map[string]string{ + "artifacts.taskrun.format": "tekton", + "artifacts.taskrun.signer": "x509", + "artifacts.taskrun.storage": "tekton", + "artifacts.oci.format": "simplesigning", + "artifacts.oci.signer": "x509", + "artifacts.oci.storage": "tekton", + "transparency.enabled": "manual", + }, + getObject: getTaskRunObject, + }, + { + name: "pipelinerun", + cm: map[string]string{ + "artifacts.pipelinerun.format": "tekton", + "artifacts.pipelinerun.signer": "x509", + "artifacts.pipelinerun.storage": "tekton", + "artifacts.oci.format": "simplesigning", + "artifacts.oci.signer": "x509", + "artifacts.oci.storage": "tekton", + "transparency.enabled": "manual", + }, + getObject: getPipelineRunObject, + }, + } - // Setup the right config. - resetConfig := setConfigMap(ctx, t, c, map[string]string{ - "artifacts.taskrun.format": "tekton", - "artifacts.taskrun.signer": "x509", - "artifacts.taskrun.storage": "tekton", - "artifacts.oci.format": "simplesigning", - "artifacts.oci.signer": "x509", - "artifacts.oci.storage": "tekton", - "transparency.enabled": "manual", - }) - defer resetConfig() + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + c, ns, cleanup := setup(ctx, t, setupOpts{}) + defer cleanup() - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, &imageTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + // Setup the right config. + resetConfig := setConfigMap(ctx, t, c, test.cm) + defer resetConfig() - // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, done, 60*time.Second) + createdObj := tekton.CreateObject(t, ctx, c.PipelineClient, test.getObject(ns)) - // It can take up to a minute for the secret data to be updated! - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) + // Give it a minute to complete. + waitForCondition(ctx, t, c.PipelineClient, createdObj, done, 60*time.Second) - if v, ok := tr.Annotations[chains.ChainsTransparencyAnnotation]; !ok || v == "" { - t.Fatal("failed to upload to tlog") - } + // It can take up to a minute for the secret data to be updated! + obj := waitForCondition(ctx, t, c.PipelineClient, createdObj, signed, 120*time.Second) - // Verify the payload signature. - verifySignature(ctx, t, c, tr) + if v, ok := obj.GetAnnotations()[chains.ChainsTransparencyAnnotation]; !ok || v == "" { + t.Fatal("failed to upload to tlog") + } + + // Verify the payload signature. + verifySignature(ctx, t, c, obj) + }) + } } func TestOCISigning(t *testing.T) { @@ -134,7 +187,8 @@ func TestOCISigning(t *testing.T) { { name: "x509", opts: setupOpts{}, - }, { + }, + { name: "cosign", opts: setupOpts{useCosignSigner: true}, }, @@ -151,22 +205,20 @@ func TestOCISigning(t *testing.T) { defer resetConfig() - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, &imageTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } - t.Logf("Created TaskRun: %s", tr.Name) + tro := getTaskRunObject(ns) + + createdTro := tekton.CreateObject(t, ctx, c.PipelineClient, tro) // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, done, 60*time.Second) + waitForCondition(ctx, t, c.PipelineClient, createdTro, done, 60*time.Second) // It can take up to a minute for the secret data to be updated! - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) + obj := waitForCondition(ctx, t, c.PipelineClient, createdTro, signed, 120*time.Second) // Let's fetch the signature and body: - t.Log(tr.Annotations) + t.Log(obj.GetAnnotations()) - sig, body := tr.Annotations["chains.tekton.dev/signature-05f95b26ed10"], tr.Annotations["chains.tekton.dev/payload-05f95b26ed10"] + sig, body := obj.GetAnnotations()["chains.tekton.dev/signature-05f95b26ed10"], obj.GetAnnotations()["chains.tekton.dev/payload-05f95b26ed10"] // base64 decode them sigBytes, err := base64.StdEncoding.DecodeString(sig) if err != nil { @@ -215,19 +267,18 @@ func TestGCSStorage(t *testing.T) { defer resetConfig() time.Sleep(3 * time.Second) - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, &simpleTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + tro := getTaskRunObject(ns) + + createdTro := tekton.CreateObject(t, ctx, c.PipelineClient, tro) // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, done, 60*time.Second) + waitForCondition(ctx, t, c.PipelineClient, createdTro, done, 60*time.Second) // It can take up to a minute for the secret data to be updated! - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) + obj := waitForCondition(ctx, t, c.PipelineClient, createdTro, signed, 120*time.Second) // Verify the payload signature. - verifySignature(ctx, t, c, tr) + verifySignature(ctx, t, c, obj) } func TestFulcio(t *testing.T) { @@ -249,24 +300,25 @@ func TestFulcio(t *testing.T) { defer resetConfig() time.Sleep(3 * time.Second) - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, &simpleTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + tro := getTaskRunObject(ns) + + createdTro := tekton.CreateObject(t, ctx, c.PipelineClient, tro) // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, done, 60*time.Second) - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) + waitForCondition(ctx, t, c.PipelineClient, createdTro, done, 60*time.Second) + + // It can take up to a minute for the secret data to be updated! + obj := waitForCondition(ctx, t, c.PipelineClient, createdTro, signed, 120*time.Second) // verify the cert against the signature and payload - sigKey := fmt.Sprintf("chains.tekton.dev/signature-taskrun-%s", tr.UID) - payloadKey := fmt.Sprintf("chains.tekton.dev/payload-taskrun-%s", tr.UID) - certKey := fmt.Sprintf("chains.tekton.dev/cert-taskrun-%s", tr.UID) + sigKey := fmt.Sprintf("chains.tekton.dev/signature-taskrun-%s", obj.GetUID()) + payloadKey := fmt.Sprintf("chains.tekton.dev/payload-taskrun-%s", obj.GetUID()) + certKey := fmt.Sprintf("chains.tekton.dev/cert-taskrun-%s", obj.GetUID()) - envelopeBytes := base64Decode(t, tr.Annotations[sigKey]) - payload := base64Decode(t, tr.Annotations[payloadKey]) - certPEM := base64Decode(t, tr.Annotations[certKey]) + envelopeBytes := base64Decode(t, obj.GetAnnotations()[sigKey]) + payload := base64Decode(t, obj.GetAnnotations()[payloadKey]) + certPEM := base64Decode(t, obj.GetAnnotations()[certKey]) certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(certPEM)) if err != nil { @@ -339,117 +391,134 @@ func TestOCIStorage(t *testing.T) { imageName := "chains-test-oci-storage" image := fmt.Sprintf("%s/%s", c.internalRegistry, imageName) task := kanikoTask(t, ns, image) - if _, err := c.PipelineClient.TektonV1beta1().Tasks(ns).Create(ctx, task, metav1.CreateOptions{}); err != nil { t.Fatalf("error creating task: %s", err) } - taskRun := kanikoTaskRun(ns) - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, taskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + tro := kanikoTaskRun(ns) + + createdTro := tekton.CreateObject(t, ctx, c.PipelineClient, tro) // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 60*time.Second) + waitForCondition(ctx, t, c.PipelineClient, createdTro, done, 60*time.Second) + + // It can take up to a minute for the secret data to be updated! + waitForCondition(ctx, t, c.PipelineClient, createdTro, signed, 120*time.Second) publicKey, err := cryptoutils.MarshalPublicKeyToPEM(c.secret.x509priv.Public()) if err != nil { t.Error(err) } - verifyTaskRun := verifyKanikoTaskRun(ns, image, string(publicKey)) - tr, err = c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, verifyTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("error creating task: %s", err) - } - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, successful, 120*time.Second) - verifySignature(ctx, t, c, tr) + verifyTrObj := verifyKanikoTaskRun(ns, image, string(publicKey)) + createdObj := tekton.CreateObject(t, ctx, c.PipelineClient, verifyTrObj) + obj := waitForCondition(ctx, t, c.PipelineClient, createdObj, successful, 120*time.Second) + verifySignature(ctx, t, c, obj) } func TestMultiBackendStorage(t *testing.T) { - ctx := logtesting.TestContextWithLogger(t) - c, ns, cleanup := setup(ctx, t, setupOpts{registry: true}) - defer cleanup() - - resetConfig := setConfigMap(ctx, t, c, map[string]string{ - "artifacts.oci.format": "simplesigning", - "artifacts.oci.storage": "tekton,oci", - "artifacts.oci.signer": "x509", - "artifacts.taskrun.format": "tekton", - "artifacts.taskrun.signer": "x509", - "artifacts.taskrun.storage": "tekton,oci", - "storage.oci.repository.insecure": "true", - }) - defer resetConfig() - time.Sleep(3 * time.Second) - - // create necessary resources - imageName := "chains-test-oci-storage" - image := fmt.Sprintf("%s/%s", c.internalRegistry, imageName) - task := kanikoTask(t, ns, image) - - if _, err := c.PipelineClient.TektonV1beta1().Tasks(ns).Create(ctx, task, metav1.CreateOptions{}); err != nil { - t.Fatalf("error creating task: %s", err) + tests := []struct { + name string + cm map[string]string + getObject func(ns string) objects.TektonObject + }{ + { + name: "taskrun", + cm: map[string]string{ + "artifacts.oci.format": "simplesigning", + "artifacts.oci.storage": "tekton,oci", + "artifacts.oci.signer": "x509", + "artifacts.taskrun.format": "in-toto", + "artifacts.taskrun.signer": "x509", + "artifacts.taskrun.storage": "tekton,oci", + "storage.oci.repository.insecure": "true", + }, + getObject: kanikoTaskRun, + }, + { + name: "pipelinerun", + cm: map[string]string{ + "artifacts.pipelinerun.format": "in-toto", + "artifacts.pipelinerun.signer": "x509", + "artifacts.pipelinerun.storage": "tekton,oci", + "storage.oci.repository.insecure": "true", + }, + getObject: kanikoPipelineRun, + }, } - taskRun := kanikoTaskRun(ns) - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, taskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + image := "chains-test-multibackendstorage" + ctx := logtesting.TestContextWithLogger(t) + c, ns, cleanup := setup(ctx, t, setupOpts{ + registry: true, + kanikoTaskImage: image, + }) + defer cleanup() - publicKey, err := cryptoutils.MarshalPublicKeyToPEM(c.secret.x509priv.Public()) - if err != nil { - t.Error(err) - } - verifyTaskRun := verifyKanikoTaskRun(ns, image, string(publicKey)) - tr, err = c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, verifyTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("error creating task: %s", err) - } + resetConfig := setConfigMap(ctx, t, c, test.cm) + defer resetConfig() + time.Sleep(3 * time.Second) - // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 60*time.Second) + createdObj := tekton.CreateObject(t, ctx, c.PipelineClient, test.getObject(ns)) - // It can take up to a minute for the secret data to be updated! - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) + publicKey, err := cryptoutils.MarshalPublicKeyToPEM(c.secret.x509priv.Public()) + if err != nil { + t.Error(err) + } - // Verify the payload signature. - verifySignature(ctx, t, c, tr) + // Verify object has been signed + obj := waitForCondition(ctx, t, c.PipelineClient, createdObj, signed, 120*time.Second) + // Verify the payload signature. + verifySignature(ctx, t, c, obj) + + verifyTro := verifyKanikoTaskRun(ns, fmt.Sprintf("%s/%s", c.internalRegistry, image), string(publicKey)) + createdVerifyTro := tekton.CreateObject(t, ctx, c.PipelineClient, verifyTro) + waitForCondition(ctx, t, c.PipelineClient, createdVerifyTro, successful, 60*time.Second) + }) + } } func TestRetryFailed(t *testing.T) { - ctx := logtesting.TestContextWithLogger(t) - c, ns, cleanup := setup(ctx, t, setupOpts{registry: true}) - defer cleanup() + tests := []struct { + name string + cm map[string]string + opts setupOpts + getObject func(ns string) objects.TektonObject + }{ + { + name: "taskrun", + cm: map[string]string{ + // don't set insecure repository, forcing signature upload to fail + "artifacts.oci.storage": "oci", + "artifacts.taskrun.storage": "tekton", + "storage.oci.repository": "gcr.io/not-real", + }, + opts: setupOpts{ + registry: true, + kanikoTaskImage: "chains-test-tr-retryfailed", + }, + getObject: getTaskRunObject, + }, + } - resetConfig := setConfigMap(ctx, t, c, map[string]string{ - // don't set insecure repository, forcing signature upload to fail - "artifacts.oci.storage": "oci", - "artifacts.taskrun.storage": "tekton", - "storage.oci.repository": "gcr.io/not-real", - }) - defer resetConfig() - time.Sleep(3 * time.Second) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + c, ns, cleanup := setup(ctx, t, test.opts) + defer cleanup() - // create necessary resources - imageName := "chains-test-retry" - image := fmt.Sprintf("%s/%s", c.internalRegistry, imageName) - task := kanikoTask(t, ns, image) + resetConfig := setConfigMap(ctx, t, c, test.cm) + defer resetConfig() + time.Sleep(3 * time.Second) - if _, err := c.PipelineClient.TektonV1beta1().Tasks(ns).Create(ctx, task, metav1.CreateOptions{}); err != nil { - t.Fatalf("error creating task: %s", err) - } + obj := tekton.CreateObject(t, ctx, c.PipelineClient, test.getObject(ns)) - taskRun := kanikoTaskRun(ns) - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, taskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) + // Give it a minute to complete. + waitForCondition(ctx, t, c.PipelineClient, obj, failed, 60*time.Second) + }) } - - // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, failed, 60*time.Second) } var imageTaskSpec = v1beta1.TaskSpec{ @@ -511,71 +580,148 @@ var imageTaskRun = v1beta1.TaskRun{ }, } -func TestProvenanceMaterials(t *testing.T) { - ctx := logtesting.TestContextWithLogger(t) - c, ns, cleanup := setup(ctx, t, setupOpts{}) - defer cleanup() +func getTaskRunObject(ns string) objects.TektonObject { + o := objects.NewTaskRunObject(&imageTaskRun) + o.Namespace = ns + return o +} - // Setup the right config. - resetConfig := setConfigMap(ctx, t, c, map[string]string{ - "artifacts.taskrun.format": "in-toto", - "artifacts.taskrun.signer": "x509", - "artifacts.taskrun.storage": "tekton", - }) - defer resetConfig() +func getTaskRunObjectWithParams(ns string, params []v1beta1.Param) objects.TektonObject { + o := objects.NewTaskRunObject(&imageTaskRun) + o.Namespace = ns + o.Spec.Params = params + return o +} - // modify image task run to add in the params we want to check for - commit := "my-git-commit" - url := "https://my-git-url" - imageTaskRun.Spec.Params = []v1beta1.Param{ - { - Name: "CHAINS-GIT_COMMIT", Value: *v1beta1.NewArrayOrString(commit), - }, { - Name: "CHAINS-GIT_URL", Value: *v1beta1.NewArrayOrString(url), +var imagePipelineRun = v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "image-pipelinerun", + Annotations: map[string]string{chains.RekorAnnotation: "true"}, + }, + Spec: v1beta1.PipelineRunSpec{ + PipelineSpec: &v1beta1.PipelineSpec{ + Tasks: []v1beta1.PipelineTask{ + { + Name: "echo", + TaskSpec: &v1beta1.EmbeddedTask{ + TaskSpec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{ + { + Image: "busybox", + Script: "echo success", + }, + }, + }, + }, + }, + }, }, - } - - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, &imageTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + }, +} - // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, done, 60*time.Second) +func getPipelineRunObject(ns string) objects.TektonObject { + o := objects.NewPipelineRunObject(&imagePipelineRun) + o.Namespace = ns + return o +} - // It can take up to a minute for the secret data to be updated! - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) +func getPipelineRunObjectWithParams(ns string, params []v1beta1.Param) objects.TektonObject { + o := objects.NewPipelineRunObject(&imagePipelineRun) + o.Namespace = ns + o.Spec.Params = params + return o +} - // get provenance, and make sure it has a materials section - payloadKey := fmt.Sprintf("chains.tekton.dev/payload-taskrun-%s", tr.UID) - body := tr.Annotations[payloadKey] - bodyBytes, err := base64.StdEncoding.DecodeString(body) - if err != nil { - t.Error(err) - } - var actualProvenance in_toto.Statement - if err := json.Unmarshal(bodyBytes, &actualProvenance); err != nil { - t.Error(err) - } - predicateBytes, err := json.Marshal(actualProvenance.Predicate) - if err := json.Unmarshal(bodyBytes, &actualProvenance); err != nil { - t.Error(err) - } - var predicate provenance.ProvenancePredicate - if err := json.Unmarshal(predicateBytes, &predicate); err != nil { - t.Fatal(err) - } - want := []provenance.ProvenanceMaterial{ +func TestProvenanceMaterials(t *testing.T) { + tests := []struct { + name string + cm map[string]string + getObjectWithParams func(ns string, params []v1beta1.Param) objects.TektonObject + payloadKey string + }{ + { + name: "taskrun", + cm: map[string]string{ + "artifacts.taskrun.format": "in-toto", + "artifacts.taskrun.signer": "x509", + "artifacts.taskrun.storage": "tekton", + }, + payloadKey: "chains.tekton.dev/payload-taskrun-%s", + getObjectWithParams: getTaskRunObjectWithParams, + }, { - URI: "git+" + url + ".git", - Digest: provenance.DigestSet{ - "sha1": commit, + name: "pipelinerun", + cm: map[string]string{ + "artifacts.pipelinerun.format": "in-toto", + "artifacts.pipelinerun.signer": "x509", + "artifacts.pipelinerun.storage": "tekton", }, + payloadKey: "chains.tekton.dev/payload-pipelinerun-%s", + getObjectWithParams: getPipelineRunObjectWithParams, }, } - got := predicate.Materials - if d := cmp.Diff(want, got); d != "" { - t.Fatal(string(d)) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + c, ns, cleanup := setup(ctx, t, setupOpts{}) + defer cleanup() + + // Setup the right config. + resetConfig := setConfigMap(ctx, t, c, test.cm) + defer resetConfig() + + commit := "my-git-commit" + url := "https://my-git-url" + params := []v1beta1.Param{ + { + Name: "CHAINS-GIT_COMMIT", Value: *v1beta1.NewArrayOrString(commit), + }, { + Name: "CHAINS-GIT_URL", Value: *v1beta1.NewArrayOrString(url), + }, + } + obj := test.getObjectWithParams(ns, params) + + createdObj := tekton.CreateObject(t, ctx, c.PipelineClient, obj) + + // Give it a minute to complete. + waitForCondition(ctx, t, c.PipelineClient, createdObj, done, 60*time.Second) + + // It can take up to a minute for the secret data to be updated! + signedObj := waitForCondition(ctx, t, c.PipelineClient, createdObj, signed, 120*time.Second) + + // get provenance, and make sure it has a materials section + payloadKey := fmt.Sprintf(test.payloadKey, signedObj.GetUID()) + body := signedObj.GetAnnotations()[payloadKey] + bodyBytes, err := base64.StdEncoding.DecodeString(body) + if err != nil { + t.Error(err) + } + var actualProvenance in_toto.Statement + if err := json.Unmarshal(bodyBytes, &actualProvenance); err != nil { + t.Error(err) + } + predicateBytes, err := json.Marshal(actualProvenance.Predicate) + if err := json.Unmarshal(bodyBytes, &actualProvenance); err != nil { + t.Error(err) + } + var predicate provenance.ProvenancePredicate + if err := json.Unmarshal(predicateBytes, &predicate); err != nil { + t.Fatal(err) + } + want := []provenance.ProvenanceMaterial{ + { + URI: "git+" + url + ".git", + Digest: provenance.DigestSet{ + "sha1": commit, + }, + }, + } + got := predicate.Materials + if d := cmp.Diff(want, got); d != "" { + t.Fatal(string(d)) + } + }) } } @@ -598,25 +744,24 @@ func TestVaultKMSSpire(t *testing.T) { defer resetConfig() time.Sleep(3 * time.Second) - tr, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, &imageTaskRun, metav1.CreateOptions{}) - if err != nil { - t.Errorf("error creating taskrun: %s", err) - } + tro := getTaskRunObject(ns) + createdTro := tekton.CreateObject(t, ctx, c.PipelineClient, tro) // Give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, done, 60*time.Second) - tr = waitForCondition(ctx, t, c.PipelineClient, tr.Name, ns, signed, 120*time.Second) - t.Log(tr.Annotations) + waitForCondition(ctx, t, c.PipelineClient, createdTro, done, 60*time.Second) + + // It can take up to a minute for the secret data to be updated! + obj := waitForCondition(ctx, t, c.PipelineClient, createdTro, signed, 120*time.Second) + t.Log(obj.GetAnnotations()) // Verify the payload signature. - verifySignature(ctx, t, c, tr) // TODO: consider removing + verifySignature(ctx, t, c, obj) // TODO: consider removing // verify the cert against the signature and payload - - sigKey := fmt.Sprintf("chains.tekton.dev/signature-taskrun-%s", tr.UID) - payloadKey := fmt.Sprintf("chains.tekton.dev/payload-taskrun-%s", tr.UID) - sigBytes := base64Decode(t, tr.Annotations[sigKey]) - payloadBytes := base64Decode(t, tr.Annotations[payloadKey]) + sigKey := fmt.Sprintf("chains.tekton.dev/signature-taskrun-%s", obj.GetUID()) + payloadKey := fmt.Sprintf("chains.tekton.dev/payload-taskrun-%s", obj.GetUID()) + sigBytes := base64Decode(t, obj.GetAnnotations()[sigKey]) + payloadBytes := base64Decode(t, obj.GetAnnotations()[payloadKey]) certPEM, err := os.ReadFile("testdata/vault.pub") if err != nil { diff --git a/test/examples_test.go b/test/examples_test.go index 9bfe114a32..f6716cee9d 100644 --- a/test/examples_test.go +++ b/test/examples_test.go @@ -42,55 +42,84 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/ghodss/yaml" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/test/tekton" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( - examplesPath = "../examples/taskruns" + taskRunExamplesPath = "../examples/taskruns" + pipelineRunExamplesPath = "../examples/pipelineruns" ) +type TestExample struct { + name string + cm map[string]string + getExampleObjects func(t *testing.T, ns string) map[string]objects.TektonObject + payloadKey string + signatureKey string +} + // TestExamples copies the format in the tektoncd/pipelines repo // https://github.com/tektoncd/pipeline/blob/main/test/examples_test.go func TestExamples(t *testing.T) { - ctx := context.Background() - c, ns, cleanup := setup(ctx, t, setupOpts{}) - defer cleanup() + tests := []TestExample{ + { + name: "taskrun-examples", + cm: map[string]string{ + "artifacts.taskrun.format": "in-toto", + "artifacts.oci.storage": "tekton", + }, + getExampleObjects: getTaskRunExamples, + payloadKey: "chains.tekton.dev/payload-taskrun-%s", + signatureKey: "chains.tekton.dev/signature-taskrun-%s", + }, + { + name: "pipelinerun-examples", + cm: map[string]string{ + "artifacts.pipelinerun.format": "in-toto", + "artifacts.pipelinerun.storage": "tekton", + }, + getExampleObjects: getPipelineRunExamples, + payloadKey: "chains.tekton.dev/payload-pipelinerun-%s", + signatureKey: "chains.tekton.dev/signature-pipelinerun-%s", + }, + } - cleanUpInTotoFormatter := setupInTotoFormatter(ctx, t, c) - runInTotoFormatterTests(ctx, t, ns, c) - cleanUpInTotoFormatter() -} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + c, ns, cleanup := setup(ctx, t, setupOpts{}) + defer cleanup() -func setupInTotoFormatter(ctx context.Context, t *testing.T, c *clients) func() { - // Setup the right config. - return setConfigMap(ctx, t, c, map[string]string{ - "artifacts.taskrun.format": "in-toto", - "artifacts.oci.storage": "tekton", - }) + cleanUpInTotoFormatter := setConfigMap(ctx, t, c, test.cm) + runInTotoFormatterTests(ctx, t, ns, c, test) + cleanUpInTotoFormatter() + }) + } } -func runInTotoFormatterTests(ctx context.Context, t *testing.T, ns string, c *clients) { - t.Parallel() - examples := getExamplePaths(t, examplesPath) - for _, example := range examples { - example := example - t.Run(example, func(t *testing.T) { - t.Logf("creating taskrun %v", example) - tr := taskRunFromExample(t, example) +func runInTotoFormatterTests(ctx context.Context, t *testing.T, ns string, c *clients, test TestExample) { + + // TODO: Commenting this out for now. Causes race condition where tests write and revert the chains-config + // and signing-secrets out of order + // t.Parallel() + + for path, obj := range test.getExampleObjects(t, ns) { + obj := obj + t.Run(path, func(t *testing.T) { + t.Logf("creating object %v", path) // create the task run - taskRun, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Create(ctx, tr, metav1.CreateOptions{}) - if err != nil { - t.Fatal(err) - } + createdObj := tekton.CreateObject(t, ctx, c.PipelineClient, obj) + // give it a minute to complete. - waitForCondition(ctx, t, c.PipelineClient, taskRun.Name, ns, done, 60*time.Second) + waitForCondition(ctx, t, c.PipelineClient, createdObj, done, 60*time.Second) // now validate the in-toto attestation - completed := waitForCondition(ctx, t, c.PipelineClient, taskRun.Name, ns, signed, 120*time.Second) - payload, _ := base64.StdEncoding.DecodeString(completed.Annotations[fmt.Sprintf("chains.tekton.dev/payload-taskrun-%s", completed.UID)]) - signature, _ := base64.StdEncoding.DecodeString(completed.Annotations[fmt.Sprintf("chains.tekton.dev/signature-taskrun-%s", completed.UID)]) + completed := waitForCondition(ctx, t, c.PipelineClient, createdObj, signed, 120*time.Second) + payload, _ := base64.StdEncoding.DecodeString(completed.GetAnnotations()[fmt.Sprintf(test.payloadKey, completed.GetUID())]) + signature, _ := base64.StdEncoding.DecodeString(completed.GetAnnotations()[fmt.Sprintf(test.signatureKey, completed.GetUID())]) t.Logf("Got attestation: %s", string(payload)) // make sure provenance is correct @@ -98,7 +127,7 @@ func runInTotoFormatterTests(ctx context.Context, t *testing.T, ns string, c *cl if err := json.Unmarshal(payload, &gotProvenance); err != nil { t.Fatal(err) } - expected := expectedProvenance(t, example, completed) + expected := expectedProvenance(t, path, completed) if d := cmp.Diff(gotProvenance, expected); d != "" { t.Fatalf("expected and got do not match:\n%v", d) @@ -148,17 +177,30 @@ func (v *verifier) Public() crypto.PublicKey { return v.pub } -func expectedProvenance(t *testing.T, example string, tr *v1beta1.TaskRun) intoto.ProvenanceStatement { - path := filepath.Join("testdata/intoto", strings.Replace(filepath.Base(example), ".yaml", ".json", 1)) - t.Logf("Reading expected provenance from %s", path) - - type Format struct { - Entrypoint string - BuildStartedOn string - BuildFinishedOn string - ContainerNames []string - StepImages []string +func expectedProvenance(t *testing.T, example string, obj objects.TektonObject) intoto.ProvenanceStatement { + switch obj.(type) { + case *objects.TaskRunObject: + return expectedTaskRunProvenance(t, example, obj) + case *objects.PipelineRunObject: + return expectedPipelineRunProvenance(t, example, obj) + default: + t.Error("Unexpected type trying to get provenance") } + return intoto.ProvenanceStatement{} +} + +type Format struct { + Entrypoint string + PipelineStartedOn string + PipelineFinishedOn string + BuildStartTimes []string + BuildFinishedTimes []string + ContainerNames []string + StepImages []string +} + +func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonObject) intoto.ProvenanceStatement { + tr := obj.GetObject().(*v1beta1.TaskRun) name := tr.Name if tr.Spec.TaskRef != nil { @@ -173,13 +215,40 @@ func expectedProvenance(t *testing.T, example string, tr *v1beta1.TaskRun) intot } f := Format{ - Entrypoint: name, - BuildStartedOn: tr.Status.StartTime.Time.UTC().Format(time.RFC3339), - BuildFinishedOn: tr.Status.CompletionTime.Time.UTC().Format(time.RFC3339), - ContainerNames: stepNames, - StepImages: images, + Entrypoint: name, + BuildStartTimes: []string{tr.Status.StartTime.Time.UTC().Format(time.RFC3339)}, + BuildFinishedTimes: []string{tr.Status.CompletionTime.Time.UTC().Format(time.RFC3339)}, + ContainerNames: stepNames, + StepImages: images, } + return readExpectedAttestation(t, example, f) +} + +func expectedPipelineRunProvenance(t *testing.T, example string, obj objects.TektonObject) intoto.ProvenanceStatement { + pr := obj.GetObject().(*v1beta1.PipelineRun) + + buildStartTimes := []string{} + buildFinishedTimes := []string{} + for _, trStatus := range pr.Status.TaskRuns { + buildStartTimes = append(buildStartTimes, trStatus.Status.StartTime.Time.UTC().Format(time.RFC3339)) + buildFinishedTimes = append(buildFinishedTimes, trStatus.Status.CompletionTime.Time.UTC().Format(time.RFC3339)) + } + + f := Format{ + PipelineStartedOn: pr.Status.StartTime.Time.UTC().Format(time.RFC3339), + PipelineFinishedOn: pr.Status.CompletionTime.Time.UTC().Format(time.RFC3339), + BuildStartTimes: buildStartTimes, + BuildFinishedTimes: buildFinishedTimes, + } + + return readExpectedAttestation(t, example, f) +} + +func readExpectedAttestation(t *testing.T, example string, f Format) intoto.ProvenanceStatement { + path := filepath.Join("testdata/intoto", strings.Replace(filepath.Base(example), ".yaml", ".json", 1)) + t.Logf("Reading expected provenance from %s", path) + contents, err := ioutil.ReadFile(path) if err != nil { t.Fatal(err) @@ -201,16 +270,20 @@ func expectedProvenance(t *testing.T, example string, tr *v1beta1.TaskRun) intot return expected } -func taskRunFromExample(t *testing.T, example string) *v1beta1.TaskRun { - contents, err := ioutil.ReadFile(example) - if err != nil { - t.Fatal(err) +func getTaskRunExamples(t *testing.T, ns string) map[string]objects.TektonObject { + examples := make(map[string]objects.TektonObject) + for _, example := range getExamplePaths(t, taskRunExamplesPath) { + examples[example] = taskRunFromExample(t, ns, example) } - var tr *v1beta1.TaskRun - if err := yaml.Unmarshal(contents, &tr); err != nil { - t.Fatal(err) + return examples +} + +func getPipelineRunExamples(t *testing.T, ns string) map[string]objects.TektonObject { + examples := make(map[string]objects.TektonObject) + for _, example := range getExamplePaths(t, pipelineRunExamplesPath) { + examples[example] = pipelineRunFromExample(t, ns, example) } - return tr + return examples } func getExamplePaths(t *testing.T, dir string) []string { @@ -234,3 +307,29 @@ func getExamplePaths(t *testing.T, dir string) []string { } return examplePaths } + +func taskRunFromExample(t *testing.T, ns, example string) objects.TektonObject { + contents, err := ioutil.ReadFile(example) + if err != nil { + t.Fatal(err) + } + var tr *v1beta1.TaskRun + if err := yaml.Unmarshal(contents, &tr); err != nil { + t.Fatal(err) + } + tr.Namespace = ns + return objects.NewTaskRunObject(tr) +} + +func pipelineRunFromExample(t *testing.T, ns, example string) objects.TektonObject { + contents, err := ioutil.ReadFile(example) + if err != nil { + t.Fatal(err) + } + var pr *v1beta1.PipelineRun + if err := yaml.Unmarshal(contents, &pr); err != nil { + t.Fatal(err) + } + pr.Namespace = ns + return objects.NewPipelineRunObject(pr) +} diff --git a/test/kaniko.go b/test/kaniko.go index c922370bed..a43c2ed5f9 100644 --- a/test/kaniko.go +++ b/test/kaniko.go @@ -21,6 +21,8 @@ import ( "testing" "github.com/google/go-containerregistry/pkg/name" + "github.com/tektoncd/chains/pkg/chains" + "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,8 +32,42 @@ const ( taskName = "kaniko-task" ) -func kanikoTaskRun(namespace string) *v1beta1.TaskRun { - return &v1beta1.TaskRun{ +func kanikoPipelineRun(ns string) objects.TektonObject { + imagePipelineRun := v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "image-pipelinerun", + Namespace: ns, + Annotations: map[string]string{chains.RekorAnnotation: "true"}, + }, + Spec: v1beta1.PipelineRunSpec{ + PipelineSpec: &v1beta1.PipelineSpec{ + Tasks: []v1beta1.PipelineTask{ + { + Name: "kaniko", + TaskRef: &v1beta1.TaskRef{ + Name: "kaniko-task", + Kind: v1beta1.NamespacedTaskKind, + }, + }, + }, + Results: []v1beta1.PipelineResult{ + { + Name: "IMAGE_URL", + Value: *v1beta1.NewArrayOrString("$(tasks.kaniko.results.IMAGE_URL)"), + }, + { + Name: "IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString("$(tasks.kaniko.results.IMAGE_DIGEST)"), + }, + }, + }, + }, + } + return objects.NewPipelineRunObject(&imagePipelineRun) +} + +func kanikoTaskRun(namespace string) objects.TektonObject { + tr := &v1beta1.TaskRun{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "kaniko-taskrun", Namespace: namespace, @@ -42,6 +78,7 @@ func kanikoTaskRun(namespace string) *v1beta1.TaskRun { }, }, } + return objects.NewTaskRunObject(tr) } func kanikoTask(t *testing.T, namespace, destinationImage string) *v1beta1.Task { @@ -112,7 +149,7 @@ func kanikoTask(t *testing.T, namespace, destinationImage string) *v1beta1.Task } } -func verifyKanikoTaskRun(namespace, destinationImage, publicKey string) *v1beta1.TaskRun { +func verifyKanikoTaskRun(namespace, destinationImage, publicKey string) objects.TektonObject { script := `#!/busybox/sh # save the public key @@ -125,7 +162,7 @@ cosign verify --allow-insecure-registry --key cosign.pub %s cosign verify-attestation --allow-insecure-registry --key cosign.pub %s` script = fmt.Sprintf(script, publicKey, destinationImage, destinationImage) - return &v1beta1.TaskRun{ + return objects.NewTaskRunObject(&v1beta1.TaskRun{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "verify-kaniko-taskrun", Namespace: namespace, @@ -141,5 +178,5 @@ cosign verify-attestation --allow-insecure-registry --key cosign.pub %s` }, }, }, - } + }) } diff --git a/test/test_utils.go b/test/test_utils.go index 53a21be3b6..493f5c1780 100644 --- a/test/test_utils.go +++ b/test/test_utils.go @@ -32,8 +32,9 @@ import ( "cloud.google.com/go/storage" "github.com/tektoncd/chains/pkg/chains/objects" - chainsstrorage "github.com/tektoncd/chains/pkg/chains/storage" + chainsstorage "github.com/tektoncd/chains/pkg/chains/storage" "github.com/tektoncd/chains/pkg/config" + "github.com/tektoncd/chains/pkg/test/tekton" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" pipelineclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,23 +50,23 @@ func getTr(ctx context.Context, t *testing.T, c pipelineclientset.Interface, nam return tr } -type conditionFn func(*v1beta1.TaskRun) bool +type conditionFn func(obj objects.TektonObject) bool -func waitForCondition(ctx context.Context, t *testing.T, c pipelineclientset.Interface, name, ns string, cond conditionFn, timeout time.Duration) *v1beta1.TaskRun { +func waitForCondition(ctx context.Context, t *testing.T, c pipelineclientset.Interface, obj objects.TektonObject, cond conditionFn, timeout time.Duration) objects.TektonObject { t.Helper() // Do a first quick check before setting the watch - tr := getTr(ctx, t, c, name, ns) - if cond(tr) { - return tr + o, err := tekton.GetObject(t, ctx, c, obj) + if err != nil { + t.Errorf("error watching object: %s", err) + } + if cond(o) { + return o } - w, err := c.TektonV1beta1().TaskRuns(ns).Watch(ctx, metav1.SingleObject(metav1.ObjectMeta{ - Name: name, - Namespace: ns, - })) + w, err := tekton.WatchObject(t, ctx, c, obj) if err != nil { - t.Errorf("error watching taskrun: %s", err) + t.Errorf("error watching object: %s", err) } // Setup a timeout channel @@ -79,33 +80,36 @@ func waitForCondition(ctx context.Context, t *testing.T, c pipelineclientset.Int for { select { case ev := <-w.ResultChan(): - tr := ev.Object.(*v1beta1.TaskRun) - if cond(tr) { - return tr + obj, err := objects.NewTektonObject(ev.Object) + if err != nil { + t.Errorf("watch result of unrecognized type: %s", err) + } + if cond(obj) { + return obj } case <-timeoutChan: // print logs from the TaskRun on timeout - printDebugging(t, ns, name) + printDebugging(t, obj) t.Fatal("time out") } } } -func successful(tr *v1beta1.TaskRun) bool { - return tr.IsSuccessful() +func successful(obj objects.TektonObject) bool { + return obj.IsSuccessful() } -func failed(tr *v1beta1.TaskRun) bool { - failed, ok := tr.Annotations["chains.tekton.dev/signed"] - return ok && failed == "failed" && tr.Annotations["chains.tekton.dev/retries"] == "3" +func failed(obj objects.TektonObject) bool { + failed, ok := obj.GetAnnotations()["chains.tekton.dev/signed"] + return ok && failed == "failed" && obj.GetAnnotations()["chains.tekton.dev/retries"] == "3" } -func done(tr *v1beta1.TaskRun) bool { - return tr.IsDone() +func done(obj objects.TektonObject) bool { + return obj.IsDone() } -func signed(tr *v1beta1.TaskRun) bool { - _, ok := tr.Annotations["chains.tekton.dev/signed"] +func signed(obj objects.TektonObject) bool { + _, ok := obj.GetAnnotations()["chains.tekton.dev/signed"] return ok } @@ -208,21 +212,21 @@ func setConfigMap(ctx context.Context, t *testing.T, c *clients, data map[string } } -func printDebugging(t *testing.T, ns, taskRunName string) { - t.Log("============================== TaskRun Logs ==============================") - output, _ := exec.Command("tkn", "tr", "logs", "-n", ns, taskRunName).CombinedOutput() +func printDebugging(t *testing.T, obj objects.TektonObject) { + t.Logf("============================== %s logs ==============================", obj.GetKind()) + output, _ := exec.Command("tkn", obj.GetKind(), "logs", "-n", obj.GetNamespace(), obj.GetName()).CombinedOutput() t.Log(string(output)) - t.Log("============================== TaskRun Describe ==============================") - output, _ = exec.Command("tkn", "tr", "describe", "-n", ns, taskRunName).CombinedOutput() + t.Logf("============================== %s describe ==============================", obj.GetKind()) + output, _ = exec.Command("tkn", obj.GetKind(), "describe", "-n", obj.GetNamespace(), obj.GetName()).CombinedOutput() t.Log(string(output)) - t.Log("============================== Chains Controller Logs ==============================") + t.Log("============================== chains controller cogs ==============================") output, _ = exec.Command("kubectl", "logs", "deploy/tekton-chains-controller", "-n", "tekton-chains").CombinedOutput() t.Log(string(output)) } -func verifySignature(ctx context.Context, t *testing.T, c *clients, tr *v1beta1.TaskRun) { +func verifySignature(ctx context.Context, t *testing.T, c *clients, obj objects.TektonObject) { // Retrieve the configuration. chainsConfig, err := c.KubeClient.CoreV1().ConfigMaps("tekton-chains").Get(ctx, "chains-config", metav1.GetOptions{}) if err != nil { @@ -237,27 +241,37 @@ func verifySignature(ctx context.Context, t *testing.T, c *clients, tr *v1beta1. logger := logging.FromContext(ctx) // Initialize the backend. - backends, err := chainsstrorage.InitializeBackends(ctx, c.PipelineClient, c.KubeClient, logger, *cfg) + backends, err := chainsstorage.InitializeBackends(ctx, c.PipelineClient, c.KubeClient, logger, *cfg) if err != nil { t.Errorf("error initializing backends: %s", err) } - for _, b := range cfg.Artifacts.TaskRuns.StorageBackend.List() { + + var configuredBackends []string + var key string + switch obj.GetObject().(type) { + case *objects.TaskRunObject: + configuredBackends = cfg.Artifacts.TaskRuns.StorageBackend.List() + key = fmt.Sprintf("taskrun-%s", obj.GetUID()) + case *objects.PipelineRunObject: + configuredBackends = cfg.Artifacts.PipelineRuns.StorageBackend.List() + key = fmt.Sprintf("pipelinerun-%s", obj.GetUID()) + } + + for _, b := range configuredBackends { t.Logf("Backend name: %q\n", b) backend := backends[b] // Initialize the storage options. opts := config.StorageOpts{ - ShortKey: fmt.Sprintf("taskrun-%s", tr.UID), + ShortKey: key, } - trObj := objects.NewTaskRunObject(tr) - // Let's fetch the signature and body. - signatures, err := backend.RetrieveSignatures(ctx, trObj, opts) + signatures, err := backend.RetrieveSignatures(ctx, obj, opts) if err != nil { t.Errorf("error retrieving the signature: %s", err) } - payloads, err := backend.RetrievePayloads(ctx, trObj, opts) + payloads, err := backend.RetrievePayloads(ctx, obj, opts) if err != nil { t.Errorf("error retrieving the payload: %s", err) } diff --git a/test/testdata/intoto/pipeline-output-image.json b/test/testdata/intoto/pipeline-output-image.json new file mode 100644 index 0000000000..757dea64ac --- /dev/null +++ b/test/testdata/intoto/pipeline-output-image.json @@ -0,0 +1,81 @@ +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", + "subject": [ + { + "name": "gcr.io/foo/bar", + "digest": { + "sha256": "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5" + } + } + ], + "predicate": { + "builder": { + "id": "https://tekton.dev/chains/v2" + }, + "buildType": "tekton.dev/v1beta1/PipelineRun", + "invocation": { + "configSource": {}, + "parameters": { + "CHAINS-GIT_COMMIT": "my-git-commit", + "CHAINS-GIT_URL": "https://my-git-url" + } + }, + "buildConfig": { + "tasks": [ + { + "name": "buildimage", + "ref": {}, + "startedOn": "{{index .BuildStartTimes 0}}", + "finishedOn": "{{index .BuildFinishedTimes 0}}", + "status": "Succeeded", + "steps": [ + { + "entryPoint": "#!/usr/bin/env bash\necho 'gcr.io/foo/bar' | tee /tekton/results/IMAGE_URL\necho 'sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5' | tee /tekton/results/IMAGE_DIGEST", + "arguments": null, + "environment": { + "container": "create-dockerfile", + "image": "docker.io/library/bash@sha256:0ba55510cdffa76de0d3d8149a3fa7cb62d9725d1a606fc234d18778e7807ac3" + }, + "annotations": null + } + ], + "invocation": { + "configSource": {}, + "parameters": {} + }, + "results": [ + { + "name": "IMAGE_DIGEST", + "type": "string", + "value": "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5\n" + }, + { + "name": "IMAGE_URL", + "type": "string", + "value": "gcr.io/foo/bar\n" + } + ] + } + ] + }, + "metadata": { + "buildStartedOn": "{{.PipelineStartedOn}}", + "buildFinishedOn": "{{.PipelineFinishedOn}}", + "completeness": { + "parameters": false, + "environment": false, + "materials": false + }, + "reproducible": false + }, + "materials": [ + { + "uri": "git+https://my-git-url.git", + "digest": { + "sha1": "my-git-commit" + } + } + ] + } +} diff --git a/test/testdata/intoto/task-output-image.json b/test/testdata/intoto/task-output-image.json index 4a42f128f5..a4e440129f 100644 --- a/test/testdata/intoto/task-output-image.json +++ b/test/testdata/intoto/task-output-image.json @@ -68,8 +68,8 @@ ] }, "metadata": { - "buildStartedOn": "{{.BuildStartedOn}}", - "buildFinishedOn": "{{.BuildFinishedOn}}", + "buildStartedOn": "{{index .BuildStartTimes 0}}", + "buildFinishedOn": "{{index .BuildFinishedTimes 0}}", "completeness": { "parameters": false, "environment": false,