mirror of https://github.com/docker/cli.git
add internal fork of docker/docker/registry
This adds an internal fork of [github.com/docker/docker/registry], taken
at commit [moby@f651a5d]. Git history was not preserved in this fork,
but can be found using the URLs provided.
This fork was created to remove the dependency on the "Moby" codebase,
and because the CLI only needs a subset of its features. The original
package was written specifically for use in the daemon code, and includes
functionality that cannot be used in the CLI.
[github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry
[moby@49306c6]: 49306c607b/registry
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
636a4cf2dc
commit
f6b90bc253
|
|
@ -16,8 +16,8 @@ import (
|
|||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/moby/moby/api/types/auxprogress"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import (
|
|||
"github.com/docker/cli/cli/config/configfile"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/pkg/errors"
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import (
|
|||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import (
|
|||
"github.com/docker/cli/cli/debug"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/ocischema"
|
||||
|
|
@ -13,7 +14,6 @@ import (
|
|||
"github.com/docker/distribution/registry/api/errcode"
|
||||
v2 "github.com/docker/distribution/registry/api/v2"
|
||||
distclient "github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import (
|
|||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import (
|
|||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/oauth"
|
||||
"github.com/docker/cli/internal/oauth/api"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func buildAuthConfigs() map[string]registry.AuthConfig {
|
||||
authConfigs := map[string]registry.AuthConfig{}
|
||||
|
||||
for _, reg := range []string{"testIndex", IndexServer} {
|
||||
authConfigs[reg] = registry.AuthConfig{
|
||||
Username: "docker-user",
|
||||
Password: "docker-pass",
|
||||
}
|
||||
}
|
||||
|
||||
return authConfigs
|
||||
}
|
||||
|
||||
func TestResolveAuthConfigIndexServer(t *testing.T) {
|
||||
authConfigs := buildAuthConfigs()
|
||||
indexConfig := authConfigs[IndexServer]
|
||||
|
||||
officialIndex := ®istry.IndexInfo{
|
||||
Official: true,
|
||||
}
|
||||
privateIndex := ®istry.IndexInfo{
|
||||
Official: false,
|
||||
}
|
||||
|
||||
resolved := ResolveAuthConfig(authConfigs, officialIndex)
|
||||
assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer")
|
||||
|
||||
resolved = ResolveAuthConfig(authConfigs, privateIndex)
|
||||
assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer")
|
||||
}
|
||||
|
||||
func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||
authConfigs := buildAuthConfigs()
|
||||
|
||||
registryAuth := registry.AuthConfig{
|
||||
Username: "foo-user",
|
||||
Password: "foo-pass",
|
||||
}
|
||||
localAuth := registry.AuthConfig{
|
||||
Username: "bar-user",
|
||||
Password: "bar-pass",
|
||||
}
|
||||
officialAuth := registry.AuthConfig{
|
||||
Username: "baz-user",
|
||||
Password: "baz-pass",
|
||||
}
|
||||
authConfigs[IndexServer] = officialAuth
|
||||
|
||||
expectedAuths := map[string]registry.AuthConfig{
|
||||
"registry.example.com": registryAuth,
|
||||
"localhost:8000": localAuth,
|
||||
"example.com": localAuth,
|
||||
}
|
||||
|
||||
validRegistries := map[string][]string{
|
||||
"registry.example.com": {
|
||||
"https://registry.example.com/v1/",
|
||||
"http://registry.example.com/v1/",
|
||||
"registry.example.com",
|
||||
"registry.example.com/v1/",
|
||||
},
|
||||
"localhost:8000": {
|
||||
"https://localhost:8000/v1/",
|
||||
"http://localhost:8000/v1/",
|
||||
"localhost:8000",
|
||||
"localhost:8000/v1/",
|
||||
},
|
||||
"example.com": {
|
||||
"https://example.com/v1/",
|
||||
"http://example.com/v1/",
|
||||
"example.com",
|
||||
"example.com/v1/",
|
||||
},
|
||||
}
|
||||
|
||||
for configKey, registries := range validRegistries {
|
||||
configured, ok := expectedAuths[configKey]
|
||||
if !ok {
|
||||
t.Fail()
|
||||
}
|
||||
index := ®istry.IndexInfo{
|
||||
Name: configKey,
|
||||
}
|
||||
for _, reg := range registries {
|
||||
authConfigs[reg] = configured
|
||||
resolved := ResolveAuthConfig(authConfigs, index)
|
||||
if resolved.Username != configured.Username || resolved.Password != configured.Password {
|
||||
t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
|
||||
}
|
||||
delete(authConfigs, reg)
|
||||
resolved = ResolveAuthConfig(authConfigs, index)
|
||||
if resolved.Username == configured.Username || resolved.Password == configured.Password {
|
||||
t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestValidateMirror(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
output string
|
||||
expectedErr string
|
||||
}{
|
||||
// Valid cases
|
||||
{
|
||||
input: "http://mirror-1.example.com",
|
||||
output: "http://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/",
|
||||
output: "http://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com",
|
||||
output: "https://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/",
|
||||
output: "https://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "http://localhost",
|
||||
output: "http://localhost/",
|
||||
},
|
||||
{
|
||||
input: "https://localhost",
|
||||
output: "https://localhost/",
|
||||
},
|
||||
{
|
||||
input: "http://localhost:5000",
|
||||
output: "http://localhost:5000/",
|
||||
},
|
||||
{
|
||||
input: "https://localhost:5000",
|
||||
output: "https://localhost:5000/",
|
||||
},
|
||||
{
|
||||
input: "http://127.0.0.1",
|
||||
output: "http://127.0.0.1/",
|
||||
},
|
||||
{
|
||||
input: "https://127.0.0.1",
|
||||
output: "https://127.0.0.1/",
|
||||
},
|
||||
{
|
||||
input: "http://127.0.0.1:5000",
|
||||
output: "http://127.0.0.1:5000/",
|
||||
},
|
||||
{
|
||||
input: "https://127.0.0.1:5000",
|
||||
output: "https://127.0.0.1:5000/",
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/v1/",
|
||||
output: "http://mirror-1.example.com/v1/",
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/v1/",
|
||||
output: "https://mirror-1.example.com/v1/",
|
||||
},
|
||||
|
||||
// Invalid cases
|
||||
{
|
||||
input: "!invalid!://%as%",
|
||||
expectedErr: `invalid mirror: "!invalid!://%as%" is not a valid URI: parse "!invalid!://%as%": first path segment in URL cannot contain colon`,
|
||||
},
|
||||
{
|
||||
input: "mirror-1.example.com",
|
||||
expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
input: "mirror-1.example.com:5000",
|
||||
expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com:5000": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
input: "ftp://mirror-1.example.com",
|
||||
expectedErr: `invalid mirror: unsupported scheme "ftp" in "ftp://mirror-1.example.com": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/?q=foo",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/?q=foo"`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/v1/?q=foo",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo"`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/v1/?q=foo#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo#frag"`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com?q=foo",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com?q=foo"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com#frag"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/#frag"`,
|
||||
},
|
||||
{
|
||||
input: "http://foo:bar@mirror-1.example.com/",
|
||||
expectedErr: `invalid mirror: username/password not allowed in URI "http://foo:xxxxx@mirror-1.example.com/"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/v1/#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/v1/#frag"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com?q",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com?q"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
out, err := ValidateMirror(tc.input)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, err, tc.expectedErr)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
assert.Check(t, is.Equal(out, tc.output))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInsecureRegistries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
registries []string
|
||||
index string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
registries: []string{"127.0.0.1"},
|
||||
index: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
registries: []string{"127.0.0.1:8080"},
|
||||
index: "127.0.0.1:8080",
|
||||
},
|
||||
{
|
||||
registries: []string{"2001:db8::1"},
|
||||
index: "2001:db8::1",
|
||||
},
|
||||
{
|
||||
registries: []string{"[2001:db8::1]:80"},
|
||||
index: "[2001:db8::1]:80",
|
||||
},
|
||||
{
|
||||
registries: []string{"http://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"https://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"HTTP://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"svn://myregistry.example.com"},
|
||||
err: "insecure registry svn://myregistry.example.com should not contain '://'",
|
||||
},
|
||||
{
|
||||
registries: []string{"-invalid-registry"},
|
||||
err: "Cannot begin or end with a hyphen",
|
||||
},
|
||||
{
|
||||
registries: []string{`mytest-.com`},
|
||||
err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
|
||||
err: `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`myregistry.example.com:500000`},
|
||||
err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`"myregistry.example.com"`},
|
||||
err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`,
|
||||
},
|
||||
{
|
||||
registries: []string{`"myregistry.example.com:5000"`},
|
||||
err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
config := &serviceConfig{}
|
||||
err := config.loadInsecureRegistries(testCase.registries)
|
||||
if testCase.err == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got '%s'", err)
|
||||
}
|
||||
match := false
|
||||
for index := range config.IndexConfigs {
|
||||
if index == testCase.index {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.IndexConfigs)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatalf("expect error '%s', got no error", testCase.err)
|
||||
}
|
||||
assert.ErrorContains(t, err, testCase.err)
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServiceConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
opts ServiceOptions
|
||||
errStr string
|
||||
}{
|
||||
{
|
||||
doc: "empty config",
|
||||
},
|
||||
{
|
||||
doc: "invalid mirror",
|
||||
opts: ServiceOptions{
|
||||
Mirrors: []string{"example.com:5000"},
|
||||
},
|
||||
errStr: `invalid mirror: no scheme specified for "example.com:5000": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
doc: "valid mirror",
|
||||
opts: ServiceOptions{
|
||||
Mirrors: []string{"https://example.com:5000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "invalid insecure registry",
|
||||
opts: ServiceOptions{
|
||||
InsecureRegistries: []string{"[fe80::]/64"},
|
||||
},
|
||||
errStr: `insecure registry [fe80::]/64 is not valid: invalid host "[fe80::]/64"`,
|
||||
},
|
||||
{
|
||||
doc: "valid insecure registry",
|
||||
opts: ServiceOptions{
|
||||
InsecureRegistries: []string{"102.10.8.1/24"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
_, err := newServiceConfig(tc.opts)
|
||||
if tc.errStr != "" {
|
||||
assert.Check(t, is.Error(err, tc.errStr))
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err))
|
||||
} else {
|
||||
assert.Check(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIndexName(t *testing.T) {
|
||||
valid := []struct {
|
||||
index string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
index: "index.docker.io",
|
||||
expect: "docker.io",
|
||||
},
|
||||
{
|
||||
index: "example.com",
|
||||
expect: "example.com",
|
||||
},
|
||||
{
|
||||
index: "127.0.0.1:8080",
|
||||
expect: "127.0.0.1:8080",
|
||||
},
|
||||
{
|
||||
index: "mytest-1.com",
|
||||
expect: "mytest-1.com",
|
||||
},
|
||||
{
|
||||
index: "mirror-1.example.com/v1/?q=foo",
|
||||
expect: "mirror-1.example.com/v1/?q=foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range valid {
|
||||
result, err := ValidateIndexName(testCase.index)
|
||||
if assert.Check(t, err) {
|
||||
assert.Check(t, is.Equal(testCase.expect, result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIndexNameWithError(t *testing.T) {
|
||||
invalid := []struct {
|
||||
index string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
index: "docker.io-",
|
||||
err: "invalid index name (docker.io-). Cannot begin or end with a hyphen",
|
||||
},
|
||||
{
|
||||
index: "-example.com",
|
||||
err: "invalid index name (-example.com). Cannot begin or end with a hyphen",
|
||||
},
|
||||
{
|
||||
index: "mirror-1.example.com/v1/?q=foo-",
|
||||
err: "invalid index name (mirror-1.example.com/v1/?q=foo-). Cannot begin or end with a hyphen",
|
||||
},
|
||||
}
|
||||
for _, testCase := range invalid {
|
||||
_, err := ValidateIndexName(testCase.index)
|
||||
assert.Check(t, is.Error(err, testCase.err))
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Package registry is a fork of [github.com/docker/docker/registry], taken
|
||||
// at commit [moby@49306c6]. Git history was not preserved in this fork,
|
||||
// but can be found using the URLs provided.
|
||||
//
|
||||
// This fork was created to remove the dependency on the "Moby" codebase,
|
||||
// and because the CLI only needs a subset of its features. The original
|
||||
// package was written specifically for use in the daemon code, and includes
|
||||
// functionality that cannot be used in the CLI.
|
||||
//
|
||||
// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry
|
||||
// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry
|
||||
package registry
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testHTTPServer *httptest.Server
|
||||
testHTTPSServer *httptest.Server
|
||||
)
|
||||
|
||||
func init() {
|
||||
r := http.NewServeMux()
|
||||
|
||||
// /v1/
|
||||
r.HandleFunc("/v1/_ping", handlerGetPing)
|
||||
r.HandleFunc("/v1/search", handlerSearch)
|
||||
|
||||
// /v2/
|
||||
r.HandleFunc("/v2/version", handlerGetPing)
|
||||
|
||||
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
||||
testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
|
||||
}
|
||||
|
||||
func handlerAccessLog(handler http.Handler) http.Handler {
|
||||
logHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL)
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(logHandler)
|
||||
}
|
||||
|
||||
func makeURL(req string) string {
|
||||
return testHTTPServer.URL + req
|
||||
}
|
||||
|
||||
func makeHTTPSURL(req string) string {
|
||||
return testHTTPSServer.URL + req
|
||||
}
|
||||
|
||||
func makeIndex(req string) *registry.IndexInfo {
|
||||
return ®istry.IndexInfo{
|
||||
Name: makeURL(req),
|
||||
}
|
||||
}
|
||||
|
||||
func makeHTTPSIndex(req string) *registry.IndexInfo {
|
||||
return ®istry.IndexInfo{
|
||||
Name: makeHTTPSURL(req),
|
||||
}
|
||||
}
|
||||
|
||||
func makePublicIndex() *registry.IndexInfo {
|
||||
return ®istry.IndexInfo{
|
||||
Name: IndexServer,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
}
|
||||
|
||||
func writeHeaders(w http.ResponseWriter) {
|
||||
h := w.Header()
|
||||
h.Add("Server", "docker-tests/mock")
|
||||
h.Add("Expires", "-1")
|
||||
h.Add("Content-Type", "application/json")
|
||||
h.Add("Pragma", "no-cache")
|
||||
h.Add("Cache-Control", "no-cache")
|
||||
}
|
||||
|
||||
func writeResponse(w http.ResponseWriter, message interface{}, code int) {
|
||||
writeHeaders(w)
|
||||
w.WriteHeader(code)
|
||||
body, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(w, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func handlerGetPing(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
writeResponse(w, true, http.StatusOK)
|
||||
}
|
||||
|
||||
func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
result := ®istry.SearchResults{
|
||||
Query: "fakequery",
|
||||
NumResults: 1,
|
||||
Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
||||
}
|
||||
writeResponse(w, result, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
res, err := http.Get(makeURL("/v1/_ping"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, res.StatusCode, http.StatusOK, "")
|
||||
assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
|
||||
_ = res.Body.Close()
|
||||
}
|
||||
|
|
@ -0,0 +1,637 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
// overrideLookupIP overrides net.LookupIP for testing.
|
||||
func overrideLookupIP(t *testing.T) {
|
||||
t.Helper()
|
||||
restoreLookup := lookupIP
|
||||
|
||||
// override net.LookupIP
|
||||
lookupIP = func(host string) ([]net.IP, error) {
|
||||
mockHosts := map[string][]net.IP{
|
||||
"": {net.ParseIP("0.0.0.0")},
|
||||
"localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
||||
"example.com": {net.ParseIP("42.42.42.42")},
|
||||
"other.com": {net.ParseIP("43.43.43.43")},
|
||||
}
|
||||
if addrs, ok := mockHosts[host]; ok {
|
||||
return addrs, nil
|
||||
}
|
||||
return nil, errors.New("lookup: no such host")
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
lookupIP = restoreLookup
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseRepositoryInfo(t *testing.T) {
|
||||
type staticRepositoryInfo struct {
|
||||
Index *registry.IndexInfo
|
||||
RemoteName string
|
||||
CanonicalName string
|
||||
LocalName string
|
||||
}
|
||||
|
||||
tests := map[string]staticRepositoryInfo{
|
||||
"fooo/bar": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "fooo/bar",
|
||||
LocalName: "fooo/bar",
|
||||
CanonicalName: "docker.io/fooo/bar",
|
||||
},
|
||||
"library/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"nonlibrary/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "nonlibrary/ubuntu",
|
||||
LocalName: "nonlibrary/ubuntu",
|
||||
CanonicalName: "docker.io/nonlibrary/ubuntu",
|
||||
},
|
||||
"ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"other/library": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "other/library",
|
||||
LocalName: "other/library",
|
||||
CanonicalName: "docker.io/other/library",
|
||||
},
|
||||
"127.0.0.1:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "127.0.0.1:8000/private/moonbase",
|
||||
CanonicalName: "127.0.0.1:8000/private/moonbase",
|
||||
},
|
||||
"127.0.0.1:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "127.0.0.1:8000/privatebase",
|
||||
CanonicalName: "127.0.0.1:8000/privatebase",
|
||||
},
|
||||
"[::1]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::1]:8000/private/moonbase",
|
||||
CanonicalName: "[::1]:8000/private/moonbase",
|
||||
},
|
||||
"[::1]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::1]:8000/privatebase",
|
||||
CanonicalName: "[::1]:8000/privatebase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::2]:8000/private/moonbase",
|
||||
CanonicalName: "[::2]:8000/private/moonbase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::2]:8000/privatebase",
|
||||
CanonicalName: "[::2]:8000/privatebase",
|
||||
},
|
||||
"localhost:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost:8000/private/moonbase",
|
||||
CanonicalName: "localhost:8000/private/moonbase",
|
||||
},
|
||||
"localhost:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost:8000/privatebase",
|
||||
CanonicalName: "localhost:8000/privatebase",
|
||||
},
|
||||
"example.com/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com/private/moonbase",
|
||||
CanonicalName: "example.com/private/moonbase",
|
||||
},
|
||||
"example.com/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com/privatebase",
|
||||
CanonicalName: "example.com/privatebase",
|
||||
},
|
||||
"example.com:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com:8000/private/moonbase",
|
||||
CanonicalName: "example.com:8000/private/moonbase",
|
||||
},
|
||||
"example.com:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com:8000/privatebase",
|
||||
CanonicalName: "example.com:8000/privatebase",
|
||||
},
|
||||
"localhost/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost/private/moonbase",
|
||||
CanonicalName: "localhost/private/moonbase",
|
||||
},
|
||||
"localhost/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost/privatebase",
|
||||
CanonicalName: "localhost/privatebase",
|
||||
},
|
||||
IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"index." + IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
"index." + IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
}
|
||||
|
||||
for reposName, expected := range tests {
|
||||
t.Run(reposName, func(t *testing.T) {
|
||||
named, err := reference.ParseNormalizedNamed(reposName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
repoInfo, err := ParseRepositoryInfo(named)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index))
|
||||
assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName))
|
||||
assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName))
|
||||
assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIndexInfo(t *testing.T) {
|
||||
overrideLookupIP(t)
|
||||
|
||||
// ipv6Loopback is the CIDR for the IPv6 loopback address ("::1"); "::1/128"
|
||||
ipv6Loopback := &net.IPNet{
|
||||
IP: net.IPv6loopback,
|
||||
Mask: net.CIDRMask(128, 128),
|
||||
}
|
||||
|
||||
// ipv4Loopback is the CIDR for IPv4 loopback addresses ("127.0.0.0/8")
|
||||
ipv4Loopback := &net.IPNet{
|
||||
IP: net.IPv4(127, 0, 0, 0),
|
||||
Mask: net.CIDRMask(8, 32),
|
||||
}
|
||||
|
||||
// emptyServiceConfig is a default service-config for situations where
|
||||
// no config-file is available (e.g. when used in the CLI). It won't
|
||||
// have mirrors configured, but does have the default insecure registry
|
||||
// CIDRs for loopback interfaces configured.
|
||||
emptyServiceConfig := &serviceConfig{
|
||||
IndexConfigs: map[string]*registry.IndexInfo{
|
||||
IndexName: {
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
},
|
||||
InsecureRegistryCIDRs: []*registry.NetIPNet{
|
||||
(*registry.NetIPNet)(ipv6Loopback),
|
||||
(*registry.NetIPNet)(ipv4Loopback),
|
||||
},
|
||||
}
|
||||
|
||||
expectedIndexInfos := map[string]*registry.IndexInfo{
|
||||
IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"index." + IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"example.com": {
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1:5000": {
|
||||
Name: "127.0.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
}
|
||||
t.Run("no mirrors", func(t *testing.T) {
|
||||
for indexName, expected := range expectedIndexInfos {
|
||||
t.Run(indexName, func(t *testing.T) {
|
||||
actual := newIndexInfo(emptyServiceConfig, indexName)
|
||||
assert.Check(t, is.DeepEqual(actual, expected))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
expectedIndexInfos = map[string]*registry.IndexInfo{
|
||||
IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
||||
},
|
||||
"index." + IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
||||
},
|
||||
"example.com": {
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"example.com:5000": {
|
||||
Name: "example.com:5000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1": {
|
||||
Name: "127.0.0.1",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1:5000": {
|
||||
Name: "127.0.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.255.255.255": {
|
||||
Name: "127.255.255.255",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.255.255.255:5000": {
|
||||
Name: "127.255.255.255:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"::1": {
|
||||
Name: "::1",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"[::1]:5000": {
|
||||
Name: "[::1]:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"::2": {
|
||||
Name: "::2",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:5000": {
|
||||
Name: "[::2]:5000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"other.com": {
|
||||
Name: "other.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
}
|
||||
t.Run("mirrors", func(t *testing.T) {
|
||||
// Note that newServiceConfig calls ValidateMirror internally, which normalizes
|
||||
// mirror-URLs to have a trailing slash.
|
||||
config, err := newServiceConfig(ServiceOptions{
|
||||
Mirrors: []string{"http://mirror1.local", "http://mirror2.local"},
|
||||
InsecureRegistries: []string{"example.com"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
for indexName, expected := range expectedIndexInfos {
|
||||
t.Run(indexName, func(t *testing.T) {
|
||||
actual := newIndexInfo(config, indexName)
|
||||
assert.Check(t, is.DeepEqual(actual, expected))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
expectedIndexInfos = map[string]*registry.IndexInfo{
|
||||
"example.com": {
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"example.com:5000": {
|
||||
Name: "example.com:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1": {
|
||||
Name: "127.0.0.1",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1:5000": {
|
||||
Name: "127.0.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"42.42.0.1:5000": {
|
||||
Name: "42.42.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"42.43.0.1:5000": {
|
||||
Name: "42.43.0.1:5000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"other.com": {
|
||||
Name: "other.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
}
|
||||
t.Run("custom insecure", func(t *testing.T) {
|
||||
config, err := newServiceConfig(ServiceOptions{
|
||||
InsecureRegistries: []string{"42.42.0.0/16"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
for indexName, expected := range expectedIndexInfos {
|
||||
t.Run(indexName, func(t *testing.T) {
|
||||
actual := newIndexInfo(config, indexName)
|
||||
assert.Check(t, is.DeepEqual(actual, expected))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMirrorEndpointLookup(t *testing.T) {
|
||||
containsMirror := func(endpoints []APIEndpoint) bool {
|
||||
for _, pe := range endpoints {
|
||||
if pe.URL.Host == "my.mirror" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
cfg, err := newServiceConfig(ServiceOptions{
|
||||
Mirrors: []string{"https://my.mirror"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
s := Service{config: cfg}
|
||||
|
||||
imageName, err := reference.WithName(IndexName + "/test/image")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if containsMirror(pushAPIEndpoints) {
|
||||
t.Fatal("Push endpoint should not contain mirror")
|
||||
}
|
||||
|
||||
pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsMirror(pullAPIEndpoints) {
|
||||
t.Fatal("Pull endpoint should contain mirror")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSecureIndex(t *testing.T) {
|
||||
overrideLookupIP(t)
|
||||
tests := []struct {
|
||||
addr string
|
||||
insecureRegistries []string
|
||||
expected bool
|
||||
}{
|
||||
{IndexName, nil, true},
|
||||
{"example.com", []string{}, true},
|
||||
{"example.com", []string{"example.com"}, false},
|
||||
{"localhost", []string{"localhost:5000"}, false},
|
||||
{"localhost:5000", []string{"localhost:5000"}, false},
|
||||
{"localhost", []string{"example.com"}, false},
|
||||
{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
|
||||
{"localhost", nil, false},
|
||||
{"localhost:5000", nil, false},
|
||||
{"127.0.0.1", nil, false},
|
||||
{"localhost", []string{"example.com"}, false},
|
||||
{"127.0.0.1", []string{"example.com"}, false},
|
||||
{"example.com", nil, true},
|
||||
{"example.com", []string{"example.com"}, false},
|
||||
{"127.0.0.1", []string{"example.com"}, false},
|
||||
{"127.0.0.1:5000", []string{"example.com"}, false},
|
||||
{"example.com:5000", []string{"42.42.0.0/16"}, false},
|
||||
{"example.com", []string{"42.42.0.0/16"}, false},
|
||||
{"example.com:5000", []string{"42.42.42.42/8"}, false},
|
||||
{"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
|
||||
{"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
|
||||
{"invalid.example.com", []string{"42.42.0.0/16"}, true},
|
||||
{"invalid.example.com", []string{"invalid.example.com"}, false},
|
||||
{"invalid.example.com:5000", []string{"invalid.example.com"}, true},
|
||||
{"invalid.example.com:5000", []string{"invalid.example.com:5000"}, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
config, err := newServiceConfig(ServiceOptions{
|
||||
InsecureRegistries: tc.insecureRegistries,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
sec := config.isSecureIndex(tc.addr)
|
||||
assert.Equal(t, sec, tc.expected, "isSecureIndex failed for %q %v, expected %v got %v", tc.addr, tc.insecureRegistries, tc.expected, sec)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestV1EndpointPing(t *testing.T) {
|
||||
testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
|
||||
ep, err := newV1Endpoint(context.Background(), index, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
regInfo, err := ep.ping(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage)
|
||||
}
|
||||
|
||||
testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
|
||||
testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)")
|
||||
testPing(makePublicIndex(), false, "Expected standalone to be false for public index")
|
||||
}
|
||||
|
||||
func TestV1Endpoint(t *testing.T) {
|
||||
// Simple wrapper to fail test if err != nil
|
||||
expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
|
||||
endpoint, err := newV1Endpoint(context.Background(), index, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
assertInsecureIndex := func(index *registry.IndexInfo) {
|
||||
index.Secure = true
|
||||
_, err := newV1Endpoint(context.Background(), index, nil)
|
||||
assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index")
|
||||
index.Secure = false
|
||||
}
|
||||
|
||||
assertSecureIndex := func(index *registry.IndexInfo) {
|
||||
index.Secure = true
|
||||
_, err := newV1Endpoint(context.Background(), index, nil)
|
||||
assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
|
||||
index.Secure = false
|
||||
}
|
||||
|
||||
index := ®istry.IndexInfo{}
|
||||
index.Name = makeURL("/v1/")
|
||||
endpoint := expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||
assertInsecureIndex(index)
|
||||
|
||||
index.Name = makeURL("")
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
||||
assertInsecureIndex(index)
|
||||
|
||||
httpURL := makeURL("")
|
||||
index.Name = strings.SplitN(httpURL, "://", 2)[1]
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
|
||||
assertInsecureIndex(index)
|
||||
|
||||
index.Name = makeHTTPSURL("/v1/")
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||
assertSecureIndex(index)
|
||||
|
||||
index.Name = makeHTTPSURL("")
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
||||
assertSecureIndex(index)
|
||||
|
||||
httpsURL := makeHTTPSURL("")
|
||||
index.Name = strings.SplitN(httpsURL, "://", 2)[1]
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
|
||||
assertSecureIndex(index)
|
||||
|
||||
badEndpoints := []string{
|
||||
"http://127.0.0.1/v1/",
|
||||
"https://127.0.0.1/v1/",
|
||||
"http://127.0.0.1",
|
||||
"https://127.0.0.1",
|
||||
"127.0.0.1",
|
||||
}
|
||||
for _, address := range badEndpoints {
|
||||
index.Name = address
|
||||
_, err := newV1Endpoint(context.Background(), index, nil)
|
||||
assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1EndpointParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
address string
|
||||
expected string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
address: IndexServer,
|
||||
expected: IndexServer,
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/v1/",
|
||||
expected: "https://0.0.0.0:5000/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000",
|
||||
expected: "https://0.0.0.0:5000/v1/",
|
||||
},
|
||||
{
|
||||
address: "0.0.0.0:5000",
|
||||
expected: "https://0.0.0.0:5000/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/nonversion/",
|
||||
expected: "https://0.0.0.0:5000/nonversion/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/v0/",
|
||||
expected: "https://0.0.0.0:5000/v0/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/v2/",
|
||||
expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.address, func(t *testing.T) {
|
||||
ep, err := newV1EndpointFromStr(tc.address, nil, nil)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Check(t, is.Error(err, tc.expectedErr))
|
||||
assert.Check(t, is.Nil(ep))
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(ep.String(), tc.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that a registry endpoint that responds with a 401 only is determined
|
||||
// to be a valid v1 registry endpoint
|
||||
func TestV1EndpointValidate(t *testing.T) {
|
||||
requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
// Make a test server which should validate as a v1 server.
|
||||
testServer := httptest.NewServer(requireBasicAuthHandler)
|
||||
defer testServer.Close()
|
||||
|
||||
testEndpoint, err := newV1Endpoint(context.Background(), ®istry.IndexInfo{Name: testServer.URL}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if testEndpoint.URL.Scheme != "http" {
|
||||
t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedLocation(t *testing.T) {
|
||||
for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
|
||||
req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
|
||||
assert.Check(t, !trustedLocation(req))
|
||||
}
|
||||
|
||||
for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} {
|
||||
req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
|
||||
assert.Check(t, trustedLocation(req))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
|
||||
for _, urls := range [][]string{
|
||||
{"http://docker.io", "https://docker.com"},
|
||||
{"https://foo.docker.io:7777", "http://bar.docker.com"},
|
||||
{"https://foo.docker.io", "https://example.com"},
|
||||
} {
|
||||
reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody)
|
||||
reqFrom.Header.Add("Content-Type", "application/json")
|
||||
reqFrom.Header.Add("Authorization", "super_secret")
|
||||
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
|
||||
|
||||
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
||||
|
||||
if len(reqTo.Header) != 1 {
|
||||
t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
|
||||
}
|
||||
|
||||
if reqTo.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatal("'Content-Type' should be 'application/json'")
|
||||
}
|
||||
|
||||
if reqTo.Header.Get("Authorization") != "" {
|
||||
t.Fatal("'Authorization' should be empty")
|
||||
}
|
||||
}
|
||||
|
||||
for _, urls := range [][]string{
|
||||
{"https://docker.io", "https://docker.com"},
|
||||
{"https://foo.docker.io:7777", "https://bar.docker.com"},
|
||||
} {
|
||||
reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody)
|
||||
reqFrom.Header.Add("Content-Type", "application/json")
|
||||
reqFrom.Header.Add("Authorization", "super_secret")
|
||||
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
|
||||
|
||||
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
||||
|
||||
if len(reqTo.Header) != 2 {
|
||||
t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
|
||||
}
|
||||
|
||||
if reqTo.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatal("'Content-Type' should be 'application/json'")
|
||||
}
|
||||
|
||||
if reqTo.Header.Get("Authorization") != "super_secret" {
|
||||
t.Fatal("'Authorization' should be 'super_secret'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"testing"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/moby/moby/api/types/filters"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func spawnTestRegistrySession(t *testing.T) *session {
|
||||
t.Helper()
|
||||
authConfig := ®istry.AuthConfig{}
|
||||
endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userAgent := "docker test client"
|
||||
var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
|
||||
tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
|
||||
client := httpClient(tr)
|
||||
|
||||
if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := newSession(client, endpoint)
|
||||
|
||||
// In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true`
|
||||
// header while authenticating, in order to retrieve a token that can be later used to
|
||||
// perform authenticated actions.
|
||||
//
|
||||
// The mock v1 registry does not support that, (TODO(tiborvass): support it), instead,
|
||||
// it will consider authenticated any request with the header `X-Docker-Token: fake-token`.
|
||||
//
|
||||
// Because we know that the client's transport is an `*authTransport` we simply cast it,
|
||||
// in order to set the internal cached token to the fake token, and thus send that fake token
|
||||
// upon every subsequent requests.
|
||||
r.client.Transport.(*authTransport).token = []string{"fake-token"}
|
||||
return r
|
||||
}
|
||||
|
||||
type debugTransport struct {
|
||||
http.RoundTripper
|
||||
log func(...interface{})
|
||||
}
|
||||
|
||||
func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
dump, err := httputil.DumpRequestOut(req, false)
|
||||
if err != nil {
|
||||
tr.log("could not dump request")
|
||||
}
|
||||
tr.log(string(dump))
|
||||
resp, err := tr.RoundTripper.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dump, err = httputil.DumpResponse(resp, false)
|
||||
if err != nil {
|
||||
tr.log("could not dump response")
|
||||
}
|
||||
tr.log(string(dump))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func TestSearchRepositories(t *testing.T) {
|
||||
r := spawnTestRegistrySession(t)
|
||||
results, err := r.searchRepositories(context.Background(), "fakequery", 25)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if results == nil {
|
||||
t.Fatal("Expected non-nil SearchResults object")
|
||||
}
|
||||
assert.Equal(t, results.NumResults, 1, "Expected 1 search results")
|
||||
assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query")
|
||||
assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars")
|
||||
}
|
||||
|
||||
func TestSearchErrors(t *testing.T) {
|
||||
errorCases := []struct {
|
||||
filtersArgs filters.Args
|
||||
shouldReturnError bool
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
expectedError: "unexpected status code 500",
|
||||
shouldReturnError: true,
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("type", "custom")),
|
||||
expectedError: "invalid filter 'type'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "invalid")),
|
||||
expectedError: "invalid filter 'is-automated=[invalid]'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("is-automated", "true"),
|
||||
filters.Arg("is-automated", "false"),
|
||||
),
|
||||
expectedError: "invalid filter 'is-automated",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "invalid")),
|
||||
expectedError: "invalid filter 'is-official=[invalid]'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("is-official", "true"),
|
||||
filters.Arg("is-official", "false"),
|
||||
),
|
||||
expectedError: "invalid filter 'is-official",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "invalid")),
|
||||
expectedError: "invalid filter 'stars=invalid'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("stars", "1"),
|
||||
filters.Arg("stars", "invalid"),
|
||||
),
|
||||
expectedError: "invalid filter 'stars=invalid'",
|
||||
},
|
||||
}
|
||||
for _, tc := range errorCases {
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !tc.shouldReturnError {
|
||||
t.Errorf("unexpected HTTP request")
|
||||
}
|
||||
http.Error(w, "no search for you", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Construct the search term by cutting the 'http://' prefix off srv.URL.
|
||||
term := srv.URL[7:] + "/term"
|
||||
|
||||
reg, err := NewService(ServiceOptions{})
|
||||
assert.NilError(t, err)
|
||||
_, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
if tc.shouldReturnError {
|
||||
assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err)
|
||||
return
|
||||
}
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
const term = "term"
|
||||
successCases := []struct {
|
||||
name string
|
||||
filtersArgs filters.Args
|
||||
registryResults []registry.SearchResult
|
||||
expectedResults []registry.SearchResult
|
||||
}{
|
||||
{
|
||||
name: "empty results",
|
||||
registryResults: []registry.SearchResult{},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "no filter",
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-automated=true, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-automated=true",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-automated=false, IsAutomated reset to false",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-automated=false",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-official=true, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-official=true",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: true,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-official=false, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: true,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-official=false",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: false,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stars=0",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "0")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
StarCount: 0,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
StarCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stars=0, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
StarCount: 0,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "stars=1",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name0",
|
||||
Description: "description0",
|
||||
StarCount: 0,
|
||||
},
|
||||
{
|
||||
Name: "name1",
|
||||
Description: "description1",
|
||||
StarCount: 1,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name1",
|
||||
Description: "description1",
|
||||
StarCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stars=1, is-official=true, is-automated=true",
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("stars", "1"),
|
||||
filters.Arg("is-official", "true"),
|
||||
filters.Arg("is-automated", "true"),
|
||||
),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name0",
|
||||
Description: "description0",
|
||||
StarCount: 0,
|
||||
IsOfficial: true,
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
{
|
||||
Name: "name1",
|
||||
Description: "description1",
|
||||
StarCount: 1,
|
||||
IsOfficial: false,
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
{
|
||||
Name: "name2",
|
||||
Description: "description2",
|
||||
StarCount: 1,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
Name: "name3",
|
||||
Description: "description3",
|
||||
StarCount: 2,
|
||||
IsOfficial: true,
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
}
|
||||
for _, tc := range successCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(registry.SearchResults{
|
||||
Query: term,
|
||||
NumResults: len(tc.registryResults),
|
||||
Results: tc.registryResults,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Construct the search term by cutting the 'http://' prefix off srv.URL.
|
||||
searchTerm := srv.URL[7:] + "/" + term
|
||||
|
||||
reg, err := NewService(ServiceOptions{})
|
||||
assert.NilError(t, err)
|
||||
results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, results, tc.expectedResults)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ replace (
|
|||
require (
|
||||
dario.cat/mergo v1.0.1
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/log v0.1.0
|
||||
github.com/containerd/platforms v1.0.0-rc.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||
github.com/creack/pty v1.1.24
|
||||
|
|
@ -55,6 +56,7 @@ require (
|
|||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
|
||||
|
|
@ -78,7 +80,6 @@ require (
|
|||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
|
|
@ -105,7 +106,6 @@ require (
|
|||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ github.com/docker/docker/pkg/jsonmessage
|
|||
github.com/docker/docker/pkg/process
|
||||
github.com/docker/docker/pkg/progress
|
||||
github.com/docker/docker/pkg/streamformatter
|
||||
github.com/docker/docker/registry
|
||||
# github.com/docker/docker-credential-helpers v0.9.3
|
||||
## explicit; go 1.21
|
||||
github.com/docker/docker-credential-helpers/client
|
||||
|
|
|
|||
Loading…
Reference in New Issue