diff --git a/devcontainer/devcontainer.go b/devcontainer/devcontainer.go index 4db3496a..cb3f1c5e 100644 --- a/devcontainer/devcontainer.go +++ b/devcontainer/devcontainer.go @@ -191,41 +191,37 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, scratchDir, remoteUser, dock if err != nil { return "", fmt.Errorf("parse feature ref %s: %w", featureRefRaw, err) } - featureImage := featureRefParsed.Repository.Name() - featureTag := featureRefParsed.TagStr() featureOpts := map[string]any{} switch t := s.Features[featureRefRaw].(type) { case string: - featureTag = t + // As a shorthand, the value of the `features`` property can be provided as a + // single string. This string is mapped to an option called version. + // https://containers.dev/implementors/features/#devcontainer-json-properties + featureOpts["version"] = t case map[string]any: featureOpts = t } - featureRef := featureImage - if featureTag != "" { - featureRef += ":" + featureTag - } - // It's important for caching that this directory is static. // If it changes on each run then the container will not be cached. // // devcontainers/cli has a very complex method of computing the feature // name from the feature reference. We're just going to hash it for simplicity. - featureSha := md5.Sum([]byte(featureRef)) - featureName := filepath.Base(featureImage) + featureSha := md5.Sum([]byte(featureRefRaw)) + featureName := filepath.Base(featureRefParsed.Repository.Name()) featureDir := filepath.Join(featuresDir, fmt.Sprintf("%s-%x", featureName, featureSha[:4])) err = fs.MkdirAll(featureDir, 0644) if err != nil { return "", err } - spec, err := features.Extract(fs, featureDir, featureRef) + spec, err := features.Extract(fs, featureDir, featureRefRaw) if err != nil { - return "", fmt.Errorf("extract feature %s: %w", featureRef, err) + return "", fmt.Errorf("extract feature %s: %w", featureRefRaw, err) } directive, err := spec.Compile(featureOpts) if err != nil { - return "", fmt.Errorf("compile feature %s: %w", featureRef, err) + return "", fmt.Errorf("compile feature %s: %w", featureRefRaw, err) } featureDirectives = append(featureDirectives, directive) } diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go index 3f1b5376..440f9020 100644 --- a/devcontainer/devcontainer_test.go +++ b/devcontainer/devcontainer_test.go @@ -41,7 +41,7 @@ func TestParse(t *testing.T) { func TestCompileWithFeatures(t *testing.T) { t.Parallel() registry := registrytest.New(t) - featureOne := registrytest.WriteContainer(t, registry, "coder/test:tomato", features.TarLayerMediaType, map[string]any{ + featureOne := registrytest.WriteContainer(t, registry, "coder/one:tomato", features.TarLayerMediaType, map[string]any{ "install.sh": "hey", "devcontainer-feature.json": features.Spec{ ID: "rust", @@ -53,7 +53,7 @@ func TestCompileWithFeatures(t *testing.T) { }, }, }) - featureTwo := registrytest.WriteContainer(t, registry, "coder/test:potato", features.TarLayerMediaType, map[string]any{ + featureTwo := registrytest.WriteContainer(t, registry, "coder/two:potato", features.TarLayerMediaType, map[string]any{ "install.sh": "hey", "devcontainer-feature.json": features.Spec{ ID: "go", @@ -63,10 +63,13 @@ func TestCompileWithFeatures(t *testing.T) { ContainerEnv: map[string]string{ "POTATO": "example", }, + Options: map[string]features.Option{ + "version": { + Type: "string", + }, + }, }, }) - // Update the tag to ensure it comes from the feature value! - featureTwoFake := strings.Join(append(strings.Split(featureTwo, ":")[:2], "faketag"), ":") raw := `{ "build": { @@ -77,7 +80,7 @@ func TestCompileWithFeatures(t *testing.T) { "image": "codercom/code-server:latest", "features": { "` + featureOne + `": {}, - "` + featureTwoFake + `": "potato" + "` + featureTwo + `": "potato" } }` dc, err := devcontainer.Parse([]byte(raw)) @@ -95,12 +98,12 @@ func TestCompileWithFeatures(t *testing.T) { require.Equal(t, `FROM codercom/code-server:latest USER root -# Go potato - Example description! -ENV POTATO=example -RUN .envbuilder/features/test-`+featureTwoSha+`/install.sh # Rust tomato - Example description! ENV TOMATO=example -RUN .envbuilder/features/test-`+featureOneSha+`/install.sh +RUN .envbuilder/features/one-`+featureOneSha+`/install.sh +# Go potato - Example description! +ENV POTATO=example +RUN VERSION=potato .envbuilder/features/two-`+featureTwoSha+`/install.sh USER 1000`, params.DockerfileContent) }