distribution/reference/repository.go
Tibor Vass f8c09b6a7d Add a new reference package abstracting repositories, tags and digests
There seems to be a need for a type that represents a way of pointing
to an image, irrespective of the implementation.

This patch defines a Reference interface and provides 3 implementations:
- TagReference: when only a tag is provided
- DigestReference: when a digest (according to the digest package) is
  provided, can include optional tag as well

Validation of references are purely syntactic.

There is also a strong type for tags, analogous to digests, as well
as a strong type for Repository from which clients can access the
hostname alone, or the repository name without the hostname, or both
together via the String() method.

For Repository, the files names.go and names_test.go were moved from
the v2 package.

Signed-off-by: Tibor Vass <tibor@docker.com>
2015-10-09 16:05:34 -07:00

136 lines
5.1 KiB
Go

package reference
import (
"errors"
"fmt"
"regexp"
"strings"
)
const (
// RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name.
RepositoryNameTotalLengthMax = 255
)
// RepositoryNameComponentRegexp restricts registry path component names to
// start with at least one letter or number, with following parts able to
// be separated by one period, dash or underscore.
var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-zA-Z0-9]+(?:[._-][a-z0-9]+)*`)
// RepositoryNameComponentAnchoredRegexp is the version of
// RepositoryNameComponentRegexp which must completely match the content
var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`)
// RepositoryNameHostnameRegexp restricts the registry hostname component of a repository name to
// start with a component as defined by RepositoryNameComponentRegexp and followed by an optional port.
var RepositoryNameHostnameRegexp = regexp.MustCompile(RepositoryNameComponentRegexp.String() + `(?::[0-9]+)?`)
// RepositoryNameHostnameAnchoredRegexp is the version of
// RepositoryNameHostnameRegexp which must completely match the content.
var RepositoryNameHostnameAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String() + `$`)
// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow
// multiple path components, separated by a forward slash.
var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameHostnameRegexp.String() + `/)?(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String())
var (
// ErrRepositoryNameEmpty is returned for empty, invalid repository names.
ErrRepositoryNameEmpty = errors.New("repository name must have at least one component")
// ErrRepositoryNameMissingHostname is returned when a repository name
// does not start with a hostname
ErrRepositoryNameMissingHostname = errors.New("repository name must start with a hostname")
// ErrRepositoryNameHostnameInvalid is returned when a repository name
// does not match RepositoryNameHostnameRegexp
ErrRepositoryNameHostnameInvalid = fmt.Errorf("repository name must match %q", RepositoryNameHostnameRegexp.String())
// ErrRepositoryNameLong is returned when a repository name is longer than
// RepositoryNameTotalLengthMax
ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
// ErrRepositoryNameComponentInvalid is returned when a repository name does
// not match RepositoryNameComponentRegexp
ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
)
// Repository represents a reference to a Repository.
type Repository struct {
// Hostname refers to the registry hostname where the repository resides.
Hostname string
// Name is a slash (`/`) separated list of string components.
Name string
}
// String returns the string representation of a repository.
func (r Repository) String() string {
// Hostname is not supposed to be empty, but let's be nice.
if len(r.Hostname) == 0 {
return r.Name
}
return r.Hostname + "/" + r.Name
}
// Validate ensures the repository name is valid for use in the
// registry. This function accepts a superset of what might be accepted by
// docker core or docker hub. If the name does not pass validation, an error,
// describing the conditions, is returned.
//
// Effectively, the name should comply with the following grammar:
//
// repository := hostname ['/' component]+
// hostname := component [':' port-number]
// component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-zA-Z0-9]+/
// separator := /[._-]/
// port-number := /[0-9]+/
//
// The result of the production should be limited to 255 characters.
func (r Repository) Validate() error {
n := len(r.String())
switch {
case n == 0:
return ErrRepositoryNameEmpty
case n > RepositoryNameTotalLengthMax:
return ErrRepositoryNameLong
case len(r.Hostname) <= 0:
return ErrRepositoryNameMissingHostname
case !RepositoryNameHostnameAnchoredRegexp.MatchString(r.Hostname):
return ErrRepositoryNameHostnameInvalid
}
components := r.Name
for {
var component string
sep := strings.Index(components, "/")
if sep >= 0 {
component = components[:sep]
components = components[sep+1:]
} else { // if no more slashes
component = components
components = ""
}
if !RepositoryNameComponentAnchoredRegexp.MatchString(component) {
return ErrRepositoryNameComponentInvalid
}
if sep < 0 {
return nil
}
}
}
// NewRepository returns a valid Repository from an input string representing
// the canonical form of a repository name.
// If the validation fails, an error is returned.
func NewRepository(canonicalName string) (repo Repository, err error) {
if len(canonicalName) == 0 {
return repo, ErrRepositoryNameEmpty
}
i := strings.Index(canonicalName, "/")
if i <= 0 {
return repo, ErrRepositoryNameMissingHostname
}
repo.Hostname = canonicalName[:i]
repo.Name = canonicalName[i+1:]
return repo, repo.Validate()
}