From 7466a1492be0a9b7064ffa5db1b4b529cf9bc76e Mon Sep 17 00:00:00 2001 From: Serge Hallyn Date: Fri, 26 Apr 2019 07:19:49 -0500 Subject: [PATCH] Add client support for opencontainers/distribution-spec Add a new transport with URL prefix called "dist://" This is an implementation of an OCI image repository client, based on the opencontainers/distribution-spec. For a full commit history, please see https://github.com/anuvu/image/commits/master Consequently, a container image tool like skopeo when linked with this version of containers/image can do: "skopeo copy dist://src-server dist://dest-server" where src-server and dest-server are both OCI dist-spec compliant servers. For the current state of server-side implementations of opencontainers/distribution-spec, please see https://oci.bloodorange.io/ Signed-off-by: Ramkumar Chinchani Signed-off-by: Serge Hallyn Signed-off-by: Tycho Andersen --- dist/dist_client.go | 500 ++++++++++++++++++++++ dist/image_dest.go | 121 ++++++ dist/image_src.go | 66 +++ dist/transport.go | 186 ++++++++ go.sum | 1 + transports/alltransports/alltransports.go | 1 + 6 files changed, 875 insertions(+) create mode 100644 dist/dist_client.go create mode 100644 dist/image_dest.go create mode 100644 dist/image_src.go create mode 100644 dist/transport.go diff --git a/dist/dist_client.go b/dist/dist_client.go new file mode 100644 index 0000000000..c91202a9a4 --- /dev/null +++ b/dist/dist_client.go @@ -0,0 +1,500 @@ +package dist + +import ( + "bytes" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + + "github.com/containers/image/v5/pkg/docker/config" + "github.com/containers/image/v5/pkg/tlsclientconfig" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/pkg/errors" +) + +type OciRepo struct { + url url.URL + ref *distReference + authCreds string + client *http.Client +} + +//nolint (funlen) +func NewOciRepo(ref *distReference, sys *types.SystemContext) (r OciRepo, err error) { + server := "127.0.0.1" + port := "8080" + hostName := "" + + if ref.server != "" { + server = ref.server + hostName = server + } + + if ref.port != -1 { + port = fmt.Sprintf("%d", ref.port) + hostName += ":" + port + } + + insecureSkipVerify := false + if sys != nil { + insecureSkipVerify = (sys.DockerInsecureSkipTLSVerify == types.OptionalBoolTrue) + } + + tlsClientConfig := &tls.Config{ + MinVersion: tls.VersionTLS10, + PreferServerCipherSuites: true, + InsecureSkipVerify: insecureSkipVerify, + } + + certDir, err := ociCertDir(sys, hostName) + if err != nil { + return r, err + } + if err := tlsclientconfig.SetupCertificates(certDir, tlsClientConfig); err != nil { + return r, err + } + + transport := &http.Transport{TLSClientConfig: tlsClientConfig} + client := &http.Client{Transport: transport} + creds := "" + if sys != nil { + if sys.DockerAuthConfig != nil { + a := sys.DockerAuthConfig + creds = base64.StdEncoding.EncodeToString([]byte(a.Username + ":" + a.Password)) + } else { + registry := fmt.Sprintf("%s:%s", server, port) + if username, password, err := config.GetAuthentication(sys, registry); err == nil { + creds = base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + } + } + } + + r = OciRepo{ref: ref, authCreds: creds, client: client} + + ping := func(scheme string) error { + u := url.URL{Scheme: scheme, Host: fmt.Sprintf("%s:%s", server, port), Path: fmt.Sprintf("/v2/")} + resp, err := client.Get(u.String()) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized { + return errors.Errorf("Error pinging registry %s:%s, response code %d (%s)", + server, port, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + return nil + } + + scheme := "https" + err = ping(scheme) + if err != nil && insecureSkipVerify { + scheme = "http" + err = ping(scheme) + } + if err != nil { + return r, errors.Wrap(err, "unable to ping registry") + } + + r.url = url.URL{Scheme: scheme, Host: fmt.Sprintf("%s:%s", server, port)} + return r, nil +} + +func (o *OciRepo) GetManifest() ([]byte, *ispec.Manifest, error) { + name := o.ref.name + tag := o.ref.tag + m := &ispec.Manifest{} + + var body []byte + + manifestURI, err := url.Parse(fmt.Sprintf("/v2/%s/manifests/%s", name, tag)) + if err != nil { + return body, m, errors.Wrapf(err, "couldn't parse manifest url for repo %s and image %s", name, tag) + } + + manifestURI = o.url.ResolveReference(manifestURI) + req, err := http.NewRequest(http.MethodGet, manifestURI.String(), nil) + + if err != nil { + return body, m, errors.Wrapf(err, "Couldn't create DELETE request for %v", manifestURI) + } + + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + + req.Header.Add("Accept", "application/vnd.oci.image.manifest.v1+json") + resp, err := o.client.Do(req) + + if err != nil { + return body, m, errors.Wrapf(err, "Error getting manifest %s %s from %v", name, tag, o.url) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return body, m, fmt.Errorf("bad return code %d getting manifest", resp.StatusCode) + } + + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return body, m, errors.Wrapf(err, "Error reading response body for %s", tag) + } + + err = json.Unmarshal(body, m) + + if err != nil { + return body, m, errors.Wrap(err, "Failed decoding response") + } + + return body, m, nil +} + +func (o *OciRepo) RemoveManifest() error { + name := o.ref.name + tag := o.ref.tag + manifestURI, err := url.Parse(fmt.Sprintf("/v2/%s/manifests/%s", name, tag)) + + if err != nil { + return errors.Wrapf(err, "couldn't parse manifest url for repo %s and image %s", name, tag) + } + + manifestURI = o.url.ResolveReference(manifestURI) + req, err := http.NewRequest(http.MethodDelete, manifestURI.String(), nil) + + if err != nil { + return errors.Wrapf(err, "Couldn't create DELETE request for %v", manifestURI) + } + + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + + resp, err := o.client.Do(req) + if err != nil { + return errors.Wrapf(err, "Error deleting manifest") + } + + if resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("server returned unexpected code %d", resp.StatusCode) + } + + defer resp.Body.Close() + + return nil +} + +func (o *OciRepo) PutManifest(body []byte) error { + name := o.ref.name + tag := o.ref.tag + manifestURI, err := url.Parse(fmt.Sprintf("/v2/%s/manifests/%s", name, tag)) + + if err != nil { + return errors.Wrapf(err, "couldn't parse manifest url for repo %s and image %s", name, tag) + } + + manifestURI = o.url.ResolveReference(manifestURI) + req, err := http.NewRequest(http.MethodPut, manifestURI.String(), bytes.NewReader(body)) + + if err != nil { + return errors.Wrapf(err, "Couldn't create PUT request for %v", manifestURI) + } + + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + + req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json") + resp, err := o.client.Do(req) + + if err != nil { + return errors.Wrapf(err, "Error posting manifest") + } + + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("server returned unexpected code %d", resp.StatusCode) + } + + defer resp.Body.Close() + + return nil +} + +//HEAD /v2//blobs/ -> 200 (has layer) +func (o *OciRepo) HasLayer(ldigest string) bool { + name := o.ref.name + blobURI, err := url.Parse(fmt.Sprintf("/v2/%s/blobs/%s", name, ldigest)) + + if err != nil { + return false + } + + blobURI = o.url.ResolveReference(blobURI) + req, err := http.NewRequest(http.MethodHead, blobURI.String(), nil) + + if err != nil { + return false + } + + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + + resp, err := o.client.Do(req) + + if err != nil { + return false + } + + defer resp.Body.Close() + + return resp.StatusCode == http.StatusOK +} + +func (o *OciRepo) GetLayer(ldigest string) (io.ReadCloser, int64, error) { + name := o.ref.name + blobURI, err := url.Parse(fmt.Sprintf("/v2/%s/blobs/%s", name, ldigest)) + + if err != nil { + return nil, -1, errors.Wrapf(err, "couldn't parse URL for repo %s blob %s", name, ldigest) + } + + blobURI = o.url.ResolveReference(blobURI) + req, err := http.NewRequest("GET", blobURI.String(), nil) + + if err != nil { + return nil, -1, errors.Wrapf(err, "Couldn't create GET request for %v", blobURI) + } + + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + + resp, err := o.client.Do(req) + + if err != nil { + return nil, -1, errors.Wrapf(err, "Error getting layer %s", ldigest) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, -1, fmt.Errorf("bad return code %d getting layer", resp.StatusCode) + } + + return resp.Body, -1, err +} + +// StartLayer starts a blob upload session +func (o *OciRepo) StartLayer() (*url.URL, error) { + name := o.ref.name + uploadURI, err := url.Parse(fmt.Sprintf("/v2/%s/blobs/uploads/", name)) + + if err != nil { + return nil, errors.Wrap(err, "Failed to parse upload URL") + } + + uploadURI = o.url.ResolveReference(uploadURI) + req, err := http.NewRequest("POST", uploadURI.String(), nil) + + if err != nil { + return nil, errors.Wrap(err, "Failed opening POST request") + } + + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + + resp, err := o.client.Do(req) + + if err != nil { + return nil, errors.Wrapf(err, "Failed posting request %v", req) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusAccepted { + return nil, fmt.Errorf("server returned an error %d", resp.StatusCode) + } + + _, err = ioutil.ReadAll(resp.Body) + + if err != nil { + return nil, errors.Wrapf(err, "Error reading response body for %s", name) + } + + session, err := resp.Location() + + if err != nil { + return nil, errors.Wrap(err, "Failed decoding response") + } + + return o.url.ResolveReference(session), nil +} + +// @path is the uuid upload path returned by the server to our Post request. +// @stream is the data source for the layer. +// Return the digest and size of the layer that was uploaded. +//nolint (gocognit) +func (o *OciRepo) CompleteLayer(session *url.URL, stream io.Reader) (digest.Digest, int64, error) { + digester := sha256.New() + hashReader := io.TeeReader(stream, digester) + // using "chunked" upload + count := int64(0) + for { + const maxSize = 10 * 1024 * 1024 + var buf bytes.Buffer + size, err := io.CopyN(&buf, hashReader, maxSize) + if size == 0 { + if err != io.EOF { + return "", -1, errors.Wrapf(err, "Failed to copy stream") + } + break + } + req, err := http.NewRequest(http.MethodPatch, session.String(), &buf) + if err != nil { + return "", -1, errors.Wrap(err, "Failed opening Patch request") + } + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + + req.ContentLength = size + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", count, count+size-1)) + resp, err := o.client.Do(req) + if err != nil { + return "", -1, errors.Wrapf(err, "Failed posting request %v", req) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + return "", -1, fmt.Errorf("Server returned an error %d", resp.StatusCode) + } + count += size + session, err = resp.Location() + if err != nil { + return "", -1, errors.Wrap(err, "Failed decoding response") + } + + session = o.url.ResolveReference(session) + } + + ourDigest := fmt.Sprintf("%x", digester.Sum(nil)) + d := digest.NewDigestFromEncoded(digest.SHA256, ourDigest) + q := session.Query() + q.Set("digest", d.String()) + session.RawQuery = q.Encode() + req, err := http.NewRequest(http.MethodPut, session.String(), nil) + if err != nil { + return "", -1, errors.Wrap(err, "Failed opening Put request") + } + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + req.Header.Set("Content-Type", "application/octet-stream") + putResp, err := o.client.Do(req) + if err != nil { + return "", -1, errors.Wrapf(err, "Failed putting request %v", req) + } + defer putResp.Body.Close() + if putResp.StatusCode != http.StatusCreated { + return "", -1, fmt.Errorf("Server returned an error %d", putResp.StatusCode) + } + + servDigest, ok := putResp.Header["Docker-Content-Digest"] + if !ok || len(servDigest) != 1 { + return "", -1, fmt.Errorf("Server returned incomplete headers") + } + + blobLoc, err := putResp.Location() + if err != nil { + return "", -1, errors.Wrap(err, "Failed decoding response") + } + + blobLoc = o.url.ResolveReference(blobLoc) + req, err = http.NewRequest("HEAD", blobLoc.String(), nil) + if err != nil { + return "", -1, errors.Wrap(err, "Failed opening Head request") + } + if o.authCreds != "" { + req.Header.Add("Authorization", "Basic "+o.authCreds) + } + resp, err := o.client.Do(req) + if err != nil { + return "", -1, errors.Wrapf(err, "Failed getting new layer %v", blobLoc) + } + + Length, ok := resp.Header["Content-Length"] + if !ok || len(Length) != 1 { + return "", -1, fmt.Errorf("Server returned incomplete headers") + } + length, err := strconv.ParseInt(Length[0], 10, 64) + if err != nil { + return "", -1, errors.Wrap(err, "Failed decoding length in response") + } + + if servDigest[0] != d.String() { + return "", -1, errors.Wrapf(err, "Server calculated digest %s, not our %s", servDigest[0], ourDigest) + } + + // TODO dist is returning the wrong thing - the hash, + // not the "digest", which is "sha256:hash" + + return d, length, nil +} + +// ociCertDir returns a path to a directory to be consumed by +// tlsclientconfig.SetupCertificates() depending on ctx and hostPort. +func ociCertDir(sys *types.SystemContext, hostPort string) (string, error) { + if sys != nil && sys.DockerCertPath != "" { + return sys.DockerCertPath, nil + } + + if sys != nil && sys.DockerPerHostCertDirPath != "" { + return filepath.Join(sys.DockerPerHostCertDirPath, hostPort), nil + } + + var ( + hostCertDir string + fullCertDirPath string + systemPerHostCertDirPaths = [1]string{"/etc/containers/certs.d"} + ) + + for _, systemPerHostCertDirPath := range systemPerHostCertDirPaths { + if sys != nil && sys.RootForImplicitAbsolutePaths != "" { + hostCertDir = filepath.Join(sys.RootForImplicitAbsolutePaths, systemPerHostCertDirPath) + } else { + hostCertDir = systemPerHostCertDirPath + } + + fullCertDirPath = filepath.Join(hostCertDir, hostPort) + _, err := os.Stat(fullCertDirPath) + + if err == nil { + break + } + + if os.IsNotExist(err) { + continue + } + + if os.IsPermission(err) { + continue + } + + if err != nil { + return "", err + } + } + + return fullCertDirPath, nil +} diff --git a/dist/image_dest.go b/dist/image_dest.go new file mode 100644 index 0000000000..ae57ae4e40 --- /dev/null +++ b/dist/image_dest.go @@ -0,0 +1,121 @@ +package dist + +import ( + "context" + "io" + + "github.com/containers/image/v5/pkg/blobinfocache/none" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +// NOTE - the ImageDestination interface is defined in types.go + +type distImageDest struct { + s *OciRepo + ref distReference +} + +func (o *distImageDest) Reference() types.ImageReference { + return o.ref +} + +func (o *distImageDest) Close() error { + return nil +} + +func (o *distImageDest) SupportedManifestMIMETypes() []string { + return []string{ + ispec.MediaTypeImageManifest, + } +} + +func (o *distImageDest) SupportsSignatures(ctx context.Context) error { + return nil +} + +func (o *distImageDest) DesiredLayerCompression() types.LayerCompression { + return types.PreserveOriginal +} + +func (o *distImageDest) AcceptsForeignLayerURLs() bool { + return true +} + +func (o *distImageDest) MustMatchRuntimeOS() bool { + return false +} + +func (o *distImageDest) IgnoresEmbeddedDockerReference() bool { + // Return value does not make a difference if Reference().DockerReference() + // is nil. + return true +} + +// PutBlob writes contents of stream and returns data representing the result. +// inputInfo.Digest can be optionally provided if known; it is not mandatory for the implementation to verify it. +// inputInfo.Size is the expected length of stream, if known. +// inputInfo.MediaType describes the blob format, if known. +// May update cache. +// WARNING: The contents of stream are being verified on the fly. +// Until stream.Read() returns io.EOF, the contents of the data SHOULD NOT be available +// to any other readers for download using the supplied digest. +// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST +// - 1) fail, and 2) delete any data stored so far. +func (o *distImageDest) PutBlob(ctx context.Context, stream io.Reader, + inputInfo types.BlobInfo, cache types.BlobInfoCache, isConfig bool) (types.BlobInfo, error) { + if inputInfo.Digest.String() != "" { + ok, info, err := o.TryReusingBlob(ctx, inputInfo, none.NoCache, false) + if err != nil { + return types.BlobInfo{}, err + } + + if ok { + return info, nil + } + } + + // Do this as a chunked upload so we can calculate the digest, since + // caller is not giving it to us. + u, err := o.s.StartLayer() + + if err != nil { + return types.BlobInfo{}, err + } + + digest, size, err := o.s.CompleteLayer(u, stream) + + return types.BlobInfo{Digest: digest, Size: size}, err +} + +// HasThreadSafePutBlob indicates whether PutBlob can be executed concurrently. +func (o *distImageDest) HasThreadSafePutBlob() bool { + return true +} + +func (o *distImageDest) TryReusingBlob(ctx context.Context, info types.BlobInfo, + cache types.BlobInfoCache, canSubstitute bool) (bool, types.BlobInfo, error) { + if info.Digest == "" { + return false, types.BlobInfo{}, errors.Errorf(`"Can not check for a blob with unknown digest`) + } + + if o.s.HasLayer(info.Digest.String()) { + return true, types.BlobInfo{Digest: info.Digest, Size: -1}, nil + } + + return false, types.BlobInfo{}, nil +} + +func (o *distImageDest) PutManifest(ctx context.Context, m []byte, d *digest.Digest) error { + return o.s.PutManifest(m) +} + +func (o *distImageDest) PutSignatures(ctx context.Context, signatures [][]byte, d *digest.Digest) error { + return nil +} + +func (o *distImageDest) Commit(ctx context.Context, image types.UnparsedImage) error { + return nil +} diff --git a/dist/image_src.go b/dist/image_src.go new file mode 100644 index 0000000000..9077efb65c --- /dev/null +++ b/dist/image_src.go @@ -0,0 +1,66 @@ +package dist + +import ( + "context" + "fmt" + "io" + + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +// NOTE - the ImageSource interface is defined and commented in types.go + +type distImageSource struct { + s *OciRepo + ref distReference + manifest *ispec.Manifest + cachedManifest []byte +} + +func (o *distImageSource) Reference() types.ImageReference { + return o.ref +} + +func (o *distImageSource) Close() error { + return nil +} + +func (o *distImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { + if instanceDigest != nil { + return nil, "", fmt.Errorf("GetManifest with instanceDigest is not implemented") + } + + if o.manifest == nil { + bytes, m, err := o.s.GetManifest() + if err != nil { + return nil, "", errors.Wrap(err, "Failed fetching manifest") + } + + o.cachedManifest = bytes + o.manifest = m + } + + return o.cachedManifest, ispec.MediaTypeImageManifest, nil +} + +func (o *distImageSource) GetBlob(ctx context.Context, info types.BlobInfo, + cache types.BlobInfoCache) (io.ReadCloser, int64, error) { + digest := info.Digest.String() + + return o.s.GetLayer(digest) +} + +func (o *distImageSource) HasThreadSafeGetBlob() bool { + return true +} + +func (o *distImageSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { + return [][]byte{}, nil +} + +func (o *distImageSource) LayerInfosForCopy(ctx context.Context, layerDigest *digest.Digest) ([]types.BlobInfo, error) { + return nil, nil +} diff --git a/dist/transport.go b/dist/transport.go new file mode 100644 index 0000000000..b439b8326e --- /dev/null +++ b/dist/transport.go @@ -0,0 +1,186 @@ +package dist + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/image" + "github.com/containers/image/v5/transports" + "github.com/containers/image/v5/types" + "github.com/pkg/errors" +) + +//nolint (gochecknoinits) +func init() { + transports.Register(Transport) +} + +// nolint (gochecknoglobals) +var Transport = distTransport{} + +type distTransport struct{} + +func (s distTransport) Name() string { + return "dist" +} + +func splitReference(ref string) (fullname, server string, port int, err error) { + port = 8080 + err = nil + + if ref[0] == '/' { + fullname = ref[1:] + return fullname, "", port, nil + } + + fields := strings.SplitN(ref, "/", 2) + subFields := strings.Split(fields[0], ":") + + if len(subFields) > 2 { + err = fmt.Errorf("bad server:port") + return "", "", -1, err + } + + server = subFields[0] + + if len(subFields) == 2 { + port, err = strconv.Atoi(subFields[1]) + if err != nil { + return "", "", -1, err + } + + if port < 1 || port > 65535 { + err = fmt.Errorf("bad port %d", port) + return "", "", -1, err + } + } + + fullname = fields[1] + + return fullname, server, port, nil +} + +// NOTE - the transport interface is defined in types/types.go. +// Valid uris are: +// dist:///name1/name2/tag +// dist://server/name1/name2/name3/tag +// The tag can be separated by either / or : +// dist://server:port/name1/name2/name3/tag +// dist://server:port/name1/name2/name3:tag +// So the reference passed in here would be e.g. +// ///name1/name2/tag +// //server:port/name1/name2/tag +func (s distTransport) ParseReference(reference string) (types.ImageReference, error) { + if !strings.HasPrefix(reference, "//") { + return nil, errors.Errorf("dist image reference %s does not start with //", reference) + } + + fullname, server, port, err := splitReference(reference[2:]) + if err != nil { + return nil, errors.Wrapf(err, "Failed parsing reference: '%s'", reference) + } + + // support : for tag separateion + var name, tag string + + fields := strings.Split(fullname, ":") + + if len(fields) != 2 || len(fields[0]) == 0 || len(fields[1]) == 0 { + return nil, fmt.Errorf("no tag specified in '%s'", fullname) + } + + name = fields[0] + tag = fields[1] + + return distReference{ + server: server, + port: port, + fullname: fullname, + name: name, + tag: tag, + }, nil +} + +func (s distTransport) ValidatePolicyConfigurationScope(scope string) error { + return nil +} + +type distReference struct { + server string + port int + fullname string + name string + tag string +} + +func (ref distReference) Transport() types.ImageTransport { + return Transport +} + +func (ref distReference) StringWithinTransport() string { + port := "" + + if ref.port != -1 { + port = fmt.Sprintf("%d/", ref.port) + } + + return fmt.Sprintf("//%s:%s%s", ref.server, port, ref.fullname) +} + +func (ref distReference) DockerReference() reference.Named { + return nil +} + +func (ref distReference) PolicyConfigurationIdentity() string { + return ref.StringWithinTransport() +} + +func (ref distReference) PolicyConfigurationNamespaces() []string { + return []string{} +} + +func (ref distReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) { + src, err := ref.NewImageSource(ctx, sys) + if err != nil { + return nil, err + } + + return image.FromSource(ctx, sys, src) +} + +func (ref distReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) { + s, err := NewOciRepo(&ref, sys) + if err != nil { + return nil, errors.Wrap(err, "Failed connecting to server") + } + + return &distImageSource{ + ref: ref, + s: &s, + }, nil +} + +func (ref distReference) NewImageDestination(ctx context.Context, + sys *types.SystemContext) (types.ImageDestination, error) { + s, err := NewOciRepo(&ref, sys) + if err != nil { + return nil, errors.Wrap(err, "Failed connecting to server") + } + + return &distImageDest{ + ref: ref, + s: &s, + }, nil +} + +func (ref distReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error { + s, err := NewOciRepo(&ref, sys) + if err != nil { + return errors.Wrap(err, "Failed connecting to server") + } + + return s.RemoveManifest() +} diff --git a/go.sum b/go.sum index 55817b2c6b..5dd25db1af 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,7 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containers/image v3.0.2+incompatible h1:B1lqAE8MUPCrsBLE86J0gnXleeRq8zJnQryhiiGQNyE= github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE= github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/ocicrypt v0.0.0-20190930154801-b87a4a69c741 h1:8tQkOcednLJtUcZgK7sPglscXtxvMOnFOa6wd09VWLM= diff --git a/transports/alltransports/alltransports.go b/transports/alltransports/alltransports.go index 2110a091d2..7feacb3058 100644 --- a/transports/alltransports/alltransports.go +++ b/transports/alltransports/alltransports.go @@ -7,6 +7,7 @@ import ( // NOTE: Make sure docs/containers-policy.json.5.md is updated when adding or updating // a transport. _ "github.com/containers/image/v5/directory" + _ "github.com/containers/image/v5/dist" _ "github.com/containers/image/v5/docker" _ "github.com/containers/image/v5/docker/archive" _ "github.com/containers/image/v5/oci/archive"