Skip to content

Commit

Permalink
Adds resolver to provide task resolution for images
Browse files Browse the repository at this point in the history
This partially addresses the desire to fetch tasks
from an OCI image artifact.

Issue: #1839

Signed-off-by: Sunil Thaha <sthaha@redhat.com>
  • Loading branch information
sthaha committed Mar 6, 2020
1 parent c7407e6 commit 8874c6d
Show file tree
Hide file tree
Showing 11 changed files with 885 additions and 0 deletions.
109 changes: 109 additions & 0 deletions pkg/remote/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2019 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 remote

import (
"fmt"
"io/ioutil"
"strings"

"github.com/google/go-containerregistry/pkg/authn"
imgname "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"
"github.com/tektoncd/pipeline/pkg/client/clientset/versioned/scheme"
)

// KeychainProvider is an input to the OCIResolver which returns a keychain for fetching remote images with
// authentication.
type KeychainProvider func() (authn.Keychain, error)

// ImageResolver will attempt to fetch Tekton resources from an OCI compliant image repository.
type OCIResolver struct {
imageReference string
keychainProvider KeychainProvider
}

// GetTask will retrieve the specified task from the resolver's defined image and return its spec. If it cannot be
// retrieved for any reason, an error is returned.
func (o OCIResolver) GetTask(taskName string) (*v1beta1.TaskSpec, error) {
taskContents, err := o.readImageLayer("task", taskName)
if err != nil {
return nil, err
}

// Deserialize the task into a valid task spec.
var task v1beta1.Task
_, _, err = scheme.Codecs.UniversalDeserializer().Decode(taskContents, nil, &task)
if err != nil {
return nil, fmt.Errorf("Invalid remote task %s: %w", taskName, err)
}

return &task.Spec, nil
}

func (o OCIResolver) readImageLayer(kind string, name string) ([]byte, error) {
imgRef, err := imgname.ParseReference(o.imageReference)
if err != nil {
return nil, fmt.Errorf("%s is an unparseable task image reference: %w", o.imageReference, err)
}

// Create a keychain for use in authenticating against the remote repository.
keychain, err := o.keychainProvider()
if err != nil {
return nil, err
}

img, err := remote.Image(imgRef, remote.WithAuthFromKeychain(keychain))
if err != nil {
return nil, fmt.Errorf("Error pulling image %q: %w", o.imageReference, err)
}
// Ensure the media type is exclusively the Tekton catalog media type.
if mt, err := img.MediaType(); err != nil || string(mt) != "application/vnd.cdf.tekton.catalog.v1beta1+yaml" {
return nil, fmt.Errorf("cannot parse reference from image type %s: %w", string(mt), err)
}

m, err := img.Manifest()
if err != nil {
return nil, err
}

ls, err := img.Layers()
if err != nil {
return nil, err
}
var layer v1.Layer
for idx, l := range m.Layers {
if l.Annotations["org.opencontainers.image.title"] == o.getLayerName(kind, name) {
layer = ls[idx]
}
}
if layer == nil {
return nil, fmt.Errorf("Resource %s/%s not found", kind, name)
}
rc, err := layer.Uncompressed()
if err != nil {
return nil, err
}
defer rc.Close()
return ioutil.ReadAll(rc)
}

func (o OCIResolver) getLayerName(kind string, name string) string {
return fmt.Sprintf("%s/%s", strings.ToLower(kind), name)
}
124 changes: 124 additions & 0 deletions pkg/remote/oci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2019 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 remote

import (
"fmt"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/ghodss/yaml"
"github.com/google/go-cmp/cmp"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
imgv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
remoteimg "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func pushImage(imgRef name.Reference, task *v1beta1.Task) (imgv1.Image, error) {
taskRaw, err := yaml.Marshal(task)
if err != nil {
return nil, fmt.Errorf("invalid sample task def %s", err.Error())
}

img := mutate.MediaType(empty.Image, types.MediaType("application/vnd.cdf.tekton.catalog.v1beta1+yaml"))
layer, err := tarball.LayerFromReader(strings.NewReader(string(taskRaw)))
if err != nil {
return nil, fmt.Errorf("unexpected error adding task layer to image %s", err.Error())
}

img, err = mutate.Append(img, mutate.Addendum{
Layer: layer,
Annotations: map[string]string{
"org.opencontainers.image.title": fmt.Sprintf("task/%s", task.GetName()),
},
})
if err != nil {
return nil, fmt.Errorf("could not add layer to image %s", err.Error())
}

if err := remoteimg.Write(imgRef, img); err != nil {
return nil, fmt.Errorf("could not push example image to registry")
}

return img, nil
}

func TestOCIResolver(t *testing.T) {
// 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)
}

imgRef, err := name.ParseReference(fmt.Sprintf("%s/test/ociresolver", u.Host))
if err != nil {
t.Errorf("undexpected error producing image reference %s", err.Error())
}

// Create the image using an example task.
task := v1beta1.Task{
ObjectMeta: metav1.ObjectMeta{
Name: "hello-world",
},
Spec: v1beta1.TaskSpec{
Steps: []v1beta1.Step{
{
Container: v1.Container{
Image: "ubuntu",
},
Script: "echo \"Hello World!\"",
},
},
},
}
img, err := pushImage(imgRef, &task)
if err != nil {
t.Error(err)
}

// Now we can call our resolver and see if the spec returned is the same.
digest, err := img.Digest()
if err != nil {
t.Errorf("unexpected error getting digest of image: %s", err.Error())
}
resolver := OCIResolver{
imageReference: imgRef.Context().Digest(digest.String()).String(),
keychainProvider: func() (authn.Keychain, error) { return authn.DefaultKeychain, nil },
}

actual, err := resolver.GetTask("hello-world")
if err != nil {
t.Errorf("failed to fetch task hello-world: %s", err.Error())
}

if diff := cmp.Diff(actual, &task.Spec); diff != "" {
t.Error(diff)
}
}
38 changes: 38 additions & 0 deletions pkg/remote/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2019 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 remote

import (
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
)

// Resolver will retreive Tekton resources like Tasks from remote repositories like an OCI image repositories.
type Resolver interface {
GetTask(taskName string) (*v1beta1.TaskSpec, error)
}

// TODO: Right now, there is only one resolver type. When more are added, this will need to be updated.
func NewResolver(imageReference string, serviceAccountName string) Resolver {
return OCIResolver{
imageReference: imageReference,
keychainProvider: func() (authn.Keychain, error) {
return k8schain.NewInCluster(k8schain.Options{ServiceAccountName: serviceAccountName})
},
}
}
2 changes: 2 additions & 0 deletions third_party/github.com/hashicorp/errwrap/go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions third_party/github.com/hashicorp/go-multierror/go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8874c6d

Please sign in to comment.