diff --git a/e2e/compose-env.yaml b/e2e/compose-env.yaml index 651d5d145aee..812ec26f5d96 100644 --- a/e2e/compose-env.yaml +++ b/e2e/compose-env.yaml @@ -3,9 +3,23 @@ services: registry: image: 'registry:3' + private-registry: + image: 'registry:3' + networks: + default: + aliases: + - privateregistry + environment: + - REGISTRY_HTTP_ADDR=0.0.0.0:5001 + - REGISTRY_AUTH=htpasswd + - REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm + - REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd + volumes: + - ./e2e/testdata/registry/auth:/auth:ro + engine: image: 'docker:${ENGINE_VERSION:-29}-dind' privileged: true - command: ['--insecure-registry=registry:5000', '--experimental'] + command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental'] environment: - DOCKER_TLS_CERTDIR= diff --git a/e2e/image/private_test.go b/e2e/image/private_test.go new file mode 100644 index 000000000000..ba1de7fd9a10 --- /dev/null +++ b/e2e/image/private_test.go @@ -0,0 +1,112 @@ +package image + +import ( + "strings" + "testing" + "time" + + "github.com/docker/cli/e2e/internal/fixtures" + "gotest.tools/v3/assert" + "gotest.tools/v3/icmd" +) + +const privateRegistryPrefix = "privateregistry:5001" + +// Regression test for https://github.com/docker/cli/issues/5963 +func TestPullPushPrivateRepository(t *testing.T) { + t.Parallel() + + dir := fixtures.SetupConfigFile(t) + t.Cleanup(dir.Remove) + emptyConfigDir := t.TempDir() + + sourceImage := fixtures.AlpineImage + privateImage := privateRegistryPrefix + "/private/alpine:test-private-pull-push" + + runWithPrivateRegistryRetry(t, + icmd.Command("docker", "pull", sourceImage), + ).Assert(t, icmd.Success) + t.Cleanup(func() { + icmd.RunCommand("docker", "image", "rm", "-f", privateImage).Assert(t, icmd.Success) + }) + + icmd.RunCommand("docker", "tag", sourceImage, privateImage).Assert(t, icmd.Success) + + pushNoAuth := runWithPrivateRegistryRetry(t, + icmd.Command("docker", "push", privateImage), + fixtures.WithConfig(emptyConfigDir), + ) + pushNoAuth.Assert(t, icmd.Expected{ExitCode: 1}) + assertAuthDenied(t, pushNoAuth) + + pushWithAuth := runWithPrivateRegistryRetry(t, + icmd.Command("docker", "push", privateImage), + fixtures.WithConfig(dir.Path()), + ) + pushWithAuth.Assert(t, icmd.Success) + assert.Check(t, strings.Contains(pushWithAuth.Combined(), "The push refers to repository ["+privateImage+"]"), pushWithAuth.Combined()) + + icmd.RunCommand("docker", "image", "rm", "-f", privateImage).Assert(t, icmd.Success) + + pullNoAuth := runWithPrivateRegistryRetry(t, + icmd.Command("docker", "pull", privateImage), + fixtures.WithConfig(emptyConfigDir), + ) + pullNoAuth.Assert(t, icmd.Expected{ExitCode: 1}) + assertAuthDenied(t, pullNoAuth) + + pullWithAuth := runWithPrivateRegistryRetry(t, + icmd.Command("docker", "pull", privateImage), + fixtures.WithConfig(dir.Path()), + ) + pullWithAuth.Assert(t, icmd.Success) + assert.Check(t, strings.Contains(pullWithAuth.Combined(), privateImage), pullWithAuth.Combined()) +} + +func assertAuthDenied(t *testing.T, result *icmd.Result) { + t.Helper() + output := result.Combined() + if isPrivateRegistryTransient(output) { + t.Fatalf("private registry unavailable while expecting auth failure: %s", output) + } + + assert.Check(t, + strings.Contains(output, "requested access to the resource is denied") || + strings.Contains(output, "no basic auth credentials") || + strings.Contains(output, "unauthorized") || + strings.Contains(output, "authentication required"), + output, + ) +} + +func runWithPrivateRegistryRetry(t *testing.T, cmd icmd.Cmd, opts ...icmd.CmdOp) *icmd.Result { + t.Helper() + + deadline := time.Now().Add(90 * time.Second) + for { + result := icmd.RunCmd(cmd, opts...) + output := result.Combined() + if isPrivateRegistryTransient(output) { + if time.Now().Before(deadline) { + t.Logf("waiting for private registry availability: %s", output) + time.Sleep(500 * time.Millisecond) + continue + } + } + return result + } +} + +func isPrivateRegistryTransient(output string) bool { + return strings.Contains(output, "lookup privateregistry") || + strings.Contains(output, "lookup registry") || + strings.Contains(output, "no such host") || + strings.Contains(output, "server misbehaving") || + strings.Contains(output, "Temporary failure in name resolution") || + strings.Contains(output, "connection refused") || + strings.Contains(output, "i/o timeout") || + strings.Contains(output, "TLS handshake timeout") || + strings.Contains(output, "context deadline exceeded") || + strings.Contains(output, "connection reset by peer") || + strings.Contains(output, "unexpected EOF") +} diff --git a/e2e/internal/fixtures/fixtures.go b/e2e/internal/fixtures/fixtures.go index 256e14f17612..238942d1b7e0 100644 --- a/e2e/internal/fixtures/fixtures.go +++ b/e2e/internal/fixtures/fixtures.go @@ -23,6 +23,9 @@ func SetupConfigFile(t *testing.T) fs.Dir { "auths": { "registry:5000": { "auth": "ZWlhaXM6cGFzc3dvcmQK" + }, + "privateregistry:5001": { + "auth": "ZTJlOnBhc3N3b3Jk" } }}`), fs.WithDir("trust", fs.WithDir("private"))) return *dir diff --git a/e2e/testdata/registry/auth/htpasswd b/e2e/testdata/registry/auth/htpasswd new file mode 100644 index 000000000000..1715b8934ce7 --- /dev/null +++ b/e2e/testdata/registry/auth/htpasswd @@ -0,0 +1 @@ +e2e:$2y$05$UozlY7.SA2NMcojF.qocv.W9Q4rsr75uLMW.mVEsAPx90BVeMgveC