image parsing
parent
3c8f114087
commit
cd32373301
|
@ -0,0 +1,110 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Reference is an opaque object that include identifier such as a name, tag, repository, registry, etc...
|
||||
type Reference struct {
|
||||
named Named
|
||||
tag string
|
||||
}
|
||||
|
||||
// Name returns the image's name. (ie: debian[:8.2])
|
||||
func (r Reference) Name() string {
|
||||
return r.named.RemoteName() + r.tag
|
||||
}
|
||||
|
||||
// ShortName returns the image's name (ie: debian)
|
||||
func (r Reference) ShortName() string {
|
||||
return r.named.RemoteName()
|
||||
}
|
||||
|
||||
// Tag returns the image's tag (or digest).
|
||||
func (r Reference) Tag() string {
|
||||
if len(r.tag) > 1 {
|
||||
return r.tag[1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Registry returns the image's registry. (ie: host[:port])
|
||||
func (r Reference) Registry() string {
|
||||
return r.named.Hostname()
|
||||
}
|
||||
|
||||
// Repository returns the image's repository. (ie: registry/name)
|
||||
func (r Reference) Repository() string {
|
||||
return r.named.FullName()
|
||||
}
|
||||
|
||||
// Remote returns the image's remote identifier. (ie: registry/name[:tag])
|
||||
func (r Reference) Remote() string {
|
||||
return r.named.FullName() + r.tag
|
||||
}
|
||||
|
||||
func clean(url string) string {
|
||||
|
||||
s := url
|
||||
|
||||
if strings.HasPrefix(url, "http://") {
|
||||
s = strings.Replace(url, "http://", "", 1)
|
||||
} else if strings.HasPrefix(url, "https://") {
|
||||
s = strings.Replace(url, "https://", "", 1)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Parse returns a Reference from analyzing the given remote identifier.
|
||||
func Parse(remote string) (*Reference, error) {
|
||||
|
||||
n, err := ParseNamed(clean(remote))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n = WithDefaultTag(n)
|
||||
|
||||
var t string
|
||||
switch x := n.(type) {
|
||||
case Canonical:
|
||||
t = "@" + x.Digest().String()
|
||||
case NamedTagged:
|
||||
t = ":" + x.Tag()
|
||||
}
|
||||
|
||||
return &Reference{named: n, tag: t}, nil
|
||||
}
|
||||
|
||||
// ParseRepo - parses remote
|
||||
// pretty much the same as Parse but better for testing
|
||||
func ParseRepo(remote string) (*Repository, error) {
|
||||
|
||||
n, err := ParseNamed(clean(remote))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n = WithDefaultTag(n)
|
||||
|
||||
var t string
|
||||
switch x := n.(type) {
|
||||
case Canonical:
|
||||
t = "@" + x.Digest().String()
|
||||
case NamedTagged:
|
||||
t = ":" + x.Tag()
|
||||
}
|
||||
|
||||
ref := &Reference{named: n, tag: t}
|
||||
|
||||
return &Repository{
|
||||
Name: ref.Name(),
|
||||
Repository: ref.Repository(),
|
||||
Registry: ref.Registry(),
|
||||
ShortName: ref.ShortName(),
|
||||
Tag: ref.Tag(),
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShortParseWithTag(t *testing.T) {
|
||||
|
||||
reference, err := Parse("foo/bar:1.1")
|
||||
if err != nil {
|
||||
t.Errorf("error while parsing tag: %s", err)
|
||||
}
|
||||
|
||||
if reference.Tag() != "1.1" {
|
||||
t.Errorf("unexpected tag: %s", reference.Tag())
|
||||
}
|
||||
|
||||
if reference.Registry() != DefaultHostname {
|
||||
t.Errorf("unexpected registry: %s", reference.Registry())
|
||||
}
|
||||
|
||||
if reference.ShortName() != "foo/bar" {
|
||||
t.Errorf("unexpected name: %s", reference.ShortName())
|
||||
}
|
||||
|
||||
if reference.Name() != "foo/bar:1.1" {
|
||||
t.Errorf("unexpected name: %s", reference.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRepo(t *testing.T) {
|
||||
type args struct {
|
||||
remote string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Repository
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "foo/bar:1.1",
|
||||
args: args{remote: "foo/bar:1.1"},
|
||||
want: &Repository{
|
||||
Name: "foo/bar:1.1",
|
||||
Repository: "docker.io/foo/bar",
|
||||
Registry: DefaultHostname,
|
||||
ShortName: "foo/bar",
|
||||
Tag: "1.1",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "localhost.localdomain/foo/bar:1.1",
|
||||
args: args{remote: "localhost.localdomain/foo/bar:1.1"},
|
||||
want: &Repository{
|
||||
Name: "foo/bar:1.1",
|
||||
Repository: "localhost.localdomain/foo/bar",
|
||||
Registry: "localhost.localdomain",
|
||||
ShortName: "foo/bar",
|
||||
Tag: "1.1",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseRepo(tt.args.remote)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseRepo() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseRepo() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultTag defines the default tag used when performing images related actions and no tag or digest is specified
|
||||
DefaultTag = "latest"
|
||||
// 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/"
|
||||
)
|
||||
|
||||
// Repository is an object created from Named interface
|
||||
type Repository struct {
|
||||
Name string // Name returns the image's name. (ie: debian[:8.2])
|
||||
Repository string // Repository returns the image's repository. (ie: registry/name)
|
||||
Registry string // Registry returns the image's registry. (ie: host[:port])
|
||||
ShortName string // ShortName returns the image's name (ie: debian)
|
||||
Tag string // Tag returns the image's tag (or digest).
|
||||
}
|
||||
|
||||
// Named is an object with a full name
|
||||
type Named interface {
|
||||
// Name returns normalized repository name, like "ubuntu".
|
||||
Name() string
|
||||
// String returns full reference, like "ubuntu@sha256:abcdef..."
|
||||
String() string
|
||||
// FullName returns full repository name with hostname, like "docker.io/library/ubuntu"
|
||||
FullName() string
|
||||
// Hostname returns hostname for the reference, like "docker.io"
|
||||
Hostname() string
|
||||
// RemoteName returns the repository component of the full name, like "library/ubuntu"
|
||||
RemoteName() string
|
||||
}
|
||||
|
||||
// NamedTagged is an object including a name and tag.
|
||||
type NamedTagged interface {
|
||||
Named
|
||||
Tag() string
|
||||
}
|
||||
|
||||
// Canonical reference is an object with a fully unique
|
||||
// name including a name with hostname and digest
|
||||
type Canonical interface {
|
||||
Named
|
||||
Digest() digest.Digest
|
||||
}
|
||||
|
||||
// ParseNamed 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 ParseNamed(s string) (Named, error) {
|
||||
named, err := reference.ParseNamed(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", s)
|
||||
}
|
||||
r, err := WithName(named.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if canonical, isCanonical := named.(reference.Canonical); isCanonical {
|
||||
return WithDigest(r, canonical.Digest())
|
||||
}
|
||||
|
||||
if tagged, isTagged := named.(reference.NamedTagged); isTagged {
|
||||
return WithTag(r, tagged.Tag())
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// WithName returns a named object representing the given string. If the input
|
||||
// is invalid ErrReferenceInvalidFormat will be returned.
|
||||
func WithName(name string) (Named, error) {
|
||||
name, err := normalize(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := reference.WithName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &namedRef{r}, nil
|
||||
}
|
||||
|
||||
// WithTag combines the name from "name" and the tag from "tag" to form a
|
||||
// reference incorporating both the name and the tag.
|
||||
func WithTag(name Named, tag string) (NamedTagged, error) {
|
||||
r, err := reference.WithTag(name, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &taggedRef{namedRef{r}}, 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) {
|
||||
r, err := reference.WithDigest(name, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &canonicalRef{namedRef{r}}, nil
|
||||
}
|
||||
|
||||
type namedRef struct {
|
||||
reference.Named
|
||||
}
|
||||
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.(reference.NamedTagged).Tag()
|
||||
}
|
||||
func (r *canonicalRef) Digest() digest.Digest {
|
||||
return r.namedRef.Named.(reference.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)
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 = 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
|
||||
// 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 == DefaultHostname {
|
||||
if strings.HasPrefix(remoteName, DefaultRepoPrefix) {
|
||||
return strings.TrimPrefix(remoteName, DefaultRepoPrefix), nil
|
||||
}
|
||||
return remoteName, nil
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func validateName(name string) error {
|
||||
if err := ValidateID(name); err == nil {
|
||||
return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
||||
|
||||
// ValidateID checks whether an ID string is a valid image ID.
|
||||
func ValidateID(id string) error {
|
||||
if ok := validHex.MatchString(id); !ok {
|
||||
return fmt.Errorf("image ID '%s' is invalid ", id)
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue