Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions devspace-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,14 @@
],
"description": "Enabled specifies if the given import should be enabled"
},
"mergeStrategy": {
"type": "string",
"enum": [
"shallowMerge",
"deepMerge"
],
"description": "MergeStrategy specifies how the imported config should be merged (e.g. shallowMerge or deepMerge)"
},
"path": {
"type": "string",
"description": "Path is the local path where DevSpace can find the artifact.\nThis option is mutually exclusive with the git option.",
Expand Down
8 changes: 8 additions & 0 deletions docs/schemas/config-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,14 @@
"type": "boolean",
"description": "Enabled specifies if the given import should be enabled"
},
"mergeStrategy": {
"type": "string",
"enum": [
"shallowMerge",
"deepMerge"
],
"description": "MergeStrategy specifies how the imported config should be merged (e.g. shallowMerge or deepMerge)"
},
"path": {
"type": "string",
"description": "Path is the local path where DevSpace can find the artifact.\nThis option is mutually exclusive with the git option.",
Expand Down
59 changes: 54 additions & 5 deletions pkg/devspace/config/loader/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/loft-sh/devspace/pkg/devspace/config/loader/variable"
"github.com/loft-sh/devspace/pkg/devspace/config/versions"
"github.com/loft-sh/devspace/pkg/devspace/config/versions/latest"
"github.com/loft-sh/devspace/pkg/devspace/config/versions/util"
dependencyutil "github.com/loft-sh/devspace/pkg/devspace/dependency/util"
"github.com/loft-sh/devspace/pkg/util/log"
Expand Down Expand Up @@ -119,11 +120,13 @@ func ResolveImports(ctx context.Context, resolver variable.Resolver, basePath st
mergedMap[section] = map[string]interface{}{}
}

for key, value := range sectionMap {
_, ok := mergedMap[section].(map[string]interface{})[key]
if !ok {
mergedMap[section].(map[string]interface{})[key] = value
}
switch i.MergeStrategy {
case latest.MergeStrategyDeep:
deepMerge(mergedMap[section].(map[string]interface{}), sectionMap)
case "", latest.MergeStrategyShallow:
shallowMerge(mergedMap[section].(map[string]interface{}), sectionMap)
default:
return nil, fmt.Errorf("invalid mergeStrategy %q in import %s", i.MergeStrategy, configPath)
}
}

Expand All @@ -143,3 +146,49 @@ func ResolveImports(ctx context.Context, resolver variable.Resolver, basePath st

return mergedMap, nil
}

// deepMerge recursively merges src into dst.
// Maps are deep merged, other types (arrays, scalars) in dst take precedence.
func deepMerge(dst, src map[string]interface{}) {
for key, srcVal := range src {
// Skip nil source values
if srcVal == nil {
continue
}

dstVal, exists := dst[key]

// Key doesn't exist in dst - add from src
if !exists {
dst[key] = srcVal
continue
}

// Dst value is nil - use src value
if dstVal == nil {
dst[key] = srcVal
continue
}

// Both exist and are non-nil - check if both are maps
srcMap, srcIsMap := srcVal.(map[string]interface{})
dstMap, dstIsMap := dstVal.(map[string]interface{})

if srcIsMap && dstIsMap {
// Both are maps - merge recursively
deepMerge(dstMap, srcMap)
}

// For other types (arrays, scalars), dst takes precedence (no action needed)
}
}

// shallowMerge merges src into dst only at the top level.
// Existing keys in dst take precedence.
func shallowMerge(dst, src map[string]interface{}) {
for key, value := range src {
if _, ok := dst[key]; !ok {
dst[key] = value
}
}
}
Loading