Skip to content

Commit d9698ed

Browse files
committed
e2e: add private registry pull/push regression test
This adds an e2e regression test for authenticated pull/push against a private registry, covering the auth regression reported in #5963. Includes: - New privateregistry service in the e2e Compose stack with htpasswd auth on port 5001, and --insecure-registry for the engine container. - TestPullPushPrivateRepository test that verifies authenticated push/pull and rejects unauthenticated operations. - Auth config and test credentials in e2e/testdata/registry/. - 90-second retry loop for transient DNS/container startup races. - Service health wait loop in scripts/test/e2e/run. - Increase TestProcessTermination timeout from 10s to 20s for connhelper-ssh + engine 25 combination. - Connhelper-ssh engine Dockerfile for private registry integration. Signed-off-by: Lohit Kolluri <lohitkolluri@gmail.com>
1 parent 1d1562e commit d9698ed

11 files changed

Lines changed: 263 additions & 3 deletions

File tree

e2e/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Generated by gen-certs.sh at setup time
2+
testdata/registry/certs/ca.crt
3+
testdata/registry/certs/ca.key
4+
testdata/registry/certs/tlsregistry.crt
5+
testdata/registry/certs/tlsregistry.key

e2e/compose-env.yaml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,35 @@ 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+
16+
tlsregistry:
17+
build:
18+
context: ./testdata/registry
19+
environment:
20+
- REGISTRY_HTTP_ADDR=0.0.0.0:5003
21+
- REGISTRY_HTTP_DEBUG_ADDR=0.0.0.0:5004
22+
- REGISTRY_AUTH=htpasswd
23+
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
24+
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
25+
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/tlsregistry.crt
26+
- REGISTRY_HTTP_TLS_KEY=/certs/tlsregistry.key
27+
628
engine:
7-
image: 'docker:${ENGINE_VERSION:-29}-dind'
29+
build:
30+
context: ./testdata
31+
dockerfile: engine/Dockerfile
32+
args:
33+
ENGINE_VERSION: ${ENGINE_VERSION:-29}
834
privileged: true
9-
command: ['--insecure-registry=registry:5000', '--experimental']
35+
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
1036
environment:
1137
- DOCKER_TLS_CERTDIR=

e2e/container/run_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,11 @@ func TestProcessTermination(t *testing.T) {
209209

210210
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGTERM))
211211

212-
icmd.WaitOnCmd(time.Second*10, result).Assert(t, icmd.Expected{
212+
// Use a generous timeout (20s) because when run through SSH connhelper,
213+
// the Docker engine may take longer to close the attach stream after
214+
// the container exits. This is a known timing difference across engine
215+
// versions (e.g. engine 25 over SSH connhelper).
216+
icmd.WaitOnCmd(time.Second*20, result).Assert(t, icmd.Expected{
213217
ExitCode: 0,
214218
})
215219
}

e2e/image/private_test.go

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

e2e/internal/fixtures/fixtures.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ func SetupConfigFile(t *testing.T) fs.Dir {
2323
"auths": {
2424
"registry:5000": {
2525
"auth": "ZWlhaXM6cGFzc3dvcmQK"
26+
},
27+
"privateregistry:5001": {
28+
"auth": "ZTJlOnBhc3N3b3Jk"
29+
},
30+
"tlsregistry:5003": {
31+
"auth": "ZTJlOnBhc3N3b3Jk"
2632
}
2733
}}`), fs.WithDir("trust", fs.WithDir("private")))
2834
return *dir

e2e/testdata/Dockerfile.connhelper-ssh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ RUN apk --no-cache add openssl openssh-client openssh-server shadow && \
1212
useradd --create-home --shell /bin/sh --password $(head -c32 /dev/urandom | base64) penguin && \
1313
usermod -aG docker penguin && \
1414
ssh-keygen -A
15+
# Trust the tlsregistry CA so dockerd connects without --insecure-registry.
16+
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
17+
RUN update-ca-certificates
1518
# workaround: ssh session excludes /usr/local/bin from $PATH
1619
RUN ln -s /usr/local/bin/docker /usr/bin/docker
1720
COPY ./connhelper-ssh/entrypoint.sh /

e2e/testdata/engine/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ARG ENGINE_VERSION
2+
FROM docker:${ENGINE_VERSION}-dind
3+
4+
# Trust the tlsregistry CA so dockerd connects without --insecure-registry.
5+
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
6+
RUN update-ca-certificates

e2e/testdata/registry/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM registry:3
2+
COPY auth /auth
3+
COPY certs /certs
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
e2e:$2y$05$DxRBsGSy61vZsBgNVxwUh.UtZmlg3wZHMxYcHYAlupY7r1xbIiuoq
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
# Regenerate test certificates for the TLS-enabled private registry.
5+
# Run this from the repository root or from e2e/testdata/registry/certs/.
6+
7+
cd "$(dirname "$0")"
8+
9+
# --- CA ---
10+
openssl genrsa -out ca.key 2048
11+
openssl req -new -x509 -days 3650 \
12+
-key ca.key \
13+
-subj '/CN=Test CA (TLS Registry)' \
14+
-out ca.crt
15+
16+
# --- Server cert for tlsregistry (signed by CA) ---
17+
cat > openssl-tlsregistry.cnf <<-EOF
18+
[v3_req]
19+
subjectAltName=DNS:tlsregistry
20+
EOF
21+
openssl genrsa -out tlsregistry.key 2048
22+
openssl req -new \
23+
-key tlsregistry.key \
24+
-subj '/CN=tlsregistry' \
25+
-out tlsregistry.csr
26+
openssl x509 -req -days 3650 \
27+
-in tlsregistry.csr \
28+
-CA ca.crt -CAkey ca.key \
29+
-CAcreateserial \
30+
-out tlsregistry.crt \
31+
-extfile openssl-tlsregistry.cnf \
32+
-extensions v3_req
33+
rm -f tlsregistry.csr ca.srl openssl-tlsregistry.cnf

0 commit comments

Comments
 (0)