diff --git a/pkg/remote/oci.go b/pkg/remote/oci.go new file mode 100644 index 00000000000..d4d48d3c5f9 --- /dev/null +++ b/pkg/remote/oci.go @@ -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) +} diff --git a/pkg/remote/oci_test.go b/pkg/remote/oci_test.go new file mode 100644 index 00000000000..95c52bb8072 --- /dev/null +++ b/pkg/remote/oci_test.go @@ -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) + } +} diff --git a/pkg/remote/resolver.go b/pkg/remote/resolver.go new file mode 100644 index 00000000000..dc82c4582ff --- /dev/null +++ b/pkg/remote/resolver.go @@ -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}) + }, + } +} diff --git a/third_party/github.com/hashicorp/errwrap/go.mod b/third_party/github.com/hashicorp/errwrap/go.mod index c9b84022cf7..d7ed369809b 100644 --- a/third_party/github.com/hashicorp/errwrap/go.mod +++ b/third_party/github.com/hashicorp/errwrap/go.mod @@ -1 +1,3 @@ module github.com/hashicorp/errwrap + +go 1.13 diff --git a/third_party/github.com/hashicorp/go-multierror/go.mod b/third_party/github.com/hashicorp/go-multierror/go.mod index 2534331d5f9..8c91af65b26 100644 --- a/third_party/github.com/hashicorp/go-multierror/go.mod +++ b/third_party/github.com/hashicorp/go-multierror/go.mod @@ -1,3 +1,5 @@ module github.com/hashicorp/go-multierror require github.com/hashicorp/errwrap v1.0.0 + +go 1.13 diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go new file mode 100644 index 00000000000..978ff480310 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go @@ -0,0 +1,224 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// 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 registry + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "math/rand" + "net/http" + "path" + "strings" + "sync" +) + +// Returns whether this url should be handled by the blob handler +// This is complicated because blob is indicated by the trailing path, not the leading path. +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-a-layer +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-a-layer +func isBlob(req *http.Request) bool { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + if elem[len(elem)-1] == "" { + elem = elem[:len(elem)-1] + } + if len(elem) < 3 { + return false + } + return elem[len(elem)-2] == "blobs" || (elem[len(elem)-3] == "blobs" && + elem[len(elem)-2] == "uploads") +} + +// blobs +type blobs struct { + // Blobs are content addresses. we store them globally underneath their sha and make no distinctions per image. + contents map[string][]byte + // Each upload gets a unique id that writes occur to until finalized. + uploads map[string][]byte + lock sync.Mutex +} + +func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + if elem[len(elem)-1] == "" { + elem = elem[:len(elem)-1] + } + // Must have a path of form /v2/{name}/blobs/{upload,sha256:} + if len(elem) < 4 { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "blobs must be attached to a repo", + } + } + target := elem[len(elem)-1] + service := elem[len(elem)-2] + digest := req.URL.Query().Get("digest") + contentRange := req.Header.Get("Content-Range") + + if req.Method == "HEAD" { + b.lock.Lock() + defer b.lock.Unlock() + b, ok := b.contents[target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "BLOB_UNKNOWN", + Message: "Unknown blob", + } + } + + resp.Header().Set("Content-Length", fmt.Sprint(len(b))) + resp.Header().Set("Docker-Content-Digest", target) + resp.WriteHeader(http.StatusOK) + return nil + } + + if req.Method == "GET" { + b.lock.Lock() + defer b.lock.Unlock() + b, ok := b.contents[target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "BLOB_UNKNOWN", + Message: "Unknown blob", + } + } + + resp.Header().Set("Content-Length", fmt.Sprint(len(b))) + resp.Header().Set("Docker-Content-Digest", target) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader(b)) + return nil + } + + if req.Method == "POST" && target == "uploads" && digest != "" { + l := &bytes.Buffer{} + io.Copy(l, req.Body) + rd := sha256.Sum256(l.Bytes()) + d := "sha256:" + hex.EncodeToString(rd[:]) + if d != digest { + return ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest does not match contents", + } + } + + b.lock.Lock() + defer b.lock.Unlock() + b.contents[d] = l.Bytes() + resp.Header().Set("Docker-Content-Digest", d) + resp.WriteHeader(http.StatusCreated) + return nil + } + + if req.Method == "POST" && target == "uploads" && digest == "" { + id := fmt.Sprint(rand.Int63()) + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id)) + resp.Header().Set("Range", "0-0") + resp.WriteHeader(http.StatusAccepted) + return nil + } + + if req.Method == "PATCH" && service == "uploads" && contentRange != "" { + start, end := 0, 0 + if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil { + return ®Error{ + Status: http.StatusRequestedRangeNotSatisfiable, + Code: "BLOB_UPLOAD_UNKNOWN", + Message: "We don't understand your Content-Range", + } + } + b.lock.Lock() + defer b.lock.Unlock() + if start != len(b.uploads[target]) { + return ®Error{ + Status: http.StatusRequestedRangeNotSatisfiable, + Code: "BLOB_UPLOAD_UNKNOWN", + Message: "Your content range doesn't match what we have", + } + } + l := bytes.NewBuffer(b.uploads[target]) + io.Copy(l, req.Body) + b.uploads[target] = l.Bytes() + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) + resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1)) + resp.WriteHeader(http.StatusNoContent) + return nil + } + + if req.Method == "PATCH" && service == "uploads" && contentRange == "" { + b.lock.Lock() + defer b.lock.Unlock() + if _, ok := b.uploads[target]; ok { + return ®Error{ + Status: http.StatusBadRequest, + Code: "BLOB_UPLOAD_INVALID", + Message: "Stream uploads after first write are not allowed", + } + } + + l := &bytes.Buffer{} + io.Copy(l, req.Body) + + b.uploads[target] = l.Bytes() + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) + resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1)) + resp.WriteHeader(http.StatusNoContent) + return nil + } + + if req.Method == "PUT" && service == "uploads" && digest == "" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest not specified", + } + } + + if req.Method == "PUT" && service == "uploads" && digest != "" { + b.lock.Lock() + defer b.lock.Unlock() + l := bytes.NewBuffer(b.uploads[target]) + io.Copy(l, req.Body) + rd := sha256.Sum256(l.Bytes()) + d := "sha256:" + hex.EncodeToString(rd[:]) + if d != digest { + return ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest does not match contents", + } + } + + b.contents[d] = l.Bytes() + delete(b.uploads, target) + resp.Header().Set("Docker-Content-Digest", d) + resp.WriteHeader(http.StatusCreated) + return nil + } + + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/error.go b/vendor/github.com/google/go-containerregistry/pkg/registry/error.go new file mode 100644 index 00000000000..64e98671c3e --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/error.go @@ -0,0 +1,46 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// 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 registry + +import ( + "encoding/json" + "net/http" +) + +type regError struct { + Status int + Code string + Message string +} + +func (r *regError) Write(resp http.ResponseWriter) error { + resp.WriteHeader(r.Status) + + type err struct { + Code string `json:"code"` + Message string `json:"message"` + } + type wrap struct { + Errors []err `json:"errors"` + } + return json.NewEncoder(resp).Encode(wrap{ + Errors: []err{ + { + Code: r.Code, + Message: r.Message, + }, + }, + }) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go b/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go new file mode 100644 index 00000000000..736b7fc90ba --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go @@ -0,0 +1,139 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// 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 registry + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "strings" + "sync" +) + +type manifest struct { + contentType string + blob []byte +} + +type manifests struct { + // maps repo -> manifest tag/digest -> manifest + manifests map[string]map[string]manifest + lock sync.Mutex +} + +func isManifest(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 4 { + return false + } + return elems[len(elems)-2] == "manifests" +} + +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image +func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + target := elem[len(elem)-1] + repo := strings.Join(elem[1:len(elem)-2], "/") + + if req.Method == "GET" { + m.lock.Lock() + defer m.lock.Unlock() + c, ok := m.manifests[repo] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + m, ok := c[target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + rd := sha256.Sum256(m.blob) + d := "sha256:" + hex.EncodeToString(rd[:]) + resp.Header().Set("Docker-Content-Digest", d) + resp.Header().Set("Content-Type", m.contentType) + resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader(m.blob)) + return nil + } + + if req.Method == "HEAD" { + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + m, ok := m.manifests[repo][target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + rd := sha256.Sum256(m.blob) + d := "sha256:" + hex.EncodeToString(rd[:]) + resp.Header().Set("Docker-Content-Digest", d) + resp.Header().Set("Content-Type", m.contentType) + resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob))) + resp.WriteHeader(http.StatusOK) + return nil + } + + if req.Method == "PUT" { + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + m.manifests[repo] = map[string]manifest{} + } + b := &bytes.Buffer{} + io.Copy(b, req.Body) + rd := sha256.Sum256(b.Bytes()) + digest := "sha256:" + hex.EncodeToString(rd[:]) + mf := manifest{ + blob: b.Bytes(), + contentType: req.Header.Get("Content-Type"), + } + // Allow future references by target (tag) and immutable digest. + // See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier. + m.manifests[repo][target] = mf + m.manifests[repo][digest] = mf + resp.Header().Set("Docker-Content-Digest", digest) + resp.WriteHeader(http.StatusCreated) + return nil + } + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go b/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go new file mode 100644 index 00000000000..1d76ee16990 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/registry.go @@ -0,0 +1,96 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// 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 registry implements a docker V2 registry and the OCI distribution specification. +// +// It is designed to be used anywhere a low dependency container registry is needed, with an +// initial focus on tests. +// +// Its goal is to be standards compliant and its strictness will increase over time. +// +// This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it +// in production, please let us know how and send us CL's for integration tests. +package registry + +import ( + "log" + "net/http" + "os" +) + +type registry struct { + log *log.Logger + blobs blobs + manifests manifests +} + +// https://docs.docker.com/registry/spec/api/#api-version-check +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#api-version-check +func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError { + if isBlob(req) { + return r.blobs.handle(resp, req) + } + if isManifest(req) { + return r.manifests.handle(resp, req) + } + resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0") + if req.URL.Path != "/v2/" && req.URL.Path != "/v2" { + return ®Error{ + Status: http.StatusNotFound, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } + resp.WriteHeader(200) + return nil +} + +func (r *registry) root(resp http.ResponseWriter, req *http.Request) { + if rerr := r.v2(resp, req); rerr != nil { + r.log.Printf("%s %s %d %s %s", req.Method, req.URL, rerr.Status, rerr.Code, rerr.Message) + rerr.Write(resp) + return + } + r.log.Printf("%s %s", req.Method, req.URL) +} + +// New returns a handler which implements the docker registry protocol. +// It should be registered at the site root. +func New(opts ...Option) http.Handler { + r := ®istry{ + log: log.New(os.Stderr, "", log.LstdFlags), + blobs: blobs{ + contents: map[string][]byte{}, + uploads: map[string][]byte{}, + }, + manifests: manifests{ + manifests: map[string]map[string]manifest{}, + }, + } + for _, o := range opts { + o(r) + } + return http.HandlerFunc(r.root) +} + +// Option describes the available options +// for creating the registry. +type Option func(r *registry) + +// Logger overrides the logger used to record requests to the registry. +func Logger(l *log.Logger) Option { + return func(r *registry) { + r.log = l + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go b/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go new file mode 100644 index 00000000000..70bcc33de29 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/registry/tls.go @@ -0,0 +1,104 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// 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 registry + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "math/big" + "net" + "net/http" + "net/http/httptest" + "time" +) + +// TLS returns an httptest server, with an http client that has been configured to +// send all requests to the returned server. The TLS certs are generated for the given domain +// which should correspond to the domain the image is stored in. +// If you need a transport, Client().Transport is correctly configured. +func TLS(domain string) (*httptest.Server, error) { + s := httptest.NewUnstartedServer(New()) + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(time.Hour), + IPAddresses: []net.IP{ + net.IPv4(127, 0, 0, 1), + net.IPv6loopback, + }, + DNSNames: []string{domain}, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return nil, err + } + + b, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, err + } + + pc := &bytes.Buffer{} + if err := pem.Encode(pc, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil { + return nil, err + } + + ek, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, err + } + + pk := &bytes.Buffer{} + if err := pem.Encode(pk, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ek}); err != nil { + return nil, err + } + + c, err := tls.X509KeyPair(pc.Bytes(), pk.Bytes()) + if err != nil { + return nil, err + } + s.TLS = &tls.Config{ + Certificates: []tls.Certificate{c}, + } + s.StartTLS() + + certpool := x509.NewCertPool() + certpool.AddCert(s.Certificate()) + + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certpool, + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial(s.Listener.Addr().Network(), s.Listener.Addr().String()) + }, + } + s.Client().Transport = t + + return s, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f7da4d2f10d..8dcfe5d4465 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -175,6 +175,7 @@ github.com/google/go-containerregistry/pkg/internal/retry github.com/google/go-containerregistry/pkg/internal/retry/wait github.com/google/go-containerregistry/pkg/logs github.com/google/go-containerregistry/pkg/name +github.com/google/go-containerregistry/pkg/registry github.com/google/go-containerregistry/pkg/v1 github.com/google/go-containerregistry/pkg/v1/empty github.com/google/go-containerregistry/pkg/v1/layout