Signed-off-by: Derek McGowan <derek@mcg.dev>
| ... | ... |
@@ -5,8 +5,8 @@ import ( |
| 5 | 5 |
|
| 6 | 6 |
"github.com/docker/docker/daemon/config" |
| 7 | 7 |
"github.com/docker/docker/daemon/pkg/opts" |
| 8 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 8 | 9 |
dopts "github.com/docker/docker/internal/opts" |
| 9 |
- "github.com/docker/docker/registry" |
|
| 10 | 10 |
"github.com/spf13/pflag" |
| 11 | 11 |
) |
| 12 | 12 |
|
| ... | ... |
@@ -14,8 +14,8 @@ import ( |
| 14 | 14 |
"dario.cat/mergo" |
| 15 | 15 |
"github.com/containerd/log" |
| 16 | 16 |
"github.com/docker/docker/daemon/pkg/opts" |
| 17 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 17 | 18 |
dopts "github.com/docker/docker/internal/opts" |
| 18 |
- "github.com/docker/docker/registry" |
|
| 19 | 19 |
"github.com/moby/moby/api" |
| 20 | 20 |
"github.com/moby/moby/api/types/versions" |
| 21 | 21 |
"github.com/pkg/errors" |
| ... | ... |
@@ -13,7 +13,7 @@ import ( |
| 13 | 13 |
"dario.cat/mergo" |
| 14 | 14 |
"github.com/docker/docker/daemon/libnetwork/ipamutils" |
| 15 | 15 |
"github.com/docker/docker/daemon/pkg/opts" |
| 16 |
- "github.com/docker/docker/registry" |
|
| 16 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 17 | 17 |
"github.com/google/go-cmp/cmp" |
| 18 | 18 |
"github.com/google/go-cmp/cmp/cmpopts" |
| 19 | 19 |
"github.com/spf13/pflag" |
| ... | ... |
@@ -10,9 +10,9 @@ import ( |
| 10 | 10 |
cerrdefs "github.com/containerd/errdefs" |
| 11 | 11 |
"github.com/containerd/log" |
| 12 | 12 |
"github.com/distribution/reference" |
| 13 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 13 | 14 |
"github.com/docker/docker/dockerversion" |
| 14 | 15 |
"github.com/docker/docker/pkg/useragent" |
| 15 |
- "github.com/docker/docker/registry" |
|
| 16 | 16 |
registrytypes "github.com/moby/moby/api/types/registry" |
| 17 | 17 |
) |
| 18 | 18 |
|
| ... | ... |
@@ -55,6 +55,7 @@ import ( |
| 55 | 55 |
dlogger "github.com/docker/docker/daemon/logger" |
| 56 | 56 |
"github.com/docker/docker/daemon/network" |
| 57 | 57 |
"github.com/docker/docker/daemon/pkg/plugin" |
| 58 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 58 | 59 |
"github.com/docker/docker/daemon/snapshotter" |
| 59 | 60 |
"github.com/docker/docker/daemon/stats" |
| 60 | 61 |
volumesservice "github.com/docker/docker/daemon/volume/service" |
| ... | ... |
@@ -64,7 +65,6 @@ import ( |
| 64 | 64 |
"github.com/docker/docker/pkg/idtools" |
| 65 | 65 |
"github.com/docker/docker/pkg/plugingetter" |
| 66 | 66 |
"github.com/docker/docker/pkg/sysinfo" |
| 67 |
- "github.com/docker/docker/registry" |
|
| 68 | 67 |
"github.com/moby/buildkit/util/grpcerrors" |
| 69 | 68 |
"github.com/moby/buildkit/util/tracing" |
| 70 | 69 |
"github.com/moby/locker" |
| ... | ... |
@@ -16,7 +16,7 @@ import ( |
| 16 | 16 |
"github.com/containerd/containerd/v2/core/remotes/docker" |
| 17 | 17 |
hostconfig "github.com/containerd/containerd/v2/core/remotes/docker/config" |
| 18 | 18 |
cerrdefs "github.com/containerd/errdefs" |
| 19 |
- "github.com/docker/docker/registry" |
|
| 19 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 20 | 20 |
"github.com/pkg/errors" |
| 21 | 21 |
) |
| 22 | 22 |
|
| ... | ... |
@@ -19,13 +19,13 @@ import ( |
| 19 | 19 |
"github.com/docker/docker/daemon/internal/filedescriptors" |
| 20 | 20 |
"github.com/docker/docker/daemon/internal/metrics" |
| 21 | 21 |
"github.com/docker/docker/daemon/logger" |
| 22 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 22 | 23 |
"github.com/docker/docker/dockerversion" |
| 23 | 24 |
"github.com/docker/docker/internal/platform" |
| 24 | 25 |
"github.com/docker/docker/pkg/meminfo" |
| 25 | 26 |
"github.com/docker/docker/pkg/parsers/kernel" |
| 26 | 27 |
"github.com/docker/docker/pkg/parsers/operatingsystem" |
| 27 | 28 |
"github.com/docker/docker/pkg/sysinfo" |
| 28 |
- "github.com/docker/docker/registry" |
|
| 29 | 29 |
"github.com/moby/moby/api/types" |
| 30 | 30 |
"github.com/moby/moby/api/types/system" |
| 31 | 31 |
"github.com/opencontainers/selinux/go-selinux" |
| ... | ... |
@@ -14,8 +14,8 @@ import ( |
| 14 | 14 |
"github.com/docker/docker/daemon/internal/image" |
| 15 | 15 |
"github.com/docker/docker/daemon/internal/layer" |
| 16 | 16 |
refstore "github.com/docker/docker/daemon/internal/refstore" |
| 17 |
+ registrypkg "github.com/docker/docker/daemon/pkg/registry" |
|
| 17 | 18 |
"github.com/docker/docker/pkg/progress" |
| 18 |
- registrypkg "github.com/docker/docker/registry" |
|
| 19 | 19 |
"github.com/moby/moby/api/types/events" |
| 20 | 20 |
"github.com/moby/moby/api/types/registry" |
| 21 | 21 |
"github.com/opencontainers/go-digest" |
| ... | ... |
@@ -15,7 +15,7 @@ import ( |
| 15 | 15 |
"github.com/docker/distribution" |
| 16 | 16 |
"github.com/docker/distribution/manifest/manifestlist" |
| 17 | 17 |
"github.com/docker/distribution/manifest/schema2" |
| 18 |
- "github.com/docker/docker/registry" |
|
| 18 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 19 | 19 |
"github.com/opencontainers/go-digest" |
| 20 | 20 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 21 | 21 |
"github.com/pkg/errors" |
| ... | ... |
@@ -7,7 +7,7 @@ import ( |
| 7 | 7 |
"github.com/containerd/log" |
| 8 | 8 |
"github.com/distribution/reference" |
| 9 | 9 |
refstore "github.com/docker/docker/daemon/internal/refstore" |
| 10 |
- "github.com/docker/docker/registry" |
|
| 10 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 11 | 11 |
"github.com/moby/moby/api/types/events" |
| 12 | 12 |
"github.com/opencontainers/go-digest" |
| 13 | 13 |
"github.com/pkg/errors" |
| ... | ... |
@@ -22,10 +22,10 @@ import ( |
| 22 | 22 |
"github.com/docker/docker/daemon/internal/image" |
| 23 | 23 |
"github.com/docker/docker/daemon/internal/layer" |
| 24 | 24 |
refstore "github.com/docker/docker/daemon/internal/refstore" |
| 25 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 25 | 26 |
"github.com/docker/docker/pkg/ioutils" |
| 26 | 27 |
"github.com/docker/docker/pkg/progress" |
| 27 | 28 |
"github.com/docker/docker/pkg/stringid" |
| 28 |
- "github.com/docker/docker/registry" |
|
| 29 | 29 |
"github.com/opencontainers/go-digest" |
| 30 | 30 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 31 | 31 |
"github.com/pkg/errors" |
| ... | ... |
@@ -12,7 +12,7 @@ import ( |
| 12 | 12 |
|
| 13 | 13 |
"github.com/distribution/reference" |
| 14 | 14 |
"github.com/docker/docker/daemon/internal/image" |
| 15 |
- "github.com/docker/docker/registry" |
|
| 15 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 16 | 16 |
registrytypes "github.com/moby/moby/api/types/registry" |
| 17 | 17 |
"github.com/opencontainers/go-digest" |
| 18 | 18 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| ... | ... |
@@ -17,10 +17,10 @@ import ( |
| 17 | 17 |
"github.com/docker/docker/daemon/internal/distribution/metadata" |
| 18 | 18 |
"github.com/docker/docker/daemon/internal/distribution/xfer" |
| 19 | 19 |
"github.com/docker/docker/daemon/internal/layer" |
| 20 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 20 | 21 |
"github.com/docker/docker/pkg/ioutils" |
| 21 | 22 |
"github.com/docker/docker/pkg/progress" |
| 22 | 23 |
"github.com/docker/docker/pkg/stringid" |
| 23 |
- "github.com/docker/docker/registry" |
|
| 24 | 24 |
apitypes "github.com/moby/moby/api/types" |
| 25 | 25 |
"github.com/opencontainers/go-digest" |
| 26 | 26 |
"github.com/pkg/errors" |
| ... | ... |
@@ -13,8 +13,8 @@ import ( |
| 13 | 13 |
"github.com/docker/docker/daemon/internal/distribution/metadata" |
| 14 | 14 |
"github.com/docker/docker/daemon/internal/layer" |
| 15 | 15 |
refstore "github.com/docker/docker/daemon/internal/refstore" |
| 16 |
+ registrypkg "github.com/docker/docker/daemon/pkg/registry" |
|
| 16 | 17 |
"github.com/docker/docker/pkg/progress" |
| 17 |
- registrypkg "github.com/docker/docker/registry" |
|
| 18 | 18 |
"github.com/moby/moby/api/types/registry" |
| 19 | 19 |
"github.com/opencontainers/go-digest" |
| 20 | 20 |
"gotest.tools/v3/assert" |
| ... | ... |
@@ -12,8 +12,8 @@ import ( |
| 12 | 12 |
"github.com/docker/distribution/manifest/schema2" |
| 13 | 13 |
"github.com/docker/distribution/registry/client" |
| 14 | 14 |
"github.com/docker/distribution/registry/client/auth" |
| 15 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 15 | 16 |
"github.com/docker/docker/dockerversion" |
| 16 |
- "github.com/docker/docker/registry" |
|
| 17 | 17 |
registrytypes "github.com/moby/moby/api/types/registry" |
| 18 | 18 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 19 | 19 |
) |
| ... | ... |
@@ -16,10 +16,10 @@ import ( |
| 16 | 16 |
"github.com/containerd/containerd/v2/plugins/content/local" |
| 17 | 17 |
"github.com/containerd/log" |
| 18 | 18 |
v2 "github.com/docker/docker/daemon/pkg/plugin/v2" |
| 19 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 19 | 20 |
"github.com/docker/docker/internal/containerfs" |
| 20 | 21 |
"github.com/docker/docker/internal/lazyregexp" |
| 21 | 22 |
"github.com/docker/docker/pkg/authorization" |
| 22 |
- "github.com/docker/docker/registry" |
|
| 23 | 23 |
"github.com/moby/moby/api/types" |
| 24 | 24 |
"github.com/moby/moby/api/types/events" |
| 25 | 25 |
"github.com/moby/pubsub" |
| 26 | 26 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,205 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "net/url" |
|
| 6 |
+ "strings" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/containerd/log" |
|
| 10 |
+ "github.com/docker/distribution/registry/client/auth" |
|
| 11 |
+ "github.com/docker/distribution/registry/client/auth/challenge" |
|
| 12 |
+ "github.com/docker/distribution/registry/client/transport" |
|
| 13 |
+ "github.com/moby/moby/api/types/registry" |
|
| 14 |
+ "github.com/pkg/errors" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// AuthClientID is used the ClientID used for the token server |
|
| 18 |
+const AuthClientID = "docker" |
|
| 19 |
+ |
|
| 20 |
+type loginCredentialStore struct {
|
|
| 21 |
+ authConfig *registry.AuthConfig |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
|
|
| 25 |
+ return lcs.authConfig.Username, lcs.authConfig.Password |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
|
|
| 29 |
+ return lcs.authConfig.IdentityToken |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
|
|
| 33 |
+ lcs.authConfig.IdentityToken = token |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+type staticCredentialStore struct {
|
|
| 37 |
+ auth *registry.AuthConfig |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// NewStaticCredentialStore returns a credential store |
|
| 41 |
+// which always returns the same credential values. |
|
| 42 |
+func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore {
|
|
| 43 |
+ return staticCredentialStore{
|
|
| 44 |
+ auth: ac, |
|
| 45 |
+ } |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
|
|
| 49 |
+ if scs.auth == nil {
|
|
| 50 |
+ return "", "" |
|
| 51 |
+ } |
|
| 52 |
+ return scs.auth.Username, scs.auth.Password |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
|
|
| 56 |
+ if scs.auth == nil {
|
|
| 57 |
+ return "" |
|
| 58 |
+ } |
|
| 59 |
+ return scs.auth.IdentityToken |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
|
|
| 63 |
+} |
|
| 64 |
+ |
|
| 65 |
+// loginV2 tries to login to the v2 registry server. The given registry |
|
| 66 |
+// endpoint will be pinged to get authorization challenges. These challenges |
|
| 67 |
+// will be used to authenticate against the registry to validate credentials. |
|
| 68 |
+func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (token string, _ error) {
|
|
| 69 |
+ endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" |
|
| 70 |
+ log.G(ctx).WithField("endpoint", endpointStr).Debug("attempting v2 login to registry endpoint")
|
|
| 71 |
+ |
|
| 72 |
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointStr, http.NoBody) |
|
| 73 |
+ if err != nil {
|
|
| 74 |
+ return "", err |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ var ( |
|
| 78 |
+ modifiers = Headers(userAgent, nil) |
|
| 79 |
+ authTrans = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...) |
|
| 80 |
+ credentialAuthConfig = *authConfig |
|
| 81 |
+ creds = loginCredentialStore{authConfig: &credentialAuthConfig}
|
|
| 82 |
+ ) |
|
| 83 |
+ |
|
| 84 |
+ loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil) |
|
| 85 |
+ if err != nil {
|
|
| 86 |
+ return "", err |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ resp, err := loginClient.Do(req) |
|
| 90 |
+ if err != nil {
|
|
| 91 |
+ err = translateV2AuthError(err) |
|
| 92 |
+ return "", err |
|
| 93 |
+ } |
|
| 94 |
+ defer resp.Body.Close() |
|
| 95 |
+ |
|
| 96 |
+ if resp.StatusCode != http.StatusOK {
|
|
| 97 |
+ // TODO(dmcgowan): Attempt to further interpret result, status code and error code string |
|
| 98 |
+ return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
| 99 |
+ } |
|
| 100 |
+ |
|
| 101 |
+ return credentialAuthConfig.IdentityToken, nil |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) {
|
|
| 105 |
+ challengeManager, err := PingV2Registry(endpoint, authTransport) |
|
| 106 |
+ if err != nil {
|
|
| 107 |
+ return nil, err |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ authHandlers := []auth.AuthenticationHandler{
|
|
| 111 |
+ auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
|
|
| 112 |
+ Transport: authTransport, |
|
| 113 |
+ Credentials: creds, |
|
| 114 |
+ OfflineAccess: true, |
|
| 115 |
+ ClientID: AuthClientID, |
|
| 116 |
+ Scopes: scopes, |
|
| 117 |
+ }), |
|
| 118 |
+ auth.NewBasicHandler(creds), |
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+ modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, authHandlers...)) |
|
| 122 |
+ |
|
| 123 |
+ return &http.Client{
|
|
| 124 |
+ Transport: transport.NewTransport(authTransport, modifiers...), |
|
| 125 |
+ Timeout: 15 * time.Second, |
|
| 126 |
+ }, nil |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+// ConvertToHostname normalizes a registry URL which has http|https prepended |
|
| 130 |
+// to just its hostname. It is used to match credentials, which may be either |
|
| 131 |
+// stored as hostname or as hostname including scheme (in legacy configuration |
|
| 132 |
+// files). |
|
| 133 |
+func ConvertToHostname(maybeURL string) string {
|
|
| 134 |
+ stripped := maybeURL |
|
| 135 |
+ if scheme, remainder, ok := strings.Cut(stripped, "://"); ok {
|
|
| 136 |
+ switch scheme {
|
|
| 137 |
+ case "http", "https": |
|
| 138 |
+ stripped = remainder |
|
| 139 |
+ default: |
|
| 140 |
+ // unknown, or no scheme; doing nothing for now, as we never did. |
|
| 141 |
+ } |
|
| 142 |
+ } |
|
| 143 |
+ stripped, _, _ = strings.Cut(stripped, "/") |
|
| 144 |
+ return stripped |
|
| 145 |
+} |
|
| 146 |
+ |
|
| 147 |
+// ResolveAuthConfig matches an auth configuration to a server address or a URL |
|
| 148 |
+func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig {
|
|
| 149 |
+ configKey := GetAuthConfigKey(index) |
|
| 150 |
+ // First try the happy case |
|
| 151 |
+ if c, found := authConfigs[configKey]; found || index.Official {
|
|
| 152 |
+ return c |
|
| 153 |
+ } |
|
| 154 |
+ |
|
| 155 |
+ // Maybe they have a legacy config file, we will iterate the keys converting |
|
| 156 |
+ // them to the new format and testing |
|
| 157 |
+ for registryURL, ac := range authConfigs {
|
|
| 158 |
+ if configKey == ConvertToHostname(registryURL) {
|
|
| 159 |
+ return ac |
|
| 160 |
+ } |
|
| 161 |
+ } |
|
| 162 |
+ |
|
| 163 |
+ // When all else fails, return an empty auth config |
|
| 164 |
+ return registry.AuthConfig{}
|
|
| 165 |
+} |
|
| 166 |
+ |
|
| 167 |
+// PingResponseError is used when the response from a ping |
|
| 168 |
+// was received but invalid. |
|
| 169 |
+type PingResponseError struct {
|
|
| 170 |
+ Err error |
|
| 171 |
+} |
|
| 172 |
+ |
|
| 173 |
+func (err PingResponseError) Error() string {
|
|
| 174 |
+ return err.Err.Error() |
|
| 175 |
+} |
|
| 176 |
+ |
|
| 177 |
+// PingV2Registry attempts to ping a v2 registry and on success return a |
|
| 178 |
+// challenge manager for the supported authentication types. |
|
| 179 |
+// If a response is received but cannot be interpreted, a PingResponseError will be returned. |
|
| 180 |
+func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
|
|
| 181 |
+ pingClient := &http.Client{
|
|
| 182 |
+ Transport: authTransport, |
|
| 183 |
+ Timeout: 15 * time.Second, |
|
| 184 |
+ } |
|
| 185 |
+ endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" |
|
| 186 |
+ req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody) |
|
| 187 |
+ if err != nil {
|
|
| 188 |
+ return nil, err |
|
| 189 |
+ } |
|
| 190 |
+ resp, err := pingClient.Do(req) |
|
| 191 |
+ if err != nil {
|
|
| 192 |
+ return nil, err |
|
| 193 |
+ } |
|
| 194 |
+ defer resp.Body.Close() |
|
| 195 |
+ |
|
| 196 |
+ challengeManager := challenge.NewSimpleManager() |
|
| 197 |
+ if err := challengeManager.AddResponse(resp); err != nil {
|
|
| 198 |
+ return nil, PingResponseError{
|
|
| 199 |
+ Err: err, |
|
| 200 |
+ } |
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+ return challengeManager, nil |
|
| 204 |
+} |
| 0 | 205 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,106 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/moby/moby/api/types/registry" |
|
| 6 |
+ "gotest.tools/v3/assert" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func buildAuthConfigs() map[string]registry.AuthConfig {
|
|
| 10 |
+ authConfigs := map[string]registry.AuthConfig{}
|
|
| 11 |
+ |
|
| 12 |
+ for _, reg := range []string{"testIndex", IndexServer} {
|
|
| 13 |
+ authConfigs[reg] = registry.AuthConfig{
|
|
| 14 |
+ Username: "docker-user", |
|
| 15 |
+ Password: "docker-pass", |
|
| 16 |
+ } |
|
| 17 |
+ } |
|
| 18 |
+ |
|
| 19 |
+ return authConfigs |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+func TestResolveAuthConfigIndexServer(t *testing.T) {
|
|
| 23 |
+ authConfigs := buildAuthConfigs() |
|
| 24 |
+ indexConfig := authConfigs[IndexServer] |
|
| 25 |
+ |
|
| 26 |
+ officialIndex := ®istry.IndexInfo{
|
|
| 27 |
+ Official: true, |
|
| 28 |
+ } |
|
| 29 |
+ privateIndex := ®istry.IndexInfo{
|
|
| 30 |
+ Official: false, |
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ resolved := ResolveAuthConfig(authConfigs, officialIndex) |
|
| 34 |
+ assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") |
|
| 35 |
+ |
|
| 36 |
+ resolved = ResolveAuthConfig(authConfigs, privateIndex) |
|
| 37 |
+ assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer") |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+func TestResolveAuthConfigFullURL(t *testing.T) {
|
|
| 41 |
+ authConfigs := buildAuthConfigs() |
|
| 42 |
+ |
|
| 43 |
+ registryAuth := registry.AuthConfig{
|
|
| 44 |
+ Username: "foo-user", |
|
| 45 |
+ Password: "foo-pass", |
|
| 46 |
+ } |
|
| 47 |
+ localAuth := registry.AuthConfig{
|
|
| 48 |
+ Username: "bar-user", |
|
| 49 |
+ Password: "bar-pass", |
|
| 50 |
+ } |
|
| 51 |
+ officialAuth := registry.AuthConfig{
|
|
| 52 |
+ Username: "baz-user", |
|
| 53 |
+ Password: "baz-pass", |
|
| 54 |
+ } |
|
| 55 |
+ authConfigs[IndexServer] = officialAuth |
|
| 56 |
+ |
|
| 57 |
+ expectedAuths := map[string]registry.AuthConfig{
|
|
| 58 |
+ "registry.example.com": registryAuth, |
|
| 59 |
+ "localhost:8000": localAuth, |
|
| 60 |
+ "example.com": localAuth, |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ validRegistries := map[string][]string{
|
|
| 64 |
+ "registry.example.com": {
|
|
| 65 |
+ "https://registry.example.com/v1/", |
|
| 66 |
+ "http://registry.example.com/v1/", |
|
| 67 |
+ "registry.example.com", |
|
| 68 |
+ "registry.example.com/v1/", |
|
| 69 |
+ }, |
|
| 70 |
+ "localhost:8000": {
|
|
| 71 |
+ "https://localhost:8000/v1/", |
|
| 72 |
+ "http://localhost:8000/v1/", |
|
| 73 |
+ "localhost:8000", |
|
| 74 |
+ "localhost:8000/v1/", |
|
| 75 |
+ }, |
|
| 76 |
+ "example.com": {
|
|
| 77 |
+ "https://example.com/v1/", |
|
| 78 |
+ "http://example.com/v1/", |
|
| 79 |
+ "example.com", |
|
| 80 |
+ "example.com/v1/", |
|
| 81 |
+ }, |
|
| 82 |
+ } |
|
| 83 |
+ |
|
| 84 |
+ for configKey, registries := range validRegistries {
|
|
| 85 |
+ configured, ok := expectedAuths[configKey] |
|
| 86 |
+ if !ok {
|
|
| 87 |
+ t.Fail() |
|
| 88 |
+ } |
|
| 89 |
+ index := ®istry.IndexInfo{
|
|
| 90 |
+ Name: configKey, |
|
| 91 |
+ } |
|
| 92 |
+ for _, reg := range registries {
|
|
| 93 |
+ authConfigs[reg] = configured |
|
| 94 |
+ resolved := ResolveAuthConfig(authConfigs, index) |
|
| 95 |
+ if resolved.Username != configured.Username || resolved.Password != configured.Password {
|
|
| 96 |
+ t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
|
|
| 97 |
+ } |
|
| 98 |
+ delete(authConfigs, reg) |
|
| 99 |
+ resolved = ResolveAuthConfig(authConfigs, index) |
|
| 100 |
+ if resolved.Username == configured.Username || resolved.Password == configured.Password {
|
|
| 101 |
+ t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
|
|
| 102 |
+ } |
|
| 103 |
+ } |
|
| 104 |
+ } |
|
| 105 |
+} |
| 0 | 106 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,449 @@ |
| 0 |
+// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: |
|
| 1 |
+//go:build go1.23 |
|
| 2 |
+ |
|
| 3 |
+package registry |
|
| 4 |
+ |
|
| 5 |
+import ( |
|
| 6 |
+ "context" |
|
| 7 |
+ "net" |
|
| 8 |
+ "net/url" |
|
| 9 |
+ "os" |
|
| 10 |
+ "path/filepath" |
|
| 11 |
+ "regexp" |
|
| 12 |
+ "runtime" |
|
| 13 |
+ "strconv" |
|
| 14 |
+ "strings" |
|
| 15 |
+ "sync" |
|
| 16 |
+ |
|
| 17 |
+ "github.com/containerd/log" |
|
| 18 |
+ "github.com/distribution/reference" |
|
| 19 |
+ "github.com/moby/moby/api/types/registry" |
|
| 20 |
+) |
|
| 21 |
+ |
|
| 22 |
+// ServiceOptions holds command line options. |
|
| 23 |
+type ServiceOptions struct {
|
|
| 24 |
+ Mirrors []string `json:"registry-mirrors,omitempty"` |
|
| 25 |
+ InsecureRegistries []string `json:"insecure-registries,omitempty"` |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+// serviceConfig holds daemon configuration for the registry service. |
|
| 29 |
+type serviceConfig registry.ServiceConfig |
|
| 30 |
+ |
|
| 31 |
+// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains |
|
| 32 |
+// are here for historic reasons and backward-compatibility. These domains |
|
| 33 |
+// are still supported by Docker Hub (and will continue to be supported), but |
|
| 34 |
+// there are new domains already in use, and plans to consolidate all legacy |
|
| 35 |
+// domains to new "canonical" domains. Once those domains are decided on, we |
|
| 36 |
+// should update these consts (but making sure to preserve compatibility with |
|
| 37 |
+// existing installs, clients, and user configuration). |
|
| 38 |
+const ( |
|
| 39 |
+ // DefaultNamespace is the default namespace |
|
| 40 |
+ DefaultNamespace = "docker.io" |
|
| 41 |
+ // DefaultRegistryHost is the hostname for the default (Docker Hub) registry |
|
| 42 |
+ // used for pushing and pulling images. This hostname is hard-coded to handle |
|
| 43 |
+ // the conversion from image references without registry name (e.g. "ubuntu", |
|
| 44 |
+ // or "ubuntu:latest"), as well as references using the "docker.io" domain |
|
| 45 |
+ // name, which is used as canonical reference for images on Docker Hub, but |
|
| 46 |
+ // does not match the domain-name of Docker Hub's registry. |
|
| 47 |
+ DefaultRegistryHost = "registry-1.docker.io" |
|
| 48 |
+ // IndexHostname is the index hostname, used for authentication and image search. |
|
| 49 |
+ IndexHostname = "index.docker.io" |
|
| 50 |
+ // IndexServer is used for user auth and image search |
|
| 51 |
+ IndexServer = "https://" + IndexHostname + "/v1/" |
|
| 52 |
+ // IndexName is the name of the index |
|
| 53 |
+ IndexName = "docker.io" |
|
| 54 |
+) |
|
| 55 |
+ |
|
| 56 |
+var ( |
|
| 57 |
+ // DefaultV2Registry is the URI of the default (Docker Hub) registry. |
|
| 58 |
+ DefaultV2Registry = &url.URL{
|
|
| 59 |
+ Scheme: "https", |
|
| 60 |
+ Host: DefaultRegistryHost, |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
|
|
| 64 |
+ return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`) |
|
| 65 |
+ }) |
|
| 66 |
+) |
|
| 67 |
+ |
|
| 68 |
+// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit], |
|
| 69 |
+// but inlining it to prevent adding that as a dependency for docker/cli. |
|
| 70 |
+// |
|
| 71 |
+// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 |
|
| 72 |
+func runningWithRootlessKit() bool {
|
|
| 73 |
+ return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != ""
|
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+// CertsDir is the directory where certificates are stored. |
|
| 77 |
+// |
|
| 78 |
+// - Linux: "/etc/docker/certs.d/" |
|
| 79 |
+// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/" |
|
| 80 |
+// - Windows: "%PROGRAMDATA%/docker/certs.d/" |
|
| 81 |
+// |
|
| 82 |
+// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless. |
|
| 83 |
+func CertsDir() string {
|
|
| 84 |
+ certsDir := "/etc/docker/certs.d" |
|
| 85 |
+ if runningWithRootlessKit() {
|
|
| 86 |
+ if configHome, _ := os.UserConfigDir(); configHome != "" {
|
|
| 87 |
+ certsDir = filepath.Join(configHome, "docker", "certs.d") |
|
| 88 |
+ } |
|
| 89 |
+ } else if runtime.GOOS == "windows" {
|
|
| 90 |
+ certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d")
|
|
| 91 |
+ } |
|
| 92 |
+ return certsDir |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// newServiceConfig returns a new instance of ServiceConfig |
|
| 96 |
+func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
|
|
| 97 |
+ config := &serviceConfig{}
|
|
| 98 |
+ if err := config.loadMirrors(options.Mirrors); err != nil {
|
|
| 99 |
+ return nil, err |
|
| 100 |
+ } |
|
| 101 |
+ if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil {
|
|
| 102 |
+ return nil, err |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ return config, nil |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+// copy constructs a new ServiceConfig with a copy of the configuration in config. |
|
| 109 |
+func (config *serviceConfig) copy() *registry.ServiceConfig {
|
|
| 110 |
+ ic := make(map[string]*registry.IndexInfo) |
|
| 111 |
+ for key, value := range config.IndexConfigs {
|
|
| 112 |
+ ic[key] = value |
|
| 113 |
+ } |
|
| 114 |
+ return ®istry.ServiceConfig{
|
|
| 115 |
+ InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...), |
|
| 116 |
+ IndexConfigs: ic, |
|
| 117 |
+ Mirrors: append([]string(nil), config.Mirrors...), |
|
| 118 |
+ } |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+// loadMirrors loads mirrors to config, after removing duplicates. |
|
| 122 |
+// Returns an error if mirrors contains an invalid mirror. |
|
| 123 |
+func (config *serviceConfig) loadMirrors(mirrors []string) error {
|
|
| 124 |
+ mMap := map[string]struct{}{}
|
|
| 125 |
+ unique := []string{}
|
|
| 126 |
+ |
|
| 127 |
+ for _, mirror := range mirrors {
|
|
| 128 |
+ m, err := ValidateMirror(mirror) |
|
| 129 |
+ if err != nil {
|
|
| 130 |
+ return err |
|
| 131 |
+ } |
|
| 132 |
+ if _, exist := mMap[m]; !exist {
|
|
| 133 |
+ mMap[m] = struct{}{}
|
|
| 134 |
+ unique = append(unique, m) |
|
| 135 |
+ } |
|
| 136 |
+ } |
|
| 137 |
+ |
|
| 138 |
+ config.Mirrors = unique |
|
| 139 |
+ |
|
| 140 |
+ // Configure public registry since mirrors may have changed. |
|
| 141 |
+ config.IndexConfigs = map[string]*registry.IndexInfo{
|
|
| 142 |
+ IndexName: {
|
|
| 143 |
+ Name: IndexName, |
|
| 144 |
+ Mirrors: unique, |
|
| 145 |
+ Secure: true, |
|
| 146 |
+ Official: true, |
|
| 147 |
+ }, |
|
| 148 |
+ } |
|
| 149 |
+ |
|
| 150 |
+ return nil |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+// loadInsecureRegistries loads insecure registries to config |
|
| 154 |
+func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
|
|
| 155 |
+ // Localhost is by default considered as an insecure registry. This is a |
|
| 156 |
+ // stop-gap for people who are running a private registry on localhost. |
|
| 157 |
+ registries = append(registries, "::1/128", "127.0.0.0/8") |
|
| 158 |
+ |
|
| 159 |
+ var ( |
|
| 160 |
+ insecureRegistryCIDRs = make([]*registry.NetIPNet, 0) |
|
| 161 |
+ indexConfigs = make(map[string]*registry.IndexInfo) |
|
| 162 |
+ ) |
|
| 163 |
+ |
|
| 164 |
+skip: |
|
| 165 |
+ for _, r := range registries {
|
|
| 166 |
+ // validate insecure registry |
|
| 167 |
+ if _, err := ValidateIndexName(r); err != nil {
|
|
| 168 |
+ return err |
|
| 169 |
+ } |
|
| 170 |
+ if scheme, host, ok := strings.Cut(r, "://"); ok {
|
|
| 171 |
+ switch strings.ToLower(scheme) {
|
|
| 172 |
+ case "http", "https": |
|
| 173 |
+ log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme)
|
|
| 174 |
+ r = host |
|
| 175 |
+ default: |
|
| 176 |
+ // unsupported scheme |
|
| 177 |
+ return invalidParamf("insecure registry %s should not contain '://'", r)
|
|
| 178 |
+ } |
|
| 179 |
+ } |
|
| 180 |
+ // Check if CIDR was passed to --insecure-registry |
|
| 181 |
+ _, ipnet, err := net.ParseCIDR(r) |
|
| 182 |
+ if err == nil {
|
|
| 183 |
+ // Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip. |
|
| 184 |
+ data := (*registry.NetIPNet)(ipnet) |
|
| 185 |
+ for _, value := range insecureRegistryCIDRs {
|
|
| 186 |
+ if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
|
|
| 187 |
+ continue skip |
|
| 188 |
+ } |
|
| 189 |
+ } |
|
| 190 |
+ // ipnet is not found, add it in config.InsecureRegistryCIDRs |
|
| 191 |
+ insecureRegistryCIDRs = append(insecureRegistryCIDRs, data) |
|
| 192 |
+ } else {
|
|
| 193 |
+ if err := validateHostPort(r); err != nil {
|
|
| 194 |
+ return invalidParamWrapf(err, "insecure registry %s is not valid", r) |
|
| 195 |
+ } |
|
| 196 |
+ // Assume `host:port` if not CIDR. |
|
| 197 |
+ indexConfigs[r] = ®istry.IndexInfo{
|
|
| 198 |
+ Name: r, |
|
| 199 |
+ Mirrors: []string{},
|
|
| 200 |
+ Secure: false, |
|
| 201 |
+ Official: false, |
|
| 202 |
+ } |
|
| 203 |
+ } |
|
| 204 |
+ } |
|
| 205 |
+ |
|
| 206 |
+ // Configure public registry. |
|
| 207 |
+ indexConfigs[IndexName] = ®istry.IndexInfo{
|
|
| 208 |
+ Name: IndexName, |
|
| 209 |
+ Mirrors: config.Mirrors, |
|
| 210 |
+ Secure: true, |
|
| 211 |
+ Official: true, |
|
| 212 |
+ } |
|
| 213 |
+ config.InsecureRegistryCIDRs = insecureRegistryCIDRs |
|
| 214 |
+ config.IndexConfigs = indexConfigs |
|
| 215 |
+ |
|
| 216 |
+ return nil |
|
| 217 |
+} |
|
| 218 |
+ |
|
| 219 |
+// isSecureIndex returns false if the provided indexName is part of the list of insecure registries |
|
| 220 |
+// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. |
|
| 221 |
+// |
|
| 222 |
+// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. |
|
| 223 |
+// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered |
|
| 224 |
+// insecure. |
|
| 225 |
+// |
|
| 226 |
+// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name |
|
| 227 |
+// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained |
|
| 228 |
+// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element |
|
| 229 |
+// of insecureRegistries. |
|
| 230 |
+func (config *serviceConfig) isSecureIndex(indexName string) bool {
|
|
| 231 |
+ // Check for configured index, first. This is needed in case isSecureIndex |
|
| 232 |
+ // is called from anything besides newIndexInfo, in order to honor per-index configurations. |
|
| 233 |
+ if index, ok := config.IndexConfigs[indexName]; ok {
|
|
| 234 |
+ return index.Secure |
|
| 235 |
+ } |
|
| 236 |
+ |
|
| 237 |
+ return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName) |
|
| 238 |
+} |
|
| 239 |
+ |
|
| 240 |
+// for mocking in unit tests. |
|
| 241 |
+var lookupIP = net.LookupIP |
|
| 242 |
+ |
|
| 243 |
+// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host")
|
|
| 244 |
+// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be |
|
| 245 |
+// resolved to IP addresses for matching. If resolution fails, false is returned. |
|
| 246 |
+func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
|
|
| 247 |
+ if len(cidrs) == 0 {
|
|
| 248 |
+ return false |
|
| 249 |
+ } |
|
| 250 |
+ |
|
| 251 |
+ host, _, err := net.SplitHostPort(urlHost) |
|
| 252 |
+ if err != nil {
|
|
| 253 |
+ // Assume urlHost is a host without port and go on. |
|
| 254 |
+ host = urlHost |
|
| 255 |
+ } |
|
| 256 |
+ |
|
| 257 |
+ var addresses []net.IP |
|
| 258 |
+ if ip := net.ParseIP(host); ip != nil {
|
|
| 259 |
+ // Host is an IP-address. |
|
| 260 |
+ addresses = append(addresses, ip) |
|
| 261 |
+ } else {
|
|
| 262 |
+ // Try to resolve the host's IP-address. |
|
| 263 |
+ addresses, err = lookupIP(host) |
|
| 264 |
+ if err != nil {
|
|
| 265 |
+ // We failed to resolve the host; assume there's no match. |
|
| 266 |
+ return false |
|
| 267 |
+ } |
|
| 268 |
+ } |
|
| 269 |
+ |
|
| 270 |
+ for _, addr := range addresses {
|
|
| 271 |
+ for _, ipnet := range cidrs {
|
|
| 272 |
+ // check if the addr falls in the subnet |
|
| 273 |
+ if (*net.IPNet)(ipnet).Contains(addr) {
|
|
| 274 |
+ return true |
|
| 275 |
+ } |
|
| 276 |
+ } |
|
| 277 |
+ } |
|
| 278 |
+ |
|
| 279 |
+ return false |
|
| 280 |
+} |
|
| 281 |
+ |
|
| 282 |
+// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It |
|
| 283 |
+// returns an error if the given mirrorURL is invalid, or the normalized |
|
| 284 |
+// format for the URL otherwise. |
|
| 285 |
+// |
|
| 286 |
+// It is used by the daemon to validate the daemon configuration. |
|
| 287 |
+func ValidateMirror(mirrorURL string) (string, error) {
|
|
| 288 |
+ // Fast path for missing scheme, as url.Parse splits by ":", which can |
|
| 289 |
+ // cause the hostname to be considered the "scheme" when using "hostname:port". |
|
| 290 |
+ if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" {
|
|
| 291 |
+ return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL)
|
|
| 292 |
+ } |
|
| 293 |
+ uri, err := url.Parse(mirrorURL) |
|
| 294 |
+ if err != nil {
|
|
| 295 |
+ return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL) |
|
| 296 |
+ } |
|
| 297 |
+ if uri.Scheme != "http" && uri.Scheme != "https" {
|
|
| 298 |
+ return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri)
|
|
| 299 |
+ } |
|
| 300 |
+ if uri.RawQuery != "" || uri.Fragment != "" {
|
|
| 301 |
+ return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri)
|
|
| 302 |
+ } |
|
| 303 |
+ if uri.User != nil {
|
|
| 304 |
+ // strip password from output |
|
| 305 |
+ uri.User = url.UserPassword(uri.User.Username(), "xxxxx") |
|
| 306 |
+ return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
|
|
| 307 |
+ } |
|
| 308 |
+ return strings.TrimSuffix(mirrorURL, "/") + "/", nil |
|
| 309 |
+} |
|
| 310 |
+ |
|
| 311 |
+// ValidateIndexName validates an index name. It is used by the daemon to |
|
| 312 |
+// validate the daemon configuration. |
|
| 313 |
+func ValidateIndexName(val string) (string, error) {
|
|
| 314 |
+ val = normalizeIndexName(val) |
|
| 315 |
+ if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
|
|
| 316 |
+ return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
|
|
| 317 |
+ } |
|
| 318 |
+ return val, nil |
|
| 319 |
+} |
|
| 320 |
+ |
|
| 321 |
+func normalizeIndexName(val string) string {
|
|
| 322 |
+ // TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/". |
|
| 323 |
+ // TODO: upstream this to check to reference package |
|
| 324 |
+ if val == "index.docker.io" {
|
|
| 325 |
+ return "docker.io" |
|
| 326 |
+ } |
|
| 327 |
+ return val |
|
| 328 |
+} |
|
| 329 |
+ |
|
| 330 |
+func hasScheme(reposName string) bool {
|
|
| 331 |
+ return strings.Contains(reposName, "://") |
|
| 332 |
+} |
|
| 333 |
+ |
|
| 334 |
+func validateHostPort(s string) error {
|
|
| 335 |
+ // Split host and port, and in case s can not be split, assume host only |
|
| 336 |
+ host, port, err := net.SplitHostPort(s) |
|
| 337 |
+ if err != nil {
|
|
| 338 |
+ host = s |
|
| 339 |
+ port = "" |
|
| 340 |
+ } |
|
| 341 |
+ // If match against the `host:port` pattern fails, |
|
| 342 |
+ // it might be `IPv6:port`, which will be captured by net.ParseIP(host) |
|
| 343 |
+ if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil {
|
|
| 344 |
+ return invalidParamf("invalid host %q", host)
|
|
| 345 |
+ } |
|
| 346 |
+ if port != "" {
|
|
| 347 |
+ v, err := strconv.Atoi(port) |
|
| 348 |
+ if err != nil {
|
|
| 349 |
+ return err |
|
| 350 |
+ } |
|
| 351 |
+ if v < 0 || v > 65535 {
|
|
| 352 |
+ return invalidParamf("invalid port %q", port)
|
|
| 353 |
+ } |
|
| 354 |
+ } |
|
| 355 |
+ return nil |
|
| 356 |
+} |
|
| 357 |
+ |
|
| 358 |
+// newIndexInfo returns IndexInfo configuration from indexName |
|
| 359 |
+func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
|
|
| 360 |
+ indexName = normalizeIndexName(indexName) |
|
| 361 |
+ |
|
| 362 |
+ // Return any configured index info, first. |
|
| 363 |
+ if index, ok := config.IndexConfigs[indexName]; ok {
|
|
| 364 |
+ return index |
|
| 365 |
+ } |
|
| 366 |
+ |
|
| 367 |
+ // Construct a non-configured index info. |
|
| 368 |
+ return ®istry.IndexInfo{
|
|
| 369 |
+ Name: indexName, |
|
| 370 |
+ Mirrors: []string{},
|
|
| 371 |
+ Secure: config.isSecureIndex(indexName), |
|
| 372 |
+ } |
|
| 373 |
+} |
|
| 374 |
+ |
|
| 375 |
+// GetAuthConfigKey special-cases using the full index address of the official |
|
| 376 |
+// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. |
|
| 377 |
+func GetAuthConfigKey(index *registry.IndexInfo) string {
|
|
| 378 |
+ if index.Official {
|
|
| 379 |
+ return IndexServer |
|
| 380 |
+ } |
|
| 381 |
+ return index.Name |
|
| 382 |
+} |
|
| 383 |
+ |
|
| 384 |
+// ParseRepositoryInfo performs the breakdown of a repository name into a |
|
| 385 |
+// [RepositoryInfo], but lacks registry configuration. |
|
| 386 |
+// |
|
| 387 |
+// It is used by the Docker cli to interact with registry-related endpoints. |
|
| 388 |
+func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
|
| 389 |
+ indexName := normalizeIndexName(reference.Domain(reposName)) |
|
| 390 |
+ if indexName == IndexName {
|
|
| 391 |
+ return &RepositoryInfo{
|
|
| 392 |
+ Name: reference.TrimNamed(reposName), |
|
| 393 |
+ Index: ®istry.IndexInfo{
|
|
| 394 |
+ Name: IndexName, |
|
| 395 |
+ Mirrors: []string{},
|
|
| 396 |
+ Secure: true, |
|
| 397 |
+ Official: true, |
|
| 398 |
+ }, |
|
| 399 |
+ }, nil |
|
| 400 |
+ } |
|
| 401 |
+ |
|
| 402 |
+ return &RepositoryInfo{
|
|
| 403 |
+ Name: reference.TrimNamed(reposName), |
|
| 404 |
+ Index: ®istry.IndexInfo{
|
|
| 405 |
+ Name: indexName, |
|
| 406 |
+ Mirrors: []string{},
|
|
| 407 |
+ Secure: !isInsecure(indexName), |
|
| 408 |
+ }, |
|
| 409 |
+ }, nil |
|
| 410 |
+} |
|
| 411 |
+ |
|
| 412 |
+// isInsecure is used to detect whether a registry domain or IP-address is allowed |
|
| 413 |
+// to use an insecure (non-TLS, or self-signed cert) connection according to the |
|
| 414 |
+// defaults, which allows for insecure connections with registries running on a |
|
| 415 |
+// loopback address ("localhost", "::1/128", "127.0.0.0/8").
|
|
| 416 |
+// |
|
| 417 |
+// It is used in situations where we don't have access to the daemon's configuration, |
|
| 418 |
+// for example, when used from the client / CLI. |
|
| 419 |
+func isInsecure(hostNameOrIP string) bool {
|
|
| 420 |
+ // Attempt to strip port if present; this also strips brackets for |
|
| 421 |
+ // IPv6 addresses with a port (e.g. "[::1]:5000"). |
|
| 422 |
+ // |
|
| 423 |
+ // This is best-effort; we'll continue using the address as-is if it fails. |
|
| 424 |
+ if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
|
|
| 425 |
+ hostNameOrIP = host |
|
| 426 |
+ } |
|
| 427 |
+ if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
|
|
| 428 |
+ // Fast path; no need to resolve these, assuming nobody overrides |
|
| 429 |
+ // "localhost" for anything else than a loopback address (sorry, not sorry). |
|
| 430 |
+ return true |
|
| 431 |
+ } |
|
| 432 |
+ |
|
| 433 |
+ var addresses []net.IP |
|
| 434 |
+ if ip := net.ParseIP(hostNameOrIP); ip != nil {
|
|
| 435 |
+ addresses = append(addresses, ip) |
|
| 436 |
+ } else {
|
|
| 437 |
+ // Try to resolve the host's IP-addresses. |
|
| 438 |
+ addrs, _ := lookupIP(hostNameOrIP) |
|
| 439 |
+ addresses = append(addresses, addrs...) |
|
| 440 |
+ } |
|
| 441 |
+ |
|
| 442 |
+ for _, addr := range addresses {
|
|
| 443 |
+ if addr.IsLoopback() {
|
|
| 444 |
+ return true |
|
| 445 |
+ } |
|
| 446 |
+ } |
|
| 447 |
+ return false |
|
| 448 |
+} |
| 0 | 449 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,340 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 6 |
+ "gotest.tools/v3/assert" |
|
| 7 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func TestValidateMirror(t *testing.T) {
|
|
| 11 |
+ tests := []struct {
|
|
| 12 |
+ input string |
|
| 13 |
+ output string |
|
| 14 |
+ expectedErr string |
|
| 15 |
+ }{
|
|
| 16 |
+ // Valid cases |
|
| 17 |
+ {
|
|
| 18 |
+ input: "http://mirror-1.example.com", |
|
| 19 |
+ output: "http://mirror-1.example.com/", |
|
| 20 |
+ }, |
|
| 21 |
+ {
|
|
| 22 |
+ input: "http://mirror-1.example.com/", |
|
| 23 |
+ output: "http://mirror-1.example.com/", |
|
| 24 |
+ }, |
|
| 25 |
+ {
|
|
| 26 |
+ input: "https://mirror-1.example.com", |
|
| 27 |
+ output: "https://mirror-1.example.com/", |
|
| 28 |
+ }, |
|
| 29 |
+ {
|
|
| 30 |
+ input: "https://mirror-1.example.com/", |
|
| 31 |
+ output: "https://mirror-1.example.com/", |
|
| 32 |
+ }, |
|
| 33 |
+ {
|
|
| 34 |
+ input: "http://localhost", |
|
| 35 |
+ output: "http://localhost/", |
|
| 36 |
+ }, |
|
| 37 |
+ {
|
|
| 38 |
+ input: "https://localhost", |
|
| 39 |
+ output: "https://localhost/", |
|
| 40 |
+ }, |
|
| 41 |
+ {
|
|
| 42 |
+ input: "http://localhost:5000", |
|
| 43 |
+ output: "http://localhost:5000/", |
|
| 44 |
+ }, |
|
| 45 |
+ {
|
|
| 46 |
+ input: "https://localhost:5000", |
|
| 47 |
+ output: "https://localhost:5000/", |
|
| 48 |
+ }, |
|
| 49 |
+ {
|
|
| 50 |
+ input: "http://127.0.0.1", |
|
| 51 |
+ output: "http://127.0.0.1/", |
|
| 52 |
+ }, |
|
| 53 |
+ {
|
|
| 54 |
+ input: "https://127.0.0.1", |
|
| 55 |
+ output: "https://127.0.0.1/", |
|
| 56 |
+ }, |
|
| 57 |
+ {
|
|
| 58 |
+ input: "http://127.0.0.1:5000", |
|
| 59 |
+ output: "http://127.0.0.1:5000/", |
|
| 60 |
+ }, |
|
| 61 |
+ {
|
|
| 62 |
+ input: "https://127.0.0.1:5000", |
|
| 63 |
+ output: "https://127.0.0.1:5000/", |
|
| 64 |
+ }, |
|
| 65 |
+ {
|
|
| 66 |
+ input: "http://mirror-1.example.com/v1/", |
|
| 67 |
+ output: "http://mirror-1.example.com/v1/", |
|
| 68 |
+ }, |
|
| 69 |
+ {
|
|
| 70 |
+ input: "https://mirror-1.example.com/v1/", |
|
| 71 |
+ output: "https://mirror-1.example.com/v1/", |
|
| 72 |
+ }, |
|
| 73 |
+ |
|
| 74 |
+ // Invalid cases |
|
| 75 |
+ {
|
|
| 76 |
+ input: "!invalid!://%as%", |
|
| 77 |
+ expectedErr: `invalid mirror: "!invalid!://%as%" is not a valid URI: parse "!invalid!://%as%": first path segment in URL cannot contain colon`, |
|
| 78 |
+ }, |
|
| 79 |
+ {
|
|
| 80 |
+ input: "mirror-1.example.com", |
|
| 81 |
+ expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com": must use either 'https://' or 'http://'`, |
|
| 82 |
+ }, |
|
| 83 |
+ {
|
|
| 84 |
+ input: "mirror-1.example.com:5000", |
|
| 85 |
+ expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com:5000": must use either 'https://' or 'http://'`, |
|
| 86 |
+ }, |
|
| 87 |
+ {
|
|
| 88 |
+ input: "ftp://mirror-1.example.com", |
|
| 89 |
+ expectedErr: `invalid mirror: unsupported scheme "ftp" in "ftp://mirror-1.example.com": must use either 'https://' or 'http://'`, |
|
| 90 |
+ }, |
|
| 91 |
+ {
|
|
| 92 |
+ input: "http://mirror-1.example.com/?q=foo", |
|
| 93 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/?q=foo"`, |
|
| 94 |
+ }, |
|
| 95 |
+ {
|
|
| 96 |
+ input: "http://mirror-1.example.com/v1/?q=foo", |
|
| 97 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo"`, |
|
| 98 |
+ }, |
|
| 99 |
+ {
|
|
| 100 |
+ input: "http://mirror-1.example.com/v1/?q=foo#frag", |
|
| 101 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo#frag"`, |
|
| 102 |
+ }, |
|
| 103 |
+ {
|
|
| 104 |
+ input: "http://mirror-1.example.com?q=foo", |
|
| 105 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com?q=foo"`, |
|
| 106 |
+ }, |
|
| 107 |
+ {
|
|
| 108 |
+ input: "https://mirror-1.example.com#frag", |
|
| 109 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com#frag"`, |
|
| 110 |
+ }, |
|
| 111 |
+ {
|
|
| 112 |
+ input: "https://mirror-1.example.com/#frag", |
|
| 113 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/#frag"`, |
|
| 114 |
+ }, |
|
| 115 |
+ {
|
|
| 116 |
+ input: "http://foo:bar@mirror-1.example.com/", |
|
| 117 |
+ expectedErr: `invalid mirror: username/password not allowed in URI "http://foo:xxxxx@mirror-1.example.com/"`, |
|
| 118 |
+ }, |
|
| 119 |
+ {
|
|
| 120 |
+ input: "https://mirror-1.example.com/v1/#frag", |
|
| 121 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/v1/#frag"`, |
|
| 122 |
+ }, |
|
| 123 |
+ {
|
|
| 124 |
+ input: "https://mirror-1.example.com?q", |
|
| 125 |
+ expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com?q"`, |
|
| 126 |
+ }, |
|
| 127 |
+ } |
|
| 128 |
+ |
|
| 129 |
+ for _, tc := range tests {
|
|
| 130 |
+ t.Run(tc.input, func(t *testing.T) {
|
|
| 131 |
+ out, err := ValidateMirror(tc.input) |
|
| 132 |
+ if tc.expectedErr != "" {
|
|
| 133 |
+ assert.Error(t, err, tc.expectedErr) |
|
| 134 |
+ } else {
|
|
| 135 |
+ assert.NilError(t, err) |
|
| 136 |
+ } |
|
| 137 |
+ assert.Check(t, is.Equal(out, tc.output)) |
|
| 138 |
+ }) |
|
| 139 |
+ } |
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+func TestLoadInsecureRegistries(t *testing.T) {
|
|
| 143 |
+ testCases := []struct {
|
|
| 144 |
+ registries []string |
|
| 145 |
+ index string |
|
| 146 |
+ err string |
|
| 147 |
+ }{
|
|
| 148 |
+ {
|
|
| 149 |
+ registries: []string{"127.0.0.1"},
|
|
| 150 |
+ index: "127.0.0.1", |
|
| 151 |
+ }, |
|
| 152 |
+ {
|
|
| 153 |
+ registries: []string{"127.0.0.1:8080"},
|
|
| 154 |
+ index: "127.0.0.1:8080", |
|
| 155 |
+ }, |
|
| 156 |
+ {
|
|
| 157 |
+ registries: []string{"2001:db8::1"},
|
|
| 158 |
+ index: "2001:db8::1", |
|
| 159 |
+ }, |
|
| 160 |
+ {
|
|
| 161 |
+ registries: []string{"[2001:db8::1]:80"},
|
|
| 162 |
+ index: "[2001:db8::1]:80", |
|
| 163 |
+ }, |
|
| 164 |
+ {
|
|
| 165 |
+ registries: []string{"http://myregistry.example.com"},
|
|
| 166 |
+ index: "myregistry.example.com", |
|
| 167 |
+ }, |
|
| 168 |
+ {
|
|
| 169 |
+ registries: []string{"https://myregistry.example.com"},
|
|
| 170 |
+ index: "myregistry.example.com", |
|
| 171 |
+ }, |
|
| 172 |
+ {
|
|
| 173 |
+ registries: []string{"HTTP://myregistry.example.com"},
|
|
| 174 |
+ index: "myregistry.example.com", |
|
| 175 |
+ }, |
|
| 176 |
+ {
|
|
| 177 |
+ registries: []string{"svn://myregistry.example.com"},
|
|
| 178 |
+ err: "insecure registry svn://myregistry.example.com should not contain '://'", |
|
| 179 |
+ }, |
|
| 180 |
+ {
|
|
| 181 |
+ registries: []string{"-invalid-registry"},
|
|
| 182 |
+ err: "Cannot begin or end with a hyphen", |
|
| 183 |
+ }, |
|
| 184 |
+ {
|
|
| 185 |
+ registries: []string{`mytest-.com`},
|
|
| 186 |
+ err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`, |
|
| 187 |
+ }, |
|
| 188 |
+ {
|
|
| 189 |
+ registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
|
|
| 190 |
+ 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"`, |
|
| 191 |
+ }, |
|
| 192 |
+ {
|
|
| 193 |
+ registries: []string{`myregistry.example.com:500000`},
|
|
| 194 |
+ err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`, |
|
| 195 |
+ }, |
|
| 196 |
+ {
|
|
| 197 |
+ registries: []string{`"myregistry.example.com"`},
|
|
| 198 |
+ err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`, |
|
| 199 |
+ }, |
|
| 200 |
+ {
|
|
| 201 |
+ registries: []string{`"myregistry.example.com:5000"`},
|
|
| 202 |
+ err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`, |
|
| 203 |
+ }, |
|
| 204 |
+ } |
|
| 205 |
+ for _, testCase := range testCases {
|
|
| 206 |
+ config := &serviceConfig{}
|
|
| 207 |
+ err := config.loadInsecureRegistries(testCase.registries) |
|
| 208 |
+ if testCase.err == "" {
|
|
| 209 |
+ if err != nil {
|
|
| 210 |
+ t.Fatalf("expect no error, got '%s'", err)
|
|
| 211 |
+ } |
|
| 212 |
+ match := false |
|
| 213 |
+ for index := range config.IndexConfigs {
|
|
| 214 |
+ if index == testCase.index {
|
|
| 215 |
+ match = true |
|
| 216 |
+ } |
|
| 217 |
+ } |
|
| 218 |
+ if !match {
|
|
| 219 |
+ t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.IndexConfigs)
|
|
| 220 |
+ } |
|
| 221 |
+ } else {
|
|
| 222 |
+ if err == nil {
|
|
| 223 |
+ t.Fatalf("expect error '%s', got no error", testCase.err)
|
|
| 224 |
+ } |
|
| 225 |
+ assert.ErrorContains(t, err, testCase.err) |
|
| 226 |
+ assert.Check(t, cerrdefs.IsInvalidArgument(err)) |
|
| 227 |
+ } |
|
| 228 |
+ } |
|
| 229 |
+} |
|
| 230 |
+ |
|
| 231 |
+func TestNewServiceConfig(t *testing.T) {
|
|
| 232 |
+ tests := []struct {
|
|
| 233 |
+ doc string |
|
| 234 |
+ opts ServiceOptions |
|
| 235 |
+ errStr string |
|
| 236 |
+ }{
|
|
| 237 |
+ {
|
|
| 238 |
+ doc: "empty config", |
|
| 239 |
+ }, |
|
| 240 |
+ {
|
|
| 241 |
+ doc: "invalid mirror", |
|
| 242 |
+ opts: ServiceOptions{
|
|
| 243 |
+ Mirrors: []string{"example.com:5000"},
|
|
| 244 |
+ }, |
|
| 245 |
+ errStr: `invalid mirror: no scheme specified for "example.com:5000": must use either 'https://' or 'http://'`, |
|
| 246 |
+ }, |
|
| 247 |
+ {
|
|
| 248 |
+ doc: "valid mirror", |
|
| 249 |
+ opts: ServiceOptions{
|
|
| 250 |
+ Mirrors: []string{"https://example.com:5000"},
|
|
| 251 |
+ }, |
|
| 252 |
+ }, |
|
| 253 |
+ {
|
|
| 254 |
+ doc: "invalid insecure registry", |
|
| 255 |
+ opts: ServiceOptions{
|
|
| 256 |
+ InsecureRegistries: []string{"[fe80::]/64"},
|
|
| 257 |
+ }, |
|
| 258 |
+ errStr: `insecure registry [fe80::]/64 is not valid: invalid host "[fe80::]/64"`, |
|
| 259 |
+ }, |
|
| 260 |
+ {
|
|
| 261 |
+ doc: "valid insecure registry", |
|
| 262 |
+ opts: ServiceOptions{
|
|
| 263 |
+ InsecureRegistries: []string{"102.10.8.1/24"},
|
|
| 264 |
+ }, |
|
| 265 |
+ }, |
|
| 266 |
+ } |
|
| 267 |
+ |
|
| 268 |
+ for _, tc := range tests {
|
|
| 269 |
+ t.Run(tc.doc, func(t *testing.T) {
|
|
| 270 |
+ _, err := newServiceConfig(tc.opts) |
|
| 271 |
+ if tc.errStr != "" {
|
|
| 272 |
+ assert.Check(t, is.Error(err, tc.errStr)) |
|
| 273 |
+ assert.Check(t, cerrdefs.IsInvalidArgument(err)) |
|
| 274 |
+ } else {
|
|
| 275 |
+ assert.Check(t, err) |
|
| 276 |
+ } |
|
| 277 |
+ }) |
|
| 278 |
+ } |
|
| 279 |
+} |
|
| 280 |
+ |
|
| 281 |
+func TestValidateIndexName(t *testing.T) {
|
|
| 282 |
+ valid := []struct {
|
|
| 283 |
+ index string |
|
| 284 |
+ expect string |
|
| 285 |
+ }{
|
|
| 286 |
+ {
|
|
| 287 |
+ index: "index.docker.io", |
|
| 288 |
+ expect: "docker.io", |
|
| 289 |
+ }, |
|
| 290 |
+ {
|
|
| 291 |
+ index: "example.com", |
|
| 292 |
+ expect: "example.com", |
|
| 293 |
+ }, |
|
| 294 |
+ {
|
|
| 295 |
+ index: "127.0.0.1:8080", |
|
| 296 |
+ expect: "127.0.0.1:8080", |
|
| 297 |
+ }, |
|
| 298 |
+ {
|
|
| 299 |
+ index: "mytest-1.com", |
|
| 300 |
+ expect: "mytest-1.com", |
|
| 301 |
+ }, |
|
| 302 |
+ {
|
|
| 303 |
+ index: "mirror-1.example.com/v1/?q=foo", |
|
| 304 |
+ expect: "mirror-1.example.com/v1/?q=foo", |
|
| 305 |
+ }, |
|
| 306 |
+ } |
|
| 307 |
+ |
|
| 308 |
+ for _, testCase := range valid {
|
|
| 309 |
+ result, err := ValidateIndexName(testCase.index) |
|
| 310 |
+ if assert.Check(t, err) {
|
|
| 311 |
+ assert.Check(t, is.Equal(testCase.expect, result)) |
|
| 312 |
+ } |
|
| 313 |
+ } |
|
| 314 |
+} |
|
| 315 |
+ |
|
| 316 |
+func TestValidateIndexNameWithError(t *testing.T) {
|
|
| 317 |
+ invalid := []struct {
|
|
| 318 |
+ index string |
|
| 319 |
+ err string |
|
| 320 |
+ }{
|
|
| 321 |
+ {
|
|
| 322 |
+ index: "docker.io-", |
|
| 323 |
+ err: "invalid index name (docker.io-). Cannot begin or end with a hyphen", |
|
| 324 |
+ }, |
|
| 325 |
+ {
|
|
| 326 |
+ index: "-example.com", |
|
| 327 |
+ err: "invalid index name (-example.com). Cannot begin or end with a hyphen", |
|
| 328 |
+ }, |
|
| 329 |
+ {
|
|
| 330 |
+ index: "mirror-1.example.com/v1/?q=foo-", |
|
| 331 |
+ err: "invalid index name (mirror-1.example.com/v1/?q=foo-). Cannot begin or end with a hyphen", |
|
| 332 |
+ }, |
|
| 333 |
+ } |
|
| 334 |
+ for _, testCase := range invalid {
|
|
| 335 |
+ _, err := ValidateIndexName(testCase.index) |
|
| 336 |
+ assert.Check(t, is.Error(err, testCase.err)) |
|
| 337 |
+ assert.Check(t, cerrdefs.IsInvalidArgument(err)) |
|
| 338 |
+ } |
|
| 339 |
+} |
| 0 | 340 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,67 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "net/url" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/distribution/registry/api/errcode" |
|
| 6 |
+ "github.com/pkg/errors" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func translateV2AuthError(err error) error {
|
|
| 10 |
+ var e *url.Error |
|
| 11 |
+ if errors.As(err, &e) {
|
|
| 12 |
+ var e2 errcode.Error |
|
| 13 |
+ if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) {
|
|
| 14 |
+ return unauthorizedErr{err}
|
|
| 15 |
+ } |
|
| 16 |
+ } |
|
| 17 |
+ return err |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+func invalidParam(err error) error {
|
|
| 21 |
+ return invalidParameterErr{err}
|
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func invalidParamf(format string, args ...interface{}) error {
|
|
| 25 |
+ return invalidParameterErr{errors.Errorf(format, args...)}
|
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func invalidParamWrapf(err error, format string, args ...interface{}) error {
|
|
| 29 |
+ return invalidParameterErr{errors.Wrapf(err, format, args...)}
|
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+type unauthorizedErr struct{ error }
|
|
| 33 |
+ |
|
| 34 |
+func (unauthorizedErr) Unauthorized() {}
|
|
| 35 |
+ |
|
| 36 |
+func (e unauthorizedErr) Cause() error {
|
|
| 37 |
+ return e.error |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+func (e unauthorizedErr) Unwrap() error {
|
|
| 41 |
+ return e.error |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+type invalidParameterErr struct{ error }
|
|
| 45 |
+ |
|
| 46 |
+func (invalidParameterErr) InvalidParameter() {}
|
|
| 47 |
+ |
|
| 48 |
+func (e invalidParameterErr) Unwrap() error {
|
|
| 49 |
+ return e.error |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+type systemErr struct{ error }
|
|
| 53 |
+ |
|
| 54 |
+func (systemErr) System() {}
|
|
| 55 |
+ |
|
| 56 |
+func (e systemErr) Unwrap() error {
|
|
| 57 |
+ return e.error |
|
| 58 |
+} |
|
| 59 |
+ |
|
| 60 |
+type errUnknown struct{ error }
|
|
| 61 |
+ |
|
| 62 |
+func (errUnknown) Unknown() {}
|
|
| 63 |
+ |
|
| 64 |
+func (e errUnknown) Unwrap() error {
|
|
| 65 |
+ return e.error |
|
| 66 |
+} |
| 0 | 67 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,155 @@ |
| 0 |
+// Package registry contains client primitives to interact with a remote Docker registry. |
|
| 1 |
+package registry |
|
| 2 |
+ |
|
| 3 |
+import ( |
|
| 4 |
+ "context" |
|
| 5 |
+ "crypto/tls" |
|
| 6 |
+ "net" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "os" |
|
| 9 |
+ "path/filepath" |
|
| 10 |
+ "runtime" |
|
| 11 |
+ "strings" |
|
| 12 |
+ "time" |
|
| 13 |
+ |
|
| 14 |
+ "github.com/containerd/log" |
|
| 15 |
+ "github.com/docker/distribution/registry/client/transport" |
|
| 16 |
+ "github.com/docker/go-connections/tlsconfig" |
|
| 17 |
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
|
| 18 |
+) |
|
| 19 |
+ |
|
| 20 |
+// hostCertsDir returns the config directory for a specific host. |
|
| 21 |
+func hostCertsDir(hostnameAndPort string) string {
|
|
| 22 |
+ if runtime.GOOS == "windows" {
|
|
| 23 |
+ // Ensure that a directory name is valid; hostnameAndPort may contain |
|
| 24 |
+ // a colon (:) if a port is included, and Windows does not allow colons |
|
| 25 |
+ // in directory names. |
|
| 26 |
+ hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", "")) |
|
| 27 |
+ } |
|
| 28 |
+ return filepath.Join(CertsDir(), hostnameAndPort) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// newTLSConfig constructs a client TLS configuration based on server defaults |
|
| 32 |
+func newTLSConfig(ctx context.Context, hostname string, isSecure bool) (*tls.Config, error) {
|
|
| 33 |
+ // PreferredServerCipherSuites should have no effect |
|
| 34 |
+ tlsConfig := tlsconfig.ServerDefault() |
|
| 35 |
+ tlsConfig.InsecureSkipVerify = !isSecure |
|
| 36 |
+ |
|
| 37 |
+ if isSecure {
|
|
| 38 |
+ hostDir := hostCertsDir(hostname) |
|
| 39 |
+ log.G(ctx).Debugf("hostDir: %s", hostDir)
|
|
| 40 |
+ if err := loadTLSConfig(ctx, hostDir, tlsConfig); err != nil {
|
|
| 41 |
+ return nil, err |
|
| 42 |
+ } |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ return tlsConfig, nil |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+func hasFile(files []os.DirEntry, name string) bool {
|
|
| 49 |
+ for _, f := range files {
|
|
| 50 |
+ if f.Name() == name {
|
|
| 51 |
+ return true |
|
| 52 |
+ } |
|
| 53 |
+ } |
|
| 54 |
+ return false |
|
| 55 |
+} |
|
| 56 |
+ |
|
| 57 |
+// ReadCertsDirectory reads the directory for TLS certificates |
|
| 58 |
+// including roots and certificate pairs and updates the |
|
| 59 |
+// provided TLS configuration. |
|
| 60 |
+func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
|
|
| 61 |
+ return loadTLSConfig(context.TODO(), directory, tlsConfig) |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// loadTLSConfig reads the directory for TLS certificates including roots and |
|
| 65 |
+// certificate pairs, and updates the provided TLS configuration. |
|
| 66 |
+func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error {
|
|
| 67 |
+ fs, err := os.ReadDir(directory) |
|
| 68 |
+ if err != nil {
|
|
| 69 |
+ if os.IsNotExist(err) {
|
|
| 70 |
+ return nil |
|
| 71 |
+ } |
|
| 72 |
+ return invalidParam(err) |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ for _, f := range fs {
|
|
| 76 |
+ if ctx.Err() != nil {
|
|
| 77 |
+ return ctx.Err() |
|
| 78 |
+ } |
|
| 79 |
+ switch filepath.Ext(f.Name()) {
|
|
| 80 |
+ case ".crt": |
|
| 81 |
+ if tlsConfig.RootCAs == nil {
|
|
| 82 |
+ systemPool, err := tlsconfig.SystemCertPool() |
|
| 83 |
+ if err != nil {
|
|
| 84 |
+ return invalidParamWrapf(err, "unable to get system cert pool") |
|
| 85 |
+ } |
|
| 86 |
+ tlsConfig.RootCAs = systemPool |
|
| 87 |
+ } |
|
| 88 |
+ fileName := filepath.Join(directory, f.Name()) |
|
| 89 |
+ log.G(ctx).Debugf("crt: %s", fileName)
|
|
| 90 |
+ data, err := os.ReadFile(fileName) |
|
| 91 |
+ if err != nil {
|
|
| 92 |
+ return err |
|
| 93 |
+ } |
|
| 94 |
+ tlsConfig.RootCAs.AppendCertsFromPEM(data) |
|
| 95 |
+ case ".cert": |
|
| 96 |
+ certName := f.Name() |
|
| 97 |
+ keyName := certName[:len(certName)-5] + ".key" |
|
| 98 |
+ log.G(ctx).Debugf("cert: %s", filepath.Join(directory, certName))
|
|
| 99 |
+ if !hasFile(fs, keyName) {
|
|
| 100 |
+ return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
|
|
| 101 |
+ } |
|
| 102 |
+ cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) |
|
| 103 |
+ if err != nil {
|
|
| 104 |
+ return err |
|
| 105 |
+ } |
|
| 106 |
+ tlsConfig.Certificates = append(tlsConfig.Certificates, cert) |
|
| 107 |
+ case ".key": |
|
| 108 |
+ keyName := f.Name() |
|
| 109 |
+ certName := keyName[:len(keyName)-4] + ".cert" |
|
| 110 |
+ log.G(ctx).Debugf("key: %s", filepath.Join(directory, keyName))
|
|
| 111 |
+ if !hasFile(fs, certName) {
|
|
| 112 |
+ return invalidParamf("missing client certificate %s for key %s", certName, keyName)
|
|
| 113 |
+ } |
|
| 114 |
+ } |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ return nil |
|
| 118 |
+} |
|
| 119 |
+ |
|
| 120 |
+// Headers returns request modifiers with a User-Agent and metaHeaders |
|
| 121 |
+func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier {
|
|
| 122 |
+ modifiers := []transport.RequestModifier{}
|
|
| 123 |
+ if userAgent != "" {
|
|
| 124 |
+ modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{
|
|
| 125 |
+ "User-Agent": []string{userAgent},
|
|
| 126 |
+ })) |
|
| 127 |
+ } |
|
| 128 |
+ if metaHeaders != nil {
|
|
| 129 |
+ modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) |
|
| 130 |
+ } |
|
| 131 |
+ return modifiers |
|
| 132 |
+} |
|
| 133 |
+ |
|
| 134 |
+// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the |
|
| 135 |
+// default TLS configuration. |
|
| 136 |
+func newTransport(tlsConfig *tls.Config) http.RoundTripper {
|
|
| 137 |
+ if tlsConfig == nil {
|
|
| 138 |
+ tlsConfig = tlsconfig.ServerDefault() |
|
| 139 |
+ } |
|
| 140 |
+ |
|
| 141 |
+ return otelhttp.NewTransport( |
|
| 142 |
+ &http.Transport{
|
|
| 143 |
+ Proxy: http.ProxyFromEnvironment, |
|
| 144 |
+ DialContext: (&net.Dialer{
|
|
| 145 |
+ Timeout: 30 * time.Second, |
|
| 146 |
+ KeepAlive: 30 * time.Second, |
|
| 147 |
+ }).DialContext, |
|
| 148 |
+ TLSHandshakeTimeout: 10 * time.Second, |
|
| 149 |
+ TLSClientConfig: tlsConfig, |
|
| 150 |
+ // TODO(dmcgowan): Call close idle connections when complete and use keep alive |
|
| 151 |
+ DisableKeepAlives: true, |
|
| 152 |
+ }, |
|
| 153 |
+ ) |
|
| 154 |
+} |
| 0 | 155 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,120 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "io" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ "net/http/httptest" |
|
| 8 |
+ "testing" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/containerd/log" |
|
| 11 |
+ "github.com/moby/moby/api/types/registry" |
|
| 12 |
+ "gotest.tools/v3/assert" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+var ( |
|
| 16 |
+ testHTTPServer *httptest.Server |
|
| 17 |
+ testHTTPSServer *httptest.Server |
|
| 18 |
+) |
|
| 19 |
+ |
|
| 20 |
+func init() {
|
|
| 21 |
+ r := http.NewServeMux() |
|
| 22 |
+ |
|
| 23 |
+ // /v1/ |
|
| 24 |
+ r.HandleFunc("/v1/_ping", handlerGetPing)
|
|
| 25 |
+ r.HandleFunc("/v1/search", handlerSearch)
|
|
| 26 |
+ |
|
| 27 |
+ // /v2/ |
|
| 28 |
+ r.HandleFunc("/v2/version", handlerGetPing)
|
|
| 29 |
+ |
|
| 30 |
+ testHTTPServer = httptest.NewServer(handlerAccessLog(r)) |
|
| 31 |
+ testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r)) |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+func handlerAccessLog(handler http.Handler) http.Handler {
|
|
| 35 |
+ logHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
| 36 |
+ log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL) |
|
| 37 |
+ handler.ServeHTTP(w, r) |
|
| 38 |
+ } |
|
| 39 |
+ return http.HandlerFunc(logHandler) |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func makeURL(req string) string {
|
|
| 43 |
+ return testHTTPServer.URL + req |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+func makeHTTPSURL(req string) string {
|
|
| 47 |
+ return testHTTPSServer.URL + req |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+func makeIndex(req string) *registry.IndexInfo {
|
|
| 51 |
+ return ®istry.IndexInfo{
|
|
| 52 |
+ Name: makeURL(req), |
|
| 53 |
+ } |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+func makeHTTPSIndex(req string) *registry.IndexInfo {
|
|
| 57 |
+ return ®istry.IndexInfo{
|
|
| 58 |
+ Name: makeHTTPSURL(req), |
|
| 59 |
+ } |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+func makePublicIndex() *registry.IndexInfo {
|
|
| 63 |
+ return ®istry.IndexInfo{
|
|
| 64 |
+ Name: IndexServer, |
|
| 65 |
+ Secure: true, |
|
| 66 |
+ Official: true, |
|
| 67 |
+ } |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func writeHeaders(w http.ResponseWriter) {
|
|
| 71 |
+ h := w.Header() |
|
| 72 |
+ h.Add("Server", "docker-tests/mock")
|
|
| 73 |
+ h.Add("Expires", "-1")
|
|
| 74 |
+ h.Add("Content-Type", "application/json")
|
|
| 75 |
+ h.Add("Pragma", "no-cache")
|
|
| 76 |
+ h.Add("Cache-Control", "no-cache")
|
|
| 77 |
+} |
|
| 78 |
+ |
|
| 79 |
+func writeResponse(w http.ResponseWriter, message interface{}, code int) {
|
|
| 80 |
+ writeHeaders(w) |
|
| 81 |
+ w.WriteHeader(code) |
|
| 82 |
+ body, err := json.Marshal(message) |
|
| 83 |
+ if err != nil {
|
|
| 84 |
+ _, _ = io.WriteString(w, err.Error()) |
|
| 85 |
+ return |
|
| 86 |
+ } |
|
| 87 |
+ _, _ = w.Write(body) |
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+func handlerGetPing(w http.ResponseWriter, r *http.Request) {
|
|
| 91 |
+ if r.Method != http.MethodGet {
|
|
| 92 |
+ writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) |
|
| 93 |
+ return |
|
| 94 |
+ } |
|
| 95 |
+ writeResponse(w, true, http.StatusOK) |
|
| 96 |
+} |
|
| 97 |
+ |
|
| 98 |
+func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
|
| 99 |
+ if r.Method != http.MethodGet {
|
|
| 100 |
+ writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) |
|
| 101 |
+ return |
|
| 102 |
+ } |
|
| 103 |
+ result := ®istry.SearchResults{
|
|
| 104 |
+ Query: "fakequery", |
|
| 105 |
+ NumResults: 1, |
|
| 106 |
+ Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
|
| 107 |
+ } |
|
| 108 |
+ writeResponse(w, result, http.StatusOK) |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+func TestPing(t *testing.T) {
|
|
| 112 |
+ res, err := http.Get(makeURL("/v1/_ping"))
|
|
| 113 |
+ if err != nil {
|
|
| 114 |
+ t.Fatal(err) |
|
| 115 |
+ } |
|
| 116 |
+ assert.Equal(t, res.StatusCode, http.StatusOK, "") |
|
| 117 |
+ assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
|
|
| 118 |
+ _ = res.Body.Close() |
|
| 119 |
+} |
| 0 | 120 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,637 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "errors" |
|
| 4 |
+ "net" |
|
| 5 |
+ "testing" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/distribution/reference" |
|
| 8 |
+ "github.com/moby/moby/api/types/registry" |
|
| 9 |
+ "gotest.tools/v3/assert" |
|
| 10 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// overrideLookupIP overrides net.LookupIP for testing. |
|
| 14 |
+func overrideLookupIP(t *testing.T) {
|
|
| 15 |
+ t.Helper() |
|
| 16 |
+ restoreLookup := lookupIP |
|
| 17 |
+ |
|
| 18 |
+ // override net.LookupIP |
|
| 19 |
+ lookupIP = func(host string) ([]net.IP, error) {
|
|
| 20 |
+ mockHosts := map[string][]net.IP{
|
|
| 21 |
+ "": {net.ParseIP("0.0.0.0")},
|
|
| 22 |
+ "localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
|
| 23 |
+ "example.com": {net.ParseIP("42.42.42.42")},
|
|
| 24 |
+ "other.com": {net.ParseIP("43.43.43.43")},
|
|
| 25 |
+ } |
|
| 26 |
+ if addrs, ok := mockHosts[host]; ok {
|
|
| 27 |
+ return addrs, nil |
|
| 28 |
+ } |
|
| 29 |
+ return nil, errors.New("lookup: no such host")
|
|
| 30 |
+ } |
|
| 31 |
+ t.Cleanup(func() {
|
|
| 32 |
+ lookupIP = restoreLookup |
|
| 33 |
+ }) |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+func TestParseRepositoryInfo(t *testing.T) {
|
|
| 37 |
+ type staticRepositoryInfo struct {
|
|
| 38 |
+ Index *registry.IndexInfo |
|
| 39 |
+ RemoteName string |
|
| 40 |
+ CanonicalName string |
|
| 41 |
+ LocalName string |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ tests := map[string]staticRepositoryInfo{
|
|
| 45 |
+ "fooo/bar": {
|
|
| 46 |
+ Index: ®istry.IndexInfo{
|
|
| 47 |
+ Name: IndexName, |
|
| 48 |
+ Mirrors: []string{},
|
|
| 49 |
+ Official: true, |
|
| 50 |
+ Secure: true, |
|
| 51 |
+ }, |
|
| 52 |
+ RemoteName: "fooo/bar", |
|
| 53 |
+ LocalName: "fooo/bar", |
|
| 54 |
+ CanonicalName: "docker.io/fooo/bar", |
|
| 55 |
+ }, |
|
| 56 |
+ "library/ubuntu": {
|
|
| 57 |
+ Index: ®istry.IndexInfo{
|
|
| 58 |
+ Name: IndexName, |
|
| 59 |
+ Mirrors: []string{},
|
|
| 60 |
+ Official: true, |
|
| 61 |
+ Secure: true, |
|
| 62 |
+ }, |
|
| 63 |
+ RemoteName: "library/ubuntu", |
|
| 64 |
+ LocalName: "ubuntu", |
|
| 65 |
+ CanonicalName: "docker.io/library/ubuntu", |
|
| 66 |
+ }, |
|
| 67 |
+ "nonlibrary/ubuntu": {
|
|
| 68 |
+ Index: ®istry.IndexInfo{
|
|
| 69 |
+ Name: IndexName, |
|
| 70 |
+ Mirrors: []string{},
|
|
| 71 |
+ Official: true, |
|
| 72 |
+ Secure: true, |
|
| 73 |
+ }, |
|
| 74 |
+ RemoteName: "nonlibrary/ubuntu", |
|
| 75 |
+ LocalName: "nonlibrary/ubuntu", |
|
| 76 |
+ CanonicalName: "docker.io/nonlibrary/ubuntu", |
|
| 77 |
+ }, |
|
| 78 |
+ "ubuntu": {
|
|
| 79 |
+ Index: ®istry.IndexInfo{
|
|
| 80 |
+ Name: IndexName, |
|
| 81 |
+ Mirrors: []string{},
|
|
| 82 |
+ Official: true, |
|
| 83 |
+ Secure: true, |
|
| 84 |
+ }, |
|
| 85 |
+ RemoteName: "library/ubuntu", |
|
| 86 |
+ LocalName: "ubuntu", |
|
| 87 |
+ CanonicalName: "docker.io/library/ubuntu", |
|
| 88 |
+ }, |
|
| 89 |
+ "other/library": {
|
|
| 90 |
+ Index: ®istry.IndexInfo{
|
|
| 91 |
+ Name: IndexName, |
|
| 92 |
+ Mirrors: []string{},
|
|
| 93 |
+ Official: true, |
|
| 94 |
+ Secure: true, |
|
| 95 |
+ }, |
|
| 96 |
+ RemoteName: "other/library", |
|
| 97 |
+ LocalName: "other/library", |
|
| 98 |
+ CanonicalName: "docker.io/other/library", |
|
| 99 |
+ }, |
|
| 100 |
+ "127.0.0.1:8000/private/moonbase": {
|
|
| 101 |
+ Index: ®istry.IndexInfo{
|
|
| 102 |
+ Name: "127.0.0.1:8000", |
|
| 103 |
+ Mirrors: []string{},
|
|
| 104 |
+ Official: false, |
|
| 105 |
+ Secure: false, |
|
| 106 |
+ }, |
|
| 107 |
+ RemoteName: "private/moonbase", |
|
| 108 |
+ LocalName: "127.0.0.1:8000/private/moonbase", |
|
| 109 |
+ CanonicalName: "127.0.0.1:8000/private/moonbase", |
|
| 110 |
+ }, |
|
| 111 |
+ "127.0.0.1:8000/privatebase": {
|
|
| 112 |
+ Index: ®istry.IndexInfo{
|
|
| 113 |
+ Name: "127.0.0.1:8000", |
|
| 114 |
+ Mirrors: []string{},
|
|
| 115 |
+ Official: false, |
|
| 116 |
+ Secure: false, |
|
| 117 |
+ }, |
|
| 118 |
+ RemoteName: "privatebase", |
|
| 119 |
+ LocalName: "127.0.0.1:8000/privatebase", |
|
| 120 |
+ CanonicalName: "127.0.0.1:8000/privatebase", |
|
| 121 |
+ }, |
|
| 122 |
+ "[::1]:8000/private/moonbase": {
|
|
| 123 |
+ Index: ®istry.IndexInfo{
|
|
| 124 |
+ Name: "[::1]:8000", |
|
| 125 |
+ Mirrors: []string{},
|
|
| 126 |
+ Official: false, |
|
| 127 |
+ Secure: false, |
|
| 128 |
+ }, |
|
| 129 |
+ RemoteName: "private/moonbase", |
|
| 130 |
+ LocalName: "[::1]:8000/private/moonbase", |
|
| 131 |
+ CanonicalName: "[::1]:8000/private/moonbase", |
|
| 132 |
+ }, |
|
| 133 |
+ "[::1]:8000/privatebase": {
|
|
| 134 |
+ Index: ®istry.IndexInfo{
|
|
| 135 |
+ Name: "[::1]:8000", |
|
| 136 |
+ Mirrors: []string{},
|
|
| 137 |
+ Official: false, |
|
| 138 |
+ Secure: false, |
|
| 139 |
+ }, |
|
| 140 |
+ RemoteName: "privatebase", |
|
| 141 |
+ LocalName: "[::1]:8000/privatebase", |
|
| 142 |
+ CanonicalName: "[::1]:8000/privatebase", |
|
| 143 |
+ }, |
|
| 144 |
+ // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 145 |
+ // hence not marked "insecure". |
|
| 146 |
+ "[::2]:8000/private/moonbase": {
|
|
| 147 |
+ Index: ®istry.IndexInfo{
|
|
| 148 |
+ Name: "[::2]:8000", |
|
| 149 |
+ Mirrors: []string{},
|
|
| 150 |
+ Official: false, |
|
| 151 |
+ Secure: true, |
|
| 152 |
+ }, |
|
| 153 |
+ RemoteName: "private/moonbase", |
|
| 154 |
+ LocalName: "[::2]:8000/private/moonbase", |
|
| 155 |
+ CanonicalName: "[::2]:8000/private/moonbase", |
|
| 156 |
+ }, |
|
| 157 |
+ // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 158 |
+ // hence not marked "insecure". |
|
| 159 |
+ "[::2]:8000/privatebase": {
|
|
| 160 |
+ Index: ®istry.IndexInfo{
|
|
| 161 |
+ Name: "[::2]:8000", |
|
| 162 |
+ Mirrors: []string{},
|
|
| 163 |
+ Official: false, |
|
| 164 |
+ Secure: true, |
|
| 165 |
+ }, |
|
| 166 |
+ RemoteName: "privatebase", |
|
| 167 |
+ LocalName: "[::2]:8000/privatebase", |
|
| 168 |
+ CanonicalName: "[::2]:8000/privatebase", |
|
| 169 |
+ }, |
|
| 170 |
+ "localhost:8000/private/moonbase": {
|
|
| 171 |
+ Index: ®istry.IndexInfo{
|
|
| 172 |
+ Name: "localhost:8000", |
|
| 173 |
+ Mirrors: []string{},
|
|
| 174 |
+ Official: false, |
|
| 175 |
+ Secure: false, |
|
| 176 |
+ }, |
|
| 177 |
+ RemoteName: "private/moonbase", |
|
| 178 |
+ LocalName: "localhost:8000/private/moonbase", |
|
| 179 |
+ CanonicalName: "localhost:8000/private/moonbase", |
|
| 180 |
+ }, |
|
| 181 |
+ "localhost:8000/privatebase": {
|
|
| 182 |
+ Index: ®istry.IndexInfo{
|
|
| 183 |
+ Name: "localhost:8000", |
|
| 184 |
+ Mirrors: []string{},
|
|
| 185 |
+ Official: false, |
|
| 186 |
+ Secure: false, |
|
| 187 |
+ }, |
|
| 188 |
+ RemoteName: "privatebase", |
|
| 189 |
+ LocalName: "localhost:8000/privatebase", |
|
| 190 |
+ CanonicalName: "localhost:8000/privatebase", |
|
| 191 |
+ }, |
|
| 192 |
+ "example.com/private/moonbase": {
|
|
| 193 |
+ Index: ®istry.IndexInfo{
|
|
| 194 |
+ Name: "example.com", |
|
| 195 |
+ Mirrors: []string{},
|
|
| 196 |
+ Official: false, |
|
| 197 |
+ Secure: true, |
|
| 198 |
+ }, |
|
| 199 |
+ RemoteName: "private/moonbase", |
|
| 200 |
+ LocalName: "example.com/private/moonbase", |
|
| 201 |
+ CanonicalName: "example.com/private/moonbase", |
|
| 202 |
+ }, |
|
| 203 |
+ "example.com/privatebase": {
|
|
| 204 |
+ Index: ®istry.IndexInfo{
|
|
| 205 |
+ Name: "example.com", |
|
| 206 |
+ Mirrors: []string{},
|
|
| 207 |
+ Official: false, |
|
| 208 |
+ Secure: true, |
|
| 209 |
+ }, |
|
| 210 |
+ RemoteName: "privatebase", |
|
| 211 |
+ LocalName: "example.com/privatebase", |
|
| 212 |
+ CanonicalName: "example.com/privatebase", |
|
| 213 |
+ }, |
|
| 214 |
+ "example.com:8000/private/moonbase": {
|
|
| 215 |
+ Index: ®istry.IndexInfo{
|
|
| 216 |
+ Name: "example.com:8000", |
|
| 217 |
+ Mirrors: []string{},
|
|
| 218 |
+ Official: false, |
|
| 219 |
+ Secure: true, |
|
| 220 |
+ }, |
|
| 221 |
+ RemoteName: "private/moonbase", |
|
| 222 |
+ LocalName: "example.com:8000/private/moonbase", |
|
| 223 |
+ CanonicalName: "example.com:8000/private/moonbase", |
|
| 224 |
+ }, |
|
| 225 |
+ "example.com:8000/privatebase": {
|
|
| 226 |
+ Index: ®istry.IndexInfo{
|
|
| 227 |
+ Name: "example.com:8000", |
|
| 228 |
+ Mirrors: []string{},
|
|
| 229 |
+ Official: false, |
|
| 230 |
+ Secure: true, |
|
| 231 |
+ }, |
|
| 232 |
+ RemoteName: "privatebase", |
|
| 233 |
+ LocalName: "example.com:8000/privatebase", |
|
| 234 |
+ CanonicalName: "example.com:8000/privatebase", |
|
| 235 |
+ }, |
|
| 236 |
+ "localhost/private/moonbase": {
|
|
| 237 |
+ Index: ®istry.IndexInfo{
|
|
| 238 |
+ Name: "localhost", |
|
| 239 |
+ Mirrors: []string{},
|
|
| 240 |
+ Official: false, |
|
| 241 |
+ Secure: false, |
|
| 242 |
+ }, |
|
| 243 |
+ RemoteName: "private/moonbase", |
|
| 244 |
+ LocalName: "localhost/private/moonbase", |
|
| 245 |
+ CanonicalName: "localhost/private/moonbase", |
|
| 246 |
+ }, |
|
| 247 |
+ "localhost/privatebase": {
|
|
| 248 |
+ Index: ®istry.IndexInfo{
|
|
| 249 |
+ Name: "localhost", |
|
| 250 |
+ Mirrors: []string{},
|
|
| 251 |
+ Official: false, |
|
| 252 |
+ Secure: false, |
|
| 253 |
+ }, |
|
| 254 |
+ RemoteName: "privatebase", |
|
| 255 |
+ LocalName: "localhost/privatebase", |
|
| 256 |
+ CanonicalName: "localhost/privatebase", |
|
| 257 |
+ }, |
|
| 258 |
+ IndexName + "/public/moonbase": {
|
|
| 259 |
+ Index: ®istry.IndexInfo{
|
|
| 260 |
+ Name: IndexName, |
|
| 261 |
+ Mirrors: []string{},
|
|
| 262 |
+ Official: true, |
|
| 263 |
+ Secure: true, |
|
| 264 |
+ }, |
|
| 265 |
+ RemoteName: "public/moonbase", |
|
| 266 |
+ LocalName: "public/moonbase", |
|
| 267 |
+ CanonicalName: "docker.io/public/moonbase", |
|
| 268 |
+ }, |
|
| 269 |
+ "index." + IndexName + "/public/moonbase": {
|
|
| 270 |
+ Index: ®istry.IndexInfo{
|
|
| 271 |
+ Name: IndexName, |
|
| 272 |
+ Mirrors: []string{},
|
|
| 273 |
+ Official: true, |
|
| 274 |
+ Secure: true, |
|
| 275 |
+ }, |
|
| 276 |
+ RemoteName: "public/moonbase", |
|
| 277 |
+ LocalName: "public/moonbase", |
|
| 278 |
+ CanonicalName: "docker.io/public/moonbase", |
|
| 279 |
+ }, |
|
| 280 |
+ "ubuntu-12.04-base": {
|
|
| 281 |
+ Index: ®istry.IndexInfo{
|
|
| 282 |
+ Name: IndexName, |
|
| 283 |
+ Mirrors: []string{},
|
|
| 284 |
+ Official: true, |
|
| 285 |
+ Secure: true, |
|
| 286 |
+ }, |
|
| 287 |
+ RemoteName: "library/ubuntu-12.04-base", |
|
| 288 |
+ LocalName: "ubuntu-12.04-base", |
|
| 289 |
+ CanonicalName: "docker.io/library/ubuntu-12.04-base", |
|
| 290 |
+ }, |
|
| 291 |
+ IndexName + "/ubuntu-12.04-base": {
|
|
| 292 |
+ Index: ®istry.IndexInfo{
|
|
| 293 |
+ Name: IndexName, |
|
| 294 |
+ Mirrors: []string{},
|
|
| 295 |
+ Official: true, |
|
| 296 |
+ Secure: true, |
|
| 297 |
+ }, |
|
| 298 |
+ RemoteName: "library/ubuntu-12.04-base", |
|
| 299 |
+ LocalName: "ubuntu-12.04-base", |
|
| 300 |
+ CanonicalName: "docker.io/library/ubuntu-12.04-base", |
|
| 301 |
+ }, |
|
| 302 |
+ "index." + IndexName + "/ubuntu-12.04-base": {
|
|
| 303 |
+ Index: ®istry.IndexInfo{
|
|
| 304 |
+ Name: IndexName, |
|
| 305 |
+ Mirrors: []string{},
|
|
| 306 |
+ Official: true, |
|
| 307 |
+ Secure: true, |
|
| 308 |
+ }, |
|
| 309 |
+ RemoteName: "library/ubuntu-12.04-base", |
|
| 310 |
+ LocalName: "ubuntu-12.04-base", |
|
| 311 |
+ CanonicalName: "docker.io/library/ubuntu-12.04-base", |
|
| 312 |
+ }, |
|
| 313 |
+ } |
|
| 314 |
+ |
|
| 315 |
+ for reposName, expected := range tests {
|
|
| 316 |
+ t.Run(reposName, func(t *testing.T) {
|
|
| 317 |
+ named, err := reference.ParseNormalizedNamed(reposName) |
|
| 318 |
+ assert.NilError(t, err) |
|
| 319 |
+ |
|
| 320 |
+ repoInfo, err := ParseRepositoryInfo(named) |
|
| 321 |
+ assert.NilError(t, err) |
|
| 322 |
+ |
|
| 323 |
+ assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index)) |
|
| 324 |
+ assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName)) |
|
| 325 |
+ assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName)) |
|
| 326 |
+ assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName)) |
|
| 327 |
+ }) |
|
| 328 |
+ } |
|
| 329 |
+} |
|
| 330 |
+ |
|
| 331 |
+func TestNewIndexInfo(t *testing.T) {
|
|
| 332 |
+ overrideLookupIP(t) |
|
| 333 |
+ |
|
| 334 |
+ // ipv6Loopback is the CIDR for the IPv6 loopback address ("::1"); "::1/128"
|
|
| 335 |
+ ipv6Loopback := &net.IPNet{
|
|
| 336 |
+ IP: net.IPv6loopback, |
|
| 337 |
+ Mask: net.CIDRMask(128, 128), |
|
| 338 |
+ } |
|
| 339 |
+ |
|
| 340 |
+ // ipv4Loopback is the CIDR for IPv4 loopback addresses ("127.0.0.0/8")
|
|
| 341 |
+ ipv4Loopback := &net.IPNet{
|
|
| 342 |
+ IP: net.IPv4(127, 0, 0, 0), |
|
| 343 |
+ Mask: net.CIDRMask(8, 32), |
|
| 344 |
+ } |
|
| 345 |
+ |
|
| 346 |
+ // emptyServiceConfig is a default service-config for situations where |
|
| 347 |
+ // no config-file is available (e.g. when used in the CLI). It won't |
|
| 348 |
+ // have mirrors configured, but does have the default insecure registry |
|
| 349 |
+ // CIDRs for loopback interfaces configured. |
|
| 350 |
+ emptyServiceConfig := &serviceConfig{
|
|
| 351 |
+ IndexConfigs: map[string]*registry.IndexInfo{
|
|
| 352 |
+ IndexName: {
|
|
| 353 |
+ Name: IndexName, |
|
| 354 |
+ Mirrors: []string{},
|
|
| 355 |
+ Secure: true, |
|
| 356 |
+ Official: true, |
|
| 357 |
+ }, |
|
| 358 |
+ }, |
|
| 359 |
+ InsecureRegistryCIDRs: []*registry.NetIPNet{
|
|
| 360 |
+ (*registry.NetIPNet)(ipv6Loopback), |
|
| 361 |
+ (*registry.NetIPNet)(ipv4Loopback), |
|
| 362 |
+ }, |
|
| 363 |
+ } |
|
| 364 |
+ |
|
| 365 |
+ expectedIndexInfos := map[string]*registry.IndexInfo{
|
|
| 366 |
+ IndexName: {
|
|
| 367 |
+ Name: IndexName, |
|
| 368 |
+ Official: true, |
|
| 369 |
+ Secure: true, |
|
| 370 |
+ Mirrors: []string{},
|
|
| 371 |
+ }, |
|
| 372 |
+ "index." + IndexName: {
|
|
| 373 |
+ Name: IndexName, |
|
| 374 |
+ Official: true, |
|
| 375 |
+ Secure: true, |
|
| 376 |
+ Mirrors: []string{},
|
|
| 377 |
+ }, |
|
| 378 |
+ "example.com": {
|
|
| 379 |
+ Name: "example.com", |
|
| 380 |
+ Official: false, |
|
| 381 |
+ Secure: true, |
|
| 382 |
+ Mirrors: []string{},
|
|
| 383 |
+ }, |
|
| 384 |
+ "127.0.0.1:5000": {
|
|
| 385 |
+ Name: "127.0.0.1:5000", |
|
| 386 |
+ Official: false, |
|
| 387 |
+ Secure: false, |
|
| 388 |
+ Mirrors: []string{},
|
|
| 389 |
+ }, |
|
| 390 |
+ } |
|
| 391 |
+ t.Run("no mirrors", func(t *testing.T) {
|
|
| 392 |
+ for indexName, expected := range expectedIndexInfos {
|
|
| 393 |
+ t.Run(indexName, func(t *testing.T) {
|
|
| 394 |
+ actual := newIndexInfo(emptyServiceConfig, indexName) |
|
| 395 |
+ assert.Check(t, is.DeepEqual(actual, expected)) |
|
| 396 |
+ }) |
|
| 397 |
+ } |
|
| 398 |
+ }) |
|
| 399 |
+ |
|
| 400 |
+ expectedIndexInfos = map[string]*registry.IndexInfo{
|
|
| 401 |
+ IndexName: {
|
|
| 402 |
+ Name: IndexName, |
|
| 403 |
+ Official: true, |
|
| 404 |
+ Secure: true, |
|
| 405 |
+ Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
|
| 406 |
+ }, |
|
| 407 |
+ "index." + IndexName: {
|
|
| 408 |
+ Name: IndexName, |
|
| 409 |
+ Official: true, |
|
| 410 |
+ Secure: true, |
|
| 411 |
+ Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
|
| 412 |
+ }, |
|
| 413 |
+ "example.com": {
|
|
| 414 |
+ Name: "example.com", |
|
| 415 |
+ Official: false, |
|
| 416 |
+ Secure: false, |
|
| 417 |
+ Mirrors: []string{},
|
|
| 418 |
+ }, |
|
| 419 |
+ "example.com:5000": {
|
|
| 420 |
+ Name: "example.com:5000", |
|
| 421 |
+ Official: false, |
|
| 422 |
+ Secure: true, |
|
| 423 |
+ Mirrors: []string{},
|
|
| 424 |
+ }, |
|
| 425 |
+ "127.0.0.1": {
|
|
| 426 |
+ Name: "127.0.0.1", |
|
| 427 |
+ Official: false, |
|
| 428 |
+ Secure: false, |
|
| 429 |
+ Mirrors: []string{},
|
|
| 430 |
+ }, |
|
| 431 |
+ "127.0.0.1:5000": {
|
|
| 432 |
+ Name: "127.0.0.1:5000", |
|
| 433 |
+ Official: false, |
|
| 434 |
+ Secure: false, |
|
| 435 |
+ Mirrors: []string{},
|
|
| 436 |
+ }, |
|
| 437 |
+ "127.255.255.255": {
|
|
| 438 |
+ Name: "127.255.255.255", |
|
| 439 |
+ Official: false, |
|
| 440 |
+ Secure: false, |
|
| 441 |
+ Mirrors: []string{},
|
|
| 442 |
+ }, |
|
| 443 |
+ "127.255.255.255:5000": {
|
|
| 444 |
+ Name: "127.255.255.255:5000", |
|
| 445 |
+ Official: false, |
|
| 446 |
+ Secure: false, |
|
| 447 |
+ Mirrors: []string{},
|
|
| 448 |
+ }, |
|
| 449 |
+ "::1": {
|
|
| 450 |
+ Name: "::1", |
|
| 451 |
+ Official: false, |
|
| 452 |
+ Secure: false, |
|
| 453 |
+ Mirrors: []string{},
|
|
| 454 |
+ }, |
|
| 455 |
+ "[::1]:5000": {
|
|
| 456 |
+ Name: "[::1]:5000", |
|
| 457 |
+ Official: false, |
|
| 458 |
+ Secure: false, |
|
| 459 |
+ Mirrors: []string{},
|
|
| 460 |
+ }, |
|
| 461 |
+ // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 462 |
+ // hence not marked "insecure". |
|
| 463 |
+ "::2": {
|
|
| 464 |
+ Name: "::2", |
|
| 465 |
+ Official: false, |
|
| 466 |
+ Secure: true, |
|
| 467 |
+ Mirrors: []string{},
|
|
| 468 |
+ }, |
|
| 469 |
+ // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 470 |
+ // hence not marked "insecure". |
|
| 471 |
+ "[::2]:5000": {
|
|
| 472 |
+ Name: "[::2]:5000", |
|
| 473 |
+ Official: false, |
|
| 474 |
+ Secure: true, |
|
| 475 |
+ Mirrors: []string{},
|
|
| 476 |
+ }, |
|
| 477 |
+ "other.com": {
|
|
| 478 |
+ Name: "other.com", |
|
| 479 |
+ Official: false, |
|
| 480 |
+ Secure: true, |
|
| 481 |
+ Mirrors: []string{},
|
|
| 482 |
+ }, |
|
| 483 |
+ } |
|
| 484 |
+ t.Run("mirrors", func(t *testing.T) {
|
|
| 485 |
+ // Note that newServiceConfig calls ValidateMirror internally, which normalizes |
|
| 486 |
+ // mirror-URLs to have a trailing slash. |
|
| 487 |
+ config, err := newServiceConfig(ServiceOptions{
|
|
| 488 |
+ Mirrors: []string{"http://mirror1.local", "http://mirror2.local"},
|
|
| 489 |
+ InsecureRegistries: []string{"example.com"},
|
|
| 490 |
+ }) |
|
| 491 |
+ assert.NilError(t, err) |
|
| 492 |
+ for indexName, expected := range expectedIndexInfos {
|
|
| 493 |
+ t.Run(indexName, func(t *testing.T) {
|
|
| 494 |
+ actual := newIndexInfo(config, indexName) |
|
| 495 |
+ assert.Check(t, is.DeepEqual(actual, expected)) |
|
| 496 |
+ }) |
|
| 497 |
+ } |
|
| 498 |
+ }) |
|
| 499 |
+ |
|
| 500 |
+ expectedIndexInfos = map[string]*registry.IndexInfo{
|
|
| 501 |
+ "example.com": {
|
|
| 502 |
+ Name: "example.com", |
|
| 503 |
+ Official: false, |
|
| 504 |
+ Secure: false, |
|
| 505 |
+ Mirrors: []string{},
|
|
| 506 |
+ }, |
|
| 507 |
+ "example.com:5000": {
|
|
| 508 |
+ Name: "example.com:5000", |
|
| 509 |
+ Official: false, |
|
| 510 |
+ Secure: false, |
|
| 511 |
+ Mirrors: []string{},
|
|
| 512 |
+ }, |
|
| 513 |
+ "127.0.0.1": {
|
|
| 514 |
+ Name: "127.0.0.1", |
|
| 515 |
+ Official: false, |
|
| 516 |
+ Secure: false, |
|
| 517 |
+ Mirrors: []string{},
|
|
| 518 |
+ }, |
|
| 519 |
+ "127.0.0.1:5000": {
|
|
| 520 |
+ Name: "127.0.0.1:5000", |
|
| 521 |
+ Official: false, |
|
| 522 |
+ Secure: false, |
|
| 523 |
+ Mirrors: []string{},
|
|
| 524 |
+ }, |
|
| 525 |
+ "42.42.0.1:5000": {
|
|
| 526 |
+ Name: "42.42.0.1:5000", |
|
| 527 |
+ Official: false, |
|
| 528 |
+ Secure: false, |
|
| 529 |
+ Mirrors: []string{},
|
|
| 530 |
+ }, |
|
| 531 |
+ "42.43.0.1:5000": {
|
|
| 532 |
+ Name: "42.43.0.1:5000", |
|
| 533 |
+ Official: false, |
|
| 534 |
+ Secure: true, |
|
| 535 |
+ Mirrors: []string{},
|
|
| 536 |
+ }, |
|
| 537 |
+ "other.com": {
|
|
| 538 |
+ Name: "other.com", |
|
| 539 |
+ Official: false, |
|
| 540 |
+ Secure: true, |
|
| 541 |
+ Mirrors: []string{},
|
|
| 542 |
+ }, |
|
| 543 |
+ } |
|
| 544 |
+ t.Run("custom insecure", func(t *testing.T) {
|
|
| 545 |
+ config, err := newServiceConfig(ServiceOptions{
|
|
| 546 |
+ InsecureRegistries: []string{"42.42.0.0/16"},
|
|
| 547 |
+ }) |
|
| 548 |
+ assert.NilError(t, err) |
|
| 549 |
+ for indexName, expected := range expectedIndexInfos {
|
|
| 550 |
+ t.Run(indexName, func(t *testing.T) {
|
|
| 551 |
+ actual := newIndexInfo(config, indexName) |
|
| 552 |
+ assert.Check(t, is.DeepEqual(actual, expected)) |
|
| 553 |
+ }) |
|
| 554 |
+ } |
|
| 555 |
+ }) |
|
| 556 |
+} |
|
| 557 |
+ |
|
| 558 |
+func TestMirrorEndpointLookup(t *testing.T) {
|
|
| 559 |
+ containsMirror := func(endpoints []APIEndpoint) bool {
|
|
| 560 |
+ for _, pe := range endpoints {
|
|
| 561 |
+ if pe.URL.Host == "my.mirror" {
|
|
| 562 |
+ return true |
|
| 563 |
+ } |
|
| 564 |
+ } |
|
| 565 |
+ return false |
|
| 566 |
+ } |
|
| 567 |
+ cfg, err := newServiceConfig(ServiceOptions{
|
|
| 568 |
+ Mirrors: []string{"https://my.mirror"},
|
|
| 569 |
+ }) |
|
| 570 |
+ assert.NilError(t, err) |
|
| 571 |
+ s := Service{config: cfg}
|
|
| 572 |
+ |
|
| 573 |
+ imageName, err := reference.WithName(IndexName + "/test/image") |
|
| 574 |
+ if err != nil {
|
|
| 575 |
+ t.Error(err) |
|
| 576 |
+ } |
|
| 577 |
+ pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName)) |
|
| 578 |
+ if err != nil {
|
|
| 579 |
+ t.Fatal(err) |
|
| 580 |
+ } |
|
| 581 |
+ if containsMirror(pushAPIEndpoints) {
|
|
| 582 |
+ t.Fatal("Push endpoint should not contain mirror")
|
|
| 583 |
+ } |
|
| 584 |
+ |
|
| 585 |
+ pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName)) |
|
| 586 |
+ if err != nil {
|
|
| 587 |
+ t.Fatal(err) |
|
| 588 |
+ } |
|
| 589 |
+ if !containsMirror(pullAPIEndpoints) {
|
|
| 590 |
+ t.Fatal("Pull endpoint should contain mirror")
|
|
| 591 |
+ } |
|
| 592 |
+} |
|
| 593 |
+ |
|
| 594 |
+func TestIsSecureIndex(t *testing.T) {
|
|
| 595 |
+ overrideLookupIP(t) |
|
| 596 |
+ tests := []struct {
|
|
| 597 |
+ addr string |
|
| 598 |
+ insecureRegistries []string |
|
| 599 |
+ expected bool |
|
| 600 |
+ }{
|
|
| 601 |
+ {IndexName, nil, true},
|
|
| 602 |
+ {"example.com", []string{}, true},
|
|
| 603 |
+ {"example.com", []string{"example.com"}, false},
|
|
| 604 |
+ {"localhost", []string{"localhost:5000"}, false},
|
|
| 605 |
+ {"localhost:5000", []string{"localhost:5000"}, false},
|
|
| 606 |
+ {"localhost", []string{"example.com"}, false},
|
|
| 607 |
+ {"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
|
|
| 608 |
+ {"localhost", nil, false},
|
|
| 609 |
+ {"localhost:5000", nil, false},
|
|
| 610 |
+ {"127.0.0.1", nil, false},
|
|
| 611 |
+ {"localhost", []string{"example.com"}, false},
|
|
| 612 |
+ {"127.0.0.1", []string{"example.com"}, false},
|
|
| 613 |
+ {"example.com", nil, true},
|
|
| 614 |
+ {"example.com", []string{"example.com"}, false},
|
|
| 615 |
+ {"127.0.0.1", []string{"example.com"}, false},
|
|
| 616 |
+ {"127.0.0.1:5000", []string{"example.com"}, false},
|
|
| 617 |
+ {"example.com:5000", []string{"42.42.0.0/16"}, false},
|
|
| 618 |
+ {"example.com", []string{"42.42.0.0/16"}, false},
|
|
| 619 |
+ {"example.com:5000", []string{"42.42.42.42/8"}, false},
|
|
| 620 |
+ {"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
|
|
| 621 |
+ {"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
|
|
| 622 |
+ {"invalid.example.com", []string{"42.42.0.0/16"}, true},
|
|
| 623 |
+ {"invalid.example.com", []string{"invalid.example.com"}, false},
|
|
| 624 |
+ {"invalid.example.com:5000", []string{"invalid.example.com"}, true},
|
|
| 625 |
+ {"invalid.example.com:5000", []string{"invalid.example.com:5000"}, false},
|
|
| 626 |
+ } |
|
| 627 |
+ for _, tc := range tests {
|
|
| 628 |
+ config, err := newServiceConfig(ServiceOptions{
|
|
| 629 |
+ InsecureRegistries: tc.insecureRegistries, |
|
| 630 |
+ }) |
|
| 631 |
+ assert.NilError(t, err) |
|
| 632 |
+ |
|
| 633 |
+ sec := config.isSecureIndex(tc.addr) |
|
| 634 |
+ assert.Equal(t, sec, tc.expected, "isSecureIndex failed for %q %v, expected %v got %v", tc.addr, tc.insecureRegistries, tc.expected, sec) |
|
| 635 |
+ } |
|
| 636 |
+} |
| 0 | 637 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,100 @@ |
| 0 |
+package resumable |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "time" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/containerd/log" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+type requestReader struct {
|
|
| 14 |
+ client *http.Client |
|
| 15 |
+ request *http.Request |
|
| 16 |
+ lastRange int64 |
|
| 17 |
+ totalSize int64 |
|
| 18 |
+ currentResponse *http.Response |
|
| 19 |
+ failures uint32 |
|
| 20 |
+ maxFailures uint32 |
|
| 21 |
+ waitDuration time.Duration |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+// NewRequestReader makes it possible to resume reading a request's body transparently |
|
| 25 |
+// maxfail is the number of times we retry to make requests again (not resumes) |
|
| 26 |
+// totalsize is the total length of the body; auto detect if not provided |
|
| 27 |
+func NewRequestReader(c *http.Client, r *http.Request, maxfail uint32, totalsize int64) io.ReadCloser {
|
|
| 28 |
+ return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, waitDuration: 5 * time.Second}
|
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// NewRequestReaderWithInitialResponse makes it possible to resume |
|
| 32 |
+// reading the body of an already initiated request. |
|
| 33 |
+func NewRequestReaderWithInitialResponse(c *http.Client, r *http.Request, maxfail uint32, totalsize int64, initialResponse *http.Response) io.ReadCloser {
|
|
| 34 |
+ return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, currentResponse: initialResponse, waitDuration: 5 * time.Second}
|
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func (r *requestReader) Read(p []byte) (n int, _ error) {
|
|
| 38 |
+ if r.client == nil || r.request == nil {
|
|
| 39 |
+ return 0, errors.New("client and request can't be nil")
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ var err error |
|
| 43 |
+ isFreshRequest := false |
|
| 44 |
+ if r.lastRange != 0 && r.currentResponse == nil {
|
|
| 45 |
+ readRange := fmt.Sprintf("bytes=%d-%d", r.lastRange, r.totalSize)
|
|
| 46 |
+ r.request.Header.Set("Range", readRange)
|
|
| 47 |
+ time.Sleep(r.waitDuration) |
|
| 48 |
+ } |
|
| 49 |
+ if r.currentResponse == nil {
|
|
| 50 |
+ r.currentResponse, err = r.client.Do(r.request) |
|
| 51 |
+ isFreshRequest = true |
|
| 52 |
+ } |
|
| 53 |
+ if err != nil && r.failures+1 != r.maxFailures {
|
|
| 54 |
+ r.cleanUpResponse() |
|
| 55 |
+ r.failures++ |
|
| 56 |
+ time.Sleep(time.Duration(r.failures) * r.waitDuration) |
|
| 57 |
+ return 0, nil |
|
| 58 |
+ } else if err != nil {
|
|
| 59 |
+ r.cleanUpResponse() |
|
| 60 |
+ return 0, err |
|
| 61 |
+ } |
|
| 62 |
+ if r.currentResponse.StatusCode == http.StatusRequestedRangeNotSatisfiable && r.lastRange == r.totalSize && r.currentResponse.ContentLength == 0 {
|
|
| 63 |
+ r.cleanUpResponse() |
|
| 64 |
+ return 0, io.EOF |
|
| 65 |
+ } else if r.currentResponse.StatusCode != http.StatusPartialContent && r.lastRange != 0 && isFreshRequest {
|
|
| 66 |
+ r.cleanUpResponse() |
|
| 67 |
+ return 0, errors.New("the server doesn't support byte ranges")
|
|
| 68 |
+ } |
|
| 69 |
+ if r.totalSize == 0 {
|
|
| 70 |
+ r.totalSize = r.currentResponse.ContentLength |
|
| 71 |
+ } else if r.totalSize <= 0 {
|
|
| 72 |
+ r.cleanUpResponse() |
|
| 73 |
+ return 0, errors.New("failed to auto detect content length")
|
|
| 74 |
+ } |
|
| 75 |
+ n, err = r.currentResponse.Body.Read(p) |
|
| 76 |
+ r.lastRange += int64(n) |
|
| 77 |
+ if err != nil {
|
|
| 78 |
+ r.cleanUpResponse() |
|
| 79 |
+ } |
|
| 80 |
+ if err != nil && !errors.Is(err, io.EOF) {
|
|
| 81 |
+ log.G(context.TODO()).Infof("encountered error during pull and clearing it before resume: %s", err)
|
|
| 82 |
+ err = nil |
|
| 83 |
+ } |
|
| 84 |
+ return n, err |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+func (r *requestReader) Close() error {
|
|
| 88 |
+ r.cleanUpResponse() |
|
| 89 |
+ r.client = nil |
|
| 90 |
+ r.request = nil |
|
| 91 |
+ return nil |
|
| 92 |
+} |
|
| 93 |
+ |
|
| 94 |
+func (r *requestReader) cleanUpResponse() {
|
|
| 95 |
+ if r.currentResponse != nil {
|
|
| 96 |
+ r.currentResponse.Body.Close() |
|
| 97 |
+ r.currentResponse = nil |
|
| 98 |
+ } |
|
| 99 |
+} |
| 0 | 100 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,258 @@ |
| 0 |
+package resumable |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "errors" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ "net/http/httptest" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "testing" |
|
| 10 |
+ "time" |
|
| 11 |
+ |
|
| 12 |
+ "gotest.tools/v3/assert" |
|
| 13 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+func TestResumableRequestHeaderSimpleErrors(t *testing.T) {
|
|
| 17 |
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 18 |
+ fmt.Fprintln(w, "Hello, world !") |
|
| 19 |
+ })) |
|
| 20 |
+ defer ts.Close() |
|
| 21 |
+ |
|
| 22 |
+ client := &http.Client{}
|
|
| 23 |
+ |
|
| 24 |
+ var req *http.Request |
|
| 25 |
+ req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 26 |
+ assert.NilError(t, err) |
|
| 27 |
+ |
|
| 28 |
+ resreq := &requestReader{}
|
|
| 29 |
+ _, err = resreq.Read([]byte{})
|
|
| 30 |
+ assert.Check(t, is.Error(err, "client and request can't be nil")) |
|
| 31 |
+ |
|
| 32 |
+ resreq = &requestReader{
|
|
| 33 |
+ client: client, |
|
| 34 |
+ request: req, |
|
| 35 |
+ totalSize: -1, |
|
| 36 |
+ } |
|
| 37 |
+ _, err = resreq.Read([]byte{})
|
|
| 38 |
+ assert.Check(t, is.Error(err, "failed to auto detect content length")) |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+// Not too much failures, bails out after some wait |
|
| 42 |
+func TestResumableRequestHeaderNotTooMuchFailures(t *testing.T) {
|
|
| 43 |
+ client := &http.Client{}
|
|
| 44 |
+ |
|
| 45 |
+ var badReq *http.Request |
|
| 46 |
+ badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody) |
|
| 47 |
+ assert.NilError(t, err) |
|
| 48 |
+ |
|
| 49 |
+ resreq := &requestReader{
|
|
| 50 |
+ client: client, |
|
| 51 |
+ request: badReq, |
|
| 52 |
+ failures: 0, |
|
| 53 |
+ maxFailures: 2, |
|
| 54 |
+ waitDuration: 10 * time.Millisecond, |
|
| 55 |
+ } |
|
| 56 |
+ read, err := resreq.Read([]byte{})
|
|
| 57 |
+ assert.NilError(t, err) |
|
| 58 |
+ assert.Check(t, is.Equal(0, read)) |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+// Too much failures, returns the error |
|
| 62 |
+func TestResumableRequestHeaderTooMuchFailures(t *testing.T) {
|
|
| 63 |
+ client := &http.Client{}
|
|
| 64 |
+ |
|
| 65 |
+ var badReq *http.Request |
|
| 66 |
+ badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody) |
|
| 67 |
+ assert.NilError(t, err) |
|
| 68 |
+ |
|
| 69 |
+ resreq := &requestReader{
|
|
| 70 |
+ client: client, |
|
| 71 |
+ request: badReq, |
|
| 72 |
+ failures: 0, |
|
| 73 |
+ maxFailures: 1, |
|
| 74 |
+ } |
|
| 75 |
+ defer resreq.Close() |
|
| 76 |
+ |
|
| 77 |
+ read, err := resreq.Read([]byte{})
|
|
| 78 |
+ assert.Assert(t, err != nil) |
|
| 79 |
+ assert.Check(t, is.ErrorContains(err, "unsupported protocol scheme")) |
|
| 80 |
+ assert.Check(t, is.ErrorContains(err, "I%27m%20not%20an%20url")) |
|
| 81 |
+ assert.Check(t, is.Equal(0, read)) |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+type errorReaderCloser struct{}
|
|
| 85 |
+ |
|
| 86 |
+func (errorReaderCloser) Close() error { return nil }
|
|
| 87 |
+ |
|
| 88 |
+func (errorReaderCloser) Read(p []byte) (int, error) {
|
|
| 89 |
+ return 0, errors.New("an error occurred")
|
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+// If an unknown error is encountered, return 0, nil and log it |
|
| 93 |
+func TestResumableRequestReaderWithReadError(t *testing.T) {
|
|
| 94 |
+ var req *http.Request |
|
| 95 |
+ req, err := http.NewRequest(http.MethodGet, "", http.NoBody) |
|
| 96 |
+ assert.NilError(t, err) |
|
| 97 |
+ |
|
| 98 |
+ client := &http.Client{}
|
|
| 99 |
+ |
|
| 100 |
+ response := &http.Response{
|
|
| 101 |
+ Status: "500 Internal Server", |
|
| 102 |
+ StatusCode: http.StatusInternalServerError, |
|
| 103 |
+ ContentLength: 0, |
|
| 104 |
+ Close: true, |
|
| 105 |
+ Body: errorReaderCloser{},
|
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 108 |
+ resreq := &requestReader{
|
|
| 109 |
+ client: client, |
|
| 110 |
+ request: req, |
|
| 111 |
+ currentResponse: response, |
|
| 112 |
+ lastRange: 1, |
|
| 113 |
+ totalSize: 1, |
|
| 114 |
+ } |
|
| 115 |
+ defer resreq.Close() |
|
| 116 |
+ |
|
| 117 |
+ buf := make([]byte, 1) |
|
| 118 |
+ read, err := resreq.Read(buf) |
|
| 119 |
+ assert.NilError(t, err) |
|
| 120 |
+ |
|
| 121 |
+ assert.Check(t, is.Equal(0, read)) |
|
| 122 |
+} |
|
| 123 |
+ |
|
| 124 |
+func TestResumableRequestReaderWithEOFWith416Response(t *testing.T) {
|
|
| 125 |
+ var req *http.Request |
|
| 126 |
+ req, err := http.NewRequest(http.MethodGet, "", http.NoBody) |
|
| 127 |
+ assert.NilError(t, err) |
|
| 128 |
+ |
|
| 129 |
+ client := &http.Client{}
|
|
| 130 |
+ |
|
| 131 |
+ response := &http.Response{
|
|
| 132 |
+ Status: "416 Requested Range Not Satisfiable", |
|
| 133 |
+ StatusCode: http.StatusRequestedRangeNotSatisfiable, |
|
| 134 |
+ ContentLength: 0, |
|
| 135 |
+ Close: true, |
|
| 136 |
+ Body: io.NopCloser(strings.NewReader("")),
|
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ resreq := &requestReader{
|
|
| 140 |
+ client: client, |
|
| 141 |
+ request: req, |
|
| 142 |
+ currentResponse: response, |
|
| 143 |
+ lastRange: 1, |
|
| 144 |
+ totalSize: 1, |
|
| 145 |
+ } |
|
| 146 |
+ defer resreq.Close() |
|
| 147 |
+ |
|
| 148 |
+ buf := make([]byte, 1) |
|
| 149 |
+ _, err = resreq.Read(buf) |
|
| 150 |
+ assert.Check(t, is.Error(err, io.EOF.Error())) |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+func TestResumableRequestReaderWithServerDoesntSupportByteRanges(t *testing.T) {
|
|
| 154 |
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 155 |
+ if r.Header.Get("Range") == "" {
|
|
| 156 |
+ t.Fatalf("Expected a Range HTTP header, got nothing")
|
|
| 157 |
+ } |
|
| 158 |
+ })) |
|
| 159 |
+ defer ts.Close() |
|
| 160 |
+ |
|
| 161 |
+ var req *http.Request |
|
| 162 |
+ req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 163 |
+ assert.NilError(t, err) |
|
| 164 |
+ |
|
| 165 |
+ client := &http.Client{}
|
|
| 166 |
+ |
|
| 167 |
+ resreq := &requestReader{
|
|
| 168 |
+ client: client, |
|
| 169 |
+ request: req, |
|
| 170 |
+ lastRange: 1, |
|
| 171 |
+ } |
|
| 172 |
+ defer resreq.Close() |
|
| 173 |
+ |
|
| 174 |
+ buf := make([]byte, 2) |
|
| 175 |
+ _, err = resreq.Read(buf) |
|
| 176 |
+ assert.Check(t, is.Error(err, "the server doesn't support byte ranges")) |
|
| 177 |
+} |
|
| 178 |
+ |
|
| 179 |
+func TestResumableRequestReaderWithZeroTotalSize(t *testing.T) {
|
|
| 180 |
+ srvtxt := "some response text data" |
|
| 181 |
+ |
|
| 182 |
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 183 |
+ fmt.Fprintln(w, srvtxt) |
|
| 184 |
+ })) |
|
| 185 |
+ defer ts.Close() |
|
| 186 |
+ |
|
| 187 |
+ var req *http.Request |
|
| 188 |
+ req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 189 |
+ assert.NilError(t, err) |
|
| 190 |
+ |
|
| 191 |
+ client := &http.Client{}
|
|
| 192 |
+ retries := uint32(5) |
|
| 193 |
+ |
|
| 194 |
+ resreq := NewRequestReader(client, req, retries, 0) |
|
| 195 |
+ defer resreq.Close() |
|
| 196 |
+ |
|
| 197 |
+ data, err := io.ReadAll(resreq) |
|
| 198 |
+ assert.NilError(t, err) |
|
| 199 |
+ |
|
| 200 |
+ resstr := strings.TrimSuffix(string(data), "\n") |
|
| 201 |
+ assert.Check(t, is.Equal(srvtxt, resstr)) |
|
| 202 |
+} |
|
| 203 |
+ |
|
| 204 |
+func TestResumableRequestReader(t *testing.T) {
|
|
| 205 |
+ srvtxt := "some response text data" |
|
| 206 |
+ |
|
| 207 |
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 208 |
+ fmt.Fprintln(w, srvtxt) |
|
| 209 |
+ })) |
|
| 210 |
+ defer ts.Close() |
|
| 211 |
+ |
|
| 212 |
+ var req *http.Request |
|
| 213 |
+ req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 214 |
+ assert.NilError(t, err) |
|
| 215 |
+ |
|
| 216 |
+ client := &http.Client{}
|
|
| 217 |
+ retries := uint32(5) |
|
| 218 |
+ imgSize := int64(len(srvtxt)) |
|
| 219 |
+ |
|
| 220 |
+ resreq := NewRequestReader(client, req, retries, imgSize) |
|
| 221 |
+ defer resreq.Close() |
|
| 222 |
+ |
|
| 223 |
+ data, err := io.ReadAll(resreq) |
|
| 224 |
+ assert.NilError(t, err) |
|
| 225 |
+ |
|
| 226 |
+ resstr := strings.TrimSuffix(string(data), "\n") |
|
| 227 |
+ assert.Check(t, is.Equal(srvtxt, resstr)) |
|
| 228 |
+} |
|
| 229 |
+ |
|
| 230 |
+func TestResumableRequestReaderWithInitialResponse(t *testing.T) {
|
|
| 231 |
+ srvtxt := "some response text data" |
|
| 232 |
+ |
|
| 233 |
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 234 |
+ fmt.Fprintln(w, srvtxt) |
|
| 235 |
+ })) |
|
| 236 |
+ defer ts.Close() |
|
| 237 |
+ |
|
| 238 |
+ var req *http.Request |
|
| 239 |
+ req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 240 |
+ assert.NilError(t, err) |
|
| 241 |
+ |
|
| 242 |
+ client := &http.Client{}
|
|
| 243 |
+ retries := uint32(5) |
|
| 244 |
+ imgSize := int64(len(srvtxt)) |
|
| 245 |
+ |
|
| 246 |
+ res, err := client.Do(req) |
|
| 247 |
+ assert.NilError(t, err) |
|
| 248 |
+ |
|
| 249 |
+ resreq := NewRequestReaderWithInitialResponse(client, req, retries, imgSize, res) |
|
| 250 |
+ defer resreq.Close() |
|
| 251 |
+ |
|
| 252 |
+ data, err := io.ReadAll(resreq) |
|
| 253 |
+ assert.NilError(t, err) |
|
| 254 |
+ |
|
| 255 |
+ resstr := strings.TrimSuffix(string(data), "\n") |
|
| 256 |
+ assert.Check(t, is.Equal(srvtxt, resstr)) |
|
| 257 |
+} |
| 0 | 258 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,170 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/containerd/log" |
|
| 9 |
+ "github.com/docker/distribution/registry/client/auth" |
|
| 10 |
+ "github.com/moby/moby/api/types/filters" |
|
| 11 |
+ "github.com/moby/moby/api/types/registry" |
|
| 12 |
+ "github.com/pkg/errors" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+var acceptedSearchFilterTags = map[string]bool{
|
|
| 16 |
+ "is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future. |
|
| 17 |
+ "is-official": true, |
|
| 18 |
+ "stars": true, |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+// Search queries the public registry for repositories matching the specified |
|
| 22 |
+// search term and filters. |
|
| 23 |
+func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
|
|
| 24 |
+ if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
|
|
| 25 |
+ return nil, err |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
|
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return nil, err |
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ // "is-automated" is deprecated and filtering for `true` will yield no results. |
|
| 34 |
+ if isAutomated {
|
|
| 35 |
+ return []registry.SearchResult{}, nil
|
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
|
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return nil, err |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ hasStarFilter := 0 |
|
| 44 |
+ if searchFilters.Contains("stars") {
|
|
| 45 |
+ hasStars := searchFilters.Get("stars")
|
|
| 46 |
+ for _, hasStar := range hasStars {
|
|
| 47 |
+ iHasStar, err := strconv.Atoi(hasStar) |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)}
|
|
| 50 |
+ } |
|
| 51 |
+ if iHasStar > hasStarFilter {
|
|
| 52 |
+ hasStarFilter = iHasStar |
|
| 53 |
+ } |
|
| 54 |
+ } |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers) |
|
| 58 |
+ if err != nil {
|
|
| 59 |
+ return nil, err |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ filteredResults := []registry.SearchResult{}
|
|
| 63 |
+ for _, result := range unfilteredResult.Results {
|
|
| 64 |
+ if searchFilters.Contains("is-official") {
|
|
| 65 |
+ if isOfficial != result.IsOfficial {
|
|
| 66 |
+ continue |
|
| 67 |
+ } |
|
| 68 |
+ } |
|
| 69 |
+ if searchFilters.Contains("stars") {
|
|
| 70 |
+ if result.StarCount < hasStarFilter {
|
|
| 71 |
+ continue |
|
| 72 |
+ } |
|
| 73 |
+ } |
|
| 74 |
+ // "is-automated" is deprecated and the value in Docker Hub search |
|
| 75 |
+ // results is untrustworthy. Force it to false so as to not mislead our |
|
| 76 |
+ // clients. |
|
| 77 |
+ result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated) |
|
| 78 |
+ filteredResults = append(filteredResults, result) |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ return filteredResults, nil |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
|
|
| 85 |
+ if hasScheme(term) {
|
|
| 86 |
+ return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
|
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ indexName, remoteName := splitReposSearchTerm(term) |
|
| 90 |
+ |
|
| 91 |
+ // Search is a long-running operation, just lock s.config to avoid block others. |
|
| 92 |
+ s.mu.RLock() |
|
| 93 |
+ index := newIndexInfo(s.config, indexName) |
|
| 94 |
+ s.mu.RUnlock() |
|
| 95 |
+ if index.Official {
|
|
| 96 |
+ // If pull "library/foo", it's stored locally under "foo" |
|
| 97 |
+ remoteName = strings.TrimPrefix(remoteName, "library/") |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ endpoint, err := newV1Endpoint(ctx, index, headers) |
|
| 101 |
+ if err != nil {
|
|
| 102 |
+ return nil, err |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ var client *http.Client |
|
| 106 |
+ if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
|
|
| 107 |
+ creds := NewStaticCredentialStore(authConfig) |
|
| 108 |
+ |
|
| 109 |
+ // TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac) |
|
| 110 |
+ modifiers := Headers(headers.Get("User-Agent"), nil)
|
|
| 111 |
+ v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{
|
|
| 112 |
+ auth.RegistryScope{Name: "catalog", Actions: []string{"search"}},
|
|
| 113 |
+ }) |
|
| 114 |
+ if err != nil {
|
|
| 115 |
+ return nil, err |
|
| 116 |
+ } |
|
| 117 |
+ // Copy non transport http client features |
|
| 118 |
+ v2Client.Timeout = endpoint.client.Timeout |
|
| 119 |
+ v2Client.CheckRedirect = endpoint.client.CheckRedirect |
|
| 120 |
+ v2Client.Jar = endpoint.client.Jar |
|
| 121 |
+ |
|
| 122 |
+ log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
|
|
| 123 |
+ client = v2Client |
|
| 124 |
+ } else {
|
|
| 125 |
+ client = endpoint.client |
|
| 126 |
+ if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil {
|
|
| 127 |
+ return nil, err |
|
| 128 |
+ } |
|
| 129 |
+ } |
|
| 130 |
+ |
|
| 131 |
+ return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit) |
|
| 132 |
+} |
|
| 133 |
+ |
|
| 134 |
+// splitReposSearchTerm breaks a search term into an index name and remote name |
|
| 135 |
+func splitReposSearchTerm(reposName string) (string, string) {
|
|
| 136 |
+ nameParts := strings.SplitN(reposName, "/", 2) |
|
| 137 |
+ if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && |
|
| 138 |
+ !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
|
| 139 |
+ // This is a Docker Hub repository (ex: samalba/hipache or ubuntu), |
|
| 140 |
+ // use the default Docker Hub registry (docker.io) |
|
| 141 |
+ return IndexName, reposName |
|
| 142 |
+ } |
|
| 143 |
+ return nameParts[0], nameParts[1] |
|
| 144 |
+} |
|
| 145 |
+ |
|
| 146 |
+// ParseSearchIndexInfo will use repository name to get back an indexInfo. |
|
| 147 |
+// |
|
| 148 |
+// TODO(thaJeztah) this function is only used by the CLI, and used to get |
|
| 149 |
+// information of the registry (to provide credentials if needed). We should |
|
| 150 |
+// move this function (or equivalent) to the CLI, as it's doing too much just |
|
| 151 |
+// for that. |
|
| 152 |
+func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
|
|
| 153 |
+ indexName, _ := splitReposSearchTerm(reposName) |
|
| 154 |
+ indexName = normalizeIndexName(indexName) |
|
| 155 |
+ if indexName == IndexName {
|
|
| 156 |
+ return ®istry.IndexInfo{
|
|
| 157 |
+ Name: IndexName, |
|
| 158 |
+ Mirrors: []string{},
|
|
| 159 |
+ Secure: true, |
|
| 160 |
+ Official: true, |
|
| 161 |
+ }, nil |
|
| 162 |
+ } |
|
| 163 |
+ |
|
| 164 |
+ return ®istry.IndexInfo{
|
|
| 165 |
+ Name: indexName, |
|
| 166 |
+ Mirrors: []string{},
|
|
| 167 |
+ Secure: !isInsecure(indexName), |
|
| 168 |
+ }, nil |
|
| 169 |
+} |
| 0 | 170 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,213 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "crypto/tls" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "errors" |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "net/http" |
|
| 9 |
+ "net/url" |
|
| 10 |
+ "strings" |
|
| 11 |
+ |
|
| 12 |
+ "github.com/containerd/log" |
|
| 13 |
+ "github.com/docker/distribution/registry/client/transport" |
|
| 14 |
+ "github.com/moby/moby/api/types/registry" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// v1PingResult contains the information returned when pinging a registry. It |
|
| 18 |
+// indicates whether the registry claims to be a standalone registry. |
|
| 19 |
+type v1PingResult struct {
|
|
| 20 |
+ // Standalone is set to true if the registry indicates it is a |
|
| 21 |
+ // standalone registry in the X-Docker-Registry-Standalone |
|
| 22 |
+ // header |
|
| 23 |
+ Standalone bool `json:"standalone"` |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// v1Endpoint stores basic information about a V1 registry endpoint. |
|
| 27 |
+type v1Endpoint struct {
|
|
| 28 |
+ client *http.Client |
|
| 29 |
+ URL *url.URL |
|
| 30 |
+ IsSecure bool |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+// newV1Endpoint parses the given address to return a registry endpoint. |
|
| 34 |
+// TODO: remove. This is only used by search. |
|
| 35 |
+func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
|
|
| 36 |
+ tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ return nil, err |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers) |
|
| 42 |
+ if err != nil {
|
|
| 43 |
+ return nil, err |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ if endpoint.String() == IndexServer {
|
|
| 47 |
+ // Skip the check, we know this one is valid |
|
| 48 |
+ // (and we never want to fall back to http in case of error) |
|
| 49 |
+ return endpoint, nil |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ // Try HTTPS ping to registry |
|
| 53 |
+ endpoint.URL.Scheme = "https" |
|
| 54 |
+ if _, err := endpoint.ping(ctx); err != nil {
|
|
| 55 |
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
| 56 |
+ return nil, err |
|
| 57 |
+ } |
|
| 58 |
+ if endpoint.IsSecure {
|
|
| 59 |
+ // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` |
|
| 60 |
+ // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP. |
|
| 61 |
+ hint := fmt.Sprintf( |
|
| 62 |
+ ". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+ |
|
| 63 |
+ "In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt", |
|
| 64 |
+ endpoint.URL.Host, |
|
| 65 |
+ ) |
|
| 66 |
+ return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint)
|
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 69 |
+ // registry is insecure and HTTPS failed, fallback to HTTP. |
|
| 70 |
+ log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
|
|
| 71 |
+ endpoint.URL.Scheme = "http" |
|
| 72 |
+ if _, err2 := endpoint.ping(ctx); err2 != nil {
|
|
| 73 |
+ return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
|
|
| 74 |
+ } |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ return endpoint, nil |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+// trimV1Address trims the "v1" version suffix off the address and returns |
|
| 81 |
+// the trimmed address. It returns an error on "v2" endpoints. |
|
| 82 |
+func trimV1Address(address string) (string, error) {
|
|
| 83 |
+ trimmed := strings.TrimSuffix(address, "/") |
|
| 84 |
+ if strings.HasSuffix(trimmed, "/v2") {
|
|
| 85 |
+ return "", invalidParamf("search is not supported on v2 endpoints: %s", address)
|
|
| 86 |
+ } |
|
| 87 |
+ return strings.TrimSuffix(trimmed, "/v1"), nil |
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
|
|
| 91 |
+ if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
|
| 92 |
+ address = "https://" + address |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ address, err := trimV1Address(address) |
|
| 96 |
+ if err != nil {
|
|
| 97 |
+ return nil, err |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ uri, err := url.Parse(address) |
|
| 101 |
+ if err != nil {
|
|
| 102 |
+ return nil, invalidParam(err) |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ // TODO(tiborvass): make sure a ConnectTimeout transport is used |
|
| 106 |
+ tr := newTransport(tlsConfig) |
|
| 107 |
+ |
|
| 108 |
+ return &v1Endpoint{
|
|
| 109 |
+ IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify, |
|
| 110 |
+ URL: uri, |
|
| 111 |
+ client: httpClient(transport.NewTransport(tr, Headers("", headers)...)),
|
|
| 112 |
+ }, nil |
|
| 113 |
+} |
|
| 114 |
+ |
|
| 115 |
+// Get the formatted URL for the root of this registry Endpoint |
|
| 116 |
+func (e *v1Endpoint) String() string {
|
|
| 117 |
+ return e.URL.String() + "/v1/" |
|
| 118 |
+} |
|
| 119 |
+ |
|
| 120 |
+// ping returns a v1PingResult which indicates whether the registry is standalone or not. |
|
| 121 |
+func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
|
|
| 122 |
+ if e.String() == IndexServer {
|
|
| 123 |
+ // Skip the check, we know this one is valid |
|
| 124 |
+ // (and we never want to fallback to http in case of error) |
|
| 125 |
+ return v1PingResult{}, nil
|
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ pingURL := e.String() + "_ping" |
|
| 129 |
+ log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
|
|
| 130 |
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody) |
|
| 131 |
+ if err != nil {
|
|
| 132 |
+ return v1PingResult{}, invalidParam(err)
|
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ resp, err := e.client.Do(req) |
|
| 136 |
+ if err != nil {
|
|
| 137 |
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
| 138 |
+ return v1PingResult{}, err
|
|
| 139 |
+ } |
|
| 140 |
+ return v1PingResult{}, invalidParam(err)
|
|
| 141 |
+ } |
|
| 142 |
+ |
|
| 143 |
+ defer resp.Body.Close() |
|
| 144 |
+ |
|
| 145 |
+ if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" {
|
|
| 146 |
+ info := v1PingResult{}
|
|
| 147 |
+ // Accepted values are "1", and "true" (case-insensitive). |
|
| 148 |
+ if v == "1" || strings.EqualFold(v, "true") {
|
|
| 149 |
+ info.Standalone = true |
|
| 150 |
+ } |
|
| 151 |
+ log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
|
|
| 152 |
+ return info, nil |
|
| 153 |
+ } |
|
| 154 |
+ |
|
| 155 |
+ // If the header is absent, we assume true for compatibility with earlier |
|
| 156 |
+ // versions of the registry. default to true |
|
| 157 |
+ info := v1PingResult{
|
|
| 158 |
+ Standalone: true, |
|
| 159 |
+ } |
|
| 160 |
+ if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
|
| 161 |
+ log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
|
|
| 162 |
+ // don't stop here. Just assume sane defaults |
|
| 163 |
+ } |
|
| 164 |
+ |
|
| 165 |
+ log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
|
|
| 166 |
+ return info, nil |
|
| 167 |
+} |
|
| 168 |
+ |
|
| 169 |
+// httpClient returns an HTTP client structure which uses the given transport |
|
| 170 |
+// and contains the necessary headers for redirected requests |
|
| 171 |
+func httpClient(tr http.RoundTripper) *http.Client {
|
|
| 172 |
+ return &http.Client{
|
|
| 173 |
+ Transport: tr, |
|
| 174 |
+ CheckRedirect: addRequiredHeadersToRedirectedRequests, |
|
| 175 |
+ } |
|
| 176 |
+} |
|
| 177 |
+ |
|
| 178 |
+func trustedLocation(req *http.Request) bool {
|
|
| 179 |
+ var ( |
|
| 180 |
+ trusteds = []string{"docker.com", "docker.io"}
|
|
| 181 |
+ hostname = strings.SplitN(req.Host, ":", 2)[0] |
|
| 182 |
+ ) |
|
| 183 |
+ if req.URL.Scheme != "https" {
|
|
| 184 |
+ return false |
|
| 185 |
+ } |
|
| 186 |
+ |
|
| 187 |
+ for _, trusted := range trusteds {
|
|
| 188 |
+ if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
|
|
| 189 |
+ return true |
|
| 190 |
+ } |
|
| 191 |
+ } |
|
| 192 |
+ return false |
|
| 193 |
+} |
|
| 194 |
+ |
|
| 195 |
+// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers |
|
| 196 |
+// for redirected requests |
|
| 197 |
+func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
|
|
| 198 |
+ if len(via) != 0 && via[0] != nil {
|
|
| 199 |
+ if trustedLocation(req) && trustedLocation(via[0]) {
|
|
| 200 |
+ req.Header = via[0].Header |
|
| 201 |
+ return nil |
|
| 202 |
+ } |
|
| 203 |
+ for k, v := range via[0].Header {
|
|
| 204 |
+ if k != "Authorization" {
|
|
| 205 |
+ for _, vv := range v {
|
|
| 206 |
+ req.Header.Add(k, vv) |
|
| 207 |
+ } |
|
| 208 |
+ } |
|
| 209 |
+ } |
|
| 210 |
+ } |
|
| 211 |
+ return nil |
|
| 212 |
+} |
| 0 | 213 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,237 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "net/http/httptest" |
|
| 6 |
+ "strings" |
|
| 7 |
+ "testing" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/moby/moby/api/types/registry" |
|
| 10 |
+ "gotest.tools/v3/assert" |
|
| 11 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+func TestV1EndpointPing(t *testing.T) {
|
|
| 15 |
+ testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
|
|
| 16 |
+ ep, err := newV1Endpoint(context.Background(), index, nil) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ t.Fatal(err) |
|
| 19 |
+ } |
|
| 20 |
+ regInfo, err := ep.ping(context.Background()) |
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ t.Fatal(err) |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage) |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
|
|
| 29 |
+ testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)")
|
|
| 30 |
+ testPing(makePublicIndex(), false, "Expected standalone to be false for public index") |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+func TestV1Endpoint(t *testing.T) {
|
|
| 34 |
+ // Simple wrapper to fail test if err != nil |
|
| 35 |
+ expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
|
|
| 36 |
+ endpoint, err := newV1Endpoint(context.Background(), index, nil) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ t.Fatal(err) |
|
| 39 |
+ } |
|
| 40 |
+ return endpoint |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ assertInsecureIndex := func(index *registry.IndexInfo) {
|
|
| 44 |
+ index.Secure = true |
|
| 45 |
+ _, err := newV1Endpoint(context.Background(), index, nil) |
|
| 46 |
+ assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index") |
|
| 47 |
+ index.Secure = false |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ assertSecureIndex := func(index *registry.IndexInfo) {
|
|
| 51 |
+ index.Secure = true |
|
| 52 |
+ _, err := newV1Endpoint(context.Background(), index, nil) |
|
| 53 |
+ assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index") |
|
| 54 |
+ index.Secure = false |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ index := ®istry.IndexInfo{}
|
|
| 58 |
+ index.Name = makeURL("/v1/")
|
|
| 59 |
+ endpoint := expandEndpoint(index) |
|
| 60 |
+ assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) |
|
| 61 |
+ assertInsecureIndex(index) |
|
| 62 |
+ |
|
| 63 |
+ index.Name = makeURL("")
|
|
| 64 |
+ endpoint = expandEndpoint(index) |
|
| 65 |
+ assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") |
|
| 66 |
+ assertInsecureIndex(index) |
|
| 67 |
+ |
|
| 68 |
+ httpURL := makeURL("")
|
|
| 69 |
+ index.Name = strings.SplitN(httpURL, "://", 2)[1] |
|
| 70 |
+ endpoint = expandEndpoint(index) |
|
| 71 |
+ assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") |
|
| 72 |
+ assertInsecureIndex(index) |
|
| 73 |
+ |
|
| 74 |
+ index.Name = makeHTTPSURL("/v1/")
|
|
| 75 |
+ endpoint = expandEndpoint(index) |
|
| 76 |
+ assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) |
|
| 77 |
+ assertSecureIndex(index) |
|
| 78 |
+ |
|
| 79 |
+ index.Name = makeHTTPSURL("")
|
|
| 80 |
+ endpoint = expandEndpoint(index) |
|
| 81 |
+ assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") |
|
| 82 |
+ assertSecureIndex(index) |
|
| 83 |
+ |
|
| 84 |
+ httpsURL := makeHTTPSURL("")
|
|
| 85 |
+ index.Name = strings.SplitN(httpsURL, "://", 2)[1] |
|
| 86 |
+ endpoint = expandEndpoint(index) |
|
| 87 |
+ assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") |
|
| 88 |
+ assertSecureIndex(index) |
|
| 89 |
+ |
|
| 90 |
+ badEndpoints := []string{
|
|
| 91 |
+ "http://127.0.0.1/v1/", |
|
| 92 |
+ "https://127.0.0.1/v1/", |
|
| 93 |
+ "http://127.0.0.1", |
|
| 94 |
+ "https://127.0.0.1", |
|
| 95 |
+ "127.0.0.1", |
|
| 96 |
+ } |
|
| 97 |
+ for _, address := range badEndpoints {
|
|
| 98 |
+ index.Name = address |
|
| 99 |
+ _, err := newV1Endpoint(context.Background(), index, nil) |
|
| 100 |
+ assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address) |
|
| 101 |
+ } |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+func TestV1EndpointParse(t *testing.T) {
|
|
| 105 |
+ tests := []struct {
|
|
| 106 |
+ address string |
|
| 107 |
+ expected string |
|
| 108 |
+ expectedErr string |
|
| 109 |
+ }{
|
|
| 110 |
+ {
|
|
| 111 |
+ address: IndexServer, |
|
| 112 |
+ expected: IndexServer, |
|
| 113 |
+ }, |
|
| 114 |
+ {
|
|
| 115 |
+ address: "https://0.0.0.0:5000/v1/", |
|
| 116 |
+ expected: "https://0.0.0.0:5000/v1/", |
|
| 117 |
+ }, |
|
| 118 |
+ {
|
|
| 119 |
+ address: "https://0.0.0.0:5000", |
|
| 120 |
+ expected: "https://0.0.0.0:5000/v1/", |
|
| 121 |
+ }, |
|
| 122 |
+ {
|
|
| 123 |
+ address: "0.0.0.0:5000", |
|
| 124 |
+ expected: "https://0.0.0.0:5000/v1/", |
|
| 125 |
+ }, |
|
| 126 |
+ {
|
|
| 127 |
+ address: "https://0.0.0.0:5000/nonversion/", |
|
| 128 |
+ expected: "https://0.0.0.0:5000/nonversion/v1/", |
|
| 129 |
+ }, |
|
| 130 |
+ {
|
|
| 131 |
+ address: "https://0.0.0.0:5000/v0/", |
|
| 132 |
+ expected: "https://0.0.0.0:5000/v0/v1/", |
|
| 133 |
+ }, |
|
| 134 |
+ {
|
|
| 135 |
+ address: "https://0.0.0.0:5000/v2/", |
|
| 136 |
+ expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/", |
|
| 137 |
+ }, |
|
| 138 |
+ } |
|
| 139 |
+ for _, tc := range tests {
|
|
| 140 |
+ t.Run(tc.address, func(t *testing.T) {
|
|
| 141 |
+ ep, err := newV1EndpointFromStr(tc.address, nil, nil) |
|
| 142 |
+ if tc.expectedErr != "" {
|
|
| 143 |
+ assert.Check(t, is.Error(err, tc.expectedErr)) |
|
| 144 |
+ assert.Check(t, is.Nil(ep)) |
|
| 145 |
+ } else {
|
|
| 146 |
+ assert.NilError(t, err) |
|
| 147 |
+ assert.Check(t, is.Equal(ep.String(), tc.expected)) |
|
| 148 |
+ } |
|
| 149 |
+ }) |
|
| 150 |
+ } |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+// Ensure that a registry endpoint that responds with a 401 only is determined |
|
| 154 |
+// to be a valid v1 registry endpoint |
|
| 155 |
+func TestV1EndpointValidate(t *testing.T) {
|
|
| 156 |
+ requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 157 |
+ w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
|
|
| 158 |
+ w.WriteHeader(http.StatusUnauthorized) |
|
| 159 |
+ }) |
|
| 160 |
+ |
|
| 161 |
+ // Make a test server which should validate as a v1 server. |
|
| 162 |
+ testServer := httptest.NewServer(requireBasicAuthHandler) |
|
| 163 |
+ defer testServer.Close() |
|
| 164 |
+ |
|
| 165 |
+ testEndpoint, err := newV1Endpoint(context.Background(), ®istry.IndexInfo{Name: testServer.URL}, nil)
|
|
| 166 |
+ if err != nil {
|
|
| 167 |
+ t.Fatal(err) |
|
| 168 |
+ } |
|
| 169 |
+ |
|
| 170 |
+ if testEndpoint.URL.Scheme != "http" {
|
|
| 171 |
+ t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
|
|
| 172 |
+ } |
|
| 173 |
+} |
|
| 174 |
+ |
|
| 175 |
+func TestTrustedLocation(t *testing.T) {
|
|
| 176 |
+ for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
|
|
| 177 |
+ req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) |
|
| 178 |
+ assert.Check(t, !trustedLocation(req)) |
|
| 179 |
+ } |
|
| 180 |
+ |
|
| 181 |
+ for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} {
|
|
| 182 |
+ req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) |
|
| 183 |
+ assert.Check(t, trustedLocation(req)) |
|
| 184 |
+ } |
|
| 185 |
+} |
|
| 186 |
+ |
|
| 187 |
+func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
|
|
| 188 |
+ for _, urls := range [][]string{
|
|
| 189 |
+ {"http://docker.io", "https://docker.com"},
|
|
| 190 |
+ {"https://foo.docker.io:7777", "http://bar.docker.com"},
|
|
| 191 |
+ {"https://foo.docker.io", "https://example.com"},
|
|
| 192 |
+ } {
|
|
| 193 |
+ reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) |
|
| 194 |
+ reqFrom.Header.Add("Content-Type", "application/json")
|
|
| 195 |
+ reqFrom.Header.Add("Authorization", "super_secret")
|
|
| 196 |
+ reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) |
|
| 197 |
+ |
|
| 198 |
+ _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
|
| 199 |
+ |
|
| 200 |
+ if len(reqTo.Header) != 1 {
|
|
| 201 |
+ t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
|
|
| 202 |
+ } |
|
| 203 |
+ |
|
| 204 |
+ if reqTo.Header.Get("Content-Type") != "application/json" {
|
|
| 205 |
+ t.Fatal("'Content-Type' should be 'application/json'")
|
|
| 206 |
+ } |
|
| 207 |
+ |
|
| 208 |
+ if reqTo.Header.Get("Authorization") != "" {
|
|
| 209 |
+ t.Fatal("'Authorization' should be empty")
|
|
| 210 |
+ } |
|
| 211 |
+ } |
|
| 212 |
+ |
|
| 213 |
+ for _, urls := range [][]string{
|
|
| 214 |
+ {"https://docker.io", "https://docker.com"},
|
|
| 215 |
+ {"https://foo.docker.io:7777", "https://bar.docker.com"},
|
|
| 216 |
+ } {
|
|
| 217 |
+ reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) |
|
| 218 |
+ reqFrom.Header.Add("Content-Type", "application/json")
|
|
| 219 |
+ reqFrom.Header.Add("Authorization", "super_secret")
|
|
| 220 |
+ reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) |
|
| 221 |
+ |
|
| 222 |
+ _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
|
| 223 |
+ |
|
| 224 |
+ if len(reqTo.Header) != 2 {
|
|
| 225 |
+ t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
|
|
| 226 |
+ } |
|
| 227 |
+ |
|
| 228 |
+ if reqTo.Header.Get("Content-Type") != "application/json" {
|
|
| 229 |
+ t.Fatal("'Content-Type' should be 'application/json'")
|
|
| 230 |
+ } |
|
| 231 |
+ |
|
| 232 |
+ if reqTo.Header.Get("Authorization") != "super_secret" {
|
|
| 233 |
+ t.Fatal("'Authorization' should be 'super_secret'")
|
|
| 234 |
+ } |
|
| 235 |
+ } |
|
| 236 |
+} |
| 0 | 237 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,248 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ // this is required for some certificates |
|
| 4 |
+ "context" |
|
| 5 |
+ _ "crypto/sha512" |
|
| 6 |
+ "encoding/json" |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "io" |
|
| 9 |
+ "net/http" |
|
| 10 |
+ "net/http/cookiejar" |
|
| 11 |
+ "net/url" |
|
| 12 |
+ "strconv" |
|
| 13 |
+ "strings" |
|
| 14 |
+ "sync" |
|
| 15 |
+ |
|
| 16 |
+ "github.com/containerd/log" |
|
| 17 |
+ "github.com/moby/moby/api/types/registry" |
|
| 18 |
+ "github.com/pkg/errors" |
|
| 19 |
+) |
|
| 20 |
+ |
|
| 21 |
+// A session is used to communicate with a V1 registry |
|
| 22 |
+type session struct {
|
|
| 23 |
+ indexEndpoint *v1Endpoint |
|
| 24 |
+ client *http.Client |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+type authTransport struct {
|
|
| 28 |
+ base http.RoundTripper |
|
| 29 |
+ authConfig *registry.AuthConfig |
|
| 30 |
+ |
|
| 31 |
+ alwaysSetBasicAuth bool |
|
| 32 |
+ token []string |
|
| 33 |
+ |
|
| 34 |
+ mu sync.Mutex // guards modReq |
|
| 35 |
+ modReq map[*http.Request]*http.Request // original -> modified |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official) |
|
| 39 |
+// |
|
| 40 |
+// For private v1 registries, set alwaysSetBasicAuth to true. |
|
| 41 |
+// |
|
| 42 |
+// For the official v1 registry, if there isn't already an Authorization header in the request, |
|
| 43 |
+// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. |
|
| 44 |
+// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing |
|
| 45 |
+// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent |
|
| 46 |
+// requests. |
|
| 47 |
+// |
|
| 48 |
+// If the server sends a token without the client having requested it, it is ignored. |
|
| 49 |
+// |
|
| 50 |
+// This RoundTripper also has a CancelRequest method important for correct timeout handling. |
|
| 51 |
+func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
|
|
| 52 |
+ if base == nil {
|
|
| 53 |
+ base = http.DefaultTransport |
|
| 54 |
+ } |
|
| 55 |
+ return &authTransport{
|
|
| 56 |
+ base: base, |
|
| 57 |
+ authConfig: authConfig, |
|
| 58 |
+ alwaysSetBasicAuth: alwaysSetBasicAuth, |
|
| 59 |
+ modReq: make(map[*http.Request]*http.Request), |
|
| 60 |
+ } |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+// cloneRequest returns a clone of the provided *http.Request. |
|
| 64 |
+// The clone is a shallow copy of the struct and its Header map. |
|
| 65 |
+func cloneRequest(r *http.Request) *http.Request {
|
|
| 66 |
+ // shallow copy of the struct |
|
| 67 |
+ r2 := new(http.Request) |
|
| 68 |
+ *r2 = *r |
|
| 69 |
+ // deep copy of the Header |
|
| 70 |
+ r2.Header = make(http.Header, len(r.Header)) |
|
| 71 |
+ for k, s := range r.Header {
|
|
| 72 |
+ r2.Header[k] = append([]string(nil), s...) |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ return r2 |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+// onEOFReader wraps an io.ReadCloser and a function |
|
| 79 |
+// the function will run at the end of file or close the file. |
|
| 80 |
+type onEOFReader struct {
|
|
| 81 |
+ Rc io.ReadCloser |
|
| 82 |
+ Fn func() |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+func (r *onEOFReader) Read(p []byte) (int, error) {
|
|
| 86 |
+ n, err := r.Rc.Read(p) |
|
| 87 |
+ if err == io.EOF {
|
|
| 88 |
+ r.runFunc() |
|
| 89 |
+ } |
|
| 90 |
+ return n, err |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+// Close closes the file and run the function. |
|
| 94 |
+func (r *onEOFReader) Close() error {
|
|
| 95 |
+ err := r.Rc.Close() |
|
| 96 |
+ r.runFunc() |
|
| 97 |
+ return err |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+func (r *onEOFReader) runFunc() {
|
|
| 101 |
+ if fn := r.Fn; fn != nil {
|
|
| 102 |
+ fn() |
|
| 103 |
+ r.Fn = nil |
|
| 104 |
+ } |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+// RoundTrip changes an HTTP request's headers to add the necessary |
|
| 108 |
+// authentication-related headers |
|
| 109 |
+func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
|
| 110 |
+ // Authorization should not be set on 302 redirect for untrusted locations. |
|
| 111 |
+ // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. |
|
| 112 |
+ // As the authorization logic is currently implemented in RoundTrip, |
|
| 113 |
+ // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. |
|
| 114 |
+ // This is safe as Docker doesn't set Referrer in other scenarios. |
|
| 115 |
+ if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
|
|
| 116 |
+ return tr.base.RoundTrip(orig) |
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ req := cloneRequest(orig) |
|
| 120 |
+ tr.mu.Lock() |
|
| 121 |
+ tr.modReq[orig] = req |
|
| 122 |
+ tr.mu.Unlock() |
|
| 123 |
+ |
|
| 124 |
+ if tr.alwaysSetBasicAuth {
|
|
| 125 |
+ if tr.authConfig == nil {
|
|
| 126 |
+ return nil, errors.New("unexpected error: empty auth config")
|
|
| 127 |
+ } |
|
| 128 |
+ req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) |
|
| 129 |
+ return tr.base.RoundTrip(req) |
|
| 130 |
+ } |
|
| 131 |
+ |
|
| 132 |
+ // Don't override |
|
| 133 |
+ if req.Header.Get("Authorization") == "" {
|
|
| 134 |
+ if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" {
|
|
| 135 |
+ req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) |
|
| 136 |
+ } else if len(tr.token) > 0 {
|
|
| 137 |
+ req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
|
|
| 138 |
+ } |
|
| 139 |
+ } |
|
| 140 |
+ resp, err := tr.base.RoundTrip(req) |
|
| 141 |
+ if err != nil {
|
|
| 142 |
+ tr.mu.Lock() |
|
| 143 |
+ delete(tr.modReq, orig) |
|
| 144 |
+ tr.mu.Unlock() |
|
| 145 |
+ return nil, err |
|
| 146 |
+ } |
|
| 147 |
+ if len(resp.Header["X-Docker-Token"]) > 0 {
|
|
| 148 |
+ tr.token = resp.Header["X-Docker-Token"] |
|
| 149 |
+ } |
|
| 150 |
+ resp.Body = &onEOFReader{
|
|
| 151 |
+ Rc: resp.Body, |
|
| 152 |
+ Fn: func() {
|
|
| 153 |
+ tr.mu.Lock() |
|
| 154 |
+ delete(tr.modReq, orig) |
|
| 155 |
+ tr.mu.Unlock() |
|
| 156 |
+ }, |
|
| 157 |
+ } |
|
| 158 |
+ return resp, nil |
|
| 159 |
+} |
|
| 160 |
+ |
|
| 161 |
+// CancelRequest cancels an in-flight request by closing its connection. |
|
| 162 |
+func (tr *authTransport) CancelRequest(req *http.Request) {
|
|
| 163 |
+ type canceler interface {
|
|
| 164 |
+ CancelRequest(*http.Request) |
|
| 165 |
+ } |
|
| 166 |
+ if cr, ok := tr.base.(canceler); ok {
|
|
| 167 |
+ tr.mu.Lock() |
|
| 168 |
+ modReq := tr.modReq[req] |
|
| 169 |
+ delete(tr.modReq, req) |
|
| 170 |
+ tr.mu.Unlock() |
|
| 171 |
+ cr.CancelRequest(modReq) |
|
| 172 |
+ } |
|
| 173 |
+} |
|
| 174 |
+ |
|
| 175 |
+func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
|
|
| 176 |
+ var alwaysSetBasicAuth bool |
|
| 177 |
+ |
|
| 178 |
+ // If we're working with a standalone private registry over HTTPS, send Basic Auth headers |
|
| 179 |
+ // alongside all our requests. |
|
| 180 |
+ if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
|
|
| 181 |
+ info, err := endpoint.ping(ctx) |
|
| 182 |
+ if err != nil {
|
|
| 183 |
+ return err |
|
| 184 |
+ } |
|
| 185 |
+ if info.Standalone && authConfig != nil {
|
|
| 186 |
+ log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth")
|
|
| 187 |
+ alwaysSetBasicAuth = true |
|
| 188 |
+ } |
|
| 189 |
+ } |
|
| 190 |
+ |
|
| 191 |
+ // Annotate the transport unconditionally so that v2 can |
|
| 192 |
+ // properly fallback on v1 when an image is not found. |
|
| 193 |
+ client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) |
|
| 194 |
+ |
|
| 195 |
+ jar, err := cookiejar.New(nil) |
|
| 196 |
+ if err != nil {
|
|
| 197 |
+ return systemErr{errors.New("cookiejar.New is not supposed to return an error")}
|
|
| 198 |
+ } |
|
| 199 |
+ client.Jar = jar |
|
| 200 |
+ |
|
| 201 |
+ return nil |
|
| 202 |
+} |
|
| 203 |
+ |
|
| 204 |
+func newSession(client *http.Client, endpoint *v1Endpoint) *session {
|
|
| 205 |
+ return &session{
|
|
| 206 |
+ client: client, |
|
| 207 |
+ indexEndpoint: endpoint, |
|
| 208 |
+ } |
|
| 209 |
+} |
|
| 210 |
+ |
|
| 211 |
+// defaultSearchLimit is the default value for maximum number of returned search results. |
|
| 212 |
+const defaultSearchLimit = 25 |
|
| 213 |
+ |
|
| 214 |
+// searchRepositories performs a search against the remote repository |
|
| 215 |
+func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
|
|
| 216 |
+ if limit == 0 {
|
|
| 217 |
+ limit = defaultSearchLimit |
|
| 218 |
+ } |
|
| 219 |
+ if limit < 1 || limit > 100 {
|
|
| 220 |
+ return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
|
|
| 221 |
+ } |
|
| 222 |
+ u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit)) |
|
| 223 |
+ log.G(ctx).WithField("url", u).Debug("searchRepositories")
|
|
| 224 |
+ |
|
| 225 |
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) |
|
| 226 |
+ if err != nil {
|
|
| 227 |
+ return nil, invalidParamWrapf(err, "error building request") |
|
| 228 |
+ } |
|
| 229 |
+ // Have the AuthTransport send authentication, when logged in. |
|
| 230 |
+ req.Header.Set("X-Docker-Token", "true")
|
|
| 231 |
+ res, err := r.client.Do(req) |
|
| 232 |
+ if err != nil {
|
|
| 233 |
+ return nil, systemErr{err}
|
|
| 234 |
+ } |
|
| 235 |
+ defer res.Body.Close() |
|
| 236 |
+ if res.StatusCode != http.StatusOK {
|
|
| 237 |
+ // TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286). |
|
| 238 |
+ // TODO(thaJeztah): handle other status-codes to return correct error-type |
|
| 239 |
+ return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)}
|
|
| 240 |
+ } |
|
| 241 |
+ result := ®istry.SearchResults{}
|
|
| 242 |
+ err = json.NewDecoder(res.Body).Decode(result) |
|
| 243 |
+ if err != nil {
|
|
| 244 |
+ return nil, systemErr{errors.Wrap(err, "error decoding registry search results")}
|
|
| 245 |
+ } |
|
| 246 |
+ return result, nil |
|
| 247 |
+} |
| 0 | 248 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,418 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "net/http" |
|
| 6 |
+ "net/http/httptest" |
|
| 7 |
+ "net/http/httputil" |
|
| 8 |
+ "testing" |
|
| 9 |
+ |
|
| 10 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 11 |
+ "github.com/docker/distribution/registry/client/transport" |
|
| 12 |
+ "github.com/moby/moby/api/types/filters" |
|
| 13 |
+ "github.com/moby/moby/api/types/registry" |
|
| 14 |
+ "gotest.tools/v3/assert" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+func spawnTestRegistrySession(t *testing.T) *session {
|
|
| 18 |
+ t.Helper() |
|
| 19 |
+ authConfig := ®istry.AuthConfig{}
|
|
| 20 |
+ endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil)
|
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ t.Fatal(err) |
|
| 23 |
+ } |
|
| 24 |
+ userAgent := "docker test client" |
|
| 25 |
+ var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
|
|
| 26 |
+ tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...) |
|
| 27 |
+ client := httpClient(tr) |
|
| 28 |
+ |
|
| 29 |
+ if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil {
|
|
| 30 |
+ t.Fatal(err) |
|
| 31 |
+ } |
|
| 32 |
+ r := newSession(client, endpoint) |
|
| 33 |
+ |
|
| 34 |
+ // In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true` |
|
| 35 |
+ // header while authenticating, in order to retrieve a token that can be later used to |
|
| 36 |
+ // perform authenticated actions. |
|
| 37 |
+ // |
|
| 38 |
+ // The mock v1 registry does not support that, (TODO(tiborvass): support it), instead, |
|
| 39 |
+ // it will consider authenticated any request with the header `X-Docker-Token: fake-token`. |
|
| 40 |
+ // |
|
| 41 |
+ // Because we know that the client's transport is an `*authTransport` we simply cast it, |
|
| 42 |
+ // in order to set the internal cached token to the fake token, and thus send that fake token |
|
| 43 |
+ // upon every subsequent requests. |
|
| 44 |
+ r.client.Transport.(*authTransport).token = []string{"fake-token"}
|
|
| 45 |
+ return r |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+type debugTransport struct {
|
|
| 49 |
+ http.RoundTripper |
|
| 50 |
+ log func(...interface{})
|
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
| 54 |
+ dump, err := httputil.DumpRequestOut(req, false) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ tr.log("could not dump request")
|
|
| 57 |
+ } |
|
| 58 |
+ tr.log(string(dump)) |
|
| 59 |
+ resp, err := tr.RoundTripper.RoundTrip(req) |
|
| 60 |
+ if err != nil {
|
|
| 61 |
+ return nil, err |
|
| 62 |
+ } |
|
| 63 |
+ dump, err = httputil.DumpResponse(resp, false) |
|
| 64 |
+ if err != nil {
|
|
| 65 |
+ tr.log("could not dump response")
|
|
| 66 |
+ } |
|
| 67 |
+ tr.log(string(dump)) |
|
| 68 |
+ return resp, err |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+func TestSearchRepositories(t *testing.T) {
|
|
| 72 |
+ r := spawnTestRegistrySession(t) |
|
| 73 |
+ results, err := r.searchRepositories(context.Background(), "fakequery", 25) |
|
| 74 |
+ if err != nil {
|
|
| 75 |
+ t.Fatal(err) |
|
| 76 |
+ } |
|
| 77 |
+ if results == nil {
|
|
| 78 |
+ t.Fatal("Expected non-nil SearchResults object")
|
|
| 79 |
+ } |
|
| 80 |
+ assert.Equal(t, results.NumResults, 1, "Expected 1 search results") |
|
| 81 |
+ assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query") |
|
| 82 |
+ assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+func TestSearchErrors(t *testing.T) {
|
|
| 86 |
+ errorCases := []struct {
|
|
| 87 |
+ filtersArgs filters.Args |
|
| 88 |
+ shouldReturnError bool |
|
| 89 |
+ expectedError string |
|
| 90 |
+ }{
|
|
| 91 |
+ {
|
|
| 92 |
+ expectedError: "unexpected status code 500", |
|
| 93 |
+ shouldReturnError: true, |
|
| 94 |
+ }, |
|
| 95 |
+ {
|
|
| 96 |
+ filtersArgs: filters.NewArgs(filters.Arg("type", "custom")),
|
|
| 97 |
+ expectedError: "invalid filter 'type'", |
|
| 98 |
+ }, |
|
| 99 |
+ {
|
|
| 100 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-automated", "invalid")),
|
|
| 101 |
+ expectedError: "invalid filter 'is-automated=[invalid]'", |
|
| 102 |
+ }, |
|
| 103 |
+ {
|
|
| 104 |
+ filtersArgs: filters.NewArgs( |
|
| 105 |
+ filters.Arg("is-automated", "true"),
|
|
| 106 |
+ filters.Arg("is-automated", "false"),
|
|
| 107 |
+ ), |
|
| 108 |
+ expectedError: "invalid filter 'is-automated", |
|
| 109 |
+ }, |
|
| 110 |
+ {
|
|
| 111 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-official", "invalid")),
|
|
| 112 |
+ expectedError: "invalid filter 'is-official=[invalid]'", |
|
| 113 |
+ }, |
|
| 114 |
+ {
|
|
| 115 |
+ filtersArgs: filters.NewArgs( |
|
| 116 |
+ filters.Arg("is-official", "true"),
|
|
| 117 |
+ filters.Arg("is-official", "false"),
|
|
| 118 |
+ ), |
|
| 119 |
+ expectedError: "invalid filter 'is-official", |
|
| 120 |
+ }, |
|
| 121 |
+ {
|
|
| 122 |
+ filtersArgs: filters.NewArgs(filters.Arg("stars", "invalid")),
|
|
| 123 |
+ expectedError: "invalid filter 'stars=invalid'", |
|
| 124 |
+ }, |
|
| 125 |
+ {
|
|
| 126 |
+ filtersArgs: filters.NewArgs( |
|
| 127 |
+ filters.Arg("stars", "1"),
|
|
| 128 |
+ filters.Arg("stars", "invalid"),
|
|
| 129 |
+ ), |
|
| 130 |
+ expectedError: "invalid filter 'stars=invalid'", |
|
| 131 |
+ }, |
|
| 132 |
+ } |
|
| 133 |
+ for _, tc := range errorCases {
|
|
| 134 |
+ t.Run(tc.expectedError, func(t *testing.T) {
|
|
| 135 |
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 136 |
+ if !tc.shouldReturnError {
|
|
| 137 |
+ t.Errorf("unexpected HTTP request")
|
|
| 138 |
+ } |
|
| 139 |
+ http.Error(w, "no search for you", http.StatusInternalServerError) |
|
| 140 |
+ })) |
|
| 141 |
+ defer srv.Close() |
|
| 142 |
+ |
|
| 143 |
+ // Construct the search term by cutting the 'http://' prefix off srv.URL. |
|
| 144 |
+ term := srv.URL[7:] + "/term" |
|
| 145 |
+ |
|
| 146 |
+ reg, err := NewService(ServiceOptions{})
|
|
| 147 |
+ assert.NilError(t, err) |
|
| 148 |
+ _, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
|
|
| 149 |
+ assert.ErrorContains(t, err, tc.expectedError) |
|
| 150 |
+ if tc.shouldReturnError {
|
|
| 151 |
+ assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err) |
|
| 152 |
+ return |
|
| 153 |
+ } |
|
| 154 |
+ assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err) |
|
| 155 |
+ }) |
|
| 156 |
+ } |
|
| 157 |
+} |
|
| 158 |
+ |
|
| 159 |
+func TestSearch(t *testing.T) {
|
|
| 160 |
+ const term = "term" |
|
| 161 |
+ successCases := []struct {
|
|
| 162 |
+ name string |
|
| 163 |
+ filtersArgs filters.Args |
|
| 164 |
+ registryResults []registry.SearchResult |
|
| 165 |
+ expectedResults []registry.SearchResult |
|
| 166 |
+ }{
|
|
| 167 |
+ {
|
|
| 168 |
+ name: "empty results", |
|
| 169 |
+ registryResults: []registry.SearchResult{},
|
|
| 170 |
+ expectedResults: []registry.SearchResult{},
|
|
| 171 |
+ }, |
|
| 172 |
+ {
|
|
| 173 |
+ name: "no filter", |
|
| 174 |
+ registryResults: []registry.SearchResult{
|
|
| 175 |
+ {
|
|
| 176 |
+ Name: "name", |
|
| 177 |
+ Description: "description", |
|
| 178 |
+ }, |
|
| 179 |
+ }, |
|
| 180 |
+ expectedResults: []registry.SearchResult{
|
|
| 181 |
+ {
|
|
| 182 |
+ Name: "name", |
|
| 183 |
+ Description: "description", |
|
| 184 |
+ }, |
|
| 185 |
+ }, |
|
| 186 |
+ }, |
|
| 187 |
+ {
|
|
| 188 |
+ name: "is-automated=true, no results", |
|
| 189 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
|
| 190 |
+ registryResults: []registry.SearchResult{
|
|
| 191 |
+ {
|
|
| 192 |
+ Name: "name", |
|
| 193 |
+ Description: "description", |
|
| 194 |
+ }, |
|
| 195 |
+ }, |
|
| 196 |
+ expectedResults: []registry.SearchResult{},
|
|
| 197 |
+ }, |
|
| 198 |
+ {
|
|
| 199 |
+ name: "is-automated=true", |
|
| 200 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
|
| 201 |
+ registryResults: []registry.SearchResult{
|
|
| 202 |
+ {
|
|
| 203 |
+ Name: "name", |
|
| 204 |
+ Description: "description", |
|
| 205 |
+ IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 206 |
+ }, |
|
| 207 |
+ }, |
|
| 208 |
+ expectedResults: []registry.SearchResult{},
|
|
| 209 |
+ }, |
|
| 210 |
+ {
|
|
| 211 |
+ name: "is-automated=false, IsAutomated reset to false", |
|
| 212 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
|
| 213 |
+ registryResults: []registry.SearchResult{
|
|
| 214 |
+ {
|
|
| 215 |
+ Name: "name", |
|
| 216 |
+ Description: "description", |
|
| 217 |
+ IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 218 |
+ }, |
|
| 219 |
+ }, |
|
| 220 |
+ expectedResults: []registry.SearchResult{
|
|
| 221 |
+ {
|
|
| 222 |
+ Name: "name", |
|
| 223 |
+ Description: "description", |
|
| 224 |
+ IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 225 |
+ }, |
|
| 226 |
+ }, |
|
| 227 |
+ }, |
|
| 228 |
+ {
|
|
| 229 |
+ name: "is-automated=false", |
|
| 230 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
|
| 231 |
+ registryResults: []registry.SearchResult{
|
|
| 232 |
+ {
|
|
| 233 |
+ Name: "name", |
|
| 234 |
+ Description: "description", |
|
| 235 |
+ }, |
|
| 236 |
+ }, |
|
| 237 |
+ expectedResults: []registry.SearchResult{
|
|
| 238 |
+ {
|
|
| 239 |
+ Name: "name", |
|
| 240 |
+ Description: "description", |
|
| 241 |
+ }, |
|
| 242 |
+ }, |
|
| 243 |
+ }, |
|
| 244 |
+ {
|
|
| 245 |
+ name: "is-official=true, no results", |
|
| 246 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
|
| 247 |
+ registryResults: []registry.SearchResult{
|
|
| 248 |
+ {
|
|
| 249 |
+ Name: "name", |
|
| 250 |
+ Description: "description", |
|
| 251 |
+ }, |
|
| 252 |
+ }, |
|
| 253 |
+ expectedResults: []registry.SearchResult{},
|
|
| 254 |
+ }, |
|
| 255 |
+ {
|
|
| 256 |
+ name: "is-official=true", |
|
| 257 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
|
| 258 |
+ registryResults: []registry.SearchResult{
|
|
| 259 |
+ {
|
|
| 260 |
+ Name: "name", |
|
| 261 |
+ Description: "description", |
|
| 262 |
+ IsOfficial: true, |
|
| 263 |
+ }, |
|
| 264 |
+ }, |
|
| 265 |
+ expectedResults: []registry.SearchResult{
|
|
| 266 |
+ {
|
|
| 267 |
+ Name: "name", |
|
| 268 |
+ Description: "description", |
|
| 269 |
+ IsOfficial: true, |
|
| 270 |
+ }, |
|
| 271 |
+ }, |
|
| 272 |
+ }, |
|
| 273 |
+ {
|
|
| 274 |
+ name: "is-official=false, no results", |
|
| 275 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
|
| 276 |
+ registryResults: []registry.SearchResult{
|
|
| 277 |
+ {
|
|
| 278 |
+ Name: "name", |
|
| 279 |
+ Description: "description", |
|
| 280 |
+ IsOfficial: true, |
|
| 281 |
+ }, |
|
| 282 |
+ }, |
|
| 283 |
+ expectedResults: []registry.SearchResult{},
|
|
| 284 |
+ }, |
|
| 285 |
+ {
|
|
| 286 |
+ name: "is-official=false", |
|
| 287 |
+ filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
|
| 288 |
+ registryResults: []registry.SearchResult{
|
|
| 289 |
+ {
|
|
| 290 |
+ Name: "name", |
|
| 291 |
+ Description: "description", |
|
| 292 |
+ IsOfficial: false, |
|
| 293 |
+ }, |
|
| 294 |
+ }, |
|
| 295 |
+ expectedResults: []registry.SearchResult{
|
|
| 296 |
+ {
|
|
| 297 |
+ Name: "name", |
|
| 298 |
+ Description: "description", |
|
| 299 |
+ IsOfficial: false, |
|
| 300 |
+ }, |
|
| 301 |
+ }, |
|
| 302 |
+ }, |
|
| 303 |
+ {
|
|
| 304 |
+ name: "stars=0", |
|
| 305 |
+ filtersArgs: filters.NewArgs(filters.Arg("stars", "0")),
|
|
| 306 |
+ registryResults: []registry.SearchResult{
|
|
| 307 |
+ {
|
|
| 308 |
+ Name: "name", |
|
| 309 |
+ Description: "description", |
|
| 310 |
+ StarCount: 0, |
|
| 311 |
+ }, |
|
| 312 |
+ }, |
|
| 313 |
+ expectedResults: []registry.SearchResult{
|
|
| 314 |
+ {
|
|
| 315 |
+ Name: "name", |
|
| 316 |
+ Description: "description", |
|
| 317 |
+ StarCount: 0, |
|
| 318 |
+ }, |
|
| 319 |
+ }, |
|
| 320 |
+ }, |
|
| 321 |
+ {
|
|
| 322 |
+ name: "stars=0, no results", |
|
| 323 |
+ filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
|
| 324 |
+ registryResults: []registry.SearchResult{
|
|
| 325 |
+ {
|
|
| 326 |
+ Name: "name", |
|
| 327 |
+ Description: "description", |
|
| 328 |
+ StarCount: 0, |
|
| 329 |
+ }, |
|
| 330 |
+ }, |
|
| 331 |
+ expectedResults: []registry.SearchResult{},
|
|
| 332 |
+ }, |
|
| 333 |
+ {
|
|
| 334 |
+ name: "stars=1", |
|
| 335 |
+ filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
|
| 336 |
+ registryResults: []registry.SearchResult{
|
|
| 337 |
+ {
|
|
| 338 |
+ Name: "name0", |
|
| 339 |
+ Description: "description0", |
|
| 340 |
+ StarCount: 0, |
|
| 341 |
+ }, |
|
| 342 |
+ {
|
|
| 343 |
+ Name: "name1", |
|
| 344 |
+ Description: "description1", |
|
| 345 |
+ StarCount: 1, |
|
| 346 |
+ }, |
|
| 347 |
+ }, |
|
| 348 |
+ expectedResults: []registry.SearchResult{
|
|
| 349 |
+ {
|
|
| 350 |
+ Name: "name1", |
|
| 351 |
+ Description: "description1", |
|
| 352 |
+ StarCount: 1, |
|
| 353 |
+ }, |
|
| 354 |
+ }, |
|
| 355 |
+ }, |
|
| 356 |
+ {
|
|
| 357 |
+ name: "stars=1, is-official=true, is-automated=true", |
|
| 358 |
+ filtersArgs: filters.NewArgs( |
|
| 359 |
+ filters.Arg("stars", "1"),
|
|
| 360 |
+ filters.Arg("is-official", "true"),
|
|
| 361 |
+ filters.Arg("is-automated", "true"),
|
|
| 362 |
+ ), |
|
| 363 |
+ registryResults: []registry.SearchResult{
|
|
| 364 |
+ {
|
|
| 365 |
+ Name: "name0", |
|
| 366 |
+ Description: "description0", |
|
| 367 |
+ StarCount: 0, |
|
| 368 |
+ IsOfficial: true, |
|
| 369 |
+ IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 370 |
+ }, |
|
| 371 |
+ {
|
|
| 372 |
+ Name: "name1", |
|
| 373 |
+ Description: "description1", |
|
| 374 |
+ StarCount: 1, |
|
| 375 |
+ IsOfficial: false, |
|
| 376 |
+ IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 377 |
+ }, |
|
| 378 |
+ {
|
|
| 379 |
+ Name: "name2", |
|
| 380 |
+ Description: "description2", |
|
| 381 |
+ StarCount: 1, |
|
| 382 |
+ IsOfficial: true, |
|
| 383 |
+ }, |
|
| 384 |
+ {
|
|
| 385 |
+ Name: "name3", |
|
| 386 |
+ Description: "description3", |
|
| 387 |
+ StarCount: 2, |
|
| 388 |
+ IsOfficial: true, |
|
| 389 |
+ IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 390 |
+ }, |
|
| 391 |
+ }, |
|
| 392 |
+ expectedResults: []registry.SearchResult{},
|
|
| 393 |
+ }, |
|
| 394 |
+ } |
|
| 395 |
+ for _, tc := range successCases {
|
|
| 396 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 397 |
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 398 |
+ w.Header().Set("Content-type", "application/json")
|
|
| 399 |
+ json.NewEncoder(w).Encode(registry.SearchResults{
|
|
| 400 |
+ Query: term, |
|
| 401 |
+ NumResults: len(tc.registryResults), |
|
| 402 |
+ Results: tc.registryResults, |
|
| 403 |
+ }) |
|
| 404 |
+ })) |
|
| 405 |
+ defer srv.Close() |
|
| 406 |
+ |
|
| 407 |
+ // Construct the search term by cutting the 'http://' prefix off srv.URL. |
|
| 408 |
+ searchTerm := srv.URL[7:] + "/" + term |
|
| 409 |
+ |
|
| 410 |
+ reg, err := NewService(ServiceOptions{})
|
|
| 411 |
+ assert.NilError(t, err) |
|
| 412 |
+ results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
|
|
| 413 |
+ assert.NilError(t, err) |
|
| 414 |
+ assert.DeepEqual(t, results, tc.expectedResults) |
|
| 415 |
+ }) |
|
| 416 |
+ } |
|
| 417 |
+} |
| 0 | 418 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,160 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "crypto/tls" |
|
| 5 |
+ "errors" |
|
| 6 |
+ "net/url" |
|
| 7 |
+ "strings" |
|
| 8 |
+ "sync" |
|
| 9 |
+ |
|
| 10 |
+ cerrdefs "github.com/containerd/errdefs" |
|
| 11 |
+ "github.com/containerd/log" |
|
| 12 |
+ "github.com/distribution/reference" |
|
| 13 |
+ "github.com/moby/moby/api/types/registry" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+// Service is a registry service. It tracks configuration data such as a list |
|
| 17 |
+// of mirrors. |
|
| 18 |
+type Service struct {
|
|
| 19 |
+ config *serviceConfig |
|
| 20 |
+ mu sync.RWMutex |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// NewService returns a new instance of [Service] ready to be installed into |
|
| 24 |
+// an engine. |
|
| 25 |
+func NewService(options ServiceOptions) (*Service, error) {
|
|
| 26 |
+ config, err := newServiceConfig(options) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return nil, err |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ return &Service{config: config}, err
|
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// ServiceConfig returns a copy of the public registry service's configuration. |
|
| 35 |
+func (s *Service) ServiceConfig() *registry.ServiceConfig {
|
|
| 36 |
+ s.mu.RLock() |
|
| 37 |
+ defer s.mu.RUnlock() |
|
| 38 |
+ return s.config.copy() |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+// ReplaceConfig prepares a transaction which will atomically replace the |
|
| 42 |
+// registry service's configuration when the returned commit function is called. |
|
| 43 |
+func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) {
|
|
| 44 |
+ config, err := newServiceConfig(options) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return nil, err |
|
| 47 |
+ } |
|
| 48 |
+ return func() {
|
|
| 49 |
+ s.mu.Lock() |
|
| 50 |
+ defer s.mu.Unlock() |
|
| 51 |
+ s.config = config |
|
| 52 |
+ }, nil |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+// Auth contacts the public registry with the provided credentials, |
|
| 56 |
+// and returns OK if authentication was successful. |
|
| 57 |
+// It can be used to verify the validity of a client's credentials. |
|
| 58 |
+func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) {
|
|
| 59 |
+ // TODO Use ctx when searching for repositories |
|
| 60 |
+ registryHostName := IndexHostname |
|
| 61 |
+ |
|
| 62 |
+ if authConfig.ServerAddress != "" {
|
|
| 63 |
+ serverAddress := authConfig.ServerAddress |
|
| 64 |
+ if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
|
| 65 |
+ serverAddress = "https://" + serverAddress |
|
| 66 |
+ } |
|
| 67 |
+ u, err := url.Parse(serverAddress) |
|
| 68 |
+ if err != nil {
|
|
| 69 |
+ return "", "", invalidParamWrapf(err, "unable to parse server address") |
|
| 70 |
+ } |
|
| 71 |
+ registryHostName = u.Host |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ // Lookup endpoints for authentication but exclude mirrors to prevent |
|
| 75 |
+ // sending credentials of the upstream registry to a mirror. |
|
| 76 |
+ s.mu.RLock() |
|
| 77 |
+ endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false) |
|
| 78 |
+ s.mu.RUnlock() |
|
| 79 |
+ if err != nil {
|
|
| 80 |
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
| 81 |
+ return "", "", err |
|
| 82 |
+ } |
|
| 83 |
+ return "", "", invalidParam(err) |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ var lastErr error |
|
| 87 |
+ for _, endpoint := range endpoints {
|
|
| 88 |
+ authToken, err := loginV2(ctx, authConfig, endpoint, userAgent) |
|
| 89 |
+ if err != nil {
|
|
| 90 |
+ if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
|
|
| 91 |
+ // Failed to authenticate; don't continue with (non-TLS) endpoints. |
|
| 92 |
+ return "", "", err |
|
| 93 |
+ } |
|
| 94 |
+ // Try next endpoint |
|
| 95 |
+ log.G(ctx).WithFields(log.Fields{
|
|
| 96 |
+ "error": err, |
|
| 97 |
+ "endpoint": endpoint, |
|
| 98 |
+ }).Infof("Error logging in to endpoint, trying next endpoint")
|
|
| 99 |
+ lastErr = err |
|
| 100 |
+ continue |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ // TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here? |
|
| 104 |
+ return "Login Succeeded", authToken, nil |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ return "", "", lastErr |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+// ResolveAuthConfig looks up authentication for the given reference from the |
|
| 111 |
+// given authConfigs. |
|
| 112 |
+// |
|
| 113 |
+// IMPORTANT: This function is for internal use and should not be used by external projects. |
|
| 114 |
+func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
|
|
| 115 |
+ s.mu.RLock() |
|
| 116 |
+ defer s.mu.RUnlock() |
|
| 117 |
+ // Simplified version of "newIndexInfo" without handling of insecure |
|
| 118 |
+ // registries and mirrors, as we don't need that information to resolve |
|
| 119 |
+ // the auth-config. |
|
| 120 |
+ indexName := normalizeIndexName(reference.Domain(ref)) |
|
| 121 |
+ registryInfo, ok := s.config.IndexConfigs[indexName] |
|
| 122 |
+ if !ok {
|
|
| 123 |
+ registryInfo = ®istry.IndexInfo{Name: indexName}
|
|
| 124 |
+ } |
|
| 125 |
+ return ResolveAuthConfig(authConfigs, registryInfo) |
|
| 126 |
+} |
|
| 127 |
+ |
|
| 128 |
+// APIEndpoint represents a remote API endpoint |
|
| 129 |
+type APIEndpoint struct {
|
|
| 130 |
+ Mirror bool |
|
| 131 |
+ URL *url.URL |
|
| 132 |
+ TLSConfig *tls.Config |
|
| 133 |
+} |
|
| 134 |
+ |
|
| 135 |
+// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference. |
|
| 136 |
+// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP. |
|
| 137 |
+func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
|
|
| 138 |
+ s.mu.RLock() |
|
| 139 |
+ defer s.mu.RUnlock() |
|
| 140 |
+ |
|
| 141 |
+ return s.lookupV2Endpoints(context.TODO(), hostname, true) |
|
| 142 |
+} |
|
| 143 |
+ |
|
| 144 |
+// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference. |
|
| 145 |
+// It gives preference to HTTPS over plain HTTP. Mirrors are not included. |
|
| 146 |
+func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
|
|
| 147 |
+ s.mu.RLock() |
|
| 148 |
+ defer s.mu.RUnlock() |
|
| 149 |
+ |
|
| 150 |
+ return s.lookupV2Endpoints(context.TODO(), hostname, false) |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+// IsInsecureRegistry returns true if the registry at given host is configured as |
|
| 154 |
+// insecure registry. |
|
| 155 |
+func (s *Service) IsInsecureRegistry(host string) bool {
|
|
| 156 |
+ s.mu.RLock() |
|
| 157 |
+ defer s.mu.RUnlock() |
|
| 158 |
+ return !s.config.isSecureIndex(host) |
|
| 159 |
+} |
| 0 | 160 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,73 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ "strings" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/go-connections/tlsconfig" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) {
|
|
| 11 |
+ var endpoints []APIEndpoint |
|
| 12 |
+ if hostname == DefaultNamespace || hostname == IndexHostname {
|
|
| 13 |
+ if includeMirrors {
|
|
| 14 |
+ for _, mirror := range s.config.Mirrors {
|
|
| 15 |
+ if ctx.Err() != nil {
|
|
| 16 |
+ return nil, ctx.Err() |
|
| 17 |
+ } |
|
| 18 |
+ if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
|
|
| 19 |
+ mirror = "https://" + mirror |
|
| 20 |
+ } |
|
| 21 |
+ mirrorURL, err := url.Parse(mirror) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return nil, invalidParam(err) |
|
| 24 |
+ } |
|
| 25 |
+ // TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time. |
|
| 26 |
+ mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host)) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return nil, err |
|
| 29 |
+ } |
|
| 30 |
+ endpoints = append(endpoints, APIEndpoint{
|
|
| 31 |
+ URL: mirrorURL, |
|
| 32 |
+ Mirror: true, |
|
| 33 |
+ TLSConfig: mirrorTLSConfig, |
|
| 34 |
+ }) |
|
| 35 |
+ } |
|
| 36 |
+ } |
|
| 37 |
+ endpoints = append(endpoints, APIEndpoint{
|
|
| 38 |
+ URL: DefaultV2Registry, |
|
| 39 |
+ TLSConfig: tlsconfig.ServerDefault(), |
|
| 40 |
+ }) |
|
| 41 |
+ |
|
| 42 |
+ return endpoints, nil |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname)) |
|
| 46 |
+ if err != nil {
|
|
| 47 |
+ return nil, err |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ endpoints = []APIEndpoint{
|
|
| 51 |
+ {
|
|
| 52 |
+ URL: &url.URL{
|
|
| 53 |
+ Scheme: "https", |
|
| 54 |
+ Host: hostname, |
|
| 55 |
+ }, |
|
| 56 |
+ TLSConfig: tlsConfig, |
|
| 57 |
+ }, |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ if tlsConfig.InsecureSkipVerify {
|
|
| 61 |
+ endpoints = append(endpoints, APIEndpoint{
|
|
| 62 |
+ URL: &url.URL{
|
|
| 63 |
+ Scheme: "http", |
|
| 64 |
+ Host: hostname, |
|
| 65 |
+ }, |
|
| 66 |
+ // used to check if supposed to be secure via InsecureSkipVerify |
|
| 67 |
+ TLSConfig: tlsConfig, |
|
| 68 |
+ }) |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ return endpoints, nil |
|
| 72 |
+} |
| 0 | 73 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,13 @@ |
| 0 |
+package registry |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/distribution/reference" |
|
| 4 |
+ "github.com/moby/moby/api/types/registry" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// RepositoryInfo describes a repository |
|
| 8 |
+type RepositoryInfo struct {
|
|
| 9 |
+ Name reference.Named |
|
| 10 |
+ // Index points to registry information |
|
| 11 |
+ Index *registry.IndexInfo |
|
| 12 |
+} |
| ... | ... |
@@ -9,7 +9,7 @@ import ( |
| 9 | 9 |
"github.com/docker/docker/daemon/config" |
| 10 | 10 |
"github.com/docker/docker/daemon/images" |
| 11 | 11 |
"github.com/docker/docker/daemon/libnetwork" |
| 12 |
- "github.com/docker/docker/registry" |
|
| 12 |
+ "github.com/docker/docker/daemon/pkg/registry" |
|
| 13 | 13 |
"gotest.tools/v3/assert" |
| 14 | 14 |
) |
| 15 | 15 |
|
| ... | ... |
@@ -4,8 +4,8 @@ import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"testing" |
| 6 | 6 |
|
| 7 |
+ registrypkg "github.com/docker/docker/daemon/pkg/registry" |
|
| 7 | 8 |
"github.com/docker/docker/integration/internal/requirement" |
| 8 |
- registrypkg "github.com/docker/docker/registry" |
|
| 9 | 9 |
"github.com/moby/moby/api/types/registry" |
| 10 | 10 |
"gotest.tools/v3/assert" |
| 11 | 11 |
is "gotest.tools/v3/assert/cmp" |
| 12 | 12 |
deleted file mode 100644 |
| ... | ... |
@@ -1,205 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "net/http" |
|
| 6 |
- "net/url" |
|
| 7 |
- "strings" |
|
| 8 |
- "time" |
|
| 9 |
- |
|
| 10 |
- "github.com/containerd/log" |
|
| 11 |
- "github.com/docker/distribution/registry/client/auth" |
|
| 12 |
- "github.com/docker/distribution/registry/client/auth/challenge" |
|
| 13 |
- "github.com/docker/distribution/registry/client/transport" |
|
| 14 |
- "github.com/moby/moby/api/types/registry" |
|
| 15 |
- "github.com/pkg/errors" |
|
| 16 |
-) |
|
| 17 |
- |
|
| 18 |
-// AuthClientID is used the ClientID used for the token server |
|
| 19 |
-const AuthClientID = "docker" |
|
| 20 |
- |
|
| 21 |
-type loginCredentialStore struct {
|
|
| 22 |
- authConfig *registry.AuthConfig |
|
| 23 |
-} |
|
| 24 |
- |
|
| 25 |
-func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
|
|
| 26 |
- return lcs.authConfig.Username, lcs.authConfig.Password |
|
| 27 |
-} |
|
| 28 |
- |
|
| 29 |
-func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
|
|
| 30 |
- return lcs.authConfig.IdentityToken |
|
| 31 |
-} |
|
| 32 |
- |
|
| 33 |
-func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
|
|
| 34 |
- lcs.authConfig.IdentityToken = token |
|
| 35 |
-} |
|
| 36 |
- |
|
| 37 |
-type staticCredentialStore struct {
|
|
| 38 |
- auth *registry.AuthConfig |
|
| 39 |
-} |
|
| 40 |
- |
|
| 41 |
-// NewStaticCredentialStore returns a credential store |
|
| 42 |
-// which always returns the same credential values. |
|
| 43 |
-func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore {
|
|
| 44 |
- return staticCredentialStore{
|
|
| 45 |
- auth: ac, |
|
| 46 |
- } |
|
| 47 |
-} |
|
| 48 |
- |
|
| 49 |
-func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
|
|
| 50 |
- if scs.auth == nil {
|
|
| 51 |
- return "", "" |
|
| 52 |
- } |
|
| 53 |
- return scs.auth.Username, scs.auth.Password |
|
| 54 |
-} |
|
| 55 |
- |
|
| 56 |
-func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
|
|
| 57 |
- if scs.auth == nil {
|
|
| 58 |
- return "" |
|
| 59 |
- } |
|
| 60 |
- return scs.auth.IdentityToken |
|
| 61 |
-} |
|
| 62 |
- |
|
| 63 |
-func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
|
|
| 64 |
-} |
|
| 65 |
- |
|
| 66 |
-// loginV2 tries to login to the v2 registry server. The given registry |
|
| 67 |
-// endpoint will be pinged to get authorization challenges. These challenges |
|
| 68 |
-// will be used to authenticate against the registry to validate credentials. |
|
| 69 |
-func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (token string, _ error) {
|
|
| 70 |
- endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" |
|
| 71 |
- log.G(ctx).WithField("endpoint", endpointStr).Debug("attempting v2 login to registry endpoint")
|
|
| 72 |
- |
|
| 73 |
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointStr, http.NoBody) |
|
| 74 |
- if err != nil {
|
|
| 75 |
- return "", err |
|
| 76 |
- } |
|
| 77 |
- |
|
| 78 |
- var ( |
|
| 79 |
- modifiers = Headers(userAgent, nil) |
|
| 80 |
- authTrans = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...) |
|
| 81 |
- credentialAuthConfig = *authConfig |
|
| 82 |
- creds = loginCredentialStore{authConfig: &credentialAuthConfig}
|
|
| 83 |
- ) |
|
| 84 |
- |
|
| 85 |
- loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil) |
|
| 86 |
- if err != nil {
|
|
| 87 |
- return "", err |
|
| 88 |
- } |
|
| 89 |
- |
|
| 90 |
- resp, err := loginClient.Do(req) |
|
| 91 |
- if err != nil {
|
|
| 92 |
- err = translateV2AuthError(err) |
|
| 93 |
- return "", err |
|
| 94 |
- } |
|
| 95 |
- defer resp.Body.Close() |
|
| 96 |
- |
|
| 97 |
- if resp.StatusCode != http.StatusOK {
|
|
| 98 |
- // TODO(dmcgowan): Attempt to further interpret result, status code and error code string |
|
| 99 |
- return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
| 100 |
- } |
|
| 101 |
- |
|
| 102 |
- return credentialAuthConfig.IdentityToken, nil |
|
| 103 |
-} |
|
| 104 |
- |
|
| 105 |
-func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) {
|
|
| 106 |
- challengeManager, err := PingV2Registry(endpoint, authTransport) |
|
| 107 |
- if err != nil {
|
|
| 108 |
- return nil, err |
|
| 109 |
- } |
|
| 110 |
- |
|
| 111 |
- authHandlers := []auth.AuthenticationHandler{
|
|
| 112 |
- auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
|
|
| 113 |
- Transport: authTransport, |
|
| 114 |
- Credentials: creds, |
|
| 115 |
- OfflineAccess: true, |
|
| 116 |
- ClientID: AuthClientID, |
|
| 117 |
- Scopes: scopes, |
|
| 118 |
- }), |
|
| 119 |
- auth.NewBasicHandler(creds), |
|
| 120 |
- } |
|
| 121 |
- |
|
| 122 |
- modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, authHandlers...)) |
|
| 123 |
- |
|
| 124 |
- return &http.Client{
|
|
| 125 |
- Transport: transport.NewTransport(authTransport, modifiers...), |
|
| 126 |
- Timeout: 15 * time.Second, |
|
| 127 |
- }, nil |
|
| 128 |
-} |
|
| 129 |
- |
|
| 130 |
-// ConvertToHostname normalizes a registry URL which has http|https prepended |
|
| 131 |
-// to just its hostname. It is used to match credentials, which may be either |
|
| 132 |
-// stored as hostname or as hostname including scheme (in legacy configuration |
|
| 133 |
-// files). |
|
| 134 |
-func ConvertToHostname(maybeURL string) string {
|
|
| 135 |
- stripped := maybeURL |
|
| 136 |
- if scheme, remainder, ok := strings.Cut(stripped, "://"); ok {
|
|
| 137 |
- switch scheme {
|
|
| 138 |
- case "http", "https": |
|
| 139 |
- stripped = remainder |
|
| 140 |
- default: |
|
| 141 |
- // unknown, or no scheme; doing nothing for now, as we never did. |
|
| 142 |
- } |
|
| 143 |
- } |
|
| 144 |
- stripped, _, _ = strings.Cut(stripped, "/") |
|
| 145 |
- return stripped |
|
| 146 |
-} |
|
| 147 |
- |
|
| 148 |
-// ResolveAuthConfig matches an auth configuration to a server address or a URL |
|
| 149 |
-func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig {
|
|
| 150 |
- configKey := GetAuthConfigKey(index) |
|
| 151 |
- // First try the happy case |
|
| 152 |
- if c, found := authConfigs[configKey]; found || index.Official {
|
|
| 153 |
- return c |
|
| 154 |
- } |
|
| 155 |
- |
|
| 156 |
- // Maybe they have a legacy config file, we will iterate the keys converting |
|
| 157 |
- // them to the new format and testing |
|
| 158 |
- for registryURL, ac := range authConfigs {
|
|
| 159 |
- if configKey == ConvertToHostname(registryURL) {
|
|
| 160 |
- return ac |
|
| 161 |
- } |
|
| 162 |
- } |
|
| 163 |
- |
|
| 164 |
- // When all else fails, return an empty auth config |
|
| 165 |
- return registry.AuthConfig{}
|
|
| 166 |
-} |
|
| 167 |
- |
|
| 168 |
-// PingResponseError is used when the response from a ping |
|
| 169 |
-// was received but invalid. |
|
| 170 |
-type PingResponseError struct {
|
|
| 171 |
- Err error |
|
| 172 |
-} |
|
| 173 |
- |
|
| 174 |
-func (err PingResponseError) Error() string {
|
|
| 175 |
- return err.Err.Error() |
|
| 176 |
-} |
|
| 177 |
- |
|
| 178 |
-// PingV2Registry attempts to ping a v2 registry and on success return a |
|
| 179 |
-// challenge manager for the supported authentication types. |
|
| 180 |
-// If a response is received but cannot be interpreted, a PingResponseError will be returned. |
|
| 181 |
-func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
|
|
| 182 |
- pingClient := &http.Client{
|
|
| 183 |
- Transport: authTransport, |
|
| 184 |
- Timeout: 15 * time.Second, |
|
| 185 |
- } |
|
| 186 |
- endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" |
|
| 187 |
- req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody) |
|
| 188 |
- if err != nil {
|
|
| 189 |
- return nil, err |
|
| 190 |
- } |
|
| 191 |
- resp, err := pingClient.Do(req) |
|
| 192 |
- if err != nil {
|
|
| 193 |
- return nil, err |
|
| 194 |
- } |
|
| 195 |
- defer resp.Body.Close() |
|
| 196 |
- |
|
| 197 |
- challengeManager := challenge.NewSimpleManager() |
|
| 198 |
- if err := challengeManager.AddResponse(resp); err != nil {
|
|
| 199 |
- return nil, PingResponseError{
|
|
| 200 |
- Err: err, |
|
| 201 |
- } |
|
| 202 |
- } |
|
| 203 |
- |
|
| 204 |
- return challengeManager, nil |
|
| 205 |
-} |
| 206 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,106 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "testing" |
|
| 5 |
- |
|
| 6 |
- "github.com/moby/moby/api/types/registry" |
|
| 7 |
- "gotest.tools/v3/assert" |
|
| 8 |
-) |
|
| 9 |
- |
|
| 10 |
-func buildAuthConfigs() map[string]registry.AuthConfig {
|
|
| 11 |
- authConfigs := map[string]registry.AuthConfig{}
|
|
| 12 |
- |
|
| 13 |
- for _, reg := range []string{"testIndex", IndexServer} {
|
|
| 14 |
- authConfigs[reg] = registry.AuthConfig{
|
|
| 15 |
- Username: "docker-user", |
|
| 16 |
- Password: "docker-pass", |
|
| 17 |
- } |
|
| 18 |
- } |
|
| 19 |
- |
|
| 20 |
- return authConfigs |
|
| 21 |
-} |
|
| 22 |
- |
|
| 23 |
-func TestResolveAuthConfigIndexServer(t *testing.T) {
|
|
| 24 |
- authConfigs := buildAuthConfigs() |
|
| 25 |
- indexConfig := authConfigs[IndexServer] |
|
| 26 |
- |
|
| 27 |
- officialIndex := ®istry.IndexInfo{
|
|
| 28 |
- Official: true, |
|
| 29 |
- } |
|
| 30 |
- privateIndex := ®istry.IndexInfo{
|
|
| 31 |
- Official: false, |
|
| 32 |
- } |
|
| 33 |
- |
|
| 34 |
- resolved := ResolveAuthConfig(authConfigs, officialIndex) |
|
| 35 |
- assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") |
|
| 36 |
- |
|
| 37 |
- resolved = ResolveAuthConfig(authConfigs, privateIndex) |
|
| 38 |
- assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer") |
|
| 39 |
-} |
|
| 40 |
- |
|
| 41 |
-func TestResolveAuthConfigFullURL(t *testing.T) {
|
|
| 42 |
- authConfigs := buildAuthConfigs() |
|
| 43 |
- |
|
| 44 |
- registryAuth := registry.AuthConfig{
|
|
| 45 |
- Username: "foo-user", |
|
| 46 |
- Password: "foo-pass", |
|
| 47 |
- } |
|
| 48 |
- localAuth := registry.AuthConfig{
|
|
| 49 |
- Username: "bar-user", |
|
| 50 |
- Password: "bar-pass", |
|
| 51 |
- } |
|
| 52 |
- officialAuth := registry.AuthConfig{
|
|
| 53 |
- Username: "baz-user", |
|
| 54 |
- Password: "baz-pass", |
|
| 55 |
- } |
|
| 56 |
- authConfigs[IndexServer] = officialAuth |
|
| 57 |
- |
|
| 58 |
- expectedAuths := map[string]registry.AuthConfig{
|
|
| 59 |
- "registry.example.com": registryAuth, |
|
| 60 |
- "localhost:8000": localAuth, |
|
| 61 |
- "example.com": localAuth, |
|
| 62 |
- } |
|
| 63 |
- |
|
| 64 |
- validRegistries := map[string][]string{
|
|
| 65 |
- "registry.example.com": {
|
|
| 66 |
- "https://registry.example.com/v1/", |
|
| 67 |
- "http://registry.example.com/v1/", |
|
| 68 |
- "registry.example.com", |
|
| 69 |
- "registry.example.com/v1/", |
|
| 70 |
- }, |
|
| 71 |
- "localhost:8000": {
|
|
| 72 |
- "https://localhost:8000/v1/", |
|
| 73 |
- "http://localhost:8000/v1/", |
|
| 74 |
- "localhost:8000", |
|
| 75 |
- "localhost:8000/v1/", |
|
| 76 |
- }, |
|
| 77 |
- "example.com": {
|
|
| 78 |
- "https://example.com/v1/", |
|
| 79 |
- "http://example.com/v1/", |
|
| 80 |
- "example.com", |
|
| 81 |
- "example.com/v1/", |
|
| 82 |
- }, |
|
| 83 |
- } |
|
| 84 |
- |
|
| 85 |
- for configKey, registries := range validRegistries {
|
|
| 86 |
- configured, ok := expectedAuths[configKey] |
|
| 87 |
- if !ok {
|
|
| 88 |
- t.Fail() |
|
| 89 |
- } |
|
| 90 |
- index := ®istry.IndexInfo{
|
|
| 91 |
- Name: configKey, |
|
| 92 |
- } |
|
| 93 |
- for _, reg := range registries {
|
|
| 94 |
- authConfigs[reg] = configured |
|
| 95 |
- resolved := ResolveAuthConfig(authConfigs, index) |
|
| 96 |
- if resolved.Username != configured.Username || resolved.Password != configured.Password {
|
|
| 97 |
- t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
|
|
| 98 |
- } |
|
| 99 |
- delete(authConfigs, reg) |
|
| 100 |
- resolved = ResolveAuthConfig(authConfigs, index) |
|
| 101 |
- if resolved.Username == configured.Username || resolved.Password == configured.Password {
|
|
| 102 |
- t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
|
|
| 103 |
- } |
|
| 104 |
- } |
|
| 105 |
- } |
|
| 106 |
-} |
| 107 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,449 +0,0 @@ |
| 1 |
-// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: |
|
| 2 |
-//go:build go1.23 |
|
| 3 |
- |
|
| 4 |
-package registry |
|
| 5 |
- |
|
| 6 |
-import ( |
|
| 7 |
- "context" |
|
| 8 |
- "net" |
|
| 9 |
- "net/url" |
|
| 10 |
- "os" |
|
| 11 |
- "path/filepath" |
|
| 12 |
- "regexp" |
|
| 13 |
- "runtime" |
|
| 14 |
- "strconv" |
|
| 15 |
- "strings" |
|
| 16 |
- "sync" |
|
| 17 |
- |
|
| 18 |
- "github.com/containerd/log" |
|
| 19 |
- "github.com/distribution/reference" |
|
| 20 |
- "github.com/moby/moby/api/types/registry" |
|
| 21 |
-) |
|
| 22 |
- |
|
| 23 |
-// ServiceOptions holds command line options. |
|
| 24 |
-type ServiceOptions struct {
|
|
| 25 |
- Mirrors []string `json:"registry-mirrors,omitempty"` |
|
| 26 |
- InsecureRegistries []string `json:"insecure-registries,omitempty"` |
|
| 27 |
-} |
|
| 28 |
- |
|
| 29 |
-// serviceConfig holds daemon configuration for the registry service. |
|
| 30 |
-type serviceConfig registry.ServiceConfig |
|
| 31 |
- |
|
| 32 |
-// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains |
|
| 33 |
-// are here for historic reasons and backward-compatibility. These domains |
|
| 34 |
-// are still supported by Docker Hub (and will continue to be supported), but |
|
| 35 |
-// there are new domains already in use, and plans to consolidate all legacy |
|
| 36 |
-// domains to new "canonical" domains. Once those domains are decided on, we |
|
| 37 |
-// should update these consts (but making sure to preserve compatibility with |
|
| 38 |
-// existing installs, clients, and user configuration). |
|
| 39 |
-const ( |
|
| 40 |
- // DefaultNamespace is the default namespace |
|
| 41 |
- DefaultNamespace = "docker.io" |
|
| 42 |
- // DefaultRegistryHost is the hostname for the default (Docker Hub) registry |
|
| 43 |
- // used for pushing and pulling images. This hostname is hard-coded to handle |
|
| 44 |
- // the conversion from image references without registry name (e.g. "ubuntu", |
|
| 45 |
- // or "ubuntu:latest"), as well as references using the "docker.io" domain |
|
| 46 |
- // name, which is used as canonical reference for images on Docker Hub, but |
|
| 47 |
- // does not match the domain-name of Docker Hub's registry. |
|
| 48 |
- DefaultRegistryHost = "registry-1.docker.io" |
|
| 49 |
- // IndexHostname is the index hostname, used for authentication and image search. |
|
| 50 |
- IndexHostname = "index.docker.io" |
|
| 51 |
- // IndexServer is used for user auth and image search |
|
| 52 |
- IndexServer = "https://" + IndexHostname + "/v1/" |
|
| 53 |
- // IndexName is the name of the index |
|
| 54 |
- IndexName = "docker.io" |
|
| 55 |
-) |
|
| 56 |
- |
|
| 57 |
-var ( |
|
| 58 |
- // DefaultV2Registry is the URI of the default (Docker Hub) registry. |
|
| 59 |
- DefaultV2Registry = &url.URL{
|
|
| 60 |
- Scheme: "https", |
|
| 61 |
- Host: DefaultRegistryHost, |
|
| 62 |
- } |
|
| 63 |
- |
|
| 64 |
- validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
|
|
| 65 |
- return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`) |
|
| 66 |
- }) |
|
| 67 |
-) |
|
| 68 |
- |
|
| 69 |
-// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit], |
|
| 70 |
-// but inlining it to prevent adding that as a dependency for docker/cli. |
|
| 71 |
-// |
|
| 72 |
-// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 |
|
| 73 |
-func runningWithRootlessKit() bool {
|
|
| 74 |
- return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != ""
|
|
| 75 |
-} |
|
| 76 |
- |
|
| 77 |
-// CertsDir is the directory where certificates are stored. |
|
| 78 |
-// |
|
| 79 |
-// - Linux: "/etc/docker/certs.d/" |
|
| 80 |
-// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/" |
|
| 81 |
-// - Windows: "%PROGRAMDATA%/docker/certs.d/" |
|
| 82 |
-// |
|
| 83 |
-// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless. |
|
| 84 |
-func CertsDir() string {
|
|
| 85 |
- certsDir := "/etc/docker/certs.d" |
|
| 86 |
- if runningWithRootlessKit() {
|
|
| 87 |
- if configHome, _ := os.UserConfigDir(); configHome != "" {
|
|
| 88 |
- certsDir = filepath.Join(configHome, "docker", "certs.d") |
|
| 89 |
- } |
|
| 90 |
- } else if runtime.GOOS == "windows" {
|
|
| 91 |
- certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d")
|
|
| 92 |
- } |
|
| 93 |
- return certsDir |
|
| 94 |
-} |
|
| 95 |
- |
|
| 96 |
-// newServiceConfig returns a new instance of ServiceConfig |
|
| 97 |
-func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
|
|
| 98 |
- config := &serviceConfig{}
|
|
| 99 |
- if err := config.loadMirrors(options.Mirrors); err != nil {
|
|
| 100 |
- return nil, err |
|
| 101 |
- } |
|
| 102 |
- if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil {
|
|
| 103 |
- return nil, err |
|
| 104 |
- } |
|
| 105 |
- |
|
| 106 |
- return config, nil |
|
| 107 |
-} |
|
| 108 |
- |
|
| 109 |
-// copy constructs a new ServiceConfig with a copy of the configuration in config. |
|
| 110 |
-func (config *serviceConfig) copy() *registry.ServiceConfig {
|
|
| 111 |
- ic := make(map[string]*registry.IndexInfo) |
|
| 112 |
- for key, value := range config.IndexConfigs {
|
|
| 113 |
- ic[key] = value |
|
| 114 |
- } |
|
| 115 |
- return ®istry.ServiceConfig{
|
|
| 116 |
- InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...), |
|
| 117 |
- IndexConfigs: ic, |
|
| 118 |
- Mirrors: append([]string(nil), config.Mirrors...), |
|
| 119 |
- } |
|
| 120 |
-} |
|
| 121 |
- |
|
| 122 |
-// loadMirrors loads mirrors to config, after removing duplicates. |
|
| 123 |
-// Returns an error if mirrors contains an invalid mirror. |
|
| 124 |
-func (config *serviceConfig) loadMirrors(mirrors []string) error {
|
|
| 125 |
- mMap := map[string]struct{}{}
|
|
| 126 |
- unique := []string{}
|
|
| 127 |
- |
|
| 128 |
- for _, mirror := range mirrors {
|
|
| 129 |
- m, err := ValidateMirror(mirror) |
|
| 130 |
- if err != nil {
|
|
| 131 |
- return err |
|
| 132 |
- } |
|
| 133 |
- if _, exist := mMap[m]; !exist {
|
|
| 134 |
- mMap[m] = struct{}{}
|
|
| 135 |
- unique = append(unique, m) |
|
| 136 |
- } |
|
| 137 |
- } |
|
| 138 |
- |
|
| 139 |
- config.Mirrors = unique |
|
| 140 |
- |
|
| 141 |
- // Configure public registry since mirrors may have changed. |
|
| 142 |
- config.IndexConfigs = map[string]*registry.IndexInfo{
|
|
| 143 |
- IndexName: {
|
|
| 144 |
- Name: IndexName, |
|
| 145 |
- Mirrors: unique, |
|
| 146 |
- Secure: true, |
|
| 147 |
- Official: true, |
|
| 148 |
- }, |
|
| 149 |
- } |
|
| 150 |
- |
|
| 151 |
- return nil |
|
| 152 |
-} |
|
| 153 |
- |
|
| 154 |
-// loadInsecureRegistries loads insecure registries to config |
|
| 155 |
-func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
|
|
| 156 |
- // Localhost is by default considered as an insecure registry. This is a |
|
| 157 |
- // stop-gap for people who are running a private registry on localhost. |
|
| 158 |
- registries = append(registries, "::1/128", "127.0.0.0/8") |
|
| 159 |
- |
|
| 160 |
- var ( |
|
| 161 |
- insecureRegistryCIDRs = make([]*registry.NetIPNet, 0) |
|
| 162 |
- indexConfigs = make(map[string]*registry.IndexInfo) |
|
| 163 |
- ) |
|
| 164 |
- |
|
| 165 |
-skip: |
|
| 166 |
- for _, r := range registries {
|
|
| 167 |
- // validate insecure registry |
|
| 168 |
- if _, err := ValidateIndexName(r); err != nil {
|
|
| 169 |
- return err |
|
| 170 |
- } |
|
| 171 |
- if scheme, host, ok := strings.Cut(r, "://"); ok {
|
|
| 172 |
- switch strings.ToLower(scheme) {
|
|
| 173 |
- case "http", "https": |
|
| 174 |
- log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme)
|
|
| 175 |
- r = host |
|
| 176 |
- default: |
|
| 177 |
- // unsupported scheme |
|
| 178 |
- return invalidParamf("insecure registry %s should not contain '://'", r)
|
|
| 179 |
- } |
|
| 180 |
- } |
|
| 181 |
- // Check if CIDR was passed to --insecure-registry |
|
| 182 |
- _, ipnet, err := net.ParseCIDR(r) |
|
| 183 |
- if err == nil {
|
|
| 184 |
- // Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip. |
|
| 185 |
- data := (*registry.NetIPNet)(ipnet) |
|
| 186 |
- for _, value := range insecureRegistryCIDRs {
|
|
| 187 |
- if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
|
|
| 188 |
- continue skip |
|
| 189 |
- } |
|
| 190 |
- } |
|
| 191 |
- // ipnet is not found, add it in config.InsecureRegistryCIDRs |
|
| 192 |
- insecureRegistryCIDRs = append(insecureRegistryCIDRs, data) |
|
| 193 |
- } else {
|
|
| 194 |
- if err := validateHostPort(r); err != nil {
|
|
| 195 |
- return invalidParamWrapf(err, "insecure registry %s is not valid", r) |
|
| 196 |
- } |
|
| 197 |
- // Assume `host:port` if not CIDR. |
|
| 198 |
- indexConfigs[r] = ®istry.IndexInfo{
|
|
| 199 |
- Name: r, |
|
| 200 |
- Mirrors: []string{},
|
|
| 201 |
- Secure: false, |
|
| 202 |
- Official: false, |
|
| 203 |
- } |
|
| 204 |
- } |
|
| 205 |
- } |
|
| 206 |
- |
|
| 207 |
- // Configure public registry. |
|
| 208 |
- indexConfigs[IndexName] = ®istry.IndexInfo{
|
|
| 209 |
- Name: IndexName, |
|
| 210 |
- Mirrors: config.Mirrors, |
|
| 211 |
- Secure: true, |
|
| 212 |
- Official: true, |
|
| 213 |
- } |
|
| 214 |
- config.InsecureRegistryCIDRs = insecureRegistryCIDRs |
|
| 215 |
- config.IndexConfigs = indexConfigs |
|
| 216 |
- |
|
| 217 |
- return nil |
|
| 218 |
-} |
|
| 219 |
- |
|
| 220 |
-// isSecureIndex returns false if the provided indexName is part of the list of insecure registries |
|
| 221 |
-// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. |
|
| 222 |
-// |
|
| 223 |
-// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. |
|
| 224 |
-// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered |
|
| 225 |
-// insecure. |
|
| 226 |
-// |
|
| 227 |
-// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name |
|
| 228 |
-// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained |
|
| 229 |
-// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element |
|
| 230 |
-// of insecureRegistries. |
|
| 231 |
-func (config *serviceConfig) isSecureIndex(indexName string) bool {
|
|
| 232 |
- // Check for configured index, first. This is needed in case isSecureIndex |
|
| 233 |
- // is called from anything besides newIndexInfo, in order to honor per-index configurations. |
|
| 234 |
- if index, ok := config.IndexConfigs[indexName]; ok {
|
|
| 235 |
- return index.Secure |
|
| 236 |
- } |
|
| 237 |
- |
|
| 238 |
- return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName) |
|
| 239 |
-} |
|
| 240 |
- |
|
| 241 |
-// for mocking in unit tests. |
|
| 242 |
-var lookupIP = net.LookupIP |
|
| 243 |
- |
|
| 244 |
-// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host")
|
|
| 245 |
-// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be |
|
| 246 |
-// resolved to IP addresses for matching. If resolution fails, false is returned. |
|
| 247 |
-func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
|
|
| 248 |
- if len(cidrs) == 0 {
|
|
| 249 |
- return false |
|
| 250 |
- } |
|
| 251 |
- |
|
| 252 |
- host, _, err := net.SplitHostPort(urlHost) |
|
| 253 |
- if err != nil {
|
|
| 254 |
- // Assume urlHost is a host without port and go on. |
|
| 255 |
- host = urlHost |
|
| 256 |
- } |
|
| 257 |
- |
|
| 258 |
- var addresses []net.IP |
|
| 259 |
- if ip := net.ParseIP(host); ip != nil {
|
|
| 260 |
- // Host is an IP-address. |
|
| 261 |
- addresses = append(addresses, ip) |
|
| 262 |
- } else {
|
|
| 263 |
- // Try to resolve the host's IP-address. |
|
| 264 |
- addresses, err = lookupIP(host) |
|
| 265 |
- if err != nil {
|
|
| 266 |
- // We failed to resolve the host; assume there's no match. |
|
| 267 |
- return false |
|
| 268 |
- } |
|
| 269 |
- } |
|
| 270 |
- |
|
| 271 |
- for _, addr := range addresses {
|
|
| 272 |
- for _, ipnet := range cidrs {
|
|
| 273 |
- // check if the addr falls in the subnet |
|
| 274 |
- if (*net.IPNet)(ipnet).Contains(addr) {
|
|
| 275 |
- return true |
|
| 276 |
- } |
|
| 277 |
- } |
|
| 278 |
- } |
|
| 279 |
- |
|
| 280 |
- return false |
|
| 281 |
-} |
|
| 282 |
- |
|
| 283 |
-// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It |
|
| 284 |
-// returns an error if the given mirrorURL is invalid, or the normalized |
|
| 285 |
-// format for the URL otherwise. |
|
| 286 |
-// |
|
| 287 |
-// It is used by the daemon to validate the daemon configuration. |
|
| 288 |
-func ValidateMirror(mirrorURL string) (string, error) {
|
|
| 289 |
- // Fast path for missing scheme, as url.Parse splits by ":", which can |
|
| 290 |
- // cause the hostname to be considered the "scheme" when using "hostname:port". |
|
| 291 |
- if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" {
|
|
| 292 |
- return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL)
|
|
| 293 |
- } |
|
| 294 |
- uri, err := url.Parse(mirrorURL) |
|
| 295 |
- if err != nil {
|
|
| 296 |
- return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL) |
|
| 297 |
- } |
|
| 298 |
- if uri.Scheme != "http" && uri.Scheme != "https" {
|
|
| 299 |
- return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri)
|
|
| 300 |
- } |
|
| 301 |
- if uri.RawQuery != "" || uri.Fragment != "" {
|
|
| 302 |
- return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri)
|
|
| 303 |
- } |
|
| 304 |
- if uri.User != nil {
|
|
| 305 |
- // strip password from output |
|
| 306 |
- uri.User = url.UserPassword(uri.User.Username(), "xxxxx") |
|
| 307 |
- return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
|
|
| 308 |
- } |
|
| 309 |
- return strings.TrimSuffix(mirrorURL, "/") + "/", nil |
|
| 310 |
-} |
|
| 311 |
- |
|
| 312 |
-// ValidateIndexName validates an index name. It is used by the daemon to |
|
| 313 |
-// validate the daemon configuration. |
|
| 314 |
-func ValidateIndexName(val string) (string, error) {
|
|
| 315 |
- val = normalizeIndexName(val) |
|
| 316 |
- if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
|
|
| 317 |
- return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
|
|
| 318 |
- } |
|
| 319 |
- return val, nil |
|
| 320 |
-} |
|
| 321 |
- |
|
| 322 |
-func normalizeIndexName(val string) string {
|
|
| 323 |
- // TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/". |
|
| 324 |
- // TODO: upstream this to check to reference package |
|
| 325 |
- if val == "index.docker.io" {
|
|
| 326 |
- return "docker.io" |
|
| 327 |
- } |
|
| 328 |
- return val |
|
| 329 |
-} |
|
| 330 |
- |
|
| 331 |
-func hasScheme(reposName string) bool {
|
|
| 332 |
- return strings.Contains(reposName, "://") |
|
| 333 |
-} |
|
| 334 |
- |
|
| 335 |
-func validateHostPort(s string) error {
|
|
| 336 |
- // Split host and port, and in case s can not be split, assume host only |
|
| 337 |
- host, port, err := net.SplitHostPort(s) |
|
| 338 |
- if err != nil {
|
|
| 339 |
- host = s |
|
| 340 |
- port = "" |
|
| 341 |
- } |
|
| 342 |
- // If match against the `host:port` pattern fails, |
|
| 343 |
- // it might be `IPv6:port`, which will be captured by net.ParseIP(host) |
|
| 344 |
- if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil {
|
|
| 345 |
- return invalidParamf("invalid host %q", host)
|
|
| 346 |
- } |
|
| 347 |
- if port != "" {
|
|
| 348 |
- v, err := strconv.Atoi(port) |
|
| 349 |
- if err != nil {
|
|
| 350 |
- return err |
|
| 351 |
- } |
|
| 352 |
- if v < 0 || v > 65535 {
|
|
| 353 |
- return invalidParamf("invalid port %q", port)
|
|
| 354 |
- } |
|
| 355 |
- } |
|
| 356 |
- return nil |
|
| 357 |
-} |
|
| 358 |
- |
|
| 359 |
-// newIndexInfo returns IndexInfo configuration from indexName |
|
| 360 |
-func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
|
|
| 361 |
- indexName = normalizeIndexName(indexName) |
|
| 362 |
- |
|
| 363 |
- // Return any configured index info, first. |
|
| 364 |
- if index, ok := config.IndexConfigs[indexName]; ok {
|
|
| 365 |
- return index |
|
| 366 |
- } |
|
| 367 |
- |
|
| 368 |
- // Construct a non-configured index info. |
|
| 369 |
- return ®istry.IndexInfo{
|
|
| 370 |
- Name: indexName, |
|
| 371 |
- Mirrors: []string{},
|
|
| 372 |
- Secure: config.isSecureIndex(indexName), |
|
| 373 |
- } |
|
| 374 |
-} |
|
| 375 |
- |
|
| 376 |
-// GetAuthConfigKey special-cases using the full index address of the official |
|
| 377 |
-// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. |
|
| 378 |
-func GetAuthConfigKey(index *registry.IndexInfo) string {
|
|
| 379 |
- if index.Official {
|
|
| 380 |
- return IndexServer |
|
| 381 |
- } |
|
| 382 |
- return index.Name |
|
| 383 |
-} |
|
| 384 |
- |
|
| 385 |
-// ParseRepositoryInfo performs the breakdown of a repository name into a |
|
| 386 |
-// [RepositoryInfo], but lacks registry configuration. |
|
| 387 |
-// |
|
| 388 |
-// It is used by the Docker cli to interact with registry-related endpoints. |
|
| 389 |
-func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
|
| 390 |
- indexName := normalizeIndexName(reference.Domain(reposName)) |
|
| 391 |
- if indexName == IndexName {
|
|
| 392 |
- return &RepositoryInfo{
|
|
| 393 |
- Name: reference.TrimNamed(reposName), |
|
| 394 |
- Index: ®istry.IndexInfo{
|
|
| 395 |
- Name: IndexName, |
|
| 396 |
- Mirrors: []string{},
|
|
| 397 |
- Secure: true, |
|
| 398 |
- Official: true, |
|
| 399 |
- }, |
|
| 400 |
- }, nil |
|
| 401 |
- } |
|
| 402 |
- |
|
| 403 |
- return &RepositoryInfo{
|
|
| 404 |
- Name: reference.TrimNamed(reposName), |
|
| 405 |
- Index: ®istry.IndexInfo{
|
|
| 406 |
- Name: indexName, |
|
| 407 |
- Mirrors: []string{},
|
|
| 408 |
- Secure: !isInsecure(indexName), |
|
| 409 |
- }, |
|
| 410 |
- }, nil |
|
| 411 |
-} |
|
| 412 |
- |
|
| 413 |
-// isInsecure is used to detect whether a registry domain or IP-address is allowed |
|
| 414 |
-// to use an insecure (non-TLS, or self-signed cert) connection according to the |
|
| 415 |
-// defaults, which allows for insecure connections with registries running on a |
|
| 416 |
-// loopback address ("localhost", "::1/128", "127.0.0.0/8").
|
|
| 417 |
-// |
|
| 418 |
-// It is used in situations where we don't have access to the daemon's configuration, |
|
| 419 |
-// for example, when used from the client / CLI. |
|
| 420 |
-func isInsecure(hostNameOrIP string) bool {
|
|
| 421 |
- // Attempt to strip port if present; this also strips brackets for |
|
| 422 |
- // IPv6 addresses with a port (e.g. "[::1]:5000"). |
|
| 423 |
- // |
|
| 424 |
- // This is best-effort; we'll continue using the address as-is if it fails. |
|
| 425 |
- if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
|
|
| 426 |
- hostNameOrIP = host |
|
| 427 |
- } |
|
| 428 |
- if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
|
|
| 429 |
- // Fast path; no need to resolve these, assuming nobody overrides |
|
| 430 |
- // "localhost" for anything else than a loopback address (sorry, not sorry). |
|
| 431 |
- return true |
|
| 432 |
- } |
|
| 433 |
- |
|
| 434 |
- var addresses []net.IP |
|
| 435 |
- if ip := net.ParseIP(hostNameOrIP); ip != nil {
|
|
| 436 |
- addresses = append(addresses, ip) |
|
| 437 |
- } else {
|
|
| 438 |
- // Try to resolve the host's IP-addresses. |
|
| 439 |
- addrs, _ := lookupIP(hostNameOrIP) |
|
| 440 |
- addresses = append(addresses, addrs...) |
|
| 441 |
- } |
|
| 442 |
- |
|
| 443 |
- for _, addr := range addresses {
|
|
| 444 |
- if addr.IsLoopback() {
|
|
| 445 |
- return true |
|
| 446 |
- } |
|
| 447 |
- } |
|
| 448 |
- return false |
|
| 449 |
-} |
| 450 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,340 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "testing" |
|
| 5 |
- |
|
| 6 |
- cerrdefs "github.com/containerd/errdefs" |
|
| 7 |
- "gotest.tools/v3/assert" |
|
| 8 |
- is "gotest.tools/v3/assert/cmp" |
|
| 9 |
-) |
|
| 10 |
- |
|
| 11 |
-func TestValidateMirror(t *testing.T) {
|
|
| 12 |
- tests := []struct {
|
|
| 13 |
- input string |
|
| 14 |
- output string |
|
| 15 |
- expectedErr string |
|
| 16 |
- }{
|
|
| 17 |
- // Valid cases |
|
| 18 |
- {
|
|
| 19 |
- input: "http://mirror-1.example.com", |
|
| 20 |
- output: "http://mirror-1.example.com/", |
|
| 21 |
- }, |
|
| 22 |
- {
|
|
| 23 |
- input: "http://mirror-1.example.com/", |
|
| 24 |
- output: "http://mirror-1.example.com/", |
|
| 25 |
- }, |
|
| 26 |
- {
|
|
| 27 |
- input: "https://mirror-1.example.com", |
|
| 28 |
- output: "https://mirror-1.example.com/", |
|
| 29 |
- }, |
|
| 30 |
- {
|
|
| 31 |
- input: "https://mirror-1.example.com/", |
|
| 32 |
- output: "https://mirror-1.example.com/", |
|
| 33 |
- }, |
|
| 34 |
- {
|
|
| 35 |
- input: "http://localhost", |
|
| 36 |
- output: "http://localhost/", |
|
| 37 |
- }, |
|
| 38 |
- {
|
|
| 39 |
- input: "https://localhost", |
|
| 40 |
- output: "https://localhost/", |
|
| 41 |
- }, |
|
| 42 |
- {
|
|
| 43 |
- input: "http://localhost:5000", |
|
| 44 |
- output: "http://localhost:5000/", |
|
| 45 |
- }, |
|
| 46 |
- {
|
|
| 47 |
- input: "https://localhost:5000", |
|
| 48 |
- output: "https://localhost:5000/", |
|
| 49 |
- }, |
|
| 50 |
- {
|
|
| 51 |
- input: "http://127.0.0.1", |
|
| 52 |
- output: "http://127.0.0.1/", |
|
| 53 |
- }, |
|
| 54 |
- {
|
|
| 55 |
- input: "https://127.0.0.1", |
|
| 56 |
- output: "https://127.0.0.1/", |
|
| 57 |
- }, |
|
| 58 |
- {
|
|
| 59 |
- input: "http://127.0.0.1:5000", |
|
| 60 |
- output: "http://127.0.0.1:5000/", |
|
| 61 |
- }, |
|
| 62 |
- {
|
|
| 63 |
- input: "https://127.0.0.1:5000", |
|
| 64 |
- output: "https://127.0.0.1:5000/", |
|
| 65 |
- }, |
|
| 66 |
- {
|
|
| 67 |
- input: "http://mirror-1.example.com/v1/", |
|
| 68 |
- output: "http://mirror-1.example.com/v1/", |
|
| 69 |
- }, |
|
| 70 |
- {
|
|
| 71 |
- input: "https://mirror-1.example.com/v1/", |
|
| 72 |
- output: "https://mirror-1.example.com/v1/", |
|
| 73 |
- }, |
|
| 74 |
- |
|
| 75 |
- // Invalid cases |
|
| 76 |
- {
|
|
| 77 |
- input: "!invalid!://%as%", |
|
| 78 |
- expectedErr: `invalid mirror: "!invalid!://%as%" is not a valid URI: parse "!invalid!://%as%": first path segment in URL cannot contain colon`, |
|
| 79 |
- }, |
|
| 80 |
- {
|
|
| 81 |
- input: "mirror-1.example.com", |
|
| 82 |
- expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com": must use either 'https://' or 'http://'`, |
|
| 83 |
- }, |
|
| 84 |
- {
|
|
| 85 |
- input: "mirror-1.example.com:5000", |
|
| 86 |
- expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com:5000": must use either 'https://' or 'http://'`, |
|
| 87 |
- }, |
|
| 88 |
- {
|
|
| 89 |
- input: "ftp://mirror-1.example.com", |
|
| 90 |
- expectedErr: `invalid mirror: unsupported scheme "ftp" in "ftp://mirror-1.example.com": must use either 'https://' or 'http://'`, |
|
| 91 |
- }, |
|
| 92 |
- {
|
|
| 93 |
- input: "http://mirror-1.example.com/?q=foo", |
|
| 94 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/?q=foo"`, |
|
| 95 |
- }, |
|
| 96 |
- {
|
|
| 97 |
- input: "http://mirror-1.example.com/v1/?q=foo", |
|
| 98 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo"`, |
|
| 99 |
- }, |
|
| 100 |
- {
|
|
| 101 |
- input: "http://mirror-1.example.com/v1/?q=foo#frag", |
|
| 102 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo#frag"`, |
|
| 103 |
- }, |
|
| 104 |
- {
|
|
| 105 |
- input: "http://mirror-1.example.com?q=foo", |
|
| 106 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com?q=foo"`, |
|
| 107 |
- }, |
|
| 108 |
- {
|
|
| 109 |
- input: "https://mirror-1.example.com#frag", |
|
| 110 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com#frag"`, |
|
| 111 |
- }, |
|
| 112 |
- {
|
|
| 113 |
- input: "https://mirror-1.example.com/#frag", |
|
| 114 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/#frag"`, |
|
| 115 |
- }, |
|
| 116 |
- {
|
|
| 117 |
- input: "http://foo:bar@mirror-1.example.com/", |
|
| 118 |
- expectedErr: `invalid mirror: username/password not allowed in URI "http://foo:xxxxx@mirror-1.example.com/"`, |
|
| 119 |
- }, |
|
| 120 |
- {
|
|
| 121 |
- input: "https://mirror-1.example.com/v1/#frag", |
|
| 122 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/v1/#frag"`, |
|
| 123 |
- }, |
|
| 124 |
- {
|
|
| 125 |
- input: "https://mirror-1.example.com?q", |
|
| 126 |
- expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com?q"`, |
|
| 127 |
- }, |
|
| 128 |
- } |
|
| 129 |
- |
|
| 130 |
- for _, tc := range tests {
|
|
| 131 |
- t.Run(tc.input, func(t *testing.T) {
|
|
| 132 |
- out, err := ValidateMirror(tc.input) |
|
| 133 |
- if tc.expectedErr != "" {
|
|
| 134 |
- assert.Error(t, err, tc.expectedErr) |
|
| 135 |
- } else {
|
|
| 136 |
- assert.NilError(t, err) |
|
| 137 |
- } |
|
| 138 |
- assert.Check(t, is.Equal(out, tc.output)) |
|
| 139 |
- }) |
|
| 140 |
- } |
|
| 141 |
-} |
|
| 142 |
- |
|
| 143 |
-func TestLoadInsecureRegistries(t *testing.T) {
|
|
| 144 |
- testCases := []struct {
|
|
| 145 |
- registries []string |
|
| 146 |
- index string |
|
| 147 |
- err string |
|
| 148 |
- }{
|
|
| 149 |
- {
|
|
| 150 |
- registries: []string{"127.0.0.1"},
|
|
| 151 |
- index: "127.0.0.1", |
|
| 152 |
- }, |
|
| 153 |
- {
|
|
| 154 |
- registries: []string{"127.0.0.1:8080"},
|
|
| 155 |
- index: "127.0.0.1:8080", |
|
| 156 |
- }, |
|
| 157 |
- {
|
|
| 158 |
- registries: []string{"2001:db8::1"},
|
|
| 159 |
- index: "2001:db8::1", |
|
| 160 |
- }, |
|
| 161 |
- {
|
|
| 162 |
- registries: []string{"[2001:db8::1]:80"},
|
|
| 163 |
- index: "[2001:db8::1]:80", |
|
| 164 |
- }, |
|
| 165 |
- {
|
|
| 166 |
- registries: []string{"http://myregistry.example.com"},
|
|
| 167 |
- index: "myregistry.example.com", |
|
| 168 |
- }, |
|
| 169 |
- {
|
|
| 170 |
- registries: []string{"https://myregistry.example.com"},
|
|
| 171 |
- index: "myregistry.example.com", |
|
| 172 |
- }, |
|
| 173 |
- {
|
|
| 174 |
- registries: []string{"HTTP://myregistry.example.com"},
|
|
| 175 |
- index: "myregistry.example.com", |
|
| 176 |
- }, |
|
| 177 |
- {
|
|
| 178 |
- registries: []string{"svn://myregistry.example.com"},
|
|
| 179 |
- err: "insecure registry svn://myregistry.example.com should not contain '://'", |
|
| 180 |
- }, |
|
| 181 |
- {
|
|
| 182 |
- registries: []string{"-invalid-registry"},
|
|
| 183 |
- err: "Cannot begin or end with a hyphen", |
|
| 184 |
- }, |
|
| 185 |
- {
|
|
| 186 |
- registries: []string{`mytest-.com`},
|
|
| 187 |
- err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`, |
|
| 188 |
- }, |
|
| 189 |
- {
|
|
| 190 |
- registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
|
|
| 191 |
- 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"`, |
|
| 192 |
- }, |
|
| 193 |
- {
|
|
| 194 |
- registries: []string{`myregistry.example.com:500000`},
|
|
| 195 |
- err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`, |
|
| 196 |
- }, |
|
| 197 |
- {
|
|
| 198 |
- registries: []string{`"myregistry.example.com"`},
|
|
| 199 |
- err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`, |
|
| 200 |
- }, |
|
| 201 |
- {
|
|
| 202 |
- registries: []string{`"myregistry.example.com:5000"`},
|
|
| 203 |
- err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`, |
|
| 204 |
- }, |
|
| 205 |
- } |
|
| 206 |
- for _, testCase := range testCases {
|
|
| 207 |
- config := &serviceConfig{}
|
|
| 208 |
- err := config.loadInsecureRegistries(testCase.registries) |
|
| 209 |
- if testCase.err == "" {
|
|
| 210 |
- if err != nil {
|
|
| 211 |
- t.Fatalf("expect no error, got '%s'", err)
|
|
| 212 |
- } |
|
| 213 |
- match := false |
|
| 214 |
- for index := range config.IndexConfigs {
|
|
| 215 |
- if index == testCase.index {
|
|
| 216 |
- match = true |
|
| 217 |
- } |
|
| 218 |
- } |
|
| 219 |
- if !match {
|
|
| 220 |
- t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.IndexConfigs)
|
|
| 221 |
- } |
|
| 222 |
- } else {
|
|
| 223 |
- if err == nil {
|
|
| 224 |
- t.Fatalf("expect error '%s', got no error", testCase.err)
|
|
| 225 |
- } |
|
| 226 |
- assert.ErrorContains(t, err, testCase.err) |
|
| 227 |
- assert.Check(t, cerrdefs.IsInvalidArgument(err)) |
|
| 228 |
- } |
|
| 229 |
- } |
|
| 230 |
-} |
|
| 231 |
- |
|
| 232 |
-func TestNewServiceConfig(t *testing.T) {
|
|
| 233 |
- tests := []struct {
|
|
| 234 |
- doc string |
|
| 235 |
- opts ServiceOptions |
|
| 236 |
- errStr string |
|
| 237 |
- }{
|
|
| 238 |
- {
|
|
| 239 |
- doc: "empty config", |
|
| 240 |
- }, |
|
| 241 |
- {
|
|
| 242 |
- doc: "invalid mirror", |
|
| 243 |
- opts: ServiceOptions{
|
|
| 244 |
- Mirrors: []string{"example.com:5000"},
|
|
| 245 |
- }, |
|
| 246 |
- errStr: `invalid mirror: no scheme specified for "example.com:5000": must use either 'https://' or 'http://'`, |
|
| 247 |
- }, |
|
| 248 |
- {
|
|
| 249 |
- doc: "valid mirror", |
|
| 250 |
- opts: ServiceOptions{
|
|
| 251 |
- Mirrors: []string{"https://example.com:5000"},
|
|
| 252 |
- }, |
|
| 253 |
- }, |
|
| 254 |
- {
|
|
| 255 |
- doc: "invalid insecure registry", |
|
| 256 |
- opts: ServiceOptions{
|
|
| 257 |
- InsecureRegistries: []string{"[fe80::]/64"},
|
|
| 258 |
- }, |
|
| 259 |
- errStr: `insecure registry [fe80::]/64 is not valid: invalid host "[fe80::]/64"`, |
|
| 260 |
- }, |
|
| 261 |
- {
|
|
| 262 |
- doc: "valid insecure registry", |
|
| 263 |
- opts: ServiceOptions{
|
|
| 264 |
- InsecureRegistries: []string{"102.10.8.1/24"},
|
|
| 265 |
- }, |
|
| 266 |
- }, |
|
| 267 |
- } |
|
| 268 |
- |
|
| 269 |
- for _, tc := range tests {
|
|
| 270 |
- t.Run(tc.doc, func(t *testing.T) {
|
|
| 271 |
- _, err := newServiceConfig(tc.opts) |
|
| 272 |
- if tc.errStr != "" {
|
|
| 273 |
- assert.Check(t, is.Error(err, tc.errStr)) |
|
| 274 |
- assert.Check(t, cerrdefs.IsInvalidArgument(err)) |
|
| 275 |
- } else {
|
|
| 276 |
- assert.Check(t, err) |
|
| 277 |
- } |
|
| 278 |
- }) |
|
| 279 |
- } |
|
| 280 |
-} |
|
| 281 |
- |
|
| 282 |
-func TestValidateIndexName(t *testing.T) {
|
|
| 283 |
- valid := []struct {
|
|
| 284 |
- index string |
|
| 285 |
- expect string |
|
| 286 |
- }{
|
|
| 287 |
- {
|
|
| 288 |
- index: "index.docker.io", |
|
| 289 |
- expect: "docker.io", |
|
| 290 |
- }, |
|
| 291 |
- {
|
|
| 292 |
- index: "example.com", |
|
| 293 |
- expect: "example.com", |
|
| 294 |
- }, |
|
| 295 |
- {
|
|
| 296 |
- index: "127.0.0.1:8080", |
|
| 297 |
- expect: "127.0.0.1:8080", |
|
| 298 |
- }, |
|
| 299 |
- {
|
|
| 300 |
- index: "mytest-1.com", |
|
| 301 |
- expect: "mytest-1.com", |
|
| 302 |
- }, |
|
| 303 |
- {
|
|
| 304 |
- index: "mirror-1.example.com/v1/?q=foo", |
|
| 305 |
- expect: "mirror-1.example.com/v1/?q=foo", |
|
| 306 |
- }, |
|
| 307 |
- } |
|
| 308 |
- |
|
| 309 |
- for _, testCase := range valid {
|
|
| 310 |
- result, err := ValidateIndexName(testCase.index) |
|
| 311 |
- if assert.Check(t, err) {
|
|
| 312 |
- assert.Check(t, is.Equal(testCase.expect, result)) |
|
| 313 |
- } |
|
| 314 |
- } |
|
| 315 |
-} |
|
| 316 |
- |
|
| 317 |
-func TestValidateIndexNameWithError(t *testing.T) {
|
|
| 318 |
- invalid := []struct {
|
|
| 319 |
- index string |
|
| 320 |
- err string |
|
| 321 |
- }{
|
|
| 322 |
- {
|
|
| 323 |
- index: "docker.io-", |
|
| 324 |
- err: "invalid index name (docker.io-). Cannot begin or end with a hyphen", |
|
| 325 |
- }, |
|
| 326 |
- {
|
|
| 327 |
- index: "-example.com", |
|
| 328 |
- err: "invalid index name (-example.com). Cannot begin or end with a hyphen", |
|
| 329 |
- }, |
|
| 330 |
- {
|
|
| 331 |
- index: "mirror-1.example.com/v1/?q=foo-", |
|
| 332 |
- err: "invalid index name (mirror-1.example.com/v1/?q=foo-). Cannot begin or end with a hyphen", |
|
| 333 |
- }, |
|
| 334 |
- } |
|
| 335 |
- for _, testCase := range invalid {
|
|
| 336 |
- _, err := ValidateIndexName(testCase.index) |
|
| 337 |
- assert.Check(t, is.Error(err, testCase.err)) |
|
| 338 |
- assert.Check(t, cerrdefs.IsInvalidArgument(err)) |
|
| 339 |
- } |
|
| 340 |
-} |
| 341 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,67 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "net/url" |
|
| 5 |
- |
|
| 6 |
- "github.com/docker/distribution/registry/api/errcode" |
|
| 7 |
- "github.com/pkg/errors" |
|
| 8 |
-) |
|
| 9 |
- |
|
| 10 |
-func translateV2AuthError(err error) error {
|
|
| 11 |
- var e *url.Error |
|
| 12 |
- if errors.As(err, &e) {
|
|
| 13 |
- var e2 errcode.Error |
|
| 14 |
- if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) {
|
|
| 15 |
- return unauthorizedErr{err}
|
|
| 16 |
- } |
|
| 17 |
- } |
|
| 18 |
- return err |
|
| 19 |
-} |
|
| 20 |
- |
|
| 21 |
-func invalidParam(err error) error {
|
|
| 22 |
- return invalidParameterErr{err}
|
|
| 23 |
-} |
|
| 24 |
- |
|
| 25 |
-func invalidParamf(format string, args ...interface{}) error {
|
|
| 26 |
- return invalidParameterErr{errors.Errorf(format, args...)}
|
|
| 27 |
-} |
|
| 28 |
- |
|
| 29 |
-func invalidParamWrapf(err error, format string, args ...interface{}) error {
|
|
| 30 |
- return invalidParameterErr{errors.Wrapf(err, format, args...)}
|
|
| 31 |
-} |
|
| 32 |
- |
|
| 33 |
-type unauthorizedErr struct{ error }
|
|
| 34 |
- |
|
| 35 |
-func (unauthorizedErr) Unauthorized() {}
|
|
| 36 |
- |
|
| 37 |
-func (e unauthorizedErr) Cause() error {
|
|
| 38 |
- return e.error |
|
| 39 |
-} |
|
| 40 |
- |
|
| 41 |
-func (e unauthorizedErr) Unwrap() error {
|
|
| 42 |
- return e.error |
|
| 43 |
-} |
|
| 44 |
- |
|
| 45 |
-type invalidParameterErr struct{ error }
|
|
| 46 |
- |
|
| 47 |
-func (invalidParameterErr) InvalidParameter() {}
|
|
| 48 |
- |
|
| 49 |
-func (e invalidParameterErr) Unwrap() error {
|
|
| 50 |
- return e.error |
|
| 51 |
-} |
|
| 52 |
- |
|
| 53 |
-type systemErr struct{ error }
|
|
| 54 |
- |
|
| 55 |
-func (systemErr) System() {}
|
|
| 56 |
- |
|
| 57 |
-func (e systemErr) Unwrap() error {
|
|
| 58 |
- return e.error |
|
| 59 |
-} |
|
| 60 |
- |
|
| 61 |
-type errUnknown struct{ error }
|
|
| 62 |
- |
|
| 63 |
-func (errUnknown) Unknown() {}
|
|
| 64 |
- |
|
| 65 |
-func (e errUnknown) Unwrap() error {
|
|
| 66 |
- return e.error |
|
| 67 |
-} |
| 68 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,155 +0,0 @@ |
| 1 |
-// Package registry contains client primitives to interact with a remote Docker registry. |
|
| 2 |
-package registry |
|
| 3 |
- |
|
| 4 |
-import ( |
|
| 5 |
- "context" |
|
| 6 |
- "crypto/tls" |
|
| 7 |
- "net" |
|
| 8 |
- "net/http" |
|
| 9 |
- "os" |
|
| 10 |
- "path/filepath" |
|
| 11 |
- "runtime" |
|
| 12 |
- "strings" |
|
| 13 |
- "time" |
|
| 14 |
- |
|
| 15 |
- "github.com/containerd/log" |
|
| 16 |
- "github.com/docker/distribution/registry/client/transport" |
|
| 17 |
- "github.com/docker/go-connections/tlsconfig" |
|
| 18 |
- "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
|
| 19 |
-) |
|
| 20 |
- |
|
| 21 |
-// hostCertsDir returns the config directory for a specific host. |
|
| 22 |
-func hostCertsDir(hostnameAndPort string) string {
|
|
| 23 |
- if runtime.GOOS == "windows" {
|
|
| 24 |
- // Ensure that a directory name is valid; hostnameAndPort may contain |
|
| 25 |
- // a colon (:) if a port is included, and Windows does not allow colons |
|
| 26 |
- // in directory names. |
|
| 27 |
- hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", "")) |
|
| 28 |
- } |
|
| 29 |
- return filepath.Join(CertsDir(), hostnameAndPort) |
|
| 30 |
-} |
|
| 31 |
- |
|
| 32 |
-// newTLSConfig constructs a client TLS configuration based on server defaults |
|
| 33 |
-func newTLSConfig(ctx context.Context, hostname string, isSecure bool) (*tls.Config, error) {
|
|
| 34 |
- // PreferredServerCipherSuites should have no effect |
|
| 35 |
- tlsConfig := tlsconfig.ServerDefault() |
|
| 36 |
- tlsConfig.InsecureSkipVerify = !isSecure |
|
| 37 |
- |
|
| 38 |
- if isSecure {
|
|
| 39 |
- hostDir := hostCertsDir(hostname) |
|
| 40 |
- log.G(ctx).Debugf("hostDir: %s", hostDir)
|
|
| 41 |
- if err := loadTLSConfig(ctx, hostDir, tlsConfig); err != nil {
|
|
| 42 |
- return nil, err |
|
| 43 |
- } |
|
| 44 |
- } |
|
| 45 |
- |
|
| 46 |
- return tlsConfig, nil |
|
| 47 |
-} |
|
| 48 |
- |
|
| 49 |
-func hasFile(files []os.DirEntry, name string) bool {
|
|
| 50 |
- for _, f := range files {
|
|
| 51 |
- if f.Name() == name {
|
|
| 52 |
- return true |
|
| 53 |
- } |
|
| 54 |
- } |
|
| 55 |
- return false |
|
| 56 |
-} |
|
| 57 |
- |
|
| 58 |
-// ReadCertsDirectory reads the directory for TLS certificates |
|
| 59 |
-// including roots and certificate pairs and updates the |
|
| 60 |
-// provided TLS configuration. |
|
| 61 |
-func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
|
|
| 62 |
- return loadTLSConfig(context.TODO(), directory, tlsConfig) |
|
| 63 |
-} |
|
| 64 |
- |
|
| 65 |
-// loadTLSConfig reads the directory for TLS certificates including roots and |
|
| 66 |
-// certificate pairs, and updates the provided TLS configuration. |
|
| 67 |
-func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error {
|
|
| 68 |
- fs, err := os.ReadDir(directory) |
|
| 69 |
- if err != nil {
|
|
| 70 |
- if os.IsNotExist(err) {
|
|
| 71 |
- return nil |
|
| 72 |
- } |
|
| 73 |
- return invalidParam(err) |
|
| 74 |
- } |
|
| 75 |
- |
|
| 76 |
- for _, f := range fs {
|
|
| 77 |
- if ctx.Err() != nil {
|
|
| 78 |
- return ctx.Err() |
|
| 79 |
- } |
|
| 80 |
- switch filepath.Ext(f.Name()) {
|
|
| 81 |
- case ".crt": |
|
| 82 |
- if tlsConfig.RootCAs == nil {
|
|
| 83 |
- systemPool, err := tlsconfig.SystemCertPool() |
|
| 84 |
- if err != nil {
|
|
| 85 |
- return invalidParamWrapf(err, "unable to get system cert pool") |
|
| 86 |
- } |
|
| 87 |
- tlsConfig.RootCAs = systemPool |
|
| 88 |
- } |
|
| 89 |
- fileName := filepath.Join(directory, f.Name()) |
|
| 90 |
- log.G(ctx).Debugf("crt: %s", fileName)
|
|
| 91 |
- data, err := os.ReadFile(fileName) |
|
| 92 |
- if err != nil {
|
|
| 93 |
- return err |
|
| 94 |
- } |
|
| 95 |
- tlsConfig.RootCAs.AppendCertsFromPEM(data) |
|
| 96 |
- case ".cert": |
|
| 97 |
- certName := f.Name() |
|
| 98 |
- keyName := certName[:len(certName)-5] + ".key" |
|
| 99 |
- log.G(ctx).Debugf("cert: %s", filepath.Join(directory, certName))
|
|
| 100 |
- if !hasFile(fs, keyName) {
|
|
| 101 |
- return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
|
|
| 102 |
- } |
|
| 103 |
- cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) |
|
| 104 |
- if err != nil {
|
|
| 105 |
- return err |
|
| 106 |
- } |
|
| 107 |
- tlsConfig.Certificates = append(tlsConfig.Certificates, cert) |
|
| 108 |
- case ".key": |
|
| 109 |
- keyName := f.Name() |
|
| 110 |
- certName := keyName[:len(keyName)-4] + ".cert" |
|
| 111 |
- log.G(ctx).Debugf("key: %s", filepath.Join(directory, keyName))
|
|
| 112 |
- if !hasFile(fs, certName) {
|
|
| 113 |
- return invalidParamf("missing client certificate %s for key %s", certName, keyName)
|
|
| 114 |
- } |
|
| 115 |
- } |
|
| 116 |
- } |
|
| 117 |
- |
|
| 118 |
- return nil |
|
| 119 |
-} |
|
| 120 |
- |
|
| 121 |
-// Headers returns request modifiers with a User-Agent and metaHeaders |
|
| 122 |
-func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier {
|
|
| 123 |
- modifiers := []transport.RequestModifier{}
|
|
| 124 |
- if userAgent != "" {
|
|
| 125 |
- modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{
|
|
| 126 |
- "User-Agent": []string{userAgent},
|
|
| 127 |
- })) |
|
| 128 |
- } |
|
| 129 |
- if metaHeaders != nil {
|
|
| 130 |
- modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) |
|
| 131 |
- } |
|
| 132 |
- return modifiers |
|
| 133 |
-} |
|
| 134 |
- |
|
| 135 |
-// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the |
|
| 136 |
-// default TLS configuration. |
|
| 137 |
-func newTransport(tlsConfig *tls.Config) http.RoundTripper {
|
|
| 138 |
- if tlsConfig == nil {
|
|
| 139 |
- tlsConfig = tlsconfig.ServerDefault() |
|
| 140 |
- } |
|
| 141 |
- |
|
| 142 |
- return otelhttp.NewTransport( |
|
| 143 |
- &http.Transport{
|
|
| 144 |
- Proxy: http.ProxyFromEnvironment, |
|
| 145 |
- DialContext: (&net.Dialer{
|
|
| 146 |
- Timeout: 30 * time.Second, |
|
| 147 |
- KeepAlive: 30 * time.Second, |
|
| 148 |
- }).DialContext, |
|
| 149 |
- TLSHandshakeTimeout: 10 * time.Second, |
|
| 150 |
- TLSClientConfig: tlsConfig, |
|
| 151 |
- // TODO(dmcgowan): Call close idle connections when complete and use keep alive |
|
| 152 |
- DisableKeepAlives: true, |
|
| 153 |
- }, |
|
| 154 |
- ) |
|
| 155 |
-} |
| 156 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,120 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "encoding/json" |
|
| 6 |
- "io" |
|
| 7 |
- "net/http" |
|
| 8 |
- "net/http/httptest" |
|
| 9 |
- "testing" |
|
| 10 |
- |
|
| 11 |
- "github.com/containerd/log" |
|
| 12 |
- "github.com/moby/moby/api/types/registry" |
|
| 13 |
- "gotest.tools/v3/assert" |
|
| 14 |
-) |
|
| 15 |
- |
|
| 16 |
-var ( |
|
| 17 |
- testHTTPServer *httptest.Server |
|
| 18 |
- testHTTPSServer *httptest.Server |
|
| 19 |
-) |
|
| 20 |
- |
|
| 21 |
-func init() {
|
|
| 22 |
- r := http.NewServeMux() |
|
| 23 |
- |
|
| 24 |
- // /v1/ |
|
| 25 |
- r.HandleFunc("/v1/_ping", handlerGetPing)
|
|
| 26 |
- r.HandleFunc("/v1/search", handlerSearch)
|
|
| 27 |
- |
|
| 28 |
- // /v2/ |
|
| 29 |
- r.HandleFunc("/v2/version", handlerGetPing)
|
|
| 30 |
- |
|
| 31 |
- testHTTPServer = httptest.NewServer(handlerAccessLog(r)) |
|
| 32 |
- testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r)) |
|
| 33 |
-} |
|
| 34 |
- |
|
| 35 |
-func handlerAccessLog(handler http.Handler) http.Handler {
|
|
| 36 |
- logHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
| 37 |
- log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL) |
|
| 38 |
- handler.ServeHTTP(w, r) |
|
| 39 |
- } |
|
| 40 |
- return http.HandlerFunc(logHandler) |
|
| 41 |
-} |
|
| 42 |
- |
|
| 43 |
-func makeURL(req string) string {
|
|
| 44 |
- return testHTTPServer.URL + req |
|
| 45 |
-} |
|
| 46 |
- |
|
| 47 |
-func makeHTTPSURL(req string) string {
|
|
| 48 |
- return testHTTPSServer.URL + req |
|
| 49 |
-} |
|
| 50 |
- |
|
| 51 |
-func makeIndex(req string) *registry.IndexInfo {
|
|
| 52 |
- return ®istry.IndexInfo{
|
|
| 53 |
- Name: makeURL(req), |
|
| 54 |
- } |
|
| 55 |
-} |
|
| 56 |
- |
|
| 57 |
-func makeHTTPSIndex(req string) *registry.IndexInfo {
|
|
| 58 |
- return ®istry.IndexInfo{
|
|
| 59 |
- Name: makeHTTPSURL(req), |
|
| 60 |
- } |
|
| 61 |
-} |
|
| 62 |
- |
|
| 63 |
-func makePublicIndex() *registry.IndexInfo {
|
|
| 64 |
- return ®istry.IndexInfo{
|
|
| 65 |
- Name: IndexServer, |
|
| 66 |
- Secure: true, |
|
| 67 |
- Official: true, |
|
| 68 |
- } |
|
| 69 |
-} |
|
| 70 |
- |
|
| 71 |
-func writeHeaders(w http.ResponseWriter) {
|
|
| 72 |
- h := w.Header() |
|
| 73 |
- h.Add("Server", "docker-tests/mock")
|
|
| 74 |
- h.Add("Expires", "-1")
|
|
| 75 |
- h.Add("Content-Type", "application/json")
|
|
| 76 |
- h.Add("Pragma", "no-cache")
|
|
| 77 |
- h.Add("Cache-Control", "no-cache")
|
|
| 78 |
-} |
|
| 79 |
- |
|
| 80 |
-func writeResponse(w http.ResponseWriter, message interface{}, code int) {
|
|
| 81 |
- writeHeaders(w) |
|
| 82 |
- w.WriteHeader(code) |
|
| 83 |
- body, err := json.Marshal(message) |
|
| 84 |
- if err != nil {
|
|
| 85 |
- _, _ = io.WriteString(w, err.Error()) |
|
| 86 |
- return |
|
| 87 |
- } |
|
| 88 |
- _, _ = w.Write(body) |
|
| 89 |
-} |
|
| 90 |
- |
|
| 91 |
-func handlerGetPing(w http.ResponseWriter, r *http.Request) {
|
|
| 92 |
- if r.Method != http.MethodGet {
|
|
| 93 |
- writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) |
|
| 94 |
- return |
|
| 95 |
- } |
|
| 96 |
- writeResponse(w, true, http.StatusOK) |
|
| 97 |
-} |
|
| 98 |
- |
|
| 99 |
-func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
|
| 100 |
- if r.Method != http.MethodGet {
|
|
| 101 |
- writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) |
|
| 102 |
- return |
|
| 103 |
- } |
|
| 104 |
- result := ®istry.SearchResults{
|
|
| 105 |
- Query: "fakequery", |
|
| 106 |
- NumResults: 1, |
|
| 107 |
- Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
|
| 108 |
- } |
|
| 109 |
- writeResponse(w, result, http.StatusOK) |
|
| 110 |
-} |
|
| 111 |
- |
|
| 112 |
-func TestPing(t *testing.T) {
|
|
| 113 |
- res, err := http.Get(makeURL("/v1/_ping"))
|
|
| 114 |
- if err != nil {
|
|
| 115 |
- t.Fatal(err) |
|
| 116 |
- } |
|
| 117 |
- assert.Equal(t, res.StatusCode, http.StatusOK, "") |
|
| 118 |
- assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
|
|
| 119 |
- _ = res.Body.Close() |
|
| 120 |
-} |
| 121 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,637 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "errors" |
|
| 5 |
- "net" |
|
| 6 |
- "testing" |
|
| 7 |
- |
|
| 8 |
- "github.com/distribution/reference" |
|
| 9 |
- "github.com/moby/moby/api/types/registry" |
|
| 10 |
- "gotest.tools/v3/assert" |
|
| 11 |
- is "gotest.tools/v3/assert/cmp" |
|
| 12 |
-) |
|
| 13 |
- |
|
| 14 |
-// overrideLookupIP overrides net.LookupIP for testing. |
|
| 15 |
-func overrideLookupIP(t *testing.T) {
|
|
| 16 |
- t.Helper() |
|
| 17 |
- restoreLookup := lookupIP |
|
| 18 |
- |
|
| 19 |
- // override net.LookupIP |
|
| 20 |
- lookupIP = func(host string) ([]net.IP, error) {
|
|
| 21 |
- mockHosts := map[string][]net.IP{
|
|
| 22 |
- "": {net.ParseIP("0.0.0.0")},
|
|
| 23 |
- "localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
|
| 24 |
- "example.com": {net.ParseIP("42.42.42.42")},
|
|
| 25 |
- "other.com": {net.ParseIP("43.43.43.43")},
|
|
| 26 |
- } |
|
| 27 |
- if addrs, ok := mockHosts[host]; ok {
|
|
| 28 |
- return addrs, nil |
|
| 29 |
- } |
|
| 30 |
- return nil, errors.New("lookup: no such host")
|
|
| 31 |
- } |
|
| 32 |
- t.Cleanup(func() {
|
|
| 33 |
- lookupIP = restoreLookup |
|
| 34 |
- }) |
|
| 35 |
-} |
|
| 36 |
- |
|
| 37 |
-func TestParseRepositoryInfo(t *testing.T) {
|
|
| 38 |
- type staticRepositoryInfo struct {
|
|
| 39 |
- Index *registry.IndexInfo |
|
| 40 |
- RemoteName string |
|
| 41 |
- CanonicalName string |
|
| 42 |
- LocalName string |
|
| 43 |
- } |
|
| 44 |
- |
|
| 45 |
- tests := map[string]staticRepositoryInfo{
|
|
| 46 |
- "fooo/bar": {
|
|
| 47 |
- Index: ®istry.IndexInfo{
|
|
| 48 |
- Name: IndexName, |
|
| 49 |
- Mirrors: []string{},
|
|
| 50 |
- Official: true, |
|
| 51 |
- Secure: true, |
|
| 52 |
- }, |
|
| 53 |
- RemoteName: "fooo/bar", |
|
| 54 |
- LocalName: "fooo/bar", |
|
| 55 |
- CanonicalName: "docker.io/fooo/bar", |
|
| 56 |
- }, |
|
| 57 |
- "library/ubuntu": {
|
|
| 58 |
- Index: ®istry.IndexInfo{
|
|
| 59 |
- Name: IndexName, |
|
| 60 |
- Mirrors: []string{},
|
|
| 61 |
- Official: true, |
|
| 62 |
- Secure: true, |
|
| 63 |
- }, |
|
| 64 |
- RemoteName: "library/ubuntu", |
|
| 65 |
- LocalName: "ubuntu", |
|
| 66 |
- CanonicalName: "docker.io/library/ubuntu", |
|
| 67 |
- }, |
|
| 68 |
- "nonlibrary/ubuntu": {
|
|
| 69 |
- Index: ®istry.IndexInfo{
|
|
| 70 |
- Name: IndexName, |
|
| 71 |
- Mirrors: []string{},
|
|
| 72 |
- Official: true, |
|
| 73 |
- Secure: true, |
|
| 74 |
- }, |
|
| 75 |
- RemoteName: "nonlibrary/ubuntu", |
|
| 76 |
- LocalName: "nonlibrary/ubuntu", |
|
| 77 |
- CanonicalName: "docker.io/nonlibrary/ubuntu", |
|
| 78 |
- }, |
|
| 79 |
- "ubuntu": {
|
|
| 80 |
- Index: ®istry.IndexInfo{
|
|
| 81 |
- Name: IndexName, |
|
| 82 |
- Mirrors: []string{},
|
|
| 83 |
- Official: true, |
|
| 84 |
- Secure: true, |
|
| 85 |
- }, |
|
| 86 |
- RemoteName: "library/ubuntu", |
|
| 87 |
- LocalName: "ubuntu", |
|
| 88 |
- CanonicalName: "docker.io/library/ubuntu", |
|
| 89 |
- }, |
|
| 90 |
- "other/library": {
|
|
| 91 |
- Index: ®istry.IndexInfo{
|
|
| 92 |
- Name: IndexName, |
|
| 93 |
- Mirrors: []string{},
|
|
| 94 |
- Official: true, |
|
| 95 |
- Secure: true, |
|
| 96 |
- }, |
|
| 97 |
- RemoteName: "other/library", |
|
| 98 |
- LocalName: "other/library", |
|
| 99 |
- CanonicalName: "docker.io/other/library", |
|
| 100 |
- }, |
|
| 101 |
- "127.0.0.1:8000/private/moonbase": {
|
|
| 102 |
- Index: ®istry.IndexInfo{
|
|
| 103 |
- Name: "127.0.0.1:8000", |
|
| 104 |
- Mirrors: []string{},
|
|
| 105 |
- Official: false, |
|
| 106 |
- Secure: false, |
|
| 107 |
- }, |
|
| 108 |
- RemoteName: "private/moonbase", |
|
| 109 |
- LocalName: "127.0.0.1:8000/private/moonbase", |
|
| 110 |
- CanonicalName: "127.0.0.1:8000/private/moonbase", |
|
| 111 |
- }, |
|
| 112 |
- "127.0.0.1:8000/privatebase": {
|
|
| 113 |
- Index: ®istry.IndexInfo{
|
|
| 114 |
- Name: "127.0.0.1:8000", |
|
| 115 |
- Mirrors: []string{},
|
|
| 116 |
- Official: false, |
|
| 117 |
- Secure: false, |
|
| 118 |
- }, |
|
| 119 |
- RemoteName: "privatebase", |
|
| 120 |
- LocalName: "127.0.0.1:8000/privatebase", |
|
| 121 |
- CanonicalName: "127.0.0.1:8000/privatebase", |
|
| 122 |
- }, |
|
| 123 |
- "[::1]:8000/private/moonbase": {
|
|
| 124 |
- Index: ®istry.IndexInfo{
|
|
| 125 |
- Name: "[::1]:8000", |
|
| 126 |
- Mirrors: []string{},
|
|
| 127 |
- Official: false, |
|
| 128 |
- Secure: false, |
|
| 129 |
- }, |
|
| 130 |
- RemoteName: "private/moonbase", |
|
| 131 |
- LocalName: "[::1]:8000/private/moonbase", |
|
| 132 |
- CanonicalName: "[::1]:8000/private/moonbase", |
|
| 133 |
- }, |
|
| 134 |
- "[::1]:8000/privatebase": {
|
|
| 135 |
- Index: ®istry.IndexInfo{
|
|
| 136 |
- Name: "[::1]:8000", |
|
| 137 |
- Mirrors: []string{},
|
|
| 138 |
- Official: false, |
|
| 139 |
- Secure: false, |
|
| 140 |
- }, |
|
| 141 |
- RemoteName: "privatebase", |
|
| 142 |
- LocalName: "[::1]:8000/privatebase", |
|
| 143 |
- CanonicalName: "[::1]:8000/privatebase", |
|
| 144 |
- }, |
|
| 145 |
- // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 146 |
- // hence not marked "insecure". |
|
| 147 |
- "[::2]:8000/private/moonbase": {
|
|
| 148 |
- Index: ®istry.IndexInfo{
|
|
| 149 |
- Name: "[::2]:8000", |
|
| 150 |
- Mirrors: []string{},
|
|
| 151 |
- Official: false, |
|
| 152 |
- Secure: true, |
|
| 153 |
- }, |
|
| 154 |
- RemoteName: "private/moonbase", |
|
| 155 |
- LocalName: "[::2]:8000/private/moonbase", |
|
| 156 |
- CanonicalName: "[::2]:8000/private/moonbase", |
|
| 157 |
- }, |
|
| 158 |
- // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 159 |
- // hence not marked "insecure". |
|
| 160 |
- "[::2]:8000/privatebase": {
|
|
| 161 |
- Index: ®istry.IndexInfo{
|
|
| 162 |
- Name: "[::2]:8000", |
|
| 163 |
- Mirrors: []string{},
|
|
| 164 |
- Official: false, |
|
| 165 |
- Secure: true, |
|
| 166 |
- }, |
|
| 167 |
- RemoteName: "privatebase", |
|
| 168 |
- LocalName: "[::2]:8000/privatebase", |
|
| 169 |
- CanonicalName: "[::2]:8000/privatebase", |
|
| 170 |
- }, |
|
| 171 |
- "localhost:8000/private/moonbase": {
|
|
| 172 |
- Index: ®istry.IndexInfo{
|
|
| 173 |
- Name: "localhost:8000", |
|
| 174 |
- Mirrors: []string{},
|
|
| 175 |
- Official: false, |
|
| 176 |
- Secure: false, |
|
| 177 |
- }, |
|
| 178 |
- RemoteName: "private/moonbase", |
|
| 179 |
- LocalName: "localhost:8000/private/moonbase", |
|
| 180 |
- CanonicalName: "localhost:8000/private/moonbase", |
|
| 181 |
- }, |
|
| 182 |
- "localhost:8000/privatebase": {
|
|
| 183 |
- Index: ®istry.IndexInfo{
|
|
| 184 |
- Name: "localhost:8000", |
|
| 185 |
- Mirrors: []string{},
|
|
| 186 |
- Official: false, |
|
| 187 |
- Secure: false, |
|
| 188 |
- }, |
|
| 189 |
- RemoteName: "privatebase", |
|
| 190 |
- LocalName: "localhost:8000/privatebase", |
|
| 191 |
- CanonicalName: "localhost:8000/privatebase", |
|
| 192 |
- }, |
|
| 193 |
- "example.com/private/moonbase": {
|
|
| 194 |
- Index: ®istry.IndexInfo{
|
|
| 195 |
- Name: "example.com", |
|
| 196 |
- Mirrors: []string{},
|
|
| 197 |
- Official: false, |
|
| 198 |
- Secure: true, |
|
| 199 |
- }, |
|
| 200 |
- RemoteName: "private/moonbase", |
|
| 201 |
- LocalName: "example.com/private/moonbase", |
|
| 202 |
- CanonicalName: "example.com/private/moonbase", |
|
| 203 |
- }, |
|
| 204 |
- "example.com/privatebase": {
|
|
| 205 |
- Index: ®istry.IndexInfo{
|
|
| 206 |
- Name: "example.com", |
|
| 207 |
- Mirrors: []string{},
|
|
| 208 |
- Official: false, |
|
| 209 |
- Secure: true, |
|
| 210 |
- }, |
|
| 211 |
- RemoteName: "privatebase", |
|
| 212 |
- LocalName: "example.com/privatebase", |
|
| 213 |
- CanonicalName: "example.com/privatebase", |
|
| 214 |
- }, |
|
| 215 |
- "example.com:8000/private/moonbase": {
|
|
| 216 |
- Index: ®istry.IndexInfo{
|
|
| 217 |
- Name: "example.com:8000", |
|
| 218 |
- Mirrors: []string{},
|
|
| 219 |
- Official: false, |
|
| 220 |
- Secure: true, |
|
| 221 |
- }, |
|
| 222 |
- RemoteName: "private/moonbase", |
|
| 223 |
- LocalName: "example.com:8000/private/moonbase", |
|
| 224 |
- CanonicalName: "example.com:8000/private/moonbase", |
|
| 225 |
- }, |
|
| 226 |
- "example.com:8000/privatebase": {
|
|
| 227 |
- Index: ®istry.IndexInfo{
|
|
| 228 |
- Name: "example.com:8000", |
|
| 229 |
- Mirrors: []string{},
|
|
| 230 |
- Official: false, |
|
| 231 |
- Secure: true, |
|
| 232 |
- }, |
|
| 233 |
- RemoteName: "privatebase", |
|
| 234 |
- LocalName: "example.com:8000/privatebase", |
|
| 235 |
- CanonicalName: "example.com:8000/privatebase", |
|
| 236 |
- }, |
|
| 237 |
- "localhost/private/moonbase": {
|
|
| 238 |
- Index: ®istry.IndexInfo{
|
|
| 239 |
- Name: "localhost", |
|
| 240 |
- Mirrors: []string{},
|
|
| 241 |
- Official: false, |
|
| 242 |
- Secure: false, |
|
| 243 |
- }, |
|
| 244 |
- RemoteName: "private/moonbase", |
|
| 245 |
- LocalName: "localhost/private/moonbase", |
|
| 246 |
- CanonicalName: "localhost/private/moonbase", |
|
| 247 |
- }, |
|
| 248 |
- "localhost/privatebase": {
|
|
| 249 |
- Index: ®istry.IndexInfo{
|
|
| 250 |
- Name: "localhost", |
|
| 251 |
- Mirrors: []string{},
|
|
| 252 |
- Official: false, |
|
| 253 |
- Secure: false, |
|
| 254 |
- }, |
|
| 255 |
- RemoteName: "privatebase", |
|
| 256 |
- LocalName: "localhost/privatebase", |
|
| 257 |
- CanonicalName: "localhost/privatebase", |
|
| 258 |
- }, |
|
| 259 |
- IndexName + "/public/moonbase": {
|
|
| 260 |
- Index: ®istry.IndexInfo{
|
|
| 261 |
- Name: IndexName, |
|
| 262 |
- Mirrors: []string{},
|
|
| 263 |
- Official: true, |
|
| 264 |
- Secure: true, |
|
| 265 |
- }, |
|
| 266 |
- RemoteName: "public/moonbase", |
|
| 267 |
- LocalName: "public/moonbase", |
|
| 268 |
- CanonicalName: "docker.io/public/moonbase", |
|
| 269 |
- }, |
|
| 270 |
- "index." + IndexName + "/public/moonbase": {
|
|
| 271 |
- Index: ®istry.IndexInfo{
|
|
| 272 |
- Name: IndexName, |
|
| 273 |
- Mirrors: []string{},
|
|
| 274 |
- Official: true, |
|
| 275 |
- Secure: true, |
|
| 276 |
- }, |
|
| 277 |
- RemoteName: "public/moonbase", |
|
| 278 |
- LocalName: "public/moonbase", |
|
| 279 |
- CanonicalName: "docker.io/public/moonbase", |
|
| 280 |
- }, |
|
| 281 |
- "ubuntu-12.04-base": {
|
|
| 282 |
- Index: ®istry.IndexInfo{
|
|
| 283 |
- Name: IndexName, |
|
| 284 |
- Mirrors: []string{},
|
|
| 285 |
- Official: true, |
|
| 286 |
- Secure: true, |
|
| 287 |
- }, |
|
| 288 |
- RemoteName: "library/ubuntu-12.04-base", |
|
| 289 |
- LocalName: "ubuntu-12.04-base", |
|
| 290 |
- CanonicalName: "docker.io/library/ubuntu-12.04-base", |
|
| 291 |
- }, |
|
| 292 |
- IndexName + "/ubuntu-12.04-base": {
|
|
| 293 |
- Index: ®istry.IndexInfo{
|
|
| 294 |
- Name: IndexName, |
|
| 295 |
- Mirrors: []string{},
|
|
| 296 |
- Official: true, |
|
| 297 |
- Secure: true, |
|
| 298 |
- }, |
|
| 299 |
- RemoteName: "library/ubuntu-12.04-base", |
|
| 300 |
- LocalName: "ubuntu-12.04-base", |
|
| 301 |
- CanonicalName: "docker.io/library/ubuntu-12.04-base", |
|
| 302 |
- }, |
|
| 303 |
- "index." + IndexName + "/ubuntu-12.04-base": {
|
|
| 304 |
- Index: ®istry.IndexInfo{
|
|
| 305 |
- Name: IndexName, |
|
| 306 |
- Mirrors: []string{},
|
|
| 307 |
- Official: true, |
|
| 308 |
- Secure: true, |
|
| 309 |
- }, |
|
| 310 |
- RemoteName: "library/ubuntu-12.04-base", |
|
| 311 |
- LocalName: "ubuntu-12.04-base", |
|
| 312 |
- CanonicalName: "docker.io/library/ubuntu-12.04-base", |
|
| 313 |
- }, |
|
| 314 |
- } |
|
| 315 |
- |
|
| 316 |
- for reposName, expected := range tests {
|
|
| 317 |
- t.Run(reposName, func(t *testing.T) {
|
|
| 318 |
- named, err := reference.ParseNormalizedNamed(reposName) |
|
| 319 |
- assert.NilError(t, err) |
|
| 320 |
- |
|
| 321 |
- repoInfo, err := ParseRepositoryInfo(named) |
|
| 322 |
- assert.NilError(t, err) |
|
| 323 |
- |
|
| 324 |
- assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index)) |
|
| 325 |
- assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName)) |
|
| 326 |
- assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName)) |
|
| 327 |
- assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName)) |
|
| 328 |
- }) |
|
| 329 |
- } |
|
| 330 |
-} |
|
| 331 |
- |
|
| 332 |
-func TestNewIndexInfo(t *testing.T) {
|
|
| 333 |
- overrideLookupIP(t) |
|
| 334 |
- |
|
| 335 |
- // ipv6Loopback is the CIDR for the IPv6 loopback address ("::1"); "::1/128"
|
|
| 336 |
- ipv6Loopback := &net.IPNet{
|
|
| 337 |
- IP: net.IPv6loopback, |
|
| 338 |
- Mask: net.CIDRMask(128, 128), |
|
| 339 |
- } |
|
| 340 |
- |
|
| 341 |
- // ipv4Loopback is the CIDR for IPv4 loopback addresses ("127.0.0.0/8")
|
|
| 342 |
- ipv4Loopback := &net.IPNet{
|
|
| 343 |
- IP: net.IPv4(127, 0, 0, 0), |
|
| 344 |
- Mask: net.CIDRMask(8, 32), |
|
| 345 |
- } |
|
| 346 |
- |
|
| 347 |
- // emptyServiceConfig is a default service-config for situations where |
|
| 348 |
- // no config-file is available (e.g. when used in the CLI). It won't |
|
| 349 |
- // have mirrors configured, but does have the default insecure registry |
|
| 350 |
- // CIDRs for loopback interfaces configured. |
|
| 351 |
- emptyServiceConfig := &serviceConfig{
|
|
| 352 |
- IndexConfigs: map[string]*registry.IndexInfo{
|
|
| 353 |
- IndexName: {
|
|
| 354 |
- Name: IndexName, |
|
| 355 |
- Mirrors: []string{},
|
|
| 356 |
- Secure: true, |
|
| 357 |
- Official: true, |
|
| 358 |
- }, |
|
| 359 |
- }, |
|
| 360 |
- InsecureRegistryCIDRs: []*registry.NetIPNet{
|
|
| 361 |
- (*registry.NetIPNet)(ipv6Loopback), |
|
| 362 |
- (*registry.NetIPNet)(ipv4Loopback), |
|
| 363 |
- }, |
|
| 364 |
- } |
|
| 365 |
- |
|
| 366 |
- expectedIndexInfos := map[string]*registry.IndexInfo{
|
|
| 367 |
- IndexName: {
|
|
| 368 |
- Name: IndexName, |
|
| 369 |
- Official: true, |
|
| 370 |
- Secure: true, |
|
| 371 |
- Mirrors: []string{},
|
|
| 372 |
- }, |
|
| 373 |
- "index." + IndexName: {
|
|
| 374 |
- Name: IndexName, |
|
| 375 |
- Official: true, |
|
| 376 |
- Secure: true, |
|
| 377 |
- Mirrors: []string{},
|
|
| 378 |
- }, |
|
| 379 |
- "example.com": {
|
|
| 380 |
- Name: "example.com", |
|
| 381 |
- Official: false, |
|
| 382 |
- Secure: true, |
|
| 383 |
- Mirrors: []string{},
|
|
| 384 |
- }, |
|
| 385 |
- "127.0.0.1:5000": {
|
|
| 386 |
- Name: "127.0.0.1:5000", |
|
| 387 |
- Official: false, |
|
| 388 |
- Secure: false, |
|
| 389 |
- Mirrors: []string{},
|
|
| 390 |
- }, |
|
| 391 |
- } |
|
| 392 |
- t.Run("no mirrors", func(t *testing.T) {
|
|
| 393 |
- for indexName, expected := range expectedIndexInfos {
|
|
| 394 |
- t.Run(indexName, func(t *testing.T) {
|
|
| 395 |
- actual := newIndexInfo(emptyServiceConfig, indexName) |
|
| 396 |
- assert.Check(t, is.DeepEqual(actual, expected)) |
|
| 397 |
- }) |
|
| 398 |
- } |
|
| 399 |
- }) |
|
| 400 |
- |
|
| 401 |
- expectedIndexInfos = map[string]*registry.IndexInfo{
|
|
| 402 |
- IndexName: {
|
|
| 403 |
- Name: IndexName, |
|
| 404 |
- Official: true, |
|
| 405 |
- Secure: true, |
|
| 406 |
- Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
|
| 407 |
- }, |
|
| 408 |
- "index." + IndexName: {
|
|
| 409 |
- Name: IndexName, |
|
| 410 |
- Official: true, |
|
| 411 |
- Secure: true, |
|
| 412 |
- Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
|
| 413 |
- }, |
|
| 414 |
- "example.com": {
|
|
| 415 |
- Name: "example.com", |
|
| 416 |
- Official: false, |
|
| 417 |
- Secure: false, |
|
| 418 |
- Mirrors: []string{},
|
|
| 419 |
- }, |
|
| 420 |
- "example.com:5000": {
|
|
| 421 |
- Name: "example.com:5000", |
|
| 422 |
- Official: false, |
|
| 423 |
- Secure: true, |
|
| 424 |
- Mirrors: []string{},
|
|
| 425 |
- }, |
|
| 426 |
- "127.0.0.1": {
|
|
| 427 |
- Name: "127.0.0.1", |
|
| 428 |
- Official: false, |
|
| 429 |
- Secure: false, |
|
| 430 |
- Mirrors: []string{},
|
|
| 431 |
- }, |
|
| 432 |
- "127.0.0.1:5000": {
|
|
| 433 |
- Name: "127.0.0.1:5000", |
|
| 434 |
- Official: false, |
|
| 435 |
- Secure: false, |
|
| 436 |
- Mirrors: []string{},
|
|
| 437 |
- }, |
|
| 438 |
- "127.255.255.255": {
|
|
| 439 |
- Name: "127.255.255.255", |
|
| 440 |
- Official: false, |
|
| 441 |
- Secure: false, |
|
| 442 |
- Mirrors: []string{},
|
|
| 443 |
- }, |
|
| 444 |
- "127.255.255.255:5000": {
|
|
| 445 |
- Name: "127.255.255.255:5000", |
|
| 446 |
- Official: false, |
|
| 447 |
- Secure: false, |
|
| 448 |
- Mirrors: []string{},
|
|
| 449 |
- }, |
|
| 450 |
- "::1": {
|
|
| 451 |
- Name: "::1", |
|
| 452 |
- Official: false, |
|
| 453 |
- Secure: false, |
|
| 454 |
- Mirrors: []string{},
|
|
| 455 |
- }, |
|
| 456 |
- "[::1]:5000": {
|
|
| 457 |
- Name: "[::1]:5000", |
|
| 458 |
- Official: false, |
|
| 459 |
- Secure: false, |
|
| 460 |
- Mirrors: []string{},
|
|
| 461 |
- }, |
|
| 462 |
- // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 463 |
- // hence not marked "insecure". |
|
| 464 |
- "::2": {
|
|
| 465 |
- Name: "::2", |
|
| 466 |
- Official: false, |
|
| 467 |
- Secure: true, |
|
| 468 |
- Mirrors: []string{},
|
|
| 469 |
- }, |
|
| 470 |
- // IPv6 only has a single loopback address, so ::2 is not a loopback, |
|
| 471 |
- // hence not marked "insecure". |
|
| 472 |
- "[::2]:5000": {
|
|
| 473 |
- Name: "[::2]:5000", |
|
| 474 |
- Official: false, |
|
| 475 |
- Secure: true, |
|
| 476 |
- Mirrors: []string{},
|
|
| 477 |
- }, |
|
| 478 |
- "other.com": {
|
|
| 479 |
- Name: "other.com", |
|
| 480 |
- Official: false, |
|
| 481 |
- Secure: true, |
|
| 482 |
- Mirrors: []string{},
|
|
| 483 |
- }, |
|
| 484 |
- } |
|
| 485 |
- t.Run("mirrors", func(t *testing.T) {
|
|
| 486 |
- // Note that newServiceConfig calls ValidateMirror internally, which normalizes |
|
| 487 |
- // mirror-URLs to have a trailing slash. |
|
| 488 |
- config, err := newServiceConfig(ServiceOptions{
|
|
| 489 |
- Mirrors: []string{"http://mirror1.local", "http://mirror2.local"},
|
|
| 490 |
- InsecureRegistries: []string{"example.com"},
|
|
| 491 |
- }) |
|
| 492 |
- assert.NilError(t, err) |
|
| 493 |
- for indexName, expected := range expectedIndexInfos {
|
|
| 494 |
- t.Run(indexName, func(t *testing.T) {
|
|
| 495 |
- actual := newIndexInfo(config, indexName) |
|
| 496 |
- assert.Check(t, is.DeepEqual(actual, expected)) |
|
| 497 |
- }) |
|
| 498 |
- } |
|
| 499 |
- }) |
|
| 500 |
- |
|
| 501 |
- expectedIndexInfos = map[string]*registry.IndexInfo{
|
|
| 502 |
- "example.com": {
|
|
| 503 |
- Name: "example.com", |
|
| 504 |
- Official: false, |
|
| 505 |
- Secure: false, |
|
| 506 |
- Mirrors: []string{},
|
|
| 507 |
- }, |
|
| 508 |
- "example.com:5000": {
|
|
| 509 |
- Name: "example.com:5000", |
|
| 510 |
- Official: false, |
|
| 511 |
- Secure: false, |
|
| 512 |
- Mirrors: []string{},
|
|
| 513 |
- }, |
|
| 514 |
- "127.0.0.1": {
|
|
| 515 |
- Name: "127.0.0.1", |
|
| 516 |
- Official: false, |
|
| 517 |
- Secure: false, |
|
| 518 |
- Mirrors: []string{},
|
|
| 519 |
- }, |
|
| 520 |
- "127.0.0.1:5000": {
|
|
| 521 |
- Name: "127.0.0.1:5000", |
|
| 522 |
- Official: false, |
|
| 523 |
- Secure: false, |
|
| 524 |
- Mirrors: []string{},
|
|
| 525 |
- }, |
|
| 526 |
- "42.42.0.1:5000": {
|
|
| 527 |
- Name: "42.42.0.1:5000", |
|
| 528 |
- Official: false, |
|
| 529 |
- Secure: false, |
|
| 530 |
- Mirrors: []string{},
|
|
| 531 |
- }, |
|
| 532 |
- "42.43.0.1:5000": {
|
|
| 533 |
- Name: "42.43.0.1:5000", |
|
| 534 |
- Official: false, |
|
| 535 |
- Secure: true, |
|
| 536 |
- Mirrors: []string{},
|
|
| 537 |
- }, |
|
| 538 |
- "other.com": {
|
|
| 539 |
- Name: "other.com", |
|
| 540 |
- Official: false, |
|
| 541 |
- Secure: true, |
|
| 542 |
- Mirrors: []string{},
|
|
| 543 |
- }, |
|
| 544 |
- } |
|
| 545 |
- t.Run("custom insecure", func(t *testing.T) {
|
|
| 546 |
- config, err := newServiceConfig(ServiceOptions{
|
|
| 547 |
- InsecureRegistries: []string{"42.42.0.0/16"},
|
|
| 548 |
- }) |
|
| 549 |
- assert.NilError(t, err) |
|
| 550 |
- for indexName, expected := range expectedIndexInfos {
|
|
| 551 |
- t.Run(indexName, func(t *testing.T) {
|
|
| 552 |
- actual := newIndexInfo(config, indexName) |
|
| 553 |
- assert.Check(t, is.DeepEqual(actual, expected)) |
|
| 554 |
- }) |
|
| 555 |
- } |
|
| 556 |
- }) |
|
| 557 |
-} |
|
| 558 |
- |
|
| 559 |
-func TestMirrorEndpointLookup(t *testing.T) {
|
|
| 560 |
- containsMirror := func(endpoints []APIEndpoint) bool {
|
|
| 561 |
- for _, pe := range endpoints {
|
|
| 562 |
- if pe.URL.Host == "my.mirror" {
|
|
| 563 |
- return true |
|
| 564 |
- } |
|
| 565 |
- } |
|
| 566 |
- return false |
|
| 567 |
- } |
|
| 568 |
- cfg, err := newServiceConfig(ServiceOptions{
|
|
| 569 |
- Mirrors: []string{"https://my.mirror"},
|
|
| 570 |
- }) |
|
| 571 |
- assert.NilError(t, err) |
|
| 572 |
- s := Service{config: cfg}
|
|
| 573 |
- |
|
| 574 |
- imageName, err := reference.WithName(IndexName + "/test/image") |
|
| 575 |
- if err != nil {
|
|
| 576 |
- t.Error(err) |
|
| 577 |
- } |
|
| 578 |
- pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName)) |
|
| 579 |
- if err != nil {
|
|
| 580 |
- t.Fatal(err) |
|
| 581 |
- } |
|
| 582 |
- if containsMirror(pushAPIEndpoints) {
|
|
| 583 |
- t.Fatal("Push endpoint should not contain mirror")
|
|
| 584 |
- } |
|
| 585 |
- |
|
| 586 |
- pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName)) |
|
| 587 |
- if err != nil {
|
|
| 588 |
- t.Fatal(err) |
|
| 589 |
- } |
|
| 590 |
- if !containsMirror(pullAPIEndpoints) {
|
|
| 591 |
- t.Fatal("Pull endpoint should contain mirror")
|
|
| 592 |
- } |
|
| 593 |
-} |
|
| 594 |
- |
|
| 595 |
-func TestIsSecureIndex(t *testing.T) {
|
|
| 596 |
- overrideLookupIP(t) |
|
| 597 |
- tests := []struct {
|
|
| 598 |
- addr string |
|
| 599 |
- insecureRegistries []string |
|
| 600 |
- expected bool |
|
| 601 |
- }{
|
|
| 602 |
- {IndexName, nil, true},
|
|
| 603 |
- {"example.com", []string{}, true},
|
|
| 604 |
- {"example.com", []string{"example.com"}, false},
|
|
| 605 |
- {"localhost", []string{"localhost:5000"}, false},
|
|
| 606 |
- {"localhost:5000", []string{"localhost:5000"}, false},
|
|
| 607 |
- {"localhost", []string{"example.com"}, false},
|
|
| 608 |
- {"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
|
|
| 609 |
- {"localhost", nil, false},
|
|
| 610 |
- {"localhost:5000", nil, false},
|
|
| 611 |
- {"127.0.0.1", nil, false},
|
|
| 612 |
- {"localhost", []string{"example.com"}, false},
|
|
| 613 |
- {"127.0.0.1", []string{"example.com"}, false},
|
|
| 614 |
- {"example.com", nil, true},
|
|
| 615 |
- {"example.com", []string{"example.com"}, false},
|
|
| 616 |
- {"127.0.0.1", []string{"example.com"}, false},
|
|
| 617 |
- {"127.0.0.1:5000", []string{"example.com"}, false},
|
|
| 618 |
- {"example.com:5000", []string{"42.42.0.0/16"}, false},
|
|
| 619 |
- {"example.com", []string{"42.42.0.0/16"}, false},
|
|
| 620 |
- {"example.com:5000", []string{"42.42.42.42/8"}, false},
|
|
| 621 |
- {"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
|
|
| 622 |
- {"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
|
|
| 623 |
- {"invalid.example.com", []string{"42.42.0.0/16"}, true},
|
|
| 624 |
- {"invalid.example.com", []string{"invalid.example.com"}, false},
|
|
| 625 |
- {"invalid.example.com:5000", []string{"invalid.example.com"}, true},
|
|
| 626 |
- {"invalid.example.com:5000", []string{"invalid.example.com:5000"}, false},
|
|
| 627 |
- } |
|
| 628 |
- for _, tc := range tests {
|
|
| 629 |
- config, err := newServiceConfig(ServiceOptions{
|
|
| 630 |
- InsecureRegistries: tc.insecureRegistries, |
|
| 631 |
- }) |
|
| 632 |
- assert.NilError(t, err) |
|
| 633 |
- |
|
| 634 |
- sec := config.isSecureIndex(tc.addr) |
|
| 635 |
- assert.Equal(t, sec, tc.expected, "isSecureIndex failed for %q %v, expected %v got %v", tc.addr, tc.insecureRegistries, tc.expected, sec) |
|
| 636 |
- } |
|
| 637 |
-} |
| 638 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,100 +0,0 @@ |
| 1 |
-package resumable |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "errors" |
|
| 6 |
- "fmt" |
|
| 7 |
- "io" |
|
| 8 |
- "net/http" |
|
| 9 |
- "time" |
|
| 10 |
- |
|
| 11 |
- "github.com/containerd/log" |
|
| 12 |
-) |
|
| 13 |
- |
|
| 14 |
-type requestReader struct {
|
|
| 15 |
- client *http.Client |
|
| 16 |
- request *http.Request |
|
| 17 |
- lastRange int64 |
|
| 18 |
- totalSize int64 |
|
| 19 |
- currentResponse *http.Response |
|
| 20 |
- failures uint32 |
|
| 21 |
- maxFailures uint32 |
|
| 22 |
- waitDuration time.Duration |
|
| 23 |
-} |
|
| 24 |
- |
|
| 25 |
-// NewRequestReader makes it possible to resume reading a request's body transparently |
|
| 26 |
-// maxfail is the number of times we retry to make requests again (not resumes) |
|
| 27 |
-// totalsize is the total length of the body; auto detect if not provided |
|
| 28 |
-func NewRequestReader(c *http.Client, r *http.Request, maxfail uint32, totalsize int64) io.ReadCloser {
|
|
| 29 |
- return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, waitDuration: 5 * time.Second}
|
|
| 30 |
-} |
|
| 31 |
- |
|
| 32 |
-// NewRequestReaderWithInitialResponse makes it possible to resume |
|
| 33 |
-// reading the body of an already initiated request. |
|
| 34 |
-func NewRequestReaderWithInitialResponse(c *http.Client, r *http.Request, maxfail uint32, totalsize int64, initialResponse *http.Response) io.ReadCloser {
|
|
| 35 |
- return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, currentResponse: initialResponse, waitDuration: 5 * time.Second}
|
|
| 36 |
-} |
|
| 37 |
- |
|
| 38 |
-func (r *requestReader) Read(p []byte) (n int, _ error) {
|
|
| 39 |
- if r.client == nil || r.request == nil {
|
|
| 40 |
- return 0, errors.New("client and request can't be nil")
|
|
| 41 |
- } |
|
| 42 |
- |
|
| 43 |
- var err error |
|
| 44 |
- isFreshRequest := false |
|
| 45 |
- if r.lastRange != 0 && r.currentResponse == nil {
|
|
| 46 |
- readRange := fmt.Sprintf("bytes=%d-%d", r.lastRange, r.totalSize)
|
|
| 47 |
- r.request.Header.Set("Range", readRange)
|
|
| 48 |
- time.Sleep(r.waitDuration) |
|
| 49 |
- } |
|
| 50 |
- if r.currentResponse == nil {
|
|
| 51 |
- r.currentResponse, err = r.client.Do(r.request) |
|
| 52 |
- isFreshRequest = true |
|
| 53 |
- } |
|
| 54 |
- if err != nil && r.failures+1 != r.maxFailures {
|
|
| 55 |
- r.cleanUpResponse() |
|
| 56 |
- r.failures++ |
|
| 57 |
- time.Sleep(time.Duration(r.failures) * r.waitDuration) |
|
| 58 |
- return 0, nil |
|
| 59 |
- } else if err != nil {
|
|
| 60 |
- r.cleanUpResponse() |
|
| 61 |
- return 0, err |
|
| 62 |
- } |
|
| 63 |
- if r.currentResponse.StatusCode == http.StatusRequestedRangeNotSatisfiable && r.lastRange == r.totalSize && r.currentResponse.ContentLength == 0 {
|
|
| 64 |
- r.cleanUpResponse() |
|
| 65 |
- return 0, io.EOF |
|
| 66 |
- } else if r.currentResponse.StatusCode != http.StatusPartialContent && r.lastRange != 0 && isFreshRequest {
|
|
| 67 |
- r.cleanUpResponse() |
|
| 68 |
- return 0, errors.New("the server doesn't support byte ranges")
|
|
| 69 |
- } |
|
| 70 |
- if r.totalSize == 0 {
|
|
| 71 |
- r.totalSize = r.currentResponse.ContentLength |
|
| 72 |
- } else if r.totalSize <= 0 {
|
|
| 73 |
- r.cleanUpResponse() |
|
| 74 |
- return 0, errors.New("failed to auto detect content length")
|
|
| 75 |
- } |
|
| 76 |
- n, err = r.currentResponse.Body.Read(p) |
|
| 77 |
- r.lastRange += int64(n) |
|
| 78 |
- if err != nil {
|
|
| 79 |
- r.cleanUpResponse() |
|
| 80 |
- } |
|
| 81 |
- if err != nil && !errors.Is(err, io.EOF) {
|
|
| 82 |
- log.G(context.TODO()).Infof("encountered error during pull and clearing it before resume: %s", err)
|
|
| 83 |
- err = nil |
|
| 84 |
- } |
|
| 85 |
- return n, err |
|
| 86 |
-} |
|
| 87 |
- |
|
| 88 |
-func (r *requestReader) Close() error {
|
|
| 89 |
- r.cleanUpResponse() |
|
| 90 |
- r.client = nil |
|
| 91 |
- r.request = nil |
|
| 92 |
- return nil |
|
| 93 |
-} |
|
| 94 |
- |
|
| 95 |
-func (r *requestReader) cleanUpResponse() {
|
|
| 96 |
- if r.currentResponse != nil {
|
|
| 97 |
- r.currentResponse.Body.Close() |
|
| 98 |
- r.currentResponse = nil |
|
| 99 |
- } |
|
| 100 |
-} |
| 101 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,258 +0,0 @@ |
| 1 |
-package resumable |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "errors" |
|
| 5 |
- "fmt" |
|
| 6 |
- "io" |
|
| 7 |
- "net/http" |
|
| 8 |
- "net/http/httptest" |
|
| 9 |
- "strings" |
|
| 10 |
- "testing" |
|
| 11 |
- "time" |
|
| 12 |
- |
|
| 13 |
- "gotest.tools/v3/assert" |
|
| 14 |
- is "gotest.tools/v3/assert/cmp" |
|
| 15 |
-) |
|
| 16 |
- |
|
| 17 |
-func TestResumableRequestHeaderSimpleErrors(t *testing.T) {
|
|
| 18 |
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 19 |
- fmt.Fprintln(w, "Hello, world !") |
|
| 20 |
- })) |
|
| 21 |
- defer ts.Close() |
|
| 22 |
- |
|
| 23 |
- client := &http.Client{}
|
|
| 24 |
- |
|
| 25 |
- var req *http.Request |
|
| 26 |
- req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 27 |
- assert.NilError(t, err) |
|
| 28 |
- |
|
| 29 |
- resreq := &requestReader{}
|
|
| 30 |
- _, err = resreq.Read([]byte{})
|
|
| 31 |
- assert.Check(t, is.Error(err, "client and request can't be nil")) |
|
| 32 |
- |
|
| 33 |
- resreq = &requestReader{
|
|
| 34 |
- client: client, |
|
| 35 |
- request: req, |
|
| 36 |
- totalSize: -1, |
|
| 37 |
- } |
|
| 38 |
- _, err = resreq.Read([]byte{})
|
|
| 39 |
- assert.Check(t, is.Error(err, "failed to auto detect content length")) |
|
| 40 |
-} |
|
| 41 |
- |
|
| 42 |
-// Not too much failures, bails out after some wait |
|
| 43 |
-func TestResumableRequestHeaderNotTooMuchFailures(t *testing.T) {
|
|
| 44 |
- client := &http.Client{}
|
|
| 45 |
- |
|
| 46 |
- var badReq *http.Request |
|
| 47 |
- badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody) |
|
| 48 |
- assert.NilError(t, err) |
|
| 49 |
- |
|
| 50 |
- resreq := &requestReader{
|
|
| 51 |
- client: client, |
|
| 52 |
- request: badReq, |
|
| 53 |
- failures: 0, |
|
| 54 |
- maxFailures: 2, |
|
| 55 |
- waitDuration: 10 * time.Millisecond, |
|
| 56 |
- } |
|
| 57 |
- read, err := resreq.Read([]byte{})
|
|
| 58 |
- assert.NilError(t, err) |
|
| 59 |
- assert.Check(t, is.Equal(0, read)) |
|
| 60 |
-} |
|
| 61 |
- |
|
| 62 |
-// Too much failures, returns the error |
|
| 63 |
-func TestResumableRequestHeaderTooMuchFailures(t *testing.T) {
|
|
| 64 |
- client := &http.Client{}
|
|
| 65 |
- |
|
| 66 |
- var badReq *http.Request |
|
| 67 |
- badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody) |
|
| 68 |
- assert.NilError(t, err) |
|
| 69 |
- |
|
| 70 |
- resreq := &requestReader{
|
|
| 71 |
- client: client, |
|
| 72 |
- request: badReq, |
|
| 73 |
- failures: 0, |
|
| 74 |
- maxFailures: 1, |
|
| 75 |
- } |
|
| 76 |
- defer resreq.Close() |
|
| 77 |
- |
|
| 78 |
- read, err := resreq.Read([]byte{})
|
|
| 79 |
- assert.Assert(t, err != nil) |
|
| 80 |
- assert.Check(t, is.ErrorContains(err, "unsupported protocol scheme")) |
|
| 81 |
- assert.Check(t, is.ErrorContains(err, "I%27m%20not%20an%20url")) |
|
| 82 |
- assert.Check(t, is.Equal(0, read)) |
|
| 83 |
-} |
|
| 84 |
- |
|
| 85 |
-type errorReaderCloser struct{}
|
|
| 86 |
- |
|
| 87 |
-func (errorReaderCloser) Close() error { return nil }
|
|
| 88 |
- |
|
| 89 |
-func (errorReaderCloser) Read(p []byte) (int, error) {
|
|
| 90 |
- return 0, errors.New("an error occurred")
|
|
| 91 |
-} |
|
| 92 |
- |
|
| 93 |
-// If an unknown error is encountered, return 0, nil and log it |
|
| 94 |
-func TestResumableRequestReaderWithReadError(t *testing.T) {
|
|
| 95 |
- var req *http.Request |
|
| 96 |
- req, err := http.NewRequest(http.MethodGet, "", http.NoBody) |
|
| 97 |
- assert.NilError(t, err) |
|
| 98 |
- |
|
| 99 |
- client := &http.Client{}
|
|
| 100 |
- |
|
| 101 |
- response := &http.Response{
|
|
| 102 |
- Status: "500 Internal Server", |
|
| 103 |
- StatusCode: http.StatusInternalServerError, |
|
| 104 |
- ContentLength: 0, |
|
| 105 |
- Close: true, |
|
| 106 |
- Body: errorReaderCloser{},
|
|
| 107 |
- } |
|
| 108 |
- |
|
| 109 |
- resreq := &requestReader{
|
|
| 110 |
- client: client, |
|
| 111 |
- request: req, |
|
| 112 |
- currentResponse: response, |
|
| 113 |
- lastRange: 1, |
|
| 114 |
- totalSize: 1, |
|
| 115 |
- } |
|
| 116 |
- defer resreq.Close() |
|
| 117 |
- |
|
| 118 |
- buf := make([]byte, 1) |
|
| 119 |
- read, err := resreq.Read(buf) |
|
| 120 |
- assert.NilError(t, err) |
|
| 121 |
- |
|
| 122 |
- assert.Check(t, is.Equal(0, read)) |
|
| 123 |
-} |
|
| 124 |
- |
|
| 125 |
-func TestResumableRequestReaderWithEOFWith416Response(t *testing.T) {
|
|
| 126 |
- var req *http.Request |
|
| 127 |
- req, err := http.NewRequest(http.MethodGet, "", http.NoBody) |
|
| 128 |
- assert.NilError(t, err) |
|
| 129 |
- |
|
| 130 |
- client := &http.Client{}
|
|
| 131 |
- |
|
| 132 |
- response := &http.Response{
|
|
| 133 |
- Status: "416 Requested Range Not Satisfiable", |
|
| 134 |
- StatusCode: http.StatusRequestedRangeNotSatisfiable, |
|
| 135 |
- ContentLength: 0, |
|
| 136 |
- Close: true, |
|
| 137 |
- Body: io.NopCloser(strings.NewReader("")),
|
|
| 138 |
- } |
|
| 139 |
- |
|
| 140 |
- resreq := &requestReader{
|
|
| 141 |
- client: client, |
|
| 142 |
- request: req, |
|
| 143 |
- currentResponse: response, |
|
| 144 |
- lastRange: 1, |
|
| 145 |
- totalSize: 1, |
|
| 146 |
- } |
|
| 147 |
- defer resreq.Close() |
|
| 148 |
- |
|
| 149 |
- buf := make([]byte, 1) |
|
| 150 |
- _, err = resreq.Read(buf) |
|
| 151 |
- assert.Check(t, is.Error(err, io.EOF.Error())) |
|
| 152 |
-} |
|
| 153 |
- |
|
| 154 |
-func TestResumableRequestReaderWithServerDoesntSupportByteRanges(t *testing.T) {
|
|
| 155 |
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 156 |
- if r.Header.Get("Range") == "" {
|
|
| 157 |
- t.Fatalf("Expected a Range HTTP header, got nothing")
|
|
| 158 |
- } |
|
| 159 |
- })) |
|
| 160 |
- defer ts.Close() |
|
| 161 |
- |
|
| 162 |
- var req *http.Request |
|
| 163 |
- req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 164 |
- assert.NilError(t, err) |
|
| 165 |
- |
|
| 166 |
- client := &http.Client{}
|
|
| 167 |
- |
|
| 168 |
- resreq := &requestReader{
|
|
| 169 |
- client: client, |
|
| 170 |
- request: req, |
|
| 171 |
- lastRange: 1, |
|
| 172 |
- } |
|
| 173 |
- defer resreq.Close() |
|
| 174 |
- |
|
| 175 |
- buf := make([]byte, 2) |
|
| 176 |
- _, err = resreq.Read(buf) |
|
| 177 |
- assert.Check(t, is.Error(err, "the server doesn't support byte ranges")) |
|
| 178 |
-} |
|
| 179 |
- |
|
| 180 |
-func TestResumableRequestReaderWithZeroTotalSize(t *testing.T) {
|
|
| 181 |
- srvtxt := "some response text data" |
|
| 182 |
- |
|
| 183 |
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 184 |
- fmt.Fprintln(w, srvtxt) |
|
| 185 |
- })) |
|
| 186 |
- defer ts.Close() |
|
| 187 |
- |
|
| 188 |
- var req *http.Request |
|
| 189 |
- req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 190 |
- assert.NilError(t, err) |
|
| 191 |
- |
|
| 192 |
- client := &http.Client{}
|
|
| 193 |
- retries := uint32(5) |
|
| 194 |
- |
|
| 195 |
- resreq := NewRequestReader(client, req, retries, 0) |
|
| 196 |
- defer resreq.Close() |
|
| 197 |
- |
|
| 198 |
- data, err := io.ReadAll(resreq) |
|
| 199 |
- assert.NilError(t, err) |
|
| 200 |
- |
|
| 201 |
- resstr := strings.TrimSuffix(string(data), "\n") |
|
| 202 |
- assert.Check(t, is.Equal(srvtxt, resstr)) |
|
| 203 |
-} |
|
| 204 |
- |
|
| 205 |
-func TestResumableRequestReader(t *testing.T) {
|
|
| 206 |
- srvtxt := "some response text data" |
|
| 207 |
- |
|
| 208 |
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 209 |
- fmt.Fprintln(w, srvtxt) |
|
| 210 |
- })) |
|
| 211 |
- defer ts.Close() |
|
| 212 |
- |
|
| 213 |
- var req *http.Request |
|
| 214 |
- req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 215 |
- assert.NilError(t, err) |
|
| 216 |
- |
|
| 217 |
- client := &http.Client{}
|
|
| 218 |
- retries := uint32(5) |
|
| 219 |
- imgSize := int64(len(srvtxt)) |
|
| 220 |
- |
|
| 221 |
- resreq := NewRequestReader(client, req, retries, imgSize) |
|
| 222 |
- defer resreq.Close() |
|
| 223 |
- |
|
| 224 |
- data, err := io.ReadAll(resreq) |
|
| 225 |
- assert.NilError(t, err) |
|
| 226 |
- |
|
| 227 |
- resstr := strings.TrimSuffix(string(data), "\n") |
|
| 228 |
- assert.Check(t, is.Equal(srvtxt, resstr)) |
|
| 229 |
-} |
|
| 230 |
- |
|
| 231 |
-func TestResumableRequestReaderWithInitialResponse(t *testing.T) {
|
|
| 232 |
- srvtxt := "some response text data" |
|
| 233 |
- |
|
| 234 |
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 235 |
- fmt.Fprintln(w, srvtxt) |
|
| 236 |
- })) |
|
| 237 |
- defer ts.Close() |
|
| 238 |
- |
|
| 239 |
- var req *http.Request |
|
| 240 |
- req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody) |
|
| 241 |
- assert.NilError(t, err) |
|
| 242 |
- |
|
| 243 |
- client := &http.Client{}
|
|
| 244 |
- retries := uint32(5) |
|
| 245 |
- imgSize := int64(len(srvtxt)) |
|
| 246 |
- |
|
| 247 |
- res, err := client.Do(req) |
|
| 248 |
- assert.NilError(t, err) |
|
| 249 |
- |
|
| 250 |
- resreq := NewRequestReaderWithInitialResponse(client, req, retries, imgSize, res) |
|
| 251 |
- defer resreq.Close() |
|
| 252 |
- |
|
| 253 |
- data, err := io.ReadAll(resreq) |
|
| 254 |
- assert.NilError(t, err) |
|
| 255 |
- |
|
| 256 |
- resstr := strings.TrimSuffix(string(data), "\n") |
|
| 257 |
- assert.Check(t, is.Equal(srvtxt, resstr)) |
|
| 258 |
-} |
| 259 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,170 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "net/http" |
|
| 6 |
- "strconv" |
|
| 7 |
- "strings" |
|
| 8 |
- |
|
| 9 |
- "github.com/containerd/log" |
|
| 10 |
- "github.com/docker/distribution/registry/client/auth" |
|
| 11 |
- "github.com/moby/moby/api/types/filters" |
|
| 12 |
- "github.com/moby/moby/api/types/registry" |
|
| 13 |
- "github.com/pkg/errors" |
|
| 14 |
-) |
|
| 15 |
- |
|
| 16 |
-var acceptedSearchFilterTags = map[string]bool{
|
|
| 17 |
- "is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future. |
|
| 18 |
- "is-official": true, |
|
| 19 |
- "stars": true, |
|
| 20 |
-} |
|
| 21 |
- |
|
| 22 |
-// Search queries the public registry for repositories matching the specified |
|
| 23 |
-// search term and filters. |
|
| 24 |
-func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
|
|
| 25 |
- if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
|
|
| 26 |
- return nil, err |
|
| 27 |
- } |
|
| 28 |
- |
|
| 29 |
- isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
|
|
| 30 |
- if err != nil {
|
|
| 31 |
- return nil, err |
|
| 32 |
- } |
|
| 33 |
- |
|
| 34 |
- // "is-automated" is deprecated and filtering for `true` will yield no results. |
|
| 35 |
- if isAutomated {
|
|
| 36 |
- return []registry.SearchResult{}, nil
|
|
| 37 |
- } |
|
| 38 |
- |
|
| 39 |
- isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
|
|
| 40 |
- if err != nil {
|
|
| 41 |
- return nil, err |
|
| 42 |
- } |
|
| 43 |
- |
|
| 44 |
- hasStarFilter := 0 |
|
| 45 |
- if searchFilters.Contains("stars") {
|
|
| 46 |
- hasStars := searchFilters.Get("stars")
|
|
| 47 |
- for _, hasStar := range hasStars {
|
|
| 48 |
- iHasStar, err := strconv.Atoi(hasStar) |
|
| 49 |
- if err != nil {
|
|
| 50 |
- return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)}
|
|
| 51 |
- } |
|
| 52 |
- if iHasStar > hasStarFilter {
|
|
| 53 |
- hasStarFilter = iHasStar |
|
| 54 |
- } |
|
| 55 |
- } |
|
| 56 |
- } |
|
| 57 |
- |
|
| 58 |
- unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers) |
|
| 59 |
- if err != nil {
|
|
| 60 |
- return nil, err |
|
| 61 |
- } |
|
| 62 |
- |
|
| 63 |
- filteredResults := []registry.SearchResult{}
|
|
| 64 |
- for _, result := range unfilteredResult.Results {
|
|
| 65 |
- if searchFilters.Contains("is-official") {
|
|
| 66 |
- if isOfficial != result.IsOfficial {
|
|
| 67 |
- continue |
|
| 68 |
- } |
|
| 69 |
- } |
|
| 70 |
- if searchFilters.Contains("stars") {
|
|
| 71 |
- if result.StarCount < hasStarFilter {
|
|
| 72 |
- continue |
|
| 73 |
- } |
|
| 74 |
- } |
|
| 75 |
- // "is-automated" is deprecated and the value in Docker Hub search |
|
| 76 |
- // results is untrustworthy. Force it to false so as to not mislead our |
|
| 77 |
- // clients. |
|
| 78 |
- result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated) |
|
| 79 |
- filteredResults = append(filteredResults, result) |
|
| 80 |
- } |
|
| 81 |
- |
|
| 82 |
- return filteredResults, nil |
|
| 83 |
-} |
|
| 84 |
- |
|
| 85 |
-func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
|
|
| 86 |
- if hasScheme(term) {
|
|
| 87 |
- return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
|
|
| 88 |
- } |
|
| 89 |
- |
|
| 90 |
- indexName, remoteName := splitReposSearchTerm(term) |
|
| 91 |
- |
|
| 92 |
- // Search is a long-running operation, just lock s.config to avoid block others. |
|
| 93 |
- s.mu.RLock() |
|
| 94 |
- index := newIndexInfo(s.config, indexName) |
|
| 95 |
- s.mu.RUnlock() |
|
| 96 |
- if index.Official {
|
|
| 97 |
- // If pull "library/foo", it's stored locally under "foo" |
|
| 98 |
- remoteName = strings.TrimPrefix(remoteName, "library/") |
|
| 99 |
- } |
|
| 100 |
- |
|
| 101 |
- endpoint, err := newV1Endpoint(ctx, index, headers) |
|
| 102 |
- if err != nil {
|
|
| 103 |
- return nil, err |
|
| 104 |
- } |
|
| 105 |
- |
|
| 106 |
- var client *http.Client |
|
| 107 |
- if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
|
|
| 108 |
- creds := NewStaticCredentialStore(authConfig) |
|
| 109 |
- |
|
| 110 |
- // TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac) |
|
| 111 |
- modifiers := Headers(headers.Get("User-Agent"), nil)
|
|
| 112 |
- v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{
|
|
| 113 |
- auth.RegistryScope{Name: "catalog", Actions: []string{"search"}},
|
|
| 114 |
- }) |
|
| 115 |
- if err != nil {
|
|
| 116 |
- return nil, err |
|
| 117 |
- } |
|
| 118 |
- // Copy non transport http client features |
|
| 119 |
- v2Client.Timeout = endpoint.client.Timeout |
|
| 120 |
- v2Client.CheckRedirect = endpoint.client.CheckRedirect |
|
| 121 |
- v2Client.Jar = endpoint.client.Jar |
|
| 122 |
- |
|
| 123 |
- log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
|
|
| 124 |
- client = v2Client |
|
| 125 |
- } else {
|
|
| 126 |
- client = endpoint.client |
|
| 127 |
- if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil {
|
|
| 128 |
- return nil, err |
|
| 129 |
- } |
|
| 130 |
- } |
|
| 131 |
- |
|
| 132 |
- return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit) |
|
| 133 |
-} |
|
| 134 |
- |
|
| 135 |
-// splitReposSearchTerm breaks a search term into an index name and remote name |
|
| 136 |
-func splitReposSearchTerm(reposName string) (string, string) {
|
|
| 137 |
- nameParts := strings.SplitN(reposName, "/", 2) |
|
| 138 |
- if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && |
|
| 139 |
- !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
|
| 140 |
- // This is a Docker Hub repository (ex: samalba/hipache or ubuntu), |
|
| 141 |
- // use the default Docker Hub registry (docker.io) |
|
| 142 |
- return IndexName, reposName |
|
| 143 |
- } |
|
| 144 |
- return nameParts[0], nameParts[1] |
|
| 145 |
-} |
|
| 146 |
- |
|
| 147 |
-// ParseSearchIndexInfo will use repository name to get back an indexInfo. |
|
| 148 |
-// |
|
| 149 |
-// TODO(thaJeztah) this function is only used by the CLI, and used to get |
|
| 150 |
-// information of the registry (to provide credentials if needed). We should |
|
| 151 |
-// move this function (or equivalent) to the CLI, as it's doing too much just |
|
| 152 |
-// for that. |
|
| 153 |
-func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
|
|
| 154 |
- indexName, _ := splitReposSearchTerm(reposName) |
|
| 155 |
- indexName = normalizeIndexName(indexName) |
|
| 156 |
- if indexName == IndexName {
|
|
| 157 |
- return ®istry.IndexInfo{
|
|
| 158 |
- Name: IndexName, |
|
| 159 |
- Mirrors: []string{},
|
|
| 160 |
- Secure: true, |
|
| 161 |
- Official: true, |
|
| 162 |
- }, nil |
|
| 163 |
- } |
|
| 164 |
- |
|
| 165 |
- return ®istry.IndexInfo{
|
|
| 166 |
- Name: indexName, |
|
| 167 |
- Mirrors: []string{},
|
|
| 168 |
- Secure: !isInsecure(indexName), |
|
| 169 |
- }, nil |
|
| 170 |
-} |
| 171 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,213 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "crypto/tls" |
|
| 6 |
- "encoding/json" |
|
| 7 |
- "errors" |
|
| 8 |
- "fmt" |
|
| 9 |
- "net/http" |
|
| 10 |
- "net/url" |
|
| 11 |
- "strings" |
|
| 12 |
- |
|
| 13 |
- "github.com/containerd/log" |
|
| 14 |
- "github.com/docker/distribution/registry/client/transport" |
|
| 15 |
- "github.com/moby/moby/api/types/registry" |
|
| 16 |
-) |
|
| 17 |
- |
|
| 18 |
-// v1PingResult contains the information returned when pinging a registry. It |
|
| 19 |
-// indicates whether the registry claims to be a standalone registry. |
|
| 20 |
-type v1PingResult struct {
|
|
| 21 |
- // Standalone is set to true if the registry indicates it is a |
|
| 22 |
- // standalone registry in the X-Docker-Registry-Standalone |
|
| 23 |
- // header |
|
| 24 |
- Standalone bool `json:"standalone"` |
|
| 25 |
-} |
|
| 26 |
- |
|
| 27 |
-// v1Endpoint stores basic information about a V1 registry endpoint. |
|
| 28 |
-type v1Endpoint struct {
|
|
| 29 |
- client *http.Client |
|
| 30 |
- URL *url.URL |
|
| 31 |
- IsSecure bool |
|
| 32 |
-} |
|
| 33 |
- |
|
| 34 |
-// newV1Endpoint parses the given address to return a registry endpoint. |
|
| 35 |
-// TODO: remove. This is only used by search. |
|
| 36 |
-func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
|
|
| 37 |
- tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure) |
|
| 38 |
- if err != nil {
|
|
| 39 |
- return nil, err |
|
| 40 |
- } |
|
| 41 |
- |
|
| 42 |
- endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers) |
|
| 43 |
- if err != nil {
|
|
| 44 |
- return nil, err |
|
| 45 |
- } |
|
| 46 |
- |
|
| 47 |
- if endpoint.String() == IndexServer {
|
|
| 48 |
- // Skip the check, we know this one is valid |
|
| 49 |
- // (and we never want to fall back to http in case of error) |
|
| 50 |
- return endpoint, nil |
|
| 51 |
- } |
|
| 52 |
- |
|
| 53 |
- // Try HTTPS ping to registry |
|
| 54 |
- endpoint.URL.Scheme = "https" |
|
| 55 |
- if _, err := endpoint.ping(ctx); err != nil {
|
|
| 56 |
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
| 57 |
- return nil, err |
|
| 58 |
- } |
|
| 59 |
- if endpoint.IsSecure {
|
|
| 60 |
- // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` |
|
| 61 |
- // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP. |
|
| 62 |
- hint := fmt.Sprintf( |
|
| 63 |
- ". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+ |
|
| 64 |
- "In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt", |
|
| 65 |
- endpoint.URL.Host, |
|
| 66 |
- ) |
|
| 67 |
- return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint)
|
|
| 68 |
- } |
|
| 69 |
- |
|
| 70 |
- // registry is insecure and HTTPS failed, fallback to HTTP. |
|
| 71 |
- log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
|
|
| 72 |
- endpoint.URL.Scheme = "http" |
|
| 73 |
- if _, err2 := endpoint.ping(ctx); err2 != nil {
|
|
| 74 |
- return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
|
|
| 75 |
- } |
|
| 76 |
- } |
|
| 77 |
- |
|
| 78 |
- return endpoint, nil |
|
| 79 |
-} |
|
| 80 |
- |
|
| 81 |
-// trimV1Address trims the "v1" version suffix off the address and returns |
|
| 82 |
-// the trimmed address. It returns an error on "v2" endpoints. |
|
| 83 |
-func trimV1Address(address string) (string, error) {
|
|
| 84 |
- trimmed := strings.TrimSuffix(address, "/") |
|
| 85 |
- if strings.HasSuffix(trimmed, "/v2") {
|
|
| 86 |
- return "", invalidParamf("search is not supported on v2 endpoints: %s", address)
|
|
| 87 |
- } |
|
| 88 |
- return strings.TrimSuffix(trimmed, "/v1"), nil |
|
| 89 |
-} |
|
| 90 |
- |
|
| 91 |
-func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
|
|
| 92 |
- if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
|
| 93 |
- address = "https://" + address |
|
| 94 |
- } |
|
| 95 |
- |
|
| 96 |
- address, err := trimV1Address(address) |
|
| 97 |
- if err != nil {
|
|
| 98 |
- return nil, err |
|
| 99 |
- } |
|
| 100 |
- |
|
| 101 |
- uri, err := url.Parse(address) |
|
| 102 |
- if err != nil {
|
|
| 103 |
- return nil, invalidParam(err) |
|
| 104 |
- } |
|
| 105 |
- |
|
| 106 |
- // TODO(tiborvass): make sure a ConnectTimeout transport is used |
|
| 107 |
- tr := newTransport(tlsConfig) |
|
| 108 |
- |
|
| 109 |
- return &v1Endpoint{
|
|
| 110 |
- IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify, |
|
| 111 |
- URL: uri, |
|
| 112 |
- client: httpClient(transport.NewTransport(tr, Headers("", headers)...)),
|
|
| 113 |
- }, nil |
|
| 114 |
-} |
|
| 115 |
- |
|
| 116 |
-// Get the formatted URL for the root of this registry Endpoint |
|
| 117 |
-func (e *v1Endpoint) String() string {
|
|
| 118 |
- return e.URL.String() + "/v1/" |
|
| 119 |
-} |
|
| 120 |
- |
|
| 121 |
-// ping returns a v1PingResult which indicates whether the registry is standalone or not. |
|
| 122 |
-func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
|
|
| 123 |
- if e.String() == IndexServer {
|
|
| 124 |
- // Skip the check, we know this one is valid |
|
| 125 |
- // (and we never want to fallback to http in case of error) |
|
| 126 |
- return v1PingResult{}, nil
|
|
| 127 |
- } |
|
| 128 |
- |
|
| 129 |
- pingURL := e.String() + "_ping" |
|
| 130 |
- log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
|
|
| 131 |
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody) |
|
| 132 |
- if err != nil {
|
|
| 133 |
- return v1PingResult{}, invalidParam(err)
|
|
| 134 |
- } |
|
| 135 |
- |
|
| 136 |
- resp, err := e.client.Do(req) |
|
| 137 |
- if err != nil {
|
|
| 138 |
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
| 139 |
- return v1PingResult{}, err
|
|
| 140 |
- } |
|
| 141 |
- return v1PingResult{}, invalidParam(err)
|
|
| 142 |
- } |
|
| 143 |
- |
|
| 144 |
- defer resp.Body.Close() |
|
| 145 |
- |
|
| 146 |
- if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" {
|
|
| 147 |
- info := v1PingResult{}
|
|
| 148 |
- // Accepted values are "1", and "true" (case-insensitive). |
|
| 149 |
- if v == "1" || strings.EqualFold(v, "true") {
|
|
| 150 |
- info.Standalone = true |
|
| 151 |
- } |
|
| 152 |
- log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
|
|
| 153 |
- return info, nil |
|
| 154 |
- } |
|
| 155 |
- |
|
| 156 |
- // If the header is absent, we assume true for compatibility with earlier |
|
| 157 |
- // versions of the registry. default to true |
|
| 158 |
- info := v1PingResult{
|
|
| 159 |
- Standalone: true, |
|
| 160 |
- } |
|
| 161 |
- if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
|
| 162 |
- log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
|
|
| 163 |
- // don't stop here. Just assume sane defaults |
|
| 164 |
- } |
|
| 165 |
- |
|
| 166 |
- log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
|
|
| 167 |
- return info, nil |
|
| 168 |
-} |
|
| 169 |
- |
|
| 170 |
-// httpClient returns an HTTP client structure which uses the given transport |
|
| 171 |
-// and contains the necessary headers for redirected requests |
|
| 172 |
-func httpClient(tr http.RoundTripper) *http.Client {
|
|
| 173 |
- return &http.Client{
|
|
| 174 |
- Transport: tr, |
|
| 175 |
- CheckRedirect: addRequiredHeadersToRedirectedRequests, |
|
| 176 |
- } |
|
| 177 |
-} |
|
| 178 |
- |
|
| 179 |
-func trustedLocation(req *http.Request) bool {
|
|
| 180 |
- var ( |
|
| 181 |
- trusteds = []string{"docker.com", "docker.io"}
|
|
| 182 |
- hostname = strings.SplitN(req.Host, ":", 2)[0] |
|
| 183 |
- ) |
|
| 184 |
- if req.URL.Scheme != "https" {
|
|
| 185 |
- return false |
|
| 186 |
- } |
|
| 187 |
- |
|
| 188 |
- for _, trusted := range trusteds {
|
|
| 189 |
- if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
|
|
| 190 |
- return true |
|
| 191 |
- } |
|
| 192 |
- } |
|
| 193 |
- return false |
|
| 194 |
-} |
|
| 195 |
- |
|
| 196 |
-// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers |
|
| 197 |
-// for redirected requests |
|
| 198 |
-func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
|
|
| 199 |
- if len(via) != 0 && via[0] != nil {
|
|
| 200 |
- if trustedLocation(req) && trustedLocation(via[0]) {
|
|
| 201 |
- req.Header = via[0].Header |
|
| 202 |
- return nil |
|
| 203 |
- } |
|
| 204 |
- for k, v := range via[0].Header {
|
|
| 205 |
- if k != "Authorization" {
|
|
| 206 |
- for _, vv := range v {
|
|
| 207 |
- req.Header.Add(k, vv) |
|
| 208 |
- } |
|
| 209 |
- } |
|
| 210 |
- } |
|
| 211 |
- } |
|
| 212 |
- return nil |
|
| 213 |
-} |
| 214 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,237 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "net/http" |
|
| 6 |
- "net/http/httptest" |
|
| 7 |
- "strings" |
|
| 8 |
- "testing" |
|
| 9 |
- |
|
| 10 |
- "github.com/moby/moby/api/types/registry" |
|
| 11 |
- "gotest.tools/v3/assert" |
|
| 12 |
- is "gotest.tools/v3/assert/cmp" |
|
| 13 |
-) |
|
| 14 |
- |
|
| 15 |
-func TestV1EndpointPing(t *testing.T) {
|
|
| 16 |
- testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
|
|
| 17 |
- ep, err := newV1Endpoint(context.Background(), index, nil) |
|
| 18 |
- if err != nil {
|
|
| 19 |
- t.Fatal(err) |
|
| 20 |
- } |
|
| 21 |
- regInfo, err := ep.ping(context.Background()) |
|
| 22 |
- if err != nil {
|
|
| 23 |
- t.Fatal(err) |
|
| 24 |
- } |
|
| 25 |
- |
|
| 26 |
- assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage) |
|
| 27 |
- } |
|
| 28 |
- |
|
| 29 |
- testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
|
|
| 30 |
- testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)")
|
|
| 31 |
- testPing(makePublicIndex(), false, "Expected standalone to be false for public index") |
|
| 32 |
-} |
|
| 33 |
- |
|
| 34 |
-func TestV1Endpoint(t *testing.T) {
|
|
| 35 |
- // Simple wrapper to fail test if err != nil |
|
| 36 |
- expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
|
|
| 37 |
- endpoint, err := newV1Endpoint(context.Background(), index, nil) |
|
| 38 |
- if err != nil {
|
|
| 39 |
- t.Fatal(err) |
|
| 40 |
- } |
|
| 41 |
- return endpoint |
|
| 42 |
- } |
|
| 43 |
- |
|
| 44 |
- assertInsecureIndex := func(index *registry.IndexInfo) {
|
|
| 45 |
- index.Secure = true |
|
| 46 |
- _, err := newV1Endpoint(context.Background(), index, nil) |
|
| 47 |
- assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index") |
|
| 48 |
- index.Secure = false |
|
| 49 |
- } |
|
| 50 |
- |
|
| 51 |
- assertSecureIndex := func(index *registry.IndexInfo) {
|
|
| 52 |
- index.Secure = true |
|
| 53 |
- _, err := newV1Endpoint(context.Background(), index, nil) |
|
| 54 |
- assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index") |
|
| 55 |
- index.Secure = false |
|
| 56 |
- } |
|
| 57 |
- |
|
| 58 |
- index := ®istry.IndexInfo{}
|
|
| 59 |
- index.Name = makeURL("/v1/")
|
|
| 60 |
- endpoint := expandEndpoint(index) |
|
| 61 |
- assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) |
|
| 62 |
- assertInsecureIndex(index) |
|
| 63 |
- |
|
| 64 |
- index.Name = makeURL("")
|
|
| 65 |
- endpoint = expandEndpoint(index) |
|
| 66 |
- assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") |
|
| 67 |
- assertInsecureIndex(index) |
|
| 68 |
- |
|
| 69 |
- httpURL := makeURL("")
|
|
| 70 |
- index.Name = strings.SplitN(httpURL, "://", 2)[1] |
|
| 71 |
- endpoint = expandEndpoint(index) |
|
| 72 |
- assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") |
|
| 73 |
- assertInsecureIndex(index) |
|
| 74 |
- |
|
| 75 |
- index.Name = makeHTTPSURL("/v1/")
|
|
| 76 |
- endpoint = expandEndpoint(index) |
|
| 77 |
- assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) |
|
| 78 |
- assertSecureIndex(index) |
|
| 79 |
- |
|
| 80 |
- index.Name = makeHTTPSURL("")
|
|
| 81 |
- endpoint = expandEndpoint(index) |
|
| 82 |
- assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") |
|
| 83 |
- assertSecureIndex(index) |
|
| 84 |
- |
|
| 85 |
- httpsURL := makeHTTPSURL("")
|
|
| 86 |
- index.Name = strings.SplitN(httpsURL, "://", 2)[1] |
|
| 87 |
- endpoint = expandEndpoint(index) |
|
| 88 |
- assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") |
|
| 89 |
- assertSecureIndex(index) |
|
| 90 |
- |
|
| 91 |
- badEndpoints := []string{
|
|
| 92 |
- "http://127.0.0.1/v1/", |
|
| 93 |
- "https://127.0.0.1/v1/", |
|
| 94 |
- "http://127.0.0.1", |
|
| 95 |
- "https://127.0.0.1", |
|
| 96 |
- "127.0.0.1", |
|
| 97 |
- } |
|
| 98 |
- for _, address := range badEndpoints {
|
|
| 99 |
- index.Name = address |
|
| 100 |
- _, err := newV1Endpoint(context.Background(), index, nil) |
|
| 101 |
- assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address) |
|
| 102 |
- } |
|
| 103 |
-} |
|
| 104 |
- |
|
| 105 |
-func TestV1EndpointParse(t *testing.T) {
|
|
| 106 |
- tests := []struct {
|
|
| 107 |
- address string |
|
| 108 |
- expected string |
|
| 109 |
- expectedErr string |
|
| 110 |
- }{
|
|
| 111 |
- {
|
|
| 112 |
- address: IndexServer, |
|
| 113 |
- expected: IndexServer, |
|
| 114 |
- }, |
|
| 115 |
- {
|
|
| 116 |
- address: "https://0.0.0.0:5000/v1/", |
|
| 117 |
- expected: "https://0.0.0.0:5000/v1/", |
|
| 118 |
- }, |
|
| 119 |
- {
|
|
| 120 |
- address: "https://0.0.0.0:5000", |
|
| 121 |
- expected: "https://0.0.0.0:5000/v1/", |
|
| 122 |
- }, |
|
| 123 |
- {
|
|
| 124 |
- address: "0.0.0.0:5000", |
|
| 125 |
- expected: "https://0.0.0.0:5000/v1/", |
|
| 126 |
- }, |
|
| 127 |
- {
|
|
| 128 |
- address: "https://0.0.0.0:5000/nonversion/", |
|
| 129 |
- expected: "https://0.0.0.0:5000/nonversion/v1/", |
|
| 130 |
- }, |
|
| 131 |
- {
|
|
| 132 |
- address: "https://0.0.0.0:5000/v0/", |
|
| 133 |
- expected: "https://0.0.0.0:5000/v0/v1/", |
|
| 134 |
- }, |
|
| 135 |
- {
|
|
| 136 |
- address: "https://0.0.0.0:5000/v2/", |
|
| 137 |
- expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/", |
|
| 138 |
- }, |
|
| 139 |
- } |
|
| 140 |
- for _, tc := range tests {
|
|
| 141 |
- t.Run(tc.address, func(t *testing.T) {
|
|
| 142 |
- ep, err := newV1EndpointFromStr(tc.address, nil, nil) |
|
| 143 |
- if tc.expectedErr != "" {
|
|
| 144 |
- assert.Check(t, is.Error(err, tc.expectedErr)) |
|
| 145 |
- assert.Check(t, is.Nil(ep)) |
|
| 146 |
- } else {
|
|
| 147 |
- assert.NilError(t, err) |
|
| 148 |
- assert.Check(t, is.Equal(ep.String(), tc.expected)) |
|
| 149 |
- } |
|
| 150 |
- }) |
|
| 151 |
- } |
|
| 152 |
-} |
|
| 153 |
- |
|
| 154 |
-// Ensure that a registry endpoint that responds with a 401 only is determined |
|
| 155 |
-// to be a valid v1 registry endpoint |
|
| 156 |
-func TestV1EndpointValidate(t *testing.T) {
|
|
| 157 |
- requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 158 |
- w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
|
|
| 159 |
- w.WriteHeader(http.StatusUnauthorized) |
|
| 160 |
- }) |
|
| 161 |
- |
|
| 162 |
- // Make a test server which should validate as a v1 server. |
|
| 163 |
- testServer := httptest.NewServer(requireBasicAuthHandler) |
|
| 164 |
- defer testServer.Close() |
|
| 165 |
- |
|
| 166 |
- testEndpoint, err := newV1Endpoint(context.Background(), ®istry.IndexInfo{Name: testServer.URL}, nil)
|
|
| 167 |
- if err != nil {
|
|
| 168 |
- t.Fatal(err) |
|
| 169 |
- } |
|
| 170 |
- |
|
| 171 |
- if testEndpoint.URL.Scheme != "http" {
|
|
| 172 |
- t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
|
|
| 173 |
- } |
|
| 174 |
-} |
|
| 175 |
- |
|
| 176 |
-func TestTrustedLocation(t *testing.T) {
|
|
| 177 |
- for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
|
|
| 178 |
- req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) |
|
| 179 |
- assert.Check(t, !trustedLocation(req)) |
|
| 180 |
- } |
|
| 181 |
- |
|
| 182 |
- for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} {
|
|
| 183 |
- req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) |
|
| 184 |
- assert.Check(t, trustedLocation(req)) |
|
| 185 |
- } |
|
| 186 |
-} |
|
| 187 |
- |
|
| 188 |
-func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
|
|
| 189 |
- for _, urls := range [][]string{
|
|
| 190 |
- {"http://docker.io", "https://docker.com"},
|
|
| 191 |
- {"https://foo.docker.io:7777", "http://bar.docker.com"},
|
|
| 192 |
- {"https://foo.docker.io", "https://example.com"},
|
|
| 193 |
- } {
|
|
| 194 |
- reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) |
|
| 195 |
- reqFrom.Header.Add("Content-Type", "application/json")
|
|
| 196 |
- reqFrom.Header.Add("Authorization", "super_secret")
|
|
| 197 |
- reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) |
|
| 198 |
- |
|
| 199 |
- _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
|
| 200 |
- |
|
| 201 |
- if len(reqTo.Header) != 1 {
|
|
| 202 |
- t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
|
|
| 203 |
- } |
|
| 204 |
- |
|
| 205 |
- if reqTo.Header.Get("Content-Type") != "application/json" {
|
|
| 206 |
- t.Fatal("'Content-Type' should be 'application/json'")
|
|
| 207 |
- } |
|
| 208 |
- |
|
| 209 |
- if reqTo.Header.Get("Authorization") != "" {
|
|
| 210 |
- t.Fatal("'Authorization' should be empty")
|
|
| 211 |
- } |
|
| 212 |
- } |
|
| 213 |
- |
|
| 214 |
- for _, urls := range [][]string{
|
|
| 215 |
- {"https://docker.io", "https://docker.com"},
|
|
| 216 |
- {"https://foo.docker.io:7777", "https://bar.docker.com"},
|
|
| 217 |
- } {
|
|
| 218 |
- reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) |
|
| 219 |
- reqFrom.Header.Add("Content-Type", "application/json")
|
|
| 220 |
- reqFrom.Header.Add("Authorization", "super_secret")
|
|
| 221 |
- reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) |
|
| 222 |
- |
|
| 223 |
- _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
|
| 224 |
- |
|
| 225 |
- if len(reqTo.Header) != 2 {
|
|
| 226 |
- t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
|
|
| 227 |
- } |
|
| 228 |
- |
|
| 229 |
- if reqTo.Header.Get("Content-Type") != "application/json" {
|
|
| 230 |
- t.Fatal("'Content-Type' should be 'application/json'")
|
|
| 231 |
- } |
|
| 232 |
- |
|
| 233 |
- if reqTo.Header.Get("Authorization") != "super_secret" {
|
|
| 234 |
- t.Fatal("'Authorization' should be 'super_secret'")
|
|
| 235 |
- } |
|
| 236 |
- } |
|
| 237 |
-} |
| 238 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,248 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- // this is required for some certificates |
|
| 5 |
- "context" |
|
| 6 |
- _ "crypto/sha512" |
|
| 7 |
- "encoding/json" |
|
| 8 |
- "fmt" |
|
| 9 |
- "io" |
|
| 10 |
- "net/http" |
|
| 11 |
- "net/http/cookiejar" |
|
| 12 |
- "net/url" |
|
| 13 |
- "strconv" |
|
| 14 |
- "strings" |
|
| 15 |
- "sync" |
|
| 16 |
- |
|
| 17 |
- "github.com/containerd/log" |
|
| 18 |
- "github.com/moby/moby/api/types/registry" |
|
| 19 |
- "github.com/pkg/errors" |
|
| 20 |
-) |
|
| 21 |
- |
|
| 22 |
-// A session is used to communicate with a V1 registry |
|
| 23 |
-type session struct {
|
|
| 24 |
- indexEndpoint *v1Endpoint |
|
| 25 |
- client *http.Client |
|
| 26 |
-} |
|
| 27 |
- |
|
| 28 |
-type authTransport struct {
|
|
| 29 |
- base http.RoundTripper |
|
| 30 |
- authConfig *registry.AuthConfig |
|
| 31 |
- |
|
| 32 |
- alwaysSetBasicAuth bool |
|
| 33 |
- token []string |
|
| 34 |
- |
|
| 35 |
- mu sync.Mutex // guards modReq |
|
| 36 |
- modReq map[*http.Request]*http.Request // original -> modified |
|
| 37 |
-} |
|
| 38 |
- |
|
| 39 |
-// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official) |
|
| 40 |
-// |
|
| 41 |
-// For private v1 registries, set alwaysSetBasicAuth to true. |
|
| 42 |
-// |
|
| 43 |
-// For the official v1 registry, if there isn't already an Authorization header in the request, |
|
| 44 |
-// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. |
|
| 45 |
-// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing |
|
| 46 |
-// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent |
|
| 47 |
-// requests. |
|
| 48 |
-// |
|
| 49 |
-// If the server sends a token without the client having requested it, it is ignored. |
|
| 50 |
-// |
|
| 51 |
-// This RoundTripper also has a CancelRequest method important for correct timeout handling. |
|
| 52 |
-func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
|
|
| 53 |
- if base == nil {
|
|
| 54 |
- base = http.DefaultTransport |
|
| 55 |
- } |
|
| 56 |
- return &authTransport{
|
|
| 57 |
- base: base, |
|
| 58 |
- authConfig: authConfig, |
|
| 59 |
- alwaysSetBasicAuth: alwaysSetBasicAuth, |
|
| 60 |
- modReq: make(map[*http.Request]*http.Request), |
|
| 61 |
- } |
|
| 62 |
-} |
|
| 63 |
- |
|
| 64 |
-// cloneRequest returns a clone of the provided *http.Request. |
|
| 65 |
-// The clone is a shallow copy of the struct and its Header map. |
|
| 66 |
-func cloneRequest(r *http.Request) *http.Request {
|
|
| 67 |
- // shallow copy of the struct |
|
| 68 |
- r2 := new(http.Request) |
|
| 69 |
- *r2 = *r |
|
| 70 |
- // deep copy of the Header |
|
| 71 |
- r2.Header = make(http.Header, len(r.Header)) |
|
| 72 |
- for k, s := range r.Header {
|
|
| 73 |
- r2.Header[k] = append([]string(nil), s...) |
|
| 74 |
- } |
|
| 75 |
- |
|
| 76 |
- return r2 |
|
| 77 |
-} |
|
| 78 |
- |
|
| 79 |
-// onEOFReader wraps an io.ReadCloser and a function |
|
| 80 |
-// the function will run at the end of file or close the file. |
|
| 81 |
-type onEOFReader struct {
|
|
| 82 |
- Rc io.ReadCloser |
|
| 83 |
- Fn func() |
|
| 84 |
-} |
|
| 85 |
- |
|
| 86 |
-func (r *onEOFReader) Read(p []byte) (int, error) {
|
|
| 87 |
- n, err := r.Rc.Read(p) |
|
| 88 |
- if err == io.EOF {
|
|
| 89 |
- r.runFunc() |
|
| 90 |
- } |
|
| 91 |
- return n, err |
|
| 92 |
-} |
|
| 93 |
- |
|
| 94 |
-// Close closes the file and run the function. |
|
| 95 |
-func (r *onEOFReader) Close() error {
|
|
| 96 |
- err := r.Rc.Close() |
|
| 97 |
- r.runFunc() |
|
| 98 |
- return err |
|
| 99 |
-} |
|
| 100 |
- |
|
| 101 |
-func (r *onEOFReader) runFunc() {
|
|
| 102 |
- if fn := r.Fn; fn != nil {
|
|
| 103 |
- fn() |
|
| 104 |
- r.Fn = nil |
|
| 105 |
- } |
|
| 106 |
-} |
|
| 107 |
- |
|
| 108 |
-// RoundTrip changes an HTTP request's headers to add the necessary |
|
| 109 |
-// authentication-related headers |
|
| 110 |
-func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
|
| 111 |
- // Authorization should not be set on 302 redirect for untrusted locations. |
|
| 112 |
- // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. |
|
| 113 |
- // As the authorization logic is currently implemented in RoundTrip, |
|
| 114 |
- // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. |
|
| 115 |
- // This is safe as Docker doesn't set Referrer in other scenarios. |
|
| 116 |
- if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
|
|
| 117 |
- return tr.base.RoundTrip(orig) |
|
| 118 |
- } |
|
| 119 |
- |
|
| 120 |
- req := cloneRequest(orig) |
|
| 121 |
- tr.mu.Lock() |
|
| 122 |
- tr.modReq[orig] = req |
|
| 123 |
- tr.mu.Unlock() |
|
| 124 |
- |
|
| 125 |
- if tr.alwaysSetBasicAuth {
|
|
| 126 |
- if tr.authConfig == nil {
|
|
| 127 |
- return nil, errors.New("unexpected error: empty auth config")
|
|
| 128 |
- } |
|
| 129 |
- req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) |
|
| 130 |
- return tr.base.RoundTrip(req) |
|
| 131 |
- } |
|
| 132 |
- |
|
| 133 |
- // Don't override |
|
| 134 |
- if req.Header.Get("Authorization") == "" {
|
|
| 135 |
- if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" {
|
|
| 136 |
- req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) |
|
| 137 |
- } else if len(tr.token) > 0 {
|
|
| 138 |
- req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
|
|
| 139 |
- } |
|
| 140 |
- } |
|
| 141 |
- resp, err := tr.base.RoundTrip(req) |
|
| 142 |
- if err != nil {
|
|
| 143 |
- tr.mu.Lock() |
|
| 144 |
- delete(tr.modReq, orig) |
|
| 145 |
- tr.mu.Unlock() |
|
| 146 |
- return nil, err |
|
| 147 |
- } |
|
| 148 |
- if len(resp.Header["X-Docker-Token"]) > 0 {
|
|
| 149 |
- tr.token = resp.Header["X-Docker-Token"] |
|
| 150 |
- } |
|
| 151 |
- resp.Body = &onEOFReader{
|
|
| 152 |
- Rc: resp.Body, |
|
| 153 |
- Fn: func() {
|
|
| 154 |
- tr.mu.Lock() |
|
| 155 |
- delete(tr.modReq, orig) |
|
| 156 |
- tr.mu.Unlock() |
|
| 157 |
- }, |
|
| 158 |
- } |
|
| 159 |
- return resp, nil |
|
| 160 |
-} |
|
| 161 |
- |
|
| 162 |
-// CancelRequest cancels an in-flight request by closing its connection. |
|
| 163 |
-func (tr *authTransport) CancelRequest(req *http.Request) {
|
|
| 164 |
- type canceler interface {
|
|
| 165 |
- CancelRequest(*http.Request) |
|
| 166 |
- } |
|
| 167 |
- if cr, ok := tr.base.(canceler); ok {
|
|
| 168 |
- tr.mu.Lock() |
|
| 169 |
- modReq := tr.modReq[req] |
|
| 170 |
- delete(tr.modReq, req) |
|
| 171 |
- tr.mu.Unlock() |
|
| 172 |
- cr.CancelRequest(modReq) |
|
| 173 |
- } |
|
| 174 |
-} |
|
| 175 |
- |
|
| 176 |
-func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
|
|
| 177 |
- var alwaysSetBasicAuth bool |
|
| 178 |
- |
|
| 179 |
- // If we're working with a standalone private registry over HTTPS, send Basic Auth headers |
|
| 180 |
- // alongside all our requests. |
|
| 181 |
- if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
|
|
| 182 |
- info, err := endpoint.ping(ctx) |
|
| 183 |
- if err != nil {
|
|
| 184 |
- return err |
|
| 185 |
- } |
|
| 186 |
- if info.Standalone && authConfig != nil {
|
|
| 187 |
- log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth")
|
|
| 188 |
- alwaysSetBasicAuth = true |
|
| 189 |
- } |
|
| 190 |
- } |
|
| 191 |
- |
|
| 192 |
- // Annotate the transport unconditionally so that v2 can |
|
| 193 |
- // properly fallback on v1 when an image is not found. |
|
| 194 |
- client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) |
|
| 195 |
- |
|
| 196 |
- jar, err := cookiejar.New(nil) |
|
| 197 |
- if err != nil {
|
|
| 198 |
- return systemErr{errors.New("cookiejar.New is not supposed to return an error")}
|
|
| 199 |
- } |
|
| 200 |
- client.Jar = jar |
|
| 201 |
- |
|
| 202 |
- return nil |
|
| 203 |
-} |
|
| 204 |
- |
|
| 205 |
-func newSession(client *http.Client, endpoint *v1Endpoint) *session {
|
|
| 206 |
- return &session{
|
|
| 207 |
- client: client, |
|
| 208 |
- indexEndpoint: endpoint, |
|
| 209 |
- } |
|
| 210 |
-} |
|
| 211 |
- |
|
| 212 |
-// defaultSearchLimit is the default value for maximum number of returned search results. |
|
| 213 |
-const defaultSearchLimit = 25 |
|
| 214 |
- |
|
| 215 |
-// searchRepositories performs a search against the remote repository |
|
| 216 |
-func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
|
|
| 217 |
- if limit == 0 {
|
|
| 218 |
- limit = defaultSearchLimit |
|
| 219 |
- } |
|
| 220 |
- if limit < 1 || limit > 100 {
|
|
| 221 |
- return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
|
|
| 222 |
- } |
|
| 223 |
- u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit)) |
|
| 224 |
- log.G(ctx).WithField("url", u).Debug("searchRepositories")
|
|
| 225 |
- |
|
| 226 |
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) |
|
| 227 |
- if err != nil {
|
|
| 228 |
- return nil, invalidParamWrapf(err, "error building request") |
|
| 229 |
- } |
|
| 230 |
- // Have the AuthTransport send authentication, when logged in. |
|
| 231 |
- req.Header.Set("X-Docker-Token", "true")
|
|
| 232 |
- res, err := r.client.Do(req) |
|
| 233 |
- if err != nil {
|
|
| 234 |
- return nil, systemErr{err}
|
|
| 235 |
- } |
|
| 236 |
- defer res.Body.Close() |
|
| 237 |
- if res.StatusCode != http.StatusOK {
|
|
| 238 |
- // TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286). |
|
| 239 |
- // TODO(thaJeztah): handle other status-codes to return correct error-type |
|
| 240 |
- return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)}
|
|
| 241 |
- } |
|
| 242 |
- result := ®istry.SearchResults{}
|
|
| 243 |
- err = json.NewDecoder(res.Body).Decode(result) |
|
| 244 |
- if err != nil {
|
|
| 245 |
- return nil, systemErr{errors.Wrap(err, "error decoding registry search results")}
|
|
| 246 |
- } |
|
| 247 |
- return result, nil |
|
| 248 |
-} |
| 249 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,418 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "encoding/json" |
|
| 6 |
- "net/http" |
|
| 7 |
- "net/http/httptest" |
|
| 8 |
- "net/http/httputil" |
|
| 9 |
- "testing" |
|
| 10 |
- |
|
| 11 |
- cerrdefs "github.com/containerd/errdefs" |
|
| 12 |
- "github.com/docker/distribution/registry/client/transport" |
|
| 13 |
- "github.com/moby/moby/api/types/filters" |
|
| 14 |
- "github.com/moby/moby/api/types/registry" |
|
| 15 |
- "gotest.tools/v3/assert" |
|
| 16 |
-) |
|
| 17 |
- |
|
| 18 |
-func spawnTestRegistrySession(t *testing.T) *session {
|
|
| 19 |
- t.Helper() |
|
| 20 |
- authConfig := ®istry.AuthConfig{}
|
|
| 21 |
- endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil)
|
|
| 22 |
- if err != nil {
|
|
| 23 |
- t.Fatal(err) |
|
| 24 |
- } |
|
| 25 |
- userAgent := "docker test client" |
|
| 26 |
- var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
|
|
| 27 |
- tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...) |
|
| 28 |
- client := httpClient(tr) |
|
| 29 |
- |
|
| 30 |
- if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil {
|
|
| 31 |
- t.Fatal(err) |
|
| 32 |
- } |
|
| 33 |
- r := newSession(client, endpoint) |
|
| 34 |
- |
|
| 35 |
- // In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true` |
|
| 36 |
- // header while authenticating, in order to retrieve a token that can be later used to |
|
| 37 |
- // perform authenticated actions. |
|
| 38 |
- // |
|
| 39 |
- // The mock v1 registry does not support that, (TODO(tiborvass): support it), instead, |
|
| 40 |
- // it will consider authenticated any request with the header `X-Docker-Token: fake-token`. |
|
| 41 |
- // |
|
| 42 |
- // Because we know that the client's transport is an `*authTransport` we simply cast it, |
|
| 43 |
- // in order to set the internal cached token to the fake token, and thus send that fake token |
|
| 44 |
- // upon every subsequent requests. |
|
| 45 |
- r.client.Transport.(*authTransport).token = []string{"fake-token"}
|
|
| 46 |
- return r |
|
| 47 |
-} |
|
| 48 |
- |
|
| 49 |
-type debugTransport struct {
|
|
| 50 |
- http.RoundTripper |
|
| 51 |
- log func(...interface{})
|
|
| 52 |
-} |
|
| 53 |
- |
|
| 54 |
-func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
| 55 |
- dump, err := httputil.DumpRequestOut(req, false) |
|
| 56 |
- if err != nil {
|
|
| 57 |
- tr.log("could not dump request")
|
|
| 58 |
- } |
|
| 59 |
- tr.log(string(dump)) |
|
| 60 |
- resp, err := tr.RoundTripper.RoundTrip(req) |
|
| 61 |
- if err != nil {
|
|
| 62 |
- return nil, err |
|
| 63 |
- } |
|
| 64 |
- dump, err = httputil.DumpResponse(resp, false) |
|
| 65 |
- if err != nil {
|
|
| 66 |
- tr.log("could not dump response")
|
|
| 67 |
- } |
|
| 68 |
- tr.log(string(dump)) |
|
| 69 |
- return resp, err |
|
| 70 |
-} |
|
| 71 |
- |
|
| 72 |
-func TestSearchRepositories(t *testing.T) {
|
|
| 73 |
- r := spawnTestRegistrySession(t) |
|
| 74 |
- results, err := r.searchRepositories(context.Background(), "fakequery", 25) |
|
| 75 |
- if err != nil {
|
|
| 76 |
- t.Fatal(err) |
|
| 77 |
- } |
|
| 78 |
- if results == nil {
|
|
| 79 |
- t.Fatal("Expected non-nil SearchResults object")
|
|
| 80 |
- } |
|
| 81 |
- assert.Equal(t, results.NumResults, 1, "Expected 1 search results") |
|
| 82 |
- assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query") |
|
| 83 |
- assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") |
|
| 84 |
-} |
|
| 85 |
- |
|
| 86 |
-func TestSearchErrors(t *testing.T) {
|
|
| 87 |
- errorCases := []struct {
|
|
| 88 |
- filtersArgs filters.Args |
|
| 89 |
- shouldReturnError bool |
|
| 90 |
- expectedError string |
|
| 91 |
- }{
|
|
| 92 |
- {
|
|
| 93 |
- expectedError: "unexpected status code 500", |
|
| 94 |
- shouldReturnError: true, |
|
| 95 |
- }, |
|
| 96 |
- {
|
|
| 97 |
- filtersArgs: filters.NewArgs(filters.Arg("type", "custom")),
|
|
| 98 |
- expectedError: "invalid filter 'type'", |
|
| 99 |
- }, |
|
| 100 |
- {
|
|
| 101 |
- filtersArgs: filters.NewArgs(filters.Arg("is-automated", "invalid")),
|
|
| 102 |
- expectedError: "invalid filter 'is-automated=[invalid]'", |
|
| 103 |
- }, |
|
| 104 |
- {
|
|
| 105 |
- filtersArgs: filters.NewArgs( |
|
| 106 |
- filters.Arg("is-automated", "true"),
|
|
| 107 |
- filters.Arg("is-automated", "false"),
|
|
| 108 |
- ), |
|
| 109 |
- expectedError: "invalid filter 'is-automated", |
|
| 110 |
- }, |
|
| 111 |
- {
|
|
| 112 |
- filtersArgs: filters.NewArgs(filters.Arg("is-official", "invalid")),
|
|
| 113 |
- expectedError: "invalid filter 'is-official=[invalid]'", |
|
| 114 |
- }, |
|
| 115 |
- {
|
|
| 116 |
- filtersArgs: filters.NewArgs( |
|
| 117 |
- filters.Arg("is-official", "true"),
|
|
| 118 |
- filters.Arg("is-official", "false"),
|
|
| 119 |
- ), |
|
| 120 |
- expectedError: "invalid filter 'is-official", |
|
| 121 |
- }, |
|
| 122 |
- {
|
|
| 123 |
- filtersArgs: filters.NewArgs(filters.Arg("stars", "invalid")),
|
|
| 124 |
- expectedError: "invalid filter 'stars=invalid'", |
|
| 125 |
- }, |
|
| 126 |
- {
|
|
| 127 |
- filtersArgs: filters.NewArgs( |
|
| 128 |
- filters.Arg("stars", "1"),
|
|
| 129 |
- filters.Arg("stars", "invalid"),
|
|
| 130 |
- ), |
|
| 131 |
- expectedError: "invalid filter 'stars=invalid'", |
|
| 132 |
- }, |
|
| 133 |
- } |
|
| 134 |
- for _, tc := range errorCases {
|
|
| 135 |
- t.Run(tc.expectedError, func(t *testing.T) {
|
|
| 136 |
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 137 |
- if !tc.shouldReturnError {
|
|
| 138 |
- t.Errorf("unexpected HTTP request")
|
|
| 139 |
- } |
|
| 140 |
- http.Error(w, "no search for you", http.StatusInternalServerError) |
|
| 141 |
- })) |
|
| 142 |
- defer srv.Close() |
|
| 143 |
- |
|
| 144 |
- // Construct the search term by cutting the 'http://' prefix off srv.URL. |
|
| 145 |
- term := srv.URL[7:] + "/term" |
|
| 146 |
- |
|
| 147 |
- reg, err := NewService(ServiceOptions{})
|
|
| 148 |
- assert.NilError(t, err) |
|
| 149 |
- _, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
|
|
| 150 |
- assert.ErrorContains(t, err, tc.expectedError) |
|
| 151 |
- if tc.shouldReturnError {
|
|
| 152 |
- assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err) |
|
| 153 |
- return |
|
| 154 |
- } |
|
| 155 |
- assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err) |
|
| 156 |
- }) |
|
| 157 |
- } |
|
| 158 |
-} |
|
| 159 |
- |
|
| 160 |
-func TestSearch(t *testing.T) {
|
|
| 161 |
- const term = "term" |
|
| 162 |
- successCases := []struct {
|
|
| 163 |
- name string |
|
| 164 |
- filtersArgs filters.Args |
|
| 165 |
- registryResults []registry.SearchResult |
|
| 166 |
- expectedResults []registry.SearchResult |
|
| 167 |
- }{
|
|
| 168 |
- {
|
|
| 169 |
- name: "empty results", |
|
| 170 |
- registryResults: []registry.SearchResult{},
|
|
| 171 |
- expectedResults: []registry.SearchResult{},
|
|
| 172 |
- }, |
|
| 173 |
- {
|
|
| 174 |
- name: "no filter", |
|
| 175 |
- registryResults: []registry.SearchResult{
|
|
| 176 |
- {
|
|
| 177 |
- Name: "name", |
|
| 178 |
- Description: "description", |
|
| 179 |
- }, |
|
| 180 |
- }, |
|
| 181 |
- expectedResults: []registry.SearchResult{
|
|
| 182 |
- {
|
|
| 183 |
- Name: "name", |
|
| 184 |
- Description: "description", |
|
| 185 |
- }, |
|
| 186 |
- }, |
|
| 187 |
- }, |
|
| 188 |
- {
|
|
| 189 |
- name: "is-automated=true, no results", |
|
| 190 |
- filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
|
| 191 |
- registryResults: []registry.SearchResult{
|
|
| 192 |
- {
|
|
| 193 |
- Name: "name", |
|
| 194 |
- Description: "description", |
|
| 195 |
- }, |
|
| 196 |
- }, |
|
| 197 |
- expectedResults: []registry.SearchResult{},
|
|
| 198 |
- }, |
|
| 199 |
- {
|
|
| 200 |
- name: "is-automated=true", |
|
| 201 |
- filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
|
| 202 |
- registryResults: []registry.SearchResult{
|
|
| 203 |
- {
|
|
| 204 |
- Name: "name", |
|
| 205 |
- Description: "description", |
|
| 206 |
- IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 207 |
- }, |
|
| 208 |
- }, |
|
| 209 |
- expectedResults: []registry.SearchResult{},
|
|
| 210 |
- }, |
|
| 211 |
- {
|
|
| 212 |
- name: "is-automated=false, IsAutomated reset to false", |
|
| 213 |
- filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
|
| 214 |
- registryResults: []registry.SearchResult{
|
|
| 215 |
- {
|
|
| 216 |
- Name: "name", |
|
| 217 |
- Description: "description", |
|
| 218 |
- IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 219 |
- }, |
|
| 220 |
- }, |
|
| 221 |
- expectedResults: []registry.SearchResult{
|
|
| 222 |
- {
|
|
| 223 |
- Name: "name", |
|
| 224 |
- Description: "description", |
|
| 225 |
- IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 226 |
- }, |
|
| 227 |
- }, |
|
| 228 |
- }, |
|
| 229 |
- {
|
|
| 230 |
- name: "is-automated=false", |
|
| 231 |
- filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
|
| 232 |
- registryResults: []registry.SearchResult{
|
|
| 233 |
- {
|
|
| 234 |
- Name: "name", |
|
| 235 |
- Description: "description", |
|
| 236 |
- }, |
|
| 237 |
- }, |
|
| 238 |
- expectedResults: []registry.SearchResult{
|
|
| 239 |
- {
|
|
| 240 |
- Name: "name", |
|
| 241 |
- Description: "description", |
|
| 242 |
- }, |
|
| 243 |
- }, |
|
| 244 |
- }, |
|
| 245 |
- {
|
|
| 246 |
- name: "is-official=true, no results", |
|
| 247 |
- filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
|
| 248 |
- registryResults: []registry.SearchResult{
|
|
| 249 |
- {
|
|
| 250 |
- Name: "name", |
|
| 251 |
- Description: "description", |
|
| 252 |
- }, |
|
| 253 |
- }, |
|
| 254 |
- expectedResults: []registry.SearchResult{},
|
|
| 255 |
- }, |
|
| 256 |
- {
|
|
| 257 |
- name: "is-official=true", |
|
| 258 |
- filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
|
| 259 |
- registryResults: []registry.SearchResult{
|
|
| 260 |
- {
|
|
| 261 |
- Name: "name", |
|
| 262 |
- Description: "description", |
|
| 263 |
- IsOfficial: true, |
|
| 264 |
- }, |
|
| 265 |
- }, |
|
| 266 |
- expectedResults: []registry.SearchResult{
|
|
| 267 |
- {
|
|
| 268 |
- Name: "name", |
|
| 269 |
- Description: "description", |
|
| 270 |
- IsOfficial: true, |
|
| 271 |
- }, |
|
| 272 |
- }, |
|
| 273 |
- }, |
|
| 274 |
- {
|
|
| 275 |
- name: "is-official=false, no results", |
|
| 276 |
- filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
|
| 277 |
- registryResults: []registry.SearchResult{
|
|
| 278 |
- {
|
|
| 279 |
- Name: "name", |
|
| 280 |
- Description: "description", |
|
| 281 |
- IsOfficial: true, |
|
| 282 |
- }, |
|
| 283 |
- }, |
|
| 284 |
- expectedResults: []registry.SearchResult{},
|
|
| 285 |
- }, |
|
| 286 |
- {
|
|
| 287 |
- name: "is-official=false", |
|
| 288 |
- filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
|
| 289 |
- registryResults: []registry.SearchResult{
|
|
| 290 |
- {
|
|
| 291 |
- Name: "name", |
|
| 292 |
- Description: "description", |
|
| 293 |
- IsOfficial: false, |
|
| 294 |
- }, |
|
| 295 |
- }, |
|
| 296 |
- expectedResults: []registry.SearchResult{
|
|
| 297 |
- {
|
|
| 298 |
- Name: "name", |
|
| 299 |
- Description: "description", |
|
| 300 |
- IsOfficial: false, |
|
| 301 |
- }, |
|
| 302 |
- }, |
|
| 303 |
- }, |
|
| 304 |
- {
|
|
| 305 |
- name: "stars=0", |
|
| 306 |
- filtersArgs: filters.NewArgs(filters.Arg("stars", "0")),
|
|
| 307 |
- registryResults: []registry.SearchResult{
|
|
| 308 |
- {
|
|
| 309 |
- Name: "name", |
|
| 310 |
- Description: "description", |
|
| 311 |
- StarCount: 0, |
|
| 312 |
- }, |
|
| 313 |
- }, |
|
| 314 |
- expectedResults: []registry.SearchResult{
|
|
| 315 |
- {
|
|
| 316 |
- Name: "name", |
|
| 317 |
- Description: "description", |
|
| 318 |
- StarCount: 0, |
|
| 319 |
- }, |
|
| 320 |
- }, |
|
| 321 |
- }, |
|
| 322 |
- {
|
|
| 323 |
- name: "stars=0, no results", |
|
| 324 |
- filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
|
| 325 |
- registryResults: []registry.SearchResult{
|
|
| 326 |
- {
|
|
| 327 |
- Name: "name", |
|
| 328 |
- Description: "description", |
|
| 329 |
- StarCount: 0, |
|
| 330 |
- }, |
|
| 331 |
- }, |
|
| 332 |
- expectedResults: []registry.SearchResult{},
|
|
| 333 |
- }, |
|
| 334 |
- {
|
|
| 335 |
- name: "stars=1", |
|
| 336 |
- filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
|
| 337 |
- registryResults: []registry.SearchResult{
|
|
| 338 |
- {
|
|
| 339 |
- Name: "name0", |
|
| 340 |
- Description: "description0", |
|
| 341 |
- StarCount: 0, |
|
| 342 |
- }, |
|
| 343 |
- {
|
|
| 344 |
- Name: "name1", |
|
| 345 |
- Description: "description1", |
|
| 346 |
- StarCount: 1, |
|
| 347 |
- }, |
|
| 348 |
- }, |
|
| 349 |
- expectedResults: []registry.SearchResult{
|
|
| 350 |
- {
|
|
| 351 |
- Name: "name1", |
|
| 352 |
- Description: "description1", |
|
| 353 |
- StarCount: 1, |
|
| 354 |
- }, |
|
| 355 |
- }, |
|
| 356 |
- }, |
|
| 357 |
- {
|
|
| 358 |
- name: "stars=1, is-official=true, is-automated=true", |
|
| 359 |
- filtersArgs: filters.NewArgs( |
|
| 360 |
- filters.Arg("stars", "1"),
|
|
| 361 |
- filters.Arg("is-official", "true"),
|
|
| 362 |
- filters.Arg("is-automated", "true"),
|
|
| 363 |
- ), |
|
| 364 |
- registryResults: []registry.SearchResult{
|
|
| 365 |
- {
|
|
| 366 |
- Name: "name0", |
|
| 367 |
- Description: "description0", |
|
| 368 |
- StarCount: 0, |
|
| 369 |
- IsOfficial: true, |
|
| 370 |
- IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 371 |
- }, |
|
| 372 |
- {
|
|
| 373 |
- Name: "name1", |
|
| 374 |
- Description: "description1", |
|
| 375 |
- StarCount: 1, |
|
| 376 |
- IsOfficial: false, |
|
| 377 |
- IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 378 |
- }, |
|
| 379 |
- {
|
|
| 380 |
- Name: "name2", |
|
| 381 |
- Description: "description2", |
|
| 382 |
- StarCount: 1, |
|
| 383 |
- IsOfficial: true, |
|
| 384 |
- }, |
|
| 385 |
- {
|
|
| 386 |
- Name: "name3", |
|
| 387 |
- Description: "description3", |
|
| 388 |
- StarCount: 2, |
|
| 389 |
- IsOfficial: true, |
|
| 390 |
- IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
|
| 391 |
- }, |
|
| 392 |
- }, |
|
| 393 |
- expectedResults: []registry.SearchResult{},
|
|
| 394 |
- }, |
|
| 395 |
- } |
|
| 396 |
- for _, tc := range successCases {
|
|
| 397 |
- t.Run(tc.name, func(t *testing.T) {
|
|
| 398 |
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 399 |
- w.Header().Set("Content-type", "application/json")
|
|
| 400 |
- json.NewEncoder(w).Encode(registry.SearchResults{
|
|
| 401 |
- Query: term, |
|
| 402 |
- NumResults: len(tc.registryResults), |
|
| 403 |
- Results: tc.registryResults, |
|
| 404 |
- }) |
|
| 405 |
- })) |
|
| 406 |
- defer srv.Close() |
|
| 407 |
- |
|
| 408 |
- // Construct the search term by cutting the 'http://' prefix off srv.URL. |
|
| 409 |
- searchTerm := srv.URL[7:] + "/" + term |
|
| 410 |
- |
|
| 411 |
- reg, err := NewService(ServiceOptions{})
|
|
| 412 |
- assert.NilError(t, err) |
|
| 413 |
- results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
|
|
| 414 |
- assert.NilError(t, err) |
|
| 415 |
- assert.DeepEqual(t, results, tc.expectedResults) |
|
| 416 |
- }) |
|
| 417 |
- } |
|
| 418 |
-} |
| 419 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,160 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "crypto/tls" |
|
| 6 |
- "errors" |
|
| 7 |
- "net/url" |
|
| 8 |
- "strings" |
|
| 9 |
- "sync" |
|
| 10 |
- |
|
| 11 |
- cerrdefs "github.com/containerd/errdefs" |
|
| 12 |
- "github.com/containerd/log" |
|
| 13 |
- "github.com/distribution/reference" |
|
| 14 |
- "github.com/moby/moby/api/types/registry" |
|
| 15 |
-) |
|
| 16 |
- |
|
| 17 |
-// Service is a registry service. It tracks configuration data such as a list |
|
| 18 |
-// of mirrors. |
|
| 19 |
-type Service struct {
|
|
| 20 |
- config *serviceConfig |
|
| 21 |
- mu sync.RWMutex |
|
| 22 |
-} |
|
| 23 |
- |
|
| 24 |
-// NewService returns a new instance of [Service] ready to be installed into |
|
| 25 |
-// an engine. |
|
| 26 |
-func NewService(options ServiceOptions) (*Service, error) {
|
|
| 27 |
- config, err := newServiceConfig(options) |
|
| 28 |
- if err != nil {
|
|
| 29 |
- return nil, err |
|
| 30 |
- } |
|
| 31 |
- |
|
| 32 |
- return &Service{config: config}, err
|
|
| 33 |
-} |
|
| 34 |
- |
|
| 35 |
-// ServiceConfig returns a copy of the public registry service's configuration. |
|
| 36 |
-func (s *Service) ServiceConfig() *registry.ServiceConfig {
|
|
| 37 |
- s.mu.RLock() |
|
| 38 |
- defer s.mu.RUnlock() |
|
| 39 |
- return s.config.copy() |
|
| 40 |
-} |
|
| 41 |
- |
|
| 42 |
-// ReplaceConfig prepares a transaction which will atomically replace the |
|
| 43 |
-// registry service's configuration when the returned commit function is called. |
|
| 44 |
-func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) {
|
|
| 45 |
- config, err := newServiceConfig(options) |
|
| 46 |
- if err != nil {
|
|
| 47 |
- return nil, err |
|
| 48 |
- } |
|
| 49 |
- return func() {
|
|
| 50 |
- s.mu.Lock() |
|
| 51 |
- defer s.mu.Unlock() |
|
| 52 |
- s.config = config |
|
| 53 |
- }, nil |
|
| 54 |
-} |
|
| 55 |
- |
|
| 56 |
-// Auth contacts the public registry with the provided credentials, |
|
| 57 |
-// and returns OK if authentication was successful. |
|
| 58 |
-// It can be used to verify the validity of a client's credentials. |
|
| 59 |
-func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) {
|
|
| 60 |
- // TODO Use ctx when searching for repositories |
|
| 61 |
- registryHostName := IndexHostname |
|
| 62 |
- |
|
| 63 |
- if authConfig.ServerAddress != "" {
|
|
| 64 |
- serverAddress := authConfig.ServerAddress |
|
| 65 |
- if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
|
| 66 |
- serverAddress = "https://" + serverAddress |
|
| 67 |
- } |
|
| 68 |
- u, err := url.Parse(serverAddress) |
|
| 69 |
- if err != nil {
|
|
| 70 |
- return "", "", invalidParamWrapf(err, "unable to parse server address") |
|
| 71 |
- } |
|
| 72 |
- registryHostName = u.Host |
|
| 73 |
- } |
|
| 74 |
- |
|
| 75 |
- // Lookup endpoints for authentication but exclude mirrors to prevent |
|
| 76 |
- // sending credentials of the upstream registry to a mirror. |
|
| 77 |
- s.mu.RLock() |
|
| 78 |
- endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false) |
|
| 79 |
- s.mu.RUnlock() |
|
| 80 |
- if err != nil {
|
|
| 81 |
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
| 82 |
- return "", "", err |
|
| 83 |
- } |
|
| 84 |
- return "", "", invalidParam(err) |
|
| 85 |
- } |
|
| 86 |
- |
|
| 87 |
- var lastErr error |
|
| 88 |
- for _, endpoint := range endpoints {
|
|
| 89 |
- authToken, err := loginV2(ctx, authConfig, endpoint, userAgent) |
|
| 90 |
- if err != nil {
|
|
| 91 |
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
|
|
| 92 |
- // Failed to authenticate; don't continue with (non-TLS) endpoints. |
|
| 93 |
- return "", "", err |
|
| 94 |
- } |
|
| 95 |
- // Try next endpoint |
|
| 96 |
- log.G(ctx).WithFields(log.Fields{
|
|
| 97 |
- "error": err, |
|
| 98 |
- "endpoint": endpoint, |
|
| 99 |
- }).Infof("Error logging in to endpoint, trying next endpoint")
|
|
| 100 |
- lastErr = err |
|
| 101 |
- continue |
|
| 102 |
- } |
|
| 103 |
- |
|
| 104 |
- // TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here? |
|
| 105 |
- return "Login Succeeded", authToken, nil |
|
| 106 |
- } |
|
| 107 |
- |
|
| 108 |
- return "", "", lastErr |
|
| 109 |
-} |
|
| 110 |
- |
|
| 111 |
-// ResolveAuthConfig looks up authentication for the given reference from the |
|
| 112 |
-// given authConfigs. |
|
| 113 |
-// |
|
| 114 |
-// IMPORTANT: This function is for internal use and should not be used by external projects. |
|
| 115 |
-func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
|
|
| 116 |
- s.mu.RLock() |
|
| 117 |
- defer s.mu.RUnlock() |
|
| 118 |
- // Simplified version of "newIndexInfo" without handling of insecure |
|
| 119 |
- // registries and mirrors, as we don't need that information to resolve |
|
| 120 |
- // the auth-config. |
|
| 121 |
- indexName := normalizeIndexName(reference.Domain(ref)) |
|
| 122 |
- registryInfo, ok := s.config.IndexConfigs[indexName] |
|
| 123 |
- if !ok {
|
|
| 124 |
- registryInfo = ®istry.IndexInfo{Name: indexName}
|
|
| 125 |
- } |
|
| 126 |
- return ResolveAuthConfig(authConfigs, registryInfo) |
|
| 127 |
-} |
|
| 128 |
- |
|
| 129 |
-// APIEndpoint represents a remote API endpoint |
|
| 130 |
-type APIEndpoint struct {
|
|
| 131 |
- Mirror bool |
|
| 132 |
- URL *url.URL |
|
| 133 |
- TLSConfig *tls.Config |
|
| 134 |
-} |
|
| 135 |
- |
|
| 136 |
-// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference. |
|
| 137 |
-// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP. |
|
| 138 |
-func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
|
|
| 139 |
- s.mu.RLock() |
|
| 140 |
- defer s.mu.RUnlock() |
|
| 141 |
- |
|
| 142 |
- return s.lookupV2Endpoints(context.TODO(), hostname, true) |
|
| 143 |
-} |
|
| 144 |
- |
|
| 145 |
-// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference. |
|
| 146 |
-// It gives preference to HTTPS over plain HTTP. Mirrors are not included. |
|
| 147 |
-func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
|
|
| 148 |
- s.mu.RLock() |
|
| 149 |
- defer s.mu.RUnlock() |
|
| 150 |
- |
|
| 151 |
- return s.lookupV2Endpoints(context.TODO(), hostname, false) |
|
| 152 |
-} |
|
| 153 |
- |
|
| 154 |
-// IsInsecureRegistry returns true if the registry at given host is configured as |
|
| 155 |
-// insecure registry. |
|
| 156 |
-func (s *Service) IsInsecureRegistry(host string) bool {
|
|
| 157 |
- s.mu.RLock() |
|
| 158 |
- defer s.mu.RUnlock() |
|
| 159 |
- return !s.config.isSecureIndex(host) |
|
| 160 |
-} |
| 161 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,73 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "context" |
|
| 5 |
- "net/url" |
|
| 6 |
- "strings" |
|
| 7 |
- |
|
| 8 |
- "github.com/docker/go-connections/tlsconfig" |
|
| 9 |
-) |
|
| 10 |
- |
|
| 11 |
-func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) {
|
|
| 12 |
- var endpoints []APIEndpoint |
|
| 13 |
- if hostname == DefaultNamespace || hostname == IndexHostname {
|
|
| 14 |
- if includeMirrors {
|
|
| 15 |
- for _, mirror := range s.config.Mirrors {
|
|
| 16 |
- if ctx.Err() != nil {
|
|
| 17 |
- return nil, ctx.Err() |
|
| 18 |
- } |
|
| 19 |
- if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
|
|
| 20 |
- mirror = "https://" + mirror |
|
| 21 |
- } |
|
| 22 |
- mirrorURL, err := url.Parse(mirror) |
|
| 23 |
- if err != nil {
|
|
| 24 |
- return nil, invalidParam(err) |
|
| 25 |
- } |
|
| 26 |
- // TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time. |
|
| 27 |
- mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host)) |
|
| 28 |
- if err != nil {
|
|
| 29 |
- return nil, err |
|
| 30 |
- } |
|
| 31 |
- endpoints = append(endpoints, APIEndpoint{
|
|
| 32 |
- URL: mirrorURL, |
|
| 33 |
- Mirror: true, |
|
| 34 |
- TLSConfig: mirrorTLSConfig, |
|
| 35 |
- }) |
|
| 36 |
- } |
|
| 37 |
- } |
|
| 38 |
- endpoints = append(endpoints, APIEndpoint{
|
|
| 39 |
- URL: DefaultV2Registry, |
|
| 40 |
- TLSConfig: tlsconfig.ServerDefault(), |
|
| 41 |
- }) |
|
| 42 |
- |
|
| 43 |
- return endpoints, nil |
|
| 44 |
- } |
|
| 45 |
- |
|
| 46 |
- tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname)) |
|
| 47 |
- if err != nil {
|
|
| 48 |
- return nil, err |
|
| 49 |
- } |
|
| 50 |
- |
|
| 51 |
- endpoints = []APIEndpoint{
|
|
| 52 |
- {
|
|
| 53 |
- URL: &url.URL{
|
|
| 54 |
- Scheme: "https", |
|
| 55 |
- Host: hostname, |
|
| 56 |
- }, |
|
| 57 |
- TLSConfig: tlsConfig, |
|
| 58 |
- }, |
|
| 59 |
- } |
|
| 60 |
- |
|
| 61 |
- if tlsConfig.InsecureSkipVerify {
|
|
| 62 |
- endpoints = append(endpoints, APIEndpoint{
|
|
| 63 |
- URL: &url.URL{
|
|
| 64 |
- Scheme: "http", |
|
| 65 |
- Host: hostname, |
|
| 66 |
- }, |
|
| 67 |
- // used to check if supposed to be secure via InsecureSkipVerify |
|
| 68 |
- TLSConfig: tlsConfig, |
|
| 69 |
- }) |
|
| 70 |
- } |
|
| 71 |
- |
|
| 72 |
- return endpoints, nil |
|
| 73 |
-} |
| 74 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,13 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "github.com/distribution/reference" |
|
| 5 |
- "github.com/moby/moby/api/types/registry" |
|
| 6 |
-) |
|
| 7 |
- |
|
| 8 |
-// RepositoryInfo describes a repository |
|
| 9 |
-type RepositoryInfo struct {
|
|
| 10 |
- Name reference.Named |
|
| 11 |
- // Index points to registry information |
|
| 12 |
- Index *registry.IndexInfo |
|
| 13 |
-} |
| ... | ... |
@@ -10,7 +10,7 @@ import ( |
| 10 | 10 |
"time" |
| 11 | 11 |
|
| 12 | 12 |
"github.com/docker/docker/daemon/pkg/plugin" |
| 13 |
- registrypkg "github.com/docker/docker/registry" |
|
| 13 |
+ registrypkg "github.com/docker/docker/daemon/pkg/registry" |
|
| 14 | 14 |
"github.com/moby/go-archive" |
| 15 | 15 |
"github.com/moby/moby/api/types" |
| 16 | 16 |
"github.com/moby/moby/api/types/events" |