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 ```