Skip to content

Commit

Permalink
Grafeas storage for PipelineRun-level provenance (#601)
Browse files Browse the repository at this point in the history
Support storing pipelinerun level provenance in grafeas
backend.

There are 3 main aspects of the PR:
- created different BUILD Notes for storing both taskrun and pipelinerun
level provenance/occurrence in grafeas that attest the same artifact.
- extracted artifact identifiers from intoto subjects instead of calling
individual helpers to extract from results.
- changed TektonObject "GetKind" method to "GetGVK" to get the whole
GroupVersionKind information.

Signed-off-by: Chuang Wang <chuangw@google.com>

Signed-off-by: Chuang Wang <chuangw@google.com>
  • Loading branch information
chuangw6 authored Oct 22, 2022
1 parent 487e74a commit 9a47793
Show file tree
Hide file tree
Showing 12 changed files with 764 additions and 329 deletions.
26 changes: 24 additions & 2 deletions pkg/chains/formats/intotoite6/extract/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package extract

import (
"fmt"
"sort"
"strings"

Expand All @@ -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 "<algorithm>:<actual-sha>" 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

Expand Down Expand Up @@ -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
}
165 changes: 165 additions & 0 deletions pkg/chains/formats/intotoite6/extract/extract_test.go
Original file line number Diff line number Diff line change
@@ -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 TestSubjectDigestsAndRetrieveAllArtifactURIs(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,
},
},
},
)
}
3 changes: 1 addition & 2 deletions pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ limitations under the License.
package pipelinerun

import (
"fmt"
"time"

intoto "github.com/in-toto/in-toto-golang/in_toto"
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 1 addition & 5 deletions pkg/chains/formats/intotoite6/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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),
Expand Down
25 changes: 13 additions & 12 deletions pkg/chains/objects/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/chains/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
Expand Down
Loading

0 comments on commit 9a47793

Please sign in to comment.