From b8f7be9a21ab4d2045ee4402d5bf4c7c0b43e2a4 Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Wed, 7 Apr 2021 15:44:19 +0200 Subject: [PATCH] extend the oci client with list repositories and list tags functions --- .../components-cli_component-archive.md | 6 +- .../components-cli_image-vector_add.md | 7 +- docs/reference/components-cli_oci.md | 2 + .../components-cli_oci_repositories.md | 39 +++ docs/reference/components-cli_oci_tags.md | 39 +++ go.mod | 1 + go.sum | 2 + ociclient/client.go | 259 +++++++++++++++++- ociclient/client_test.go | 73 +++++ ociclient/credentials/keyring.go | 2 + ociclient/oci/oci_suite_test.go | 50 ++++ ociclient/oci/ref.go | 80 ++++++ ociclient/ociclient_suite_test.go | 17 ++ ociclient/options/options.go | 2 +- ociclient/types.go | 11 + pkg/commands/oci/oci.go | 2 + pkg/commands/oci/repositories.go | 83 ++++++ pkg/commands/oci/tags.go | 82 ++++++ .../onsi/ginkgo/extensions/table/table.go | 110 ++++++++ .../ginkgo/extensions/table/table_entry.go | 129 +++++++++ .../opencontainers/distribution-spec/LICENSE | 201 ++++++++++++++ .../distribution-spec/specs-go/v1/error.go | 40 +++ .../specs-go/v1/repository.go | 20 ++ .../distribution-spec/specs-go/v1/tags.go | 21 ++ vendor/modules.txt | 4 + 25 files changed, 1263 insertions(+), 19 deletions(-) create mode 100644 docs/reference/components-cli_oci_repositories.md create mode 100644 docs/reference/components-cli_oci_tags.md create mode 100644 ociclient/client_test.go create mode 100644 ociclient/oci/oci_suite_test.go create mode 100644 ociclient/oci/ref.go create mode 100644 ociclient/ociclient_suite_test.go create mode 100644 pkg/commands/oci/repositories.go create mode 100644 pkg/commands/oci/tags.go create mode 100644 vendor/github.com/onsi/ginkgo/extensions/table/table.go create mode 100644 vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go create mode 100644 vendor/github.com/opencontainers/distribution-spec/LICENSE create mode 100644 vendor/github.com/opencontainers/distribution-spec/specs-go/v1/error.go create mode 100644 vendor/github.com/opencontainers/distribution-spec/specs-go/v1/repository.go create mode 100644 vendor/github.com/opencontainers/distribution-spec/specs-go/v1/tags.go diff --git a/docs/reference/components-cli_component-archive.md b/docs/reference/components-cli_component-archive.md index 12e68afc..4d34abcf 100644 --- a/docs/reference/components-cli_component-archive.md +++ b/docs/reference/components-cli_component-archive.md @@ -3,7 +3,7 @@ ``` -components-cli component-archive component-archive-path [ctf-path] [flags] +components-cli component-archive [component-archive-path] [ctf-path] [flags] ``` ### Options @@ -11,13 +11,13 @@ components-cli component-archive component-archive-path [ctf-path] [flags] ``` -a, --archive string path to the component archive directory --component-name string name of the component - -c, --component-ref stringArray path to resources definition + -c, --component-ref stringArray path to component references definition --component-version string version of the component --format CAOutputFormat archive format of the component archive. Can be "tar" or "tgz" (default tar) -h, --help help for component-archive --repo-ctx string [OPTIONAL] repository context url for component to upload. The repository url will be automatically added to the repository contexts. -r, --resources stringArray path to resources definition - -s, --sources stringArray path to resources definition + -s, --sources stringArray path to sources definition --temp-dir string temporary directory where the component archive is build. Defaults to a os-specific temp dir ``` diff --git a/docs/reference/components-cli_image-vector_add.md b/docs/reference/components-cli_image-vector_add.md index c5580de9..74307f9f 100644 --- a/docs/reference/components-cli_image-vector_add.md +++ b/docs/reference/components-cli_image-vector_add.md @@ -43,7 +43,7 @@ resources: 2. The image is defined by another component so the image is added as label ("imagevector.gardener.cloud/images") to the "componentReference". Images that are defined by other components can be specified -1. [DEPRECATED] when the image's repository matches the given "--component-prefixes" +1. when the image's repository matches the given "--component-prefixes" 2. the image is labeled with "imagevector.gardener.cloud/component-reference" If the component reference is not yet defined it will be automatically added. @@ -86,10 +86,10 @@ componentReferences: 3. The image is a generic dependency where the actual images are defined by the overwrite. -A generic dependency image is not part of a component descriptors resource but will be added as label ("imagevector.gardener.cloud/images") to the component descriptor. +A generic dependency image is not part of a component descriptor's resource but will be added as label ("imagevector.gardener.cloud/images") to the component descriptor. Generic dependencies can be defined by -1. [DEPRECATED] defined as "--generic-dependency=" +1. defined as "--generic-dependency=" 2. the label "imagevector.gardener.cloud/generic"
@@ -113,7 +113,6 @@ component:
 	  - name: hyperkube
 	    repository: k8s.gcr.io/hyperkube
 	    sourceRepository: github.com/kubernetes/kubernetes
-	    tag: v0.10.0
 	    targetVersion: '< 1.19'
 
diff --git a/docs/reference/components-cli_oci.md b/docs/reference/components-cli_oci.md index 79b26cb8..b605db4e 100644 --- a/docs/reference/components-cli_oci.md +++ b/docs/reference/components-cli_oci.md @@ -23,4 +23,6 @@ * [components-cli](components-cli.md) - components cli * [components-cli oci pull](components-cli_oci_pull.md) - Pulls a oci artifact from a registry +* [components-cli oci repositories](components-cli_oci_repositories.md) - Lists all repositories of the registry +* [components-cli oci tags](components-cli_oci_tags.md) - Lists all tags of artifact reference diff --git a/docs/reference/components-cli_oci_repositories.md b/docs/reference/components-cli_oci_repositories.md new file mode 100644 index 00000000..dd868d6c --- /dev/null +++ b/docs/reference/components-cli_oci_repositories.md @@ -0,0 +1,39 @@ +## components-cli oci repositories + +Lists all repositories of the registry + +### Synopsis + + +repositories lists all known repositories of the registry. + + + +``` +components-cli oci repositories [registry host] [flags] +``` + +### Options + +``` + --allow-plain-http allows the fallback to http if the oci registry does not support https + --cc-config string path to the local concourse config file + -h, --help help for repositories + --registry-config string path to the dockerconfig.json with the oci registry authentication information +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [components-cli oci](components-cli_oci.md) - + diff --git a/docs/reference/components-cli_oci_tags.md b/docs/reference/components-cli_oci_tags.md new file mode 100644 index 00000000..7478f5bf --- /dev/null +++ b/docs/reference/components-cli_oci_tags.md @@ -0,0 +1,39 @@ +## components-cli oci tags + +Lists all tags of artifact reference + +### Synopsis + + +tags lists all tags for a specific artifact reference that is known by the registry. + + + +``` +components-cli oci tags [artifact reference] [flags] +``` + +### Options + +``` + --allow-plain-http allows the fallback to http if the oci registry does not support https + --cc-config string path to the local concourse config file + -h, --help help for tags + --registry-config string path to the dockerconfig.json with the oci registry authentication information +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [components-cli oci](components-cli_oci.md) - + diff --git a/go.mod b/go.mod index 23288222..e0e28bee 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/mandelsoft/vfs v0.0.0-20201002134249-3c471f64a4d1 github.com/onsi/ginkgo v1.14.0 github.com/onsi/gomega v1.10.1 + github.com/opencontainers/distribution-spec v1.0.0-rc1 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.1 github.com/prometheus/client_golang v0.9.3 diff --git a/go.sum b/go.sum index de3ee496..0d2b7c59 100644 --- a/go.sum +++ b/go.sum @@ -244,6 +244,8 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/opencontainers/distribution-spec v1.0.0-rc1 h1:nuQ2aIFPy01fNMoeoY7TCedORpU9Wmm/nkMiGErgrmY= +github.com/opencontainers/distribution-spec v1.0.0-rc1/go.mod h1:copR2flp+jTEvQIFMb6MIx45OkrxzqyjszPDT3hx/5Q= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= diff --git a/ociclient/client.go b/ociclient/client.go index cb5e694e..3c7d41ca 100644 --- a/ociclient/client.go +++ b/ociclient/client.go @@ -13,12 +13,17 @@ import ( "io" "io/ioutil" "net/http" + "net/url" + "path" + "strings" "github.com/containerd/containerd/content" "github.com/containerd/containerd/errdefs" containerdlog "github.com/containerd/containerd/log" "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" "github.com/go-logr/logr" + distributionspecv1 "github.com/opencontainers/distribution-spec/specs-go/v1" "github.com/opencontainers/go-digest" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" @@ -26,14 +31,16 @@ import ( "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/credentials" + "github.com/gardener/component-cli/ociclient/oci" ) type client struct { - log logr.Logger - resolver Resolver - cache cache.Cache - allowPlainHttp bool - httpClient *http.Client + log logr.Logger + resolver Resolver + cache cache.Cache + allowPlainHttp bool + httpClient *http.Client + defaultRegistryHosts docker.RegistryHosts knownMediaTypes sets.String } @@ -46,7 +53,7 @@ func (f ResolverWrapperFunc) Resolver(ctx context.Context, ref string, client *h } // NewClient creates a new OCI Client. -func NewClient(log logr.Logger, opts ...Option) (Client, error) { +func NewClient(log logr.Logger, opts ...Option) (*client, error) { options := &Options{} options.ApplyOptions(opts) @@ -79,6 +86,10 @@ func NewClient(log logr.Logger, opts ...Option) (Client, error) { options.HTTPClient = http.DefaultClient } + authorizer := docker.NewDockerAuthorizer( + docker.WithAuthClient(options.HTTPClient), + docker.WithAuthCreds(options.Resolver.GetCredentials)) + cLogger := logrus.New() if log.V(5).Enabled() { cLogger.SetLevel(logrus.DebugLevel) @@ -89,11 +100,18 @@ func NewClient(log logr.Logger, opts ...Option) (Client, error) { containerdlog.L = logrus.NewEntry(cLogger) return &client{ - log: log, - allowPlainHttp: options.AllowPlainHttp, - httpClient: options.HTTPClient, - resolver: options.Resolver, - cache: options.Cache, + log: log, + allowPlainHttp: options.AllowPlainHttp, + httpClient: options.HTTPClient, + resolver: options.Resolver, + cache: options.Cache, + defaultRegistryHosts: docker.ConfigureDefaultRegistries( + docker.WithPlainHTTP(func(_ string) (bool, error) { + return options.AllowPlainHttp, nil + }), + docker.WithAuthorizer(authorizer), + docker.WithClient(options.HTTPClient), + ), knownMediaTypes: DefaultKnownMediaTypes.Union(options.CustomMediaTypes), }, nil } @@ -221,6 +239,225 @@ func (c *client) PushManifest(ctx context.Context, ref string, manifest *ocispec return nil } +// ListTags lists all tags for a given ref. +// Implements the distribution spec defined in https://github.com/opencontainers/distribution-spec/blob/main/spec.md#api. +// todo: do paging +func (c *client) ListTags(ctx context.Context, ref string) ([]string, error) { + refspec, err := oci.ParseRef(ref) + if err != nil { + return nil, fmt.Errorf("unable to parse reference: %w", err) + } + hosts, err := c.defaultRegistryHosts(refspec.Host) + if err != nil { + return nil, fmt.Errorf("unable to find registry host: %w", err) + } + if len(hosts) == 0 { + return nil, fmt.Errorf("no host configuration found: %w", err) + } + hostConfig := hosts[0] + hostConfig.Authorizer = c.getAuthorizerForRef(ref) + + u := &url.URL{ + Scheme: hostConfig.Scheme, + Host: hostConfig.Host, + Path: path.Join(hostConfig.Path, refspec.Repository, "tags", "list"), + // ECR returns an error if n > 1000: + // https://github.com/google/go-containerregistry/issues/681 + RawQuery: "n=1000", + } + + var tags []string + err = doWithPaging(ctx, u, func(ctx context.Context, u *url.URL) (*http.Response, error) { + resp, err := c.doRequest(ctx, hostConfig, u, "") + if err != nil { + return nil, err + } + + var data bytes.Buffer + if _, err := io.Copy(&data, resp.Body); err != nil { + return nil, fmt.Errorf("unable to read response body: %w", err) + } + if err := resp.Body.Close(); err != nil { + return nil, fmt.Errorf("unbale to close body reader: %w", err) + } + + tagList := &distributionspecv1.TagList{} + if err := json.Unmarshal(data.Bytes(), tagList); err != nil { + return nil, fmt.Errorf("unable to decode tagList list: %w", err) + } + tags = append(tags, tagList.Tags...) + return resp, nil + }) + if err != nil { + return nil, err + } + return tags, nil +} + +// ListRepositories lists all repositories for the given registry host. +func (c *client) ListRepositories(ctx context.Context, ref string) ([]string, error) { + // parse registry to also support more specific credentials e.g. for gcr with gcr.io/my-project + refspec, err := oci.ParseRef(ref) + if err != nil { + return nil, fmt.Errorf("unable to parse reference: %w", err) + } + + hosts, err := c.defaultRegistryHosts(refspec.Host) + if err != nil { + return nil, fmt.Errorf("unable to find registry host: %w", err) + } + if len(hosts) == 0 { + return nil, fmt.Errorf("no host configuration found: %w", err) + } + hostConfig := hosts[0] + hostConfig.Authorizer = c.getAuthorizerForRef(ref) + + u := &url.URL{ + Scheme: hostConfig.Scheme, + Host: hostConfig.Host, + Path: path.Join(hostConfig.Path, "_catalog"), + // ECR returns an error if n > 1000: + // https://github.com/google/go-containerregistry/issues/681 + RawQuery: "n=1000", + } + + repositories := make([]string, 0) + err = doWithPaging(ctx, u, func(ctx context.Context, u *url.URL) (*http.Response, error) { + resp, err := c.doRequest(ctx, hostConfig, u, "registry:catalog:*") + if err != nil { + return nil, err + } + + var data bytes.Buffer + if _, err := io.Copy(&data, resp.Body); err != nil { + return nil, fmt.Errorf("unable to read response body: %w", err) + } + if err := resp.Body.Close(); err != nil { + return nil, fmt.Errorf("unbale to close body reader: %w", err) + } + + repositoryList := &distributionspecv1.RepositoryList{} + if err := json.Unmarshal(data.Bytes(), repositoryList); err != nil { + return nil, fmt.Errorf("unable to decode repository list: %w", err) + } + + // the registry by default returns all repositories + // lets filter the results if a repository path is provided + if len(refspec.Repository) != 0 { + name := refspec.Name() + prefix := refspec.Repository + for _, repo := range repositoryList.Repositories { + if strings.HasPrefix(repo, prefix) || strings.HasPrefix(repo, name) { + repositories = append(repositories, repo) + } + } + return resp, nil + } + repositories = append(repositories, repositoryList.Repositories...) + return resp, nil + }) + if err != nil { + return nil, err + } + return repositories, nil +} + +// doRequest does a authenticated request to the given oci registry +func (c *client) doRequest(ctx context.Context, registry docker.RegistryHost, url *url.URL, defaultScope string) (*http.Response, error) { + req := &http.Request{ + Method: http.MethodGet, + URL: url, + Header: make(http.Header), + } + if err := registry.Authorizer.Authorize(ctx, req); err != nil { + return nil, fmt.Errorf("unable to authorize call: %w", err) + } + resp, err := registry.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("unable to get %q: %w", url.String(), err) + } + + if resp.StatusCode == http.StatusUnauthorized { + if len(defaultScope) != 0 { + // inject default scope if not requested by registry + authHeader := resp.Header.Get("WWW-Authenticate") + if len(authHeader) != 0 && !strings.Contains(authHeader, "scope") { + resp.Header.Set("WWW-Authenticate", + fmt.Sprintf("%s,scope=%q", authHeader, defaultScope)) + } + } + // do authorization if 401 is returned and retry the request + if err := registry.Authorizer.AddResponses(ctx, []*http.Response{resp}); err != nil { + return nil, fmt.Errorf("unable to authorize call: %w", err) + } + if err := registry.Authorizer.Authorize(ctx, req); err != nil { + return nil, fmt.Errorf("unable to authorize call: %w", err) + } + resp, err = registry.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("unable to get %q: %w", url.String(), err) + } + } + if resp.StatusCode != 200 { + var data bytes.Buffer + if _, err := io.Copy(&data, resp.Body); err != nil { + return nil, fmt.Errorf("unable to read response body: %w", err) + } + if err := resp.Body.Close(); err != nil { + return nil, fmt.Errorf("unbale to close body reader: %w", err) + } + // read error response + errRes := &distributionspecv1.ErrorResponse{} + if err := json.Unmarshal(data.Bytes(), errRes); err != nil { + return nil, fmt.Errorf("unable to decode error response: %w", err) + } + errMsg := "" + for _, err := range errRes.Detail() { + errMsg = errMsg + fmt.Sprintf("; Code: %q, Message: %q, Detail: %q", err.Code, err.Message, err.Detail) + } + return nil, fmt.Errorf("error during list call to registry with status code %d: %v", resp.StatusCode, errMsg) + } + return resp, nil +} + +type pagingFunc func(ctx context.Context, url *url.URL) (*http.Response, error) + +func doWithPaging(ctx context.Context, u *url.URL, pFunc pagingFunc) error { + nextUrl := u + for { + resp, err := pFunc(ctx, nextUrl) + if err != nil { + return err + } + + // parse next url + link := resp.Header.Get("Link") + if len(link) == 0 { + return nil + } + splitLink := strings.Split(link, ";") + next := strings.NewReplacer(">", "", "<", "").Replace(splitLink[0]) + nextUrl, err = url.Parse(next) + if err != nil { + return fmt.Errorf("unable to parse next url %q: %w", next, err) + } + } +} + +func (c *client) getAuthorizerForRef(ref string) docker.Authorizer { + u, p, err := c.resolver.GetCredentials(ref) + if err != nil { + return docker.NewDockerAuthorizer( + docker.WithAuthClient(c.httpClient), + docker.WithAuthCreds(c.resolver.GetCredentials)) + } + return docker.NewDockerAuthorizer( + docker.WithAuthClient(c.httpClient), + docker.WithAuthCreds(func(s string) (string, string, error) { + return u, p, nil + })) +} + func (c *client) createDescriptorFromManifest(manifest *ocispecv1.Manifest) (ocispecv1.Descriptor, error) { manifestBytes, err := json.Marshal(manifest) if err != nil { diff --git a/ociclient/client_test.go b/ociclient/client_test.go new file mode 100644 index 00000000..80245dc7 --- /dev/null +++ b/ociclient/client_test.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package ociclient_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + + testlog "github.com/go-logr/logr/testing" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/ociclient" +) + +var _ = Describe("client", func() { + + Context("List", func() { + + var ( + server *httptest.Server + host string + handler func(http.ResponseWriter, *http.Request) + makeRef = func(repo string) string { + return fmt.Sprintf("%s/%s", host, repo) + } + ) + + BeforeEach(func() { + server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + handler(writer, request) + })) + + hostUrl, err := url.Parse(server.URL) + Expect(err).ToNot(HaveOccurred()) + host = hostUrl.Host + }) + + AfterEach(func() { + server.Close() + }) + + It("", func() { + var ( + ctx = context.Background() + repository = "myproject/repo/myimage" + ) + defer ctx.Done() + handler = func(w http.ResponseWriter, req *http.Request) { + Expect(req.URL.String()).To(Equal("/v2/myproject/repo/myimage/tags/list?n=1000")) + w.WriteHeader(200) + _, _ = w.Write([]byte(` +{ + "tags": [ "0.0.1", "0.0.2" ] +} +`)) + } + + client, err := ociclient.NewClient(testlog.NullLogger{}, ociclient.AllowPlainHttp(true)) + Expect(err).ToNot(HaveOccurred()) + tags, err := client.ListTags(ctx, makeRef(repository)) + Expect(err).ToNot(HaveOccurred()) + Expect(tags).To(ConsistOf("0.0.1", "0.0.2")) + }) + + }) + +}) diff --git a/ociclient/credentials/keyring.go b/ociclient/credentials/keyring.go index 3deae37e..acc73cc8 100644 --- a/ociclient/credentials/keyring.go +++ b/ociclient/credentials/keyring.go @@ -31,6 +31,8 @@ type OCIKeyring interface { Get(resourceURl string) (dockerconfigtypes.AuthConfig, bool) // Resolver returns a new authenticated resolver. Resolver(ctx context.Context, ref string, client *http.Client, plainHTTP bool) (remotes.Resolver, error) + // GetCredentials returns the username and password for a hostname if defined. + GetCredentials(hostname string) (username, password string, err error) } // AuthConfigGetter is a function that returns a auth config for a given host name diff --git a/ociclient/oci/oci_suite_test.go b/ociclient/oci/oci_suite_test.go new file mode 100644 index 00000000..972b3310 --- /dev/null +++ b/ociclient/oci/oci_suite_test.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package oci_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + "github.com/gardener/component-cli/ociclient/oci" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "oci Test Suite") +} + +var _ = Describe("ref", func() { + + DescribeTable("parse oci references", + func(ref, host, repository, tag, digest string) { + parsed, err := oci.ParseRef(ref) + Expect(err).ToNot(HaveOccurred()) + Expect(parsed.Host).To(Equal(host)) + Expect(parsed.Repository).To(Equal(repository)) + if len(tag) == 0 { + Expect(parsed.Tag).To(BeNil()) + } else { + Expect(parsed.Tag).To(PointTo(Equal(tag))) + } + if len(digest) == 0 { + Expect(parsed.Digest).To(BeNil()) + } else { + Expect(parsed.Digest.String()).To(Equal(digest)) + } + }, + Entry("default tagged image", "example.com/test:0.0.1", "example.com", "test", "0.0.1", ""), + Entry("default image with digest", "example.com/test@sha256:77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182", "example.com", "test", "", "sha256:77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182"), + Entry("docker image", "test:0.0.1", "index.docker.io", "library/test", "0.0.1", ""), + Entry("without version", "example.com/test", "example.com", "test", "latest", ""), + Entry("docker image without version", "test", "index.docker.io", "library/test", "latest", ""), + Entry("with protocol", "https://example.com/test:0.0.1", "example.com", "test", "0.0.1", ""), + ) + +}) diff --git a/ociclient/oci/ref.go b/ociclient/oci/ref.go new file mode 100644 index 00000000..332122cb --- /dev/null +++ b/ociclient/oci/ref.go @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package oci + +import ( + "fmt" + "strings" + + dockerreference "github.com/containerd/containerd/reference/docker" + "github.com/opencontainers/go-digest" +) + +// to find a suitable secret for images on Docker Hub, we need its two domains to do matching +const ( + dockerHubDomain = "docker.io" + dockerHubLegacyDomain = "index.docker.io" +) + +// ParseRef parses a oci refernce into a internal representation. +func ParseRef(ref string) (RefSpec, error) { + if strings.Contains(ref, "://") { + // remove protocol if exists + i := strings.Index(ref, "://") + 3 + ref = ref[i:] + } + + parsedRef, err := dockerreference.ParseDockerRef(ref) + if err != nil { + return RefSpec{}, err + } + + spec := RefSpec{ + Host: dockerreference.Domain(parsedRef), + Repository: dockerreference.Path(parsedRef), + } + + switch r := parsedRef.(type) { + case dockerreference.Tagged: + tag := r.Tag() + spec.Tag = &tag + case dockerreference.Digested: + d := r.Digest() + spec.Digest = &d + } + + // fallback to legacy docker domain if applicable + // this is how containerd translates the old domain for DockerHub to the new one, taken from containerd/reference/docker/reference.go:674 + if spec.Host == dockerHubDomain { + spec.Host = dockerHubLegacyDomain + } + return spec, nil +} + +// RefSpec is a go internal representation of a oci reference. +type RefSpec struct { + // Host is the hostname of a oci ref. + Host string + // Repository is the part of a reference without its hostname + Repository string + // +optional + Tag *string + // +optional + Digest *digest.Digest +} + +func (r *RefSpec) Name() string { + return fmt.Sprintf("%s/%s", r.Host, r.Repository) +} + +func (r RefSpec) String() string { + if r.Tag != nil { + return fmt.Sprintf("%s:%s", r.Name(), *r.Tag) + } + if r.Digest != nil { + return fmt.Sprintf("%s@%s", r.Name(), r.Digest.String()) + } + return "" +} diff --git a/ociclient/ociclient_suite_test.go b/ociclient/ociclient_suite_test.go new file mode 100644 index 00000000..92e0042d --- /dev/null +++ b/ociclient/ociclient_suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package ociclient_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ociclient Test Suite") +} diff --git a/ociclient/options/options.go b/ociclient/options/options.go index 9371d3c1..29935fc6 100644 --- a/ociclient/options/options.go +++ b/ociclient/options/options.go @@ -41,7 +41,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { } // Builds a new oci client based on the given options -func (o *Options) Build(log logr.Logger, fs vfs.FileSystem) (ociclient.Client, cache.Cache, error) { +func (o *Options) Build(log logr.Logger, fs vfs.FileSystem) (ociclient.ExtendedClient, cache.Cache, error) { cache, err := cache.NewCache(log, cache.WithBasePath(o.CacheDir)) if err != nil { return nil, nil, err diff --git a/ociclient/types.go b/ociclient/types.go index 79c9cbeb..e077e9bb 100644 --- a/ociclient/types.go +++ b/ociclient/types.go @@ -28,10 +28,21 @@ type Client interface { PushManifest(ctx context.Context, ref string, manifest *ocispecv1.Manifest) error } +// ExtendedClient defines an oci client with extended functionality that may not work with all registries. +type ExtendedClient interface { + Client + // ListTags returns a list of all tags of the given ref. + ListTags(ctx context.Context, ref string) ([]string, error) + // ListRepositories lists all repositories for the given registry host. + ListRepositories(ctx context.Context, registryHost string) ([]string, error) +} + // Resolver is a interface that should return a new resolver for a given ref if called. type Resolver interface { // Resolver returns a new authenticated resolver. Resolver(ctx context.Context, ref string, client *http.Client, plainHTTP bool) (remotes.Resolver, error) + // GetCredentials returns the username and password for a hostname if defined. + GetCredentials(hostname string) (username, password string, err error) } // Options contains all client options to configure the oci client. diff --git a/pkg/commands/oci/oci.go b/pkg/commands/oci/oci.go index 05be6a10..19f5b329 100644 --- a/pkg/commands/oci/oci.go +++ b/pkg/commands/oci/oci.go @@ -16,5 +16,7 @@ func NewOCICommand(ctx context.Context) *cobra.Command { Use: "oci", } cmd.AddCommand(NewPullCommand(ctx)) + cmd.AddCommand(NewTagsCommand(ctx)) + cmd.AddCommand(NewRepositoriesCommand(ctx)) return cmd } diff --git a/pkg/commands/oci/repositories.go b/pkg/commands/oci/repositories.go new file mode 100644 index 00000000..bdf532d3 --- /dev/null +++ b/pkg/commands/oci/repositories.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package oci + +import ( + "context" + "fmt" + "os" + + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + ociopts "github.com/gardener/component-cli/ociclient/options" + "github.com/gardener/component-cli/pkg/logger" +) + +type RepositoriesOptions struct { + // Registry is url of the host. + Registry string + + // OCIOptions contains all oci client related options. + OCIOptions ociopts.Options +} + +func NewRepositoriesCommand(ctx context.Context) *cobra.Command { + opts := &RepositoriesOptions{} + cmd := &cobra.Command{ + Use: "repositories [registry host]", + Aliases: []string{"repos", "repo"}, + Args: cobra.RangeArgs(1, 2), + Short: "Lists all repositories of the registry", + Long: ` +repositories lists all known repositories of the registry. + +`, + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + opts.AddFlags(cmd.Flags()) + return cmd +} + +func (o *RepositoriesOptions) AddFlags(fs *pflag.FlagSet) { + o.OCIOptions.AddFlags(fs) +} + +func (o *RepositoriesOptions) Complete(args []string) error { + if len(args) == 0 { + return fmt.Errorf("at least one argument that defines the reference is needed") + } + o.Registry = args[0] + return nil +} + +func (o *RepositoriesOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + ociClient, _, err := o.OCIOptions.Build(log, fs) + if err != nil { + return fmt.Errorf("unable to build oci client: %s", err.Error()) + } + + repos, err := ociClient.ListRepositories(ctx, o.Registry) + if err != nil { + return err + } + for _, repo := range repos { + fmt.Println(repo) + } + return nil +} diff --git a/pkg/commands/oci/tags.go b/pkg/commands/oci/tags.go new file mode 100644 index 00000000..691b9c42 --- /dev/null +++ b/pkg/commands/oci/tags.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package oci + +import ( + "context" + "fmt" + "os" + + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + ociopts "github.com/gardener/component-cli/ociclient/options" + "github.com/gardener/component-cli/pkg/logger" +) + +type TagsOptions struct { + // Ref is the oci artifact reference. + Ref string + + // OCIOptions contains all oci client related options. + OCIOptions ociopts.Options +} + +func NewTagsCommand(ctx context.Context) *cobra.Command { + opts := &TagsOptions{} + cmd := &cobra.Command{ + Use: "tags [artifact reference]", + Args: cobra.RangeArgs(1, 2), + Short: "Lists all tags of artifact reference", + Long: ` +tags lists all tags for a specific artifact reference that is known by the registry. + +`, + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + opts.AddFlags(cmd.Flags()) + return cmd +} + +func (o *TagsOptions) AddFlags(fs *pflag.FlagSet) { + o.OCIOptions.AddFlags(fs) +} + +func (o *TagsOptions) Complete(args []string) error { + if len(args) == 0 { + return fmt.Errorf("at least one argument that defines the reference is needed") + } + o.Ref = args[0] + return nil +} + +func (o *TagsOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + ociClient, _, err := o.OCIOptions.Build(log, fs) + if err != nil { + return fmt.Errorf("unable to build oci client: %s", err.Error()) + } + + tags, err := ociClient.ListTags(ctx, o.Ref) + if err != nil { + return err + } + for _, tag := range tags { + fmt.Println(tag) + } + return nil +} diff --git a/vendor/github.com/onsi/ginkgo/extensions/table/table.go b/vendor/github.com/onsi/ginkgo/extensions/table/table.go new file mode 100644 index 00000000..4b002780 --- /dev/null +++ b/vendor/github.com/onsi/ginkgo/extensions/table/table.go @@ -0,0 +1,110 @@ +/* + +Table provides a simple DSL for Ginkgo-native Table-Driven Tests + +The godoc documentation describes Table's API. More comprehensive documentation (with examples!) is available at http://onsi.github.io/ginkgo#table-driven-tests + +*/ + +package table + +import ( + "fmt" + "reflect" + + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal/global" + "github.com/onsi/ginkgo/types" +) + +/* +DescribeTable describes a table-driven test. + +For example: + + DescribeTable("a simple table", + func(x int, y int, expected bool) { + Ω(x > y).Should(Equal(expected)) + }, + Entry("x > y", 1, 0, true), + Entry("x == y", 0, 0, false), + Entry("x < y", 0, 1, false), + ) + +The first argument to `DescribeTable` is a string description. +The second argument is a function that will be run for each table entry. Your assertions go here - the function is equivalent to a Ginkgo It. +The subsequent arguments must be of type `TableEntry`. We recommend using the `Entry` convenience constructors. + +The `Entry` constructor takes a string description followed by an arbitrary set of parameters. These parameters are passed into your function. + +Under the hood, `DescribeTable` simply generates a new Ginkgo `Describe`. Each `Entry` is turned into an `It` within the `Describe`. + +It's important to understand that the `Describe`s and `It`s are generated at evaluation time (i.e. when Ginkgo constructs the tree of tests and before the tests run). + +Individual Entries can be focused (with FEntry) or marked pending (with PEntry or XEntry). In addition, the entire table can be focused or marked pending with FDescribeTable and PDescribeTable/XDescribeTable. + +A description function can be passed to Entry in place of the description. The function is then fed with the entry parameters to generate the description of the It corresponding to that particular Entry. + +For example: + + describe := func(desc string) func(int, int, bool) string { + return func(x, y int, expected bool) string { + return fmt.Sprintf("%s x=%d y=%d expected:%t", desc, x, y, expected) + } + } + + DescribeTable("a simple table", + func(x int, y int, expected bool) { + Ω(x > y).Should(Equal(expected)) + }, + Entry(describe("x > y"), 1, 0, true), + Entry(describe("x == y"), 0, 0, false), + Entry(describe("x < y"), 0, 1, false), + ) +*/ +func DescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, types.FlagTypeNone) + return true +} + +/* +You can focus a table with `FDescribeTable`. This is equivalent to `FDescribe`. +*/ +func FDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, types.FlagTypeFocused) + return true +} + +/* +You can mark a table as pending with `PDescribeTable`. This is equivalent to `PDescribe`. +*/ +func PDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, types.FlagTypePending) + return true +} + +/* +You can mark a table as pending with `XDescribeTable`. This is equivalent to `XDescribe`. +*/ +func XDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, types.FlagTypePending) + return true +} + +func describeTable(description string, itBody interface{}, entries []TableEntry, flag types.FlagType) { + itBodyValue := reflect.ValueOf(itBody) + if itBodyValue.Kind() != reflect.Func { + panic(fmt.Sprintf("DescribeTable expects a function, got %#v", itBody)) + } + + global.Suite.PushContainerNode( + description, + func() { + for _, entry := range entries { + entry.generateIt(itBodyValue) + } + }, + flag, + codelocation.New(2), + ) +} diff --git a/vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go b/vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go new file mode 100644 index 00000000..4d9c237a --- /dev/null +++ b/vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go @@ -0,0 +1,129 @@ +package table + +import ( + "fmt" + "reflect" + + "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal/global" + "github.com/onsi/ginkgo/types" +) + +/* +TableEntry represents an entry in a table test. You generally use the `Entry` constructor. +*/ +type TableEntry struct { + Description interface{} + Parameters []interface{} + Pending bool + Focused bool + codeLocation types.CodeLocation +} + +func (t TableEntry) generateIt(itBody reflect.Value) { + var description string + descriptionValue := reflect.ValueOf(t.Description) + switch descriptionValue.Kind() { + case reflect.String: + description = descriptionValue.String() + case reflect.Func: + values := castParameters(descriptionValue, t.Parameters) + res := descriptionValue.Call(values) + if len(res) != 1 { + panic(fmt.Sprintf("The describe function should return only a value, returned %d", len(res))) + } + if res[0].Kind() != reflect.String { + panic(fmt.Sprintf("The describe function should return a string, returned %#v", res[0])) + } + description = res[0].String() + default: + panic(fmt.Sprintf("Description can either be a string or a function, got %#v", descriptionValue)) + } + + if t.Pending { + global.Suite.PushItNode(description, func() {}, types.FlagTypePending, t.codeLocation, 0) + return + } + + values := castParameters(itBody, t.Parameters) + body := func() { + itBody.Call(values) + } + + if t.Focused { + global.Suite.PushItNode(description, body, types.FlagTypeFocused, t.codeLocation, global.DefaultTimeout) + } else { + global.Suite.PushItNode(description, body, types.FlagTypeNone, t.codeLocation, global.DefaultTimeout) + } +} + +func castParameters(function reflect.Value, parameters []interface{}) []reflect.Value { + res := make([]reflect.Value, len(parameters)) + funcType := function.Type() + for i, param := range parameters { + if param == nil { + inType := funcType.In(i) + res[i] = reflect.Zero(inType) + } else { + res[i] = reflect.ValueOf(param) + } + } + return res +} + +/* +Entry constructs a TableEntry. + +The first argument is a required description (this becomes the content of the generated Ginkgo `It`). +Subsequent parameters are saved off and sent to the callback passed in to `DescribeTable`. + +Each Entry ends up generating an individual Ginkgo It. +*/ +func Entry(description interface{}, parameters ...interface{}) TableEntry { + return TableEntry{ + Description: description, + Parameters: parameters, + Pending: false, + Focused: false, + codeLocation: codelocation.New(1), + } +} + +/* +You can focus a particular entry with FEntry. This is equivalent to FIt. +*/ +func FEntry(description interface{}, parameters ...interface{}) TableEntry { + return TableEntry{ + Description: description, + Parameters: parameters, + Pending: false, + Focused: true, + codeLocation: codelocation.New(1), + } +} + +/* +You can mark a particular entry as pending with PEntry. This is equivalent to PIt. +*/ +func PEntry(description interface{}, parameters ...interface{}) TableEntry { + return TableEntry{ + Description: description, + Parameters: parameters, + Pending: true, + Focused: false, + codeLocation: codelocation.New(1), + } +} + +/* +You can mark a particular entry as pending with XEntry. This is equivalent to XIt. +*/ +func XEntry(description interface{}, parameters ...interface{}) TableEntry { + return TableEntry{ + Description: description, + Parameters: parameters, + Pending: true, + Focused: false, + codeLocation: codelocation.New(1), + } +} diff --git a/vendor/github.com/opencontainers/distribution-spec/LICENSE b/vendor/github.com/opencontainers/distribution-spec/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/vendor/github.com/opencontainers/distribution-spec/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/error.go b/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/error.go new file mode 100644 index 00000000..2be9162f --- /dev/null +++ b/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/error.go @@ -0,0 +1,40 @@ +// Copyright 2019 The Linux Foundation +// +// 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 v1 + +// ErrorResponse is returned by a registry on an invalid request. +type ErrorResponse struct { + Errors []ErrorInfo `json:"errors"` +} + +// ErrRegistry is the string returned by and ErrorResponse error. +var ErrRegistry = "distribution: registry returned error" + +// Error implements the Error interface. +func (er *ErrorResponse) Error() string { + return ErrRegistry +} + +// Detail returns an ErrorInfo +func (er *ErrorResponse) Detail() []ErrorInfo { + return er.Errors +} + +// ErrorInfo describes a server error returned from a registry. +type ErrorInfo struct { + Code string `json:"code"` + Message string `json:"message"` + Detail string `json:"detail"` +} diff --git a/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/repository.go b/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/repository.go new file mode 100644 index 00000000..5dae5707 --- /dev/null +++ b/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/repository.go @@ -0,0 +1,20 @@ +// Copyright 2019 The Linux Foundation +// +// 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 v1 + +// RepositoryList returns a catalog of repositories maintained on the registry. +type RepositoryList struct { + Repositories []string `json:"repositories"` +} diff --git a/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/tags.go b/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/tags.go new file mode 100644 index 00000000..268e726e --- /dev/null +++ b/vendor/github.com/opencontainers/distribution-spec/specs-go/v1/tags.go @@ -0,0 +1,21 @@ +// Copyright 2019 The Linux Foundation +// +// 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 v1 + +// TagList is a list of tags for a given repository. +type TagList struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ac941e86..bb76519a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -117,6 +117,7 @@ github.com/nxadm/tail/winfile ## explicit github.com/onsi/ginkgo github.com/onsi/ginkgo/config +github.com/onsi/ginkgo/extensions/table github.com/onsi/ginkgo/ginkgo github.com/onsi/ginkgo/ginkgo/convert github.com/onsi/ginkgo/ginkgo/interrupthandler @@ -157,6 +158,9 @@ github.com/onsi/gomega/matchers/support/goraph/edge github.com/onsi/gomega/matchers/support/goraph/node github.com/onsi/gomega/matchers/support/goraph/util github.com/onsi/gomega/types +# github.com/opencontainers/distribution-spec v1.0.0-rc1 +## explicit +github.com/opencontainers/distribution-spec/specs-go/v1 # github.com/opencontainers/go-digest v1.0.0 ## explicit github.com/opencontainers/go-digest