-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds resolver to provide task resolution for images
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
Showing
13 changed files
with
862 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
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" | ||
"log" | ||
"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" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
) | ||
|
||
func init() { | ||
// Add Tekton's resources to the K8s deserializer | ||
schemeBuilder := runtime.NewSchemeBuilder(v1beta1.AddToScheme) | ||
err := schemeBuilder.AddToScheme(scheme.Scheme) | ||
if err != nil { | ||
log.Panic(err) | ||
} | ||
} | ||
|
||
// ImageResolver will attempt to fetch Tekton resources from an OCI compliant image repository. | ||
type OCIResolver struct { | ||
imageReference string | ||
} | ||
|
||
// 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) { | ||
// TODO: When this is moved into the Tekton controller, authorize this | ||
// pull as a Service Account in the cluster, and don't rely on the | ||
// contents of ~/.docker/config.json (which won't exist). | ||
imgRef, err := imgname.ParseReference(o.imageReference) | ||
if err != nil { | ||
return nil, fmt.Errorf("%s is an unparseable task image reference: %w", o.imageReference, err) | ||
} | ||
|
||
img, err := remote.Image(imgRef, remote.WithAuthFromKeychain(authn.DefaultKeychain)) | ||
if err != nil { | ||
return nil, fmt.Errorf("Error pulling %q: %w", o.imageReference, 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"] != getLayerName(kind, name) { | ||
continue | ||
} | ||
|
||
// TODO: Check for application/vnd.cdf.tekton.catalog.v1beta1+yaml or similar as the media type. | ||
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 getLayerName(kind string, name string) string { | ||
return fmt.Sprintf("%s/%s", strings.ToLower(kind), name) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package remote_test | ||
|
||
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/name" | ||
"github.com/google/go-containerregistry/pkg/registry" | ||
"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/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" | ||
"github.com/tektoncd/pipeline/pkg/remote" | ||
v1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
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!\"", | ||
}, | ||
}, | ||
}, | ||
} | ||
taskRaw, err := yaml.Marshal(task) | ||
if err != nil { | ||
t.Errorf("invalid sample task def %s", err.Error()) | ||
} | ||
|
||
img := empty.Image | ||
layer, err := tarball.LayerFromReader(strings.NewReader(string(taskRaw))) | ||
if err != nil { | ||
t.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": "task/hello-world", | ||
}, | ||
}) | ||
if err != nil { | ||
t.Errorf("could not add layer to image %s", err.Error()) | ||
} | ||
|
||
if err := remoteimg.Write(imgRef, img); err != nil { | ||
t.Errorf("could not push example image to registry") | ||
} | ||
|
||
// 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 := remote.NewResolver(imgRef.Context().Digest(digest.String()).String()) | ||
|
||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* | ||
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/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) Resolver { | ||
return OCIResolver{ | ||
imageReference: imageReference, | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.