Skip to content

feat: add support for features in registries that require authentication #458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions devcontainer/devcontainer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (

const workingDir = "/.envbuilder"

var emptyRemoteOpts []remote.Option

func stubLookupEnv(string) (string, bool) {
return "", false
}
Expand All @@ -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",
Expand All @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion devcontainer/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
}
Expand Down
13 changes: 8 additions & 5 deletions devcontainer/features/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@ 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")
})
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()
Expand All @@ -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()
Expand All @@ -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{},
})
Expand All @@ -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",
Expand Down
92 changes: 89 additions & 3 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ QFBgc=
-----END OPENSSH PRIVATE KEY-----`
)

var emptyRemoteOpts []remote.Option

func TestLogs(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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{
Expand Down
4 changes: 2 additions & 2 deletions testutil/registrytest/registrytest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down