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