diff --git a/atmos.yaml b/atmos.yaml
index 6a1eb0b88..daf33b02d 100644
--- a/atmos.yaml
+++ b/atmos.yaml
@@ -68,7 +68,7 @@ stacks:
name_pattern: "{tenant}-{environment}-{stage}"
workflows:
- # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line arguments
+ # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/workflows"
@@ -301,12 +301,12 @@ integrations:
schemas:
# https://json-schema.org
jsonschema:
- # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/jsonschema"
# https://www.openpolicyagent.org
opa:
- # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/opa"
# JSON Schema to validate Atmos manifests
@@ -318,7 +318,7 @@ schemas:
# https://www.schemastore.org/json
# https://github.com/SchemaStore/schemastore
atmos:
- # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line argument
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"
@@ -338,3 +338,16 @@ templates:
timeout: 5
# https://docs.gomplate.ca/datasources
datasources: {}
+
+settings:
+ # `list_merge_strategy` specifies how lists are merged in Atmos stack manifests.
+ # Can also be set using 'ATMOS_SETTINGS_LIST_MERGE_STRATEGY' environment variable, or '--settings-list-merge-strategy' command-line argument
+ # The following strategies are supported:
+ # `replace`: Most recent list imported wins (the default behavior).
+ # `append`: The sequence of lists is appended in the same order as imports.
+ # `merge`: The items in the destination list are deep-merged with the items in the source list.
+ # The items in the source list take precedence.
+ # The items are processed starting from the first up to the length of the source list (the remaining items are not processed).
+ # If the source and destination lists have the same length, all items in the destination lists are
+ # deep-merged with all items in the source list.
+ list_merge_strategy: replace
diff --git a/examples/demo-stacks/atmos.yaml b/examples/demo-stacks/atmos.yaml
index 171ee6407..555d07515 100644
--- a/examples/demo-stacks/atmos.yaml
+++ b/examples/demo-stacks/atmos.yaml
@@ -17,5 +17,5 @@ stacks:
name_pattern: "{stage}"
logs:
- file: "/dev/stdout"
+ file: "/dev/stderr"
level: Info
diff --git a/examples/quick-start/Dockerfile b/examples/quick-start/Dockerfile
index e2908efdc..9ebd41ef4 100644
--- a/examples/quick-start/Dockerfile
+++ b/examples/quick-start/Dockerfile
@@ -6,7 +6,7 @@ ARG GEODESIC_OS=debian
# https://atmos.tools/
# https://github.com/cloudposse/atmos
# https://github.com/cloudposse/atmos/releases
-ARG ATMOS_VERSION=1.75.0
+ARG ATMOS_VERSION=1.76.0
# Terraform: https://github.com/hashicorp/terraform/releases
ARG TF_VERSION=1.8.4
diff --git a/examples/quick-start/atmos.yaml b/examples/quick-start/atmos.yaml
index c686bcf95..b5d527c0c 100644
--- a/examples/quick-start/atmos.yaml
+++ b/examples/quick-start/atmos.yaml
@@ -68,7 +68,7 @@ stacks:
name_pattern: "{tenant}-{environment}-{stage}"
workflows:
- # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line arguments
+ # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/workflows"
@@ -234,12 +234,12 @@ commands:
schemas:
# https://json-schema.org
jsonschema:
- # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/jsonschema"
# https://www.openpolicyagent.org
opa:
- # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/opa"
# JSON Schema to validate Atmos manifests
@@ -251,7 +251,7 @@ schemas:
# https://www.schemastore.org/json
# https://github.com/SchemaStore/schemastore
atmos:
- # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line argument
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"
@@ -270,3 +270,16 @@ templates:
enabled: true
# https://docs.gomplate.ca/datasources
datasources: {}
+
+settings:
+ # `list_merge_strategy` specifies how lists are merged in Atmos stack manifests.
+ # Can also be set using 'ATMOS_SETTINGS_LIST_MERGE_STRATEGY' environment variable, or '--settings-list-merge-strategy' command-line argument
+ # The following strategies are supported:
+ # `replace`: Most recent list imported wins (the default behavior).
+ # `append`: The sequence of lists is appended in the same order as imports.
+ # `merge`: The items in the destination list are deep-merged with the items in the source list.
+ # The items in the source list take precedence.
+ # The items are processed starting from the first up to the length of the source list (the remaining items are not processed).
+ # If the source and destination lists have the same length, all items in the destination lists are
+ # deep-merged with all items in the source list.
+ list_merge_strategy: replace
diff --git a/examples/quick-start/rootfs/usr/local/etc/atmos/atmos.yaml b/examples/quick-start/rootfs/usr/local/etc/atmos/atmos.yaml
index 712f77cd2..4a46b977a 100644
--- a/examples/quick-start/rootfs/usr/local/etc/atmos/atmos.yaml
+++ b/examples/quick-start/rootfs/usr/local/etc/atmos/atmos.yaml
@@ -68,7 +68,7 @@ stacks:
name_pattern: "{tenant}-{environment}-{stage}"
workflows:
- # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line arguments
+ # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/workflows"
@@ -235,12 +235,12 @@ commands:
schemas:
# https://json-schema.org
jsonschema:
- # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/jsonschema"
# https://www.openpolicyagent.org
opa:
- # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/opa"
# JSON Schema to validate Atmos manifests
@@ -252,7 +252,7 @@ schemas:
# https://www.schemastore.org/json
# https://github.com/SchemaStore/schemastore
atmos:
- # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line argument
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"
@@ -270,3 +270,16 @@ templates:
enabled: true
# https://docs.gomplate.ca/datasources
datasources: {}
+
+settings:
+ # `list_merge_strategy` specifies how lists are merged in Atmos stack manifests.
+ # Can also be set using 'ATMOS_SETTINGS_LIST_MERGE_STRATEGY' environment variable, or '--settings-list-merge-strategy' command-line argument
+ # The following strategies are supported:
+ # `replace`: Most recent list imported wins (the default behavior).
+ # `append`: The sequence of lists is appended in the same order as imports.
+ # `merge`: The items in the destination list are deep-merged with the items in the source list.
+ # The items in the source list take precedence.
+ # The items are processed starting from the first up to the length of the source list (the remaining items are not processed).
+ # If the source and destination lists have the same length, all items in the destination lists are
+ # deep-merged with all items in the source list.
+ list_merge_strategy: replace
diff --git a/examples/tests/atmos.yaml b/examples/tests/atmos.yaml
index c9348eb85..4335d6a3b 100644
--- a/examples/tests/atmos.yaml
+++ b/examples/tests/atmos.yaml
@@ -75,7 +75,7 @@ stacks:
name_template: "{{.vars.tenant}}-{{.vars.environment}}-{{.vars.stage}}"
workflows:
- # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line arguments
+ # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/workflows"
@@ -328,12 +328,12 @@ integrations:
schemas:
# https://json-schema.org
jsonschema:
- # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/jsonschema"
# https://www.openpolicyagent.org
opa:
- # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/opa"
# JSON Schema to validate Atmos manifests
@@ -345,7 +345,7 @@ schemas:
# https://www.schemastore.org/json
# https://github.com/SchemaStore/schemastore
atmos:
- # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line argument
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "../quick-start/stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"
@@ -378,3 +378,16 @@ templates:
timeout: 5
# https://docs.gomplate.ca/datasources
datasources: {}
+
+settings:
+ # `list_merge_strategy` specifies how lists are merged in Atmos stack manifests.
+ # Can also be set using 'ATMOS_SETTINGS_LIST_MERGE_STRATEGY' environment variable, or '--settings-list-merge-strategy' command-line argument
+ # The following strategies are supported:
+ # `replace`: Most recent list imported wins (the default behavior).
+ # `append`: The sequence of lists is appended in the same order as imports.
+ # `merge`: The items in the destination list are deep-merged with the items in the source list.
+ # The items in the source list take precedence.
+ # The items are processed starting from the first up to the length of the source list (the remaining items are not processed).
+ # If the source and destination lists have the same length, all items in the destination lists are
+ # deep-merged with all items in the source list.
+ list_merge_strategy: replace
diff --git a/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml b/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml
index 47dea4e14..99429c306 100644
--- a/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml
+++ b/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml
@@ -70,7 +70,7 @@ stacks:
name_template: "{{.vars.tenant}}-{{.vars.environment}}-{{.vars.stage}}"
workflows:
- # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line arguments
+ # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/workflows"
@@ -576,12 +576,12 @@ integrations:
schemas:
# https://json-schema.org
jsonschema:
- # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/jsonschema"
# https://www.openpolicyagent.org
opa:
- # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/opa"
# JSON Schema to validate Atmos manifests
@@ -593,7 +593,7 @@ schemas:
# https://www.schemastore.org/json
# https://github.com/SchemaStore/schemastore
atmos:
- # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line argument
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"
@@ -625,3 +625,16 @@ templates:
timeout: 5
# https://docs.gomplate.ca/datasources
datasources: {}
+
+settings:
+ # `list_merge_strategy` specifies how lists are merged in Atmos stack manifests.
+ # Can also be set using 'ATMOS_SETTINGS_LIST_MERGE_STRATEGY' environment variable, or '--settings-list-merge-strategy' command-line argument
+ # The following strategies are supported:
+ # `replace`: Most recent list imported wins (the default behavior).
+ # `append`: The sequence of lists is appended in the same order as imports.
+ # `merge`: The items in the destination list are deep-merged with the items in the source list.
+ # The items in the source list take precedence.
+ # The items are processed starting from the first up to the length of the source list (the remaining items are not processed).
+ # If the source and destination lists have the same length, all items in the destination lists are
+ # deep-merged with all items in the source list.
+ list_merge_strategy: replace
diff --git a/go.mod b/go.mod
index 9e1a95427..08157eaa5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/cloudposse/atmos
go 1.21
require (
+ dario.cat/mergo v1.0.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/chroma v0.10.0
github.com/arsham/figurine v1.3.0
@@ -21,7 +22,6 @@ require (
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/hcl/v2 v2.20.1
github.com/hashicorp/terraform-config-inspect v0.0.0-20240509232506-4708120f8f30
- github.com/imdario/mergo v0.3.13
github.com/ivanpirog/coloredcobra v1.0.1
github.com/json-iterator/go v1.1.12
github.com/jwalton/go-supportscolor v1.2.0
@@ -48,7 +48,6 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.6 // indirect
cloud.google.com/go/storage v1.36.0 // indirect
- dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
@@ -155,6 +154,7 @@ require (
github.com/hashicorp/vault/sdk v0.5.0 // indirect
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
+ github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
diff --git a/internal/exec/utils.go b/internal/exec/utils.go
index 0543ae64c..31475c000 100644
--- a/internal/exec/utils.go
+++ b/internal/exec/utils.go
@@ -216,6 +216,7 @@ func processCommandLineArgs(
configAndStacksInfo.RedirectStdErr = argsAndFlagsInfo.RedirectStdErr
configAndStacksInfo.LogsLevel = argsAndFlagsInfo.LogsLevel
configAndStacksInfo.LogsFile = argsAndFlagsInfo.LogsFile
+ configAndStacksInfo.SettingsListMergeStrategy = argsAndFlagsInfo.SettingsListMergeStrategy
// Check if `-h` or `--help` flags are specified
if argsAndFlagsInfo.NeedHelp {
@@ -885,6 +886,19 @@ func processArgsAndFlags(componentType string, inputArgsAndFlags []string) (sche
info.LogsFile = logsFileFlagParts[1]
}
+ if arg == cfg.SettingsListMergeStrategyFlag {
+ if len(inputArgsAndFlags) <= (i + 1) {
+ return info, fmt.Errorf("invalid flag: %s", arg)
+ }
+ info.SettingsListMergeStrategy = inputArgsAndFlags[i+1]
+ } else if strings.HasPrefix(arg+"=", cfg.SettingsListMergeStrategyFlag) {
+ var settingsListMergeStrategyParts = strings.Split(arg, "=")
+ if len(settingsListMergeStrategyParts) != 2 {
+ return info, fmt.Errorf("invalid flag: %s", arg)
+ }
+ info.SettingsListMergeStrategy = settingsListMergeStrategyParts[1]
+ }
+
if arg == cfg.FromPlanFlag {
info.UseTerraformPlan = true
}
diff --git a/pkg/config/const.go b/pkg/config/const.go
index 9cd5d1b3b..ba5569451 100644
--- a/pkg/config/const.go
+++ b/pkg/config/const.go
@@ -58,4 +58,6 @@ const (
LogsLevelFlag = "--logs-level"
LogsFileFlag = "--logs-file"
+
+ SettingsListMergeStrategyFlag = "--settings-list-merge-strategy"
)
diff --git a/pkg/config/utils.go b/pkg/config/utils.go
index 3070e6919..264a8f8dd 100644
--- a/pkg/config/utils.go
+++ b/pkg/config/utils.go
@@ -320,6 +320,12 @@ func processEnvVars(cliConfig *schema.CliConfiguration) error {
cliConfig.Logs.Level = logsLevel
}
+ listMergeStrategy := os.Getenv("ATMOS_SETTINGS_LIST_MERGE_STRATEGY")
+ if len(listMergeStrategy) > 0 {
+ u.LogTrace(*cliConfig, fmt.Sprintf("Found ENV var ATMOS_SETTINGS_LIST_MERGE_STRATEGY=%s", listMergeStrategy))
+ cliConfig.Settings.ListMergeStrategy = listMergeStrategy
+ }
+
return nil
}
@@ -416,6 +422,10 @@ func processCommandLineArgs(cliConfig *schema.CliConfiguration, configAndStacksI
cliConfig.Logs.File = configAndStacksInfo.LogsFile
u.LogTrace(*cliConfig, fmt.Sprintf("Using command line argument '%s=%s'", LogsFileFlag, configAndStacksInfo.LogsFile))
}
+ if len(configAndStacksInfo.SettingsListMergeStrategy) > 0 {
+ cliConfig.Settings.ListMergeStrategy = configAndStacksInfo.SettingsListMergeStrategy
+ u.LogTrace(*cliConfig, fmt.Sprintf("Using command line argument '%s=%s'", SettingsListMergeStrategyFlag, configAndStacksInfo.SettingsListMergeStrategy))
+ }
return nil
}
diff --git a/pkg/merge/merge.go b/pkg/merge/merge.go
index b4b530305..dceaf08e3 100644
--- a/pkg/merge/merge.go
+++ b/pkg/merge/merge.go
@@ -1,9 +1,19 @@
package merge
import (
+ "fmt"
+
+ "dario.cat/mergo"
"github.com/fatih/color"
- "github.com/imdario/mergo"
"gopkg.in/yaml.v2"
+
+ "github.com/cloudposse/atmos/pkg/schema"
+)
+
+const (
+ ListMergeStrategyReplace = "replace"
+ ListMergeStrategyAppend = "append"
+ ListMergeStrategyMerge = "merge"
)
// MergeWithOptions takes a list of maps and options as input, deep-merges the items in the order they are defined in the list,
@@ -45,12 +55,10 @@ func MergeWithOptions(inputs []map[any]any, appendSlice, sliceDeepCopy bool) (ma
// It was not working before in `github.com/imdario/mergo` so we need to disable it in our code
// opts = append(opts, mergo.WithOverwriteWithEmptyValue)
- if appendSlice {
- opts = append(opts, mergo.WithAppendSlice)
- }
-
if sliceDeepCopy {
opts = append(opts, mergo.WithSliceDeepCopy)
+ } else if appendSlice {
+ opts = append(opts, mergo.WithAppendSlice)
}
if err = mergo.Merge(&merged, dataCurrent, opts...); err != nil {
@@ -64,6 +72,31 @@ func MergeWithOptions(inputs []map[any]any, appendSlice, sliceDeepCopy bool) (ma
}
// Merge takes a list of maps as input, deep-merges the items in the order they are defined in the list, and returns a single map with the merged contents
-func Merge(inputs []map[any]any) (map[any]any, error) {
- return MergeWithOptions(inputs, false, false)
+func Merge(
+ cliConfig schema.CliConfiguration,
+ inputs []map[any]any,
+) (map[any]any, error) {
+ if cliConfig.Settings.ListMergeStrategy == "" {
+ cliConfig.Settings.ListMergeStrategy = ListMergeStrategyReplace
+ }
+
+ if cliConfig.Settings.ListMergeStrategy != ListMergeStrategyReplace &&
+ cliConfig.Settings.ListMergeStrategy != ListMergeStrategyAppend &&
+ cliConfig.Settings.ListMergeStrategy != ListMergeStrategyMerge {
+ return nil, fmt.Errorf("invalid Atmos manifests list merge strategy '%s'.\n"+
+ "Supported list merge strategies are: %s.",
+ cliConfig.Settings.ListMergeStrategy,
+ fmt.Sprintf("%s, %s, %s", ListMergeStrategyReplace, ListMergeStrategyAppend, ListMergeStrategyMerge))
+ }
+
+ sliceDeepCopy := false
+ appendSlice := false
+
+ if cliConfig.Settings.ListMergeStrategy == ListMergeStrategyMerge {
+ sliceDeepCopy = true
+ } else if cliConfig.Settings.ListMergeStrategy == ListMergeStrategyAppend {
+ appendSlice = true
+ }
+
+ return MergeWithOptions(inputs, appendSlice, sliceDeepCopy)
}
diff --git a/pkg/merge/merge_test.go b/pkg/merge/merge_test.go
index 12963e6dd..1aec7ef15 100644
--- a/pkg/merge/merge_test.go
+++ b/pkg/merge/merge_test.go
@@ -4,21 +4,28 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "gopkg.in/yaml.v2"
+
+ "github.com/cloudposse/atmos/pkg/schema"
)
func TestMergeBasic(t *testing.T) {
+ cliConfig := schema.CliConfiguration{}
+
map1 := map[any]any{"foo": "bar"}
map2 := map[any]any{"baz": "bat"}
inputs := []map[any]any{map1, map2}
expected := map[any]any{"foo": "bar", "baz": "bat"}
- result, err := Merge(inputs)
+ result, err := Merge(cliConfig, inputs)
assert.Nil(t, err)
assert.Equal(t, expected, result)
}
func TestMergeBasicOverride(t *testing.T) {
+ cliConfig := schema.CliConfiguration{}
+
map1 := map[any]any{"foo": "bar"}
map2 := map[any]any{"baz": "bat"}
map3 := map[any]any{"foo": "ood"}
@@ -26,7 +33,115 @@ func TestMergeBasicOverride(t *testing.T) {
inputs := []map[any]any{map1, map2, map3}
expected := map[any]any{"foo": "ood", "baz": "bat"}
- result, err := Merge(inputs)
+ result, err := Merge(cliConfig, inputs)
+ assert.Nil(t, err)
+ assert.Equal(t, expected, result)
+}
+
+func TestMergeListReplace(t *testing.T) {
+ cliConfig := schema.CliConfiguration{
+ Settings: schema.CliSettings{
+ ListMergeStrategy: ListMergeStrategyReplace,
+ },
+ }
+
+ map1 := map[any]any{
+ "list": []string{"1", "2", "3"},
+ }
+
+ map2 := map[any]any{
+ "list": []string{"4", "5", "6"},
+ }
+
+ inputs := []map[any]any{map1, map2}
+ expected := map[any]any{"list": []any{"4", "5", "6"}}
+
+ result, err := Merge(cliConfig, inputs)
assert.Nil(t, err)
assert.Equal(t, expected, result)
+
+ yamlConfig, err := yaml.Marshal(result)
+ assert.Nil(t, err)
+ t.Log(string(yamlConfig))
+}
+
+func TestMergeListAppend(t *testing.T) {
+ cliConfig := schema.CliConfiguration{
+ Settings: schema.CliSettings{
+ ListMergeStrategy: ListMergeStrategyAppend,
+ },
+ }
+
+ map1 := map[any]any{
+ "list": []string{"1", "2", "3"},
+ }
+
+ map2 := map[any]any{
+ "list": []string{"4", "5", "6"},
+ }
+
+ inputs := []map[any]any{map1, map2}
+ expected := map[any]any{"list": []any{"1", "2", "3", "4", "5", "6"}}
+
+ result, err := Merge(cliConfig, inputs)
+ assert.Nil(t, err)
+ assert.Equal(t, expected, result)
+
+ yamlConfig, err := yaml.Marshal(result)
+ assert.Nil(t, err)
+ t.Log(string(yamlConfig))
+}
+
+func TestMergeListMerge(t *testing.T) {
+ cliConfig := schema.CliConfiguration{
+ Settings: schema.CliSettings{
+ ListMergeStrategy: ListMergeStrategyMerge,
+ },
+ }
+
+ map1 := map[any]any{
+ "list": []map[string]string{
+ {
+ "1": "1",
+ "2": "2",
+ "3": "3",
+ "4": "4",
+ },
+ },
+ }
+
+ map2 := map[any]any{
+ "list": []map[string]string{
+ {
+ "1": "1b",
+ "2": "2",
+ "3": "3b",
+ "5": "5",
+ },
+ },
+ }
+
+ inputs := []map[any]any{map1, map2}
+
+ result, err := Merge(cliConfig, inputs)
+ assert.Nil(t, err)
+
+ var mergedList []any
+ var ok bool
+
+ if mergedList, ok = result["list"].([]any); !ok {
+ t.Errorf("invalid merge result: %v", result)
+ }
+
+ merged := mergedList[0].(map[any]any)
+
+ assert.Equal(t, "1b", merged["1"])
+ assert.Equal(t, "2", merged["2"])
+ assert.Equal(t, "3b", merged["3"])
+ assert.Equal(t, "4", merged["4"])
+ assert.Equal(t, "5", merged["5"])
+
+ yamlConfig, err := yaml.Marshal(result)
+ assert.Nil(t, err)
+ t.Log(string(yamlConfig))
}
diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go
index 63cfbc2be..a38f5f988 100644
--- a/pkg/schema/schema.go
+++ b/pkg/schema/schema.go
@@ -12,6 +12,7 @@ type CliConfiguration struct {
Integrations Integrations `yaml:"integrations,omitempty" json:"integrations,omitempty" mapstructure:"integrations"`
Schemas Schemas `yaml:"schemas,omitempty" json:"schemas,omitempty" mapstructure:"schemas"`
Templates Templates `yaml:"templates,omitempty" json:"templates,omitempty" mapstructure:"templates"`
+ Settings CliSettings `yaml:"settings,omitempty" json:"settings,omitempty" mapstructure:"settings"`
Initialized bool `yaml:"initialized" json:"initialized" mapstructure:"initialized"`
StacksBaseAbsolutePath string `yaml:"stacksBaseAbsolutePath,omitempty" json:"stacksBaseAbsolutePath,omitempty" mapstructure:"stacksBaseAbsolutePath"`
IncludeStackAbsolutePaths []string `yaml:"includeStackAbsolutePaths,omitempty" json:"includeStackAbsolutePaths,omitempty" mapstructure:"includeStackAbsolutePaths"`
@@ -23,6 +24,10 @@ type CliConfiguration struct {
StackType string `yaml:"stackType,omitempty" json:"StackType,omitempty" mapstructure:"stackType"`
}
+type CliSettings struct {
+ ListMergeStrategy string `yaml:"list_merge_strategy" json:"list_merge_strategy" mapstructure:"list_merge_strategy"`
+}
+
type Templates struct {
Settings TemplatesSettings `yaml:"settings" json:"settings" mapstructure:"settings"`
}
@@ -108,34 +113,35 @@ type Context struct {
}
type ArgsAndFlagsInfo struct {
- AdditionalArgsAndFlags []string
- SubCommand string
- SubCommand2 string
- ComponentFromArg string
- GlobalOptions []string
- TerraformCommand string
- TerraformDir string
- HelmfileCommand string
- HelmfileDir string
- ConfigDir string
- StacksDir string
- WorkflowsDir string
- BasePath string
- DeployRunInit string
- InitRunReconfigure string
- AutoGenerateBackendFile string
- UseTerraformPlan bool
- PlanFile string
- DryRun bool
- SkipInit bool
- NeedHelp bool
- JsonSchemaDir string
- OpaDir string
- CueDir string
- AtmosManifestJsonSchema string
- RedirectStdErr string
- LogsLevel string
- LogsFile string
+ AdditionalArgsAndFlags []string
+ SubCommand string
+ SubCommand2 string
+ ComponentFromArg string
+ GlobalOptions []string
+ TerraformCommand string
+ TerraformDir string
+ HelmfileCommand string
+ HelmfileDir string
+ ConfigDir string
+ StacksDir string
+ WorkflowsDir string
+ BasePath string
+ DeployRunInit string
+ InitRunReconfigure string
+ AutoGenerateBackendFile string
+ UseTerraformPlan bool
+ PlanFile string
+ DryRun bool
+ SkipInit bool
+ NeedHelp bool
+ JsonSchemaDir string
+ OpaDir string
+ CueDir string
+ AtmosManifestJsonSchema string
+ RedirectStdErr string
+ LogsLevel string
+ LogsFile string
+ SettingsListMergeStrategy string
}
type ConfigAndStacksInfo struct {
@@ -196,6 +202,7 @@ type ConfigAndStacksInfo struct {
RedirectStdErr string
LogsLevel string
LogsFile string
+ SettingsListMergeStrategy string
}
// Workflows
diff --git a/pkg/stack/stack_processor.go b/pkg/stack/stack_processor.go
index bec412a83..5e30c302b 100644
--- a/pkg/stack/stack_processor.go
+++ b/pkg/stack/stack_processor.go
@@ -292,7 +292,10 @@ func ProcessYAMLConfigFile(
}
}
- finalTerraformOverrides, err = m.Merge([]map[any]any{globalOverrides, terraformOverrides, parentTerraformOverrides})
+ finalTerraformOverrides, err = m.Merge(
+ cliConfig,
+ []map[any]any{globalOverrides, terraformOverrides, parentTerraformOverrides},
+ )
if err != nil {
return nil, nil, nil, err
}
@@ -310,7 +313,10 @@ func ProcessYAMLConfigFile(
}
}
- finalHelmfileOverrides, err = m.Merge([]map[any]any{globalOverrides, helmfileOverrides, parentHelmfileOverrides})
+ finalHelmfileOverrides, err = m.Merge(
+ cliConfig,
+ []map[any]any{globalOverrides, helmfileOverrides, parentHelmfileOverrides},
+ )
if err != nil {
return nil, nil, nil, err
}
@@ -411,7 +417,7 @@ func ProcessYAMLConfigFile(
// The parent `context` takes precedence over the current (imported) `context` and will override items with the same keys.
// TODO: instead of calling the conversion functions, we need to switch to generics and update everything to support it
listOfMaps := []map[any]any{c.MapsOfStringsToMapsOfInterfaces(importStruct.Context), c.MapsOfStringsToMapsOfInterfaces(context)}
- mergedContext, err := m.Merge(listOfMaps)
+ mergedContext, err := m.Merge(cliConfig, listOfMaps)
if err != nil {
return nil, nil, nil, err
}
@@ -452,7 +458,7 @@ func ProcessYAMLConfigFile(
}
// Deep-merge the stack manifest and all the imports
- stackConfigsDeepMerged, err := m.Merge(stackConfigs)
+ stackConfigsDeepMerged, err := m.Merge(cliConfig, stackConfigs)
if err != nil {
return nil, nil, nil, err
}
@@ -564,7 +570,7 @@ func ProcessStackConfig(
}
}
- globalAndTerraformVars, err := m.Merge([]map[any]any{globalVarsSection, terraformVars})
+ globalAndTerraformVars, err := m.Merge(cliConfig, []map[any]any{globalVarsSection, terraformVars})
if err != nil {
return nil, err
}
@@ -576,7 +582,7 @@ func ProcessStackConfig(
}
}
- globalAndTerraformSettings, err := m.Merge([]map[any]any{globalSettingsSection, terraformSettings})
+ globalAndTerraformSettings, err := m.Merge(cliConfig, []map[any]any{globalSettingsSection, terraformSettings})
if err != nil {
return nil, err
}
@@ -588,7 +594,7 @@ func ProcessStackConfig(
}
}
- globalAndTerraformEnv, err := m.Merge([]map[any]any{globalEnvSection, terraformEnv})
+ globalAndTerraformEnv, err := m.Merge(cliConfig, []map[any]any{globalEnvSection, terraformEnv})
if err != nil {
return nil, err
}
@@ -651,7 +657,7 @@ func ProcessStackConfig(
}
}
- globalAndHelmfileVars, err := m.Merge([]map[any]any{globalVarsSection, helmfileVars})
+ globalAndHelmfileVars, err := m.Merge(cliConfig, []map[any]any{globalVarsSection, helmfileVars})
if err != nil {
return nil, err
}
@@ -663,7 +669,7 @@ func ProcessStackConfig(
}
}
- globalAndHelmfileSettings, err := m.Merge([]map[any]any{globalSettingsSection, helmfileSettings})
+ globalAndHelmfileSettings, err := m.Merge(cliConfig, []map[any]any{globalSettingsSection, helmfileSettings})
if err != nil {
return nil, err
}
@@ -675,7 +681,7 @@ func ProcessStackConfig(
}
}
- globalAndHelmfileEnv, err := m.Merge([]map[any]any{globalEnvSection, helmfileEnv})
+ globalAndHelmfileEnv, err := m.Merge(cliConfig, []map[any]any{globalEnvSection, helmfileEnv})
if err != nil {
return nil, err
}
@@ -858,6 +864,7 @@ func ProcessStackConfig(
// Process the base components recursively to find `componentInheritanceChain`
err = ProcessBaseComponentConfig(
+ cliConfig,
&baseComponentConfig,
allTerraformComponentsMap,
component,
@@ -925,6 +932,7 @@ func ProcessStackConfig(
// Process the baseComponentFromInheritList components recursively to find `componentInheritanceChain`
err = ProcessBaseComponentConfig(
+ cliConfig,
&baseComponentConfig,
allTerraformComponentsMap,
component,
@@ -954,42 +962,50 @@ func ProcessStackConfig(
sort.Strings(baseComponents)
// Final configs
- finalComponentVars, err := m.Merge([]map[any]any{
- globalAndTerraformVars,
- baseComponentVars,
- componentVars,
- componentOverridesVars,
- })
+ finalComponentVars, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalAndTerraformVars,
+ baseComponentVars,
+ componentVars,
+ componentOverridesVars,
+ })
if err != nil {
return nil, err
}
- finalComponentSettings, err := m.Merge([]map[any]any{
- globalAndTerraformSettings,
- baseComponentSettings,
- componentSettings,
- componentOverridesSettings,
- })
+ finalComponentSettings, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalAndTerraformSettings,
+ baseComponentSettings,
+ componentSettings,
+ componentOverridesSettings,
+ })
if err != nil {
return nil, err
}
- finalComponentEnv, err := m.Merge([]map[any]any{
- globalAndTerraformEnv,
- baseComponentEnv,
- componentEnv,
- componentOverridesEnv,
- })
+ finalComponentEnv, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalAndTerraformEnv,
+ baseComponentEnv,
+ componentEnv,
+ componentOverridesEnv,
+ })
if err != nil {
return nil, err
}
- finalComponentProviders, err := m.Merge([]map[any]any{
- terraformProviders,
- baseComponentProviders,
- componentProviders,
- componentOverridesProviders,
- })
+ finalComponentProviders, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ terraformProviders,
+ baseComponentProviders,
+ componentProviders,
+ componentOverridesProviders,
+ })
if err != nil {
return nil, err
}
@@ -1003,11 +1019,13 @@ func ProcessStackConfig(
finalComponentBackendType = componentBackendType
}
- finalComponentBackendSection, err := m.Merge([]map[any]any{
- globalBackendSection,
- baseComponentBackendSection,
- componentBackendSection,
- })
+ finalComponentBackendSection, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalBackendSection,
+ baseComponentBackendSection,
+ componentBackendSection,
+ })
if err != nil {
return nil, err
}
@@ -1085,21 +1103,25 @@ func ProcessStackConfig(
finalComponentRemoteStateBackendType = componentRemoteStateBackendType
}
- finalComponentRemoteStateBackendSection, err := m.Merge([]map[any]any{
- globalRemoteStateBackendSection,
- baseComponentRemoteStateBackendSection,
- componentRemoteStateBackendSection,
- })
+ finalComponentRemoteStateBackendSection, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalRemoteStateBackendSection,
+ baseComponentRemoteStateBackendSection,
+ componentRemoteStateBackendSection,
+ })
if err != nil {
return nil, err
}
// Merge `backend` and `remote_state_backend` sections
// This will allow keeping `remote_state_backend` section DRY
- finalComponentRemoteStateBackendSectionMerged, err := m.Merge([]map[any]any{
- finalComponentBackendSection,
- finalComponentRemoteStateBackendSection,
- })
+ finalComponentRemoteStateBackendSectionMerged, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ finalComponentBackendSection,
+ finalComponentRemoteStateBackendSection,
+ })
if err != nil {
return nil, err
}
@@ -1296,6 +1318,7 @@ func ProcessStackConfig(
// Process the base components recursively to find `componentInheritanceChain`
err = ProcessBaseComponentConfig(
+ cliConfig,
&baseComponentConfig,
allHelmfileComponentsMap,
component,
@@ -1358,6 +1381,7 @@ func ProcessStackConfig(
// Process the baseComponentFromInheritList components recursively to find `componentInheritanceChain`
err = ProcessBaseComponentConfig(
+ cliConfig,
&baseComponentConfig,
allHelmfileComponentsMap,
component,
@@ -1384,32 +1408,38 @@ func ProcessStackConfig(
sort.Strings(baseComponents)
// Final configs
- finalComponentVars, err := m.Merge([]map[any]any{
- globalAndHelmfileVars,
- baseComponentVars,
- componentVars,
- componentOverridesVars,
- })
+ finalComponentVars, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalAndHelmfileVars,
+ baseComponentVars,
+ componentVars,
+ componentOverridesVars,
+ })
if err != nil {
return nil, err
}
- finalComponentSettings, err := m.Merge([]map[any]any{
- globalAndHelmfileSettings,
- baseComponentSettings,
- componentSettings,
- componentOverridesSettings,
- })
+ finalComponentSettings, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalAndHelmfileSettings,
+ baseComponentSettings,
+ componentSettings,
+ componentOverridesSettings,
+ })
if err != nil {
return nil, err
}
- finalComponentEnv, err := m.Merge([]map[any]any{
- globalAndHelmfileEnv,
- baseComponentEnv,
- componentEnv,
- componentOverridesEnv,
- })
+ finalComponentEnv, err := m.Merge(
+ cliConfig,
+ []map[any]any{
+ globalAndHelmfileEnv,
+ baseComponentEnv,
+ componentEnv,
+ componentOverridesEnv,
+ })
if err != nil {
return nil, err
}
diff --git a/pkg/stack/stack_processor_utils.go b/pkg/stack/stack_processor_utils.go
index 6920d7a85..b9b654fce 100644
--- a/pkg/stack/stack_processor_utils.go
+++ b/pkg/stack/stack_processor_utils.go
@@ -364,6 +364,7 @@ func getFileContent(filePath string) (string, error) {
// ProcessBaseComponentConfig processes base component(s) config
func ProcessBaseComponentConfig(
+ cliConfig schema.CliConfiguration,
baseComponentConfig *schema.BaseComponentConfig,
allComponentsMap map[any]any,
component string,
@@ -414,6 +415,7 @@ func ProcessBaseComponentConfig(
}
err := ProcessBaseComponentConfig(
+ cliConfig,
baseComponentConfig,
allComponentsMap,
baseComponent,
@@ -459,6 +461,7 @@ func ProcessBaseComponentConfig(
// Process the baseComponentFromInheritList components recursively to find `componentInheritanceChain`
err := ProcessBaseComponentConfig(
+ cliConfig,
baseComponentConfig,
allComponentsMap,
component,
@@ -546,28 +549,28 @@ func ProcessBaseComponentConfig(
}
// Base component `vars`
- merged, err := m.Merge([]map[any]any{baseComponentConfig.BaseComponentVars, baseComponentVars})
+ merged, err := m.Merge(cliConfig, []map[any]any{baseComponentConfig.BaseComponentVars, baseComponentVars})
if err != nil {
return err
}
baseComponentConfig.BaseComponentVars = merged
// Base component `settings`
- merged, err = m.Merge([]map[any]any{baseComponentConfig.BaseComponentSettings, baseComponentSettings})
+ merged, err = m.Merge(cliConfig, []map[any]any{baseComponentConfig.BaseComponentSettings, baseComponentSettings})
if err != nil {
return err
}
baseComponentConfig.BaseComponentSettings = merged
// Base component `env`
- merged, err = m.Merge([]map[any]any{baseComponentConfig.BaseComponentEnv, baseComponentEnv})
+ merged, err = m.Merge(cliConfig, []map[any]any{baseComponentConfig.BaseComponentEnv, baseComponentEnv})
if err != nil {
return err
}
baseComponentConfig.BaseComponentEnv = merged
// Base component `providers`
- merged, err = m.Merge([]map[any]any{baseComponentConfig.BaseComponentProviders, baseComponentProviders})
+ merged, err = m.Merge(cliConfig, []map[any]any{baseComponentConfig.BaseComponentProviders, baseComponentProviders})
if err != nil {
return err
}
@@ -580,7 +583,7 @@ func ProcessBaseComponentConfig(
baseComponentConfig.BaseComponentBackendType = baseComponentBackendType
// Base component `backend`
- merged, err = m.Merge([]map[any]any{baseComponentConfig.BaseComponentBackendSection, baseComponentBackendSection})
+ merged, err = m.Merge(cliConfig, []map[any]any{baseComponentConfig.BaseComponentBackendSection, baseComponentBackendSection})
if err != nil {
return err
}
@@ -590,7 +593,7 @@ func ProcessBaseComponentConfig(
baseComponentConfig.BaseComponentRemoteStateBackendType = baseComponentRemoteStateBackendType
// Base component `remote_state_backend`
- merged, err = m.Merge([]map[any]any{baseComponentConfig.BaseComponentRemoteStateBackendSection, baseComponentRemoteStateBackendSection})
+ merged, err = m.Merge(cliConfig, []map[any]any{baseComponentConfig.BaseComponentRemoteStateBackendSection, baseComponentRemoteStateBackendSection})
if err != nil {
return err
}
diff --git a/pkg/utils/template_utils.go b/pkg/utils/template_utils.go
index 6facf3538..4607b9f50 100644
--- a/pkg/utils/template_utils.go
+++ b/pkg/utils/template_utils.go
@@ -77,7 +77,7 @@ func ProcessTmplWithDatasources(
return "", err
}
- templateSettingsMerged, err := merge.Merge([]map[any]any{cliConfigTemplateSettingsMap, stackManifestTemplateSettingsMap})
+ templateSettingsMerged, err := merge.Merge(cliConfig, []map[any]any{cliConfigTemplateSettingsMap, stackManifestTemplateSettingsMap})
if err != nil {
return "", err
}
diff --git a/website/docs/cli/configuration.mdx b/website/docs/cli/configuration.mdx
index 46f802bc5..7e594a61c 100644
--- a/website/docs/cli/configuration.mdx
+++ b/website/docs/cli/configuration.mdx
@@ -134,6 +134,40 @@ If `base_path` is provided, `components.terraform.base_path`, `components.helmfi
base_path: "."
```
+## Settings
+
+The `settings` section configures Atmos global settings.
+
+
+ ```yaml
+ settings:
+ # `list_merge_strategy` specifies how lists are merged in Atmos stack manifests.
+ # Can also be set using 'ATMOS_SETTINGS_LIST_MERGE_STRATEGY' environment variable, or '--settings-list-merge-strategy' command-line argument
+ # The following strategies are supported:
+ # `replace`: Most recent list imported wins (the default behavior).
+ # `append`: The sequence of lists is appended in the same order as imports.
+ # `merge`: The items in the destination list are deep-merged with the items in the source list.
+ # The items in the source list take precedence.
+ # The items are processed starting from the first up to the length of the source list (the remaining items are not processed).
+ # If the source and destination lists have the same length, all items in the destination lists are
+ # deep-merged with all items in the source list.
+ list_merge_strategy: replace
+ ```
+
+
+- `settings.list_merge_strategy` specifies how lists are merged in Atmos stack manifests.
+ The following strategies are supported:
+
+ - `replace` - Most recent list imported wins (the default behavior).
+
+ - `append` - The sequence of lists is appended in the same order as imports.
+
+ - `merge` - The items in the destination list are deep-merged with the items in the source list.
+ The items in the source list take precedence.
+ The items are processed starting from the first up to the length of the source list (the remaining items are not processed).
+ If the source and destination lists have the same length, all items in the destination lists are
+ deep-merged with all items in the source list.
+
## Components
Specify the default behaviors for components.
@@ -341,7 +375,7 @@ Refer to [Atmos Design Patterns](/design-patterns) for the examples on how to co
```yaml
workflows:
- # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line arguments
+ # Can also be set using 'ATMOS_WORKFLOWS_BASE_PATH' ENV var, or '--workflows-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/workflows"
```
@@ -639,12 +673,12 @@ Configure the paths where to find OPA and JSON Schema files to validate Atmos st
schemas:
# https://json-schema.org
jsonschema:
- # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH' ENV var, or '--schemas-jsonschema-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/jsonschema"
# https://www.openpolicyagent.org
opa:
- # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_OPA_BASE_PATH' ENV var, or '--schemas-opa-dir' command-line argument
# Supports both absolute and relative paths
base_path: "stacks/schemas/opa"
# JSON Schema to validate Atmos manifests
@@ -654,7 +688,7 @@ schemas:
# https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json
# https://json-schema.org/draft/2020-12/release-notes
atmos:
- # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
+ # Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line argument
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"
```
@@ -1021,3 +1055,4 @@ setting `ATMOS_STACKS_BASE_PATH` to a path in `/localhost` to your local develop
| ATMOS_SCHEMAS_ATMOS_MANIFEST | schemas.atmos.manifest | Path to JSON Schema to validate Atmos stack manifests. For more details, refer to [Atmos Manifest JSON Schema](/reference/schemas) |
| ATMOS_LOGS_FILE | logs.file | The file to write Atmos logs to. Logs can be written to any file or any standard file descriptor, including `/dev/stdout`, `/dev/stderr` and `/dev/null`). If omitted, `/dev/stdout` will be used |
| ATMOS_LOGS_LEVEL | logs.level | Logs level. Supported log levels are `Trace`, `Debug`, `Info`, `Warning`, `Off`. If the log level is set to `Off`, Atmos will not log any messages (note that this does not prevent other tools like Terraform from logging) |
+| ATMOS_SETTINGS_LIST_MERGE_STRATEGY | settings.list_merge_strategy | Specifies how lists are merged in Atmos stack manifests. The following strategies are supported: `replace`, `append`, `merge` |
diff --git a/website/docs/core-concepts/components/terraform-providers.mdx b/website/docs/core-concepts/components/terraform-providers.mdx
index 5579440e4..92156333e 100644
--- a/website/docs/core-concepts/components/terraform-providers.mdx
+++ b/website/docs/core-concepts/components/terraform-providers.mdx
@@ -186,13 +186,13 @@ For example:
The above example uses a list of configuration blocks for the `aws` provider.
-Since it's a list, it doesn't currently work with deep-merging of stacks in the
-[inheritance](/core-concepts/components/inheritance) chain (list are not deep-merged, they are replaced).
+Since it's a list, by default it doesn't work with deep-merging of stacks in the
+[inheritance](/core-concepts/components/inheritance) chain since list are not deep-merged, they are replaced.
-This configuration will work for a single component.
+If you want to use the above configuration in the inheritance chain and allow appending or merging of lists, consider
+configuring the `settings.list_merge_strategy` in the `atmos.yaml` CLI config file.
-In the future Atmos releases, we'll consider an alternative syntax to describe multiple provider configurations using
-maps (maps support deep-merging at all scopes in the inheritance chain).
+For more details, refer to [Atmos CLI Settings](/cli/configuration#settings).
:::
diff --git a/website/docs/integrations/atlantis.mdx b/website/docs/integrations/atlantis.mdx
index eb58e1ff6..161526711 100644
--- a/website/docs/integrations/atlantis.mdx
+++ b/website/docs/integrations/atlantis.mdx
@@ -687,7 +687,7 @@ on:
branches: [ main ]
env:
- ATMOS_VERSION: 1.75.0
+ ATMOS_VERSION: 1.76.0
ATMOS_CLI_CONFIG_PATH: ./
jobs:
diff --git a/website/docs/integrations/github-actions/setup-atmos.md b/website/docs/integrations/github-actions/setup-atmos.md
index 593fc56df..da4e2fd82 100644
--- a/website/docs/integrations/github-actions/setup-atmos.md
+++ b/website/docs/integrations/github-actions/setup-atmos.md
@@ -27,5 +27,5 @@ jobs:
uses: cloudposse/github-action-setup-atmos
with:
# Make sure to pin to the latest version of atmos
- atmos_version: 1.75.0
+ atmos_version: 1.76.0
```