Skip to content

Commit cbd9d7c

Browse files
fix(registry): update WithHtpasswd to use os.CreateTemp instead of os.Create with filepath.Join. (#3272)
* fix: update `WithHtpasswd` to use `os.CreateTemp` instead of `os.Create` with `filepath.Join`. when starting many distribution registries all with different `WithHtpasswd` they currently accidentally share all the same file mount that is overwritten because we always mount to a static `htpasswd`. This makes it so every call to the credentials actually causes the creation of a truly random file mount so the files are unique per GenericContainerRequest * test: add coverage for Htpasswd with unique per-container credentials * chore: update `golang.org/x/crypto` to direct dependency in `go.mod`
1 parent a23f129 commit cbd9d7c

File tree

3 files changed

+64
-3
lines changed

3 files changed

+64
-3
lines changed

modules/registry/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/docker/docker v28.3.3+incompatible
1010
github.com/stretchr/testify v1.10.0
1111
github.com/testcontainers/testcontainers-go v0.38.0
12+
golang.org/x/crypto v0.37.0
1213
)
1314

1415
require (
@@ -59,7 +60,6 @@ require (
5960
go.opentelemetry.io/otel/metric v1.35.0 // indirect
6061
go.opentelemetry.io/otel/trace v1.35.0 // indirect
6162
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
62-
golang.org/x/crypto v0.37.0 // indirect
6363
golang.org/x/net v0.38.0 // indirect
6464
golang.org/x/sys v0.32.0 // indirect
6565
gopkg.in/yaml.v3 v3.0.1 // indirect

modules/registry/options.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package registry
33
import (
44
"fmt"
55
"os"
6-
"path/filepath"
76

87
"github.com/testcontainers/testcontainers-go"
98
)
@@ -37,7 +36,7 @@ func WithData(dataPath string) testcontainers.CustomizeRequestOption {
3736
// the htpasswd file, thanks to the REGISTRY_AUTH_HTPASSWD_PATH environment variable.
3837
func WithHtpasswd(credentials string) testcontainers.CustomizeRequestOption {
3938
return func(req *testcontainers.GenericContainerRequest) error {
40-
tmpFile, err := os.Create(filepath.Join(os.TempDir(), "htpasswd"))
39+
tmpFile, err := os.CreateTemp("", "htpasswd")
4140
if err != nil {
4241
tmpFile, err = os.Create(".")
4342
if err != nil {

modules/registry/registry_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/cpuguy83/dockercfg"
1111
"github.com/stretchr/testify/require"
12+
"golang.org/x/crypto/bcrypt"
1213

1314
"github.com/testcontainers/testcontainers-go"
1415
"github.com/testcontainers/testcontainers-go/modules/registry"
@@ -171,6 +172,67 @@ func TestRunContainer_authenticated_withCredentials(t *testing.T) {
171172
require.Equal(t, http.StatusOK, resp.StatusCode)
172173
}
173174

175+
func TestRunContainer_authenticated_htpasswd_atomic_per_container(t *testing.T) {
176+
t.Parallel()
177+
ctx := context.Background()
178+
r := require.New(t)
179+
180+
type container struct {
181+
pass string
182+
registry *registry.RegistryContainer
183+
addr string
184+
}
185+
186+
newContainer := func(password string) container {
187+
hash, err := bcrypt.GenerateFromPassword([]byte(password), 5)
188+
r.NoError(err)
189+
190+
reg, err := registry.Run(
191+
ctx,
192+
registry.DefaultImage,
193+
registry.WithHtpasswd("testuser:"+string(hash)),
194+
)
195+
r.NoError(err)
196+
testcontainers.CleanupContainer(t, reg)
197+
198+
addr, err := reg.Address(ctx)
199+
r.NoError(err)
200+
201+
return container{pass: password, registry: reg, addr: addr}
202+
}
203+
204+
// Create two independent registries with different credentials.
205+
regA := newContainer("passA")
206+
regB := newContainer("passB")
207+
208+
client := http.Client{}
209+
210+
// 1. Wrong password against A must fail.
211+
req, err := http.NewRequest(http.MethodGet, regA.addr+"/v2/_catalog", nil)
212+
r.NoError(err)
213+
req.SetBasicAuth("testuser", regB.pass)
214+
resp, err := client.Do(req)
215+
r.NoError(err)
216+
r.Equal(http.StatusUnauthorized, resp.StatusCode)
217+
_ = resp.Body.Close()
218+
219+
// 2. Correct password against A must succeed.
220+
req.SetBasicAuth("testuser", regA.pass)
221+
resp, err = client.Do(req)
222+
r.NoError(err)
223+
r.Equal(http.StatusOK, resp.StatusCode)
224+
_ = resp.Body.Close()
225+
226+
// 3. Correct password against B must succeed.
227+
req, err = http.NewRequest(http.MethodGet, regB.addr+"/v2/_catalog", nil)
228+
r.NoError(err)
229+
req.SetBasicAuth("testuser", regB.pass)
230+
resp, err = client.Do(req)
231+
r.NoError(err)
232+
r.Equal(http.StatusOK, resp.StatusCode)
233+
_ = resp.Body.Close()
234+
}
235+
174236
func TestRunContainer_wrongData(t *testing.T) {
175237
ctx := context.Background()
176238
registryContainer, err := registry.Run(

0 commit comments

Comments
 (0)