Skip to content

Commit fbfbf56

Browse files
authored
chore: integration: add test for pushing to cache repo that requires auth (#233)
1 parent eb01b08 commit fbfbf56

File tree

4 files changed

+178
-38
lines changed

4 files changed

+178
-38
lines changed

git_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/coder/envbuilder"
1616
"github.com/coder/envbuilder/internal/notcodersdk"
1717
"github.com/coder/envbuilder/testutil/gittest"
18+
"github.com/coder/envbuilder/testutil/mwtest"
1819
"github.com/go-git/go-billy/v5"
1920
"github.com/go-git/go-billy/v5/memfs"
2021
"github.com/go-git/go-billy/v5/osfs"
@@ -82,7 +83,7 @@ func TestCloneRepo(t *testing.T) {
8283
t.Run("AlreadyCloned", func(t *testing.T) {
8384
srvFS := memfs.New()
8485
_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "README.md", "Hello, world!", "Wow!"))
85-
authMW := gittest.BasicAuthMW(tc.srvUsername, tc.srvPassword)
86+
authMW := mwtest.BasicAuthMW(tc.srvUsername, tc.srvPassword)
8687
srv := httptest.NewServer(authMW(gittest.NewServer(srvFS)))
8788
clientFS := memfs.New()
8889
// A repo already exists!
@@ -101,7 +102,7 @@ func TestCloneRepo(t *testing.T) {
101102
t.Parallel()
102103
srvFS := memfs.New()
103104
_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "README.md", "Hello, world!", "Wow!"))
104-
authMW := gittest.BasicAuthMW(tc.srvUsername, tc.srvPassword)
105+
authMW := mwtest.BasicAuthMW(tc.srvUsername, tc.srvPassword)
105106
srv := httptest.NewServer(authMW(gittest.NewServer(srvFS)))
106107
clientFS := memfs.New()
107108

@@ -134,7 +135,7 @@ func TestCloneRepo(t *testing.T) {
134135
t.Parallel()
135136
srvFS := memfs.New()
136137
_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "README.md", "Hello, world!", "Wow!"))
137-
authMW := gittest.BasicAuthMW(tc.srvUsername, tc.srvPassword)
138+
authMW := mwtest.BasicAuthMW(tc.srvUsername, tc.srvPassword)
138139
srv := httptest.NewServer(authMW(gittest.NewServer(srvFS)))
139140

140141
authURL, err := url.Parse(srv.URL)

integration/integration_test.go

+156-20
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/coder/envbuilder"
2525
"github.com/coder/envbuilder/devcontainer/features"
2626
"github.com/coder/envbuilder/testutil/gittest"
27+
"github.com/coder/envbuilder/testutil/mwtest"
2728
"github.com/coder/envbuilder/testutil/registrytest"
2829
clitypes "github.com/docker/cli/cli/config/types"
2930
"github.com/docker/docker/api/types"
@@ -776,7 +777,7 @@ func TestPrivateRegistry(t *testing.T) {
776777
t.Parallel()
777778
// Even if something goes wrong with auth,
778779
// the pull will fail as "scratch" is a reserved name.
779-
image := setupPassthroughRegistry(t, "scratch", &registryAuth{
780+
image := setupPassthroughRegistry(t, "scratch", &setupPassthroughRegistryOptions{
780781
Username: "user",
781782
Password: "test",
782783
})
@@ -795,7 +796,7 @@ func TestPrivateRegistry(t *testing.T) {
795796
})
796797
t.Run("Auth", func(t *testing.T) {
797798
t.Parallel()
798-
image := setupPassthroughRegistry(t, "envbuilder-test-alpine:latest", &registryAuth{
799+
image := setupPassthroughRegistry(t, "envbuilder-test-alpine:latest", &setupPassthroughRegistryOptions{
799800
Username: "user",
800801
Password: "test",
801802
})
@@ -827,7 +828,7 @@ func TestPrivateRegistry(t *testing.T) {
827828
t.Parallel()
828829
// Even if something goes wrong with auth,
829830
// the pull will fail as "scratch" is a reserved name.
830-
image := setupPassthroughRegistry(t, "scratch", &registryAuth{
831+
image := setupPassthroughRegistry(t, "scratch", &setupPassthroughRegistryOptions{
831832
Username: "user",
832833
Password: "banana",
833834
})
@@ -857,38 +858,43 @@ func TestPrivateRegistry(t *testing.T) {
857858
})
858859
}
859860

860-
type registryAuth struct {
861+
type setupPassthroughRegistryOptions struct {
861862
Username string
862863
Password string
864+
Upstream string
863865
}
864866

865-
func setupPassthroughRegistry(t *testing.T, image string, auth *registryAuth) string {
867+
func setupPassthroughRegistry(t *testing.T, image string, opts *setupPassthroughRegistryOptions) string {
866868
t.Helper()
867-
dockerURL, err := url.Parse("http://localhost:5000")
869+
if opts.Upstream == "" {
870+
// Default to local test registry
871+
opts.Upstream = "http://localhost:5000"
872+
}
873+
upstreamURL, err := url.Parse(opts.Upstream)
868874
require.NoError(t, err)
869-
proxy := httputil.NewSingleHostReverseProxy(dockerURL)
875+
proxy := httputil.NewSingleHostReverseProxy(upstreamURL)
870876

871877
// The Docker registry uses short-lived JWTs to authenticate
872878
// anonymously to pull images. To test our MITM auth, we need to
873879
// generate a JWT for the proxy to use.
874-
registry, err := name.NewRegistry("localhost:5000")
880+
registry, err := name.NewRegistry(upstreamURL.Host)
875881
require.NoError(t, err)
876882
proxy.Transport, err = transport.NewWithContext(context.Background(), registry, authn.Anonymous, http.DefaultTransport, []string{})
877883
require.NoError(t, err)
878884

879885
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
880-
r.Host = "localhost:5000"
881-
r.URL.Host = "localhost:5000"
882-
r.URL.Scheme = "http"
886+
r.Host = upstreamURL.Host
887+
r.URL.Host = upstreamURL.Host
888+
r.URL.Scheme = upstreamURL.Scheme
883889

884-
if auth != nil {
890+
if opts != nil {
885891
user, pass, ok := r.BasicAuth()
886892
if !ok {
887893
w.Header().Set("WWW-Authenticate", "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"")
888894
w.WriteHeader(http.StatusUnauthorized)
889895
return
890896
}
891-
if user != auth.Username || pass != auth.Password {
897+
if user != opts.Username || pass != opts.Password {
892898
w.WriteHeader(http.StatusUnauthorized)
893899
return
894900
}
@@ -1008,7 +1014,7 @@ func TestPushImage(t *testing.T) {
10081014
})
10091015

10101016
// Given: an empty registry
1011-
testReg := setupInMemoryRegistry(t)
1017+
testReg := setupInMemoryRegistry(t, setupInMemoryRegistryOpts{})
10121018
testRepo := testReg + "/test"
10131019
ref, err := name.ParseReference(testRepo + ":latest")
10141020
require.NoError(t, err)
@@ -1062,7 +1068,7 @@ func TestPushImage(t *testing.T) {
10621068
})
10631069

10641070
// Given: an empty registry
1065-
testReg := setupInMemoryRegistry(t)
1071+
testReg := setupInMemoryRegistry(t, setupInMemoryRegistryOpts{})
10661072
testRepo := testReg + "/test"
10671073
ref, err := name.ParseReference(testRepo + ":latest")
10681074
require.NoError(t, err)
@@ -1101,6 +1107,130 @@ func TestPushImage(t *testing.T) {
11011107
require.NoError(t, err)
11021108
})
11031109

1110+
t.Run("CacheAndPushAuth", func(t *testing.T) {
1111+
t.Parallel()
1112+
1113+
srv := createGitServer(t, gitServerOptions{
1114+
files: map[string]string{
1115+
".devcontainer/Dockerfile": fmt.Sprintf("FROM %s\nRUN date --utc > /root/date.txt", testImageAlpine),
1116+
".devcontainer/devcontainer.json": `{
1117+
"name": "Test",
1118+
"build": {
1119+
"dockerfile": "Dockerfile"
1120+
},
1121+
}`,
1122+
},
1123+
})
1124+
1125+
// Given: an empty registry
1126+
opts := setupInMemoryRegistryOpts{
1127+
Username: "testing",
1128+
Password: "testing",
1129+
}
1130+
remoteAuthOpt := remote.WithAuth(&authn.Basic{Username: opts.Username, Password: opts.Password})
1131+
testReg := setupInMemoryRegistry(t, opts)
1132+
testRepo := testReg + "/test"
1133+
regAuthJSON, err := json.Marshal(envbuilder.DockerConfig{
1134+
AuthConfigs: map[string]clitypes.AuthConfig{
1135+
testRepo: {
1136+
Username: opts.Username,
1137+
Password: opts.Password,
1138+
},
1139+
},
1140+
})
1141+
require.NoError(t, err)
1142+
ref, err := name.ParseReference(testRepo + ":latest")
1143+
require.NoError(t, err)
1144+
_, err = remote.Image(ref, remoteAuthOpt)
1145+
require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
1146+
1147+
// When: we run envbuilder with GET_CACHED_IMAGE
1148+
_, err = runEnvbuilder(t, options{env: []string{
1149+
envbuilderEnv("GIT_URL", srv.URL),
1150+
envbuilderEnv("CACHE_REPO", testRepo),
1151+
envbuilderEnv("GET_CACHED_IMAGE", "1"),
1152+
}})
1153+
require.ErrorContains(t, err, "error probing build cache: uncached command")
1154+
// Then: it should fail to build the image and nothing should be pushed
1155+
_, err = remote.Image(ref, remoteAuthOpt)
1156+
require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
1157+
1158+
// When: we run envbuilder with PUSH_IMAGE set
1159+
_, err = runEnvbuilder(t, options{env: []string{
1160+
envbuilderEnv("GIT_URL", srv.URL),
1161+
envbuilderEnv("CACHE_REPO", testRepo),
1162+
envbuilderEnv("PUSH_IMAGE", "1"),
1163+
envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(regAuthJSON)),
1164+
}})
1165+
require.NoError(t, err)
1166+
1167+
// Then: the image should be pushed
1168+
_, err = remote.Image(ref, remoteAuthOpt)
1169+
require.NoError(t, err, "expected image to be present after build + push")
1170+
1171+
// Then: re-running envbuilder with GET_CACHED_IMAGE should succeed
1172+
_, err = runEnvbuilder(t, options{env: []string{
1173+
envbuilderEnv("GIT_URL", srv.URL),
1174+
envbuilderEnv("CACHE_REPO", testRepo),
1175+
envbuilderEnv("GET_CACHED_IMAGE", "1"),
1176+
envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(regAuthJSON)),
1177+
}})
1178+
require.NoError(t, err)
1179+
})
1180+
1181+
t.Run("CacheAndPushAuthFail", func(t *testing.T) {
1182+
t.Parallel()
1183+
1184+
srv := createGitServer(t, gitServerOptions{
1185+
files: map[string]string{
1186+
".devcontainer/Dockerfile": fmt.Sprintf("FROM %s\nRUN date --utc > /root/date.txt", testImageAlpine),
1187+
".devcontainer/devcontainer.json": `{
1188+
"name": "Test",
1189+
"build": {
1190+
"dockerfile": "Dockerfile"
1191+
},
1192+
}`,
1193+
},
1194+
})
1195+
1196+
// Given: an empty registry
1197+
opts := setupInMemoryRegistryOpts{
1198+
Username: "testing",
1199+
Password: "testing",
1200+
}
1201+
remoteAuthOpt := remote.WithAuth(&authn.Basic{Username: opts.Username, Password: opts.Password})
1202+
testReg := setupInMemoryRegistry(t, opts)
1203+
testRepo := testReg + "/test"
1204+
ref, err := name.ParseReference(testRepo + ":latest")
1205+
require.NoError(t, err)
1206+
_, err = remote.Image(ref, remoteAuthOpt)
1207+
require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
1208+
1209+
// When: we run envbuilder with GET_CACHED_IMAGE
1210+
_, err = runEnvbuilder(t, options{env: []string{
1211+
envbuilderEnv("GIT_URL", srv.URL),
1212+
envbuilderEnv("CACHE_REPO", testRepo),
1213+
envbuilderEnv("GET_CACHED_IMAGE", "1"),
1214+
}})
1215+
require.ErrorContains(t, err, "error probing build cache: uncached command")
1216+
// Then: it should fail to build the image and nothing should be pushed
1217+
_, err = remote.Image(ref, remoteAuthOpt)
1218+
require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
1219+
1220+
// When: we run envbuilder with PUSH_IMAGE set
1221+
_, err = runEnvbuilder(t, options{env: []string{
1222+
envbuilderEnv("GIT_URL", srv.URL),
1223+
envbuilderEnv("CACHE_REPO", testRepo),
1224+
envbuilderEnv("PUSH_IMAGE", "1"),
1225+
}})
1226+
// Then: it should fail with an Unauthorized error
1227+
require.ErrorContains(t, err, "401 Unauthorized", "expected unauthorized error using no auth when cache repo requires it")
1228+
1229+
// Then: the image should not be pushed
1230+
_, err = remote.Image(ref, remoteAuthOpt)
1231+
require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
1232+
})
1233+
11041234
t.Run("CacheAndPushMultistage", func(t *testing.T) {
11051235
// Currently fails with:
11061236
// /home/coder/src/coder/envbuilder/integration/integration_test.go:1417: "error: unable to get cached image: error fake building stage: failed to optimize instructions: failed to get files used from context: failed to get fileinfo for /.envbuilder/0/root/date.txt: lstat /.envbuilder/0/root/date.txt: no such file or directory"
@@ -1122,7 +1252,7 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine),
11221252
})
11231253

11241254
// Given: an empty registry
1125-
testReg := setupInMemoryRegistry(t)
1255+
testReg := setupInMemoryRegistry(t, setupInMemoryRegistryOpts{})
11261256
testRepo := testReg + "/test"
11271257
ref, err := name.ParseReference(testRepo + ":latest")
11281258
require.NoError(t, err)
@@ -1224,11 +1354,17 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine),
12241354
})
12251355
}
12261356

1227-
func setupInMemoryRegistry(t *testing.T) string {
1357+
type setupInMemoryRegistryOpts struct {
1358+
Username string
1359+
Password string
1360+
}
1361+
1362+
func setupInMemoryRegistry(t *testing.T, opts setupInMemoryRegistryOpts) string {
12281363
t.Helper()
12291364
tempDir := t.TempDir()
1230-
testReg := registry.New(registry.WithBlobHandler(registry.NewDiskBlobHandler(tempDir)))
1231-
regSrv := httptest.NewServer(testReg)
1365+
regHandler := registry.New(registry.WithBlobHandler(registry.NewDiskBlobHandler(tempDir)))
1366+
authHandler := mwtest.BasicAuthMW(opts.Username, opts.Password)(regHandler)
1367+
regSrv := httptest.NewServer(authHandler)
12321368
t.Cleanup(func() { regSrv.Close() })
12331369
regSrvURL, err := url.Parse(regSrv.URL)
12341370
require.NoError(t, err)
@@ -1274,7 +1410,7 @@ type gitServerOptions struct {
12741410
func createGitServer(t *testing.T, opts gitServerOptions) *httptest.Server {
12751411
t.Helper()
12761412
if opts.authMW == nil {
1277-
opts.authMW = gittest.BasicAuthMW(opts.username, opts.password)
1413+
opts.authMW = mwtest.BasicAuthMW(opts.username, opts.password)
12781414
}
12791415
commits := make([]gittest.CommitFunc, 0)
12801416
for path, content := range opts.files {

testutil/gittest/gittest.go

-15
Original file line numberDiff line numberDiff line change
@@ -249,18 +249,3 @@ func WriteFile(t *testing.T, fs billy.Filesystem, path, content string) {
249249
err = file.Close()
250250
require.NoError(t, err)
251251
}
252-
253-
func BasicAuthMW(username, password string) func(http.Handler) http.Handler {
254-
return func(next http.Handler) http.Handler {
255-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
256-
if username != "" || password != "" {
257-
authUser, authPass, ok := r.BasicAuth()
258-
if !ok || username != authUser || password != authPass {
259-
w.WriteHeader(http.StatusUnauthorized)
260-
return
261-
}
262-
}
263-
next.ServeHTTP(w, r)
264-
})
265-
}
266-
}

testutil/mwtest/auth_basic.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package mwtest
2+
3+
import "net/http"
4+
5+
func BasicAuthMW(username, password string) func(http.Handler) http.Handler {
6+
return func(next http.Handler) http.Handler {
7+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
8+
if username != "" || password != "" {
9+
authUser, authPass, ok := r.BasicAuth()
10+
if !ok || username != authUser || password != authPass {
11+
w.WriteHeader(http.StatusUnauthorized)
12+
return
13+
}
14+
}
15+
next.ServeHTTP(w, r)
16+
})
17+
}
18+
}

0 commit comments

Comments
 (0)