Skip to content

Commit

Permalink
Set ConfigSource in ociresolver
Browse files Browse the repository at this point in the history
Prior, a field named Source was introduced to `ResolutionRequest` status
to record the source where the remote resource came from. And the
individual resolvers need to implement the Source function to set the
correct source value. But the method in ociresolver returns a nil value.

Now, we return correct source value with the 3 subfields: url, digest and entrypoint
- url: [image repository name](https://pkg.go.dev/github.com/google/go-containerregistry@v0.12.0/pkg/name#Repository.Name)
- digest: image digest
- entrypoint: resource name in the OCI bundle

Signed-off-by: Chuang Wang <chuangw@google.com>
  • Loading branch information
chuangw6 committed Nov 10, 2022
1 parent 61bf43f commit 4a1f6cd
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 47 deletions.
67 changes: 67 additions & 0 deletions docs/bundle-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,73 @@ spec:
value: "tekton pipelines"
```

## `ResolutionRequest` Status
`ResolutionRequest.Status.Source` field captures the source where the remote resource came from. It includes the 3 subfields: `url`, `digest` and `entrypoint`.
- `url`: The image repository URI
- `digest`: The map of the algorithm portion -> the hex encoded portion of the image digest.
- `entrypoint`: The resource name in the OCI bundle image.

Example:
- TaskRun Resolution
```yaml
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: remote-task-reference
spec:
taskRef:
resolver: bundles
params:
- name: bundle
value: gcr.io/tekton-releases/catalog/upstream/git-clone:0.7
- name: name
value: git-clone
- name: kind
value: task
params:
- name: url
value: https://github.com/octocat/Hello-World
workspaces:
- name: output
volumeClaimTemplate:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
```

- `ResolutionRequest`
```yaml
apiVersion: resolution.tekton.dev/v1beta1
kind: ResolutionRequest
metadata:
...
labels:
resolution.tekton.dev/type: bundles
name: bundles-21ad80ec13f3e8b73fed5880a64d4611
...
spec:
params:
- name: bundle
value: gcr.io/tekton-releases/catalog/upstream/git-clone:0.7
- name: name
value: git-clone
- name: kind
value: task
status:
annotations: ...
...
data: xxx
observedGeneration: 1
source:
digest:
sha256: f51ca50f1c065acba8290ef14adec8461915ecc5f70a8eb26190c6e8e0ededaf
entryPoint: git-clone
uri: gcr.io/tekton-releases/catalog/upstream/git-clone
```

---

Except as otherwise noted, the content of this page is licensed under the
Expand Down
27 changes: 21 additions & 6 deletions pkg/resolution/resolver/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/resolution/resolver/framework"
)
Expand Down Expand Up @@ -72,9 +73,14 @@ func (br *ResolvedResource) Source() *pipelinev1beta1.ConfigSource {
// GetEntry accepts a keychain and options for the request and returns
// either a successfully resolved bundle entry or an error.
func GetEntry(ctx context.Context, keychain authn.Keychain, opts RequestOptions) (*ResolvedResource, error) {
img, err := retrieveImage(ctx, keychain, opts.Bundle)
uri, img, err := retrieveImage(ctx, keychain, opts.Bundle)
if err != nil {
return nil, err
return nil, fmt.Errorf("cannot retrieve the oci image: %w", err)
}

h, err := img.Digest()
if err != nil {
return nil, fmt.Errorf("cannot get the oci digest: %w", err)
}

manifest, err := img.Manifest()
Expand Down Expand Up @@ -117,19 +123,28 @@ func GetEntry(ctx context.Context, keychain authn.Keychain, opts RequestOptions)
ResolverAnnotationName: lName,
ResolverAnnotationAPIVersion: l.Annotations[BundleAnnotationAPIVersion],
},
source: &v1beta1.ConfigSource{
URI: uri,
Digest: map[string]string{
h.Algorithm: h.Hex,
},
EntryPoint: opts.EntryName,
},
}, nil
}
}
return nil, fmt.Errorf("could not find object in image with kind: %s and name: %s", opts.Kind, opts.EntryName)
}

// retrieveImage will fetch the image's contents and manifest.
func retrieveImage(ctx context.Context, keychain authn.Keychain, ref string) (v1.Image, error) {
// retrieveImage will fetch the image's url, contents and manifest.
func retrieveImage(ctx context.Context, keychain authn.Keychain, ref string) (string, v1.Image, error) {
imgRef, err := name.ParseReference(ref)
if err != nil {
return nil, fmt.Errorf("%s is an unparseable image reference: %w", ref, err)
return "", nil, fmt.Errorf("%s is an unparseable image reference: %w", ref, err)
}
return remote.Image(imgRef, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx))

img, err := remote.Image(imgRef, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx))
return imgRef.Context().Name(), img, err
}

// checkImageCompliance will perform common checks to ensure the Tekton Bundle is compliant to our spec.
Expand Down
116 changes: 75 additions & 41 deletions pkg/resolution/resolver/bundle/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -245,12 +246,6 @@ func TestResolve(t *testing.T) {
t.Fatalf("couldn't marshal pipeline: %v", err)
}

validObjects := map[string][]runtime.Object{
"single-task": {exampleTask},
"single-pipeline": {examplePipeline},
"multiple-resources": {exampleTask, examplePipeline},
}

// too many objects in bundle resolver test
var tooManyObjs []runtime.Object
for i := 0; i <= MaximumBundleObjects; i++ {
Expand All @@ -267,67 +262,80 @@ func TestResolve(t *testing.T) {
tooManyObjs = append(tooManyObjs, &obj)
}

invalidObjects := map[string][]runtime.Object{
"too-many-objs": tooManyObjs,
"single-task-no-version": {&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{Kind: "task"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}},
"single-task-no-kind": {&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}},
"single-task-no-name": {&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1", Kind: "task"}}},
"single-task-kind-incorrect-form": {&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1", Kind: "Task"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}},
}

// Set up a fake registry to push an image to.
s := httptest.NewServer(registry.New())
defer s.Close()
u, err := url.Parse(s.URL)
if err != nil {
t.Fatal(err)
}

validImageRefs := pushImagesToRegistry(t, u.Host, validObjects, test.DefaultObjectAnnotationMapper)
invalidImageRefs := pushImagesToRegistry(t, u.Host, invalidObjects, asIsMapper)
r := fmt.Sprintf("%s/%s", u.Host, "testbundleresolver")
testImages := map[string]*imageRef{
"single-task": pushToRegistry(t, r, "single-task", []runtime.Object{exampleTask}, test.DefaultObjectAnnotationMapper),
"single-pipeline": pushToRegistry(t, r, "single-pipeline", []runtime.Object{examplePipeline}, test.DefaultObjectAnnotationMapper),
"multiple-resources": pushToRegistry(t, r, "multiple-resources", []runtime.Object{exampleTask, examplePipeline}, test.DefaultObjectAnnotationMapper),
"too-many-objs": pushToRegistry(t, r, "too-many-objs", tooManyObjs, asIsMapper),
"single-task-no-version": pushToRegistry(t, r, "single-task-no-version", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{Kind: "task"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, asIsMapper),
"single-task-no-kind": pushToRegistry(t, r, "single-task-no-kind", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, asIsMapper),
"single-task-no-name": pushToRegistry(t, r, "single-task-no-name", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1", Kind: "task"}}}, asIsMapper),
"single-task-kind-incorrect-form": pushToRegistry(t, r, "single-task-kind-incorrect-form", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1", Kind: "Task"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, asIsMapper),
}

testcases := []struct {
name string
args *params
imageName string
expectedStatus *v1beta1.ResolutionRequestStatus
expectedErrMessage string
}{
{
name: "single task",
name: "single task: digest is included in the bundle parameter",
args: &params{
bundle: fmt.Sprintf("%s@%s:%s", testImages["single-task"].uri, testImages["single-task"].algo, testImages["single-task"].hex),
name: "example-task",
kind: "task",
},
imageName: "single-task",
expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML),
}, {
name: "single task: tag is included in the bundle parameter",
args: &params{
bundle: validImageRefs["single-task"],
bundle: testImages["single-task"].uri + ":latest",
name: "example-task",
kind: "task",
},
imageName: "single-task",
expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML),
}, {
name: "single task using default kind value from configmap",
name: "single task: using default kind value from configmap",
args: &params{
bundle: validImageRefs["single-task"],
bundle: testImages["single-task"].uri + ":latest",
name: "example-task",
},
imageName: "single-task",
expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML),
}, {
name: "single pipeline",
name: "single pipeline: using default kind value from configmap",
args: &params{
bundle: validImageRefs["single-pipeline"],
bundle: testImages["single-pipeline"].uri + ":latest",
name: "example-pipeline",
kind: "pipeline",
},
imageName: "single-pipeline",
expectedStatus: internal.CreateResolutionRequestStatusWithData(pipelineAsYAML),
}, {
name: "multiple resources",
name: "multiple resources: an image has both task and pipeline resource",
args: &params{
bundle: validImageRefs["multiple-resources"],
bundle: testImages["multiple-resources"].uri + ":latest",
name: "example-pipeline",
kind: "pipeline",
},
imageName: "multiple-resources",
expectedStatus: internal.CreateResolutionRequestStatusWithData(pipelineAsYAML),
},
{
}, {
name: "too many objects in an image",
args: &params{
bundle: invalidImageRefs["too-many-objs"],
bundle: testImages["too-many-objs"].uri + ":latest",
name: "2-task",
kind: "task",
},
Expand All @@ -336,7 +344,7 @@ func TestResolve(t *testing.T) {
}, {
name: "single task no version",
args: &params{
bundle: invalidImageRefs["single-task-no-version"],
bundle: testImages["single-task-no-version"].uri + ":latest",
name: "foo",
kind: "task",
},
Expand All @@ -345,7 +353,7 @@ func TestResolve(t *testing.T) {
}, {
name: "single task no kind",
args: &params{
bundle: invalidImageRefs["single-task-no-kind"],
bundle: testImages["single-task-no-kind"].uri + ":latest",
name: "foo",
kind: "task",
},
Expand All @@ -354,7 +362,7 @@ func TestResolve(t *testing.T) {
}, {
name: "single task no name",
args: &params{
bundle: invalidImageRefs["single-task-no-name"],
bundle: testImages["single-task-no-name"].uri + ":latest",
name: "foo",
kind: "task",
},
Expand All @@ -363,7 +371,7 @@ func TestResolve(t *testing.T) {
}, {
name: "single task kind incorrect form",
args: &params{
bundle: invalidImageRefs["single-task-kind-incorrect-form"],
bundle: testImages["single-task-kind-incorrect-form"].uri + ":latest",
name: "foo",
kind: "task",
},
Expand Down Expand Up @@ -421,6 +429,15 @@ func TestResolve(t *testing.T) {

expectedStatus.Annotations[ResolverAnnotationName] = tc.args.name
expectedStatus.Annotations[ResolverAnnotationAPIVersion] = "v1beta1"

expectedStatus.Source = &pipelinev1beta1.ConfigSource{
URI: testImages[tc.imageName].uri,
Digest: map[string]string{
testImages[tc.imageName].algo: testImages[tc.imageName].hex,
},
EntryPoint: tc.args.name,
}

} else {
expectedError = createError(tc.args.bundle, tc.expectedErrMessage)
expectedStatus.Status.Conditions[0].Message = expectedError.Error()
Expand Down Expand Up @@ -493,16 +510,33 @@ func resolverContext() context.Context {
return frtesting.ContextWithBundlesResolverEnabled(context.Background())
}

func pushImagesToRegistry(t *testing.T, host string, objects map[string][]runtime.Object, mapper test.ObjectAnnotationMapper) map[string]string {
refs := map[string]string{}
type imageRef struct {
// uri is the image repositry identifier i.e. "gcr.io/tekton-releases/catalog/upstream/golang-build"
uri string
// algo is the algorithm portion of a particular image digest i.e. "sha256".
algo string
// hex is hex encoded portion of a particular image digest i.e. "23293df97dc11957ec36a88c80101bb554039a76e8992a435112eea8283b30d4".
hex string
}

for name, objs := range objects {
ref, err := test.CreateImageWithAnnotations(fmt.Sprintf("%s/testbundleresolver/%s", host, name), mapper, objs...)
if err != nil {
t.Fatalf("couldn't push the image: %v", err)
}
refs[name] = ref
// pushToRegistry pushes an images to the registry and then returns an imageRef.
// It accepts a registry address, image name, the data and an ObjectAnnotationMapper
// to map an object to the annotations for it.
// NOTE: Every image pushed to the registry has a default tag named "latest".
func pushToRegistry(t *testing.T, registry, imageName string, data []runtime.Object, mapper test.ObjectAnnotationMapper) *imageRef {
ref, err := test.CreateImageWithAnnotations(fmt.Sprintf("%s/%s:latest", registry, imageName), mapper, data...)
if err != nil {
t.Fatalf("couldn't push the image: %v", err)
}

return refs
refSplit := strings.Split(ref, "@")
uri, digest := refSplit[0], refSplit[1]
digSplits := strings.Split(digest, ":")
algo, hex := digSplits[0], digSplits[1]

return &imageRef{
uri: uri,
algo: algo,
hex: hex,
}
}

0 comments on commit 4a1f6cd

Please sign in to comment.