Skip to content

Commit 85f220a

Browse files
authored
fix: allow features to be a mapping of versions (#44)
1 parent d649732 commit 85f220a

File tree

3 files changed

+106
-7
lines changed

3 files changed

+106
-7
lines changed

devcontainer/devcontainer.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type Spec struct {
3434
RemoteUser string `json:"remoteUser"`
3535
RemoteEnv map[string]string `json:"remoteEnv"`
3636
// Features is a map of feature names to feature configurations.
37-
Features map[string]map[string]any `json:"features"`
37+
Features map[string]any `json:"features"`
3838

3939
// Deprecated but still frequently used...
4040
Dockerfile string `json:"dockerFile"`
@@ -186,15 +186,34 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, scratchDir, remoteUser, dock
186186
// is deterministic which allows for caching.
187187
sort.Strings(featureOrder)
188188

189-
for _, featureRef := range featureOrder {
190-
featureOpts := s.Features[featureRef]
189+
for _, featureRefRaw := range featureOrder {
190+
featureRefParsed, err := name.NewTag(featureRefRaw)
191+
if err != nil {
192+
return "", fmt.Errorf("parse feature ref %s: %w", featureRefRaw, err)
193+
}
194+
featureImage := featureRefParsed.Repository.Name()
195+
featureTag := featureRefParsed.TagStr()
196+
197+
featureOpts := map[string]any{}
198+
switch t := s.Features[featureRefRaw].(type) {
199+
case string:
200+
featureTag = t
201+
case map[string]any:
202+
featureOpts = t
203+
}
204+
205+
featureRef := featureImage
206+
if featureTag != "" {
207+
featureRef += ":" + featureTag
208+
}
209+
191210
// It's important for caching that this directory is static.
192211
// If it changes on each run then the container will not be cached.
193212
//
194213
// devcontainers/cli has a very complex method of computing the feature
195214
// name from the feature reference. We're just going to hash it for simplicity.
196215
featureSha := md5.Sum([]byte(featureRef))
197-
featureName := strings.Split(filepath.Base(featureRef), ":")[0]
216+
featureName := filepath.Base(featureImage)
198217
featureDir := filepath.Join(featuresDir, fmt.Sprintf("%s-%x", featureName, featureSha[:4]))
199218
err = fs.MkdirAll(featureDir, 0644)
200219
if err != nil {

devcontainer/devcontainer_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package devcontainer_test
22

33
import (
4+
"crypto/md5"
45
"fmt"
56
"io"
67
"net/url"
@@ -11,6 +12,7 @@ import (
1112

1213
"github.com/coder/envbuilder"
1314
"github.com/coder/envbuilder/devcontainer"
15+
"github.com/coder/envbuilder/devcontainer/features"
1416
"github.com/coder/envbuilder/registrytest"
1517
"github.com/go-git/go-billy/v5/memfs"
1618
"github.com/google/go-containerregistry/pkg/name"
@@ -36,6 +38,72 @@ func TestParse(t *testing.T) {
3638
require.Equal(t, "Dockerfile", parsed.Build.Dockerfile)
3739
}
3840

41+
func TestCompileWithFeatures(t *testing.T) {
42+
t.Parallel()
43+
registry := registrytest.New(t)
44+
featureOne := registrytest.WriteContainer(t, registry, "coder/test:tomato", features.TarLayerMediaType, map[string]any{
45+
"install.sh": "hey",
46+
"devcontainer-feature.json": features.Spec{
47+
ID: "rust",
48+
Version: "tomato",
49+
Name: "Rust",
50+
Description: "Example description!",
51+
ContainerEnv: map[string]string{
52+
"TOMATO": "example",
53+
},
54+
},
55+
})
56+
featureTwo := registrytest.WriteContainer(t, registry, "coder/test:potato", features.TarLayerMediaType, map[string]any{
57+
"install.sh": "hey",
58+
"devcontainer-feature.json": features.Spec{
59+
ID: "go",
60+
Version: "potato",
61+
Name: "Go",
62+
Description: "Example description!",
63+
ContainerEnv: map[string]string{
64+
"POTATO": "example",
65+
},
66+
},
67+
})
68+
// Update the tag to ensure it comes from the feature value!
69+
featureTwoFake := strings.Join(append(strings.Split(featureTwo, ":")[:2], "faketag"), ":")
70+
71+
raw := `{
72+
"build": {
73+
"dockerfile": "Dockerfile",
74+
"context": ".",
75+
},
76+
// Comments here!
77+
"image": "codercom/code-server:latest",
78+
"features": {
79+
"` + featureOne + `": {},
80+
"` + featureTwoFake + `": "potato"
81+
}
82+
}`
83+
dc, err := devcontainer.Parse([]byte(raw))
84+
require.NoError(t, err)
85+
fs := memfs.New()
86+
params, err := dc.Compile(fs, "", envbuilder.MagicDir, "")
87+
require.NoError(t, err)
88+
89+
// We have to SHA because we get a different MD5 every time!
90+
featureOneMD5 := md5.Sum([]byte(featureOne))
91+
featureOneSha := fmt.Sprintf("%x", featureOneMD5[:4])
92+
featureTwoMD5 := md5.Sum([]byte(featureTwo))
93+
featureTwoSha := fmt.Sprintf("%x", featureTwoMD5[:4])
94+
95+
require.Equal(t, `FROM codercom/code-server:latest
96+
97+
USER root
98+
# Go potato - Example description!
99+
ENV POTATO=example
100+
RUN .envbuilder/features/test-`+featureTwoSha+`/install.sh
101+
# Rust tomato - Example description!
102+
ENV TOMATO=example
103+
RUN .envbuilder/features/test-`+featureOneSha+`/install.sh
104+
USER 1000`, params.DockerfileContent)
105+
}
106+
39107
func TestCompileDevContainer(t *testing.T) {
40108
t.Parallel()
41109
t.Run("WithImage", func(t *testing.T) {

devcontainer/features/features.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,20 @@ func (s *Spec) Compile(options map[string]any) (string, error) {
183183
runDirective = append([]string{"RUN"}, runDirective...)
184184
runDirective = append(runDirective, s.InstallScriptPath)
185185

186-
// Prefix and suffix with a newline to ensure the RUN command is on its own line.
187-
lines := []string{"\n"}
186+
comment := ""
187+
if s.Name != "" {
188+
comment += "# " + s.Name
189+
}
190+
if s.Version != "" {
191+
comment += " " + s.Version
192+
}
193+
if s.Description != "" {
194+
comment += " - " + s.Description
195+
}
196+
lines := []string{}
197+
if comment != "" {
198+
lines = append(lines, comment)
199+
}
188200
envKeys := make([]string, 0, len(s.ContainerEnv))
189201
for key := range s.ContainerEnv {
190202
envKeys = append(envKeys, key)
@@ -195,7 +207,7 @@ func (s *Spec) Compile(options map[string]any) (string, error) {
195207
for _, key := range envKeys {
196208
lines = append(lines, fmt.Sprintf("ENV %s=%s", key, s.ContainerEnv[key]))
197209
}
198-
lines = append(lines, strings.Join(runDirective, " "), "\n")
210+
lines = append(lines, strings.Join(runDirective, " "))
199211

200212
return strings.Join(lines, "\n"), nil
201213
}

0 commit comments

Comments
 (0)