Skip to content

Commit 0a027c3

Browse files
authored
Support providing feature directories in build contexts (#117)
1 parent 6a88184 commit 0a027c3

File tree

5 files changed

+45
-24
lines changed

5 files changed

+45
-24
lines changed

devcontainer/devcontainer.go

+17-13
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type Compiled struct {
6565
DockerfilePath string
6666
DockerfileContent string
6767
BuildContext string
68+
FeatureContexts map[string]string
6869
BuildArgs []string
6970

7071
User string
@@ -130,7 +131,7 @@ func (s Spec) HasDockerfile() bool {
130131
// devcontainerDir is the path to the directory where the devcontainer.json file
131132
// is located. scratchDir is the path to the directory where the Dockerfile will
132133
// be written to if one doesn't exist.
133-
func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir, fallbackDockerfile, workspaceFolder string) (*Compiled, error) {
134+
func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string, fallbackDockerfile, workspaceFolder string, useBuildContexts bool) (*Compiled, error) {
134135
params := &Compiled{
135136
User: s.ContainerUser,
136137
ContainerEnv: s.ContainerEnv,
@@ -213,25 +214,26 @@ func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir, fallbac
213214
if remoteUser == "" {
214215
remoteUser = params.User
215216
}
216-
params.DockerfileContent, err = s.compileFeatures(fs, devcontainerDir, scratchDir, params.User, remoteUser, params.DockerfileContent)
217+
params.DockerfileContent, params.FeatureContexts, err = s.compileFeatures(fs, devcontainerDir, scratchDir, params.User, remoteUser, params.DockerfileContent, useBuildContexts)
217218
if err != nil {
218219
return nil, err
219220
}
220221
return params, nil
221222
}
222223

223-
func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir, containerUser, remoteUser, dockerfileContent string) (string, error) {
224+
func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir string, containerUser, remoteUser, dockerfileContent string, useBuildContexts bool) (string, map[string]string, error) {
224225
// If there are no features, we don't need to do anything!
225226
if len(s.Features) == 0 {
226-
return dockerfileContent, nil
227+
return dockerfileContent, nil, nil
227228
}
228229

229230
featuresDir := filepath.Join(scratchDir, "features")
230231
err := fs.MkdirAll(featuresDir, 0644)
231232
if err != nil {
232-
return "", fmt.Errorf("create features directory: %w", err)
233+
return "", nil, fmt.Errorf("create features directory: %w", err)
233234
}
234235
featureDirectives := []string{}
236+
featureContexts := make(map[string]string)
235237

236238
// TODO: Respect the installation order outlined by the spec:
237239
// https://containers.dev/implementors/features/#installation-order
@@ -251,7 +253,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir,
251253
if _, featureRef, ok = strings.Cut(featureRefRaw, "./"); !ok {
252254
featureRefParsed, err := name.NewTag(featureRefRaw)
253255
if err != nil {
254-
return "", fmt.Errorf("parse feature ref %s: %w", featureRefRaw, err)
256+
return "", nil, fmt.Errorf("parse feature ref %s: %w", featureRefRaw, err)
255257
}
256258
featureRef = featureRefParsed.Repository.Name()
257259
}
@@ -275,19 +277,21 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir,
275277
featureSha := md5.Sum([]byte(featureRefRaw))
276278
featureName := filepath.Base(featureRef)
277279
featureDir := filepath.Join(featuresDir, fmt.Sprintf("%s-%x", featureName, featureSha[:4]))
278-
err = fs.MkdirAll(featureDir, 0644)
279-
if err != nil {
280-
return "", err
280+
if err := fs.MkdirAll(featureDir, 0644); err != nil {
281+
return "", nil, err
281282
}
282283
spec, err := features.Extract(fs, devcontainerDir, featureDir, featureRefRaw)
283284
if err != nil {
284-
return "", fmt.Errorf("extract feature %s: %w", featureRefRaw, err)
285+
return "", nil, fmt.Errorf("extract feature %s: %w", featureRefRaw, err)
285286
}
286-
directive, err := spec.Compile(containerUser, remoteUser, featureOpts)
287+
directive, err := spec.Compile(featureName, containerUser, remoteUser, useBuildContexts, featureOpts)
287288
if err != nil {
288-
return "", fmt.Errorf("compile feature %s: %w", featureRefRaw, err)
289+
return "", nil, fmt.Errorf("compile feature %s: %w", featureRefRaw, err)
289290
}
290291
featureDirectives = append(featureDirectives, directive)
292+
if useBuildContexts {
293+
featureContexts[featureName] = featureDir
294+
}
291295
}
292296

293297
lines := []string{"\nUSER root"}
@@ -297,7 +301,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir,
297301
// we're going to run as root.
298302
lines = append(lines, fmt.Sprintf("USER %s", remoteUser))
299303
}
300-
return strings.Join(append([]string{dockerfileContent}, lines...), "\n"), err
304+
return strings.Join(append([]string{dockerfileContent}, lines...), "\n"), featureContexts, err
301305
}
302306

303307
// UserFromDockerfile inspects the contents of a provided Dockerfile

devcontainer/devcontainer_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func TestCompileWithFeatures(t *testing.T) {
8787
dc, err := devcontainer.Parse([]byte(raw))
8888
require.NoError(t, err)
8989
fs := memfs.New()
90-
params, err := dc.Compile(fs, "", magicDir, "", "")
90+
params, err := dc.Compile(fs, "", magicDir, "", "", false)
9191
require.NoError(t, err)
9292

9393
// We have to SHA because we get a different MD5 every time!
@@ -118,7 +118,7 @@ func TestCompileDevContainer(t *testing.T) {
118118
dc := &devcontainer.Spec{
119119
Image: "codercom/code-server:latest",
120120
}
121-
params, err := dc.Compile(fs, "", magicDir, "", "")
121+
params, err := dc.Compile(fs, "", magicDir, "", "", false)
122122
require.NoError(t, err)
123123
require.Equal(t, filepath.Join(magicDir, "Dockerfile"), params.DockerfilePath)
124124
require.Equal(t, magicDir, params.BuildContext)
@@ -144,7 +144,7 @@ func TestCompileDevContainer(t *testing.T) {
144144
_, err = io.WriteString(file, "FROM ubuntu")
145145
require.NoError(t, err)
146146
_ = file.Close()
147-
params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace")
147+
params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace", false)
148148
require.NoError(t, err)
149149
require.Equal(t, "ARG1=value1", params.BuildArgs[0])
150150
require.Equal(t, "ARG2=workspace", params.BuildArgs[1])

devcontainer/features/features.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ type Spec struct {
194194

195195
// Extract unpacks the feature from the image and returns a set of lines
196196
// that should be appended to a Dockerfile to install the feature.
197-
func (s *Spec) Compile(containerUser, remoteUser string, options map[string]any) (string, error) {
197+
func (s *Spec) Compile(featureName, containerUser, remoteUser string, useBuildContexts bool, options map[string]any) (string, error) {
198198
// TODO not sure how we figure out _(REMOTE|CONTAINER)_USER_HOME
199199
// as per the feature spec.
200200
// See https://containers.dev/implementors/features/#user-env-var
@@ -219,7 +219,11 @@ func (s *Spec) Compile(containerUser, remoteUser string, options map[string]any)
219219
// regardless of map iteration order.
220220
sort.Strings(runDirective)
221221
// See https://containers.dev/implementors/features/#invoking-installsh
222-
runDirective = append([]string{"RUN"}, runDirective...)
222+
if useBuildContexts {
223+
runDirective = append([]string{"RUN", "--mount=type=bind,from=" + featureName + ",target=/envbuilder-features/" + featureName + ",rw"}, runDirective...)
224+
} else {
225+
runDirective = append([]string{"RUN"}, runDirective...)
226+
}
223227
runDirective = append(runDirective, "./install.sh")
224228

225229
comment := ""
@@ -236,7 +240,11 @@ func (s *Spec) Compile(containerUser, remoteUser string, options map[string]any)
236240
if comment != "" {
237241
lines = append(lines, comment)
238242
}
239-
lines = append(lines, "WORKDIR "+s.Directory)
243+
if useBuildContexts {
244+
lines = append(lines, "WORKDIR /envbuilder-features/"+featureName)
245+
} else {
246+
lines = append(lines, "WORKDIR "+s.Directory)
247+
}
240248
envKeys := make([]string, 0, len(s.ContainerEnv))
241249
for key := range s.ContainerEnv {
242250
envKeys = append(envKeys, key)

devcontainer/features/features_test.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func TestCompile(t *testing.T) {
7373
t.Run("UnknownOption", func(t *testing.T) {
7474
t.Parallel()
7575
spec := &features.Spec{}
76-
_, err := spec.Compile("containerUser", "remoteUser", map[string]any{
76+
_, err := spec.Compile("test", "containerUser", "remoteUser", false, map[string]any{
7777
"unknown": "value",
7878
})
7979
require.ErrorContains(t, err, "unknown option")
@@ -83,7 +83,7 @@ func TestCompile(t *testing.T) {
8383
spec := &features.Spec{
8484
Directory: "/",
8585
}
86-
directive, err := spec.Compile("containerUser", "remoteUser", nil)
86+
directive, err := spec.Compile("test", "containerUser", "remoteUser", false, nil)
8787
require.NoError(t, err)
8888
require.Equal(t, "WORKDIR /\nRUN _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
8989
})
@@ -95,7 +95,7 @@ func TestCompile(t *testing.T) {
9595
"FOO": "bar",
9696
},
9797
}
98-
directive, err := spec.Compile("containerUser", "remoteUser", nil)
98+
directive, err := spec.Compile("test", "containerUser", "remoteUser", false, nil)
9999
require.NoError(t, err)
100100
require.Equal(t, "WORKDIR /\nENV FOO=bar\nRUN _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
101101
})
@@ -109,8 +109,17 @@ func TestCompile(t *testing.T) {
109109
},
110110
},
111111
}
112-
directive, err := spec.Compile("containerUser", "remoteUser", nil)
112+
directive, err := spec.Compile("test", "containerUser", "remoteUser", false, nil)
113113
require.NoError(t, err)
114114
require.Equal(t, "WORKDIR /\nRUN FOO=\"bar\" _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
115115
})
116+
t.Run("BuildContext", func(t *testing.T) {
117+
t.Parallel()
118+
spec := &features.Spec{
119+
Directory: "/",
120+
}
121+
directive, err := spec.Compile("test", "containerUser", "remoteUser", true, nil)
122+
require.NoError(t, err)
123+
require.Equal(t, "WORKDIR /envbuilder-features/test\nRUN --mount=type=bind,from=test,target=/envbuilder-features/test,rw _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive))
124+
})
116125
}

envbuilder.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ func Run(ctx context.Context, options Options) error {
460460
logf(codersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...")
461461
fallbackDockerfile = defaultParams.DockerfilePath
462462
}
463-
buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder)
463+
buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false)
464464
if err != nil {
465465
return fmt.Errorf("compile devcontainer.json: %w", err)
466466
}

0 commit comments

Comments
 (0)