Skip to content

Commit 8962d96

Browse files
authored
Work around caching issue by copying extracted feature to an intermediate stage (#118)
1 parent 0a027c3 commit 8962d96

File tree

3 files changed

+19
-13
lines changed

3 files changed

+19
-13
lines changed

devcontainer/devcontainer.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir
245245
// is deterministic which allows for caching.
246246
sort.Strings(featureOrder)
247247

248+
var lines []string
248249
for _, featureRefRaw := range featureOrder {
249250
var (
250251
featureRef string
@@ -284,24 +285,26 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir
284285
if err != nil {
285286
return "", nil, fmt.Errorf("extract feature %s: %w", featureRefRaw, err)
286287
}
287-
directive, err := spec.Compile(featureName, containerUser, remoteUser, useBuildContexts, featureOpts)
288+
fromDirective, directive, err := spec.Compile(featureName, containerUser, remoteUser, useBuildContexts, featureOpts)
288289
if err != nil {
289290
return "", nil, fmt.Errorf("compile feature %s: %w", featureRefRaw, err)
290291
}
291292
featureDirectives = append(featureDirectives, directive)
292293
if useBuildContexts {
293294
featureContexts[featureName] = featureDir
295+
lines = append(lines, fromDirective)
294296
}
295297
}
296298

297-
lines := []string{"\nUSER root"}
299+
lines = append(lines, dockerfileContent)
300+
lines = append(lines, "\nUSER root")
298301
lines = append(lines, featureDirectives...)
299302
if remoteUser != "" {
300303
// TODO: We should warn that because we were unable to find the remote user,
301304
// we're going to run as root.
302305
lines = append(lines, fmt.Sprintf("USER %s", remoteUser))
303306
}
304-
return strings.Join(append([]string{dockerfileContent}, lines...), "\n"), featureContexts, err
307+
return strings.Join(lines, "\n"), featureContexts, err
305308
}
306309

307310
// UserFromDockerfile inspects the contents of a provided Dockerfile

devcontainer/features/features.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,11 @@ 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(featureName, containerUser, remoteUser string, useBuildContexts bool, options map[string]any) (string, error) {
197+
func (s *Spec) Compile(featureName, containerUser, remoteUser string, useBuildContexts bool, options map[string]any) (string, 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
201+
var fromDirective string
201202
runDirective := []string{
202203
"_CONTAINER_USER=" + strconv.Quote(containerUser),
203204
"_REMOTE_USER=" + strconv.Quote(remoteUser),
@@ -213,14 +214,15 @@ func (s *Spec) Compile(featureName, containerUser, remoteUser string, useBuildCo
213214
runDirective = append(runDirective, fmt.Sprintf(`%s=%q`, convertOptionNameToEnv(key), strValue))
214215
}
215216
if len(options) > 0 {
216-
return "", fmt.Errorf("unknown option: %v", options)
217+
return "", "", fmt.Errorf("unknown option: %v", options)
217218
}
218219
// It's critical that the Dockerfile produced is deterministic,
219220
// regardless of map iteration order.
220221
sort.Strings(runDirective)
221222
// See https://containers.dev/implementors/features/#invoking-installsh
222223
if useBuildContexts {
223-
runDirective = append([]string{"RUN", "--mount=type=bind,from=" + featureName + ",target=/envbuilder-features/" + featureName + ",rw"}, runDirective...)
224+
fromDirective = "FROM scratch AS envbuilder_feature_" + featureName + "\nCOPY --from=" + featureName + " / /\n"
225+
runDirective = append([]string{"RUN", "--mount=type=bind,from=envbuilder_feature_" + featureName + ",target=/envbuilder-features/" + featureName + ",rw"}, runDirective...)
224226
} else {
225227
runDirective = append([]string{"RUN"}, runDirective...)
226228
}
@@ -257,7 +259,7 @@ func (s *Spec) Compile(featureName, containerUser, remoteUser string, useBuildCo
257259
}
258260
lines = append(lines, strings.Join(runDirective, " "))
259261

260-
return strings.Join(lines, "\n"), nil
262+
return fromDirective, strings.Join(lines, "\n"), nil
261263
}
262264

263265
var (

devcontainer/features/features_test.go

+7-6
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("test", "containerUser", "remoteUser", false, 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("test", "containerUser", "remoteUser", false, 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("test", "containerUser", "remoteUser", false, 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,7 +109,7 @@ func TestCompile(t *testing.T) {
109109
},
110110
},
111111
}
112-
directive, err := spec.Compile("test", "containerUser", "remoteUser", false, 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
})
@@ -118,8 +118,9 @@ func TestCompile(t *testing.T) {
118118
spec := &features.Spec{
119119
Directory: "/",
120120
}
121-
directive, err := spec.Compile("test", "containerUser", "remoteUser", true, nil)
121+
fromDirective, runDirective, err := spec.Compile("test", "containerUser", "remoteUser", true, nil)
122122
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))
123+
require.Equal(t, "FROM scratch AS envbuilder_feature_test\nCOPY --from=test / /", strings.TrimSpace(fromDirective))
124+
require.Equal(t, "WORKDIR /envbuilder-features/test\nRUN --mount=type=bind,from=envbuilder_feature_test,target=/envbuilder-features/test,rw _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(runDirective))
124125
})
125126
}

0 commit comments

Comments
 (0)