diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go index d304e763..81d2b63b 100644 --- a/devcontainer/devcontainer_test.go +++ b/devcontainer/devcontainer_test.go @@ -24,6 +24,8 @@ import ( const workingDir = "/.envbuilder" +var emptyRemoteOpts []remote.Option + func stubLookupEnv(string) (string, bool) { return "", false } @@ -46,7 +48,7 @@ func TestParse(t *testing.T) { func TestCompileWithFeatures(t *testing.T) { t.Parallel() registry := registrytest.New(t) - featureOne := registrytest.WriteContainer(t, registry, "coder/one:tomato", features.TarLayerMediaType, map[string]any{ + featureOne := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/one:tomato", features.TarLayerMediaType, map[string]any{ "install.sh": "hey", "devcontainer-feature.json": features.Spec{ ID: "rust", @@ -58,7 +60,7 @@ func TestCompileWithFeatures(t *testing.T) { }, }, }) - featureTwo := registrytest.WriteContainer(t, registry, "coder/two:potato", features.TarLayerMediaType, map[string]any{ + featureTwo := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/two:potato", features.TarLayerMediaType, map[string]any{ "install.sh": "hey", "devcontainer-feature.json": features.Spec{ ID: "go", diff --git a/devcontainer/features/features.go b/devcontainer/features/features.go index 4775aad3..bb044d5f 100644 --- a/devcontainer/features/features.go +++ b/devcontainer/features/features.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "github.com/GoogleContainerTools/kaniko/pkg/creds" "github.com/go-git/go-billy/v5" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -25,7 +26,7 @@ func extractFromImage(fs billy.Filesystem, directory, reference string) error { if err != nil { return fmt.Errorf("parse feature ref %s: %w", reference, err) } - image, err := remote.Image(ref) + image, err := remote.Image(ref, remote.WithAuthFromKeychain(creds.GetKeychain())) if err != nil { return fmt.Errorf("fetch feature image %s: %w", reference, err) } diff --git a/devcontainer/features/features_test.go b/devcontainer/features/features_test.go index 389193c6..7f2d3bcd 100644 --- a/devcontainer/features/features_test.go +++ b/devcontainer/features/features_test.go @@ -7,15 +7,18 @@ import ( "github.com/coder/envbuilder/devcontainer/features" "github.com/coder/envbuilder/testutil/registrytest" "github.com/go-git/go-billy/v5/memfs" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/stretchr/testify/require" ) +var emptyRemoteOpts []remote.Option + func TestExtract(t *testing.T) { t.Parallel() t.Run("MissingMediaType", func(t *testing.T) { t.Parallel() registry := registrytest.New(t) - ref := registrytest.WriteContainer(t, registry, "coder/test:latest", "some/type", nil) + ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", "some/type", nil) fs := memfs.New() _, err := features.Extract(fs, "", "/", ref) require.ErrorContains(t, err, "no tar layer found") @@ -23,7 +26,7 @@ func TestExtract(t *testing.T) { t.Run("MissingInstallScript", func(t *testing.T) { t.Parallel() registry := registrytest.New(t) - ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{ + ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{ "devcontainer-feature.json": "{}", }) fs := memfs.New() @@ -33,7 +36,7 @@ func TestExtract(t *testing.T) { t.Run("MissingFeatureFile", func(t *testing.T) { t.Parallel() registry := registrytest.New(t) - ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{ + ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{ "install.sh": "hey", }) fs := memfs.New() @@ -43,7 +46,7 @@ func TestExtract(t *testing.T) { t.Run("MissingFeatureProperties", func(t *testing.T) { t.Parallel() registry := registrytest.New(t) - ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{ + ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{ "install.sh": "hey", "devcontainer-feature.json": features.Spec{}, }) @@ -54,7 +57,7 @@ func TestExtract(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Parallel() registry := registrytest.New(t) - ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{ + ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{ "install.sh": "hey", "devcontainer-feature.json": features.Spec{ ID: "go", diff --git a/integration/integration_test.go b/integration/integration_test.go index 913ab567..f221bfb1 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -72,6 +72,8 @@ QFBgc= -----END OPENSSH PRIVATE KEY-----` ) +var emptyRemoteOpts []remote.Option + func TestLogs(t *testing.T) { t.Parallel() @@ -494,7 +496,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { t.Parallel() registry := registrytest.New(t) - feature1Ref := registrytest.WriteContainer(t, registry, "coder/test1:latest", features.TarLayerMediaType, map[string]any{ + feature1Ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test1:latest", features.TarLayerMediaType, map[string]any{ "devcontainer-feature.json": &features.Spec{ ID: "test1", Name: "test1", @@ -508,7 +510,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { "install.sh": "echo $BANANAS > /test1output", }) - feature2Ref := registrytest.WriteContainer(t, registry, "coder/test2:latest", features.TarLayerMediaType, map[string]any{ + feature2Ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test2:latest", features.TarLayerMediaType, map[string]any{ "devcontainer-feature.json": &features.Spec{ ID: "test2", Name: "test2", @@ -574,6 +576,90 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { require.Equal(t, "hello from test 3!", strings.TrimSpace(test3Output)) } +func TestBuildFromDevcontainerWithFeaturesInAuthRepo(t *testing.T) { + t.Parallel() + + // Given: an empty registry with auth enabled + authOpts := setupInMemoryRegistryOpts{ + Username: "testing", + Password: "testing", + } + remoteAuthOpt := append(emptyRemoteOpts, remote.WithAuth(&authn.Basic{Username: authOpts.Username, Password: authOpts.Password})) + testReg := setupInMemoryRegistry(t, authOpts) + regAuthJSON, err := json.Marshal(envbuilder.DockerConfig{ + AuthConfigs: map[string]clitypes.AuthConfig{ + testReg: { + Username: authOpts.Username, + Password: authOpts.Password, + }, + }, + }) + require.NoError(t, err) + + // push a feature to the registry + featureRef := registrytest.WriteContainer(t, testReg, remoteAuthOpt, "features/test-feature:latest", features.TarLayerMediaType, map[string]any{ + "devcontainer-feature.json": &features.Spec{ + ID: "test1", + Name: "test1", + Version: "1.0.0", + Options: map[string]features.Option{ + "bananas": { + Type: "string", + }, + }, + }, + "install.sh": "echo $BANANAS > /test1output", + }) + + // Create a git repo with a devcontainer.json that uses the feature + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ + ".devcontainer/devcontainer.json": `{ + "name": "Test", + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "` + featureRef + `": { + "bananas": "hello from test 1!" + } + } + }`, + ".devcontainer/Dockerfile": "FROM " + testImageUbuntu, + }, + }) + opts := []string{ + envbuilderEnv("GIT_URL", srv.URL), + } + + // Test that things fail when no auth is provided + t.Run("NoAuth", func(t *testing.T) { + t.Parallel() + + // run the envbuilder with the auth config + _, err := runEnvbuilder(t, runOpts{env: opts}) + require.ErrorContains(t, err, "Unauthorized") + }) + + // test that things work when auth is provided + t.Run("WithAuth", func(t *testing.T) { + t.Parallel() + + optsWithAuth := append( + opts, + envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(regAuthJSON)), + ) + + // run the envbuilder with the auth config + ctr, err := runEnvbuilder(t, runOpts{env: optsWithAuth}) + require.NoError(t, err) + + // check that the feature was installed correctly + testOutput := execContainer(t, ctr, "cat /test1output") + require.Equal(t, "hello from test 1!", strings.TrimSpace(testOutput)) + }) +} + func TestBuildFromDockerfileAndConfig(t *testing.T) { t.Parallel() @@ -1545,7 +1631,7 @@ func TestPushImage(t *testing.T) { t.Parallel() // Write a test feature to an in-memory registry. - testFeature := registrytest.WriteContainer(t, registrytest.New(t), "features/test-feature:latest", features.TarLayerMediaType, map[string]any{ + testFeature := registrytest.WriteContainer(t, registrytest.New(t), emptyRemoteOpts, "features/test-feature:latest", features.TarLayerMediaType, map[string]any{ "install.sh": `#!/bin/sh echo "${MESSAGE}" > /root/message.txt`, "devcontainer-feature.json": features.Spec{ diff --git a/testutil/registrytest/registrytest.go b/testutil/registrytest/registrytest.go index 632c1836..8c456ba8 100644 --- a/testutil/registrytest/registrytest.go +++ b/testutil/registrytest/registrytest.go @@ -47,7 +47,7 @@ func New(t testing.TB, mws ...func(http.Handler) http.Handler) string { // WriteContainer uploads a container to the registry server. // It returns the reference to the uploaded container. -func WriteContainer(t *testing.T, serverURL, containerRef, mediaType string, files map[string]any) string { +func WriteContainer(t *testing.T, serverURL string, remoteOpt []remote.Option, containerRef, mediaType string, files map[string]any) string { var buf bytes.Buffer hasher := crypto.SHA256.New() mw := io.MultiWriter(&buf, hasher) @@ -110,7 +110,7 @@ func WriteContainer(t *testing.T, serverURL, containerRef, mediaType string, fil ref, err := name.ParseReference(strings.TrimPrefix(parsedStr, "http://")) require.NoError(t, err) - err = remote.Write(ref, image) + err = remote.Write(ref, image, remoteOpt...) require.NoError(t, err) return ref.String()