diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 0b714b31..6761fd93 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -56,6 +56,11 @@ func createApp() *cli.App { Value: "", Usage: "Path to a trust policy file", }, + cli.StringFlag{ + Name: "registries.d", + Value: "", + Usage: "use registry configuration files in `DIR` (e.g. for docker signature storage)", + }, } app.Before = func(c *cli.Context) error { if c.GlobalBool("debug") { diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 93f9e331..c5535853 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -8,10 +8,10 @@ import ( // contextFromGlobalOptions returns a types.SystemContext depending on c. func contextFromGlobalOptions(c *cli.Context) *types.SystemContext { - certPath := c.GlobalString("cert-path") tlsVerify := c.GlobalBool("tls-verify") // FIXME!! defaults to false return &types.SystemContext{ - DockerCertPath: certPath, + RegistriesDirPath: c.GlobalString("registries.d"), + DockerCertPath: c.GlobalString("cert-path"), DockerInsecureSkipTLSVerify: !tlsVerify, } } diff --git a/docs/skopeo.1.md b/docs/skopeo.1.md index 9dbce05c..ac0644f9 100644 --- a/docs/skopeo.1.md +++ b/docs/skopeo.1.md @@ -45,6 +45,8 @@ Most commands refer to container images, using a _transport_`:`_details_ format. **--policy** _path-to-policy_ Path to a policy.json file to use for verifying signatures and deciding whether an image is trusted, overriding the default trust policy file. + **--registries.d** _dir_ use registry configuration files in _dir_ (e.g. for docker signature storage), overriding the default path. + **--tls-verify** _bool-value_ Verify certificates **--help**|**-h** Show help @@ -64,7 +66,7 @@ Uses the system's trust policy to validate images, rejects images not trusted by _destination-image_ use the "image name" format described above - **--remove-signatures** do not copy signatures, if any, from _source-image_. Necessary when copying a signed image to a destination which does not support signatures. + **--remove-signatures** do not copy signatures, if any, from _source-image_. Necessary when copying a signed image to a destination which does not support signatures. **--sign-by=**_key-id_ add a signature using that key ID for an image name corresponding to _destination-image_ @@ -100,7 +102,7 @@ Get image layers of _image-name_ ## skopeo manifest-digest **skopeo manifest-digest** _manifest-file_ -Compute a manifest digest of _manifest-file_ and write it to standard output. +Compute a manifest digest of _manifest-file_ and write it to standard output. ## skopeo standalone-sign **skopeo standalone-sign** _manifest docker-reference key-fingerprint_ **--output**|**-o** _signature_ @@ -139,6 +141,10 @@ show help for `skopeo` Default trust policy file, if **--policy** is not specified. The policy format is documented in https://github.com/containers/image/blob/master/docs/policy.json.md . + **/etc/containers/registries.d** + Default directory containing registry configuration, if **--registries.d** is not specified. + The contents of this directory are documented in https://github.com/containers/image/blob/master/docs/registries.d.md . + # EXAMPLES ## skopeo copy diff --git a/vendor/github.com/containers/image/docker/docker_client.go b/vendor/github.com/containers/image/docker/docker_client.go index f5b31841..05f937de 100644 --- a/vendor/github.com/containers/image/docker/docker_client.go +++ b/vendor/github.com/containers/image/docker/docker_client.go @@ -42,17 +42,17 @@ type dockerClient struct { wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty scheme string // Cache of a value returned by a successful ping() if not empty client *http.Client + signatureBase signatureStorageBase } // newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry) -func newDockerClient(ctx *types.SystemContext, refHostname string) (*dockerClient, error) { - var registry string - if refHostname == dockerHostname { +// “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) (*dockerClient, error) { + registry := ref.ref.Hostname() + if registry == dockerHostname { registry = dockerRegistry - } else { - registry = refHostname } - username, password, err := getAuth(refHostname) + username, password, err := getAuth(ref.ref.Hostname()) if err != nil { return nil, err } @@ -78,11 +78,18 @@ func newDockerClient(ctx *types.SystemContext, refHostname string) (*dockerClien if tr != nil { client.Transport = tr } + + sigBase, err := configuredSignatureStorageBase(ctx, ref, write) + if err != nil { + return nil, err + } + return &dockerClient{ - registry: registry, - username: username, - password: password, - client: client, + registry: registry, + username: username, + password: password, + client: client, + signatureBase: sigBase, }, nil } diff --git a/vendor/github.com/containers/image/docker/docker_image_dest.go b/vendor/github.com/containers/image/docker/docker_image_dest.go index b0fd5f68..a16cf3fb 100644 --- a/vendor/github.com/containers/image/docker/docker_image_dest.go +++ b/vendor/github.com/containers/image/docker/docker_image_dest.go @@ -8,6 +8,9 @@ import ( "io" "io/ioutil" "net/http" + "net/url" + "os" + "path/filepath" "strconv" "github.com/Sirupsen/logrus" @@ -18,11 +21,13 @@ import ( type dockerImageDestination struct { ref dockerReference c *dockerClient + // State + manifestDigest string // or "" if not yet known. } // newImageDestination creates a new ImageDestination for the specified image reference. func newImageDestination(ctx *types.SystemContext, ref dockerReference) (types.ImageDestination, error) { - c, err := newDockerClient(ctx, ref.ref.Hostname()) + c, err := newDockerClient(ctx, ref, true) if err != nil { return nil, err } @@ -144,6 +149,7 @@ func (d *dockerImageDestination) PutManifest(m []byte) error { if err != nil { return err } + d.manifestDigest = digest url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), digest) headers := map[string][]string{} @@ -168,12 +174,91 @@ func (d *dockerImageDestination) PutManifest(m []byte) error { } func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error { - if len(signatures) != 0 { - return fmt.Errorf("Pushing signatures to a Docker Registry is not supported") + // FIXME? This overwrites files one at a time, definitely not atomic. + // A failure when updating signatures with a reordered copy could lose some of them. + + // Skip dealing with the manifest digest if not necessary. + if len(signatures) == 0 { + return nil } + if d.c.signatureBase == nil { + return fmt.Errorf("Pushing signatures to a Docker Registry is not supported, and there is no applicable signature storage configured") + } + + // FIXME: This assumption that signatures are stored after the manifest rather breaks the model. + if d.manifestDigest == "" { + return fmt.Errorf("Unknown manifest digest, can't add signatures") + } + + for i, signature := range signatures { + url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i) + if url == nil { + return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil") + } + err := d.putOneSignature(url, signature) + if err != nil { + return err + } + } + // Remove any other signatures, if present. + // We stop at the first missing signature; if a previous deleting loop aborted + // prematurely, this may not clean up all of them, but one missing signature + // is enough for dockerImageSource to stop looking for other signatures, so that + // is sufficient. + for i := len(signatures); ; i++ { + url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i) + if url == nil { + return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil") + } + missing, err := d.c.deleteOneSignature(url) + if err != nil { + return err + } + if missing { + break + } + } + return nil } +// putOneSignature stores one signature to url. +func (d *dockerImageDestination) putOneSignature(url *url.URL, signature []byte) error { + switch url.Scheme { + case "file": + logrus.Debugf("Writing to %s", url.Path) + err := os.MkdirAll(filepath.Dir(url.Path), 0755) + if err != nil { + return err + } + err = ioutil.WriteFile(url.Path, signature, 0644) + if err != nil { + return err + } + return nil + + default: + return fmt.Errorf("Unsupported scheme when writing signature to %s", url.String()) + } +} + +// deleteOneSignature deletes a signature from url, if it exists. +// If it successfully determines that the signature does not exist, returns (true, nil) +func (c *dockerClient) deleteOneSignature(url *url.URL) (missing bool, err error) { + switch url.Scheme { + case "file": + logrus.Debugf("Deleting %s", url.Path) + err := os.Remove(url.Path) + if err != nil && os.IsNotExist(err) { + return true, nil + } + return false, err + + default: + return false, fmt.Errorf("Unsupported scheme when deleting signature from %s", url.String()) + } +} + // Commit marks the process of storing the image as successful and asks for the image to be persisted. // WARNING: This does not have any transactional semantics: // - Uploaded data MAY be visible to others before Commit() is called diff --git a/vendor/github.com/containers/image/docker/docker_image_src.go b/vendor/github.com/containers/image/docker/docker_image_src.go index f0867e05..47d47f72 100644 --- a/vendor/github.com/containers/image/docker/docker_image_src.go +++ b/vendor/github.com/containers/image/docker/docker_image_src.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "mime" "net/http" + "net/url" + "os" "strconv" "github.com/Sirupsen/logrus" @@ -26,6 +28,9 @@ type dockerImageSource struct { ref dockerReference requestedManifestMIMETypes []string c *dockerClient + // State + cachedManifest []byte // nil if not loaded yet + cachedManifestMIMEType string // Only valid if cachedManifest != nil } // newImageSource creates a new ImageSource for the specified image reference, @@ -33,7 +38,7 @@ type dockerImageSource struct { // nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes. // The caller must call .Close() on the returned ImageSource. func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) { - c, err := newDockerClient(ctx, ref.ref.Hostname()) + c, err := newDockerClient(ctx, ref, false) if err != nil { return nil, err } @@ -71,10 +76,29 @@ func simplifyContentType(contentType string) string { } func (s *dockerImageSource) GetManifest() ([]byte, string, error) { - reference, err := s.ref.tagOrDigest() + err := s.ensureManifestIsLoaded() if err != nil { return nil, "", err } + return s.cachedManifest, s.cachedManifestMIMEType, nil +} + +// ensureManifestIsLoaded sets s.cachedManifest and s.cachedManifestMIMEType +// +// ImageSource implementations are not required or expected to do any caching, +// but because our signatures are “attached” to the manifest digest, +// we need to ensure that the digest of the manifest returned by GetManifest +// and used by GetSignatures are consistent, otherwise we would get spurious +// signature verification failures when pulling while a tag is being updated. +func (s *dockerImageSource) ensureManifestIsLoaded() error { + if s.cachedManifest != nil { + return nil + } + + reference, err := s.ref.tagOrDigest() + if err != nil { + return err + } url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), reference) // TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1 // TODO(runcom) NO, switch on the resulter manifest like Docker is doing @@ -82,18 +106,20 @@ func (s *dockerImageSource) GetManifest() ([]byte, string, error) { headers["Accept"] = s.requestedManifestMIMETypes res, err := s.c.makeRequest("GET", url, headers, nil) if err != nil { - return nil, "", err + return err } defer res.Body.Close() manblob, err := ioutil.ReadAll(res.Body) if err != nil { - return nil, "", err + return err } if res.StatusCode != http.StatusOK { - return nil, "", errFetchManifest{res.StatusCode, manblob} + return errFetchManifest{res.StatusCode, manblob} } // We might validate manblob against the Docker-Content-Digest header here to protect against transport errors. - return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil + s.cachedManifest = manblob + s.cachedManifestMIMEType = simplifyContentType(res.Header.Get("Content-Type")) + return nil } // GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). @@ -116,12 +142,77 @@ func (s *dockerImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) } func (s *dockerImageSource) GetSignatures() ([][]byte, error) { - return [][]byte{}, nil + if s.c.signatureBase == nil { // Skip dealing with the manifest digest if not necessary. + return [][]byte{}, nil + } + + if err := s.ensureManifestIsLoaded(); err != nil { + return nil, err + } + manifestDigest, err := manifest.Digest(s.cachedManifest) + if err != nil { + return nil, err + } + + signatures := [][]byte{} + for i := 0; ; i++ { + url := signatureStorageURL(s.c.signatureBase, manifestDigest, i) + if url == nil { + return nil, fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil") + } + signature, missing, err := s.getOneSignature(url) + if err != nil { + return nil, err + } + if missing { + break + } + signatures = append(signatures, signature) + } + return signatures, nil +} + +// getOneSignature downloads one signature from url. +// If it successfully determines that the signature does not exist, returns with missing set to true and error set to nil. +func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, missing bool, err error) { + switch url.Scheme { + case "file": + logrus.Debugf("Reading %s", url.Path) + sig, err := ioutil.ReadFile(url.Path) + if err != nil { + if os.IsNotExist(err) { + return nil, true, nil + } + return nil, false, err + } + return sig, false, nil + + case "http", "https": + logrus.Debugf("GET %s", url) + res, err := s.c.client.Get(url.String()) + if err != nil { + return nil, false, err + } + defer res.Body.Close() + if res.StatusCode == http.StatusNotFound { + return nil, true, nil + } else if res.StatusCode != http.StatusOK { + return nil, false, fmt.Errorf("Error reading signature from %s: status %d", url.String(), res.StatusCode) + } + sig, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, false, err + } + return sig, false, nil + + default: + return nil, false, fmt.Errorf("Unsupported scheme when reading signature from %s", url.String()) + } } // deleteImage deletes the named image from the registry, if supported. func deleteImage(ctx *types.SystemContext, ref dockerReference) error { - c, err := newDockerClient(ctx, ref.ref.Hostname()) + c, err := newDockerClient(ctx, ref, true) if err != nil { return err } @@ -141,7 +232,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error { return err } defer get.Body.Close() - body, err := ioutil.ReadAll(get.Body) + manifestBody, err := ioutil.ReadAll(get.Body) if err != nil { return err } @@ -150,7 +241,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error { case http.StatusNotFound: return fmt.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry.", ref.ref) default: - return fmt.Errorf("Failed to delete %v: %s (%v)", ref.ref, string(body), get.Status) + return fmt.Errorf("Failed to delete %v: %s (%v)", ref.ref, manifestBody, get.Status) } digest := get.Header.Get("Docker-Content-Digest") @@ -164,7 +255,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error { } defer delete.Body.Close() - body, err = ioutil.ReadAll(delete.Body) + body, err := ioutil.ReadAll(delete.Body) if err != nil { return err } @@ -172,5 +263,26 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error { return fmt.Errorf("Failed to delete %v: %s (%v)", deleteURL, string(body), delete.Status) } + if c.signatureBase != nil { + manifestDigest, err := manifest.Digest(manifestBody) + if err != nil { + return err + } + + for i := 0; ; i++ { + url := signatureStorageURL(c.signatureBase, manifestDigest, i) + if url == nil { + return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil") + } + missing, err := c.deleteOneSignature(url) + if err != nil { + return err + } + if missing { + break + } + } + } + return nil } diff --git a/vendor/github.com/containers/image/docker/lookaside.go b/vendor/github.com/containers/image/docker/lookaside.go new file mode 100644 index 00000000..989fc13f --- /dev/null +++ b/vendor/github.com/containers/image/docker/lookaside.go @@ -0,0 +1,198 @@ +package docker + +import ( + "fmt" + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/ghodss/yaml" + + "github.com/Sirupsen/logrus" + "github.com/containers/image/types" +) + +// systemRegistriesDirPath is the path to registries.d, used for locating lookaside Docker signature storage. +// You can override this at build time with +// -ldflags '-X github.com/containers/image/docker.systemRegistriesDirPath=$your_path' +var systemRegistriesDirPath = builtinRegistriesDirPath + +// builtinRegistriesDirPath is the path to registries.d. +// DO NOT change this, instead see systemRegistriesDirPath above. +const builtinRegistriesDirPath = "/etc/containers/registries.d" + +// registryConfiguration is one of the files in registriesDirPath configuring lookaside locations, or the result of merging them all. +// NOTE: Keep this in sync with docs/registries.d.md! +type registryConfiguration struct { + DefaultDocker *registryNamespace `json:"default-docker"` + // The key is a namespace, using fully-expanded Docker reference format or parent namespaces (per dockerReference.PolicyConfiguration*), + Docker map[string]registryNamespace `json:"docker"` +} + +// registryNamespace defines lookaside locations for a single namespace. +type registryNamespace struct { + SigStore string `json:"sigstore"` // For reading, and if SigStoreWrite is not present, for writing. + SigStoreWrite string `json:"sigstore-write"` // For writing only. +} + +// signatureStorageBase is an "opaque" type representing a lookaside Docker signature storage. +// Users outside of this file should use configuredSignatureStorageBase and signatureStorageURL below. +type signatureStorageBase *url.URL // The only documented value is nil, meaning storage is not supported. + +// configuredSignatureStorageBase reads configuration to find an appropriate signature storage URL for ref, for write access if “write”. +func configuredSignatureStorageBase(ctx *types.SystemContext, ref dockerReference, write bool) (signatureStorageBase, error) { + // FIXME? Loading and parsing the config could be cached across calls. + dirPath := registriesDirPath(ctx) + logrus.Debugf(`Using registries.d directory %s for sigstore configuration`, dirPath) + config, err := loadAndMergeConfig(dirPath) + if err != nil { + return nil, err + } + + topLevel := config.signatureTopLevel(ref, write) + if topLevel == "" { + return nil, nil + } + + url, err := url.Parse(topLevel) + if err != nil { + return nil, fmt.Errorf("Invalid signature storage URL %s: %v", topLevel, err) + } + // FIXME? Restrict to explicitly supported schemes? + repo := ref.ref.FullName() // 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 + return nil, fmt.Errorf("Unexpected path elements in Docker reference %s for signature storage", ref.ref.String()) + } + url.Path = url.Path + "/" + repo + return url, nil +} + +// registriesDirPath returns a path to registries.d +func registriesDirPath(ctx *types.SystemContext) string { + if ctx != nil { + if ctx.RegistriesDirPath != "" { + return ctx.RegistriesDirPath + } + if ctx.RootForImplicitAbsolutePaths != "" { + return filepath.Join(ctx.RootForImplicitAbsolutePaths, systemRegistriesDirPath) + } + } + return systemRegistriesDirPath +} + +// loadAndMergeConfig loads configuration files in dirPath +func loadAndMergeConfig(dirPath string) (*registryConfiguration, error) { + mergedConfig := registryConfiguration{Docker: map[string]registryNamespace{}} + dockerDefaultMergedFrom := "" + nsMergedFrom := map[string]string{} + + dir, err := os.Open(dirPath) + if err != nil { + if os.IsNotExist(err) { + return &mergedConfig, nil + } + return nil, err + } + configNames, err := dir.Readdirnames(0) + if err != nil { + return nil, err + } + for _, configName := range configNames { + if !strings.HasSuffix(configName, ".yaml") { + continue + } + configPath := filepath.Join(dirPath, configName) + configBytes, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, err + } + + var config registryConfiguration + err = yaml.Unmarshal(configBytes, &config) + if err != nil { + return nil, fmt.Errorf("Error parsing %s: %v", configPath, err) + } + + if config.DefaultDocker != nil { + if mergedConfig.DefaultDocker != nil { + return nil, fmt.Errorf(`Error parsing signature storage configuration: "default-docker" defined both in "%s" and "%s"`, + dockerDefaultMergedFrom, configPath) + } + mergedConfig.DefaultDocker = config.DefaultDocker + dockerDefaultMergedFrom = configPath + } + + for nsName, nsConfig := range config.Docker { // includes config.Docker == nil + if _, ok := mergedConfig.Docker[nsName]; ok { + return nil, fmt.Errorf(`Error parsing signature storage configuration: "docker" namespace "%s" defined both in "%s" and "%s"`, + nsName, nsMergedFrom[nsName], configPath) + } + mergedConfig.Docker[nsName] = nsConfig + nsMergedFrom[nsName] = configPath + } + } + + return &mergedConfig, nil +} + +// config.signatureTopLevel returns an URL string configured in config for ref, for write access if “write”. +// (the top level of the storage, namespaced by repo.FullName etc.), or "" if no signature storage should be used. +func (config *registryConfiguration) signatureTopLevel(ref dockerReference, write bool) string { + if config.Docker != nil { + // Look for a full match. + identity := ref.PolicyConfigurationIdentity() + if ns, ok := config.Docker[identity]; ok { + logrus.Debugf(` Using "docker" namespace %s`, identity) + if url := ns.signatureTopLevel(write); url != "" { + return url + } + } + + // Look for a match of the possible parent namespaces. + for _, name := range ref.PolicyConfigurationNamespaces() { + if ns, ok := config.Docker[name]; ok { + logrus.Debugf(` Using "docker" namespace %s`, name) + if url := ns.signatureTopLevel(write); url != "" { + return url + } + } + } + } + // Look for a default location + if config.DefaultDocker != nil { + logrus.Debugf(` Using "default-docker" configuration`) + if url := config.DefaultDocker.signatureTopLevel(write); url != "" { + return url + } + } + logrus.Debugf(" No signature storage configuration found for %s", ref.PolicyConfigurationIdentity()) + return "" +} + +// ns.signatureTopLevel returns an URL string configured in ns for ref, for write access if “write”. +// or "" if nothing has been configured. +func (ns registryNamespace) signatureTopLevel(write bool) string { + if write && ns.SigStoreWrite != "" { + logrus.Debugf(` Using %s`, ns.SigStoreWrite) + return ns.SigStoreWrite + } + if ns.SigStore != "" { + logrus.Debugf(` Using %s`, ns.SigStore) + return ns.SigStore + } + return "" +} + +// signatureStorageURL returns an URL usable for acessing signature index in base with known manifestDigest, or nil if not applicable. +// Returns nil iff base == nil. +func signatureStorageURL(base signatureStorageBase, manifestDigest string, index int) *url.URL { + if base == nil { + return nil + } + url := *base + url.Path = fmt.Sprintf("%s@%s/signature-%d", url.Path, manifestDigest, index+1) + return &url +} diff --git a/vendor/github.com/containers/image/types/types.go b/vendor/github.com/containers/image/types/types.go index a2cdb6fa..1ef4e67d 100644 --- a/vendor/github.com/containers/image/types/types.go +++ b/vendor/github.com/containers/image/types/types.go @@ -188,12 +188,16 @@ type SystemContext struct { // If not "", prefixed to any absolute paths used by default by the library (e.g. in /etc/). // Not used for any of the more specific path overrides available in this struct. // Not used for any paths specified by users in config files (even if the location of the config file _was_ affected by it). + // NOTE: If this is set, environment-variable overrides of paths are ignored (to keep the semantics simple: to create an /etc replacement, just set RootForImplicitAbsolutePaths . + // and there is no need to worry about the environment.) // NOTE: This does NOT affect paths starting by $HOME. RootForImplicitAbsolutePaths string // === Global configuration overrides === // If not "", overrides the system's default path for signature.Policy configuration. SignaturePolicyPath string + // If not "", overrides the system's default path for registries.d (Docker signature storage configuration) + RegistriesDirPath string // === docker.Transport overrides === DockerCertPath string // If not "", a directory containing "cert.pem" and "key.pem" used when talking to a Docker Registry diff --git a/vendor/github.com/docker/distribution/reference/reference.go b/vendor/github.com/docker/distribution/reference/reference.go index bb09fa25..5b3e08ee 100644 --- a/vendor/github.com/docker/distribution/reference/reference.go +++ b/vendor/github.com/docker/distribution/reference/reference.go @@ -24,6 +24,7 @@ package reference import ( "errors" "fmt" + "strings" "github.com/docker/distribution/digest" ) @@ -43,6 +44,9 @@ var ( // 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") @@ -149,7 +153,9 @@ func Parse(s string) (Reference, error) { if s == "" { return nil, ErrNameEmpty } - // TODO(dmcgowan): Provide more specific and helpful error + if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil { + return nil, ErrNameContainsUppercase + } return nil, ErrReferenceInvalidFormat }