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/command/completion"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/tui"
|
"github.com/docker/cli/internal/tui"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/moby/moby/api/types/auxprogress"
|
"github.com/moby/moby/api/types/auxprogress"
|
||||||
"github.com/moby/moby/api/types/image"
|
"github.com/moby/moby/api/types/image"
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/docker/registry"
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/moby/moby/api/types/image"
|
"github.com/moby/moby/api/types/image"
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/cli/internal/prompt"
|
"github.com/docker/cli/internal/prompt"
|
||||||
"github.com/docker/docker/registry"
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/moby/moby/api/types"
|
"github.com/moby/moby/api/types"
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"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"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ import (
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
configtypes "github.com/docker/cli/cli/config/types"
|
||||||
"github.com/docker/cli/internal/oauth/manager"
|
"github.com/docker/cli/internal/oauth/manager"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/tui"
|
"github.com/docker/cli/internal/tui"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import (
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
configtypes "github.com/docker/cli/cli/config/types"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/prompt"
|
"github.com/docker/cli/internal/prompt"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/moby/moby/api/types/system"
|
"github.com/moby/moby/api/types/system"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
"github.com/docker/cli/cli/config/credentials"
|
||||||
"github.com/docker/cli/internal/oauth/manager"
|
"github.com/docker/cli/internal/oauth/manager"
|
||||||
"github.com/docker/docker/registry"
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/trust"
|
"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/moby/moby/api/types/swarm"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import (
|
||||||
"github.com/docker/cli/cli/debug"
|
"github.com/docker/cli/cli/debug"
|
||||||
flagsHelper "github.com/docker/cli/cli/flags"
|
flagsHelper "github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/cli/internal/lazyregexp"
|
"github.com/docker/cli/internal/lazyregexp"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/templates"
|
"github.com/docker/cli/templates"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/moby/moby/api/types/swarm"
|
||||||
"github.com/moby/moby/api/types/system"
|
"github.com/moby/moby/api/types/system"
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/distribution/registry/client/auth"
|
"github.com/docker/distribution/registry/client/auth"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli/manifest/types"
|
"github.com/docker/cli/cli/manifest/types"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/manifest/manifestlist"
|
"github.com/docker/distribution/manifest/manifestlist"
|
||||||
"github.com/docker/distribution/manifest/ocischema"
|
"github.com/docker/distribution/manifest/ocischema"
|
||||||
|
|
@ -13,7 +14,6 @@ import (
|
||||||
"github.com/docker/distribution/registry/api/errcode"
|
"github.com/docker/distribution/registry/api/errcode"
|
||||||
v2 "github.com/docker/distribution/registry/api/v2"
|
v2 "github.com/docker/distribution/registry/api/v2"
|
||||||
distclient "github.com/docker/distribution/registry/client"
|
distclient "github.com/docker/distribution/registry/client"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ import (
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli/config"
|
"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"
|
||||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/go-connections/tlsconfig"
|
"github.com/docker/go-connections/tlsconfig"
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/docker/registry"
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/moby/moby/api/types"
|
"github.com/moby/moby/api/types"
|
||||||
registrytypes "github.com/moby/moby/api/types/registry"
|
registrytypes "github.com/moby/moby/api/types/registry"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import (
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/oauth"
|
"github.com/docker/cli/internal/oauth"
|
||||||
"github.com/docker/cli/internal/oauth/api"
|
"github.com/docker/cli/internal/oauth/api"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/tui"
|
"github.com/docker/cli/internal/tui"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/morikuni/aec"
|
"github.com/morikuni/aec"
|
||||||
"github.com/sirupsen/logrus"
|
"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 (
|
require (
|
||||||
dario.cat/mergo v1.0.1
|
dario.cat/mergo v1.0.1
|
||||||
github.com/containerd/errdefs v1.0.0
|
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/containerd/platforms v1.0.0-rc.1
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
|
|
@ -55,6 +56,7 @@ require (
|
||||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
||||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
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 v1.35.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc 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
|
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/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.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 v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // 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
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
|
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // 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/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||||
golang.org/x/crypto v0.37.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/process
|
||||||
github.com/docker/docker/pkg/progress
|
github.com/docker/docker/pkg/progress
|
||||||
github.com/docker/docker/pkg/streamformatter
|
github.com/docker/docker/pkg/streamformatter
|
||||||
github.com/docker/docker/registry
|
|
||||||
# github.com/docker/docker-credential-helpers v0.9.3
|
# github.com/docker/docker-credential-helpers v0.9.3
|
||||||
## explicit; go 1.21
|
## explicit; go 1.21
|
||||||
github.com/docker/docker-credential-helpers/client
|
github.com/docker/docker-credential-helpers/client
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue