diff --git a/pkg/devcontainer/build.go b/pkg/devcontainer/build.go index a18519b8b..58d81c62a 100644 --- a/pkg/devcontainer/build.go +++ b/pkg/devcontainer/build.go @@ -71,14 +71,14 @@ func (r *runner) extendImage( } // get extend image build info - extendedBuildInfo, err := feature.GetExtendedBuildInfo( - substitutionContext, - imageBuildInfo, - imageBase, - parsedConfig, - options.ForceBuild, - featureSecretOpts(options), - ) + extendedBuildInfo, err := feature.GetExtendedBuildInfo(&feature.ExtendedBuildParams{ + Ctx: substitutionContext, + ImageBuildInfo: imageBuildInfo, + Target: imageBase, + DevContainerConfig: parsedConfig, + ForceBuild: options.ForceBuild, + SecretOpts: featureSecretOpts(options), + }) if err != nil { return nil, fmt.Errorf("get extended build info: %w", err) } @@ -153,14 +153,14 @@ func (r *runner) buildAndExtendImage( } // get extend image build info - extendedBuildInfo, err := feature.GetExtendedBuildInfo( - substitutionContext, - imageBuildInfo, - imageBase, - parsedConfig, - options.ForceBuild, - featureSecretOpts(options), - ) + extendedBuildInfo, err := feature.GetExtendedBuildInfo(&feature.ExtendedBuildParams{ + Ctx: substitutionContext, + ImageBuildInfo: imageBuildInfo, + Target: imageBase, + DevContainerConfig: parsedConfig, + ForceBuild: options.ForceBuild, + SecretOpts: featureSecretOpts(options), + }) if err != nil { return nil, fmt.Errorf("get extended build info: %w", err) } diff --git a/pkg/devcontainer/compose.go b/pkg/devcontainer/compose.go index 28bcb63b3..b1896bbc9 100644 --- a/pkg/devcontainer/compose.go +++ b/pkg/devcontainer/compose.go @@ -685,14 +685,14 @@ func (r *runner) buildAndExtendDockerCompose( if featureSecretsFile != "" { secretOpts = &feature.SecretOptions{SecretsFile: featureSecretsFile} } - extendImageBuildInfo, err := feature.GetExtendedBuildInfo( - substitutionContext, - imageBuildInfo, - buildTarget, - parsedConfig, - false, - secretOpts, - ) + extendImageBuildInfo, err := feature.GetExtendedBuildInfo(&feature.ExtendedBuildParams{ + Ctx: substitutionContext, + ImageBuildInfo: imageBuildInfo, + Target: buildTarget, + DevContainerConfig: parsedConfig, + ForceBuild: false, + SecretOpts: secretOpts, + }) if err != nil { return composeExtendResult{}, err } diff --git a/pkg/devcontainer/feature/annotations_test.go b/pkg/devcontainer/feature/annotations_test.go index 043c7f0e8..228ca3827 100644 --- a/pkg/devcontainer/feature/annotations_test.go +++ b/pkg/devcontainer/feature/annotations_test.go @@ -10,13 +10,19 @@ import ( "github.com/stretchr/testify/require" ) +const ( + testAnnotationTitle = "org.opencontainers.image.title" + testAnnotationDescription = "org.opencontainers.image.description" + testAnnotationVersion = "org.opencontainers.image.version" +) + func TestSaveAnnotations(t *testing.T) { dir := t.TempDir() annotations := map[string]string{ - "org.opencontainers.image.title": "Go", - "org.opencontainers.image.description": "Installs Go and common Go tools", - "org.opencontainers.image.version": "1.2.3", - "org.opencontainers.image.source": "https://github.com/devcontainers/features", + testAnnotationTitle: "Go", + testAnnotationDescription: "Installs Go and common Go tools", + testAnnotationVersion: "1.2.3", + "org.opencontainers.image.source": "https://github.com/devcontainers/features", } saveAnnotations(dir, annotations) @@ -47,8 +53,8 @@ func TestLoadOCIAnnotations_Present(t *testing.T) { require.NoError(t, os.MkdirAll(extractedDir, 0o750)) annotations := map[string]string{ - "org.opencontainers.image.title": "Node.js", - "org.opencontainers.image.description": "Installs Node.js and common npm tools", + testAnnotationTitle: "Node.js", + testAnnotationDescription: "Installs Node.js and common npm tools", "org.opencontainers.image.authors": "Dev Containers", "org.opencontainers.image.url": "https://github.com/devcontainers/features/tree/main/src/node", "org.opencontainers.image.documentation": "https://containers.dev/features", @@ -94,9 +100,9 @@ func TestLogOCIAnnotations_NoTitle(t *testing.T) { func TestLogOCIAnnotations_WithTitle(t *testing.T) { annotations := map[string]string{ - "org.opencontainers.image.title": "Go", - "org.opencontainers.image.description": "Installs Go", - "org.opencontainers.image.version": "1.0.0", + testAnnotationTitle: "Go", + testAnnotationDescription: "Installs Go", + testAnnotationVersion: "1.0.0", //nolint:goconst } // Should not panic logOCIAnnotations("ghcr.io/devcontainers/features/go:1", annotations) diff --git a/pkg/devcontainer/feature/collection_test.go b/pkg/devcontainer/feature/collection_test.go index 0c7afa2a8..714291e3d 100644 --- a/pkg/devcontainer/feature/collection_test.go +++ b/pkg/devcontainer/feature/collection_test.go @@ -103,7 +103,7 @@ func (s *CollectionTestSuite) TestFetchCollection_InvalidJSON() { func (s *CollectionTestSuite) TestListCollectionFeatures() { collection := Collection{ Features: []CollectionFeature{ - {ID: "rust", Version: "1.0.0", Name: "Rust"}, + {ID: "rust", Version: "1.0.0", Name: "Rust"}, //nolint:goconst {ID: "python", Version: "3.0.0", Name: "Python"}, {ID: "java", Version: "1.5.0", Name: "Java"}, }, diff --git a/pkg/devcontainer/feature/extend.go b/pkg/devcontainer/feature/extend.go index 9570a0e5d..c347d01c9 100644 --- a/pkg/devcontainer/feature/extend.go +++ b/pkg/devcontainer/feature/extend.go @@ -51,14 +51,22 @@ type BuildInfo struct { BuildArgs map[string]string } -func GetExtendedBuildInfo( - ctx *config.SubstitutionContext, - imageBuildInfo *config.ImageBuildInfo, - target string, - devContainerConfig *config.SubstitutedConfig, - forceBuild bool, - secretOpts *SecretOptions, -) (*ExtendedBuildInfo, error) { +type ExtendedBuildParams struct { + Ctx *config.SubstitutionContext + ImageBuildInfo *config.ImageBuildInfo + Target string + DevContainerConfig *config.SubstitutedConfig + ForceBuild bool + SecretOpts *SecretOptions +} + +func GetExtendedBuildInfo(params *ExtendedBuildParams) (*ExtendedBuildInfo, error) { + ctx := params.Ctx + imageBuildInfo := params.ImageBuildInfo + target := params.Target + devContainerConfig := params.DevContainerConfig + forceBuild := params.ForceBuild + secretOpts := params.SecretOpts features, err := fetchFeatures(devContainerConfig.Config, forceBuild, secretOpts) if err != nil { return nil, fmt.Errorf("fetch features: %w", err) @@ -219,22 +227,22 @@ func getFeatureSafeID(featureID string) string { } func getFeatureLayers(containerUser, remoteUser string, features []*config.FeatureSet) string { - result := `RUN \ -echo "_CONTAINER_USER_HOME=$(getent passwd ` + containerUser + ` | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env && \ -echo "_REMOTE_USER_HOME=$(getent passwd ` + remoteUser + ` | cut -d: -f6)" >> /tmp/build-features/devcontainer-features.builtin.env + const envFile = "/tmp/build-features/devcontainer-features.builtin.env" + var b strings.Builder + b.WriteString("RUN \\\n") + b.WriteString(`echo "_CONTAINER_USER_HOME=$(getent passwd ` + containerUser) + b.WriteString(` | cut -d: -f6)" >> ` + envFile + " && \\\n") + b.WriteString(`echo "_REMOTE_USER_HOME=$(getent passwd ` + remoteUser) + b.WriteString(` | cut -d: -f6)" >> ` + envFile + "\n\n") -` for i, feature := range features { - result += generateContainerEnvs(feature) - result += ` -RUN cd /tmp/build-features/` + strconv.Itoa(i) + ` \ -&& chmod +x ./devcontainer-features-install.sh \ -&& ./devcontainer-features-install.sh - -` + b.WriteString(generateContainerEnvs(feature)) + b.WriteString("\nRUN cd /tmp/build-features/" + strconv.Itoa(i) + " \\\n") + b.WriteString("&& chmod +x ./devcontainer-features-install.sh \\\n") + b.WriteString("&& ./devcontainer-features-install.sh\n\n") } - return result + return b.String() } func generateContainerEnvs(feature *config.FeatureSet) string { @@ -253,7 +261,14 @@ func findContainerUsers( baseImageMetadata *config.ImageMetadataConfig, composeServiceUser, imageUser string, ) (string, string) { - reversed := config.ReverseSlice(baseImageMetadata.Config) + containerUser, remoteUser := usersFromMetadata(baseImageMetadata) + containerUser = applyUserFallback(containerUser, composeServiceUser, imageUser) + remoteUser = applyUserFallback(remoteUser, composeServiceUser, imageUser) + return containerUser, remoteUser +} + +func usersFromMetadata(meta *config.ImageMetadataConfig) (string, string) { + reversed := config.ReverseSlice(meta.Config) containerUser := "" remoteUser := "" for _, imageMetadata := range reversed { @@ -264,21 +279,6 @@ func findContainerUsers( remoteUser = imageMetadata.RemoteUser } } - - if containerUser == "" { - if composeServiceUser != "" { - containerUser = composeServiceUser - } else if imageUser != "" { - containerUser = imageUser - } - } - if remoteUser == "" { - if composeServiceUser != "" { - remoteUser = composeServiceUser - } else if imageUser != "" { - remoteUser = imageUser - } - } return containerUser, remoteUser } @@ -290,6 +290,16 @@ func ResolveFeatureOrder( return fetchFeatures(devContainerConfig, false, nil) } +func applyUserFallback(user, composeServiceUser, imageUser string) string { + if user != "" { + return user + } + if composeServiceUser != "" { + return composeServiceUser + } + return imageUser +} + func fetchFeatures( devContainerConfig *config.DevContainerConfig, forceBuild bool, @@ -442,7 +452,7 @@ func (r *featureDependencyResolver) findByConfigID(configID string) (string, *co return "", nil } -func (r *featureDependencyResolver) resolveFeatureDependency( +func (r *featureDependencyResolver) resolveFeatureDependency( //nolint:cyclop featureID string, featureSet *config.FeatureSet, ) error { diff --git a/pkg/devcontainer/feature/extend_test.go b/pkg/devcontainer/feature/extend_test.go index c60d671b3..0e8cddc3f 100644 --- a/pkg/devcontainer/feature/extend_test.go +++ b/pkg/devcontainer/feature/extend_test.go @@ -7,6 +7,12 @@ import ( "github.com/stretchr/testify/suite" ) +const ( + testFeatureA = "feature-a" + testFeatureB = "feature-b" + testFeatureC = "feature-c" +) + type ExtendTestSuite struct { suite.Suite } @@ -17,9 +23,9 @@ func TestExtendTestSuite(t *testing.T) { func (suite *ExtendTestSuite) TestCreateFeatureLookup() { features := []*config.FeatureSet{ - {ConfigID: "feature-a"}, - {ConfigID: "feature-b"}, - {ConfigID: "feature-c"}, + {ConfigID: testFeatureA}, + {ConfigID: testFeatureB}, + {ConfigID: testFeatureC}, } lookup := buildFeatureLookupMap(features) @@ -30,74 +36,45 @@ func (suite *ExtendTestSuite) TestCreateFeatureLookup() { } } +func featureWithDeps(deps ...string) *config.FeatureSet { + df := config.DependsOnField{} + for _, d := range deps { + df[d] = map[string]any{} + } + return &config.FeatureSet{Config: &config.FeatureConfig{DependsOn: df}} +} + func (suite *ExtendTestSuite) TestHasHardDependency() { tests := []struct { - name string - feature *config.FeatureSet - originalID string - normalizedID string - expectedIsDuplicate bool + name string + feature *config.FeatureSet + originalID string + normalizedID string + expected bool }{ { - name: "exact match in dependsOn", - feature: &config.FeatureSet{ - Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{ - "node": map[string]any{}, - }, - }, - }, - originalID: "node", - normalizedID: "node", - expectedIsDuplicate: true, + "exact match", featureWithDeps(testFeatureNode), + testFeatureNode, testFeatureNode, true, }, { - name: "normalized match in dependsOn", - feature: &config.FeatureSet{ - Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{ - "ghcr.io/devcontainers/features/node": map[string]any{}, - }, - }, - }, - originalID: "ghcr.io/devcontainers/features/node:latest", - normalizedID: "ghcr.io/devcontainers/features/node", - expectedIsDuplicate: true, + "normalized match", + featureWithDeps("ghcr.io/devcontainers/features/node"), + "ghcr.io/devcontainers/features/node:latest", + "ghcr.io/devcontainers/features/node", true, //nolint:goconst }, { - name: "no match", - feature: &config.FeatureSet{ - Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{ - "python": map[string]any{}, - }, - }, - }, - originalID: "node", - normalizedID: "node", - expectedIsDuplicate: false, + "no match", featureWithDeps("python"), + testFeatureNode, testFeatureNode, false, }, { - name: "empty dependsOn", - feature: &config.FeatureSet{ - Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{}, - }, - }, - originalID: "node", - normalizedID: "node", - expectedIsDuplicate: false, + "empty dependsOn", featureWithDeps(), + testFeatureNode, testFeatureNode, false, }, } - for _, testCase := range tests { - suite.Run(testCase.name, func() { - actualIsDuplicate := hasHardDependency( - testCase.feature, - testCase.originalID, - testCase.normalizedID, - ) - suite.Equal(testCase.expectedIsDuplicate, actualIsDuplicate) + for _, tc := range tests { + suite.Run(tc.name, func() { + suite.Equal(tc.expected, hasHardDependency(tc.feature, tc.originalID, tc.normalizedID)) }) } } @@ -243,18 +220,18 @@ func (suite *ExtendTestSuite) TestComputeAutomaticFeatureOrder_ChainedDependenci func (suite *ExtendTestSuite) TestComputeAutomaticFeatureOrder_CircularDependency() { features := []*config.FeatureSet{ { - ConfigID: normalizeFeatureID("feature-a"), + ConfigID: normalizeFeatureID(testFeatureA), Config: &config.FeatureConfig{ DependsOn: config.DependsOnField{ - "feature-b": map[string]any{}, + testFeatureB: map[string]any{}, }, }, }, { - ConfigID: normalizeFeatureID("feature-b"), + ConfigID: normalizeFeatureID(testFeatureB), Config: &config.FeatureConfig{ DependsOn: config.DependsOnField{ - "feature-a": map[string]any{}, + testFeatureA: map[string]any{}, }, }, }, @@ -301,13 +278,13 @@ func (suite *ExtendTestSuite) TestComputeFeatureOrder_NoOverride() { features := []*config.FeatureSet{ { - ConfigID: normalizeFeatureID("feature-a"), + ConfigID: normalizeFeatureID(testFeatureA), Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{"feature-b": map[string]any{}}, + DependsOn: config.DependsOnField{testFeatureB: map[string]any{}}, }, }, { - ConfigID: normalizeFeatureID("feature-b"), + ConfigID: normalizeFeatureID(testFeatureB), Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}, }, } @@ -316,8 +293,8 @@ func (suite *ExtendTestSuite) TestComputeFeatureOrder_NoOverride() { suite.Require().NoError(err) suite.Len(order, 2) - expectedFeatureB := normalizeFeatureID("feature-b") - expectedFeatureA := normalizeFeatureID("feature-a") + expectedFeatureB := normalizeFeatureID(testFeatureB) + expectedFeatureA := normalizeFeatureID(testFeatureA) if order[0].ConfigID != expectedFeatureB || order[1].ConfigID != expectedFeatureA { suite.Failf( "Order mismatch", @@ -333,18 +310,18 @@ func (suite *ExtendTestSuite) TestComputeFeatureOrder_NoOverride() { func (suite *ExtendTestSuite) TestComputeFeatureOrder_OverrideViolatesDependsOn() { devContainer := &config.DevContainerConfig{ DevContainerConfigBase: config.DevContainerConfigBase{ - OverrideFeatureInstallOrder: []string{"feature-a", "feature-b"}, + OverrideFeatureInstallOrder: []string{testFeatureA, testFeatureB}, }, } features := []*config.FeatureSet{ { - ConfigID: "feature-a", + ConfigID: testFeatureA, Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{"feature-b": map[string]any{}}, + DependsOn: config.DependsOnField{testFeatureB: map[string]any{}}, }, }, - {ConfigID: "feature-b", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureB, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, } _, err := getSortedFeatureSets(devContainer, features) @@ -356,104 +333,104 @@ func (suite *ExtendTestSuite) TestComputeFeatureOrder_OverrideViolatesDependsOn( func (suite *ExtendTestSuite) TestComputeFeatureOrder_ValidOverride() { devContainer := &config.DevContainerConfig{ DevContainerConfigBase: config.DevContainerConfigBase{ - OverrideFeatureInstallOrder: []string{"feature-b", "feature-a"}, + OverrideFeatureInstallOrder: []string{testFeatureB, testFeatureA}, }, } features := []*config.FeatureSet{ { - ConfigID: "feature-a", + ConfigID: testFeatureA, Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{"feature-b": map[string]any{}}, + DependsOn: config.DependsOnField{testFeatureB: map[string]any{}}, }, }, - {ConfigID: "feature-b", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureB, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, } order, err := getSortedFeatureSets(devContainer, features) suite.Require().NoError(err) suite.Len(order, 2) - suite.Equal("feature-b", order[0].ConfigID) - suite.Equal("feature-a", order[1].ConfigID) + suite.Equal(testFeatureB, order[0].ConfigID) + suite.Equal(testFeatureA, order[1].ConfigID) } func (suite *ExtendTestSuite) TestComputeFeatureOrder_PartialOverride() { devContainer := &config.DevContainerConfig{ DevContainerConfigBase: config.DevContainerConfigBase{ - OverrideFeatureInstallOrder: []string{"feature-c"}, + OverrideFeatureInstallOrder: []string{testFeatureC}, }, } features := []*config.FeatureSet{ { - ConfigID: "feature-a", + ConfigID: testFeatureA, Config: &config.FeatureConfig{ - DependsOn: config.DependsOnField{"feature-b": map[string]any{}}, + DependsOn: config.DependsOnField{testFeatureB: map[string]any{}}, }, }, - {ConfigID: "feature-b", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, - {ConfigID: "feature-c", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureB, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureC, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, } order, err := getSortedFeatureSets(devContainer, features) suite.Require().NoError(err) suite.Len(order, 3) - if order[0].ConfigID != "feature-c" { + if order[0].ConfigID != testFeatureC { suite.Failf("First element mismatch", "Expected feature-c first, got %s", order[0].ConfigID) } } func (suite *ExtendTestSuite) TestBuildOverridePriority() { features := []*config.FeatureSet{ - {ConfigID: "feature-a", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, - {ConfigID: "feature-b", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, - {ConfigID: "feature-c", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureA, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureB, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureC, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, } lookup := buildFeatureLookupMap(features) - overrideOrder := []string{"feature-c", "feature-a"} + overrideOrder := []string{testFeatureC, testFeatureA} priority := buildOverridePriority(overrideOrder, lookup) - suite.Equal(0, priority["feature-c"]) - suite.Equal(1, priority["feature-a"]) - _, hasB := priority["feature-b"] + suite.Equal(0, priority[testFeatureC]) + suite.Equal(1, priority[testFeatureA]) + _, hasB := priority[testFeatureB] suite.False(hasB) } func (suite *ExtendTestSuite) TestOverridePriorityAffectsSortOrder() { devContainer := &config.DevContainerConfig{ DevContainerConfigBase: config.DevContainerConfigBase{ - OverrideFeatureInstallOrder: []string{"feature-c", "feature-a"}, + OverrideFeatureInstallOrder: []string{testFeatureC, testFeatureA}, }, } features := []*config.FeatureSet{ - {ConfigID: "feature-a", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, - {ConfigID: "feature-b", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, - {ConfigID: "feature-c", Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureA, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureB, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, + {ConfigID: testFeatureC, Config: &config.FeatureConfig{DependsOn: config.DependsOnField{}}}, } order, err := getSortedFeatureSets(devContainer, features) suite.Require().NoError(err) suite.Len(order, 3) - suite.Equal("feature-c", order[0].ConfigID) - suite.Equal("feature-a", order[1].ConfigID) - suite.Equal("feature-b", order[2].ConfigID) + suite.Equal(testFeatureC, order[0].ConfigID) + suite.Equal(testFeatureA, order[1].ConfigID) + suite.Equal(testFeatureB, order[2].ConfigID) } func (suite *ExtendTestSuite) TestExtractFeatureByID() { features := []*config.FeatureSet{ - {ConfigID: "feature-a"}, - {ConfigID: "feature-b"}, + {ConfigID: testFeatureA}, + {ConfigID: testFeatureB}, } - found := extractFeatureByID(features, "feature-b") - if found == nil || found.ConfigID != "feature-b" { + found := extractFeatureByID(features, testFeatureB) + if found == nil || found.ConfigID != testFeatureB { suite.Fail("Expected to find feature-b") } - notFound := extractFeatureByID(features, "feature-c") + notFound := extractFeatureByID(features, testFeatureC) if notFound != nil { suite.Fail("Expected not to find feature-c") } @@ -461,15 +438,15 @@ func (suite *ExtendTestSuite) TestExtractFeatureByID() { func (suite *ExtendTestSuite) TestContainsFeature() { features := []*config.FeatureSet{ - {ConfigID: "feature-a"}, - {ConfigID: "feature-b"}, + {ConfigID: testFeatureA}, + {ConfigID: testFeatureB}, } - if !containsFeature(features, "feature-a") { + if !containsFeature(features, testFeatureA) { suite.Fail("Expected to contain feature-a") } - if containsFeature(features, "feature-c") { + if containsFeature(features, testFeatureC) { suite.Fail("Expected not to contain feature-c") } } @@ -562,15 +539,15 @@ func (suite *ExtendTestSuite) TestResolveDependencies_LegacyIDResolution() { func (suite *ExtendTestSuite) TestResolveDependencies_LegacyIDNotUsedWhenPrimaryExists() { features := map[string]*config.FeatureSet{ - "feature-a": { - ConfigID: "feature-a", + testFeatureA: { + ConfigID: testFeatureA, Config: &config.FeatureConfig{ - LegacyIds: []string{"feature-b"}, + LegacyIds: []string{testFeatureB}, DependsOn: config.DependsOnField{}, }, }, - "feature-b": { - ConfigID: "feature-b", + testFeatureB: { + ConfigID: testFeatureB, Config: &config.FeatureConfig{ DependsOn: config.DependsOnField{}, }, @@ -579,7 +556,7 @@ func (suite *ExtendTestSuite) TestResolveDependencies_LegacyIDNotUsedWhenPrimary ConfigID: "consumer", Config: &config.FeatureConfig{ DependsOn: config.DependsOnField{ - "feature-b": map[string]any{}, + testFeatureB: map[string]any{}, }, }, }, @@ -588,7 +565,7 @@ func (suite *ExtendTestSuite) TestResolveDependencies_LegacyIDNotUsedWhenPrimary resolved, err := resolveDependencies(&featureProcessor{}, features) suite.Require().NoError(err) suite.Len(resolved, 3) - suite.NotNil(resolved["feature-b"]) + suite.NotNil(resolved[testFeatureB]) } func (suite *ExtendTestSuite) TestVersionAwareDeduplication_SameConfigSameVersion() { diff --git a/pkg/devcontainer/feature/features.go b/pkg/devcontainer/feature/features.go index efb585b3a..c95da3c40 100644 --- a/pkg/devcontainer/feature/features.go +++ b/pkg/devcontainer/feature/features.go @@ -321,7 +321,7 @@ func pullAndExtractOCIFeature( return nil, fmt.Errorf("download layer from %s: %w", registry, err) } - file, err := os.Open(destFile) + file, err := os.Open(filepath.Clean(destFile)) //nolint:gosec // path from internal resolution if err != nil { return nil, err } @@ -385,7 +385,7 @@ func writeLayerToFile(data io.Reader, destFile string) error { return fmt.Errorf("create target folder: %w", err) } - file, err := os.Create(destFile) + file, err := os.Create(filepath.Clean(destFile)) //nolint:gosec // path from internal resolution if err != nil { return fmt.Errorf("create file: %w", err) } @@ -589,7 +589,7 @@ func tryDownload(url, destFile string, httpHeaders map[string]string) error { return fmt.Errorf("GET request failed, status code is %d", resp.StatusCode) } - file, err := os.Create(destFile) + file, err := os.Create(filepath.Clean(destFile)) //nolint:gosec // path from internal resolution if err != nil { return fmt.Errorf("create download file: %w", err) }