-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds resolver to provide task resolution for images #2137
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
|
||
// OCIResolver 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) | ||
} |
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: undexpected -> unexpected |
||
} | ||
|
||
// 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) | ||
} | ||
} |
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}) | ||
}, | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's a typical usage of this type going to look like? If it's most often going to be instantiated and immediately used like this:
then I suggest simply exposing a
GetOCITask
func that takes(imageReference string, keychainProvider KeychainProvider, taskName string)
. WDYT?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah that makes a ton of sense. I guess it comes down to how this evolves when we add a resolver for Git or other remote sources. I could see an argument for either route.
I think the idea here is that it might be easier for a
reconciler
to use a factory to instantiate aResolver
and call it instead of having to know itself whether it should use an OCIResolver or a GitResolver or whatever else we add. We might even add a "local" resolver and embed the local task resolver into this format. Could be overkill or could be a nice abstraction. Thoughts?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can definitely see a factory fitting that use case. I'm not sure it follows that we'd therefore need a
remote.Resolver
interface and creation of structs like this anywhere outside the factory's internals though.I think it'll ultimately depend on both the kind of configuration we expect to be available and the signature we want to expose for resolution. Examples of config might be that we allow specific resolvers to be turned on/off/configured via configmap / cli flags. Examples of public signature might be a func like
ResolveTaskFromRef(taskRef)
that figures out the kind of resolution it needs to make without publicly exposing the Resolver type.Not to say we need to commit to those kinds of design choices here, more that by introducing the Resolver machinery before we get to that stage we're already kind of leaning towards a design. And as often happens we might end up needing to unwind those kinds of choices based on work discovered as we dig into that stage more thoroughly. And in turn that can lead to a some extra work reviewing in future as well.
None of this is a blocker at all from my POV, just raising it from my read-through. 👍
/lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup. Makes a ton of sense. We can always wrap the resolver in a function like you mention to give a single interface to caller but I guess the next PR that embeds this in the reconciler will show if it makes it more or less "gross" and we can go from there 😄