Merge pull request #170 from mtrmac/docker-lookaside

Implement a lookaside storage for signatures of images in Docker registries
This commit is contained in:
Miloslav Trmač 2016-09-12 21:39:39 +02:00 committed by GitHub
commit d6be447ce9
9 changed files with 452 additions and 29 deletions

View File

@ -56,6 +56,11 @@ func createApp() *cli.App {
Value: "", Value: "",
Usage: "Path to a trust policy file", 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 { app.Before = func(c *cli.Context) error {
if c.GlobalBool("debug") { if c.GlobalBool("debug") {

View File

@ -8,10 +8,10 @@ import (
// contextFromGlobalOptions returns a types.SystemContext depending on c. // contextFromGlobalOptions returns a types.SystemContext depending on c.
func contextFromGlobalOptions(c *cli.Context) *types.SystemContext { func contextFromGlobalOptions(c *cli.Context) *types.SystemContext {
certPath := c.GlobalString("cert-path")
tlsVerify := c.GlobalBool("tls-verify") // FIXME!! defaults to false tlsVerify := c.GlobalBool("tls-verify") // FIXME!! defaults to false
return &types.SystemContext{ return &types.SystemContext{
DockerCertPath: certPath, RegistriesDirPath: c.GlobalString("registries.d"),
DockerCertPath: c.GlobalString("cert-path"),
DockerInsecureSkipTLSVerify: !tlsVerify, DockerInsecureSkipTLSVerify: !tlsVerify,
} }
} }

View File

@ -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. **--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 **--tls-verify** _bool-value_ Verify certificates
**--help**|**-h** Show help **--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 _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_ **--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
**skopeo manifest-digest** _manifest-file_ **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
**skopeo standalone-sign** _manifest docker-reference key-fingerprint_ **--output**|**-o** _signature_ **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. 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 . 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 # EXAMPLES
## skopeo copy ## skopeo copy

View File

@ -42,17 +42,17 @@ type dockerClient struct {
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty 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 scheme string // Cache of a value returned by a successful ping() if not empty
client *http.Client 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) // 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) { // “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
var registry string func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool) (*dockerClient, error) {
if refHostname == dockerHostname { registry := ref.ref.Hostname()
if registry == dockerHostname {
registry = dockerRegistry registry = dockerRegistry
} else {
registry = refHostname
} }
username, password, err := getAuth(refHostname) username, password, err := getAuth(ref.ref.Hostname())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,11 +78,18 @@ func newDockerClient(ctx *types.SystemContext, refHostname string) (*dockerClien
if tr != nil { if tr != nil {
client.Transport = tr client.Transport = tr
} }
sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
if err != nil {
return nil, err
}
return &dockerClient{ return &dockerClient{
registry: registry, registry: registry,
username: username, username: username,
password: password, password: password,
client: client, client: client,
signatureBase: sigBase,
}, nil }, nil
} }

View File

@ -8,6 +8,9 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os"
"path/filepath"
"strconv" "strconv"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
@ -18,11 +21,13 @@ import (
type dockerImageDestination struct { type dockerImageDestination struct {
ref dockerReference ref dockerReference
c *dockerClient c *dockerClient
// State
manifestDigest string // or "" if not yet known.
} }
// newImageDestination creates a new ImageDestination for the specified image reference. // newImageDestination creates a new ImageDestination for the specified image reference.
func newImageDestination(ctx *types.SystemContext, ref dockerReference) (types.ImageDestination, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -144,6 +149,7 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
if err != nil { if err != nil {
return err return err
} }
d.manifestDigest = digest
url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), digest) url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), digest)
headers := map[string][]string{} headers := map[string][]string{}
@ -168,12 +174,91 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
} }
func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error { func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
if len(signatures) != 0 { // FIXME? This overwrites files one at a time, definitely not atomic.
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported") // 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 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. // 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: // WARNING: This does not have any transactional semantics:
// - Uploaded data MAY be visible to others before Commit() is called // - Uploaded data MAY be visible to others before Commit() is called

View File

@ -6,6 +6,8 @@ import (
"io/ioutil" "io/ioutil"
"mime" "mime"
"net/http" "net/http"
"net/url"
"os"
"strconv" "strconv"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
@ -26,6 +28,9 @@ type dockerImageSource struct {
ref dockerReference ref dockerReference
requestedManifestMIMETypes []string requestedManifestMIMETypes []string
c *dockerClient 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, // newImageSource creates a new ImageSource for the specified image reference,
@ -33,7 +38,7 @@ type dockerImageSource struct {
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes. // nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource. // The caller must call .Close() on the returned ImageSource.
func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -71,10 +76,29 @@ func simplifyContentType(contentType string) string {
} }
func (s *dockerImageSource) GetManifest() ([]byte, string, error) { func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
reference, err := s.ref.tagOrDigest() err := s.ensureManifestIsLoaded()
if err != nil { if err != nil {
return nil, "", err 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) 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) 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 // 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 headers["Accept"] = s.requestedManifestMIMETypes
res, err := s.c.makeRequest("GET", url, headers, nil) res, err := s.c.makeRequest("GET", url, headers, nil)
if err != nil { if err != nil {
return nil, "", err return err
} }
defer res.Body.Close() defer res.Body.Close()
manblob, err := ioutil.ReadAll(res.Body) manblob, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
return nil, "", err return err
} }
if res.StatusCode != http.StatusOK { 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. // 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 blobs size (or -1 if unknown). // GetBlob returns a stream for the specified blob, and the blobs 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) { 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. // deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error { func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
c, err := newDockerClient(ctx, ref.ref.Hostname()) c, err := newDockerClient(ctx, ref, true)
if err != nil { if err != nil {
return err return err
} }
@ -141,7 +232,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
return err return err
} }
defer get.Body.Close() defer get.Body.Close()
body, err := ioutil.ReadAll(get.Body) manifestBody, err := ioutil.ReadAll(get.Body)
if err != nil { if err != nil {
return err return err
} }
@ -150,7 +241,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
case http.StatusNotFound: 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) 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: 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") digest := get.Header.Get("Docker-Content-Digest")
@ -164,7 +255,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
} }
defer delete.Body.Close() defer delete.Body.Close()
body, err = ioutil.ReadAll(delete.Body) body, err := ioutil.ReadAll(delete.Body)
if err != nil { if err != nil {
return err 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) 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 return nil
} }

198
vendor/github.com/containers/image/docker/lookaside.go generated vendored Normal file
View File

@ -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
}

View File

@ -188,12 +188,16 @@ type SystemContext struct {
// If not "", prefixed to any absolute paths used by default by the library (e.g. in /etc/). // 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 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). // 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. // NOTE: This does NOT affect paths starting by $HOME.
RootForImplicitAbsolutePaths string RootForImplicitAbsolutePaths string
// === Global configuration overrides === // === Global configuration overrides ===
// If not "", overrides the system's default path for signature.Policy configuration. // If not "", overrides the system's default path for signature.Policy configuration.
SignaturePolicyPath string SignaturePolicyPath string
// If not "", overrides the system's default path for registries.d (Docker signature storage configuration)
RegistriesDirPath string
// === docker.Transport overrides === // === docker.Transport overrides ===
DockerCertPath string // If not "", a directory containing "cert.pem" and "key.pem" used when talking to a Docker Registry DockerCertPath string // If not "", a directory containing "cert.pem" and "key.pem" used when talking to a Docker Registry

View File

@ -24,6 +24,7 @@ package reference
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
) )
@ -43,6 +44,9 @@ var (
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag. // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
ErrDigestInvalidFormat = errors.New("invalid digest format") 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 is returned for empty, invalid repository names.
ErrNameEmpty = errors.New("repository name must have at least one component") ErrNameEmpty = errors.New("repository name must have at least one component")
@ -149,7 +153,9 @@ func Parse(s string) (Reference, error) {
if s == "" { if s == "" {
return nil, ErrNameEmpty return nil, ErrNameEmpty
} }
// TODO(dmcgowan): Provide more specific and helpful error if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
return nil, ErrNameContainsUppercase
}
return nil, ErrReferenceInvalidFormat return nil, ErrReferenceInvalidFormat
} }