package reference import ( "strconv" "testing" "github.com/distribution/distribution/v3/digestset" "github.com/opencontainers/go-digest" ) func TestValidateReferenceName(t *testing.T) { validRepoNames := []string{ "docker/docker", "library/debian", "debian", "docker.io/docker/docker", "docker.io/library/debian", "docker.io/debian", "index.docker.io/docker/docker", "index.docker.io/library/debian", "index.docker.io/debian", "127.0.0.1:5000/docker/docker", "127.0.0.1:5000/library/debian", "127.0.0.1:5000/debian", "192.168.0.1", "192.168.0.1:80", "192.168.0.1:8/debian", "192.168.0.2:25000/debian", "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", "[fc00::1]:5000/docker", "[fc00::1]:5000/docker/docker", "[fc00:1:2:3:4:5:6:7]:5000/library/debian", // This test case was moved from invalid to valid since it is valid input // when specified with a hostname, it removes the ambiguity from about // whether the value is an identifier or repository name "docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", "Docker/docker", "DOCKER/docker", } invalidRepoNames := []string{ "https://github.com/docker/docker", "docker/Docker", "-docker", "-docker/docker", "-docker.io/docker/docker", "docker///docker", "docker.io/docker/Docker", "docker.io/docker///docker", "[fc00::1]", "[fc00::1]:5000", "fc00::1:5000/debian", "[fe80::1%eth0]:5000/debian", "[2001:db8:3:4::192.0.2.33]:5000/debian", "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", } for _, name := range invalidRepoNames { _, err := ParseNormalizedNamed(name) if err == nil { t.Fatalf("Expected invalid repo name for %q", name) } } for _, name := range validRepoNames { _, err := ParseNormalizedNamed(name) if err != nil { t.Fatalf("Error parsing repo name %s, got: %q", name, err) } } } func TestValidateRemoteName(t *testing.T) { validRepositoryNames := []string{ // Sanity check. "docker/docker", // Allow 64-character non-hexadecimal names (hexadecimal names are forbidden). "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", // Allow embedded hyphens. "docker-rules/docker", // Allow multiple hyphens as well. "docker---rules/docker", // Username doc and image name docker being tested. "doc/docker", // single character names are now allowed. "d/docker", "jess/t", // Consecutive underscores. "dock__er/docker", } for _, repositoryName := range validRepositoryNames { _, err := ParseNormalizedNamed(repositoryName) if err != nil { t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) } } invalidRepositoryNames := []string{ // Disallow capital letters. "docker/Docker", // Only allow one slash. "docker///docker", // Disallow 64-character hexadecimal. "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", // Disallow leading and trailing hyphens in namespace. "-docker/docker", "docker-/docker", "-docker-/docker", // Don't allow underscores everywhere (as opposed to hyphens). "____/____", "_docker/_docker", // Disallow consecutive periods. "dock..er/docker", "dock_.er/docker", "dock-.er/docker", // No repository. "docker/", // namespace too long "this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker", } for _, repositoryName := range invalidRepositoryNames { if _, err := ParseNormalizedNamed(repositoryName); err == nil { t.Errorf("Repository name should be invalid: %v", repositoryName) } } } func TestParseRepositoryInfo(t *testing.T) { type tcase struct { RemoteName, FamiliarName, FullName, AmbiguousName, Domain string } tcases := []tcase{ { RemoteName: "fooo/bar", FamiliarName: "fooo/bar", FullName: "docker.io/fooo/bar", AmbiguousName: "index.docker.io/fooo/bar", Domain: "docker.io", }, { RemoteName: "library/ubuntu", FamiliarName: "ubuntu", FullName: "docker.io/library/ubuntu", AmbiguousName: "library/ubuntu", Domain: "docker.io", }, { RemoteName: "nonlibrary/ubuntu", FamiliarName: "nonlibrary/ubuntu", FullName: "docker.io/nonlibrary/ubuntu", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "other/library", FamiliarName: "other/library", FullName: "docker.io/other/library", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "private/moonbase", FamiliarName: "127.0.0.1:8000/private/moonbase", FullName: "127.0.0.1:8000/private/moonbase", AmbiguousName: "", Domain: "127.0.0.1:8000", }, { RemoteName: "privatebase", FamiliarName: "127.0.0.1:8000/privatebase", FullName: "127.0.0.1:8000/privatebase", AmbiguousName: "", Domain: "127.0.0.1:8000", }, { RemoteName: "private/moonbase", FamiliarName: "example.com/private/moonbase", FullName: "example.com/private/moonbase", AmbiguousName: "", Domain: "example.com", }, { RemoteName: "privatebase", FamiliarName: "example.com/privatebase", FullName: "example.com/privatebase", AmbiguousName: "", Domain: "example.com", }, { RemoteName: "private/moonbase", FamiliarName: "example.com:8000/private/moonbase", FullName: "example.com:8000/private/moonbase", AmbiguousName: "", Domain: "example.com:8000", }, { RemoteName: "privatebasee", FamiliarName: "example.com:8000/privatebasee", FullName: "example.com:8000/privatebasee", AmbiguousName: "", Domain: "example.com:8000", }, { RemoteName: "library/ubuntu-12.04-base", FamiliarName: "ubuntu-12.04-base", FullName: "docker.io/library/ubuntu-12.04-base", AmbiguousName: "index.docker.io/library/ubuntu-12.04-base", Domain: "docker.io", }, { RemoteName: "library/foo", FamiliarName: "foo", FullName: "docker.io/library/foo", AmbiguousName: "docker.io/foo", Domain: "docker.io", }, { RemoteName: "library/foo/bar", FamiliarName: "library/foo/bar", FullName: "docker.io/library/foo/bar", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "store/foo/bar", FamiliarName: "store/foo/bar", FullName: "docker.io/store/foo/bar", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "bar", FamiliarName: "Foo/bar", FullName: "Foo/bar", AmbiguousName: "", Domain: "Foo", }, { RemoteName: "bar", FamiliarName: "FOO/bar", FullName: "FOO/bar", AmbiguousName: "", Domain: "FOO", }, } for _, tcase := range tcases { refStrings := []string{tcase.FamiliarName, tcase.FullName} if tcase.AmbiguousName != "" { refStrings = append(refStrings, tcase.AmbiguousName) } var refs []Named for _, r := range refStrings { named, err := ParseNormalizedNamed(r) if err != nil { t.Fatal(err) } refs = append(refs, named) } for _, r := range refs { if expected, actual := tcase.FamiliarName, FamiliarName(r); expected != actual { t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual) } if expected, actual := tcase.FullName, r.String(); expected != actual { t.Fatalf("Invalid canonical reference for %q. Expected %q, got %q", r, expected, actual) } if expected, actual := tcase.Domain, Domain(r); expected != actual { t.Fatalf("Invalid domain for %q. Expected %q, got %q", r, expected, actual) } if expected, actual := tcase.RemoteName, Path(r); expected != actual { t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual) } } } } func TestParseReferenceWithTagAndDigest(t *testing.T) { shortRef := "busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa" ref, err := ParseNormalizedNamed(shortRef) if err != nil { t.Fatal(err) } if expected, actual := "docker.io/library/"+shortRef, ref.String(); actual != expected { t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) } if _, isTagged := ref.(NamedTagged); !isTagged { t.Fatalf("Reference from %q should support tag", ref) } if _, isCanonical := ref.(Canonical); !isCanonical { t.Fatalf("Reference from %q should support digest", ref) } if expected, actual := shortRef, FamiliarString(ref); actual != expected { t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) } } func TestInvalidReferenceComponents(t *testing.T) { if _, err := ParseNormalizedNamed("-foo"); err == nil { t.Fatal("Expected WithName to detect invalid name") } ref, err := ParseNormalizedNamed("busybox") if err != nil { t.Fatal(err) } if _, err := WithTag(ref, "-foo"); err == nil { t.Fatal("Expected WithName to detect invalid tag") } if _, err := WithDigest(ref, digest.Digest("foo")); err == nil { t.Fatal("Expected WithDigest to detect invalid digest") } } func equalReference(r1, r2 Reference) bool { switch v1 := r1.(type) { case digestReference: if v2, ok := r2.(digestReference); ok { return v1 == v2 } case repository: if v2, ok := r2.(repository); ok { return v1 == v2 } case taggedReference: if v2, ok := r2.(taggedReference); ok { return v1 == v2 } case canonicalReference: if v2, ok := r2.(canonicalReference); ok { return v1 == v2 } case reference: if v2, ok := r2.(reference); ok { return v1 == v2 } } return false } func TestParseAnyReference(t *testing.T) { tcases := []struct { Reference string Equivalent string Expected Reference Digests []digest.Digest }{ { Reference: "redis", Equivalent: "docker.io/library/redis", }, { Reference: "redis:latest", Equivalent: "docker.io/library/redis:latest", }, { Reference: "docker.io/library/redis:latest", Equivalent: "docker.io/library/redis:latest", }, { Reference: "redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "dmcgowan/myapp", Equivalent: "docker.io/dmcgowan/myapp", }, { Reference: "dmcgowan/myapp:latest", Equivalent: "docker.io/dmcgowan/myapp:latest", }, { Reference: "docker.io/mcgowan/myapp:latest", Equivalent: "docker.io/mcgowan/myapp:latest", }, { Reference: "dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", }, { Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Digests: []digest.Digest{ digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), }, }, { Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", Digests: []digest.Digest{ digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), }, }, { Reference: "dbcc1c", Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Digests: []digest.Digest{ digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), }, }, { Reference: "dbcc1", Equivalent: "docker.io/library/dbcc1", Digests: []digest.Digest{ digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), }, }, { Reference: "dbcc1c", Equivalent: "docker.io/library/dbcc1c", Digests: []digest.Digest{ digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), }, }, } for _, tcase := range tcases { var ref Reference var err error if len(tcase.Digests) == 0 { ref, err = ParseAnyReference(tcase.Reference) } else { ds := digestset.NewSet() for _, dgst := range tcase.Digests { if err := ds.Add(dgst); err != nil { t.Fatalf("Error adding digest %s: %v", dgst.String(), err) } } ref, err = ParseAnyReferenceWithSet(tcase.Reference, ds) } if err != nil { t.Fatalf("Error parsing reference %s: %v", tcase.Reference, err) } if ref.String() != tcase.Equivalent { t.Fatalf("Unexpected string: %s, expected %s", ref.String(), tcase.Equivalent) } expected := tcase.Expected if expected == nil { expected, err = Parse(tcase.Equivalent) if err != nil { t.Fatalf("Error parsing reference %s: %v", tcase.Equivalent, err) } } if !equalReference(ref, expected) { t.Errorf("Unexpected reference %#v, expected %#v", ref, expected) } } } func TestNormalizedSplitHostname(t *testing.T) { testcases := []struct { input string domain string name string }{ { input: "test.com/foo", domain: "test.com", name: "foo", }, { input: "test_com/foo", domain: "docker.io", name: "test_com/foo", }, { input: "docker/migrator", domain: "docker.io", name: "docker/migrator", }, { input: "test.com:8080/foo", domain: "test.com:8080", name: "foo", }, { input: "test-com:8080/foo", domain: "test-com:8080", name: "foo", }, { input: "foo", domain: "docker.io", name: "library/foo", }, { input: "xn--n3h.com/foo", domain: "xn--n3h.com", name: "foo", }, { input: "xn--n3h.com:18080/foo", domain: "xn--n3h.com:18080", name: "foo", }, { input: "docker.io/foo", domain: "docker.io", name: "library/foo", }, { input: "docker.io/library/foo", domain: "docker.io", name: "library/foo", }, { input: "docker.io/library/foo/bar", domain: "docker.io", name: "library/foo/bar", }, } for _, testcase := range testcases { failf := func(format string, v ...interface{}) { t.Logf(strconv.Quote(testcase.input)+": "+format, v...) t.Fail() } named, err := ParseNormalizedNamed(testcase.input) if err != nil { failf("error parsing name: %s", err) } domain, name := SplitHostname(named) if domain != testcase.domain { failf("unexpected domain: got %q, expected %q", domain, testcase.domain) } if name != testcase.name { failf("unexpected name: got %q, expected %q", name, testcase.name) } } } func TestMatchError(t *testing.T) { named, err := ParseAnyReference("foo") if err != nil { t.Fatal(err) } _, err = FamiliarMatch("[-x]", named) if err == nil { t.Fatalf("expected an error, got nothing") } } func TestMatch(t *testing.T) { matchCases := []struct { reference string pattern string expected bool }{ { reference: "foo", pattern: "foo/**/ba[rz]", expected: false, }, { reference: "foo/any/bat", pattern: "foo/**/ba[rz]", expected: false, }, { reference: "foo/a/bar", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/b/baz", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/*/baz:tag", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/c/baz:tag", expected: true, }, { reference: "example.com/foo/c/baz:tag", pattern: "*/foo/c/baz", expected: true, }, { reference: "example.com/foo/c/baz:tag", pattern: "example.com/foo/c/baz", expected: true, }, } for _, c := range matchCases { named, err := ParseAnyReference(c.reference) if err != nil { t.Fatal(err) } actual, err := FamiliarMatch(c.pattern, named) if err != nil { t.Fatal(err) } if actual != c.expected { t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual) } } } func TestParseDockerRef(t *testing.T) { testcases := []struct { name string input string expected string }{ { name: "nothing", input: "busybox", expected: "docker.io/library/busybox:latest", }, { name: "tag only", input: "busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "digest only", input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", expected: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", }, { name: "path only", input: "library/busybox", expected: "docker.io/library/busybox:latest", }, { name: "hostname only", input: "docker.io/busybox", expected: "docker.io/library/busybox:latest", }, { name: "no tag", input: "docker.io/library/busybox", expected: "docker.io/library/busybox:latest", }, { name: "no path", input: "docker.io/busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "no hostname", input: "library/busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "full reference with tag", input: "docker.io/library/busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "gcr reference without tag", input: "gcr.io/library/busybox", expected: "gcr.io/library/busybox:latest", }, { name: "both tag and digest", input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", expected: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", }, } for _, test := range testcases { t.Run(test.name, func(t *testing.T) { normalized, err := ParseDockerRef(test.input) if err != nil { t.Fatal(err) } output := normalized.String() if output != test.expected { t.Fatalf("expected %q to be parsed as %v, got %v", test.input, test.expected, output) } _, err = Parse(output) if err != nil { t.Fatalf("%q should be a valid reference, but got an error: %v", output, err) } }) } }