distribution/reference/reference_test.go

752 lines
18 KiB
Go
Raw Permalink Normal View History

package reference
import (
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/json"
"strings"
"testing"
"github.com/opencontainers/go-digest"
)
func TestReferenceParse(t *testing.T) {
t.Parallel()
// tests is a unified set of testcases for
// testing the parsing of references
tests := []struct {
// input is the repository name or name component testcase
input string
// err is the error expected from Parse, or nil
err error
// repository is the string representation for the reference
repository string
// domain is the domain expected in the reference
domain string
// tag is the tag for the reference
tag string
// digest is the digest for the reference (enforces digest reference)
digest string
}{
{
input: "test_com",
repository: "test_com",
},
{
input: "test.com:tag",
repository: "test.com",
tag: "tag",
},
{
input: "test.com:5000",
repository: "test.com",
tag: "5000",
},
{
input: "test.com/repo:tag",
domain: "test.com",
repository: "test.com/repo",
tag: "tag",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "test:5000/repo:tag",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
},
{
input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "repo@sha256:ffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestInvalidLength,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestUnsupported,
},
{
input: "Uppercase:tag",
err: ErrNameContainsUppercase,
},
// FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes.
// See https://github.com/distribution/distribution/pull/1778, and https://github.com/docker/docker/pull/20175
// {
// input: "Uppercase/lowercase:tag",
// err: ErrNameContainsUppercase,
// },
{
input: "test:5000/Uppercase/lowercase:tag",
err: ErrNameContainsUppercase,
},
{
input: "lowercase:Uppercase",
repository: "lowercase",
tag: "Uppercase",
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
domain: "a",
repository: strings.Repeat("a/", 127) + "a",
tag: "tag-puts-this-over-max",
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
},
{
input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
tag: "some-long-tag",
},
{
input: "b.gcr.io/test.example.com/my-app:test.example.com",
domain: "b.gcr.io",
repository: "b.gcr.io/test.example.com/my-app",
tag: "test.example.com",
},
{
input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
domain: "xn--n3h.com",
repository: "xn--n3h.com/myimage",
tag: "xn--n3h.com",
},
{
input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
domain: "xn--7o8h.com",
repository: "xn--7o8h.com/myimage",
tag: "xn--7o8h.com",
digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "foo_bar.com:8080",
repository: "foo_bar.com",
tag: "8080",
},
{
input: "foo/foo_bar.com:8080",
domain: "foo",
repository: "foo/foo_bar.com",
tag: "8080",
},
{
input: "192.168.1.1",
repository: "192.168.1.1",
},
{
input: "192.168.1.1:tag",
repository: "192.168.1.1",
tag: "tag",
},
{
input: "192.168.1.1:5000",
repository: "192.168.1.1",
tag: "5000",
},
{
input: "192.168.1.1/repo",
domain: "192.168.1.1",
repository: "192.168.1.1/repo",
},
{
input: "192.168.1.1:5000/repo",
domain: "192.168.1.1:5000",
repository: "192.168.1.1:5000/repo",
},
{
input: "192.168.1.1:5000/repo:5050",
domain: "192.168.1.1:5000",
repository: "192.168.1.1:5000/repo",
tag: "5050",
},
{
input: "[2001:db8::1]",
err: ErrReferenceInvalidFormat,
},
{
input: "[2001:db8::1]:5000",
err: ErrReferenceInvalidFormat,
},
{
input: "[2001:db8::1]:tag",
err: ErrReferenceInvalidFormat,
},
{
input: "[2001:db8::1]/repo",
domain: "[2001:db8::1]",
repository: "[2001:db8::1]/repo",
},
{
input: "[2001:db8:1:2:3:4:5:6]/repo:tag",
domain: "[2001:db8:1:2:3:4:5:6]",
repository: "[2001:db8:1:2:3:4:5:6]/repo",
tag: "tag",
},
{
input: "[2001:db8::1]:5000/repo",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
},
{
input: "[2001:db8::1]:5000/repo:tag",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
tag: "tag",
},
{
input: "[2001:db8::1]:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "[2001:db8::1]:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "[2001:db8::]:5000/repo",
domain: "[2001:db8::]:5000",
repository: "[2001:db8::]:5000/repo",
},
{
input: "[::1]:5000/repo",
domain: "[::1]:5000",
repository: "[::1]:5000/repo",
},
{
input: "[fe80::1%eth0]:5000/repo",
err: ErrReferenceInvalidFormat,
},
{
input: "[fe80::1%@invalidzone]:5000/repo",
err: ErrReferenceInvalidFormat,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.input, func(t *testing.T) {
t.Parallel()
repo, err := Parse(tc.input)
if tc.err != nil {
if err == nil {
t.Errorf("missing expected error: %v", tc.err)
} else if tc.err != err {
t.Errorf("mismatched error: got %v, expected %v", err, tc.err)
}
return
} else if err != nil {
t.Errorf("unexpected parse error: %v", err)
return
}
if repo.String() != tc.input {
t.Errorf("mismatched repo: got %q, expected %q", repo.String(), tc.input)
}
if named, ok := repo.(Named); ok {
if named.Name() != tc.repository {
t.Errorf("unexpected repository: got %q, expected %q", named.Name(), tc.repository)
}
domain, _ := SplitHostname(named)
if domain != tc.domain {
t.Errorf("unexpected domain: got %q, expected %q", domain, tc.domain)
}
} else if tc.repository != "" || tc.domain != "" {
t.Errorf("expected named type, got %T", repo)
}
tagged, ok := repo.(Tagged)
if tc.tag != "" {
if ok {
if tagged.Tag() != tc.tag {
t.Errorf("unexpected tag: got %q, expected %q", tagged.Tag(), tc.tag)
}
} else {
t.Errorf("expected tagged type, got %T", repo)
}
} else if ok {
t.Errorf("unexpected tagged type")
}
digested, ok := repo.(Digested)
if tc.digest != "" {
if ok {
if digested.Digest().String() != tc.digest {
t.Errorf("unexpected digest: got %q, expected %q", digested.Digest().String(), tc.digest)
}
} else {
t.Errorf("expected digested type, got %T", repo)
}
} else if ok {
t.Errorf("unexpected digested type")
}
})
}
}
// TestWithNameFailure tests cases where WithName should fail. Cases where it
// should succeed are covered by TestSplitHostname, below.
func TestWithNameFailure(t *testing.T) {
t.Parallel()
tests := []struct {
input string
err error
}{
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.input, func(t *testing.T) {
t.Parallel()
_, err := WithName(tc.input)
if err == nil {
t.Errorf("no error parsing name. expected: %s", tc.err)
}
})
}
}
func TestSplitHostname(t *testing.T) {
t.Parallel()
tests := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "",
name: "test_com/foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.input, func(t *testing.T) {
t.Parallel()
named, err := WithName(tc.input)
if err != nil {
t.Errorf("error parsing name: %s", err)
}
domain, name := SplitHostname(named)
if domain != tc.domain {
t.Errorf("unexpected domain: got %q, expected %q", domain, tc.domain)
}
if name != tc.name {
t.Errorf("unexpected name: got %q, expected %q", name, tc.name)
}
})
}
}
type serializationType struct {
Description string
Field Field
}
func TestSerialization(t *testing.T) {
t.Parallel()
tests := []struct {
description string
input string
name string
tag string
digest string
err error
}{
{
description: "empty value",
err: ErrNameEmpty,
},
{
description: "just a name",
input: "example.com:8000/named",
name: "example.com:8000/named",
},
{
description: "name with a tag",
input: "example.com:8000/named:tagged",
name: "example.com:8000/named",
tag: "tagged",
},
{
description: "name with digest",
input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112",
name: "other.com/named",
digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
m := map[string]string{
"Description": tc.description,
"Field": tc.input,
}
b, err := json.Marshal(m)
if err != nil {
t.Errorf("error marshalling: %v", err)
}
st := serializationType{}
if err := json.Unmarshal(b, &st); err != nil {
if tc.err == nil {
t.Errorf("error unmarshalling: %v", err)
}
if err != tc.err {
t.Errorf("wrong error, expected %v, got %v", tc.err, err)
}
return
} else if tc.err != nil {
t.Errorf("expected error unmarshalling: %v", tc.err)
}
if st.Description != tc.description {
t.Errorf("wrong description, expected %q, got %q", tc.description, st.Description)
}
ref := st.Field.Reference()
if named, ok := ref.(Named); ok {
if named.Name() != tc.name {
t.Errorf("unexpected repository: got %q, expected %q", named.Name(), tc.name)
}
} else if tc.name != "" {
t.Errorf("expected named type, got %T", ref)
}
tagged, ok := ref.(Tagged)
if tc.tag != "" {
if ok {
if tagged.Tag() != tc.tag {
t.Errorf("unexpected tag: got %q, expected %q", tagged.Tag(), tc.tag)
}
} else {
t.Errorf("expected tagged type, got %T", ref)
}
} else if ok {
t.Errorf("unexpected tagged type")
}
digested, ok := ref.(Digested)
if tc.digest != "" {
if ok {
if digested.Digest().String() != tc.digest {
t.Errorf("unexpected digest: got %q, expected %q", digested.Digest().String(), tc.digest)
}
} else {
t.Errorf("expected digested type, got %T", ref)
}
} else if ok {
t.Errorf("unexpected digested type")
}
st = serializationType{
Description: tc.description,
Field: AsField(ref),
}
b2, err := json.Marshal(st)
if err != nil {
t.Errorf("error marshing serialization type: %v", err)
}
if string(b) != string(b2) {
t.Errorf("unexpected serialized value: expected %q, got %q", string(b), string(b2))
}
// Ensure st.Field is not implementing "Reference" directly, getting
// around the Reference type system
var fieldInterface interface{} = st.Field
if _, ok := fieldInterface.(Reference); ok {
t.Errorf("field should not implement Reference interface")
}
})
}
}
func TestWithTag(t *testing.T) {
t.Parallel()
tests := []struct {
name string
digest digest.Digest
tag string
combined string
}{
{
name: "test.com/foo",
tag: "tag",
combined: "test.com/foo:tag",
},
{
name: "foo",
tag: "tag2",
combined: "foo:tag2",
},
{
name: "test.com:8000/foo",
tag: "tag4",
combined: "test.com:8000/foo:tag4",
},
{
name: "test.com:8000/foo",
tag: "TAG5",
combined: "test.com:8000/foo:TAG5",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
tag: "TAG5",
combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.combined, func(t *testing.T) {
t.Parallel()
named, err := WithName(tc.name)
if err != nil {
t.Errorf("error parsing name: %s", err)
}
if tc.digest != "" {
canonical, err := WithDigest(named, tc.digest)
if err != nil {
t.Errorf("error adding digest")
}
named = canonical
}
tagged, err := WithTag(named, tc.tag)
if err != nil {
t.Errorf("WithTag failed: %s", err)
}
if tagged.String() != tc.combined {
t.Errorf("unexpected: got %q, expected %q", tagged.String(), tc.combined)
}
})
}
}
func TestWithDigest(t *testing.T) {
t.Parallel()
tests := []struct {
name string
digest digest.Digest
tag string
combined string
}{
{
name: "test.com/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
tag: "latest",
combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.combined, func(t *testing.T) {
t.Parallel()
named, err := WithName(tc.name)
if err != nil {
t.Errorf("error parsing name: %s", err)
}
if tc.tag != "" {
tagged, err := WithTag(named, tc.tag)
if err != nil {
t.Errorf("error adding tag")
}
named = tagged
}
digested, err := WithDigest(named, tc.digest)
if err != nil {
t.Errorf("WithDigest failed: %s", err)
}
if digested.String() != tc.combined {
t.Errorf("unexpected: got %q, expected %q", digested.String(), tc.combined)
}
})
}
}
func TestParseNamed(t *testing.T) {
t.Parallel()
tests := []struct {
input string
domain string
name string
err error
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test_com/foo",
err: ErrNameNotCanonical,
},
{
input: "test.com",
err: ErrNameNotCanonical,
},
{
input: "foo",
err: ErrNameNotCanonical,
},
{
input: "library/foo",
err: ErrNameNotCanonical,
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
// Ambiguous case, parser will add "library/" to foo
{
input: "docker.io/foo",
err: ErrNameNotCanonical,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.input, func(t *testing.T) {
t.Parallel()
named, err := ParseNamed(tc.input)
if err != nil && tc.err == nil {
t.Errorf("error parsing name: %s", err)
return
} else if err == nil && tc.err != nil {
t.Errorf("parsing succeeded: expected error %v", tc.err)
return
} else if err != tc.err {
t.Errorf("unexpected error %v, expected %v", err, tc.err)
return
} else if err != nil {
return
}
domain, name := SplitHostname(named)
if domain != tc.domain {
t.Errorf("unexpected domain: got %q, expected %q", domain, tc.domain)
}
if name != tc.name {
t.Errorf("unexpected name: got %q, expected %q", name, tc.name)
}
})
}
}