Skip to content

Commit 2fc88bc

Browse files
committed
feat: add support for features in registries that require authentication
Add support for fetching feature layers from registries that require authentication. The authentication pattern mimics what is done in other places in the codebase. It will search the running environment for registry credentials and use them to authenticate. To setup authentication follow the [same documentation as for pulling](https://github.com/coder/envbuilder/blob/main/docs/container-registry-auth.md) other images from private registries. fixes #457
1 parent 7eabaa4 commit 2fc88bc

File tree

5 files changed

+105
-13
lines changed

5 files changed

+105
-13
lines changed

devcontainer/devcontainer_test.go

+4-2
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

+2-1
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

+8-5
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

+89-3
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

@@ -494,7 +496,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) {
494496
t.Parallel()
495497

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

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

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

@@ -1545,7 +1631,7 @@ func TestPushImage(t *testing.T) {
15451631
t.Parallel()
15461632

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

testutil/registrytest/registrytest.go

+2-2
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)