Skip to content

Commit b7e44ac

Browse files
feat: add support for features in registries that require authentication (#458)
Co-authored-by: Cian Johnston <cian@coder.com>
1 parent 0134c3b commit b7e44ac

5 files changed

Lines changed: 105 additions & 13 deletions

File tree

devcontainer/devcontainer_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424

2525
const workingDir = "/.envbuilder"
2626

27+
var emptyRemoteOpts []remote.Option
28+
2729
func stubLookupEnv(string) (string, bool) {
2830
return "", false
2931
}
@@ -46,7 +48,7 @@ func TestParse(t *testing.T) {
4648
func TestCompileWithFeatures(t *testing.T) {
4749
t.Parallel()
4850
registry := registrytest.New(t)
49-
featureOne := registrytest.WriteContainer(t, registry, "coder/one:tomato", features.TarLayerMediaType, map[string]any{
51+
featureOne := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/one:tomato", features.TarLayerMediaType, map[string]any{
5052
"install.sh": "hey",
5153
"devcontainer-feature.json": features.Spec{
5254
ID: "rust",
@@ -58,7 +60,7 @@ func TestCompileWithFeatures(t *testing.T) {
5860
},
5961
},
6062
})
61-
featureTwo := registrytest.WriteContainer(t, registry, "coder/two:potato", features.TarLayerMediaType, map[string]any{
63+
featureTwo := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/two:potato", features.TarLayerMediaType, map[string]any{
6264
"install.sh": "hey",
6365
"devcontainer-feature.json": features.Spec{
6466
ID: "go",

devcontainer/features/features.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
"github.com/GoogleContainerTools/kaniko/pkg/creds"
1617
"github.com/go-git/go-billy/v5"
1718
"github.com/google/go-containerregistry/pkg/name"
1819
"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -25,7 +26,7 @@ func extractFromImage(fs billy.Filesystem, directory, reference string) error {
2526
if err != nil {
2627
return fmt.Errorf("parse feature ref %s: %w", reference, err)
2728
}
28-
image, err := remote.Image(ref)
29+
image, err := remote.Image(ref, remote.WithAuthFromKeychain(creds.GetKeychain()))
2930
if err != nil {
3031
return fmt.Errorf("fetch feature image %s: %w", reference, err)
3132
}

devcontainer/features/features_test.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,26 @@ import (
77
"github.com/coder/envbuilder/devcontainer/features"
88
"github.com/coder/envbuilder/testutil/registrytest"
99
"github.com/go-git/go-billy/v5/memfs"
10+
"github.com/google/go-containerregistry/pkg/v1/remote"
1011
"github.com/stretchr/testify/require"
1112
)
1213

14+
var emptyRemoteOpts []remote.Option
15+
1316
func TestExtract(t *testing.T) {
1417
t.Parallel()
1518
t.Run("MissingMediaType", func(t *testing.T) {
1619
t.Parallel()
1720
registry := registrytest.New(t)
18-
ref := registrytest.WriteContainer(t, registry, "coder/test:latest", "some/type", nil)
21+
ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", "some/type", nil)
1922
fs := memfs.New()
2023
_, err := features.Extract(fs, "", "/", ref)
2124
require.ErrorContains(t, err, "no tar layer found")
2225
})
2326
t.Run("MissingInstallScript", func(t *testing.T) {
2427
t.Parallel()
2528
registry := registrytest.New(t)
26-
ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{
29+
ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{
2730
"devcontainer-feature.json": "{}",
2831
})
2932
fs := memfs.New()
@@ -33,7 +36,7 @@ func TestExtract(t *testing.T) {
3336
t.Run("MissingFeatureFile", func(t *testing.T) {
3437
t.Parallel()
3538
registry := registrytest.New(t)
36-
ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{
39+
ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{
3740
"install.sh": "hey",
3841
})
3942
fs := memfs.New()
@@ -43,7 +46,7 @@ func TestExtract(t *testing.T) {
4346
t.Run("MissingFeatureProperties", func(t *testing.T) {
4447
t.Parallel()
4548
registry := registrytest.New(t)
46-
ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{
49+
ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{
4750
"install.sh": "hey",
4851
"devcontainer-feature.json": features.Spec{},
4952
})
@@ -54,7 +57,7 @@ func TestExtract(t *testing.T) {
5457
t.Run("Success", func(t *testing.T) {
5558
t.Parallel()
5659
registry := registrytest.New(t)
57-
ref := registrytest.WriteContainer(t, registry, "coder/test:latest", features.TarLayerMediaType, map[string]any{
60+
ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test:latest", features.TarLayerMediaType, map[string]any{
5861
"install.sh": "hey",
5962
"devcontainer-feature.json": features.Spec{
6063
ID: "go",

integration/integration_test.go

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ QFBgc=
7272
-----END OPENSSH PRIVATE KEY-----`
7373
)
7474

75+
var emptyRemoteOpts []remote.Option
76+
7577
func TestLogs(t *testing.T) {
7678
t.Parallel()
7779

@@ -496,7 +498,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) {
496498
t.Parallel()
497499

498500
registry := registrytest.New(t)
499-
feature1Ref := registrytest.WriteContainer(t, registry, "coder/test1:latest", features.TarLayerMediaType, map[string]any{
501+
feature1Ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test1:latest", features.TarLayerMediaType, map[string]any{
500502
"devcontainer-feature.json": &features.Spec{
501503
ID: "test1",
502504
Name: "test1",
@@ -510,7 +512,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) {
510512
"install.sh": "echo $BANANAS > /test1output",
511513
})
512514

513-
feature2Ref := registrytest.WriteContainer(t, registry, "coder/test2:latest", features.TarLayerMediaType, map[string]any{
515+
feature2Ref := registrytest.WriteContainer(t, registry, emptyRemoteOpts, "coder/test2:latest", features.TarLayerMediaType, map[string]any{
514516
"devcontainer-feature.json": &features.Spec{
515517
ID: "test2",
516518
Name: "test2",
@@ -576,6 +578,90 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) {
576578
require.Equal(t, "hello from test 3!", strings.TrimSpace(test3Output))
577579
}
578580

581+
func TestBuildFromDevcontainerWithFeaturesInAuthRepo(t *testing.T) {
582+
t.Parallel()
583+
584+
// Given: an empty registry with auth enabled
585+
authOpts := setupInMemoryRegistryOpts{
586+
Username: "testing",
587+
Password: "testing",
588+
}
589+
remoteAuthOpt := append(emptyRemoteOpts, remote.WithAuth(&authn.Basic{Username: authOpts.Username, Password: authOpts.Password}))
590+
testReg := setupInMemoryRegistry(t, authOpts)
591+
regAuthJSON, err := json.Marshal(envbuilder.DockerConfig{
592+
AuthConfigs: map[string]clitypes.AuthConfig{
593+
testReg: {
594+
Username: authOpts.Username,
595+
Password: authOpts.Password,
596+
},
597+
},
598+
})
599+
require.NoError(t, err)
600+
601+
// push a feature to the registry
602+
featureRef := registrytest.WriteContainer(t, testReg, remoteAuthOpt, "features/test-feature:latest", features.TarLayerMediaType, map[string]any{
603+
"devcontainer-feature.json": &features.Spec{
604+
ID: "test1",
605+
Name: "test1",
606+
Version: "1.0.0",
607+
Options: map[string]features.Option{
608+
"bananas": {
609+
Type: "string",
610+
},
611+
},
612+
},
613+
"install.sh": "echo $BANANAS > /test1output",
614+
})
615+
616+
// Create a git repo with a devcontainer.json that uses the feature
617+
srv := gittest.CreateGitServer(t, gittest.Options{
618+
Files: map[string]string{
619+
".devcontainer/devcontainer.json": `{
620+
"name": "Test",
621+
"build": {
622+
"dockerfile": "Dockerfile"
623+
},
624+
"features": {
625+
"` + featureRef + `": {
626+
"bananas": "hello from test 1!"
627+
}
628+
}
629+
}`,
630+
".devcontainer/Dockerfile": "FROM " + testImageUbuntu,
631+
},
632+
})
633+
opts := []string{
634+
envbuilderEnv("GIT_URL", srv.URL),
635+
}
636+
637+
// Test that things fail when no auth is provided
638+
t.Run("NoAuth", func(t *testing.T) {
639+
t.Parallel()
640+
641+
// run the envbuilder with the auth config
642+
_, err := runEnvbuilder(t, runOpts{env: opts})
643+
require.ErrorContains(t, err, "Unauthorized")
644+
})
645+
646+
// test that things work when auth is provided
647+
t.Run("WithAuth", func(t *testing.T) {
648+
t.Parallel()
649+
650+
optsWithAuth := append(
651+
opts,
652+
envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(regAuthJSON)),
653+
)
654+
655+
// run the envbuilder with the auth config
656+
ctr, err := runEnvbuilder(t, runOpts{env: optsWithAuth})
657+
require.NoError(t, err)
658+
659+
// check that the feature was installed correctly
660+
testOutput := execContainer(t, ctr, "cat /test1output")
661+
require.Equal(t, "hello from test 1!", strings.TrimSpace(testOutput))
662+
})
663+
}
664+
579665
func TestBuildFromDockerfileAndConfig(t *testing.T) {
580666
t.Parallel()
581667

@@ -1547,7 +1633,7 @@ func TestPushImage(t *testing.T) {
15471633
t.Parallel()
15481634

15491635
// Write a test feature to an in-memory registry.
1550-
testFeature := registrytest.WriteContainer(t, registrytest.New(t), "features/test-feature:latest", features.TarLayerMediaType, map[string]any{
1636+
testFeature := registrytest.WriteContainer(t, registrytest.New(t), emptyRemoteOpts, "features/test-feature:latest", features.TarLayerMediaType, map[string]any{
15511637
"install.sh": `#!/bin/sh
15521638
echo "${MESSAGE}" > /root/message.txt`,
15531639
"devcontainer-feature.json": features.Spec{

testutil/registrytest/registrytest.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func New(t testing.TB, mws ...func(http.Handler) http.Handler) string {
4747

4848
// WriteContainer uploads a container to the registry server.
4949
// It returns the reference to the uploaded container.
50-
func WriteContainer(t *testing.T, serverURL, containerRef, mediaType string, files map[string]any) string {
50+
func WriteContainer(t *testing.T, serverURL string, remoteOpt []remote.Option, containerRef, mediaType string, files map[string]any) string {
5151
var buf bytes.Buffer
5252
hasher := crypto.SHA256.New()
5353
mw := io.MultiWriter(&buf, hasher)
@@ -110,7 +110,7 @@ func WriteContainer(t *testing.T, serverURL, containerRef, mediaType string, fil
110110
ref, err := name.ParseReference(strings.TrimPrefix(parsedStr, "http://"))
111111
require.NoError(t, err)
112112

113-
err = remote.Write(ref, image)
113+
err = remote.Write(ref, image, remoteOpt...)
114114
require.NoError(t, err)
115115

116116
return ref.String()

0 commit comments

Comments
 (0)