mirror of https://github.com/containers/image.git
251 lines
8.0 KiB
Go
251 lines
8.0 KiB
Go
package reference
|
|
|
|
import (
|
|
"regexp"
|
|
"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/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
// XDefaultTag defines the default tag used when performing images related actions and no tag or digest is specified
|
|
XDefaultTag = "latest"
|
|
// XDefaultHostname is the default built-in hostname
|
|
XDefaultHostname = "docker.io"
|
|
// XLegacyDefaultHostname is automatically converted to DefaultHostname
|
|
XLegacyDefaultHostname = "index.docker.io"
|
|
// XDefaultRepoPrefix is the prefix used for default repositories in default host
|
|
XDefaultRepoPrefix = "library/"
|
|
)
|
|
|
|
// XNamed is an object with a full name
|
|
type XNamed interface {
|
|
// XName returns normalized repository name, like "ubuntu".
|
|
XName() string
|
|
// XString returns full reference, like "ubuntu@sha256:abcdef..."
|
|
XString() string
|
|
// XFullName returns full repository name with hostname, like "docker.io/library/ubuntu"
|
|
XFullName() string
|
|
// XHostname returns hostname for the reference, like "docker.io"
|
|
XHostname() string
|
|
// XRemoteName returns the repository component of the full name, like "library/ubuntu"
|
|
XRemoteName() string
|
|
}
|
|
|
|
// XNamedTagged is an object including a name and tag.
|
|
type XNamedTagged interface {
|
|
XNamed
|
|
XTag() string
|
|
}
|
|
|
|
// XCanonical reference is an object with a fully unique
|
|
// name including a name with hostname and digest
|
|
type XCanonical interface {
|
|
XNamed
|
|
XDigest() digest.Digest
|
|
}
|
|
|
|
// XParseNamed parses s and returns a syntactically valid reference implementing
|
|
// the Named interface. The reference must have a name, otherwise an error is
|
|
// returned.
|
|
// If an error was encountered it is returned, along with a nil Reference.
|
|
func XParseNamed(s string) (XNamed, error) {
|
|
named, err := distreference.ParseNormalizedNamed(s)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", s)
|
|
}
|
|
r, err := XWithName(named.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if canonical, isCanonical := named.(distreference.Canonical); isCanonical {
|
|
upstreamR, err := distreference.WithDigest(r.upstream, canonical.Digest())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ourR, err := distreference.WithDigest(r.our, canonical.Digest())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &canonicalRef{namedRef{upstream: upstreamR, our: ourR}}, nil
|
|
}
|
|
if tagged, isTagged := named.(distreference.NamedTagged); isTagged {
|
|
return XWithTag(r, tagged.Tag())
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// XWithName returns a named object representing the given string. If the input
|
|
// is invalid ErrReferenceInvalidFormat will be returned.
|
|
// FIXME: returns *namedRef to expose the upstream/our fields. Should revert to XNamed/Named.
|
|
func XWithName(name string) (*namedRef, error) {
|
|
upstreamR, err := distreference.ParseNormalizedNamed(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name, err = normalize(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateName(name); err != nil {
|
|
return nil, err
|
|
}
|
|
ourR, err := distreference.WithName(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &namedRef{upstream: upstreamR, our: ourR}, nil
|
|
}
|
|
|
|
// XWithTag combines the name from "name" and the tag from "tag" to form a
|
|
// reference incorporating both the name and the tag.
|
|
// FIXME: expects *namedRef to expose the upstream/our fields. Should revert to XNamed/Named.
|
|
func XWithTag(name *namedRef, tag string) (XNamedTagged, error) {
|
|
upstreamR, err := distreference.WithTag(name.upstream, tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ourR, err := distreference.WithTag(name.our, tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &taggedRef{namedRef{upstream: upstreamR, our: ourR}}, nil
|
|
}
|
|
|
|
type namedRef struct {
|
|
// TRANSITIONAL state: We want to transition from our semantics (Name(), String() return a minified form)
|
|
// to the upstream ones (Name(), String() return the fully expanded form). In the mean time we still
|
|
// want to call upstream distreference.* methods on the namedRef implementation.
|
|
//
|
|
// As it happens, distreference.WithTag and distreference.WithDigest can both accept
|
|
// minimized input and return minimized output, so we can keep using them even with the minimized
|
|
// values.
|
|
//
|
|
// For the transition, we keep an "upstream", fully expanded, value, and "our", which we have minimized.
|
|
// We start with "upstream" being essentially write-only, with no users in containers/image.
|
|
// Then we will, bit by bit, eliminate uses of "our".
|
|
//
|
|
// upstream uses the normalization from distreference.ParseNormalizedNamed
|
|
upstream distreference.Named
|
|
// our is what the existing code used to do, via normalize()
|
|
our distreference.Named
|
|
}
|
|
type taggedRef struct {
|
|
namedRef
|
|
}
|
|
type canonicalRef struct {
|
|
namedRef
|
|
}
|
|
|
|
func (r *namedRef) XName() string {
|
|
return distreference.FamiliarName(r.upstream)
|
|
}
|
|
func (r *namedRef) XString() string {
|
|
return distreference.FamiliarString(r.upstream)
|
|
}
|
|
func (r *namedRef) XFullName() string {
|
|
return r.upstream.Name()
|
|
}
|
|
func (r *namedRef) XHostname() string {
|
|
return distreference.Domain(r.upstream)
|
|
}
|
|
func (r *namedRef) XRemoteName() string {
|
|
return distreference.Path(r.upstream)
|
|
}
|
|
func (r *taggedRef) XTag() string {
|
|
return r.namedRef.upstream.(distreference.NamedTagged).Tag()
|
|
}
|
|
func (r *canonicalRef) XDigest() digest.Digest {
|
|
return digest.Digest(r.namedRef.upstream.(distreference.Canonical).Digest())
|
|
}
|
|
|
|
// XWithDefaultTag adds a default tag to a reference if it only has a repo name.
|
|
func XWithDefaultTag(ref XNamed) XNamed {
|
|
if XIsNameOnly(ref) {
|
|
// FIXME: uses *namedRef to expose the upstream/our fields. Should use ref without a cast.
|
|
ref, _ = XWithTag(ref.(*namedRef), XDefaultTag)
|
|
}
|
|
return ref
|
|
}
|
|
|
|
// XIsNameOnly returns true if reference only contains a repo name.
|
|
func XIsNameOnly(ref XNamed) bool {
|
|
if _, ok := ref.(XNamedTagged); ok {
|
|
return false
|
|
}
|
|
if _, ok := ref.(XCanonical); ok {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// XParseIDOrReference parses string for an image ID or a reference. ID can be
|
|
// without a default prefix.
|
|
func XParseIDOrReference(idOrRef string) (digest.Digest, XNamed, error) {
|
|
if err := validateID(idOrRef); err == nil {
|
|
idOrRef = "sha256:" + idOrRef
|
|
}
|
|
if dgst, err := digest.Parse(idOrRef); err == nil {
|
|
return dgst, nil, nil
|
|
}
|
|
ref, err := XParseNamed(idOrRef)
|
|
return "", ref, err
|
|
}
|
|
|
|
// splitHostname splits a repository name to hostname and remotename string.
|
|
// If no valid hostname is found, the default hostname is used. Repository name
|
|
// 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 = XDefaultHostname, name
|
|
} else {
|
|
hostname, remoteName = name[:i], name[i+1:]
|
|
}
|
|
if hostname == XLegacyDefaultHostname {
|
|
hostname = XDefaultHostname
|
|
}
|
|
if hostname == XDefaultHostname && !strings.ContainsRune(remoteName, '/') {
|
|
remoteName = XDefaultRepoPrefix + remoteName
|
|
}
|
|
return
|
|
}
|
|
|
|
// normalize returns a repository name in its normalized form, meaning it
|
|
// will not contain default hostname nor library/ prefix for official images.
|
|
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 == XDefaultHostname {
|
|
if strings.HasPrefix(remoteName, XDefaultRepoPrefix) {
|
|
return strings.TrimPrefix(remoteName, XDefaultRepoPrefix), nil
|
|
}
|
|
return remoteName, nil
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
|
|
|
func validateID(id string) error {
|
|
if ok := validHex.MatchString(id); !ok {
|
|
return errors.Errorf("image ID %q is invalid", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateName(name string) error {
|
|
if err := validateID(name); err == nil {
|
|
return errors.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
|
|
}
|
|
return nil
|
|
}
|