Skip to content

Commit 236b3d4

Browse files
committed
e2e: add private registry pull/push regression test
Add a privateregistry service (htpasswd auth, port 5001) to the e2e compose stack and a TestPullPushPrivateRepository test that verifies: - unauthenticated push/pull is rejected with an auth error - authenticated push/pull succeeds Fix private-registry flakiness by moving the registry debug listener off port 5001 (to avoid conflicting listeners) and fail fast during e2e setup if supporting services are not running. The volume path in compose-env.yaml is resolved relative to the compose file directory (e2e/), so use ./testdata/registry/auth, not ./e2e/testdata/registry/auth. Regression test for #5963. Closes #5965. Signed-off-by: aryansharma9917 <sharmaaryan9837@gmail.com> Signed-off-by: Lohit Kolluri <lohitkolluri@gmail.com>
1 parent 9f16882 commit 236b3d4

6 files changed

Lines changed: 162 additions & 1 deletion

File tree

e2e/compose-env.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@ services:
33
registry:
44
image: 'registry:3'
55

6+
privateregistry:
7+
build:
8+
context: ./testdata/registry
9+
environment:
10+
- REGISTRY_HTTP_ADDR=0.0.0.0:5001
11+
- REGISTRY_HTTP_DEBUG_ADDR=0.0.0.0:5002
12+
- REGISTRY_AUTH=htpasswd
13+
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
14+
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
15+
616
engine:
717
image: 'docker:${ENGINE_VERSION:-29}-dind'
818
privileged: true
9-
command: ['--insecure-registry=registry:5000', '--experimental']
19+
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
1020
environment:
1121
- DOCKER_TLS_CERTDIR=

e2e/image/private_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package image
2+
3+
import (
4+
"strings"
5+
"testing"
6+
"time"
7+
8+
"github.com/docker/cli/e2e/internal/fixtures"
9+
"gotest.tools/v3/assert"
10+
"gotest.tools/v3/icmd"
11+
)
12+
13+
const privateRegistryPrefix = "privateregistry:5001"
14+
15+
// Regression test for https://github.com/docker/cli/issues/5963
16+
func TestPullPushPrivateRepository(t *testing.T) {
17+
t.Parallel()
18+
19+
dir := fixtures.SetupConfigFile(t)
20+
t.Cleanup(dir.Remove)
21+
emptyConfigDir := t.TempDir()
22+
23+
sourceImage := fixtures.AlpineImage
24+
privateImage := privateRegistryPrefix + "/private/alpine:test-private-pull-push"
25+
26+
runWithPrivateRegistryRetry(t,
27+
icmd.Command("docker", "pull", sourceImage),
28+
).Assert(t, icmd.Success)
29+
t.Cleanup(func() {
30+
icmd.RunCommand("docker", "image", "rm", "-f", privateImage).Assert(t, icmd.Success)
31+
})
32+
33+
icmd.RunCommand("docker", "tag", sourceImage, privateImage).Assert(t, icmd.Success)
34+
35+
pushNoAuth := runWithPrivateRegistryRetry(t,
36+
icmd.Command("docker", "push", privateImage),
37+
fixtures.WithConfig(emptyConfigDir),
38+
)
39+
pushNoAuth.Assert(t, icmd.Expected{ExitCode: 1})
40+
assertAuthDenied(t, pushNoAuth)
41+
42+
pushWithAuth := runWithPrivateRegistryRetry(t,
43+
icmd.Command("docker", "push", privateImage),
44+
fixtures.WithConfig(dir.Path()),
45+
)
46+
pushWithAuth.Assert(t, icmd.Success)
47+
// Docker omits the tag in the "push refers to repository" line; strip it before asserting.
48+
privateRepo := privateImage[:strings.LastIndex(privateImage, ":")]
49+
assert.Check(t, strings.Contains(pushWithAuth.Combined(), "The push refers to repository ["+privateRepo+"]"), pushWithAuth.Combined())
50+
51+
icmd.RunCommand("docker", "image", "rm", "-f", privateImage).Assert(t, icmd.Success)
52+
53+
pullNoAuth := runWithPrivateRegistryRetry(t,
54+
icmd.Command("docker", "pull", privateImage),
55+
fixtures.WithConfig(emptyConfigDir),
56+
)
57+
pullNoAuth.Assert(t, icmd.Expected{ExitCode: 1})
58+
assertAuthDenied(t, pullNoAuth)
59+
60+
pullWithAuth := runWithPrivateRegistryRetry(t,
61+
icmd.Command("docker", "pull", privateImage),
62+
fixtures.WithConfig(dir.Path()),
63+
)
64+
pullWithAuth.Assert(t, icmd.Success)
65+
assert.Check(t, strings.Contains(pullWithAuth.Combined(), privateImage), pullWithAuth.Combined())
66+
}
67+
68+
func assertAuthDenied(t *testing.T, result *icmd.Result) {
69+
t.Helper()
70+
output := result.Combined()
71+
if isPrivateRegistryTransient(output) {
72+
t.Fatalf("private registry unavailable while expecting auth failure: %s", output)
73+
}
74+
75+
assert.Assert(t,
76+
strings.Contains(output, "requested access to the resource is denied") ||
77+
strings.Contains(output, "no basic auth credentials") ||
78+
strings.Contains(output, "unauthorized") ||
79+
strings.Contains(output, "authentication required"),
80+
output,
81+
)
82+
}
83+
84+
func runWithPrivateRegistryRetry(t *testing.T, cmd icmd.Cmd, opts ...icmd.CmdOp) *icmd.Result {
85+
t.Helper()
86+
87+
deadline := time.Now().Add(90 * time.Second)
88+
for {
89+
result := icmd.RunCmd(cmd, opts...)
90+
output := result.Combined()
91+
if isPrivateRegistryTransient(output) {
92+
if time.Now().Before(deadline) {
93+
t.Logf("waiting for private registry availability: %s", output)
94+
time.Sleep(500 * time.Millisecond)
95+
continue
96+
}
97+
}
98+
return result
99+
}
100+
}
101+
102+
func isPrivateRegistryTransient(output string) bool {
103+
return strings.Contains(output, "lookup privateregistry") ||
104+
strings.Contains(output, "lookup registry") ||
105+
strings.Contains(output, "no such host") ||
106+
strings.Contains(output, "server misbehaving") ||
107+
strings.Contains(output, "Temporary failure in name resolution") ||
108+
strings.Contains(output, "connection refused") ||
109+
strings.Contains(output, "i/o timeout") ||
110+
strings.Contains(output, "TLS handshake timeout") ||
111+
strings.Contains(output, "context deadline exceeded") ||
112+
strings.Contains(output, "connection reset by peer") ||
113+
strings.Contains(output, "unexpected EOF")
114+
}

e2e/internal/fixtures/fixtures.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ func SetupConfigFile(t *testing.T) fs.Dir {
2323
"auths": {
2424
"registry:5000": {
2525
"auth": "ZWlhaXM6cGFzc3dvcmQK"
26+
},
27+
"privateregistry:5001": {
28+
"auth": "ZTJlOnBhc3N3b3Jk"
2629
}
2730
}}`), fs.WithDir("trust", fs.WithDir("private")))
2831
return *dir

e2e/testdata/registry/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM registry:3
2+
COPY auth /auth
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
e2e:$2y$05$DxRBsGSy61vZsBgNVxwUh.UtZmlg3wZHMxYcHYAlupY7r1xbIiuoq

scripts/test/e2e/run

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,37 @@ setup() {
2828
fi
2929
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose up --build -d >&2
3030

31+
# Ensure supporting services exist before running tests. If one fails to start,
32+
# fail fast and surface logs instead of waiting on downstream DNS timeouts.
33+
local deadline=$((SECONDS + 120))
34+
while [ $SECONDS -lt $deadline ]; do
35+
local ok=1
36+
for svc in registry privateregistry engine; do
37+
cid="$(COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose ps -q "$svc" 2>/dev/null || true)"
38+
if [ -z "$cid" ]; then
39+
ok=0
40+
break
41+
fi
42+
if ! docker inspect -f '{{.State.Running}}' "$cid" 2>/dev/null | grep -q true; then
43+
ok=0
44+
break
45+
fi
46+
done
47+
if [ "$ok" -eq 1 ]; then
48+
break
49+
fi
50+
sleep 1
51+
done
52+
if [ $SECONDS -ge $deadline ]; then
53+
echo "Timed out waiting for e2e services to start" >&2
54+
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose ps >&2 || true
55+
for svc in registry privateregistry engine; do
56+
echo "--- logs: $svc ---" >&2
57+
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose logs --no-color --tail=200 "$svc" >&2 || true
58+
done
59+
exit 1
60+
fi
61+
3162
local network="${project}_default"
3263
# TODO: only run if inside a container
3364
docker network connect "$network" "$(hostname)"

0 commit comments

Comments
 (0)