image parsing

pull/26/head
Karolis Rusenas 2017-07-01 23:02:20 +01:00
parent 3c8f114087
commit cd32373301
4 changed files with 412 additions and 0 deletions

110
image/parse.go Normal file
View File

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

79
image/parse_test.go Normal file
View File

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

207
image/reference.go Normal file
View File

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

16
image/validation.go Normal file
View File

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