@@ -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\n RUN 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\n RUN 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 {
12741410func 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 {
0 commit comments