From 8ca9f75c127315af1a478f0e672573bf5efdbdba Mon Sep 17 00:00:00 2001 From: aryansharma9917 Date: Sat, 18 Apr 2026 23:51:53 +0530 Subject: [PATCH 1/4] e2e/image: add private registry auth regression test Add private registry test infrastructure and a pull/push auth regression e2e test. Signed-off-by: aryansharma9917 --- e2e/compose-env.yaml | 12 ++++- e2e/image/private_test.go | 74 +++++++++++++++++++++++++++++ e2e/internal/fixtures/fixtures.go | 3 ++ e2e/testdata/registry/auth/htpasswd | 1 + 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 e2e/image/private_test.go create mode 100644 e2e/testdata/registry/auth/htpasswd diff --git a/e2e/compose-env.yaml b/e2e/compose-env.yaml index 651d5d145aee..78f7a8d42c97 100644 --- a/e2e/compose-env.yaml +++ b/e2e/compose-env.yaml @@ -3,9 +3,19 @@ services: registry: image: 'registry:3' + private-registry: + image: 'registry:3' + 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=private-registry: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..d43d170fac2d --- /dev/null +++ b/e2e/image/private_test.go @@ -0,0 +1,74 @@ +package image + +import ( + "strings" + "testing" + + "github.com/docker/cli/e2e/internal/fixtures" + "gotest.tools/v3/assert" + "gotest.tools/v3/icmd" +) + +const privateRegistryPrefix = "private-registry: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" + + icmd.RunCommand("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 := icmd.RunCmd( + icmd.Command("docker", "push", privateImage), + fixtures.WithConfig(emptyConfigDir), + ) + pushNoAuth.Assert(t, icmd.Expected{ExitCode: 1}) + assertAuthDenied(t, pushNoAuth) + + pushWithAuth := icmd.RunCmd( + 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 := icmd.RunCmd( + icmd.Command("docker", "pull", privateImage), + fixtures.WithConfig(emptyConfigDir), + ) + pullNoAuth.Assert(t, icmd.Expected{ExitCode: 1}) + assertAuthDenied(t, pullNoAuth) + + pullWithAuth := icmd.RunCmd( + 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() + + 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, + ) +} diff --git a/e2e/internal/fixtures/fixtures.go b/e2e/internal/fixtures/fixtures.go index 256e14f17612..2978e2dc6684 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" + }, + "private-registry: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 From 5b5739dbab8a0f64da3bace8ce724abaa507d881 Mon Sep 17 00:00:00 2001 From: aryansharma9917 Date: Tue, 21 Apr 2026 12:03:42 +0530 Subject: [PATCH 2/4] e2e/image: retry private registry ops on transient DNS errors Signed-off-by: aryansharma9917 --- e2e/image/private_test.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/e2e/image/private_test.go b/e2e/image/private_test.go index d43d170fac2d..c4c3df84afe9 100644 --- a/e2e/image/private_test.go +++ b/e2e/image/private_test.go @@ -3,6 +3,7 @@ package image import ( "strings" "testing" + "time" "github.com/docker/cli/e2e/internal/fixtures" "gotest.tools/v3/assert" @@ -29,14 +30,14 @@ func TestPullPushPrivateRepository(t *testing.T) { icmd.RunCommand("docker", "tag", sourceImage, privateImage).Assert(t, icmd.Success) - pushNoAuth := icmd.RunCmd( + pushNoAuth := runWithPrivateRegistryRetry(t, icmd.Command("docker", "push", privateImage), fixtures.WithConfig(emptyConfigDir), ) pushNoAuth.Assert(t, icmd.Expected{ExitCode: 1}) assertAuthDenied(t, pushNoAuth) - pushWithAuth := icmd.RunCmd( + pushWithAuth := runWithPrivateRegistryRetry(t, icmd.Command("docker", "push", privateImage), fixtures.WithConfig(dir.Path()), ) @@ -45,14 +46,14 @@ func TestPullPushPrivateRepository(t *testing.T) { icmd.RunCommand("docker", "image", "rm", "-f", privateImage).Assert(t, icmd.Success) - pullNoAuth := icmd.RunCmd( + pullNoAuth := runWithPrivateRegistryRetry(t, icmd.Command("docker", "pull", privateImage), fixtures.WithConfig(emptyConfigDir), ) pullNoAuth.Assert(t, icmd.Expected{ExitCode: 1}) assertAuthDenied(t, pullNoAuth) - pullWithAuth := icmd.RunCmd( + pullWithAuth := runWithPrivateRegistryRetry(t, icmd.Command("docker", "pull", privateImage), fixtures.WithConfig(dir.Path()), ) @@ -72,3 +73,23 @@ func assertAuthDenied(t *testing.T, result *icmd.Result) { output, ) } + +func runWithPrivateRegistryRetry(t *testing.T, cmd *icmd.Cmd, opts ...func(*icmd.Cmd)) *icmd.Result { + t.Helper() + + deadline := time.Now().Add(30 * time.Second) + for { + result := icmd.RunCmd(cmd, opts...) + output := result.Combined() + if strings.Contains(output, "lookup private-registry") || + strings.Contains(output, "no such host") || + strings.Contains(output, "server misbehaving") { + if time.Now().Before(deadline) { + t.Logf("waiting for private registry DNS to become available: %s", output) + time.Sleep(500 * time.Millisecond) + continue + } + } + return result + } +} From 97e06a776eebca9aad45332ae8e44cac2c866b84 Mon Sep 17 00:00:00 2001 From: aryansharma9917 Date: Thu, 23 Apr 2026 00:26:52 +0530 Subject: [PATCH 3/4] e2e/image: fix private registry retry helper types and bootstrap pull Signed-off-by: aryansharma9917 --- e2e/image/private_test.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/e2e/image/private_test.go b/e2e/image/private_test.go index c4c3df84afe9..26ef553a9bb6 100644 --- a/e2e/image/private_test.go +++ b/e2e/image/private_test.go @@ -23,7 +23,9 @@ func TestPullPushPrivateRepository(t *testing.T) { sourceImage := fixtures.AlpineImage privateImage := privateRegistryPrefix + "/private/alpine:test-private-pull-push" - icmd.RunCommand("docker", "pull", sourceImage).Assert(t, icmd.Success) + 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) }) @@ -74,18 +76,20 @@ func assertAuthDenied(t *testing.T, result *icmd.Result) { ) } -func runWithPrivateRegistryRetry(t *testing.T, cmd *icmd.Cmd, opts ...func(*icmd.Cmd)) *icmd.Result { +func runWithPrivateRegistryRetry(t *testing.T, cmd icmd.Cmd, opts ...icmd.CmdOp) *icmd.Result { t.Helper() - deadline := time.Now().Add(30 * time.Second) + deadline := time.Now().Add(60 * time.Second) for { result := icmd.RunCmd(cmd, opts...) output := result.Combined() if strings.Contains(output, "lookup private-registry") || + strings.Contains(output, "lookup registry") || strings.Contains(output, "no such host") || - strings.Contains(output, "server misbehaving") { + strings.Contains(output, "server misbehaving") || + strings.Contains(output, "Temporary failure in name resolution") { if time.Now().Before(deadline) { - t.Logf("waiting for private registry DNS to become available: %s", output) + t.Logf("waiting for registry DNS to become available: %s", output) time.Sleep(500 * time.Millisecond) continue } From b6a12adcf6349b78788e4cb5dd1f2a2982705ced Mon Sep 17 00:00:00 2001 From: aryansharma9917 Date: Sat, 25 Apr 2026 21:26:27 +0530 Subject: [PATCH 4/4] e2e: harden private registry test Signed-off-by: aryansharma9917 --- e2e/compose-env.yaml | 6 +++++- e2e/image/private_test.go | 29 +++++++++++++++++++++-------- e2e/internal/fixtures/fixtures.go | 2 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/e2e/compose-env.yaml b/e2e/compose-env.yaml index 78f7a8d42c97..812ec26f5d96 100644 --- a/e2e/compose-env.yaml +++ b/e2e/compose-env.yaml @@ -5,6 +5,10 @@ services: private-registry: image: 'registry:3' + networks: + default: + aliases: + - privateregistry environment: - REGISTRY_HTTP_ADDR=0.0.0.0:5001 - REGISTRY_AUTH=htpasswd @@ -16,6 +20,6 @@ services: engine: image: 'docker:${ENGINE_VERSION:-29}-dind' privileged: true - command: ['--insecure-registry=registry:5000', '--insecure-registry=private-registry:5001', '--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 index 26ef553a9bb6..ba1de7fd9a10 100644 --- a/e2e/image/private_test.go +++ b/e2e/image/private_test.go @@ -10,7 +10,7 @@ import ( "gotest.tools/v3/icmd" ) -const privateRegistryPrefix = "private-registry:5001" +const privateRegistryPrefix = "privateregistry:5001" // Regression test for https://github.com/docker/cli/issues/5963 func TestPullPushPrivateRepository(t *testing.T) { @@ -66,6 +66,9 @@ func TestPullPushPrivateRepository(t *testing.T) { 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") || @@ -79,17 +82,13 @@ func assertAuthDenied(t *testing.T, result *icmd.Result) { func runWithPrivateRegistryRetry(t *testing.T, cmd icmd.Cmd, opts ...icmd.CmdOp) *icmd.Result { t.Helper() - deadline := time.Now().Add(60 * time.Second) + deadline := time.Now().Add(90 * time.Second) for { result := icmd.RunCmd(cmd, opts...) output := result.Combined() - if strings.Contains(output, "lookup private-registry") || - strings.Contains(output, "lookup registry") || - strings.Contains(output, "no such host") || - strings.Contains(output, "server misbehaving") || - strings.Contains(output, "Temporary failure in name resolution") { + if isPrivateRegistryTransient(output) { if time.Now().Before(deadline) { - t.Logf("waiting for registry DNS to become available: %s", output) + t.Logf("waiting for private registry availability: %s", output) time.Sleep(500 * time.Millisecond) continue } @@ -97,3 +96,17 @@ func runWithPrivateRegistryRetry(t *testing.T, cmd icmd.Cmd, opts ...icmd.CmdOp) 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 2978e2dc6684..238942d1b7e0 100644 --- a/e2e/internal/fixtures/fixtures.go +++ b/e2e/internal/fixtures/fixtures.go @@ -24,7 +24,7 @@ func SetupConfigFile(t *testing.T) fs.Dir { "registry:5000": { "auth": "ZWlhaXM6cGFzc3dvcmQK" }, - "private-registry:5001": { + "privateregistry:5001": { "auth": "ZTJlOnBhc3N3b3Jk" } }}`), fs.WithDir("trust", fs.WithDir("private")))