Merge pull request #387 from apilloud/update_image

Update containers/image
This commit is contained in:
Mrunal Patel 2017-03-13 12:09:51 -07:00 committed by GitHub
commit fdc7f5a77a
87 changed files with 3103 additions and 760 deletions

View file

@ -1,5 +1,5 @@
{ {
"memo": "43d56ce99d6232de6146fa891dafc690d66b05555ce6587759f39d037e37c84a", "memo": "f9f05813d58aa8fce2eed8aae7f05fd12d1f2965afb0fea7ed0ead9a70836e53",
"projects": [ "projects": [
{ {
"name": "github.com/BurntSushi/toml", "name": "github.com/BurntSushi/toml",
@ -50,7 +50,7 @@
{ {
"name": "github.com/containers/image", "name": "github.com/containers/image",
"branch": "master", "branch": "master",
"revision": "1c202c5d85d2ee531acb1e91740144410066d19e", "revision": "1d7e25b91705e4d1cddb5396baf112caeb1119f3",
"packages": [ "packages": [
"copy", "copy",
"directory", "directory",
@ -63,9 +63,11 @@
"manifest", "manifest",
"oci/layout", "oci/layout",
"openshift", "openshift",
"pkg/compression",
"signature", "signature",
"storage", "storage",
"transports", "transports",
"transports/alltransports",
"types", "types",
"version" "version"
] ]

View file

@ -4,7 +4,7 @@ import (
"github.com/containers/image/copy" "github.com/containers/image/copy"
"github.com/containers/image/signature" "github.com/containers/image/signature"
istorage "github.com/containers/image/storage" istorage "github.com/containers/image/storage"
"github.com/containers/image/transports" "github.com/containers/image/transports/alltransports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/storage/storage" "github.com/containers/storage/storage"
) )
@ -64,7 +64,7 @@ func (svc *imageService) ListImages(filter string) ([]ImageResult, error) {
} }
func (svc *imageService) ImageStatus(systemContext *types.SystemContext, nameOrID string) (*ImageResult, error) { func (svc *imageService) ImageStatus(systemContext *types.SystemContext, nameOrID string) (*ImageResult, error) {
ref, err := transports.ParseImageName(nameOrID) ref, err := alltransports.ParseImageName(nameOrID)
if err != nil { if err != nil {
ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, "@"+nameOrID) ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, "@"+nameOrID)
if err2 != nil { if err2 != nil {
@ -118,12 +118,12 @@ func (svc *imageService) PullImage(systemContext *types.SystemContext, imageName
if options == nil { if options == nil {
options = &copy.Options{} options = &copy.Options{}
} }
srcRef, err := transports.ParseImageName(imageName) srcRef, err := alltransports.ParseImageName(imageName)
if err != nil { if err != nil {
if svc.defaultTransport == "" { if svc.defaultTransport == "" {
return nil, err return nil, err
} }
srcRef2, err2 := transports.ParseImageName(svc.defaultTransport + imageName) srcRef2, err2 := alltransports.ParseImageName(svc.defaultTransport + imageName)
if err2 != nil { if err2 != nil {
return nil, err return nil, err
} }
@ -131,7 +131,7 @@ func (svc *imageService) PullImage(systemContext *types.SystemContext, imageName
} }
dest := imageName dest := imageName
if srcRef.DockerReference() != nil { if srcRef.DockerReference() != nil {
dest = srcRef.DockerReference().FullName() dest = srcRef.DockerReference().Name()
} }
destRef, err := istorage.Transport.ParseStoreReference(svc.store, dest) destRef, err := istorage.Transport.ParseStoreReference(svc.store, dest)
if err != nil { if err != nil {
@ -157,7 +157,7 @@ func (svc *imageService) PullImage(systemContext *types.SystemContext, imageName
} }
func (svc *imageService) RemoveImage(systemContext *types.SystemContext, nameOrID string) error { func (svc *imageService) RemoveImage(systemContext *types.SystemContext, nameOrID string) error {
ref, err := transports.ParseImageName(nameOrID) ref, err := alltransports.ParseImageName(nameOrID)
if err != nil { if err != nil {
ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, "@"+nameOrID) ref2, err2 := istorage.Transport.ParseStoreReference(svc.store, "@"+nameOrID)
if err2 != nil { if err2 != nil {

View file

@ -9,7 +9,7 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/copy" "github.com/containers/image/copy"
istorage "github.com/containers/image/storage" istorage "github.com/containers/image/storage"
"github.com/containers/image/transports" "github.com/containers/image/transports/alltransports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/storage/storage" "github.com/containers/storage/storage"
"github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/image-spec/specs-go/v1"
@ -162,9 +162,9 @@ func (r *runtimeService) createContainerOrPodSandbox(systemContext *types.System
ref, err := istorage.Transport.ParseStoreReference(r.image.GetStore(), imageName) ref, err := istorage.Transport.ParseStoreReference(r.image.GetStore(), imageName)
if err != nil { if err != nil {
// Maybe it's some other transport's copy of the image? // Maybe it's some other transport's copy of the image?
otherRef, err2 := transports.ParseImageName(imageName) otherRef, err2 := alltransports.ParseImageName(imageName)
if err2 == nil && otherRef.DockerReference() != nil { if err2 == nil && otherRef.DockerReference() != nil {
ref, err = istorage.Transport.ParseStoreReference(r.image.GetStore(), otherRef.DockerReference().FullName()) ref, err = istorage.Transport.ParseStoreReference(r.image.GetStore(), otherRef.DockerReference().Name())
} }
if err != nil { if err != nil {
// Maybe the image ID is sufficient? // Maybe the image ID is sufficient?

View file

@ -7,7 +7,7 @@ import (
"github.com/containers/image/copy" "github.com/containers/image/copy"
"github.com/containers/image/signature" "github.com/containers/image/signature"
"github.com/containers/image/storage" "github.com/containers/image/storage"
"github.com/containers/image/transports" "github.com/containers/image/transports/alltransports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/storage/pkg/reexec" "github.com/containers/storage/pkg/reexec"
sstorage "github.com/containers/storage/storage" sstorage "github.com/containers/storage/storage"
@ -137,7 +137,7 @@ func main() {
options := &copy.Options{} options := &copy.Options{}
if importFrom != "" { if importFrom != "" {
importRef, err = transports.ParseImageName(importFrom) importRef, err = alltransports.ParseImageName(importFrom)
if err != nil { if err != nil {
logrus.Errorf("error parsing image name %v: %v", importFrom, err) logrus.Errorf("error parsing image name %v: %v", importFrom, err)
os.Exit(1) os.Exit(1)
@ -145,7 +145,7 @@ func main() {
} }
if exportTo != "" { if exportTo != "" {
exportRef, err = transports.ParseImageName(exportTo) exportRef, err = alltransports.ParseImageName(exportTo)
if err != nil { if err != nil {
logrus.Errorf("error parsing image name %v: %v", exportTo, err) logrus.Errorf("error parsing image name %v: %v", exportTo, err)
os.Exit(1) os.Exit(1)

2
vendor/github.com/containers/image/.gitignore generated vendored Normal file
View file

@ -0,0 +1,2 @@
vendor
tools.timestamp

View file

@ -5,8 +5,7 @@
email: false email: false
go: go:
- 1.7 - 1.7
install: make deps script: make tools .gitvalidation validate test test-skopeo
script: make .gitvalidation && make validate && make test && make test-skopeo
dist: trusty dist: trusty
os: os:
- linux - linux

View file

@ -1,4 +1,4 @@
.PHONY: all deps test validate lint .PHONY: all tools test validate lint
# Which github repostiory and branch to use for testing with skopeo # Which github repostiory and branch to use for testing with skopeo
SKOPEO_REPO = projectatomic/skopeo SKOPEO_REPO = projectatomic/skopeo
@ -8,15 +8,27 @@ SUDO =
BUILDTAGS = btrfs_noversion libdm_no_deferred_remove BUILDTAGS = btrfs_noversion libdm_no_deferred_remove
BUILDFLAGS := -tags "$(BUILDTAGS)" BUILDFLAGS := -tags "$(BUILDTAGS)"
all: deps .gitvalidation test validate PACKAGES := $(shell go list ./... | grep -v github.com/containers/image/vendor)
deps: all: tools .gitvalidation test validate
go get -t $(BUILDFLAGS) ./...
go get -u $(BUILDFLAGS) github.com/golang/lint/golint
go get $(BUILDFLAGS) github.com/vbatts/git-validation
test: tools: tools.timestamp
@go test $(BUILDFLAGS) -cover ./...
tools.timestamp: Makefile
@go get -u $(BUILDFLAGS) github.com/golang/lint/golint
@go get $(BUILDFLAGS) github.com/vbatts/git-validation
@go get -u github.com/rancher/trash
@touch tools.timestamp
vendor: tools.timestamp vendor.conf
@trash
@touch vendor
clean:
rm -rf vendor tools.timestamp
test: vendor
@go test $(BUILDFLAGS) -cover $(PACKAGES)
# This is not run as part of (make all), but Travis CI does run this. # This is not run as part of (make all), but Travis CI does run this.
# Demonstarting a working version of skopeo (possibly with modified SKOPEO_REPO/SKOPEO_BRANCH, e.g. # Demonstarting a working version of skopeo (possibly with modified SKOPEO_REPO/SKOPEO_BRANCH, e.g.
@ -29,19 +41,19 @@ test-skopeo:
skopeo_path=$${GOPATH}/src/github.com/projectatomic/skopeo && \ skopeo_path=$${GOPATH}/src/github.com/projectatomic/skopeo && \
vendor_path=$${skopeo_path}/vendor/github.com/containers/image && \ vendor_path=$${skopeo_path}/vendor/github.com/containers/image && \
git clone -b $(SKOPEO_BRANCH) https://github.com/$(SKOPEO_REPO) $${skopeo_path} && \ git clone -b $(SKOPEO_BRANCH) https://github.com/$(SKOPEO_REPO) $${skopeo_path} && \
rm -rf $${vendor_path} && cp -r . $${vendor_path} && \ rm -rf $${vendor_path} && cp -r . $${vendor_path} && rm -rf $${vendor_path}/vendor && \
cd $${skopeo_path} && \ cd $${skopeo_path} && \
make BUILDTAGS="$(BUILDTAGS)" binary-local test-all-local && \ make BUILDTAGS="$(BUILDTAGS)" binary-local test-all-local && \
$(SUDO) make check && \ $(SUDO) make check && \
rm -rf $${skopeo_path} rm -rf $${skopeo_path}
validate: lint validate: lint
@go vet ./... @go vet $(PACKAGES)
@test -z "$$(gofmt -s -l . | tee /dev/stderr)" @test -z "$$(gofmt -s -l . | grep -ve '^vendor' | tee /dev/stderr)"
lint: lint:
@out="$$(golint ./...)"; \ @out="$$(golint $(PACKAGES))"; \
if [ -n "$$(golint ./...)" ]; then \ if [ -n "$$out" ]; then \
echo "$$out"; \ echo "$$out"; \
exit 1; \ exit 1; \
fi fi
@ -52,7 +64,7 @@ EPOCH_TEST_COMMIT ?= e68e0e1110e64f906f9b482e548f17d73e02e6b1
# When this is running in travis, it will only check the travis commit range # When this is running in travis, it will only check the travis commit range
.gitvalidation: .gitvalidation:
@which git-validation > /dev/null 2>/dev/null || (echo "ERROR: git-validation not found. Consider 'make deps' target" && false) @which git-validation > /dev/null 2>/dev/null || (echo "ERROR: git-validation not found. Consider 'make clean && make tools'" && false)
ifeq ($(TRAVIS),true) ifeq ($(TRAVIS),true)
@git-validation -q -run DCO,short-subject,dangling-whitespace @git-validation -q -run DCO,short-subject,dangling-whitespace
else else

View file

@ -1,15 +1,36 @@
[![GoDoc](https://godoc.org/github.com/containers/image?status.svg)](https://godoc.org/github.com/containers/image) [![Build Status](https://travis-ci.org/containers/image.svg?branch=master)](https://travis-ci.org/containers/image) [![GoDoc](https://godoc.org/github.com/containers/image?status.svg)](https://godoc.org/github.com/containers/image) [![Build Status](https://travis-ci.org/containers/image.svg?branch=master)](https://travis-ci.org/containers/image)
= =
`image` is a set of Go libraries aimed at working in various way with containers' images and container image registries. `image` is a set of Go libraries aimed at working in various way with
containers' images and container image registries.
The containers/image library allows application to pull and push images from container image registries, like the upstream docker registry. It also implements "simple image signing". The containers/image library allows application to pull and push images from
container image registries, like the upstream docker registry. It also
implements "simple image signing".
The containers/image library also allows you to inspect a repository on a container registry without pulling down the image. This means it fetches the repository's manifest and it is able to show you a `docker inspect`-like json output about a whole repository or a tag. This library, in contrast to `docker inspect`, helps you gather useful information about a repository or a tag without requiring you to run `docker pull`. The containers/image library also allows you to inspect a repository on a
container registry without pulling down the image. This means it fetches the
repository's manifest and it is able to show you a `docker inspect`-like json
output about a whole repository or a tag. This library, in contrast to `docker
inspect`, helps you gather useful information about a repository or a tag
without requiring you to run `docker pull`.
The containers/image library also allows you to translate from one image format to another, for example docker container images to OCI images. It also allows you to copy container images between various registries, possibly converting them as necessary, and to sign and verify images. The containers/image library also allows you to translate from one image format
to another, for example docker container images to OCI images. It also allows
you to copy container images between various registries, possibly converting
them as necessary, and to sign and verify images.
The [skopeo](https://github.com/projectatomic/skopeo) tool uses the containers/image library and takes advantage of its many features. The [skopeo](https://github.com/projectatomic/skopeo) tool uses the
containers/image library and takes advantage of its many features.
## Dependencies
Dependencies that this library prefers will not be found in the `vendor`
directory. This is so you can make well-informed decisions about which
libraries you should use with this package in your own projects.
What this project tests against dependencies-wise is located
[here](https://github.com/containers/image/blob/master/vendor.conf).
## License ## License

View file

@ -7,12 +7,14 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"reflect" "reflect"
"time"
pb "gopkg.in/cheggaaa/pb.v1" pb "gopkg.in/cheggaaa/pb.v1"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/image" "github.com/containers/image/image"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/pkg/compression"
"github.com/containers/image/signature" "github.com/containers/image/signature"
"github.com/containers/image/transports" "github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
@ -45,6 +47,8 @@ type imageCopier struct {
diffIDsAreNeeded bool diffIDsAreNeeded bool
canModifyManifest bool canModifyManifest bool
reportWriter io.Writer reportWriter io.Writer
progressInterval time.Duration
progress chan types.ProgressProperties
} }
// newDigestingReader returns an io.Reader implementation with contents of source, which will eventually return a non-EOF error // newDigestingReader returns an io.Reader implementation with contents of source, which will eventually return a non-EOF error
@ -92,14 +96,28 @@ type Options struct {
ReportWriter io.Writer ReportWriter io.Writer
SourceCtx *types.SystemContext SourceCtx *types.SystemContext
DestinationCtx *types.SystemContext DestinationCtx *types.SystemContext
ProgressInterval time.Duration // time to wait between reports to signal the progress channel
Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset.
} }
// Image copies image from srcRef to destRef, using policyContext to validate source image admissibility. // Image copies image from srcRef to destRef, using policyContext to validate
func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *Options) error { // source image admissibility.
func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *Options) (retErr error) {
// NOTE this function uses an output parameter for the error return value.
// Setting this and returning is the ideal way to return an error.
//
// the defers in this routine will wrap the error return with its own errors
// which can be valuable context in the middle of a multi-streamed copy.
if options == nil {
options = &Options{}
}
reportWriter := ioutil.Discard reportWriter := ioutil.Discard
if options != nil && options.ReportWriter != nil {
if options.ReportWriter != nil {
reportWriter = options.ReportWriter reportWriter = options.ReportWriter
} }
writeReport := func(f string, a ...interface{}) { writeReport := func(f string, a ...interface{}) {
fmt.Fprintf(reportWriter, f, a...) fmt.Fprintf(reportWriter, f, a...)
} }
@ -108,7 +126,12 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
if err != nil { if err != nil {
return errors.Wrapf(err, "Error initializing destination %s", transports.ImageName(destRef)) return errors.Wrapf(err, "Error initializing destination %s", transports.ImageName(destRef))
} }
defer dest.Close() defer func() {
if err := dest.Close(); err != nil {
retErr = errors.Wrapf(retErr, " (dest: %v)", err)
}
}()
destSupportedManifestMIMETypes := dest.SupportedManifestMIMETypes() destSupportedManifestMIMETypes := dest.SupportedManifestMIMETypes()
rawSource, err := srcRef.NewImageSource(options.SourceCtx, destSupportedManifestMIMETypes) rawSource, err := srcRef.NewImageSource(options.SourceCtx, destSupportedManifestMIMETypes)
@ -118,7 +141,9 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
unparsedImage := image.UnparsedFromSource(rawSource) unparsedImage := image.UnparsedFromSource(rawSource)
defer func() { defer func() {
if unparsedImage != nil { if unparsedImage != nil {
unparsedImage.Close() if err := unparsedImage.Close(); err != nil {
retErr = errors.Wrapf(retErr, " (unparsed: %v)", err)
}
} }
}() }()
@ -131,14 +156,18 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
return errors.Wrapf(err, "Error initializing image from source %s", transports.ImageName(srcRef)) return errors.Wrapf(err, "Error initializing image from source %s", transports.ImageName(srcRef))
} }
unparsedImage = nil unparsedImage = nil
defer src.Close() defer func() {
if err := src.Close(); err != nil {
retErr = errors.Wrapf(retErr, " (source: %v)", err)
}
}()
if src.IsMultiImage() { if src.IsMultiImage() {
return errors.Errorf("can not copy %s: manifest contains multiple images", transports.ImageName(srcRef)) return errors.Errorf("can not copy %s: manifest contains multiple images", transports.ImageName(srcRef))
} }
var sigs [][]byte var sigs [][]byte
if options != nil && options.RemoveSignatures { if options.RemoveSignatures {
sigs = [][]byte{} sigs = [][]byte{}
} else { } else {
writeReport("Getting image source signatures\n") writeReport("Getting image source signatures\n")
@ -173,6 +202,8 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
diffIDsAreNeeded: src.UpdatedImageNeedsLayerDiffIDs(manifestUpdates), diffIDsAreNeeded: src.UpdatedImageNeedsLayerDiffIDs(manifestUpdates),
canModifyManifest: canModifyManifest, canModifyManifest: canModifyManifest,
reportWriter: reportWriter, reportWriter: reportWriter,
progressInterval: options.ProgressInterval,
progress: options.Progress,
} }
if err := ic.copyLayers(); err != nil { if err := ic.copyLayers(); err != nil {
@ -199,7 +230,7 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
return err return err
} }
if options != nil && options.SignBy != "" { if options.SignBy != "" {
mech, err := signature.NewGPGSigningMechanism() mech, err := signature.NewGPGSigningMechanism()
if err != nil { if err != nil {
return errors.Wrap(err, "Error initializing GPG") return errors.Wrap(err, "Error initializing GPG")
@ -370,7 +401,7 @@ func (ic *imageCopier) copyLayer(srcInfo types.BlobInfo) (types.BlobInfo, digest
// and returns a complete blobInfo of the copied blob and perhaps a <-chan diffIDResult if diffIDIsNeeded, to be read by the caller. // and returns a complete blobInfo of the copied blob and perhaps a <-chan diffIDResult if diffIDIsNeeded, to be read by the caller.
func (ic *imageCopier) copyLayerFromStream(srcStream io.Reader, srcInfo types.BlobInfo, func (ic *imageCopier) copyLayerFromStream(srcStream io.Reader, srcInfo types.BlobInfo,
diffIDIsNeeded bool) (types.BlobInfo, <-chan diffIDResult, error) { diffIDIsNeeded bool) (types.BlobInfo, <-chan diffIDResult, error) {
var getDiffIDRecorder func(decompressorFunc) io.Writer // = nil var getDiffIDRecorder func(compression.DecompressorFunc) io.Writer // = nil
var diffIDChan chan diffIDResult var diffIDChan chan diffIDResult
err := errors.New("Internal error: unexpected panic in copyLayer") // For pipeWriter.CloseWithError below err := errors.New("Internal error: unexpected panic in copyLayer") // For pipeWriter.CloseWithError below
@ -381,7 +412,7 @@ func (ic *imageCopier) copyLayerFromStream(srcStream io.Reader, srcInfo types.Bl
pipeWriter.CloseWithError(err) // CloseWithError(nil) is equivalent to Close() pipeWriter.CloseWithError(err) // CloseWithError(nil) is equivalent to Close()
}() }()
getDiffIDRecorder = func(decompressor decompressorFunc) io.Writer { getDiffIDRecorder = func(decompressor compression.DecompressorFunc) io.Writer {
// If this fails, e.g. because we have exited and due to pipeWriter.CloseWithError() above further // If this fails, e.g. because we have exited and due to pipeWriter.CloseWithError() above further
// reading from the pipe has failed, we dont really care. // reading from the pipe has failed, we dont really care.
// We only read from diffIDChan if the rest of the flow has succeeded, and when we do read from it, // We only read from diffIDChan if the rest of the flow has succeeded, and when we do read from it,
@ -399,7 +430,7 @@ func (ic *imageCopier) copyLayerFromStream(srcStream io.Reader, srcInfo types.Bl
} }
// diffIDComputationGoroutine reads all input from layerStream, uncompresses using decompressor if necessary, and sends its digest, and status, if any, to dest. // diffIDComputationGoroutine reads all input from layerStream, uncompresses using decompressor if necessary, and sends its digest, and status, if any, to dest.
func diffIDComputationGoroutine(dest chan<- diffIDResult, layerStream io.ReadCloser, decompressor decompressorFunc) { func diffIDComputationGoroutine(dest chan<- diffIDResult, layerStream io.ReadCloser, decompressor compression.DecompressorFunc) {
result := diffIDResult{ result := diffIDResult{
digest: "", digest: "",
err: errors.New("Internal error: unexpected panic in diffIDComputationGoroutine"), err: errors.New("Internal error: unexpected panic in diffIDComputationGoroutine"),
@ -411,7 +442,7 @@ func diffIDComputationGoroutine(dest chan<- diffIDResult, layerStream io.ReadClo
} }
// computeDiffID reads all input from layerStream, uncompresses it using decompressor if necessary, and returns its digest. // computeDiffID reads all input from layerStream, uncompresses it using decompressor if necessary, and returns its digest.
func computeDiffID(stream io.Reader, decompressor decompressorFunc) (digest.Digest, error) { func computeDiffID(stream io.Reader, decompressor compression.DecompressorFunc) (digest.Digest, error) {
if decompressor != nil { if decompressor != nil {
s, err := decompressor(stream) s, err := decompressor(stream)
if err != nil { if err != nil {
@ -428,7 +459,7 @@ func computeDiffID(stream io.Reader, decompressor decompressorFunc) (digest.Dige
// perhaps compressing it if canCompress, // perhaps compressing it if canCompress,
// and returns a complete blobInfo of the copied blob. // and returns a complete blobInfo of the copied blob.
func (ic *imageCopier) copyBlobFromStream(srcStream io.Reader, srcInfo types.BlobInfo, func (ic *imageCopier) copyBlobFromStream(srcStream io.Reader, srcInfo types.BlobInfo,
getOriginalLayerCopyWriter func(decompressor decompressorFunc) io.Writer, getOriginalLayerCopyWriter func(decompressor compression.DecompressorFunc) io.Writer,
canCompress bool) (types.BlobInfo, error) { canCompress bool) (types.BlobInfo, error) {
// The copying happens through a pipeline of connected io.Readers. // The copying happens through a pipeline of connected io.Readers.
// === Input: srcStream // === Input: srcStream
@ -446,8 +477,8 @@ func (ic *imageCopier) copyBlobFromStream(srcStream io.Reader, srcInfo types.Blo
var destStream io.Reader = digestingReader var destStream io.Reader = digestingReader
// === Detect compression of the input stream. // === Detect compression of the input stream.
// This requires us to “peek ahead” into the stream to read the initial part, which requires us to chain through another io.Reader returned by detectCompression. // This requires us to “peek ahead” into the stream to read the initial part, which requires us to chain through another io.Reader returned by DetectCompression.
decompressor, destStream, err := detectCompression(destStream) // We could skip this in some cases, but let's keep the code path uniform decompressor, destStream, err := compression.DetectCompression(destStream) // We could skip this in some cases, but let's keep the code path uniform
if err != nil { if err != nil {
return types.BlobInfo{}, errors.Wrapf(err, "Error reading blob %s", srcInfo.Digest) return types.BlobInfo{}, errors.Wrapf(err, "Error reading blob %s", srcInfo.Digest)
} }
@ -489,6 +520,17 @@ func (ic *imageCopier) copyBlobFromStream(srcStream io.Reader, srcInfo types.Blo
inputInfo.Size = -1 inputInfo.Size = -1
} }
// === Report progress using the ic.progress channel, if required.
if ic.progress != nil && ic.progressInterval > 0 {
destStream = &progressReader{
source: destStream,
channel: ic.progress,
interval: ic.progressInterval,
artifact: srcInfo,
lastTime: time.Now(),
}
}
// === Finally, send the layer stream to dest. // === Finally, send the layer stream to dest.
uploadedInfo, err := ic.dest.PutBlob(destStream, inputInfo) uploadedInfo, err := ic.dest.PutBlob(destStream, inputInfo)
if err != nil { if err != nil {

View file

@ -9,6 +9,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/containers/image/pkg/compression"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -63,7 +64,7 @@ func TestDigestingReaderRead(t *testing.T) {
} }
} }
func goDiffIDComputationGoroutineWithTimeout(layerStream io.ReadCloser, decompressor decompressorFunc) *diffIDResult { func goDiffIDComputationGoroutineWithTimeout(layerStream io.ReadCloser, decompressor compression.DecompressorFunc) *diffIDResult {
ch := make(chan diffIDResult) ch := make(chan diffIDResult)
go diffIDComputationGoroutine(ch, layerStream, nil) go diffIDComputationGoroutine(ch, layerStream, nil)
timeout := time.After(time.Second) timeout := time.After(time.Second)
@ -94,12 +95,12 @@ func TestDiffIDComputationGoroutine(t *testing.T) {
func TestComputeDiffID(t *testing.T) { func TestComputeDiffID(t *testing.T) {
for _, c := range []struct { for _, c := range []struct {
filename string filename string
decompressor decompressorFunc decompressor compression.DecompressorFunc
result digest.Digest result digest.Digest
}{ }{
{"fixtures/Hello.uncompressed", nil, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"}, {"fixtures/Hello.uncompressed", nil, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
{"fixtures/Hello.gz", nil, "sha256:0bd4409dcd76476a263b8f3221b4ce04eb4686dec40bfdcc2e86a7403de13609"}, {"fixtures/Hello.gz", nil, "sha256:0bd4409dcd76476a263b8f3221b4ce04eb4686dec40bfdcc2e86a7403de13609"},
{"fixtures/Hello.gz", gzipDecompressor, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"}, {"fixtures/Hello.gz", compression.GzipDecompressor, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
} { } {
stream, err := os.Open(c.filename) stream, err := os.Open(c.filename)
require.NoError(t, err, c.filename) require.NoError(t, err, c.filename)
@ -111,7 +112,7 @@ func TestComputeDiffID(t *testing.T) {
} }
// Error initializing decompression // Error initializing decompression
_, err := computeDiffID(bytes.NewReader([]byte{}), gzipDecompressor) _, err := computeDiffID(bytes.NewReader([]byte{}), compression.GzipDecompressor)
assert.Error(t, err) assert.Error(t, err)
// Error reading input // Error reading input

Binary file not shown.

View file

@ -0,0 +1 @@
../../pkg/compression/fixtures/Hello.bz2

Binary file not shown.

View file

@ -0,0 +1 @@
../../pkg/compression/fixtures/Hello.gz

View file

@ -1 +0,0 @@
Hello

View file

@ -0,0 +1 @@
../../pkg/compression/fixtures/Hello.uncompressed

Binary file not shown.

View file

@ -0,0 +1 @@
../../pkg/compression/fixtures/Hello.xz

View file

@ -0,0 +1,28 @@
package copy
import (
"io"
"time"
"github.com/containers/image/types"
)
// progressReader is a reader that reports its progress on an interval.
type progressReader struct {
source io.Reader
channel chan types.ProgressProperties
interval time.Duration
artifact types.BlobInfo
lastTime time.Time
offset uint64
}
func (r *progressReader) Read(p []byte) (int, error) {
n, err := r.source.Read(p)
r.offset += uint64(n)
if time.Since(r.lastTime) > r.interval {
r.channel <- types.ProgressProperties{Artifact: r.artifact, Offset: r.offset}
r.lastTime = time.Now()
}
return n, err
}

View file

@ -26,7 +26,8 @@ func (d *dirImageDestination) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageDestination, if any. // Close removes resources associated with an initialized ImageDestination, if any.
func (d *dirImageDestination) Close() { func (d *dirImageDestination) Close() error {
return nil
} }
func (d *dirImageDestination) SupportedManifestMIMETypes() []string { func (d *dirImageDestination) SupportedManifestMIMETypes() []string {

View file

@ -28,7 +28,8 @@ func (s *dirImageSource) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageSource, if any. // Close removes resources associated with an initialized ImageSource, if any.
func (s *dirImageSource) Close() { func (s *dirImageSource) Close() error {
return nil
} }
// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).

View file

@ -10,10 +10,15 @@ import (
"github.com/containers/image/directory/explicitfilepath" "github.com/containers/image/directory/explicitfilepath"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/image" "github.com/containers/image/image"
"github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
func init() {
transports.Register(Transport)
}
// Transport is an ImageTransport for directory paths. // Transport is an ImageTransport for directory paths.
var Transport = dirTransport{} var Transport = dirTransport{}

View file

@ -1,29 +1,29 @@
// Package image provides libraries and commands to interact with containers images. // Package image provides libraries and commands to interact with containers images.
// //
// package main // package main
// //
// import ( // import (
// "fmt" // "fmt"
// //
// "github.com/containers/image/docker" // "github.com/containers/image/docker"
// ) // )
// //
// func main() { // func main() {
// ref, err := docker.ParseReference("fedora") // ref, err := docker.ParseReference("//fedora")
// if err != nil { // if err != nil {
// panic(err) // panic(err)
// } // }
// img, err := ref.NewImage(nil) // img, err := ref.NewImage(nil)
// if err != nil { // if err != nil {
// panic(err) // panic(err)
// } // }
// defer img.Close() // defer img.Close()
// b, _, err := img.Manifest() // b, _, err := img.Manifest()
// if err != nil { // if err != nil {
// panic(err) // panic(err)
// } // }
// fmt.Printf("%s", string(b)) // fmt.Printf("%s", string(b))
// } // }
// //
// TODO(runcom) // TODO(runcom)
package image package image

View file

@ -91,7 +91,7 @@ func imageLoadGoroutine(ctx context.Context, c *client.Client, reader *io.PipeRe
} }
// Close removes resources associated with an initialized ImageDestination, if any. // Close removes resources associated with an initialized ImageDestination, if any.
func (d *daemonImageDestination) Close() { func (d *daemonImageDestination) Close() error {
if !d.committed { if !d.committed {
logrus.Debugf("docker-daemon: Closing tar stream to abort loading") logrus.Debugf("docker-daemon: Closing tar stream to abort loading")
// In principle, goroutineCancel() should abort the HTTP request and stop the process from continuing. // In principle, goroutineCancel() should abort the HTTP request and stop the process from continuing.
@ -107,10 +107,10 @@ func (d *daemonImageDestination) Close() {
d.writer.CloseWithError(errors.New("Aborting upload, daemonImageDestination closed without a previous .Commit()")) d.writer.CloseWithError(errors.New("Aborting upload, daemonImageDestination closed without a previous .Commit()"))
} }
d.goroutineCancel() d.goroutineCancel()
return nil
} }
// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent,
// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects.
func (d *daemonImageDestination) Reference() types.ImageReference { func (d *daemonImageDestination) Reference() types.ImageReference {
return d.ref return d.ref
} }
@ -230,7 +230,7 @@ func (d *daemonImageDestination) PutManifest(m []byte) error {
// a hostname-qualified reference. // a hostname-qualified reference.
// See https://github.com/containers/image/issues/72 for a more detailed // See https://github.com/containers/image/issues/72 for a more detailed
// analysis and explanation. // analysis and explanation.
refString := fmt.Sprintf("%s:%s", d.namedTaggedRef.FullName(), d.namedTaggedRef.Tag()) refString := fmt.Sprintf("%s:%s", d.namedTaggedRef.Name(), d.namedTaggedRef.Tag())
items := []manifestItem{{ items := []manifestItem{{
Config: man.Config.Digest.String(), Config: man.Config.Digest.String(),

View file

@ -10,6 +10,7 @@ import (
"path" "path"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/pkg/compression"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
@ -91,8 +92,8 @@ func (s *daemonImageSource) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageSource, if any. // Close removes resources associated with an initialized ImageSource, if any.
func (s *daemonImageSource) Close() { func (s *daemonImageSource) Close() error {
_ = os.Remove(s.tarCopyPath) return os.Remove(s.tarCopyPath)
} }
// tarReadCloser is a way to close the backing file of a tar.Reader when the user no longer needs the tar component. // tarReadCloser is a way to close the backing file of a tar.Reader when the user no longer needs the tar component.
@ -334,6 +335,18 @@ func (s *daemonImageSource) GetTargetManifest(digest digest.Digest) ([]byte, str
return nil, "", errors.Errorf(`Manifest lists are not supported by "docker-daemon:"`) return nil, "", errors.Errorf(`Manifest lists are not supported by "docker-daemon:"`)
} }
type readCloseWrapper struct {
io.Reader
closeFunc func() error
}
func (r readCloseWrapper) Close() error {
if r.closeFunc != nil {
return r.closeFunc()
}
return nil
}
// GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown). // GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown).
func (s *daemonImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, error) { func (s *daemonImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, error) {
if err := s.ensureCachedDataIsPresent(); err != nil { if err := s.ensureCachedDataIsPresent(); err != nil {
@ -349,7 +362,37 @@ func (s *daemonImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64,
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
return stream, li.size, nil
// In order to handle the fact that digests != diffIDs (and thus that a
// caller which is trying to verify the blob will run into problems),
// we need to decompress blobs. This is a bit ugly, but it's a
// consequence of making everything addressable by their DiffID rather
// than by their digest...
//
// In particular, because the v2s2 manifest being generated uses
// DiffIDs, any caller of GetBlob is going to be asking for DiffIDs of
// layers not their _actual_ digest. The result is that copy/... will
// be verifing a "digest" which is not the actual layer's digest (but
// is instead the DiffID).
decompressFunc, reader, err := compression.DetectCompression(stream)
if err != nil {
return nil, 0, errors.Wrapf(err, "Detecting compression in blob %s", info.Digest)
}
if decompressFunc != nil {
reader, err = decompressFunc(reader)
if err != nil {
return nil, 0, errors.Wrapf(err, "Decompressing blob %s stream", info.Digest)
}
}
newStream := readCloseWrapper{
Reader: reader,
closeFunc: stream.Close,
}
return newStream, li.size, nil
} }
return nil, 0, errors.Errorf("Unknown blob %s", info.Digest) return nil, 0, errors.Errorf("Unknown blob %s", info.Digest)

View file

@ -5,10 +5,15 @@ import (
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/image" "github.com/containers/image/image"
"github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
func init() {
transports.Register(Transport)
}
// Transport is an ImageTransport for images managed by a local Docker daemon. // Transport is an ImageTransport for images managed by a local Docker daemon.
var Transport = daemonTransport{} var Transport = daemonTransport{}
@ -46,11 +51,11 @@ type daemonReference struct {
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. // ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference.
func ParseReference(refString string) (types.ImageReference, error) { func ParseReference(refString string) (types.ImageReference, error) {
// This is intended to be compatible with reference.ParseIDOrReference, but more strict about refusing some of the ambiguous cases. // This is intended to be compatible with reference.ParseAnyReference, but more strict about refusing some of the ambiguous cases.
// In particular, this rejects unprefixed digest values (64 hex chars), and sha256 digest prefixes (sha256:fewer-than-64-hex-chars). // In particular, this rejects unprefixed digest values (64 hex chars), and sha256 digest prefixes (sha256:fewer-than-64-hex-chars).
// digest:hexstring is structurally the same as a reponame:tag (meaning docker.io/library/reponame:tag). // digest:hexstring is structurally the same as a reponame:tag (meaning docker.io/library/reponame:tag).
// reference.ParseIDOrReference interprets such strings as digests. // reference.ParseAnyReference interprets such strings as digests.
if dgst, err := digest.Parse(refString); err == nil { if dgst, err := digest.Parse(refString); err == nil {
// The daemon explicitly refuses to tag images with a reponame equal to digest.Canonical - but _only_ this digest name. // The daemon explicitly refuses to tag images with a reponame equal to digest.Canonical - but _only_ this digest name.
// Other digest references are ambiguous, so refuse them. // Other digest references are ambiguous, so refuse them.
@ -60,11 +65,11 @@ func ParseReference(refString string) (types.ImageReference, error) {
return NewReference(dgst, nil) return NewReference(dgst, nil)
} }
ref, err := reference.ParseNamed(refString) // This also rejects unprefixed digest values ref, err := reference.ParseNormalizedNamed(refString) // This also rejects unprefixed digest values
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ref.Name() == digest.Canonical.String() { if reference.FamiliarName(ref) == digest.Canonical.String() {
return nil, errors.Errorf("Invalid docker-daemon: reference %s: The %s repository name is reserved for (non-shortened) digest references", refString, digest.Canonical) return nil, errors.Errorf("Invalid docker-daemon: reference %s: The %s repository name is reserved for (non-shortened) digest references", refString, digest.Canonical)
} }
return NewReference("", ref) return NewReference("", ref)
@ -77,10 +82,11 @@ func NewReference(id digest.Digest, ref reference.Named) (types.ImageReference,
} }
if ref != nil { if ref != nil {
if reference.IsNameOnly(ref) { if reference.IsNameOnly(ref) {
return nil, errors.Errorf("docker-daemon: reference %s has neither a tag nor a digest", ref.String()) return nil, errors.Errorf("docker-daemon: reference %s has neither a tag nor a digest", reference.FamiliarString(ref))
} }
// A github.com/distribution/reference value can have a tag and a digest at the same time! // A github.com/distribution/reference value can have a tag and a digest at the same time!
// docker/reference does not handle that, so fail. // Most versions of docker/reference do not handle that (ignoring the tag), so reject such input.
// This MAY be accepted in the future.
_, isTagged := ref.(reference.NamedTagged) _, isTagged := ref.(reference.NamedTagged)
_, isDigested := ref.(reference.Canonical) _, isDigested := ref.(reference.Canonical)
if isTagged && isDigested { if isTagged && isDigested {
@ -108,7 +114,7 @@ func (ref daemonReference) StringWithinTransport() string {
case ref.id != "": case ref.id != "":
return ref.id.String() return ref.id.String()
case ref.ref != nil: case ref.ref != nil:
return ref.ref.String() return reference.FamiliarString(ref.ref)
default: // Coverage: Should never happen, NewReference above should refuse such values. default: // Coverage: Should never happen, NewReference above should refuse such values.
panic("Internal inconsistency: daemonReference has empty id and nil ref") panic("Internal inconsistency: daemonReference has empty id and nil ref")
} }

View file

@ -50,15 +50,12 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err
{"sha256:XX23456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "", ""}, // Invalid digest value {"sha256:XX23456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "", ""}, // Invalid digest value
{"UPPERCASEISINVALID", "", ""}, // Invalid reference input {"UPPERCASEISINVALID", "", ""}, // Invalid reference input
{"busybox", "", ""}, // Missing tag or digest {"busybox", "", ""}, // Missing tag or digest
{"busybox:latest", "", "busybox:latest"}, // Explicit tag {"busybox:latest", "", "docker.io/library/busybox:latest"}, // Explicit tag
{"busybox@" + sha256digest, "", "busybox@" + sha256digest}, // Explicit digest {"busybox@" + sha256digest, "", "docker.io/library/busybox@" + sha256digest}, // Explicit digest
// A github.com/distribution/reference value can have a tag and a digest at the same time! // A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/docker/reference handles that by dropping the tag. That is not obviously the // Most versions of docker/reference do not handle that (ignoring the tag), so we reject such input.
// right thing to do, but it is at least reasonable, so test that we keep behaving reasonably. {"busybox:latest@" + sha256digest, "", ""}, // Both tag and digest
// This test case should not be construed to make this an API promise. {"docker.io/library/busybox:latest", "", "docker.io/library/busybox:latest"}, // All implied values explicitly specified
// FIXME? Instead work extra hard to reject such input?
{"busybox:latest@" + sha256digest, "", "busybox@" + sha256digest}, // Both tag and digest
{"docker.io/library/busybox:latest", "", "busybox:latest"}, // All implied values explicitly specified
} { } {
ref, err := fn(c.input) ref, err := fn(c.input)
if c.expectedID == "" && c.expectedRef == "" { if c.expectedID == "" && c.expectedRef == "" {
@ -67,43 +64,37 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err
require.NoError(t, err, c.input) require.NoError(t, err, c.input)
daemonRef, ok := ref.(daemonReference) daemonRef, ok := ref.(daemonReference)
require.True(t, ok, c.input) require.True(t, ok, c.input)
// If we don't reject the input, the interpretation must be consistent for reference.ParseIDOrReference // If we don't reject the input, the interpretation must be consistent with reference.ParseAnyReference
dockerID, dockerRef, err := reference.ParseIDOrReference(c.input) dockerRef, err := reference.ParseAnyReference(c.input)
require.NoError(t, err, c.input) require.NoError(t, err, c.input)
if c.expectedRef == "" { if c.expectedRef == "" {
assert.Equal(t, c.expectedID, daemonRef.id.String(), c.input) assert.Equal(t, c.expectedID, daemonRef.id.String(), c.input)
assert.Nil(t, daemonRef.ref, c.input) assert.Nil(t, daemonRef.ref, c.input)
assert.Equal(t, c.expectedID, dockerID.String(), c.input) _, ok := dockerRef.(reference.Digested)
assert.Nil(t, dockerRef, c.input) require.True(t, ok, c.input)
assert.Equal(t, c.expectedID, dockerRef.String(), c.input)
} else { } else {
assert.Equal(t, "", daemonRef.id.String(), c.input) assert.Equal(t, "", daemonRef.id.String(), c.input)
require.NotNil(t, daemonRef.ref, c.input) require.NotNil(t, daemonRef.ref, c.input)
assert.Equal(t, c.expectedRef, daemonRef.ref.String(), c.input) assert.Equal(t, c.expectedRef, daemonRef.ref.String(), c.input)
assert.Equal(t, "", dockerID.String(), c.input) _, ok := dockerRef.(reference.Named)
require.NotNil(t, dockerRef, c.input) require.True(t, ok, c.input)
assert.Equal(t, c.expectedRef, dockerRef.String(), c.input) assert.Equal(t, c.expectedRef, dockerRef.String(), c.input)
} }
} }
} }
} }
// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time.
type refWithTagAndDigest struct{ reference.Canonical }
func (ref refWithTagAndDigest) Tag() string {
return "notLatest"
}
// A common list of reference formats to test for the various ImageReference methods. // A common list of reference formats to test for the various ImageReference methods.
// (For IDs it is much simpler, we simply use them unmodified) // (For IDs it is much simpler, we simply use them unmodified)
var validNamedReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{ var validNamedReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{
{"busybox:notlatest", "busybox:notlatest", "busybox:notlatest"}, // Explicit tag {"busybox:notlatest", "docker.io/library/busybox:notlatest", "busybox:notlatest"}, // Explicit tag
{"busybox" + sha256digest, "busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest {"busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest
{"docker.io/library/busybox:latest", "busybox:latest", "busybox:latest"}, // All implied values explicitly specified {"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "busybox:latest"}, // All implied values explicitly specified
{"example.com/ns/foo:bar", "example.com/ns/foo:bar", "example.com/ns/foo:bar"}, // All values explicitly specified {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "example.com/ns/foo:bar"}, // All values explicitly specified
} }
func TestNewReference(t *testing.T) { func TestNewReference(t *testing.T) {
@ -119,7 +110,7 @@ func TestNewReference(t *testing.T) {
// Named references // Named references
for _, c := range validNamedReferenceTestCases { for _, c := range validNamedReferenceTestCases {
parsed, err := reference.ParseNamed(c.input) parsed, err := reference.ParseNormalizedNamed(c.input)
require.NoError(t, err) require.NoError(t, err)
ref, err := NewReference("", parsed) ref, err := NewReference("", parsed)
require.NoError(t, err, c.input) require.NoError(t, err, c.input)
@ -131,24 +122,25 @@ func TestNewReference(t *testing.T) {
} }
// Both an ID and a named reference provided // Both an ID and a named reference provided
parsed, err := reference.ParseNamed("busybox:latest") parsed, err := reference.ParseNormalizedNamed("busybox:latest")
require.NoError(t, err) require.NoError(t, err)
_, err = NewReference(id, parsed) _, err = NewReference(id, parsed)
assert.Error(t, err) assert.Error(t, err)
// A reference with neither a tag nor digest // A reference with neither a tag nor digest
parsed, err = reference.ParseNamed("busybox") parsed, err = reference.ParseNormalizedNamed("busybox")
require.NoError(t, err) require.NoError(t, err)
_, err = NewReference("", parsed) _, err = NewReference("", parsed)
assert.Error(t, err) assert.Error(t, err)
// A github.com/distribution/reference value can have a tag and a digest at the same time! // A github.com/distribution/reference value can have a tag and a digest at the same time!
parsed, err = reference.ParseNamed("busybox@" + sha256digest) parsed, err = reference.ParseNormalizedNamed("busybox:notlatest@" + sha256digest)
require.NoError(t, err) require.NoError(t, err)
refDigested, ok := parsed.(reference.Canonical) _, ok = parsed.(reference.Canonical)
require.True(t, ok) require.True(t, ok)
tagDigestRef := refWithTagAndDigest{refDigested} _, ok = parsed.(reference.NamedTagged)
_, err = NewReference("", tagDigestRef) require.True(t, ok)
_, err = NewReference("", parsed)
assert.Error(t, err) assert.Error(t, err)
} }

View file

@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/docker/reference"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/homedir"
"github.com/docker/go-connections/sockets" "github.com/docker/go-connections/sockets"
@ -164,11 +165,11 @@ func hasFile(files []os.FileInfo, name string) bool {
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry) // newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection) // “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) { func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
registry := ref.ref.Hostname() registry := reference.Domain(ref.ref)
if registry == dockerHostname { if registry == dockerHostname {
registry = dockerRegistry registry = dockerRegistry
} }
username, password, err := getAuth(ctx, ref.ref.Hostname()) username, password, err := getAuth(ctx, reference.Domain(ref.ref))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -202,7 +203,7 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool,
signatureBase: sigBase, signatureBase: sigBase,
scope: authScope{ scope: authScope{
actions: actions, actions: actions,
remoteName: ref.ref.RemoteName(), remoteName: reference.Path(ref.ref),
}, },
}, nil }, nil
} }

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/containers/image/docker/reference"
"github.com/containers/image/image" "github.com/containers/image/image"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -34,12 +35,12 @@ func newImage(ctx *types.SystemContext, ref dockerReference) (types.Image, error
// SourceRefFullName returns a fully expanded name for the repository this image is in. // SourceRefFullName returns a fully expanded name for the repository this image is in.
func (i *Image) SourceRefFullName() string { func (i *Image) SourceRefFullName() string {
return i.src.ref.ref.FullName() return i.src.ref.ref.Name()
} }
// GetRepositoryTags list all tags available in the repository. Note that this has no connection with the tag(s) used for this specific image, if any. // GetRepositoryTags list all tags available in the repository. Note that this has no connection with the tag(s) used for this specific image, if any.
func (i *Image) GetRepositoryTags() ([]string, error) { func (i *Image) GetRepositoryTags() ([]string, error) {
url := fmt.Sprintf(tagsURL, i.src.ref.ref.RemoteName()) url := fmt.Sprintf(tagsURL, reference.Path(i.src.ref.ref))
res, err := i.src.c.makeRequest("GET", url, nil, nil) res, err := i.src.c.makeRequest("GET", url, nil, nil)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
@ -58,7 +59,8 @@ func (d *dockerImageDestination) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageDestination, if any. // Close removes resources associated with an initialized ImageDestination, if any.
func (d *dockerImageDestination) Close() { func (d *dockerImageDestination) Close() error {
return nil
} }
func (d *dockerImageDestination) SupportedManifestMIMETypes() []string { func (d *dockerImageDestination) SupportedManifestMIMETypes() []string {
@ -98,31 +100,18 @@ func (c *sizeCounter) Write(p []byte) (n int, err error) {
// 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. // 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 (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) { func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) {
if inputInfo.Digest.String() != "" { if inputInfo.Digest.String() != "" {
checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), inputInfo.Digest.String()) haveBlob, size, err := d.HasBlob(inputInfo)
if err != nil && err != types.ErrBlobNotFound {
logrus.Debugf("Checking %s", checkURL)
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
if err != nil {
return types.BlobInfo{}, err return types.BlobInfo{}, err
} }
defer res.Body.Close() // Now err == nil || err == types.ErrBlobNotFound
switch res.StatusCode { if err == nil && haveBlob {
case http.StatusOK: return types.BlobInfo{Digest: inputInfo.Digest, Size: size}, nil
logrus.Debugf("... already exists, not uploading")
return types.BlobInfo{Digest: inputInfo.Digest, Size: getBlobSize(res)}, nil
case http.StatusUnauthorized:
logrus.Debugf("... not authorized")
return types.BlobInfo{}, errors.Errorf("not authorized to read from destination repository %s", d.ref.ref.RemoteName())
case http.StatusNotFound:
// noop
default:
return types.BlobInfo{}, errors.Errorf("failed to read from destination repository %s: %v", d.ref.ref.RemoteName(), http.StatusText(res.StatusCode))
} }
logrus.Debugf("... failed, status %d", res.StatusCode)
} }
// FIXME? Chunked upload, progress reporting, etc. // FIXME? Chunked upload, progress reporting, etc.
uploadURL := fmt.Sprintf(blobUploadURL, d.ref.ref.RemoteName()) uploadURL := fmt.Sprintf(blobUploadURL, reference.Path(d.ref.ref))
logrus.Debugf("Uploading %s", uploadURL) logrus.Debugf("Uploading %s", uploadURL)
res, err := d.c.makeRequest("POST", uploadURL, nil, nil) res, err := d.c.makeRequest("POST", uploadURL, nil, nil)
if err != nil { if err != nil {
@ -178,7 +167,7 @@ func (d *dockerImageDestination) HasBlob(info types.BlobInfo) (bool, int64, erro
if info.Digest == "" { if info.Digest == "" {
return false, -1, errors.Errorf(`"Can not check for a blob with unknown digest`) return false, -1, errors.Errorf(`"Can not check for a blob with unknown digest`)
} }
checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), info.Digest.String()) checkURL := fmt.Sprintf(blobsURL, reference.Path(d.ref.ref), info.Digest.String())
logrus.Debugf("Checking %s", checkURL) logrus.Debugf("Checking %s", checkURL)
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil) res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
@ -192,15 +181,13 @@ func (d *dockerImageDestination) HasBlob(info types.BlobInfo) (bool, int64, erro
return true, getBlobSize(res), nil return true, getBlobSize(res), nil
case http.StatusUnauthorized: case http.StatusUnauthorized:
logrus.Debugf("... not authorized") logrus.Debugf("... not authorized")
return false, -1, errors.Errorf("not authorized to read from destination repository %s", d.ref.ref.RemoteName()) return false, -1, errors.Errorf("not authorized to read from destination repository %s", reference.Path(d.ref.ref))
case http.StatusNotFound: case http.StatusNotFound:
logrus.Debugf("... not present") logrus.Debugf("... not present")
return false, -1, types.ErrBlobNotFound return false, -1, types.ErrBlobNotFound
default: default:
logrus.Errorf("failed to read from destination repository %s: %v", d.ref.ref.RemoteName(), http.StatusText(res.StatusCode)) return false, -1, errors.Errorf("failed to read from destination repository %s: %v", reference.Path(d.ref.ref), http.StatusText(res.StatusCode))
} }
logrus.Debugf("... failed, status %d, ignoring", res.StatusCode)
return false, -1, types.ErrBlobNotFound
} }
func (d *dockerImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo, error) { func (d *dockerImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo, error) {
@ -214,11 +201,11 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
} }
d.manifestDigest = digest d.manifestDigest = digest
reference, err := d.ref.tagOrDigest() refTail, err := d.ref.tagOrDigest()
if err != nil { if err != nil {
return err return err
} }
url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), reference) url := fmt.Sprintf(manifestURL, reference.Path(d.ref.ref), refTail)
headers := map[string][]string{} headers := map[string][]string{}
mimeType := manifest.GuessMIMEType(m) mimeType := manifest.GuessMIMEType(m)

View file

@ -11,6 +11,7 @@ import (
"strconv" "strconv"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/docker/distribution/registry/client" "github.com/docker/distribution/registry/client"
@ -64,7 +65,8 @@ func (s *dockerImageSource) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageSource, if any. // Close removes resources associated with an initialized ImageSource, if any.
func (s *dockerImageSource) Close() { func (s *dockerImageSource) Close() error {
return nil
} }
// simplifyContentType drops parameters from a HTTP media type (see https://tools.ietf.org/html/rfc7231#section-3.1.1.1) // simplifyContentType drops parameters from a HTTP media type (see https://tools.ietf.org/html/rfc7231#section-3.1.1.1)
@ -91,7 +93,7 @@ func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
} }
func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) { func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) {
url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), tagOrDigest) url := fmt.Sprintf(manifestURL, reference.Path(s.ref.ref), tagOrDigest)
headers := make(map[string][]string) headers := make(map[string][]string)
headers["Accept"] = s.requestedManifestMIMETypes headers["Accept"] = s.requestedManifestMIMETypes
res, err := s.c.makeRequest("GET", url, headers, nil) res, err := s.c.makeRequest("GET", url, headers, nil)
@ -177,7 +179,7 @@ func (s *dockerImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64,
return s.getExternalBlob(info.URLs) return s.getExternalBlob(info.URLs)
} }
url := fmt.Sprintf(blobsURL, s.ref.ref.RemoteName(), info.Digest.String()) url := fmt.Sprintf(blobsURL, reference.Path(s.ref.ref), info.Digest.String())
logrus.Debugf("Downloading %s", url) logrus.Debugf("Downloading %s", url)
res, err := s.c.makeRequest("GET", url, nil, nil) res, err := s.c.makeRequest("GET", url, nil, nil)
if err != nil { if err != nil {
@ -271,11 +273,11 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
headers := make(map[string][]string) headers := make(map[string][]string)
headers["Accept"] = []string{manifest.DockerV2Schema2MediaType} headers["Accept"] = []string{manifest.DockerV2Schema2MediaType}
reference, err := ref.tagOrDigest() refTail, err := ref.tagOrDigest()
if err != nil { if err != nil {
return err return err
} }
getURL := fmt.Sprintf(manifestURL, ref.ref.RemoteName(), reference) getURL := fmt.Sprintf(manifestURL, reference.Path(ref.ref), refTail)
get, err := c.makeRequest("GET", getURL, headers, nil) get, err := c.makeRequest("GET", getURL, headers, nil)
if err != nil { if err != nil {
return err return err
@ -294,7 +296,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
} }
digest := get.Header.Get("Docker-Content-Digest") digest := get.Header.Get("Docker-Content-Digest")
deleteURL := fmt.Sprintf(manifestURL, ref.ref.RemoteName(), digest) deleteURL := fmt.Sprintf(manifestURL, reference.Path(ref.ref), digest)
// When retrieving the digest from a registry >= 2.3 use the following header: // When retrieving the digest from a registry >= 2.3 use the following header:
// "Accept": "application/vnd.docker.distribution.manifest.v2+json" // "Accept": "application/vnd.docker.distribution.manifest.v2+json"

View file

@ -6,10 +6,15 @@ import (
"github.com/containers/image/docker/policyconfiguration" "github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func init() {
transports.Register(Transport)
}
// Transport is an ImageTransport for Docker registry-hosted images. // Transport is an ImageTransport for Docker registry-hosted images.
var Transport = dockerTransport{} var Transport = dockerTransport{}
@ -45,21 +50,22 @@ func ParseReference(refString string) (types.ImageReference, error) {
if !strings.HasPrefix(refString, "//") { if !strings.HasPrefix(refString, "//") {
return nil, errors.Errorf("docker: image reference %s does not start with //", refString) return nil, errors.Errorf("docker: image reference %s does not start with //", refString)
} }
ref, err := reference.ParseNamed(strings.TrimPrefix(refString, "//")) ref, err := reference.ParseNormalizedNamed(strings.TrimPrefix(refString, "//"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
ref = reference.WithDefaultTag(ref) ref = reference.TagNameOnly(ref)
return NewReference(ref) return NewReference(ref)
} }
// NewReference returns a Docker reference for a named reference. The reference must satisfy !reference.IsNameOnly(). // NewReference returns a Docker reference for a named reference. The reference must satisfy !reference.IsNameOnly().
func NewReference(ref reference.Named) (types.ImageReference, error) { func NewReference(ref reference.Named) (types.ImageReference, error) {
if reference.IsNameOnly(ref) { if reference.IsNameOnly(ref) {
return nil, errors.Errorf("Docker reference %s has neither a tag nor a digest", ref.String()) return nil, errors.Errorf("Docker reference %s has neither a tag nor a digest", reference.FamiliarString(ref))
} }
// A github.com/distribution/reference value can have a tag and a digest at the same time! // A github.com/distribution/reference value can have a tag and a digest at the same time!
// docker/reference does not handle that, so fail. // The docker/distribution API does not really support that (we cant ask for an image with a specific
// tag and digest), so fail. This MAY be accepted in the future.
// (Even if it were supported, the semantics of policy namespaces are unclear - should we drop // (Even if it were supported, the semantics of policy namespaces are unclear - should we drop
// the tag or the digest first?) // the tag or the digest first?)
_, isTagged := ref.(reference.NamedTagged) _, isTagged := ref.(reference.NamedTagged)
@ -82,7 +88,7 @@ func (ref dockerReference) Transport() types.ImageTransport {
// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. // e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa.
// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
func (ref dockerReference) StringWithinTransport() string { func (ref dockerReference) StringWithinTransport() string {
return "//" + ref.ref.String() return "//" + reference.FamiliarString(ref.ref)
} }
// DockerReference returns a Docker reference associated with this reference // DockerReference returns a Docker reference associated with this reference
@ -152,5 +158,5 @@ func (ref dockerReference) tagOrDigest() (string, error) {
return ref.Tag(), nil return ref.Tag(), nil
} }
// This should not happen, NewReference above refuses reference.IsNameOnly values. // This should not happen, NewReference above refuses reference.IsNameOnly values.
return "", errors.Errorf("Internal inconsistency: Reference %s unexpectedly has neither a digest nor a tag", ref.ref.String()) return "", errors.Errorf("Internal inconsistency: Reference %s unexpectedly has neither a digest nor a tag", reference.FamiliarString(ref.ref))
} }

View file

@ -42,18 +42,16 @@ func TestParseReference(t *testing.T) {
// testParseReference is a test shared for Transport.ParseReference and ParseReference. // testParseReference is a test shared for Transport.ParseReference and ParseReference.
func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) {
for _, c := range []struct{ input, expected string }{ for _, c := range []struct{ input, expected string }{
{"busybox", ""}, // Missing // prefix {"busybox", ""}, // Missing // prefix
{"//busybox:notlatest", "busybox:notlatest"}, // Explicit tag {"//busybox:notlatest", "docker.io/library/busybox:notlatest"}, // Explicit tag
{"//busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest {"//busybox" + sha256digest, "docker.io/library/busybox" + sha256digest}, // Explicit digest
{"//busybox", "busybox:latest"}, // Default tag {"//busybox", "docker.io/library/busybox:latest"}, // Default tag
// A github.com/distribution/reference value can have a tag and a digest at the same time! // A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/docker/reference handles that by dropping the tag. That is not obviously the // The docker/distribution API does not really support that (we cant ask for an image with a specific
// right thing to do, but it is at least reasonable, so test that we keep behaving reasonably. // tag and digest), so fail. This MAY be accepted in the future.
// This test case should not be construed to make this an API promise. {"//busybox:latest" + sha256digest, ""}, // Both tag and digest
// FIXME? Instead work extra hard to reject such input? {"//docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}, // All implied values explicitly specified
{"//busybox:latest" + sha256digest, "busybox" + sha256digest}, // Both tag and digest {"//UPPERCASEISINVALID", ""}, // Invalid input
{"//docker.io/library/busybox:latest", "busybox:latest"}, // All implied values explicitly specified
{"//UPPERCASEISINVALID", ""}, // Invalid input
} { } {
ref, err := fn(c.input) ref, err := fn(c.input)
if c.expected == "" { if c.expected == "" {
@ -67,24 +65,17 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err
} }
} }
// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time.
type refWithTagAndDigest struct{ reference.Canonical }
func (ref refWithTagAndDigest) Tag() string {
return "notLatest"
}
// A common list of reference formats to test for the various ImageReference methods. // A common list of reference formats to test for the various ImageReference methods.
var validReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{ var validReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{
{"busybox:notlatest", "busybox:notlatest", "//busybox:notlatest"}, // Explicit tag {"busybox:notlatest", "docker.io/library/busybox:notlatest", "//busybox:notlatest"}, // Explicit tag
{"busybox" + sha256digest, "busybox" + sha256digest, "//busybox" + sha256digest}, // Explicit digest {"busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, "//busybox" + sha256digest}, // Explicit digest
{"docker.io/library/busybox:latest", "busybox:latest", "//busybox:latest"}, // All implied values explicitly specified {"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "//busybox:latest"}, // All implied values explicitly specified
{"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar"}, // All values explicitly specified {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar"}, // All values explicitly specified
} }
func TestNewReference(t *testing.T) { func TestNewReference(t *testing.T) {
for _, c := range validReferenceTestCases { for _, c := range validReferenceTestCases {
parsed, err := reference.ParseNamed(c.input) parsed, err := reference.ParseNormalizedNamed(c.input)
require.NoError(t, err) require.NoError(t, err)
ref, err := NewReference(parsed) ref, err := NewReference(parsed)
require.NoError(t, err, c.input) require.NoError(t, err, c.input)
@ -94,18 +85,19 @@ func TestNewReference(t *testing.T) {
} }
// Neither a tag nor digest // Neither a tag nor digest
parsed, err := reference.ParseNamed("busybox") parsed, err := reference.ParseNormalizedNamed("busybox")
require.NoError(t, err) require.NoError(t, err)
_, err = NewReference(parsed) _, err = NewReference(parsed)
assert.Error(t, err) assert.Error(t, err)
// A github.com/distribution/reference value can have a tag and a digest at the same time! // A github.com/distribution/reference value can have a tag and a digest at the same time!
parsed, err = reference.ParseNamed("busybox" + sha256digest) parsed, err = reference.ParseNormalizedNamed("busybox:notlatest" + sha256digest)
require.NoError(t, err) require.NoError(t, err)
refDigested, ok := parsed.(reference.Canonical) _, ok := parsed.(reference.Canonical)
require.True(t, ok) require.True(t, ok)
tagDigestRef := refWithTagAndDigest{refDigested} _, ok = parsed.(reference.NamedTagged)
_, err = NewReference(tagDigestRef) require.True(t, ok)
_, err = NewReference(parsed)
assert.Error(t, err) assert.Error(t, err)
} }
@ -196,7 +188,7 @@ func TestReferenceTagOrDigest(t *testing.T) {
} }
// Invalid input // Invalid input
ref, err := reference.ParseNamed("busybox") ref, err := reference.ParseNormalizedNamed("busybox")
require.NoError(t, err) require.NoError(t, err)
dockerRef := dockerReference{ref: ref} dockerRef := dockerReference{ref: ref}
_, err = dockerRef.tagOrDigest() _, err = dockerRef.tagOrDigest()

View file

@ -64,7 +64,7 @@ func configuredSignatureStorageBase(ctx *types.SystemContext, ref dockerReferenc
return nil, errors.Wrapf(err, "Invalid signature storage URL %s", topLevel) return nil, errors.Wrapf(err, "Invalid signature storage URL %s", topLevel)
} }
// FIXME? Restrict to explicitly supported schemes? // FIXME? Restrict to explicitly supported schemes?
repo := ref.ref.FullName() // Note that this is without a tag or digest. repo := ref.ref.Name() // Note that this is without a tag or digest.
if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references
return nil, errors.Errorf("Unexpected path elements in Docker reference %s for signature storage", ref.ref.String()) return nil, errors.Errorf("Unexpected path elements in Docker reference %s for signature storage", ref.ref.String())
} }

View file

@ -3,23 +3,22 @@ package policyconfiguration
import ( import (
"strings" "strings"
"github.com/pkg/errors"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/pkg/errors"
) )
// DockerReferenceIdentity returns a string representation of the reference, suitable for policy lookup, // DockerReferenceIdentity returns a string representation of the reference, suitable for policy lookup,
// as a backend for ImageReference.PolicyConfigurationIdentity. // as a backend for ImageReference.PolicyConfigurationIdentity.
// The reference must satisfy !reference.IsNameOnly(). // The reference must satisfy !reference.IsNameOnly().
func DockerReferenceIdentity(ref reference.Named) (string, error) { func DockerReferenceIdentity(ref reference.Named) (string, error) {
res := ref.FullName() res := ref.Name()
tagged, isTagged := ref.(reference.NamedTagged) tagged, isTagged := ref.(reference.NamedTagged)
digested, isDigested := ref.(reference.Canonical) digested, isDigested := ref.(reference.Canonical)
switch { switch {
case isTagged && isDigested: // This should not happen, docker/reference.ParseNamed drops the tag. case isTagged && isDigested: // Note that this CAN actually happen.
return "", errors.Errorf("Unexpected Docker reference %s with both a name and a digest", ref.String()) return "", errors.Errorf("Unexpected Docker reference %s with both a name and a digest", reference.FamiliarString(ref))
case !isTagged && !isDigested: // This should not happen, the caller is expected to ensure !reference.IsNameOnly() case !isTagged && !isDigested: // This should not happen, the caller is expected to ensure !reference.IsNameOnly()
return "", errors.Errorf("Internal inconsistency: Docker reference %s with neither a tag nor a digest", ref.String()) return "", errors.Errorf("Internal inconsistency: Docker reference %s with neither a tag nor a digest", reference.FamiliarString(ref))
case isTagged: case isTagged:
res = res + ":" + tagged.Tag() res = res + ":" + tagged.Tag()
case isDigested: case isDigested:
@ -43,7 +42,7 @@ func DockerReferenceNamespaces(ref reference.Named) []string {
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last // ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
// iteration matches the host name (for any namespace). // iteration matches the host name (for any namespace).
res := []string{} res := []string{}
name := ref.FullName() name := ref.Name()
for { for {
res = append(res, name) res = append(res, name)

View file

@ -1,11 +1,10 @@
package policyconfiguration package policyconfiguration
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
"fmt"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -35,14 +34,9 @@ func TestDockerReference(t *testing.T) {
for inputSuffix, mappedSuffix := range map[string]string{ for inputSuffix, mappedSuffix := range map[string]string{
":tag": ":tag", ":tag": ":tag",
sha256Digest: sha256Digest, sha256Digest: sha256Digest,
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/docker/reference handles that by dropping the tag. That is not obviously the
// right thing to do, but it is at least reasonable, so test that we keep behaving reasonably.
// This test case should not be construed to make this an API promise.
":tag" + sha256Digest: sha256Digest,
} { } {
fullInput := inputName + inputSuffix fullInput := inputName + inputSuffix
ref, err := reference.ParseNamed(fullInput) ref, err := reference.ParseNormalizedNamed(fullInput)
require.NoError(t, err, fullInput) require.NoError(t, err, fullInput)
identity, err := DockerReferenceIdentity(ref) identity, err := DockerReferenceIdentity(ref)
@ -62,30 +56,24 @@ func TestDockerReference(t *testing.T) {
} }
} }
// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time.
type refWithTagAndDigest struct{ reference.Canonical }
func (ref refWithTagAndDigest) Tag() string {
return "notLatest"
}
func TestDockerReferenceIdentity(t *testing.T) { func TestDockerReferenceIdentity(t *testing.T) {
// TestDockerReference above has tested the core of the functionality, this tests only the failure cases. // TestDockerReference above has tested the core of the functionality, this tests only the failure cases.
// Neither a tag nor digest // Neither a tag nor digest
parsed, err := reference.ParseNamed("busybox") parsed, err := reference.ParseNormalizedNamed("busybox")
require.NoError(t, err) require.NoError(t, err)
id, err := DockerReferenceIdentity(parsed) id, err := DockerReferenceIdentity(parsed)
assert.Equal(t, "", id) assert.Equal(t, "", id)
assert.Error(t, err) assert.Error(t, err)
// A github.com/distribution/reference value can have a tag and a digest at the same time! // A github.com/distribution/reference value can have a tag and a digest at the same time!
parsed, err = reference.ParseNamed("busybox@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") parsed, err = reference.ParseNormalizedNamed("busybox:notlatest@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
require.NoError(t, err) require.NoError(t, err)
refDigested, ok := parsed.(reference.Canonical) _, ok := parsed.(reference.Canonical)
require.True(t, ok) require.True(t, ok)
tagDigestRef := refWithTagAndDigest{refDigested} _, ok = parsed.(reference.NamedTagged)
id, err = DockerReferenceIdentity(tagDigestRef) require.True(t, ok)
id, err = DockerReferenceIdentity(parsed)
assert.Equal(t, "", id) assert.Equal(t, "", id)
assert.Error(t, err) assert.Error(t, err)
} }

View file

@ -0,0 +1,2 @@
This is a copy of github.com/docker/distribution/reference as of commit fb0bebc4b64e3881cc52a2478d749845ed76d2a8,
except that ParseAnyReferenceWithSet has been removed to drop the dependency on github.com/docker/distribution/digestset.

View file

@ -1,6 +0,0 @@
// Package reference is a fork of the upstream docker/docker/reference package.
// The package is forked because we need consistency especially when storing and
// checking signatures (RH patches break this consistency because they modify
// docker/docker/reference as part of a patch carried in projectatomic/docker).
// The version of this package is v1.12.1 from upstream, update as necessary.
package reference

View file

@ -0,0 +1,42 @@
package reference
import "path"
// IsNameOnly returns true if reference only contains a repo name.
func IsNameOnly(ref Named) bool {
if _, ok := ref.(NamedTagged); ok {
return false
}
if _, ok := ref.(Canonical); ok {
return false
}
return true
}
// FamiliarName returns the familiar name string
// for the given named, familiarizing if needed.
func FamiliarName(ref Named) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().Name()
}
return ref.Name()
}
// FamiliarString returns the familiar string representation
// for the given reference, familiarizing if needed.
func FamiliarString(ref Reference) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().String()
}
return ref.String()
}
// FamiliarMatch reports whether ref matches the specified pattern.
// See https://godoc.org/path#Match for supported patterns.
func FamiliarMatch(pattern string, ref Reference) (bool, error) {
matched, err := path.Match(pattern, FamiliarString(ref))
if namedRef, isNamed := ref.(Named); isNamed && !matched {
matched, _ = path.Match(pattern, FamiliarName(namedRef))
}
return matched, err
}

View file

@ -0,0 +1,152 @@
package reference
import (
"errors"
"fmt"
"strings"
"github.com/opencontainers/go-digest"
)
var (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
officialRepoName = "library"
defaultTag = "latest"
)
// normalizedNamed represents a name which has been
// normalized and has a familiar form. A familiar name
// is what is used in Docker UI. An example normalized
// name is "docker.io/library/ubuntu" and corresponding
// familiar name of "ubuntu".
type normalizedNamed interface {
Named
Familiar() Named
}
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
func ParseNormalizedNamed(s string) (Named, error) {
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
}
domain, remainder := splitDockerDomain(s)
var remoteName string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remoteName = remainder[:tagSep]
} else {
remoteName = remainder
}
if strings.ToLower(remoteName) != remoteName {
return nil, errors.New("invalid reference format: repository name must be lowercase")
}
ref, err := Parse(domain + "/" + remainder)
if err != nil {
return nil, err
}
named, isNamed := ref.(Named)
if !isNamed {
return nil, fmt.Errorf("reference %s has no name", ref.String())
}
return named, nil
}
// splitDockerDomain splits a repository name to domain and remotename string.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
remainder = officialRepoName + "/" + remainder
}
return
}
// familiarizeName returns a shortened version of the name familiar
// to to the Docker UI. Familiar names have the default domain
// "docker.io" and "library/" repository prefix removed.
// For example, "docker.io/library/redis" will have the familiar
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
// Returns a familiarized named only reference.
func familiarizeName(named namedRepository) repository {
repo := repository{
domain: named.Domain(),
path: named.Path(),
}
if repo.domain == defaultDomain {
repo.domain = ""
// Handle official repositories which have the pattern "library/<official repo name>"
if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName {
repo.path = split[1]
}
}
return repo
}
func (r reference) Familiar() Named {
return reference{
namedRepository: familiarizeName(r.namedRepository),
tag: r.tag,
digest: r.digest,
}
}
func (r repository) Familiar() Named {
return familiarizeName(r)
}
func (t taggedReference) Familiar() Named {
return taggedReference{
namedRepository: familiarizeName(t.namedRepository),
tag: t.tag,
}
}
func (c canonicalReference) Familiar() Named {
return canonicalReference{
namedRepository: familiarizeName(c.namedRepository),
digest: c.digest,
}
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
func TagNameOnly(ref Named) Named {
if IsNameOnly(ref) {
namedTagged, err := WithTag(ref, defaultTag)
if err != nil {
// Default tag must be valid, to create a NamedTagged
// type with non-validated input the WithTag function
// should be used instead
panic(err)
}
return namedTagged
}
return ref
}
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
func ParseAnyReference(ref string) (Reference, error) {
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
return digestReference("sha256:" + ref), nil
}
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
return ParseNormalizedNamed(ref)
}

View file

@ -0,0 +1,573 @@
package reference
import (
"strconv"
"testing"
"github.com/opencontainers/go-digest"
)
func TestValidateReferenceName(t *testing.T) {
validRepoNames := []string{
"docker/docker",
"library/debian",
"debian",
"docker.io/docker/docker",
"docker.io/library/debian",
"docker.io/debian",
"index.docker.io/docker/docker",
"index.docker.io/library/debian",
"index.docker.io/debian",
"127.0.0.1:5000/docker/docker",
"127.0.0.1:5000/library/debian",
"127.0.0.1:5000/debian",
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// This test case was moved from invalid to valid since it is valid input
// when specified with a hostname, it removes the ambiguity from about
// whether the value is an identifier or repository name
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
invalidRepoNames := []string{
"https://github.com/docker/docker",
"docker/Docker",
"-docker",
"-docker/docker",
"-docker.io/docker/docker",
"docker///docker",
"docker.io/docker/Docker",
"docker.io/docker///docker",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
for _, name := range invalidRepoNames {
_, err := ParseNormalizedNamed(name)
if err == nil {
t.Fatalf("Expected invalid repo name for %q", name)
}
}
for _, name := range validRepoNames {
_, err := ParseNormalizedNamed(name)
if err != nil {
t.Fatalf("Error parsing repo name %s, got: %q", name, err)
}
}
}
func TestValidateRemoteName(t *testing.T) {
validRepositoryNames := []string{
// Sanity check.
"docker/docker",
// Allow 64-character non-hexadecimal names (hexadecimal names are forbidden).
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// Allow embedded hyphens.
"docker-rules/docker",
// Allow multiple hyphens as well.
"docker---rules/docker",
//Username doc and image name docker being tested.
"doc/docker",
// single character names are now allowed.
"d/docker",
"jess/t",
// Consecutive underscores.
"dock__er/docker",
}
for _, repositoryName := range validRepositoryNames {
_, err := ParseNormalizedNamed(repositoryName)
if err != nil {
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err)
}
}
invalidRepositoryNames := []string{
// Disallow capital letters.
"docker/Docker",
// Only allow one slash.
"docker///docker",
// Disallow 64-character hexadecimal.
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
// Disallow leading and trailing hyphens in namespace.
"-docker/docker",
"docker-/docker",
"-docker-/docker",
// Don't allow underscores everywhere (as opposed to hyphens).
"____/____",
"_docker/_docker",
// Disallow consecutive periods.
"dock..er/docker",
"dock_.er/docker",
"dock-.er/docker",
// No repository.
"docker/",
//namespace too long
"this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker",
}
for _, repositoryName := range invalidRepositoryNames {
if _, err := ParseNormalizedNamed(repositoryName); err == nil {
t.Errorf("Repository name should be invalid: %v", repositoryName)
}
}
}
func TestParseRepositoryInfo(t *testing.T) {
type tcase struct {
RemoteName, FamiliarName, FullName, AmbiguousName, Domain string
}
tcases := []tcase{
{
RemoteName: "fooo/bar",
FamiliarName: "fooo/bar",
FullName: "docker.io/fooo/bar",
AmbiguousName: "index.docker.io/fooo/bar",
Domain: "docker.io",
},
{
RemoteName: "library/ubuntu",
FamiliarName: "ubuntu",
FullName: "docker.io/library/ubuntu",
AmbiguousName: "library/ubuntu",
Domain: "docker.io",
},
{
RemoteName: "nonlibrary/ubuntu",
FamiliarName: "nonlibrary/ubuntu",
FullName: "docker.io/nonlibrary/ubuntu",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "other/library",
FamiliarName: "other/library",
FullName: "docker.io/other/library",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "private/moonbase",
FamiliarName: "127.0.0.1:8000/private/moonbase",
FullName: "127.0.0.1:8000/private/moonbase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "privatebase",
FamiliarName: "127.0.0.1:8000/privatebase",
FullName: "127.0.0.1:8000/privatebase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com/private/moonbase",
FullName: "example.com/private/moonbase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "privatebase",
FamiliarName: "example.com/privatebase",
FullName: "example.com/privatebase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com:8000/private/moonbase",
FullName: "example.com:8000/private/moonbase",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "privatebasee",
FamiliarName: "example.com:8000/privatebasee",
FullName: "example.com:8000/privatebasee",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "library/ubuntu-12.04-base",
FamiliarName: "ubuntu-12.04-base",
FullName: "docker.io/library/ubuntu-12.04-base",
AmbiguousName: "index.docker.io/library/ubuntu-12.04-base",
Domain: "docker.io",
},
{
RemoteName: "library/foo",
FamiliarName: "foo",
FullName: "docker.io/library/foo",
AmbiguousName: "docker.io/foo",
Domain: "docker.io",
},
{
RemoteName: "library/foo/bar",
FamiliarName: "library/foo/bar",
FullName: "docker.io/library/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "store/foo/bar",
FamiliarName: "store/foo/bar",
FullName: "docker.io/store/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
}
for _, tcase := range tcases {
refStrings := []string{tcase.FamiliarName, tcase.FullName}
if tcase.AmbiguousName != "" {
refStrings = append(refStrings, tcase.AmbiguousName)
}
var refs []Named
for _, r := range refStrings {
named, err := ParseNormalizedNamed(r)
if err != nil {
t.Fatal(err)
}
refs = append(refs, named)
}
for _, r := range refs {
if expected, actual := tcase.FamiliarName, FamiliarName(r); expected != actual {
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.FullName, r.String(); expected != actual {
t.Fatalf("Invalid canonical reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.Domain, Domain(r); expected != actual {
t.Fatalf("Invalid domain for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.RemoteName, Path(r); expected != actual {
t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual)
}
}
}
}
func TestParseReferenceWithTagAndDigest(t *testing.T) {
shortRef := "busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"
ref, err := ParseNormalizedNamed(shortRef)
if err != nil {
t.Fatal(err)
}
if expected, actual := "docker.io/library/"+shortRef, ref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
if _, isTagged := ref.(NamedTagged); !isTagged {
t.Fatalf("Reference from %q should support tag", ref)
}
if _, isCanonical := ref.(Canonical); !isCanonical {
t.Fatalf("Reference from %q should support digest", ref)
}
if expected, actual := shortRef, FamiliarString(ref); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
}
func TestInvalidReferenceComponents(t *testing.T) {
if _, err := ParseNormalizedNamed("-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid name")
}
ref, err := ParseNormalizedNamed("busybox")
if err != nil {
t.Fatal(err)
}
if _, err := WithTag(ref, "-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid tag")
}
if _, err := WithDigest(ref, digest.Digest("foo")); err == nil {
t.Fatal("Expected WithDigest to detect invalid digest")
}
}
func equalReference(r1, r2 Reference) bool {
switch v1 := r1.(type) {
case digestReference:
if v2, ok := r2.(digestReference); ok {
return v1 == v2
}
case repository:
if v2, ok := r2.(repository); ok {
return v1 == v2
}
case taggedReference:
if v2, ok := r2.(taggedReference); ok {
return v1 == v2
}
case canonicalReference:
if v2, ok := r2.(canonicalReference); ok {
return v1 == v2
}
case reference:
if v2, ok := r2.(reference); ok {
return v1 == v2
}
}
return false
}
func TestParseAnyReference(t *testing.T) {
tcases := []struct {
Reference string
Equivalent string
Expected Reference
}{
{
Reference: "redis",
Equivalent: "docker.io/library/redis",
},
{
Reference: "redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "docker.io/library/redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dmcgowan/myapp",
Equivalent: "docker.io/dmcgowan/myapp",
},
{
Reference: "dmcgowan/myapp:latest",
Equivalent: "docker.io/dmcgowan/myapp:latest",
},
{
Reference: "docker.io/mcgowan/myapp:latest",
Equivalent: "docker.io/mcgowan/myapp:latest",
},
{
Reference: "dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
},
}
for _, tcase := range tcases {
var ref Reference
var err error
ref, err = ParseAnyReference(tcase.Reference)
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Reference, err)
}
if ref.String() != tcase.Equivalent {
t.Fatalf("Unexpected string: %s, expected %s", ref.String(), tcase.Equivalent)
}
expected := tcase.Expected
if expected == nil {
expected, err = Parse(tcase.Equivalent)
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Equivalent, err)
}
}
if !equalReference(ref, expected) {
t.Errorf("Unexpected reference %#v, expected %#v", ref, expected)
}
}
}
func TestNormalizedSplitHostname(t *testing.T) {
testcases := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "docker.io",
name: "test_com/foo",
},
{
input: "docker/migrator",
domain: "docker.io",
name: "docker/migrator",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "xn--n3h.com/foo",
domain: "xn--n3h.com",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
{
input: "docker.io/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo/bar",
domain: "docker.io",
name: "library/foo/bar",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := ParseNormalizedNamed(testcase.input)
if err != nil {
failf("error parsing name: %s", err)
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
}
}
}
func TestMatchError(t *testing.T) {
named, err := ParseAnyReference("foo")
if err != nil {
t.Fatal(err)
}
_, err = FamiliarMatch("[-x]", named)
if err == nil {
t.Fatalf("expected an error, got nothing")
}
}
func TestMatch(t *testing.T) {
matchCases := []struct {
reference string
pattern string
expected bool
}{
{
reference: "foo",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/any/bat",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/a/bar",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/b/baz",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/*/baz:tag",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/c/baz:tag",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "*/foo/c/baz",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "example.com/foo/c/baz",
expected: true,
},
}
for _, c := range matchCases {
named, err := ParseAnyReference(c.reference)
if err != nil {
t.Fatal(err)
}
actual, err := FamiliarMatch(c.pattern, named)
if err != nil {
t.Fatal(err)
}
if actual != c.expected {
t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual)
}
}
}

View file

@ -1,41 +1,120 @@
// Package reference provides a general type to represent any way of referencing images within the registry.
// Its main purpose is to abstract tags and digests (content-addressable hash).
//
// Grammar
//
// reference := name [ ":" tag ] [ "@" digest ]
// name := [domain '/'] path-component ['/' path-component]*
// domain := domain-component ['.' domain-component]* [':' port-number]
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/
// path-component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/
//
// tag := /[\w][\w.-]{0,127}/
//
// digest := digest-algorithm ":" digest-hex
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]
// digest-algorithm-separator := /[+.-_]/
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
//
// identifier := /[a-f0-9]{64}/
// short-identifier := /[a-f0-9]{6,64}/
package reference package reference
import ( import (
"regexp" "errors"
"fmt"
"strings" "strings"
// "opencontainers/go-digest" requires us to load the algorithms that we
// want to use into the binary (it calls .Available).
_ "crypto/sha256"
distreference "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
) )
const ( const (
// DefaultTag defines the default tag used when performing images related actions and no tag or digest is specified // NameTotalLengthMax is the maximum total number of characters in a repository name.
DefaultTag = "latest" NameTotalLengthMax = 255
// DefaultHostname is the default built-in hostname
DefaultHostname = "docker.io"
// LegacyDefaultHostname is automatically converted to DefaultHostname
LegacyDefaultHostname = "index.docker.io"
// DefaultRepoPrefix is the prefix used for default repositories in default host
DefaultRepoPrefix = "library/"
) )
var (
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
ErrReferenceInvalidFormat = errors.New("invalid reference format")
// ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
ErrTagInvalidFormat = errors.New("invalid tag format")
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
ErrDigestInvalidFormat = errors.New("invalid digest format")
// ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
ErrNameContainsUppercase = errors.New("repository name must be lowercase")
// ErrNameEmpty is returned for empty, invalid repository names.
ErrNameEmpty = errors.New("repository name must have at least one component")
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
// ErrNameNotCanonical is returned when a name is not canonical.
ErrNameNotCanonical = errors.New("repository name must be canonical")
)
// Reference is an opaque object reference identifier that may include
// modifiers such as a hostname, name, tag, and digest.
type Reference interface {
// String returns the full reference
String() string
}
// Field provides a wrapper type for resolving correct reference types when
// working with encoding.
type Field struct {
reference Reference
}
// AsField wraps a reference in a Field for encoding.
func AsField(reference Reference) Field {
return Field{reference}
}
// Reference unwraps the reference type from the field to
// return the Reference object. This object should be
// of the appropriate type to further check for different
// reference types.
func (f Field) Reference() Reference {
return f.reference
}
// MarshalText serializes the field to byte text which
// is the string of the reference.
func (f Field) MarshalText() (p []byte, err error) {
return []byte(f.reference.String()), nil
}
// UnmarshalText parses text bytes by invoking the
// reference parser to ensure the appropriately
// typed reference object is wrapped by field.
func (f *Field) UnmarshalText(p []byte) error {
r, err := Parse(string(p))
if err != nil {
return err
}
f.reference = r
return nil
}
// Named is an object with a full name // Named is an object with a full name
type Named interface { type Named interface {
// Name returns normalized repository name, like "ubuntu". Reference
Name() string Name() string
// String returns full reference, like "ubuntu@sha256:abcdef..." }
String() string
// FullName returns full repository name with hostname, like "docker.io/library/ubuntu" // Tagged is an object which has a tag
FullName() string type Tagged interface {
// Hostname returns hostname for the reference, like "docker.io" Reference
Hostname() string Tag() string
// RemoteName returns the repository component of the full name, like "library/ubuntu"
RemoteName() string
} }
// NamedTagged is an object including a name and tag. // NamedTagged is an object including a name and tag.
@ -44,174 +123,311 @@ type NamedTagged interface {
Tag() string Tag() string
} }
// Digested is an object which has a digest
// in which it can be referenced by
type Digested interface {
Reference
Digest() digest.Digest
}
// Canonical reference is an object with a fully unique // Canonical reference is an object with a fully unique
// name including a name with hostname and digest // name including a name with domain and digest
type Canonical interface { type Canonical interface {
Named Named
Digest() digest.Digest Digest() digest.Digest
} }
// ParseNamed parses s and returns a syntactically valid reference implementing // namedRepository is a reference to a repository with a name.
// the Named interface. The reference must have a name, otherwise an error is // A namedRepository has both domain and path components.
// returned. type namedRepository interface {
Named
Domain() string
Path() string
}
// Domain returns the domain part of the Named reference
func Domain(named Named) string {
if r, ok := named.(namedRepository); ok {
return r.Domain()
}
domain, _ := splitDomain(named.Name())
return domain
}
// Path returns the name without the domain part of the Named reference
func Path(named Named) (name string) {
if r, ok := named.(namedRepository); ok {
return r.Path()
}
_, path := splitDomain(named.Name())
return path
}
func splitDomain(name string) (string, string) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
return "", name
}
return match[1], match[2]
}
// SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value
// is returned as name
// DEPRECATED: Use Domain or Path
func SplitHostname(named Named) (string, string) {
if r, ok := named.(namedRepository); ok {
return r.Domain(), r.Path()
}
return splitDomain(named.Name())
}
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference. // If an error was encountered it is returned, along with a nil Reference.
func ParseNamed(s string) (Named, error) { // NOTE: Parse will not handle short digests.
named, err := distreference.ParseNormalizedNamed(s) func Parse(s string) (Reference, error) {
if err != nil { matches := ReferenceRegexp.FindStringSubmatch(s)
return nil, errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", s) if matches == nil {
if s == "" {
return nil, ErrNameEmpty
}
if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
return nil, ErrNameContainsUppercase
}
return nil, ErrReferenceInvalidFormat
} }
r, err := WithName(named.Name())
if err != nil { if len(matches[1]) > NameTotalLengthMax {
return nil, err return nil, ErrNameTooLong
} }
if canonical, isCanonical := named.(distreference.Canonical); isCanonical {
r, err := distreference.WithDigest(r, canonical.Digest()) var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
if nameMatch != nil && len(nameMatch) == 3 {
repo.domain = nameMatch[1]
repo.path = nameMatch[2]
} else {
repo.domain = ""
repo.path = matches[1]
}
ref := reference{
namedRepository: repo,
tag: matches[2],
}
if matches[3] != "" {
var err error
ref.digest, err = digest.Parse(matches[3])
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &canonicalRef{namedRef{r}}, nil
} }
if tagged, isTagged := named.(distreference.NamedTagged); isTagged {
return WithTag(r, tagged.Tag()) r := getBestReferenceType(ref)
if r == nil {
return nil, ErrNameEmpty
} }
return r, nil return r, nil
} }
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name and be in the canonical
// form, otherwise an error is returned.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: ParseNamed will not handle short digests.
func ParseNamed(s string) (Named, error) {
named, err := ParseNormalizedNamed(s)
if err != nil {
return nil, err
}
if named.String() != s {
return nil, ErrNameNotCanonical
}
return named, nil
}
// WithName returns a named object representing the given string. If the input // WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned. // is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) { func WithName(name string) (Named, error) {
name, err := normalize(name) if len(name) > NameTotalLengthMax {
if err != nil { return nil, ErrNameTooLong
return nil, err
} }
if err := validateName(name); err != nil {
return nil, err match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return nil, ErrReferenceInvalidFormat
} }
r, err := distreference.WithName(name) return repository{
if err != nil { domain: match[1],
return nil, err path: match[2],
} }, nil
return &namedRef{r}, nil
} }
// WithTag combines the name from "name" and the tag from "tag" to form a // WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag. // reference incorporating both the name and the tag.
func WithTag(name Named, tag string) (NamedTagged, error) { func WithTag(name Named, tag string) (NamedTagged, error) {
r, err := distreference.WithTag(name, tag) if !anchoredTagRegexp.MatchString(tag) {
if err != nil { return nil, ErrTagInvalidFormat
return nil, err
} }
return &taggedRef{namedRef{r}}, nil var repo repository
} if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
type namedRef struct { repo.path = r.Path()
distreference.Named } else {
} repo.path = name.Name()
type taggedRef struct {
namedRef
}
type canonicalRef struct {
namedRef
}
func (r *namedRef) FullName() string {
hostname, remoteName := splitHostname(r.Name())
return hostname + "/" + remoteName
}
func (r *namedRef) Hostname() string {
hostname, _ := splitHostname(r.Name())
return hostname
}
func (r *namedRef) RemoteName() string {
_, remoteName := splitHostname(r.Name())
return remoteName
}
func (r *taggedRef) Tag() string {
return r.namedRef.Named.(distreference.NamedTagged).Tag()
}
func (r *canonicalRef) Digest() digest.Digest {
return digest.Digest(r.namedRef.Named.(distreference.Canonical).Digest())
}
// WithDefaultTag adds a default tag to a reference if it only has a repo name.
func WithDefaultTag(ref Named) Named {
if IsNameOnly(ref) {
ref, _ = WithTag(ref, DefaultTag)
} }
if canonical, ok := name.(Canonical); ok {
return reference{
namedRepository: repo,
tag: tag,
digest: canonical.Digest(),
}, nil
}
return taggedReference{
namedRepository: repo,
tag: tag,
}, nil
}
// WithDigest combines the name from "name" and the digest from "digest" to form
// a reference incorporating both the name and the digest.
func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
if !anchoredDigestRegexp.MatchString(digest.String()) {
return nil, ErrDigestInvalidFormat
}
var repo repository
if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if tagged, ok := name.(Tagged); ok {
return reference{
namedRepository: repo,
tag: tagged.Tag(),
digest: digest,
}, nil
}
return canonicalReference{
namedRepository: repo,
digest: digest,
}, nil
}
// TrimNamed removes any tag or digest from the named reference.
func TrimNamed(ref Named) Named {
domain, path := SplitHostname(ref)
return repository{
domain: domain,
path: path,
}
}
func getBestReferenceType(ref reference) Reference {
if ref.Name() == "" {
// Allow digest only references
if ref.digest != "" {
return digestReference(ref.digest)
}
return nil
}
if ref.tag == "" {
if ref.digest != "" {
return canonicalReference{
namedRepository: ref.namedRepository,
digest: ref.digest,
}
}
return ref.namedRepository
}
if ref.digest == "" {
return taggedReference{
namedRepository: ref.namedRepository,
tag: ref.tag,
}
}
return ref return ref
} }
// IsNameOnly returns true if reference only contains a repo name. type reference struct {
func IsNameOnly(ref Named) bool { namedRepository
if _, ok := ref.(NamedTagged); ok { tag string
return false digest digest.Digest
}
if _, ok := ref.(Canonical); ok {
return false
}
return true
} }
// ParseIDOrReference parses string for an image ID or a reference. ID can be func (r reference) String() string {
// without a default prefix. return r.Name() + ":" + r.tag + "@" + r.digest.String()
func ParseIDOrReference(idOrRef string) (digest.Digest, Named, error) {
if err := validateID(idOrRef); err == nil {
idOrRef = "sha256:" + idOrRef
}
if dgst, err := digest.Parse(idOrRef); err == nil {
return dgst, nil, nil
}
ref, err := ParseNamed(idOrRef)
return "", ref, err
} }
// splitHostname splits a repository name to hostname and remotename string. func (r reference) Tag() string {
// If no valid hostname is found, the default hostname is used. Repository name return r.tag
// needs to be already validated before.
func splitHostname(name string) (hostname, remoteName string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
hostname, remoteName = DefaultHostname, name
} else {
hostname, remoteName = name[:i], name[i+1:]
}
if hostname == LegacyDefaultHostname {
hostname = DefaultHostname
}
if hostname == DefaultHostname && !strings.ContainsRune(remoteName, '/') {
remoteName = DefaultRepoPrefix + remoteName
}
return
} }
// normalize returns a repository name in its normalized form, meaning it func (r reference) Digest() digest.Digest {
// will not contain default hostname nor library/ prefix for official images. return r.digest
func normalize(name string) (string, error) {
host, remoteName := splitHostname(name)
if strings.ToLower(remoteName) != remoteName {
return "", errors.New("invalid reference format: repository name must be lowercase")
}
if host == DefaultHostname {
if strings.HasPrefix(remoteName, DefaultRepoPrefix) {
return strings.TrimPrefix(remoteName, DefaultRepoPrefix), nil
}
return remoteName, nil
}
return name, nil
} }
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`) type repository struct {
domain string
func validateID(id string) error { path string
if ok := validHex.MatchString(id); !ok {
return errors.Errorf("image ID %q is invalid", id)
}
return nil
} }
func validateName(name string) error { func (r repository) String() string {
if err := validateID(name); err == nil { return r.Name()
return errors.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) }
}
return nil func (r repository) Name() string {
if r.domain == "" {
return r.path
}
return r.domain + "/" + r.path
}
func (r repository) Domain() string {
return r.domain
}
func (r repository) Path() string {
return r.path
}
type digestReference digest.Digest
func (d digestReference) String() string {
return digest.Digest(d).String()
}
func (d digestReference) Digest() digest.Digest {
return digest.Digest(d)
}
type taggedReference struct {
namedRepository
tag string
}
func (t taggedReference) String() string {
return t.Name() + ":" + t.tag
}
func (t taggedReference) Tag() string {
return t.tag
}
type canonicalReference struct {
namedRepository
digest digest.Digest
}
func (c canonicalReference) String() string {
return c.Name() + "@" + c.digest.String()
}
func (c canonicalReference) Digest() digest.Digest {
return c.digest
} }

View file

@ -1,272 +1,659 @@
package reference package reference
import ( import (
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/json"
"strconv"
"strings"
"testing" "testing"
_ "crypto/sha256" "github.com/opencontainers/go-digest"
) )
func TestValidateReferenceName(t *testing.T) { func TestReferenceParse(t *testing.T) {
validRepoNames := []string{ // referenceTestcases is a unified set of testcases for
"docker/docker", // testing the parsing of references
"library/debian", referenceTestcases := []struct {
"debian", // input is the repository name or name component testcase
"docker.io/docker/docker", input string
"docker.io/library/debian", // err is the error expected from Parse, or nil
"docker.io/debian", err error
"index.docker.io/docker/docker", // repository is the string representation for the reference
"index.docker.io/library/debian", repository string
"index.docker.io/debian", // domain is the domain expected in the reference
"127.0.0.1:5000/docker/docker", domain string
"127.0.0.1:5000/library/debian", // tag is the tag for the reference
"127.0.0.1:5000/debian", tag string
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", // digest is the digest for the reference (enforces digest reference)
} digest string
invalidRepoNames := []string{ }{
"https://github.com/docker/docker", {
"docker/Docker", input: "test_com",
"-docker", repository: "test_com",
"-docker/docker", },
"-docker.io/docker/docker", {
"docker///docker", input: "test.com:tag",
"docker.io/docker/Docker", repository: "test.com",
"docker.io/docker///docker", tag: "tag",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", },
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", {
input: "test.com:5000",
repository: "test.com",
tag: "5000",
},
{
input: "test.com/repo:tag",
domain: "test.com",
repository: "test.com/repo",
tag: "tag",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "test:5000/repo:tag",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
},
{
input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "repo@sha256:ffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestInvalidLength,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestUnsupported,
},
{
input: "Uppercase:tag",
err: ErrNameContainsUppercase,
},
// FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes.
// See https://github.com/docker/distribution/pull/1778, and https://github.com/docker/docker/pull/20175
//{
// input: "Uppercase/lowercase:tag",
// err: ErrNameContainsUppercase,
//},
{
input: "test:5000/Uppercase/lowercase:tag",
err: ErrNameContainsUppercase,
},
{
input: "lowercase:Uppercase",
repository: "lowercase",
tag: "Uppercase",
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
domain: "a",
repository: strings.Repeat("a/", 127) + "a",
tag: "tag-puts-this-over-max",
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
},
{
input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
tag: "some-long-tag",
},
{
input: "b.gcr.io/test.example.com/my-app:test.example.com",
domain: "b.gcr.io",
repository: "b.gcr.io/test.example.com/my-app",
tag: "test.example.com",
},
{
input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
domain: "xn--n3h.com",
repository: "xn--n3h.com/myimage",
tag: "xn--n3h.com",
},
{
input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
domain: "xn--7o8h.com",
repository: "xn--7o8h.com/myimage",
tag: "xn--7o8h.com",
digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "foo_bar.com:8080",
repository: "foo_bar.com",
tag: "8080",
},
{
input: "foo/foo_bar.com:8080",
domain: "foo",
repository: "foo/foo_bar.com",
tag: "8080",
},
} }
for _, testcase := range referenceTestcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
for _, name := range invalidRepoNames { repo, err := Parse(testcase.input)
_, err := ParseNamed(name) if testcase.err != nil {
if err == nil {
failf("missing expected error: %v", testcase.err)
} else if testcase.err != err {
failf("mismatched error: got %v, expected %v", err, testcase.err)
}
continue
} else if err != nil {
failf("unexpected parse error: %v", err)
continue
}
if repo.String() != testcase.input {
failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input)
}
if named, ok := repo.(Named); ok {
if named.Name() != testcase.repository {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository)
}
domain, _ := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
} else if testcase.repository != "" || testcase.domain != "" {
failf("expected named type, got %T", repo)
}
tagged, ok := repo.(Tagged)
if testcase.tag != "" {
if ok {
if tagged.Tag() != testcase.tag {
failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
}
} else {
failf("expected tagged type, got %T", repo)
}
} else if ok {
failf("unexpected tagged type")
}
digested, ok := repo.(Digested)
if testcase.digest != "" {
if ok {
if digested.Digest().String() != testcase.digest {
failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
}
} else {
failf("expected digested type, got %T", repo)
}
} else if ok {
failf("unexpected digested type")
}
}
}
// TestWithNameFailure tests cases where WithName should fail. Cases where it
// should succeed are covered by TestSplitHostname, below.
func TestWithNameFailure(t *testing.T) {
testcases := []struct {
input string
err error
}{
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
_, err := WithName(testcase.input)
if err == nil { if err == nil {
t.Fatalf("Expected invalid repo name for %q", name) failf("no error parsing name. expected: %s", testcase.err)
} }
} }
}
for _, name := range validRepoNames { func TestSplitHostname(t *testing.T) {
_, err := ParseNamed(name) testcases := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "",
name: "test_com/foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.input)
if err != nil { if err != nil {
t.Fatalf("Error parsing repo name %s, got: %q", name, err) failf("error parsing name: %s", err)
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
} }
} }
} }
func TestValidateRemoteName(t *testing.T) { type serializationType struct {
validRepositoryNames := []string{ Description string
// Sanity check. Field Field
"docker/docker", }
// Allow 64-character non-hexadecimal names (hexadecimal names are forbidden). func TestSerialization(t *testing.T) {
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", testcases := []struct {
description string
// Allow embedded hyphens. input string
"docker-rules/docker", name string
tag string
// Allow multiple hyphens as well. digest string
"docker---rules/docker", err error
}{
//Username doc and image name docker being tested. {
"doc/docker", description: "empty value",
err: ErrNameEmpty,
// single character names are now allowed. },
"d/docker", {
"jess/t", description: "just a name",
input: "example.com:8000/named",
// Consecutive underscores. name: "example.com:8000/named",
"dock__er/docker", },
{
description: "name with a tag",
input: "example.com:8000/named:tagged",
name: "example.com:8000/named",
tag: "tagged",
},
{
description: "name with digest",
input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112",
name: "other.com/named",
digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112",
},
} }
for _, repositoryName := range validRepositoryNames { for _, testcase := range testcases {
_, err := ParseNamed(repositoryName) failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
m := map[string]string{
"Description": testcase.description,
"Field": testcase.input,
}
b, err := json.Marshal(m)
if err != nil { if err != nil {
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) failf("error marshalling: %v", err)
} }
} t := serializationType{}
invalidRepositoryNames := []string{ if err := json.Unmarshal(b, &t); err != nil {
// Disallow capital letters. if testcase.err == nil {
"docker/Docker", failf("error unmarshalling: %v", err)
}
if err != testcase.err {
failf("wrong error, expected %v, got %v", testcase.err, err)
}
// Only allow one slash. continue
"docker///docker", } else if testcase.err != nil {
failf("expected error unmarshalling: %v", testcase.err)
// Disallow 64-character hexadecimal.
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
// Disallow leading and trailing hyphens in namespace.
"-docker/docker",
"docker-/docker",
"-docker-/docker",
// Don't allow underscores everywhere (as opposed to hyphens).
"____/____",
"_docker/_docker",
// Disallow consecutive periods.
"dock..er/docker",
"dock_.er/docker",
"dock-.er/docker",
// No repository.
"docker/",
//namespace too long
"this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker",
}
for _, repositoryName := range invalidRepositoryNames {
if _, err := ParseNamed(repositoryName); err == nil {
t.Errorf("Repository name should be invalid: %v", repositoryName)
} }
if t.Description != testcase.description {
failf("wrong description, expected %q, got %q", testcase.description, t.Description)
}
ref := t.Field.Reference()
if named, ok := ref.(Named); ok {
if named.Name() != testcase.name {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name)
}
} else if testcase.name != "" {
failf("expected named type, got %T", ref)
}
tagged, ok := ref.(Tagged)
if testcase.tag != "" {
if ok {
if tagged.Tag() != testcase.tag {
failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
}
} else {
failf("expected tagged type, got %T", ref)
}
} else if ok {
failf("unexpected tagged type")
}
digested, ok := ref.(Digested)
if testcase.digest != "" {
if ok {
if digested.Digest().String() != testcase.digest {
failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
}
} else {
failf("expected digested type, got %T", ref)
}
} else if ok {
failf("unexpected digested type")
}
t = serializationType{
Description: testcase.description,
Field: AsField(ref),
}
b2, err := json.Marshal(t)
if err != nil {
failf("error marshing serialization type: %v", err)
}
if string(b) != string(b2) {
failf("unexpected serialized value: expected %q, got %q", string(b), string(b2))
}
// Ensure t.Field is not implementing "Reference" directly, getting
// around the Reference type system
var fieldInterface interface{} = t.Field
if _, ok := fieldInterface.(Reference); ok {
failf("field should not implement Reference interface")
}
} }
} }
func TestParseRepositoryInfo(t *testing.T) { func TestWithTag(t *testing.T) {
type tcase struct { testcases := []struct {
RemoteName, NormalizedName, FullName, AmbiguousName, Hostname string name string
} digest digest.Digest
tag string
tcases := []tcase{ combined string
}{
{ {
RemoteName: "fooo/bar", name: "test.com/foo",
NormalizedName: "fooo/bar", tag: "tag",
FullName: "docker.io/fooo/bar", combined: "test.com/foo:tag",
AmbiguousName: "index.docker.io/fooo/bar",
Hostname: "docker.io",
}, },
{ {
RemoteName: "library/ubuntu", name: "foo",
NormalizedName: "ubuntu", tag: "tag2",
FullName: "docker.io/library/ubuntu", combined: "foo:tag2",
AmbiguousName: "library/ubuntu",
Hostname: "docker.io",
}, },
{ {
RemoteName: "nonlibrary/ubuntu", name: "test.com:8000/foo",
NormalizedName: "nonlibrary/ubuntu", tag: "tag4",
FullName: "docker.io/nonlibrary/ubuntu", combined: "test.com:8000/foo:tag4",
AmbiguousName: "",
Hostname: "docker.io",
}, },
{ {
RemoteName: "other/library", name: "test.com:8000/foo",
NormalizedName: "other/library", tag: "TAG5",
FullName: "docker.io/other/library", combined: "test.com:8000/foo:TAG5",
AmbiguousName: "",
Hostname: "docker.io",
}, },
{ {
RemoteName: "private/moonbase", name: "test.com:8000/foo",
NormalizedName: "127.0.0.1:8000/private/moonbase", digest: "sha256:1234567890098765432112345667890098765",
FullName: "127.0.0.1:8000/private/moonbase", tag: "TAG5",
AmbiguousName: "", combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765",
Hostname: "127.0.0.1:8000",
},
{
RemoteName: "privatebase",
NormalizedName: "127.0.0.1:8000/privatebase",
FullName: "127.0.0.1:8000/privatebase",
AmbiguousName: "",
Hostname: "127.0.0.1:8000",
},
{
RemoteName: "private/moonbase",
NormalizedName: "example.com/private/moonbase",
FullName: "example.com/private/moonbase",
AmbiguousName: "",
Hostname: "example.com",
},
{
RemoteName: "privatebase",
NormalizedName: "example.com/privatebase",
FullName: "example.com/privatebase",
AmbiguousName: "",
Hostname: "example.com",
},
{
RemoteName: "private/moonbase",
NormalizedName: "example.com:8000/private/moonbase",
FullName: "example.com:8000/private/moonbase",
AmbiguousName: "",
Hostname: "example.com:8000",
},
{
RemoteName: "privatebasee",
NormalizedName: "example.com:8000/privatebasee",
FullName: "example.com:8000/privatebasee",
AmbiguousName: "",
Hostname: "example.com:8000",
},
{
RemoteName: "library/ubuntu-12.04-base",
NormalizedName: "ubuntu-12.04-base",
FullName: "docker.io/library/ubuntu-12.04-base",
AmbiguousName: "index.docker.io/library/ubuntu-12.04-base",
Hostname: "docker.io",
}, },
} }
for _, testcase := range testcases {
for _, tcase := range tcases { failf := func(format string, v ...interface{}) {
refStrings := []string{tcase.NormalizedName, tcase.FullName} t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
if tcase.AmbiguousName != "" { t.Fail()
refStrings = append(refStrings, tcase.AmbiguousName)
} }
var refs []Named named, err := WithName(testcase.name)
for _, r := range refStrings { if err != nil {
named, err := ParseNamed(r) failf("error parsing name: %s", err)
}
if testcase.digest != "" {
canonical, err := WithDigest(named, testcase.digest)
if err != nil { if err != nil {
t.Fatal(err) failf("error adding digest")
} }
refs = append(refs, named) named = canonical
named, err = WithName(r) }
tagged, err := WithTag(named, testcase.tag)
if err != nil {
failf("WithTag failed: %s", err)
}
if tagged.String() != testcase.combined {
failf("unexpected: got %q, expected %q", tagged.String(), testcase.combined)
}
}
}
func TestWithDigest(t *testing.T) {
testcases := []struct {
name string
digest digest.Digest
tag string
combined string
}{
{
name: "test.com/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
tag: "latest",
combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.name)
if err != nil {
failf("error parsing name: %s", err)
}
if testcase.tag != "" {
tagged, err := WithTag(named, testcase.tag)
if err != nil { if err != nil {
t.Fatal(err) failf("error adding tag")
} }
refs = append(refs, named) named = tagged
} }
digested, err := WithDigest(named, testcase.digest)
for _, r := range refs { if err != nil {
if expected, actual := tcase.NormalizedName, r.Name(); expected != actual { failf("WithDigest failed: %s", err)
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual) }
} if digested.String() != testcase.combined {
if expected, actual := tcase.FullName, r.FullName(); expected != actual { failf("unexpected: got %q, expected %q", digested.String(), testcase.combined)
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.Hostname, r.Hostname(); expected != actual {
t.Fatalf("Invalid hostname for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.RemoteName, r.RemoteName(); expected != actual {
t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual)
}
} }
} }
} }
func TestParseReferenceWithTagAndDigest(t *testing.T) { func TestParseNamed(t *testing.T) {
ref, err := ParseNamed("busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa") testcases := []struct {
if err != nil { input string
t.Fatal(err) domain string
name string
err error
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test_com/foo",
err: ErrNameNotCanonical,
},
{
input: "test.com",
err: ErrNameNotCanonical,
},
{
input: "foo",
err: ErrNameNotCanonical,
},
{
input: "library/foo",
err: ErrNameNotCanonical,
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
// Ambiguous case, parser will add "library/" to foo
{
input: "docker.io/foo",
err: ErrNameNotCanonical,
},
} }
if _, isTagged := ref.(NamedTagged); isTagged { for _, testcase := range testcases {
t.Fatalf("Reference from %q should not support tag", ref) failf := func(format string, v ...interface{}) {
} t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
if _, isCanonical := ref.(Canonical); !isCanonical { t.Fail()
t.Fatalf("Reference from %q should not support digest", ref) }
}
if expected, actual := "busybox@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa", ref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
}
func TestInvalidReferenceComponents(t *testing.T) { named, err := ParseNamed(testcase.input)
if _, err := WithName("-foo"); err == nil { if err != nil && testcase.err == nil {
t.Fatal("Expected WithName to detect invalid name") failf("error parsing name: %s", err)
} continue
ref, err := WithName("busybox") } else if err == nil && testcase.err != nil {
if err != nil { failf("parsing succeded: expected error %v", testcase.err)
t.Fatal(err) continue
} } else if err != testcase.err {
if _, err := WithTag(ref, "-foo"); err == nil { failf("unexpected error %v, expected %v", err, testcase.err)
t.Fatal("Expected WithName to detect invalid tag") continue
} else if err != nil {
continue
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
}
} }
} }

View file

@ -0,0 +1,143 @@
package reference
import "regexp"
var (
// alphaNumericRegexp defines the alpha numeric atom, typically a
// component of names. This only allows lower case characters and digits.
alphaNumericRegexp = match(`[a-z0-9]+`)
// separatorRegexp defines the separators allowed to be embedded in name
// components. This allow one period, one or two underscore and multiple
// dashes.
separatorRegexp = match(`(?:[._]|__|[-]*)`)
// nameComponentRegexp restricts registry path component names to start
// with at least one letter or number, with following parts able to be
// separated by one period, one or two underscore and multiple dashes.
nameComponentRegexp = expression(
alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp)))
// domainComponentRegexp restricts the registry domain component of a
// repository name to start with a component as defined by domainRegexp
// and followed by an optional port.
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
// domainRegexp defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names.
domainRegexp = expression(
domainComponentRegexp,
optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(`[0-9]+`)))
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
TagRegexp = match(`[\w][\w.-]{0,127}`)
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = anchored(TagRegexp)
// DigestRegexp matches valid digests.
DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = anchored(DigestRegexp)
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
NameRegexp = expression(
optional(domainRegexp, literal(`/`)),
nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))
// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
anchoredNameRegexp = anchored(
optional(capture(domainRegexp), literal(`/`)),
capture(nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))))
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
ReferenceRegexp = anchored(capture(NameRegexp),
optional(literal(":"), capture(TagRegexp)),
optional(literal("@"), capture(DigestRegexp)))
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
IdentifierRegexp = match(`([a-f0-9]{64})`)
// ShortIdentifierRegexp is the format used to represent a prefix
// of an identifier. A prefix may be used to match a sha256 identifier
// within a list of trusted identifiers.
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`)
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
anchoredIdentifierRegexp = anchored(IdentifierRegexp)
// anchoredShortIdentifierRegexp is used to check if a value
// is a possible identifier prefix, anchored at start and end
// of string.
anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp)
)
// match compiles the string to a regular expression.
var match = regexp.MustCompile
// literal compiles s into a literal regular expression, escaping any regexp
// reserved characters.
func literal(s string) *regexp.Regexp {
re := match(regexp.QuoteMeta(s))
if _, complete := re.LiteralPrefix(); !complete {
panic("must be a literal")
}
return re
}
// expression defines a full expression, where each regular expression must
// follow the previous.
func expression(res ...*regexp.Regexp) *regexp.Regexp {
var s string
for _, re := range res {
s += re.String()
}
return match(s)
}
// optional wraps the expression in a non-capturing group and makes the
// production optional.
func optional(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `?`)
}
// repeated wraps the regexp in a non-capturing group to get one or more
// matches.
func repeated(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `+`)
}
// group wraps the regexp in a non-capturing group.
func group(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(?:` + expression(res...).String() + `)`)
}
// capture wraps the expression in a capturing group.
func capture(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(` + expression(res...).String() + `)`)
}
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...*regexp.Regexp) *regexp.Regexp {
return match(`^` + expression(res...).String() + `$`)
}

View file

@ -0,0 +1,553 @@
package reference
import (
"regexp"
"strings"
"testing"
)
type regexpMatch struct {
input string
match bool
subs []string
}
func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) {
matches := r.FindStringSubmatch(m.input)
if m.match && matches != nil {
if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input {
t.Fatalf("Bad match result %#v for %q", matches, m.input)
}
if len(matches) < (len(m.subs) + 1) {
t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input)
}
for i := range m.subs {
if m.subs[i] != matches[i+1] {
t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input)
}
}
} else if m.match {
t.Errorf("Expected match for %q", m.input)
} else if matches != nil {
t.Errorf("Unexpected match for %q", m.input)
}
}
func TestDomainRegexp(t *testing.T) {
hostcases := []regexpMatch{
{
input: "test.com",
match: true,
},
{
input: "test.com:10304",
match: true,
},
{
input: "test.com:http",
match: false,
},
{
input: "localhost",
match: true,
},
{
input: "localhost:8080",
match: true,
},
{
input: "a",
match: true,
},
{
input: "a.b",
match: true,
},
{
input: "ab.cd.com",
match: true,
},
{
input: "a-b.com",
match: true,
},
{
input: "-ab.com",
match: false,
},
{
input: "ab-.com",
match: false,
},
{
input: "ab.c-om",
match: true,
},
{
input: "ab.-com",
match: false,
},
{
input: "ab.com-",
match: false,
},
{
input: "0101.com",
match: true, // TODO(dmcgowan): valid if this should be allowed
},
{
input: "001a.com",
match: true,
},
{
input: "b.gbc.io:443",
match: true,
},
{
input: "b.gbc.io",
match: true,
},
{
input: "xn--n3h.com", // ☃.com in punycode
match: true,
},
{
input: "Asdf.com", // uppercase character
match: true,
},
}
r := regexp.MustCompile(`^` + domainRegexp.String() + `$`)
for i := range hostcases {
checkRegexp(t, r, hostcases[i])
}
}
func TestFullNameRegexp(t *testing.T) {
if anchoredNameRegexp.NumSubexp() != 2 {
t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2",
anchoredNameRegexp, anchoredNameRegexp.NumSubexp())
}
testcases := []regexpMatch{
{
input: "",
match: false,
},
{
input: "short",
match: true,
subs: []string{"", "short"},
},
{
input: "simple/name",
match: true,
subs: []string{"simple", "name"},
},
{
input: "library/ubuntu",
match: true,
subs: []string{"library", "ubuntu"},
},
{
input: "docker/stevvooe/app",
match: true,
subs: []string{"docker", "stevvooe/app"},
},
{
input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"},
},
{
input: "aa/aa/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/bb/bb/bb"},
},
{
input: "a/a/a/a",
match: true,
subs: []string{"a", "a/a/a"},
},
{
input: "a/a/a/a/",
match: false,
},
{
input: "a//a/a",
match: false,
},
{
input: "a",
match: true,
subs: []string{"", "a"},
},
{
input: "a/aa",
match: true,
subs: []string{"a", "aa"},
},
{
input: "a/aa/a",
match: true,
subs: []string{"a", "aa/a"},
},
{
input: "foo.com",
match: true,
subs: []string{"", "foo.com"},
},
{
input: "foo.com/",
match: false,
},
{
input: "foo.com:8080/bar",
match: true,
subs: []string{"foo.com:8080", "bar"},
},
{
input: "foo.com:http/bar",
match: false,
},
{
input: "foo.com/bar",
match: true,
subs: []string{"foo.com", "bar"},
},
{
input: "foo.com/bar/baz",
match: true,
subs: []string{"foo.com", "bar/baz"},
},
{
input: "localhost:8080/bar",
match: true,
subs: []string{"localhost:8080", "bar"},
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
match: true,
subs: []string{"sub-dom1.foo.com", "bar/baz/quux"},
},
{
input: "blog.foo.com/bar/baz",
match: true,
subs: []string{"blog.foo.com", "bar/baz"},
},
{
input: "a^a",
match: false,
},
{
input: "aa/asdf$$^/aa",
match: false,
},
{
input: "asdf$$^/aa",
match: false,
},
{
input: "aa-a/a",
match: true,
subs: []string{"aa-a", "a"},
},
{
input: strings.Repeat("a/", 128) + "a",
match: true,
subs: []string{"a", strings.Repeat("a/", 127) + "a"},
},
{
input: "a-/a/a/a",
match: false,
},
{
input: "foo.com/a-/a/a",
match: false,
},
{
input: "-foo/bar",
match: false,
},
{
input: "foo/bar-",
match: false,
},
{
input: "foo-/bar",
match: false,
},
{
input: "foo/-bar",
match: false,
},
{
input: "_foo/bar",
match: false,
},
{
input: "foo_bar",
match: true,
subs: []string{"", "foo_bar"},
},
{
input: "foo_bar.com",
match: true,
subs: []string{"", "foo_bar.com"},
},
{
input: "foo_bar.com:8080",
match: false,
},
{
input: "foo_bar.com:8080/app",
match: false,
},
{
input: "foo.com/foo_bar",
match: true,
subs: []string{"foo.com", "foo_bar"},
},
{
input: "____/____",
match: false,
},
{
input: "_docker/_docker",
match: false,
},
{
input: "docker_/docker_",
match: false,
},
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
},
{
input: "xn--n3h.com/myimage", // ☃.com in punycode
match: true,
subs: []string{"xn--n3h.com", "myimage"},
},
{
input: "xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"xn--7o8h.com", "myimage"},
},
{
input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"example.com", "xn--7o8h.com/myimage"},
},
{
input: "example.com/some_separator__underscore/myimage",
match: true,
subs: []string{"example.com", "some_separator__underscore/myimage"},
},
{
input: "example.com/__underscore/myimage",
match: false,
},
{
input: "example.com/..dots/myimage",
match: false,
},
{
input: "example.com/.dots/myimage",
match: false,
},
{
input: "example.com/nodouble..dots/myimage",
match: false,
},
{
input: "example.com/nodouble..dots/myimage",
match: false,
},
{
input: "docker./docker",
match: false,
},
{
input: ".docker/docker",
match: false,
},
{
input: "docker-/docker",
match: false,
},
{
input: "-docker/docker",
match: false,
},
{
input: "do..cker/docker",
match: false,
},
{
input: "do__cker:8080/docker",
match: false,
},
{
input: "do__cker/docker",
match: true,
subs: []string{"", "do__cker/docker"},
},
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
},
{
input: "registry.io/foo/project--id.module--name.ver---sion--name",
match: true,
subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"},
},
{
input: "Asdf.com/foo/bar", // uppercase character in hostname
match: true,
},
{
input: "Foo/FarB", // uppercase characters in remote name
match: false,
},
}
for i := range testcases {
checkRegexp(t, anchoredNameRegexp, testcases[i])
}
}
func TestReferenceRegexp(t *testing.T) {
if ReferenceRegexp.NumSubexp() != 3 {
t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3",
ReferenceRegexp, ReferenceRegexp.NumSubexp())
}
testcases := []regexpMatch{
{
input: "registry.com:8080/myapp:tag",
match: true,
subs: []string{"registry.com:8080/myapp", "tag", ""},
},
{
input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@sha256:badbadbadbad",
match: false,
},
{
input: "registry.com:8080/myapp:invalid~tag",
match: false,
},
{
input: "bad_hostname.com:8080/myapp:tag",
match: false,
},
{
input:// localhost treated as name, missing tag with 8080 as tag
"localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: false,
},
{
// localhost will be treated as an image name without a host
input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@bad",
match: false,
},
{
input: "registry.com:8080/myapp@2bad",
match: false, // TODO(dmcgowan): Support this as valid
},
}
for i := range testcases {
checkRegexp(t, ReferenceRegexp, testcases[i])
}
}
func TestIdentifierRegexp(t *testing.T) {
fullCases := []regexpMatch{
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: true,
},
{
input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf",
match: false,
},
{
input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482",
match: false,
},
}
shortCases := []regexpMatch{
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: true,
},
{
input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf",
match: true,
},
{
input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482",
match: false,
},
{
input: "da304",
match: false,
},
{
input: "da304e",
match: true,
},
}
for i := range fullCases {
checkRegexp(t, anchoredIdentifierRegexp, fullCases[i])
}
for i := range shortCases {
checkRegexp(t, anchoredShortIdentifierRegexp, shortCases[i])
}
}

View file

@ -72,7 +72,7 @@ func manifestSchema1FromManifest(manifest []byte) (genericManifest, error) {
func manifestSchema1FromComponents(ref reference.Named, fsLayers []fsLayersSchema1, history []historySchema1, architecture string) genericManifest { func manifestSchema1FromComponents(ref reference.Named, fsLayers []fsLayersSchema1, history []historySchema1, architecture string) genericManifest {
var name, tag string var name, tag string
if ref != nil { // Well, what to do if it _is_ nil? Most consumers actually don't use these fields nowadays, so we might as well try not supplying them. if ref != nil { // Well, what to do if it _is_ nil? Most consumers actually don't use these fields nowadays, so we might as well try not supplying them.
name = ref.RemoteName() name = reference.Path(ref)
if tagged, ok := ref.(reference.NamedTagged); ok { if tagged, ok := ref.(reference.NamedTagged); ok {
tag = tagged.Tag() tag = tagged.Tag()
} }

View file

@ -9,13 +9,12 @@ import (
"testing" "testing"
"time" "time"
"github.com/pkg/errors"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -26,7 +25,7 @@ type unusedImageSource struct{}
func (f unusedImageSource) Reference() types.ImageReference { func (f unusedImageSource) Reference() types.ImageReference {
panic("Unexpected call to a mock function") panic("Unexpected call to a mock function")
} }
func (f unusedImageSource) Close() { func (f unusedImageSource) Close() error {
panic("Unexpected call to a mock function") panic("Unexpected call to a mock function")
} }
func (f unusedImageSource) GetManifest() ([]byte, string, error) { func (f unusedImageSource) GetManifest() ([]byte, string, error) {
@ -326,7 +325,7 @@ func newSchema2ImageSource(t *testing.T, dockerRef string) *schema2ImageSource {
realConfigJSON, err := ioutil.ReadFile("fixtures/schema2-config.json") realConfigJSON, err := ioutil.ReadFile("fixtures/schema2-config.json")
require.NoError(t, err) require.NoError(t, err)
ref, err := reference.ParseNamed(dockerRef) ref, err := reference.ParseNormalizedNamed(dockerRef)
require.NoError(t, err) require.NoError(t, err)
return &schema2ImageSource{ return &schema2ImageSource{
@ -347,7 +346,7 @@ type memoryImageDest struct {
func (d *memoryImageDest) Reference() types.ImageReference { func (d *memoryImageDest) Reference() types.ImageReference {
return refImageReferenceMock{d.ref} return refImageReferenceMock{d.ref}
} }
func (d *memoryImageDest) Close() { func (d *memoryImageDest) Close() error {
panic("Unexpected call to a mock function") panic("Unexpected call to a mock function")
} }
func (d *memoryImageDest) SupportedManifestMIMETypes() []string { func (d *memoryImageDest) SupportedManifestMIMETypes() []string {

View file

@ -32,7 +32,8 @@ func (i *memoryImage) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized UnparsedImage, if any. // Close removes resources associated with an initialized UnparsedImage, if any.
func (i *memoryImage) Close() { func (i *memoryImage) Close() error {
return nil
} }
// Size returns the size of the image as stored, if known, or -1 if not. // Size returns the size of the image as stored, if known, or -1 if not.

View file

@ -9,13 +9,12 @@ import (
"testing" "testing"
"time" "time"
"github.com/pkg/errors"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -260,7 +259,7 @@ func newOCI1ImageSource(t *testing.T, dockerRef string) *oci1ImageSource {
realConfigJSON, err := ioutil.ReadFile("fixtures/oci1-config.json") realConfigJSON, err := ioutil.ReadFile("fixtures/oci1-config.json")
require.NoError(t, err) require.NoError(t, err)
ref, err := reference.ParseNamed(dockerRef) ref, err := reference.ParseNormalizedNamed(dockerRef)
require.NoError(t, err) require.NoError(t, err)
return &oci1ImageSource{ return &oci1ImageSource{

View file

@ -4,6 +4,7 @@ import (
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -35,8 +36,8 @@ func (i *UnparsedImage) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized UnparsedImage, if any. // Close removes resources associated with an initialized UnparsedImage, if any.
func (i *UnparsedImage) Close() { func (i *UnparsedImage) Close() error {
i.src.Close() return i.src.Close()
} }
// Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need. // Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need.
@ -52,7 +53,7 @@ func (i *UnparsedImage) Manifest() ([]byte, string, error) {
ref := i.Reference().DockerReference() ref := i.Reference().DockerReference()
if ref != nil { if ref != nil {
if canonical, ok := ref.(reference.Canonical); ok { if canonical, ok := ref.(reference.Canonical); ok {
digest := canonical.Digest() digest := digest.Digest(canonical.Digest())
matches, err := manifest.MatchesDigest(m, digest) matches, err := manifest.MatchesDigest(m, digest)
if err != nil { if err != nil {
return nil, "", errors.Wrap(err, "Error computing manifest digest") return nil, "", errors.Wrap(err, "Error computing manifest digest")

View file

@ -31,7 +31,8 @@ func (d *ociImageDestination) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageDestination, if any. // Close removes resources associated with an initialized ImageDestination, if any.
func (d *ociImageDestination) Close() { func (d *ociImageDestination) Close() error {
return nil
} }
func (d *ociImageDestination) SupportedManifestMIMETypes() []string { func (d *ociImageDestination) SupportedManifestMIMETypes() []string {

View file

@ -6,7 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@ -27,7 +26,8 @@ func (s *ociImageSource) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageSource, if any. // Close removes resources associated with an initialized ImageSource, if any.
func (s *ociImageSource) Close() { func (s *ociImageSource) Close() error {
return nil
} }
// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
@ -54,7 +54,7 @@ func (s *ociImageSource) GetManifest() ([]byte, string, error) {
return nil, "", err return nil, "", err
} }
return m, manifest.GuessMIMEType(m), nil return m, desc.MediaType, nil
} }
func (s *ociImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) { func (s *ociImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) {
@ -68,7 +68,11 @@ func (s *ociImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string
return nil, "", err return nil, "", err
} }
return m, manifest.GuessMIMEType(m), nil // XXX: GetTargetManifest means that we don't have the context of what
// mediaType the manifest has. In OCI this means that we don't know
// what reference it came from, so we just *assume* that its
// MediaTypeImageManifest.
return m, imgspecv1.MediaTypeImageManifest, nil
} }
// GetBlob returns a stream for the specified blob, and the blob's size. // GetBlob returns a stream for the specified blob, and the blob's size.

View file

@ -9,11 +9,16 @@ import (
"github.com/containers/image/directory/explicitfilepath" "github.com/containers/image/directory/explicitfilepath"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/image" "github.com/containers/image/image"
"github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func init() {
transports.Register(Transport)
}
// Transport is an ImageTransport for OCI directories. // Transport is an ImageTransport for OCI directories.
var Transport = ociTransport{} var Transport = ociTransport{}

View file

@ -13,6 +13,7 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/docker" "github.com/containers/image/docker"
"github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/image/version" "github.com/containers/image/version"
@ -153,7 +154,7 @@ func (c *openshiftClient) convertDockerImageReference(ref string) (string, error
if len(parts) != 2 { if len(parts) != 2 {
return "", errors.Errorf("Invalid format of docker reference %s: missing '/'", ref) return "", errors.Errorf("Invalid format of docker reference %s: missing '/'", ref)
} }
return c.ref.dockerReference.Hostname() + "/" + parts[1], nil return reference.Domain(c.ref.dockerReference) + "/" + parts[1], nil
} }
type openshiftImageSource struct { type openshiftImageSource struct {
@ -190,11 +191,15 @@ func (s *openshiftImageSource) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageSource, if any. // Close removes resources associated with an initialized ImageSource, if any.
func (s *openshiftImageSource) Close() { func (s *openshiftImageSource) Close() error {
if s.docker != nil { if s.docker != nil {
s.docker.Close() err := s.docker.Close()
s.docker = nil s.docker = nil
return err
} }
return nil
} }
func (s *openshiftImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) { func (s *openshiftImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) {
@ -305,7 +310,7 @@ func newImageDestination(ctx *types.SystemContext, ref openshiftReference) (type
// FIXME: Should this always use a digest, not a tag? Uploading to Docker by tag requires the tag _inside_ the manifest to match, // FIXME: Should this always use a digest, not a tag? Uploading to Docker by tag requires the tag _inside_ the manifest to match,
// i.e. a single signed image cannot be available under multiple tags. But with types.ImageDestination, we don't know // i.e. a single signed image cannot be available under multiple tags. But with types.ImageDestination, we don't know
// the manifest digest at this point. // the manifest digest at this point.
dockerRefString := fmt.Sprintf("//%s/%s/%s:%s", client.ref.dockerReference.Hostname(), client.ref.namespace, client.ref.stream, client.ref.dockerReference.Tag()) dockerRefString := fmt.Sprintf("//%s/%s/%s:%s", reference.Domain(client.ref.dockerReference), client.ref.namespace, client.ref.stream, client.ref.dockerReference.Tag())
dockerRef, err := docker.ParseReference(dockerRefString) dockerRef, err := docker.ParseReference(dockerRefString)
if err != nil { if err != nil {
return nil, err return nil, err
@ -328,8 +333,8 @@ func (d *openshiftImageDestination) Reference() types.ImageReference {
} }
// Close removes resources associated with an initialized ImageDestination, if any. // Close removes resources associated with an initialized ImageDestination, if any.
func (d *openshiftImageDestination) Close() { func (d *openshiftImageDestination) Close() error {
d.docker.Close() return d.docker.Close()
} }
func (d *openshiftImageDestination) SupportedManifestMIMETypes() []string { func (d *openshiftImageDestination) SupportedManifestMIMETypes() []string {

View file

@ -8,10 +8,15 @@ import (
"github.com/containers/image/docker/policyconfiguration" "github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
genericImage "github.com/containers/image/image" genericImage "github.com/containers/image/image"
"github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func init() {
transports.Register(Transport)
}
// Transport is an ImageTransport for OpenShift registry-hosted images. // Transport is an ImageTransport for OpenShift registry-hosted images.
var Transport = openshiftTransport{} var Transport = openshiftTransport{}
@ -51,22 +56,23 @@ type openshiftReference struct {
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OpenShift ImageReference. // ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OpenShift ImageReference.
func ParseReference(ref string) (types.ImageReference, error) { func ParseReference(ref string) (types.ImageReference, error) {
r, err := reference.ParseNamed(ref) r, err := reference.ParseNormalizedNamed(ref)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to parse image reference %q", ref) return nil, errors.Wrapf(err, "failed to parse image reference %q", ref)
} }
tagged, ok := r.(reference.NamedTagged) tagged, ok := r.(reference.NamedTagged)
if !ok { if !ok {
return nil, errors.Errorf("invalid image reference %s, %#v", ref, r) return nil, errors.Errorf("invalid image reference %s, expected format: 'hostname/namespace/stream:tag'", ref)
} }
return NewReference(tagged) return NewReference(tagged)
} }
// NewReference returns an OpenShift reference for a reference.NamedTagged // NewReference returns an OpenShift reference for a reference.NamedTagged
func NewReference(dockerRef reference.NamedTagged) (types.ImageReference, error) { func NewReference(dockerRef reference.NamedTagged) (types.ImageReference, error) {
r := strings.SplitN(dockerRef.RemoteName(), "/", 3) r := strings.SplitN(reference.Path(dockerRef), "/", 3)
if len(r) != 2 { if len(r) != 2 {
return nil, errors.Errorf("invalid image reference %s", dockerRef.String()) return nil, errors.Errorf("invalid image reference: %s, expected format: 'hostname/namespace/stream:tag'",
reference.FamiliarString(dockerRef))
} }
return openshiftReference{ return openshiftReference{
namespace: r[0], namespace: r[0],
@ -85,7 +91,7 @@ func (ref openshiftReference) Transport() types.ImageTransport {
// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. // e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa.
// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
func (ref openshiftReference) StringWithinTransport() string { func (ref openshiftReference) StringWithinTransport() string {
return ref.dockerReference.String() return reference.FamiliarString(ref.dockerReference)
} }
// DockerReference returns a Docker reference associated with this reference // DockerReference returns a Docker reference associated with this reference

View file

@ -40,14 +40,14 @@ func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
func TestNewReference(t *testing.T) { func TestNewReference(t *testing.T) {
// too many ns // too many ns
r, err := reference.ParseNamed("registry.example.com/ns1/ns2/ns3/stream:tag") r, err := reference.ParseNormalizedNamed("registry.example.com/ns1/ns2/ns3/stream:tag")
require.NoError(t, err) require.NoError(t, err)
tagged, ok := r.(reference.NamedTagged) tagged, ok := r.(reference.NamedTagged)
require.True(t, ok) require.True(t, ok)
_, err = NewReference(tagged) _, err = NewReference(tagged)
assert.Error(t, err) assert.Error(t, err)
r, err = reference.ParseNamed("registry.example.com/ns/stream:tag") r, err = reference.ParseNormalizedNamed("registry.example.com/ns/stream:tag")
require.NoError(t, err) require.NoError(t, err)
tagged, ok = r.(reference.NamedTagged) tagged, ok = r.(reference.NamedTagged)
require.True(t, ok) require.True(t, ok)
@ -64,7 +64,7 @@ func TestParseReference(t *testing.T) {
assert.Equal(t, "ns", osRef.namespace) assert.Equal(t, "ns", osRef.namespace)
assert.Equal(t, "stream", osRef.stream) assert.Equal(t, "stream", osRef.stream)
assert.Equal(t, "notlatest", osRef.dockerReference.Tag()) assert.Equal(t, "notlatest", osRef.dockerReference.Tag())
assert.Equal(t, "registry.example.com:8443", osRef.dockerReference.Hostname()) assert.Equal(t, "registry.example.com:8443", reference.Domain(osRef.dockerReference))
// Components creating an invalid Docker Reference name // Components creating an invalid Docker Reference name
_, err = ParseReference("registry.example.com/ns/UPPERCASEISINVALID:notlatest") _, err = ParseReference("registry.example.com/ns/UPPERCASEISINVALID:notlatest")

View file

@ -1,4 +1,4 @@
package copy package compression
import ( import (
"bytes" "bytes"
@ -11,32 +11,37 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
) )
// decompressorFunc, given a compressed stream, returns the decompressed stream. // DecompressorFunc returns the decompressed stream, given a compressed stream.
type decompressorFunc func(io.Reader) (io.Reader, error) type DecompressorFunc func(io.Reader) (io.Reader, error)
func gzipDecompressor(r io.Reader) (io.Reader, error) { // GzipDecompressor is a DecompressorFunc for the gzip compression algorithm.
func GzipDecompressor(r io.Reader) (io.Reader, error) {
return gzip.NewReader(r) return gzip.NewReader(r)
} }
func bzip2Decompressor(r io.Reader) (io.Reader, error) {
// Bzip2Decompressor is a DecompressorFunc for the bzip2 compression algorithm.
func Bzip2Decompressor(r io.Reader) (io.Reader, error) {
return bzip2.NewReader(r), nil return bzip2.NewReader(r), nil
} }
func xzDecompressor(r io.Reader) (io.Reader, error) {
// XzDecompressor is a DecompressorFunc for the xz compression algorithm.
func XzDecompressor(r io.Reader) (io.Reader, error) {
return nil, errors.New("Decompressing xz streams is not supported") return nil, errors.New("Decompressing xz streams is not supported")
} }
// compressionAlgos is an internal implementation detail of detectCompression // compressionAlgos is an internal implementation detail of DetectCompression
var compressionAlgos = map[string]struct { var compressionAlgos = map[string]struct {
prefix []byte prefix []byte
decompressor decompressorFunc decompressor DecompressorFunc
}{ }{
"gzip": {[]byte{0x1F, 0x8B, 0x08}, gzipDecompressor}, // gzip (RFC 1952) "gzip": {[]byte{0x1F, 0x8B, 0x08}, GzipDecompressor}, // gzip (RFC 1952)
"bzip2": {[]byte{0x42, 0x5A, 0x68}, bzip2Decompressor}, // bzip2 (decompress.c:BZ2_decompress) "bzip2": {[]byte{0x42, 0x5A, 0x68}, Bzip2Decompressor}, // bzip2 (decompress.c:BZ2_decompress)
"xz": {[]byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, xzDecompressor}, // xz (/usr/share/doc/xz/xz-file-format.txt) "xz": {[]byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, XzDecompressor}, // xz (/usr/share/doc/xz/xz-file-format.txt)
} }
// detectCompression returns a decompressorFunc if the input is recognized as a compressed format, nil otherwise. // DetectCompression returns a DecompressorFunc if the input is recognized as a compressed format, nil otherwise.
// Because it consumes the start of input, other consumers must use the returned io.Reader instead to also read from the beginning. // Because it consumes the start of input, other consumers must use the returned io.Reader instead to also read from the beginning.
func detectCompression(input io.Reader) (decompressorFunc, io.Reader, error) { func DetectCompression(input io.Reader) (DecompressorFunc, io.Reader, error) {
buffer := [8]byte{} buffer := [8]byte{}
n, err := io.ReadAtLeast(input, buffer[:], len(buffer)) n, err := io.ReadAtLeast(input, buffer[:], len(buffer))
@ -46,7 +51,7 @@ func detectCompression(input io.Reader) (decompressorFunc, io.Reader, error) {
return nil, nil, err return nil, nil, err
} }
var decompressor decompressorFunc var decompressor DecompressorFunc
for name, algo := range compressionAlgos { for name, algo := range compressionAlgos {
if bytes.HasPrefix(buffer[:n], algo.prefix) { if bytes.HasPrefix(buffer[:n], algo.prefix) {
logrus.Debugf("Detected compression format %s", name) logrus.Debugf("Detected compression format %s", name)

View file

@ -1,4 +1,4 @@
package copy package compression
import ( import (
"bytes" "bytes"
@ -33,7 +33,7 @@ func TestDetectCompression(t *testing.T) {
require.NoError(t, err, c.filename) require.NoError(t, err, c.filename)
defer stream.Close() defer stream.Close()
_, updatedStream, err := detectCompression(stream) _, updatedStream, err := DetectCompression(stream)
require.NoError(t, err, c.filename) require.NoError(t, err, c.filename)
updatedContents, err := ioutil.ReadAll(updatedStream) updatedContents, err := ioutil.ReadAll(updatedStream)
@ -47,7 +47,7 @@ func TestDetectCompression(t *testing.T) {
require.NoError(t, err, c.filename) require.NoError(t, err, c.filename)
defer stream.Close() defer stream.Close()
decompressor, updatedStream, err := detectCompression(stream) decompressor, updatedStream, err := DetectCompression(stream)
require.NoError(t, err, c.filename) require.NoError(t, err, c.filename)
var uncompressedStream io.Reader var uncompressedStream io.Reader
@ -70,7 +70,7 @@ func TestDetectCompression(t *testing.T) {
} }
// Empty input is handled reasonably. // Empty input is handled reasonably.
decompressor, updatedStream, err := detectCompression(bytes.NewReader([]byte{})) decompressor, updatedStream, err := DetectCompression(bytes.NewReader([]byte{}))
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, decompressor) assert.Nil(t, decompressor)
updatedContents, err := ioutil.ReadAll(updatedStream) updatedContents, err := ioutil.ReadAll(updatedStream)
@ -80,7 +80,7 @@ func TestDetectCompression(t *testing.T) {
// Error reading input // Error reading input
reader, writer := io.Pipe() reader, writer := io.Pipe()
defer reader.Close() defer reader.Close()
writer.CloseWithError(errors.New("Expected error reading input in detectCompression")) writer.CloseWithError(errors.New("Expected error reading input in DetectCompression"))
_, _, err = detectCompression(reader) _, _, err = DetectCompression(reader)
assert.Error(t, err) assert.Error(t, err)
} }

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
Hello

Binary file not shown.

View file

@ -25,7 +25,7 @@ func SignDockerManifest(m []byte, dockerReference string, mech SigningMechanism,
// using mech. // using mech.
func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte, func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte,
expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) { expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) {
expectedRef, err := reference.ParseNamed(expectedDockerReference) expectedRef, err := reference.ParseNormalizedNamed(expectedDockerReference)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -37,7 +37,7 @@ func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byt
return nil return nil
}, },
validateSignedDockerReference: func(signedDockerReference string) error { validateSignedDockerReference: func(signedDockerReference string) error {
signedRef, err := reference.ParseNamed(signedDockerReference) signedRef, err := reference.ParseNormalizedNamed(signedDockerReference)
if err != nil { if err != nil {
return InvalidSignatureError{msg: fmt.Sprintf("Invalid docker reference %s in signature", signedDockerReference)} return InvalidSignatureError{msg: fmt.Sprintf("Invalid docker reference %s in signature", signedDockerReference)}
} }

View file

@ -2,3 +2,5 @@
/.gpg-v21-migrated /.gpg-v21-migrated
/private-keys-v1.d /private-keys-v1.d
/random_seed /random_seed
/gnupg_spawn_agent_sentinel.lock
/.#*

View file

@ -0,0 +1 @@
../v2s1-invalid-signatures.manifest.json

View file

@ -0,0 +1 @@
../dir-img-valid/signature-1

View file

@ -0,0 +1 @@
../dir-img-valid/manifest.json

View file

@ -0,0 +1 @@
../invalid-blob.signature

View file

@ -0,0 +1 @@
../dir-img-valid/signature-1

View file

@ -0,0 +1 @@
../dir-img-valid/signature-1

View file

@ -0,0 +1 @@
../dir-img-valid/signature-1

View file

@ -0,0 +1 @@
../dir-img-valid/manifest.json

View file

@ -0,0 +1 @@
../dir-img-valid/manifest.json

View file

@ -0,0 +1 @@
../dir-img-valid/signature-1

View file

@ -0,0 +1 @@
../image.manifest.json

View file

@ -19,11 +19,10 @@ import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"github.com/pkg/errors"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/transports" "github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/pkg/errors"
) )
// systemDefaultPolicyPath is the policy path used for DefaultPolicy(). // systemDefaultPolicyPath is the policy path used for DefaultPolicy().
@ -123,10 +122,8 @@ func (m *policyTransportsMap) UnmarshalJSON(data []byte) error {
// So, use a temporary map of pointers-to-slices and convert. // So, use a temporary map of pointers-to-slices and convert.
tmpMap := map[string]*PolicyTransportScopes{} tmpMap := map[string]*PolicyTransportScopes{}
if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} { if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} {
transport, ok := transports.KnownTransports[key] // transport can be nil
if !ok { transport := transports.Get(key)
return nil
}
// paranoidUnmarshalJSONObject detects key duplication for us, check just to be safe. // paranoidUnmarshalJSONObject detects key duplication for us, check just to be safe.
if _, ok := tmpMap[key]; ok { if _, ok := tmpMap[key]; ok {
return nil return nil
@ -156,7 +153,7 @@ func (m *PolicyTransportScopes) UnmarshalJSON(data []byte) error {
} }
// policyTransportScopesWithTransport is a way to unmarshal a PolicyTransportScopes // policyTransportScopesWithTransport is a way to unmarshal a PolicyTransportScopes
// while validating using a specific ImageTransport. // while validating using a specific ImageTransport if not nil.
type policyTransportScopesWithTransport struct { type policyTransportScopesWithTransport struct {
transport types.ImageTransport transport types.ImageTransport
dest *PolicyTransportScopes dest *PolicyTransportScopes
@ -175,7 +172,7 @@ func (m *policyTransportScopesWithTransport) UnmarshalJSON(data []byte) error {
if _, ok := tmpMap[key]; ok { if _, ok := tmpMap[key]; ok {
return nil return nil
} }
if key != "" { if key != "" && m.transport != nil {
if err := m.transport.ValidatePolicyConfigurationScope(key); err != nil { if err := m.transport.ValidatePolicyConfigurationScope(key); err != nil {
return nil return nil
} }
@ -634,7 +631,7 @@ func (prm *prmMatchRepository) UnmarshalJSON(data []byte) error {
// newPRMExactReference is NewPRMExactReference, except it resturns the private type. // newPRMExactReference is NewPRMExactReference, except it resturns the private type.
func newPRMExactReference(dockerReference string) (*prmExactReference, error) { func newPRMExactReference(dockerReference string) (*prmExactReference, error) {
ref, err := reference.ParseNamed(dockerReference) ref, err := reference.ParseNormalizedNamed(dockerReference)
if err != nil { if err != nil {
return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerReference %s: %s", dockerReference, err.Error())) return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerReference %s: %s", dockerReference, err.Error()))
} }
@ -686,7 +683,7 @@ func (prm *prmExactReference) UnmarshalJSON(data []byte) error {
// newPRMExactRepository is NewPRMExactRepository, except it resturns the private type. // newPRMExactRepository is NewPRMExactRepository, except it resturns the private type.
func newPRMExactRepository(dockerRepository string) (*prmExactRepository, error) { func newPRMExactRepository(dockerRepository string) (*prmExactRepository, error) {
if _, err := reference.ParseNamed(dockerRepository); err != nil { if _, err := reference.ParseNormalizedNamed(dockerRepository); err != nil {
return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerRepository %s: %s", dockerRepository, err.Error())) return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerRepository %s: %s", dockerRepository, err.Error()))
} }
return &prmExactRepository{ return &prmExactRepository{

View file

@ -9,6 +9,8 @@ import (
"github.com/containers/image/directory" "github.com/containers/image/directory"
"github.com/containers/image/docker" "github.com/containers/image/docker"
// this import is needed where we use the "atomic" transport in TestPolicyUnmarshalJSON
_ "github.com/containers/image/openshift"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -244,6 +246,11 @@ func TestPolicyUnmarshalJSON(t *testing.T) {
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RHatomic"), NewPRMMatchRepository()), xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RHatomic"), NewPRMMatchRepository()),
}, },
}, },
"unknown": {
"registry.access.redhat.com/rhel7": []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RHatomic"), NewPRMMatchRepository()),
},
},
}, },
} }
validJSON, err := json.Marshal(validPolicy) validJSON, err := json.Marshal(validPolicy)
@ -269,9 +276,6 @@ func TestPolicyUnmarshalJSON(t *testing.T) {
func(v mSI) { v["transports"] = []string{} }, func(v mSI) { v["transports"] = []string{} },
// "default" is an invalid PolicyRequirements // "default" is an invalid PolicyRequirements
func(v mSI) { v["default"] = PolicyRequirements{} }, func(v mSI) { v["default"] = PolicyRequirements{} },
// A key in "transports" is an invalid transport name
func(v mSI) { x(v, "transports")["this is unknown"] = x(v, "transports")["docker"] },
func(v mSI) { x(v, "transports")[""] = x(v, "transports")["docker"] },
} }
for _, fn := range breakFns { for _, fn := range breakFns {
err = tryUnmarshalModifiedPolicy(t, &p, validJSON, fn) err = tryUnmarshalModifiedPolicy(t, &p, validJSON, fn)

View file

@ -17,7 +17,7 @@ import (
// dirImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference. // dirImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference.
// The caller must call .Close() on the returned UnparsedImage. // The caller must call .Close() on the returned UnparsedImage.
func dirImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage { func dirImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage {
ref, err := reference.ParseNamed(dockerReference) ref, err := reference.ParseNormalizedNamed(dockerReference)
require.NoError(t, err) require.NoError(t, err)
return dirImageMockWithRef(t, dir, refImageReferenceMock{ref}) return dirImageMockWithRef(t, dir, refImageReferenceMock{ref})
} }

View file

@ -5,8 +5,10 @@ import (
"os" "os"
"testing" "testing"
"github.com/containers/image/docker"
"github.com/containers/image/docker/policyconfiguration" "github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -103,6 +105,37 @@ func (ref pcImageReferenceMock) DeleteImage(ctx *types.SystemContext) error {
panic("unexpected call to a mock function") panic("unexpected call to a mock function")
} }
func TestPolicyContextRequirementsForImageRefNotRegisteredTransport(t *testing.T) {
transports.Delete("docker")
assert.Nil(t, transports.Get("docker"))
defer func() {
assert.Nil(t, transports.Get("docker"))
transports.Register(docker.Transport)
assert.NotNil(t, transports.Get("docker"))
}()
pr := []PolicyRequirement{
xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()),
}
policy := &Policy{
Default: PolicyRequirements{NewPRReject()},
Transports: map[string]PolicyTransportScopes{
"docker": {
"registry.access.redhat.com": pr,
},
},
}
pc, err := NewPolicyContext(policy)
require.NoError(t, err)
ref, err := reference.ParseNormalizedNamed("registry.access.redhat.com/rhel7:latest")
require.NoError(t, err)
reqs := pc.requirementsForImageRef(pcImageReferenceMock{"docker", ref})
assert.True(t, &(reqs[0]) == &(pr[0]))
assert.True(t, len(reqs) == len(pr))
}
func TestPolicyContextRequirementsForImageRef(t *testing.T) { func TestPolicyContextRequirementsForImageRef(t *testing.T) {
ktGPG := SBKeyTypeGPGKeys ktGPG := SBKeyTypeGPGKeys
prm := NewPRMMatchRepoDigestOrExact() prm := NewPRMMatchRepoDigestOrExact()
@ -159,7 +192,7 @@ func TestPolicyContextRequirementsForImageRef(t *testing.T) {
expected = policy.Default expected = policy.Default
} }
ref, err := reference.ParseNamed(c.input) ref, err := reference.ParseNormalizedNamed(c.input)
require.NoError(t, err) require.NoError(t, err)
reqs := pc.requirementsForImageRef(pcImageReferenceMock{c.inputTransport, ref}) reqs := pc.requirementsForImageRef(pcImageReferenceMock{c.inputTransport, ref})
comment := fmt.Sprintf("case %s:%s: %#v", c.inputTransport, c.input, reqs[0]) comment := fmt.Sprintf("case %s:%s: %#v", c.inputTransport, c.input, reqs[0])
@ -174,7 +207,7 @@ func TestPolicyContextRequirementsForImageRef(t *testing.T) {
// pcImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference and implementing PolicyConfigurationIdentity/PolicyConfigurationNamespaces. // pcImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference and implementing PolicyConfigurationIdentity/PolicyConfigurationNamespaces.
// The caller must call .Close() on the returned Image. // The caller must call .Close() on the returned Image.
func pcImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage { func pcImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage {
ref, err := reference.ParseNamed(dockerReference) ref, err := reference.ParseNormalizedNamed(dockerReference)
require.NoError(t, err) require.NoError(t, err)
return dirImageMockWithRef(t, dir, pcImageReferenceMock{"docker", ref}) return dirImageMockWithRef(t, dir, pcImageReferenceMock{"docker", ref})
} }

View file

@ -17,7 +17,7 @@ func parseImageAndDockerReference(image types.UnparsedImage, s2 string) (referen
return nil, nil, PolicyRequirementError(fmt.Sprintf("Docker reference match attempted on image %s with no known Docker reference identity", return nil, nil, PolicyRequirementError(fmt.Sprintf("Docker reference match attempted on image %s with no known Docker reference identity",
transports.ImageName(image.Reference()))) transports.ImageName(image.Reference())))
} }
r2, err := reference.ParseNamed(s2) r2, err := reference.ParseNormalizedNamed(s2)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -69,11 +69,11 @@ func (prm *prmMatchRepository) matchesDockerReference(image types.UnparsedImage,
// parseDockerReferences converts two reference strings into parsed entities, failing on any error // parseDockerReferences converts two reference strings into parsed entities, failing on any error
func parseDockerReferences(s1, s2 string) (reference.Named, reference.Named, error) { func parseDockerReferences(s1, s2 string) (reference.Named, reference.Named, error) {
r1, err := reference.ParseNamed(s1) r1, err := reference.ParseNormalizedNamed(s1)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
r2, err := reference.ParseNamed(s2) r2, err := reference.ParseNormalizedNamed(s2)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View file

@ -6,7 +6,6 @@ import (
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -26,12 +25,12 @@ func TestParseImageAndDockerReference(t *testing.T) {
bad2 = "" bad2 = ""
) )
// Success // Success
ref, err := reference.ParseNamed(ok1) ref, err := reference.ParseNormalizedNamed(ok1)
require.NoError(t, err) require.NoError(t, err)
r1, r2, err := parseImageAndDockerReference(refImageMock{ref}, ok2) r1, r2, err := parseImageAndDockerReference(refImageMock{ref}, ok2)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, ok1, r1.String()) assert.Equal(t, ok1, reference.FamiliarString(r1))
assert.Equal(t, ok2, r2.String()) assert.Equal(t, ok2, reference.FamiliarString(r2))
// Unidentified images are rejected. // Unidentified images are rejected.
_, _, err = parseImageAndDockerReference(refImageMock{nil}, ok2) _, _, err = parseImageAndDockerReference(refImageMock{nil}, ok2)
@ -44,7 +43,7 @@ func TestParseImageAndDockerReference(t *testing.T) {
{ok1, bad2}, {ok1, bad2},
{bad1, bad2}, {bad1, bad2},
} { } {
ref, err := reference.ParseNamed(refs[0]) ref, err := reference.ParseNormalizedNamed(refs[0])
if err == nil { if err == nil {
_, _, err := parseImageAndDockerReference(refImageMock{ref}, refs[1]) _, _, err := parseImageAndDockerReference(refImageMock{ref}, refs[1])
assert.Error(t, err) assert.Error(t, err)
@ -58,7 +57,7 @@ type refImageMock struct{ reference.Named }
func (ref refImageMock) Reference() types.ImageReference { func (ref refImageMock) Reference() types.ImageReference {
return refImageReferenceMock{ref.Named} return refImageReferenceMock{ref.Named}
} }
func (ref refImageMock) Close() { func (ref refImageMock) Close() error {
panic("unexpected call to a mock function") panic("unexpected call to a mock function")
} }
func (ref refImageMock) Manifest() ([]byte, string, error) { func (ref refImageMock) Manifest() ([]byte, string, error) {
@ -72,7 +71,7 @@ func (ref refImageMock) Signatures() ([][]byte, error) {
type refImageReferenceMock struct{ reference.Named } type refImageReferenceMock struct{ reference.Named }
func (ref refImageReferenceMock) Transport() types.ImageTransport { func (ref refImageReferenceMock) Transport() types.ImageTransport {
// We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that. // We use this in error messages, so sady we must return something. But right now we do so only when DockerReference is nil, so restrict to that.
if ref.Named == nil { if ref.Named == nil {
return nameImageTransportMock("== Transport mock") return nameImageTransportMock("== Transport mock")
} }
@ -148,14 +147,12 @@ var prmExactMatchTestTable = []prmSymmetricTableTest{
{"busybox", "busybox:latest", false}, {"busybox", "busybox:latest", false},
{"busybox", "busybox" + digestSuffix, false}, {"busybox", "busybox" + digestSuffix, false},
{"busybox", "busybox", false}, {"busybox", "busybox", false},
// References with both tags and digests: `reference.WithName` essentially drops the tag. // References with both tags and digests: We match them exactly (requiring BOTH to match)
// This is not _particularly_ desirable but it is the semantics used throughout containers/image; at least, with the digest it is clear which image the reference means,
// even if the tag may reflect a different user intent.
// NOTE: Again, this is not documented behavior; the recommendation is to sign tags, not digests, and then tag-and-digest references wont match the signed identity. // NOTE: Again, this is not documented behavior; the recommendation is to sign tags, not digests, and then tag-and-digest references wont match the signed identity.
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true}, {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false}, {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true}, // Ugly. Do not rely on this. {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, false},
{"busybox:latest" + digestSuffix, "busybox" + digestSuffix, true}, // Ugly. Do not rely on this. {"busybox:latest" + digestSuffix, "busybox" + digestSuffix, false},
{"busybox:latest" + digestSuffix, "busybox:latest", false}, {"busybox:latest" + digestSuffix, "busybox:latest", false},
// Invalid format // Invalid format
{"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false}, {"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false},
@ -194,7 +191,7 @@ var prmRepositoryMatchTestTable = []prmSymmetricTableTest{
{"hostname/library/busybox:latest", "busybox:notlatest", false}, {"hostname/library/busybox:latest", "busybox:notlatest", false},
{"busybox:latest", fullRHELRef, false}, {"busybox:latest", fullRHELRef, false},
{"busybox" + digestSuffix, "notbusybox" + digestSuffix, false}, {"busybox" + digestSuffix, "notbusybox" + digestSuffix, false},
// References with both tags and digests: `reference.WithName` essentially drops the tag, and we ignore both anyway. // References with both tags and digests: We ignore both anyway.
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true}, {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true}, {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true}, {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true},
@ -209,8 +206,8 @@ var prmRepositoryMatchTestTable = []prmSymmetricTableTest{
func testImageAndSig(t *testing.T, prm PolicyReferenceMatch, imageRef, sigRef string, result bool) { func testImageAndSig(t *testing.T, prm PolicyReferenceMatch, imageRef, sigRef string, result bool) {
// This assumes that all ways to obtain a reference.Named perform equivalent validation, // This assumes that all ways to obtain a reference.Named perform equivalent validation,
// and therefore values refused by reference.ParseNamed can not happen in practice. // and therefore values refused by reference.ParseNormalizedNamed can not happen in practice.
parsedImageRef, err := reference.ParseNamed(imageRef) parsedImageRef, err := reference.ParseNormalizedNamed(imageRef)
if err != nil { if err != nil {
return return
} }
@ -272,14 +269,12 @@ func TestPMMMatchRepoDigestOrExactMatchesDockerReference(t *testing.T) {
// Digest references accept any signature with matching repository. // Digest references accept any signature with matching repository.
{"busybox" + digestSuffix, "busybox:latest", true}, {"busybox" + digestSuffix, "busybox:latest", true},
{"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.) {"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.)
// References with both tags and digests: `reference.WithName` essentially drops the tag. // References with both tags and digests: We match them exactly (requiring BOTH to match).
// This is not _particularly_ desirable but it is the semantics used throughout containers/image; at least, with the digest it is clear which image the reference means, {"busybox:latest" + digestSuffix, "busybox:latest", false},
// even if the tag may reflect a different user intent. {"busybox:latest" + digestSuffix, "busybox:notlatest", false},
{"busybox:latest" + digestSuffix, "busybox:latest", true},
{"busybox:latest" + digestSuffix, "busybox:notlatest", true},
{"busybox:latest", "busybox:latest" + digestSuffix, false}, {"busybox:latest", "busybox:latest" + digestSuffix, false},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.) {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffixOther, true}, // Ugly. Do not rely on this. {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffixOther, false},
} { } {
testImageAndSig(t, prm, test.imageRef, test.sigRef, test.result) testImageAndSig(t, prm, test.imageRef, test.sigRef, test.result)
} }
@ -307,8 +302,8 @@ func TestParseDockerReferences(t *testing.T) {
// Success // Success
r1, r2, err := parseDockerReferences(ok1, ok2) r1, r2, err := parseDockerReferences(ok1, ok2)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, ok1, r1.String()) assert.Equal(t, ok1, reference.FamiliarString(r1))
assert.Equal(t, ok2, r2.String()) assert.Equal(t, ok2, reference.FamiliarString(r2))
// Failures // Failures
for _, refs := range [][]string{ for _, refs := range [][]string{
@ -327,7 +322,7 @@ type forbiddenImageMock struct{}
func (ref forbiddenImageMock) Reference() types.ImageReference { func (ref forbiddenImageMock) Reference() types.ImageReference {
panic("unexpected call to a mock function") panic("unexpected call to a mock function")
} }
func (ref forbiddenImageMock) Close() { func (ref forbiddenImageMock) Close() error {
panic("unexpected call to a mock function") panic("unexpected call to a mock function")
} }
func (ref forbiddenImageMock) Manifest() ([]byte, string, error) { func (ref forbiddenImageMock) Manifest() ([]byte, string, error) {

View file

@ -118,10 +118,12 @@ func (s storageImageDestination) Reference() types.ImageReference {
return s.imageRef return s.imageRef
} }
func (s storageImageSource) Close() { func (s storageImageSource) Close() error {
return nil
} }
func (s storageImageDestination) Close() { func (s storageImageDestination) Close() error {
return nil
} }
func (s storageImageDestination) ShouldCompressLayers() bool { func (s storageImageDestination) ShouldCompressLayers() bool {

View file

@ -87,7 +87,7 @@ func (s storageReference) PolicyConfigurationNamespaces() []string {
// The reference without the ID is also a valid namespace. // The reference without the ID is also a valid namespace.
namespaces = append(namespaces, storeSpec+s.reference) namespaces = append(namespaces, storeSpec+s.reference)
} }
components := strings.Split(s.name.FullName(), "/") components := strings.Split(s.name.Name(), "/")
for len(components) > 0 { for len(components) > 0 {
namespaces = append(namespaces, storeSpec+strings.Join(components, "/")) namespaces = append(namespaces, storeSpec+strings.Join(components, "/"))
components = components[:len(components)-1] components = components[:len(components)-1]

View file

@ -23,7 +23,7 @@ func TestStorageReferenceDockerReference(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
dr := ref.DockerReference() dr := ref.DockerReference()
require.NotNil(t, dr) require.NotNil(t, dr)
assert.Equal(t, "busybox:latest", dr.String()) assert.Equal(t, "docker.io/library/busybox:latest", dr.String())
ref, err = Transport.ParseReference("@" + sha256digestHex) ref, err = Transport.ParseReference("@" + sha256digestHex)
require.NoError(t, err) require.NoError(t, err)

View file

@ -9,12 +9,17 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/transports"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/storage/storage" "github.com/containers/storage/storage"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ddigest "github.com/opencontainers/go-digest" ddigest "github.com/opencontainers/go-digest"
) )
func init() {
transports.Register(Transport)
}
var ( var (
// Transport is an ImageTransport that uses either a default // Transport is an ImageTransport that uses either a default
// storage.Store or one that's it's explicitly told to use. // storage.Store or one that's it's explicitly told to use.
@ -83,14 +88,14 @@ func (s storageTransport) ParseStoreReference(store storage.Store, ref string) (
refInfo := strings.SplitN(ref, "@", 2) refInfo := strings.SplitN(ref, "@", 2)
if len(refInfo) == 1 { if len(refInfo) == 1 {
// A name. // A name.
name, err = reference.ParseNamed(refInfo[0]) name, err = reference.ParseNormalizedNamed(refInfo[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else if len(refInfo) == 2 { } else if len(refInfo) == 2 {
// An ID, possibly preceded by a name. // An ID, possibly preceded by a name.
if refInfo[0] != "" { if refInfo[0] != "" {
name, err = reference.ParseNamed(refInfo[0]) name, err = reference.ParseNormalizedNamed(refInfo[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -111,7 +116,7 @@ func (s storageTransport) ParseStoreReference(store storage.Store, ref string) (
} }
refname := "" refname := ""
if name != nil { if name != nil {
name = reference.WithDefaultTag(name) name = reference.TagNameOnly(name)
refname = verboseName(name) refname = verboseName(name)
} }
if refname == "" { if refname == "" {
@ -257,12 +262,12 @@ func (s storageTransport) ValidatePolicyConfigurationScope(scope string) error {
// that are just bare IDs. // that are just bare IDs.
scopeInfo := strings.SplitN(scope, "@", 2) scopeInfo := strings.SplitN(scope, "@", 2)
if len(scopeInfo) == 1 && scopeInfo[0] != "" { if len(scopeInfo) == 1 && scopeInfo[0] != "" {
_, err := reference.ParseNamed(scopeInfo[0]) _, err := reference.ParseNormalizedNamed(scopeInfo[0])
if err != nil { if err != nil {
return err return err
} }
} else if len(scopeInfo) == 2 && scopeInfo[0] != "" && scopeInfo[1] != "" { } else if len(scopeInfo) == 2 && scopeInfo[0] != "" && scopeInfo[1] != "" {
_, err := reference.ParseNamed(scopeInfo[0]) _, err := reference.ParseNormalizedNamed(scopeInfo[0])
if err != nil { if err != nil {
return err return err
} }
@ -277,10 +282,10 @@ func (s storageTransport) ValidatePolicyConfigurationScope(scope string) error {
} }
func verboseName(name reference.Named) string { func verboseName(name reference.Named) string {
name = reference.WithDefaultTag(name) name = reference.TagNameOnly(name)
tag := "" tag := ""
if tagged, ok := name.(reference.NamedTagged); ok { if tagged, ok := name.(reference.NamedTagged); ok {
tag = tagged.Tag() tag = tagged.Tag()
} }
return name.FullName() + ":" + tag return name.Name() + ":" + tag
} }

View file

@ -54,7 +54,7 @@ func TestTransportParseStoreReference(t *testing.T) {
if c.expectedRef == "" { if c.expectedRef == "" {
assert.Nil(t, storageRef.name, c.input) assert.Nil(t, storageRef.name, c.input)
} else { } else {
dockerRef, err := reference.ParseNamed(c.expectedRef) dockerRef, err := reference.ParseNormalizedNamed(c.expectedRef)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, storageRef.name, c.input) require.NotNil(t, storageRef.name, c.input)
assert.Equal(t, dockerRef.String(), storageRef.name.String()) assert.Equal(t, dockerRef.String(), storageRef.name.String())

View file

@ -0,0 +1,31 @@
package alltransports
import (
"strings"
// register all known transports
// NOTE: Make sure docs/policy.json.md is updated when adding or updating
// a transport.
_ "github.com/containers/image/directory"
_ "github.com/containers/image/docker"
_ "github.com/containers/image/docker/daemon"
_ "github.com/containers/image/oci/layout"
_ "github.com/containers/image/openshift"
_ "github.com/containers/image/storage"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/pkg/errors"
)
// ParseImageName converts a URL-like image name to a types.ImageReference.
func ParseImageName(imgName string) (types.ImageReference, error) {
parts := strings.SplitN(imgName, ":", 2)
if len(parts) != 2 {
return nil, errors.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, imgName)
}
transport := transports.Get(parts[0])
if transport == nil {
return nil, errors.Errorf(`Invalid image name "%s", unknown transport "%s"`, imgName, parts[0])
}
return transport.ParseReference(parts[1])
}

View file

@ -1,17 +1,13 @@
package transports package alltransports
import ( import (
"testing" "testing"
"github.com/containers/image/transports"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestKnownTransports(t *testing.T) {
assert.NotNil(t, KnownTransports) // Ensure that the initialization has actually been run
assert.True(t, len(KnownTransports) > 1)
}
func TestParseImageName(t *testing.T) { func TestParseImageName(t *testing.T) {
// This primarily tests error handling, TestImageNameHandling is a table-driven // This primarily tests error handling, TestImageNameHandling is a table-driven
// test for the expected values. // test for the expected values.
@ -36,11 +32,12 @@ func TestImageNameHandling(t *testing.T) {
{"docker-daemon", "busybox:latest", "busybox:latest"}, {"docker-daemon", "busybox:latest", "busybox:latest"},
{"oci", "/etc:sometag", "/etc:sometag"}, {"oci", "/etc:sometag", "/etc:sometag"},
// "atomic" not tested here because it depends on per-user configuration for the default cluster. // "atomic" not tested here because it depends on per-user configuration for the default cluster.
// "containers-storage" not tested here because it needs to initialize various directories on the fs.
} { } {
fullInput := c.transport + ":" + c.input fullInput := c.transport + ":" + c.input
ref, err := ParseImageName(fullInput) ref, err := ParseImageName(fullInput)
require.NoError(t, err, fullInput) require.NoError(t, err, fullInput)
s := ImageName(ref) s := transports.ImageName(ref)
assert.Equal(t, c.transport+":"+c.roundtrip, s, fullInput) assert.Equal(t, c.transport+":"+c.roundtrip, s, fullInput)
} }
} }

View file

@ -2,52 +2,61 @@ package transports
import ( import (
"fmt" "fmt"
"strings" "sync"
"github.com/containers/image/directory"
"github.com/containers/image/docker"
"github.com/containers/image/docker/daemon"
ociLayout "github.com/containers/image/oci/layout"
"github.com/containers/image/openshift"
"github.com/containers/image/storage"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/pkg/errors"
) )
// KnownTransports is a registry of known ImageTransport instances. // knownTransports is a registry of known ImageTransport instances.
var KnownTransports map[string]types.ImageTransport type knownTransports struct {
transports map[string]types.ImageTransport
mu sync.Mutex
}
func (kt *knownTransports) Get(k string) types.ImageTransport {
kt.mu.Lock()
t := kt.transports[k]
kt.mu.Unlock()
return t
}
func (kt *knownTransports) Remove(k string) {
kt.mu.Lock()
delete(kt.transports, k)
kt.mu.Unlock()
}
func (kt *knownTransports) Add(t types.ImageTransport) {
kt.mu.Lock()
defer kt.mu.Unlock()
name := t.Name()
if t := kt.transports[name]; t != nil {
panic(fmt.Sprintf("Duplicate image transport name %s", name))
}
kt.transports[name] = t
}
var kt *knownTransports
func init() { func init() {
KnownTransports = make(map[string]types.ImageTransport) kt = &knownTransports{
// NOTE: Make sure docs/policy.json.md is updated when adding or updating transports: make(map[string]types.ImageTransport),
// a transport.
for _, t := range []types.ImageTransport{
directory.Transport,
docker.Transport,
daemon.Transport,
ociLayout.Transport,
openshift.Transport,
storage.Transport,
} {
name := t.Name()
if _, ok := KnownTransports[name]; ok {
panic(fmt.Sprintf("Duplicate image transport name %s", name))
}
KnownTransports[name] = t
} }
} }
// ParseImageName converts a URL-like image name to a types.ImageReference. // Get returns the transport specified by name or nil when unavailable.
func ParseImageName(imgName string) (types.ImageReference, error) { func Get(name string) types.ImageTransport {
parts := strings.SplitN(imgName, ":", 2) return kt.Get(name)
if len(parts) != 2 { }
return nil, errors.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, imgName)
} // Delete deletes a transport from the registered transports.
transport, ok := KnownTransports[parts[0]] func Delete(name string) {
if !ok { kt.Remove(name)
return nil, errors.Errorf(`Invalid image name "%s", unknown transport "%s"`, imgName, parts[0]) }
}
return transport.ParseReference(parts[1]) // Register registers a transport.
func Register(t types.ImageTransport) {
kt.Add(t)
} }
// ImageName converts a types.ImageReference into an URL-like image name, which MUST be such that // ImageName converts a types.ImageReference into an URL-like image name, which MUST be such that

View file

@ -4,10 +4,9 @@ import (
"io" "io"
"time" "time"
"github.com/pkg/errors"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
) )
// ImageTransport is a top-level namespace for ways to to store/load an image. // ImageTransport is a top-level namespace for ways to to store/load an image.
@ -111,7 +110,7 @@ type ImageSource interface {
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. // (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
Reference() ImageReference Reference() ImageReference
// Close removes resources associated with an initialized ImageSource, if any. // Close removes resources associated with an initialized ImageSource, if any.
Close() Close() error
// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
// It may use a remote (= slow) service. // It may use a remote (= slow) service.
GetManifest() ([]byte, string, error) GetManifest() ([]byte, string, error)
@ -139,7 +138,7 @@ type ImageDestination interface {
// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects. // e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects.
Reference() ImageReference Reference() ImageReference
// Close removes resources associated with an initialized ImageDestination, if any. // Close removes resources associated with an initialized ImageDestination, if any.
Close() Close() error
// SupportedManifestMIMETypes tells which manifest mime types the destination supports // SupportedManifestMIMETypes tells which manifest mime types the destination supports
// If an empty slice or nil it's returned, then any mime type can be tried to upload // If an empty slice or nil it's returned, then any mime type can be tried to upload
@ -185,7 +184,7 @@ type UnparsedImage interface {
// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. // (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image.
Reference() ImageReference Reference() ImageReference
// Close removes resources associated with an initialized UnparsedImage, if any. // Close removes resources associated with an initialized UnparsedImage, if any.
Close() Close() error
// Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need. // Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need.
Manifest() ([]byte, string, error) Manifest() ([]byte, string, error)
// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need. // Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
@ -295,6 +294,13 @@ type SystemContext struct {
DockerDisableV1Ping bool DockerDisableV1Ping bool
} }
// ProgressProperties is used to pass information from the copy code to a monitor which
// can use the real-time information to produce output or react to changes.
type ProgressProperties struct {
Artifact BlobInfo
Offset uint64
}
var ( var (
// ErrBlobNotFound can be returned by an ImageDestination's HasBlob() method // ErrBlobNotFound can be returned by an ImageDestination's HasBlob() method
ErrBlobNotFound = errors.New("no such blob present") ErrBlobNotFound = errors.New("no such blob present")

31
vendor/github.com/containers/image/vendor.conf generated vendored Normal file
View file

@ -0,0 +1,31 @@
github.com/Sirupsen/logrus 7f4b1adc791766938c29457bed0703fb9134421a
github.com/containers/storage 5cbbc6bafb45bd7ef10486b673deb3b81bb3b787
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
github.com/docker/distribution df5327f76fb6468b84a87771e361762b8be23fdb
github.com/docker/docker 75843d36aa5c3eaade50da005f9e0ff2602f3d5e
github.com/docker/go-connections 7da10c8c50cad14494ec818dcdfb6506265c0086
github.com/docker/go-units 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
github.com/docker/libtrust aabc10ec26b754e797f9028f4589c5b7bd90dc20
github.com/ghodss/yaml 04f313413ffd65ce25f2541bfd2b2ceec5c0908c
github.com/gorilla/context 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
github.com/gorilla/mux 94e7d24fd285520f3d12ae998f7fdd6b5393d453
github.com/imdario/mergo 50d4dbd4eb0e84778abe37cefef140271d96fade
github.com/mattn/go-runewidth 14207d285c6c197daabb5c9793d63e7af9ab2d50
github.com/mattn/go-shellwords 005a0944d84452842197c2108bd9168ced206f78
github.com/mistifyio/go-zfs c0224de804d438efd11ea6e52ada8014537d6062
github.com/mtrmac/gpgme b2432428689ca58c2b8e8dea9449d3295cf96fc9
github.com/opencontainers/go-digest aa2ec055abd10d26d539eb630a92241b781ce4bc
github.com/opencontainers/image-spec v1.0.0-rc4
github.com/opencontainers/runc 6b1d0e76f239ffb435445e5ae316d2676c07c6e3
github.com/pborman/uuid 1b00554d822231195d1babd97ff4a781231955c9
github.com/pkg/errors 248dadf4e9068a0b3e79f02ed0a610d935de5302
github.com/pmezard/go-difflib 792786c7400a136282c1664665ae0a8db921c6c2
github.com/stretchr/testify 4d4bfba8f1d1027c4fdbe371823030df51419987
github.com/vbatts/tar-split bd4c5d64c3e9297f410025a3b1bd0c58f659e721
golang.org/x/crypto 453249f01cfeb54c3d549ddb75ff152ca243f9d8
golang.org/x/net 6b27048ae5e6ad1ef927e72e437531493de612fe
golang.org/x/sys 075e574b89e4c2d22f2286a7e2b919519c6f3547
gopkg.in/cheggaaa/pb.v1 d7e6ca3010b6f084d8056847f55d7f572f180678
gopkg.in/yaml.v2 a3f3340b5840cee44f372bddb5880fcbc419b46a
k8s.io/client-go bcde30fb7eaed76fd98a36b4120321b94995ffb6
github.com/xeipuuv/gojsonschema master