From f7aa382aa8b3d48be8f06cfdb27aad344b89aff4 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Wed, 22 May 2024 14:29:18 -0400 Subject: [PATCH] Allow `Go` templates in `metadata.component` section. Add `components.terraform.command` section to `atmos.yaml`. Document OpenTofu support (#604) * updates * updates * updates * updates * updates * updates * updates * updates * updates * updates * updates * updates * updates * updates * updates * updates * Update website/docs/integrations/terraform.md * Update website/docs/cli/configuration.mdx Co-authored-by: Erik Osterman (CEO @ Cloud Posse) * updates --------- Co-authored-by: Erik Osterman (CEO @ Cloud Posse) --- atmos.yaml | 10 + examples/quick-start/Dockerfile | 4 +- examples/quick-start/atmos.yaml | 10 + .../rootfs/usr/local/etc/atmos/atmos.yaml | 10 + .../stacks/orgs/acme/_defaults.yaml | 1 + examples/tests/atmos.yaml | 10 + .../rootfs/usr/local/etc/atmos/atmos.yaml | 10 + examples/tests/stacks/orgs/cp/_defaults.yaml | 1 + go.mod | 4 +- go.sum | 8 +- .../exec/atlantis_generate_repo_config.go | 2 +- internal/exec/describe_affected_utils.go | 4 +- internal/exec/describe_stacks.go | 16 +- internal/exec/stack_utils.go | 6 +- internal/exec/terraform_generate_backends.go | 2 +- internal/exec/terraform_generate_varfiles.go | 2 +- internal/exec/utils.go | 177 +++++++++++------- internal/exec/validate_stacks.go | 1 + pkg/config/const.go | 4 + pkg/config/utils.go | 20 ++ pkg/schema/schema.go | 6 + pkg/spacelift/spacelift_stack_processor.go | 10 +- pkg/stack/stack_processor.go | 64 ++++--- pkg/stack/stack_processor_utils.go | 5 +- .../terraform/terraform-generate-varfiles.mdx | 1 + website/docs/cli/configuration.mdx | 21 +++ .../docs/core-concepts/stacks/templating.md | 3 +- website/docs/integrations/atlantis.mdx | 19 +- website/docs/integrations/aws.mdx | 2 +- .../github-actions/setup-atmos.md | 2 +- website/docs/integrations/helmfile.md | 2 +- website/docs/integrations/opentofu.mdx | 111 +++++++++++ website/docs/integrations/spacelift.md | 31 ++- website/docs/integrations/terraform.md | 55 +++--- website/static/img/opentofu-icon.svg | 61 ++++++ 35 files changed, 535 insertions(+), 160 deletions(-) create mode 100644 website/docs/integrations/opentofu.mdx create mode 100644 website/static/img/opentofu-icon.svg diff --git a/atmos.yaml b/atmos.yaml index a0302b74d..da699ccd7 100644 --- a/atmos.yaml +++ b/atmos.yaml @@ -19,6 +19,16 @@ base_path: "./examples/quick-start" components: terraform: + # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands + # If not defined, `terraform` is used + # Examples: + # command: terraform + # command: /usr/local/bin/terraform + # command: /usr/local/bin/terraform-1.8 + # command: tofu + # command: /usr/local/bin/tofu-1.7.1 + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_COMMAND' ENV var, or '--terraform-command' command-line argument + command: terraform # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument # Supports both absolute and relative paths base_path: "components/terraform" diff --git a/examples/quick-start/Dockerfile b/examples/quick-start/Dockerfile index dd587aa0d..a2b3cc97a 100644 --- a/examples/quick-start/Dockerfile +++ b/examples/quick-start/Dockerfile @@ -1,12 +1,12 @@ # Geodesic: https://github.com/cloudposse/geodesic/ -ARG GEODESIC_VERSION=2.9.6 +ARG GEODESIC_VERSION=2.11.2 ARG GEODESIC_OS=debian # Atmos # https://atmos.tools/ # https://github.com/cloudposse/atmos # https://github.com/cloudposse/atmos/releases -ARG ATMOS_VERSION=1.72.0 +ARG ATMOS_VERSION=1.73.0 # Terraform: https://github.com/hashicorp/terraform/releases ARG TF_VERSION=1.8.1 diff --git a/examples/quick-start/atmos.yaml b/examples/quick-start/atmos.yaml index a9d1b818a..2cfa41219 100644 --- a/examples/quick-start/atmos.yaml +++ b/examples/quick-start/atmos.yaml @@ -19,6 +19,16 @@ base_path: "." components: terraform: + # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands + # If not defined, `terraform` is used + # Examples: + # command: terraform + # command: /usr/local/bin/terraform + # command: /usr/local/bin/terraform-1.8 + # command: tofu + # command: /usr/local/bin/tofu-1.7.1 + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_COMMAND' ENV var, or '--terraform-command' command-line argument + command: terraform # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument # Supports both absolute and relative paths base_path: "components/terraform" 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 7f64a9e1c..22235091d 100644 --- a/examples/quick-start/rootfs/usr/local/etc/atmos/atmos.yaml +++ b/examples/quick-start/rootfs/usr/local/etc/atmos/atmos.yaml @@ -19,6 +19,16 @@ base_path: "" components: terraform: + # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands + # If not defined, `terraform` is used + # Examples: + # command: terraform + # command: /usr/local/bin/terraform + # command: /usr/local/bin/terraform-1.8 + # command: tofu + # command: /usr/local/bin/tofu-1.7.1 + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_COMMAND' ENV var, or '--terraform-command' command-line argument + command: terraform # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument # Supports both absolute and relative paths base_path: "components/terraform" diff --git a/examples/quick-start/stacks/orgs/acme/_defaults.yaml b/examples/quick-start/stacks/orgs/acme/_defaults.yaml index 49e5b3a58..e513fa021 100644 --- a/examples/quick-start/stacks/orgs/acme/_defaults.yaml +++ b/examples/quick-start/stacks/orgs/acme/_defaults.yaml @@ -21,6 +21,7 @@ terraform: atmos_stack: "{{ .atmos_stack }}" atmos_manifest: "{{ .atmos_stack_file }}" terraform_workspace: "{{ .workspace }}" + terraform_component: "{{ .component }}" # Examples of using the Sprig and Gomplate functions # https://masterminds.github.io/sprig/os.html provisioned_by_user: '{{ env "USER" }}' diff --git a/examples/tests/atmos.yaml b/examples/tests/atmos.yaml index 9bc73a1cd..6b26f31cc 100644 --- a/examples/tests/atmos.yaml +++ b/examples/tests/atmos.yaml @@ -19,6 +19,16 @@ base_path: "." components: terraform: + # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands + # If not defined, `terraform` is used + # Examples: + # command: terraform + # command: /usr/local/bin/terraform + # command: /usr/local/bin/terraform-1.8 + # command: tofu + # command: /usr/local/bin/tofu-1.7.1 + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_COMMAND' ENV var, or '--terraform-command' command-line argument + command: terraform # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument # Supports both absolute and relative paths base_path: "components/terraform" diff --git a/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml b/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml index e10ebd063..468777a74 100644 --- a/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml +++ b/examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml @@ -19,6 +19,16 @@ base_path: "" components: terraform: + # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands + # If not defined, `terraform` is used + # Examples: + # command: terraform + # command: /usr/local/bin/terraform + # command: /usr/local/bin/terraform-1.8 + # command: tofu + # command: /usr/local/bin/tofu-1.7.1 + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_COMMAND' ENV var, or '--terraform-command' command-line argument + command: terraform # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument # Supports both absolute and relative paths base_path: "components/terraform" diff --git a/examples/tests/stacks/orgs/cp/_defaults.yaml b/examples/tests/stacks/orgs/cp/_defaults.yaml index 12150f39a..77513a396 100644 --- a/examples/tests/stacks/orgs/cp/_defaults.yaml +++ b/examples/tests/stacks/orgs/cp/_defaults.yaml @@ -10,6 +10,7 @@ terraform: atmos_stack: "{{ .atmos_stack }}" atmos_manifest: "{{ .atmos_stack_file }}" terraform_workspace: "{{ .workspace }}" + terraform_component: "{{ .component }}" # Examples of using the Sprig and Gomplate functions # https://masterminds.github.io/sprig/os.html provisioned_by_user: '{{ env "USER" }}' diff --git a/go.mod b/go.mod index 0b71fb9b6..e85aca66f 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/charmbracelet/bubbletea v0.26.2 github.com/charmbracelet/lipgloss v0.10.0 github.com/elewis787/boa v0.1.2 - github.com/fatih/color v1.16.0 + github.com/fatih/color v1.17.0 github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-containerregistry v0.19.1 github.com/google/go-github/v59 v59.0.0 @@ -20,7 +20,7 @@ require ( github.com/hashicorp/go-getter v1.7.4 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.20.1 - github.com/hashicorp/terraform-config-inspect v0.0.0-20240507135902-21dcc2942448 + 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 diff --git a/go.sum b/go.sum index 85725dd98..a97874a78 100644 --- a/go.sum +++ b/go.sum @@ -492,8 +492,8 @@ github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2Vvl github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -793,8 +793,8 @@ github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/hashicorp/terraform-config-inspect v0.0.0-20240507135902-21dcc2942448 h1:rrpMPsKdq+AO9+dMOQDzTO1IBHYEGcYwF1/roaSDy2Y= -github.com/hashicorp/terraform-config-inspect v0.0.0-20240507135902-21dcc2942448/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg= +github.com/hashicorp/terraform-config-inspect v0.0.0-20240509232506-4708120f8f30 h1:0qwr2oZy9mIIJMWh7W9NTHLWGMbEF5KEQ+QqM9hym34= +github.com/hashicorp/terraform-config-inspect v0.0.0-20240509232506-4708120f8f30/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= github.com/hashicorp/vault/api v1.6.0 h1:B8UUYod1y1OoiGHq9GtpiqSnGOUEWHaA26AY8RQEDY4= github.com/hashicorp/vault/api v1.6.0/go.mod h1:h1K70EO2DgnBaTz5IsL6D5ERsNt5Pce93ueVS2+t0Xc= github.com/hashicorp/vault/sdk v0.5.0 h1:EED7p0OCU3OY5SAqJwSANofY1YKMytm+jDHDQ2EzGVQ= diff --git a/internal/exec/atlantis_generate_repo_config.go b/internal/exec/atlantis_generate_repo_config.go index 90424f7b7..08be730c5 100644 --- a/internal/exec/atlantis_generate_repo_config.go +++ b/internal/exec/atlantis_generate_repo_config.go @@ -308,7 +308,7 @@ func ExecuteAtlantisGenerateRepoConfig( // If 'component' attribute is present, it's the terraform component // Otherwise, the Atmos component name is the terraform component (by default) terraformComponent := componentName - if componentAttribute, ok := componentSection["component"].(string); ok { + if componentAttribute, ok := componentSection[cfg.ComponentSectionName].(string); ok { terraformComponent = componentAttribute } diff --git a/internal/exec/describe_affected_utils.go b/internal/exec/describe_affected_utils.go index c4f8a89f7..d0c2acca9 100644 --- a/internal/exec/describe_affected_utils.go +++ b/internal/exec/describe_affected_utils.go @@ -572,7 +572,7 @@ func findAffected( } // Check the Terraform configuration of the component - if component, ok := componentSection["component"].(string); ok && component != "" { + if component, ok := componentSection[cfg.ComponentSectionName].(string); ok && component != "" { // Check if the component uses some external modules (on the local filesystem) that have changed changed, err := areTerraformComponentModulesChanged(component, cliConfig, changedFiles) if err != nil { @@ -805,7 +805,7 @@ func findAffected( } // Check the Helmfile configuration of the component - if component, ok := componentSection["component"].(string); ok && component != "" { + if component, ok := componentSection[cfg.ComponentSectionName].(string); ok && component != "" { // Check if any files in the component's folder have changed changed, err := isComponentFolderChanged(component, "helmfile", cliConfig, changedFiles) if err != nil { diff --git a/internal/exec/describe_stacks.go b/internal/exec/describe_stacks.go index 32fd30e3a..bdc03a63d 100644 --- a/internal/exec/describe_stacks.go +++ b/internal/exec/describe_stacks.go @@ -135,8 +135,8 @@ func ExecuteDescribeStacks( return nil, fmt.Errorf("invalid 'components.terraform.%s' section in the file '%s'", componentName, stackFileName) } - if comp, ok := componentSection["component"].(string); !ok || comp == "" { - componentSection["component"] = componentName + if comp, ok := componentSection[cfg.ComponentSectionName].(string); !ok || comp == "" { + componentSection[cfg.ComponentSectionName] = componentName } // Find all derived components of the provided components and include them in the output @@ -200,8 +200,8 @@ func ExecuteDescribeStacks( }, } - if comp, ok := configAndStacksInfo.ComponentSection["component"].(string); !ok || comp == "" { - configAndStacksInfo.ComponentSection["component"] = componentName + if comp, ok := configAndStacksInfo.ComponentSection[cfg.ComponentSectionName].(string); !ok || comp == "" { + configAndStacksInfo.ComponentSection[cfg.ComponentSectionName] = componentName } // Stack name @@ -309,8 +309,8 @@ func ExecuteDescribeStacks( return nil, fmt.Errorf("invalid 'components.helmfile.%s' section in the file '%s'", componentName, stackFileName) } - if comp, ok := componentSection["component"].(string); !ok || comp == "" { - componentSection["component"] = componentName + if comp, ok := componentSection[cfg.ComponentSectionName].(string); !ok || comp == "" { + componentSection[cfg.ComponentSectionName] = componentName } // Find all derived components of the provided components and include them in the output @@ -374,8 +374,8 @@ func ExecuteDescribeStacks( }, } - if comp, ok := configAndStacksInfo.ComponentSection["component"].(string); !ok || comp == "" { - configAndStacksInfo.ComponentSection["component"] = componentName + if comp, ok := configAndStacksInfo.ComponentSection[cfg.ComponentSectionName].(string); !ok || comp == "" { + configAndStacksInfo.ComponentSection[cfg.ComponentSectionName] = componentName } // Stack name diff --git a/internal/exec/stack_utils.go b/internal/exec/stack_utils.go index 8b557e3f8..7cc48aa17 100644 --- a/internal/exec/stack_utils.go +++ b/internal/exec/stack_utils.go @@ -64,7 +64,7 @@ func ProcessComponentMetadata( var componentMetadata map[any]any // Find base component in the `component` attribute - if base, ok := componentSection["component"].(string); ok { + if base, ok := componentSection[cfg.ComponentSectionName].(string); ok { baseComponentName = base } @@ -77,7 +77,7 @@ func ProcessComponentMetadata( } // Find base component in the `metadata.component` attribute // `metadata.component` overrides `component` - if componentMetadataComponent, componentMetadataComponentExists := componentMetadata["component"].(string); componentMetadataComponentExists { + if componentMetadataComponent, componentMetadataComponentExists := componentMetadata[cfg.ComponentSectionName].(string); componentMetadataComponentExists { baseComponentName = componentMetadataComponent } } @@ -155,7 +155,7 @@ func BuildComponentPath( var componentPath string - if stackComponentSection, ok := componentSectionMap["component"].(string); ok { + if stackComponentSection, ok := componentSectionMap[cfg.ComponentSectionName].(string); ok { if componentType == "terraform" { componentPath = path.Join(cliConfig.BasePath, cliConfig.Components.Terraform.BasePath, stackComponentSection) } else if componentType == "helmfile" { diff --git a/internal/exec/terraform_generate_backends.go b/internal/exec/terraform_generate_backends.go index 464ae1c0b..755975b5e 100644 --- a/internal/exec/terraform_generate_backends.go +++ b/internal/exec/terraform_generate_backends.go @@ -123,7 +123,7 @@ func ExecuteTerraformGenerateBackends(cliConfig schema.CliConfiguration, fileTem // If `component` attribute is present, it's the terraform component. // Otherwise, the YAML component name is the terraform component. terraformComponent := componentName - if componentAttribute, ok := componentSection["component"].(string); ok { + if componentAttribute, ok := componentSection[cfg.ComponentSectionName].(string); ok { terraformComponent = componentAttribute } diff --git a/internal/exec/terraform_generate_varfiles.go b/internal/exec/terraform_generate_varfiles.go index fdfc47ce2..9135f98ea 100644 --- a/internal/exec/terraform_generate_varfiles.go +++ b/internal/exec/terraform_generate_varfiles.go @@ -114,7 +114,7 @@ func ExecuteTerraformGenerateVarfiles(cliConfig schema.CliConfiguration, fileTem // If `component` attribute is present, it's the terraform component. // Otherwise, the YAML component name is the terraform component. terraformComponent := componentName - if componentAttribute, ok := componentSection["component"].(string); ok { + if componentAttribute, ok := componentSection[cfg.ComponentSectionName].(string); ok { terraformComponent = componentAttribute } diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 57eace027..8fbf86e42 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -26,7 +26,9 @@ var ( cfg.DryRunFlag, cfg.SkipInitFlag, cfg.KubeConfigConfigFlag, + cfg.TerraformCommandFlag, cfg.TerraformDirFlag, + cfg.HelmfileCommandFlag, cfg.HelmfileDirFlag, cfg.CliConfigDirFlag, cfg.StackDirFlag, @@ -111,7 +113,7 @@ func ProcessComponentConfig( if componentImportsSection, ok = stackSection["imports"].([]string); !ok { componentImportsSection = nil } - if command, ok = componentSection["command"].(string); !ok { + if command, ok = componentSection[cfg.CommandSectionName].(string); !ok { command = "" } if componentEnvSection, ok = componentSection[cfg.EnvSectionName].(map[any]any); !ok { @@ -192,7 +194,9 @@ func processCommandLineArgs( configAndStacksInfo.ComponentFromArg = argsAndFlagsInfo.ComponentFromArg configAndStacksInfo.GlobalOptions = argsAndFlagsInfo.GlobalOptions configAndStacksInfo.BasePath = argsAndFlagsInfo.BasePath + configAndStacksInfo.TerraformCommand = argsAndFlagsInfo.TerraformCommand configAndStacksInfo.TerraformDir = argsAndFlagsInfo.TerraformDir + configAndStacksInfo.HelmfileCommand = argsAndFlagsInfo.HelmfileCommand configAndStacksInfo.HelmfileDir = argsAndFlagsInfo.HelmfileDir configAndStacksInfo.StacksDir = argsAndFlagsInfo.StacksDir configAndStacksInfo.ConfigDir = argsAndFlagsInfo.ConfigDir @@ -415,54 +419,6 @@ func ProcessStacks( } } - if len(configAndStacksInfo.Command) == 0 { - configAndStacksInfo.Command = configAndStacksInfo.ComponentType - } - - // Process component path and name - configAndStacksInfo.ComponentFolderPrefix = "" - componentPathParts := strings.Split(configAndStacksInfo.ComponentFromArg, "/") - componentPathPartsLength := len(componentPathParts) - if componentPathPartsLength > 1 { - componentFromArgPartsWithoutLast := componentPathParts[:componentPathPartsLength-1] - configAndStacksInfo.ComponentFolderPrefix = strings.Join(componentFromArgPartsWithoutLast, "/") - configAndStacksInfo.Component = componentPathParts[componentPathPartsLength-1] - } else { - configAndStacksInfo.Component = configAndStacksInfo.ComponentFromArg - } - configAndStacksInfo.ComponentFolderPrefixReplaced = strings.Replace(configAndStacksInfo.ComponentFolderPrefix, "/", "-", -1) - - // Process base component path and name - if len(configAndStacksInfo.BaseComponentPath) > 0 { - baseComponentPathParts := strings.Split(configAndStacksInfo.BaseComponentPath, "/") - baseComponentPathPartsLength := len(baseComponentPathParts) - if baseComponentPathPartsLength > 1 { - baseComponentPartsWithoutLast := baseComponentPathParts[:baseComponentPathPartsLength-1] - configAndStacksInfo.ComponentFolderPrefix = strings.Join(baseComponentPartsWithoutLast, "/") - configAndStacksInfo.BaseComponent = baseComponentPathParts[baseComponentPathPartsLength-1] - } else { - configAndStacksInfo.ComponentFolderPrefix = "" - configAndStacksInfo.BaseComponent = configAndStacksInfo.BaseComponentPath - } - configAndStacksInfo.ComponentFolderPrefixReplaced = strings.Replace(configAndStacksInfo.ComponentFolderPrefix, "/", "-", -1) - } - - // Get the final component - if len(configAndStacksInfo.BaseComponent) > 0 { - configAndStacksInfo.FinalComponent = configAndStacksInfo.BaseComponent - } else { - configAndStacksInfo.FinalComponent = configAndStacksInfo.Component - } - - // Terraform workspace - workspace, err := BuildTerraformWorkspace(cliConfig, configAndStacksInfo) - if err != nil { - return configAndStacksInfo, err - } - - configAndStacksInfo.TerraformWorkspace = workspace - configAndStacksInfo.ComponentSection["workspace"] = workspace - // Add imports configAndStacksInfo.ComponentSection["imports"] = configAndStacksInfo.ComponentImportsSection @@ -480,26 +436,8 @@ func ProcessStacks( configAndStacksInfo.ComponentSection["atmos_cli_config"] = atmosCliConfig // If the command-line component does not inherit anything, then the Terraform/Helmfile component is the same as the provided one - if comp, ok := configAndStacksInfo.ComponentSection["component"].(string); !ok || comp == "" { - configAndStacksInfo.ComponentSection["component"] = configAndStacksInfo.ComponentFromArg - } - - // Spacelift stack - spaceliftStackName, err := BuildSpaceliftStackNameFromComponentConfig(cliConfig, configAndStacksInfo) - if err != nil { - return configAndStacksInfo, err - } - if spaceliftStackName != "" { - configAndStacksInfo.ComponentSection["spacelift_stack"] = spaceliftStackName - } - - // Atlantis project - atlantisProjectName, err := BuildAtlantisProjectNameFromComponentConfig(cliConfig, configAndStacksInfo) - if err != nil { - return configAndStacksInfo, err - } - if atlantisProjectName != "" { - configAndStacksInfo.ComponentSection["atlantis_project"] = atlantisProjectName + if comp, ok := configAndStacksInfo.ComponentSection[cfg.ComponentSectionName].(string); !ok || comp == "" { + configAndStacksInfo.ComponentSection[cfg.ComponentSectionName] = configAndStacksInfo.ComponentFromArg } // Add component info, including Terraform config @@ -533,6 +471,15 @@ func ProcessStacks( configAndStacksInfo.ComponentSection["deps"] = componentDeps configAndStacksInfo.ComponentSection["deps_all"] = componentDepsAll + // Terraform workspace + workspace, err := BuildTerraformWorkspace(cliConfig, configAndStacksInfo) + if err != nil { + return configAndStacksInfo, err + } + + configAndStacksInfo.TerraformWorkspace = workspace + configAndStacksInfo.ComponentSection["workspace"] = workspace + // Process `Go` templates in Atmos manifest sections componentSectionStr, err := u.ConvertToYAML(configAndStacksInfo.ComponentSection) if err != nil { @@ -599,9 +546,75 @@ func ProcessStacks( configAndStacksInfo.ComponentBackendType = i } + if i, ok := configAndStacksInfo.ComponentSection[cfg.ComponentSectionName].(string); ok { + configAndStacksInfo.Component = i + } + + // Spacelift stack + spaceliftStackName, err := BuildSpaceliftStackNameFromComponentConfig(cliConfig, configAndStacksInfo) + if err != nil { + return configAndStacksInfo, err + } + if spaceliftStackName != "" { + configAndStacksInfo.ComponentSection["spacelift_stack"] = spaceliftStackName + } + + // Atlantis project + atlantisProjectName, err := BuildAtlantisProjectNameFromComponentConfig(cliConfig, configAndStacksInfo) + if err != nil { + return configAndStacksInfo, err + } + if atlantisProjectName != "" { + configAndStacksInfo.ComponentSection["atlantis_project"] = atlantisProjectName + } + + // Process `command` + //if len(configAndStacksInfo.Command) == 0 { + // configAndStacksInfo.Command = configAndStacksInfo.ComponentType + //} + // Process the ENV variables from the `env` section configAndStacksInfo.ComponentEnvList = u.ConvertEnvVars(configAndStacksInfo.ComponentEnvSection) + // Process component metadata + _, baseComponentName, _ := ProcessComponentMetadata(configAndStacksInfo.ComponentFromArg, configAndStacksInfo.ComponentSection) + configAndStacksInfo.BaseComponentPath = baseComponentName + + // Process component path and name + configAndStacksInfo.ComponentFolderPrefix = "" + componentPathParts := strings.Split(configAndStacksInfo.ComponentFromArg, "/") + componentPathPartsLength := len(componentPathParts) + if componentPathPartsLength > 1 { + componentFromArgPartsWithoutLast := componentPathParts[:componentPathPartsLength-1] + configAndStacksInfo.ComponentFolderPrefix = strings.Join(componentFromArgPartsWithoutLast, "/") + configAndStacksInfo.Component = componentPathParts[componentPathPartsLength-1] + } else { + configAndStacksInfo.Component = configAndStacksInfo.ComponentFromArg + } + configAndStacksInfo.ComponentFolderPrefixReplaced = strings.Replace(configAndStacksInfo.ComponentFolderPrefix, "/", "-", -1) + + // Process base component path and name + if len(configAndStacksInfo.BaseComponentPath) > 0 { + baseComponentPathParts := strings.Split(configAndStacksInfo.BaseComponentPath, "/") + baseComponentPathPartsLength := len(baseComponentPathParts) + if baseComponentPathPartsLength > 1 { + baseComponentPartsWithoutLast := baseComponentPathParts[:baseComponentPathPartsLength-1] + configAndStacksInfo.ComponentFolderPrefix = strings.Join(baseComponentPartsWithoutLast, "/") + configAndStacksInfo.BaseComponent = baseComponentPathParts[baseComponentPathPartsLength-1] + } else { + configAndStacksInfo.ComponentFolderPrefix = "" + configAndStacksInfo.BaseComponent = configAndStacksInfo.BaseComponentPath + } + configAndStacksInfo.ComponentFolderPrefixReplaced = strings.Replace(configAndStacksInfo.ComponentFolderPrefix, "/", "-", -1) + } + + // Get the final component + if len(configAndStacksInfo.BaseComponent) > 0 { + configAndStacksInfo.FinalComponent = configAndStacksInfo.BaseComponent + } else { + configAndStacksInfo.FinalComponent = configAndStacksInfo.Component + } + return configAndStacksInfo, nil } @@ -623,6 +636,19 @@ func processArgsAndFlags(componentType string, inputArgsAndFlags []string) (sche globalOptionsFlagIndex = i } + if arg == cfg.TerraformCommandFlag { + if len(inputArgsAndFlags) <= (i + 1) { + return info, fmt.Errorf("invalid flag: %s", arg) + } + info.TerraformCommand = inputArgsAndFlags[i+1] + } else if strings.HasPrefix(arg+"=", cfg.TerraformCommandFlag) { + var terraformCommandFlagParts = strings.Split(arg, "=") + if len(terraformCommandFlagParts) != 2 { + return info, fmt.Errorf("invalid flag: %s", arg) + } + info.TerraformCommand = terraformCommandFlagParts[1] + } + if arg == cfg.TerraformDirFlag { if len(inputArgsAndFlags) <= (i + 1) { return info, fmt.Errorf("invalid flag: %s", arg) @@ -636,6 +662,19 @@ func processArgsAndFlags(componentType string, inputArgsAndFlags []string) (sche info.TerraformDir = terraformDirFlagParts[1] } + if arg == cfg.HelmfileCommandFlag { + if len(inputArgsAndFlags) <= (i + 1) { + return info, fmt.Errorf("invalid flag: %s", arg) + } + info.HelmfileCommand = inputArgsAndFlags[i+1] + } else if strings.HasPrefix(arg+"=", cfg.HelmfileCommandFlag) { + var helmfileCommandFlagParts = strings.Split(arg, "=") + if len(helmfileCommandFlagParts) != 2 { + return info, fmt.Errorf("invalid flag: %s", arg) + } + info.HelmfileCommand = helmfileCommandFlagParts[1] + } + if arg == cfg.HelmfileDirFlag { if len(inputArgsAndFlags) <= (i + 1) { return info, fmt.Errorf("invalid flag: %s", arg) diff --git a/internal/exec/validate_stacks.go b/internal/exec/validate_stacks.go index 61447204f..262ce06f3 100644 --- a/internal/exec/validate_stacks.go +++ b/internal/exec/validate_stacks.go @@ -97,6 +97,7 @@ func ExecuteValidateStacksCmd(cmd *cobra.Command, args []string) error { // Process and validate the stack manifest componentStackMap := map[string]map[string][]string{} _, err = s.ProcessStackConfig( + cliConfig, cliConfig.StacksBaseAbsolutePath, cliConfig.TerraformDirAbsolutePath, cliConfig.HelmfileDirAbsolutePath, diff --git a/pkg/config/const.go b/pkg/config/const.go index 084330bc3..c2eaae2aa 100644 --- a/pkg/config/const.go +++ b/pkg/config/const.go @@ -11,7 +11,9 @@ const ( // https://github.com/roboll/helmfile#cli-reference GlobalOptionsFlag = "--global-options" + TerraformCommandFlag = "--terraform-command" TerraformDirFlag = "--terraform-dir" + HelmfileCommandFlag = "--helmfile-command" HelmfileDirFlag = "--helmfile-dir" CliConfigDirFlag = "--config-dir" StackDirFlag = "--stacks-dir" @@ -48,6 +50,8 @@ const ( BackendSectionName = "backend" BackendTypeSectionName = "backend_type" MetadataSectionName = "metadata" + ComponentSectionName = "component" + CommandSectionName = "command" LogsLevelFlag = "--logs-level" LogsFileFlag = "--logs-file" diff --git a/pkg/config/utils.go b/pkg/config/utils.go index db9daa142..3070e6919 100644 --- a/pkg/config/utils.go +++ b/pkg/config/utils.go @@ -186,6 +186,12 @@ func processEnvVars(cliConfig *schema.CliConfiguration) error { cliConfig.Stacks.NameTemplate = stacksNameTemplate } + componentsTerraformCommand := os.Getenv("ATMOS_COMPONENTS_TERRAFORM_COMMAND") + if len(componentsTerraformCommand) > 0 { + u.LogTrace(*cliConfig, fmt.Sprintf("Found ENV var ATMOS_COMPONENTS_TERRAFORM_COMMAND=%s", componentsTerraformCommand)) + cliConfig.Components.Terraform.Command = componentsTerraformCommand + } + componentsTerraformBasePath := os.Getenv("ATMOS_COMPONENTS_TERRAFORM_BASE_PATH") if len(componentsTerraformBasePath) > 0 { u.LogTrace(*cliConfig, fmt.Sprintf("Found ENV var ATMOS_COMPONENTS_TERRAFORM_BASE_PATH=%s", componentsTerraformBasePath)) @@ -232,6 +238,12 @@ func processEnvVars(cliConfig *schema.CliConfiguration) error { cliConfig.Components.Terraform.AutoGenerateBackendFile = componentsTerraformAutoGenerateBackendFileBool } + componentsHelmfileCommand := os.Getenv("ATMOS_COMPONENTS_HELMFILE_COMMAND") + if len(componentsHelmfileCommand) > 0 { + u.LogTrace(*cliConfig, fmt.Sprintf("Found ENV var ATMOS_COMPONENTS_HELMFILE_COMMAND=%s", componentsHelmfileCommand)) + cliConfig.Components.Helmfile.Command = componentsHelmfileCommand + } + componentsHelmfileBasePath := os.Getenv("ATMOS_COMPONENTS_HELMFILE_BASE_PATH") if len(componentsHelmfileBasePath) > 0 { u.LogTrace(*cliConfig, fmt.Sprintf("Found ENV var ATMOS_COMPONENTS_HELMFILE_BASE_PATH=%s", componentsHelmfileBasePath)) @@ -328,10 +340,18 @@ func processCommandLineArgs(cliConfig *schema.CliConfiguration, configAndStacksI cliConfig.BasePath = configAndStacksInfo.BasePath u.LogTrace(*cliConfig, fmt.Sprintf("Using command line argument '%s' as base path for stacks and components", configAndStacksInfo.BasePath)) } + if len(configAndStacksInfo.TerraformCommand) > 0 { + cliConfig.Components.Terraform.Command = configAndStacksInfo.TerraformCommand + u.LogTrace(*cliConfig, fmt.Sprintf("Using command line argument '%s' as terraform executable", configAndStacksInfo.TerraformCommand)) + } if len(configAndStacksInfo.TerraformDir) > 0 { cliConfig.Components.Terraform.BasePath = configAndStacksInfo.TerraformDir u.LogTrace(*cliConfig, fmt.Sprintf("Using command line argument '%s' as terraform directory", configAndStacksInfo.TerraformDir)) } + if len(configAndStacksInfo.HelmfileCommand) > 0 { + cliConfig.Components.Helmfile.Command = configAndStacksInfo.HelmfileCommand + u.LogTrace(*cliConfig, fmt.Sprintf("Using command line argument '%s' as helmfile executable", configAndStacksInfo.HelmfileCommand)) + } if len(configAndStacksInfo.HelmfileDir) > 0 { cliConfig.Components.Helmfile.BasePath = configAndStacksInfo.HelmfileDir u.LogTrace(*cliConfig, fmt.Sprintf("Using command line argument '%s' as helmfile directory", configAndStacksInfo.HelmfileDir)) diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index af38277e4..63cfbc2be 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -57,6 +57,7 @@ type Terraform struct { DeployRunInit bool `yaml:"deploy_run_init" json:"deploy_run_init" mapstructure:"deploy_run_init"` InitRunReconfigure bool `yaml:"init_run_reconfigure" json:"init_run_reconfigure" mapstructure:"init_run_reconfigure"` AutoGenerateBackendFile bool `yaml:"auto_generate_backend_file" json:"auto_generate_backend_file" mapstructure:"auto_generate_backend_file"` + Command string `yaml:"command" json:"command" mapstructure:"command"` } type Helmfile struct { @@ -65,6 +66,7 @@ type Helmfile struct { KubeconfigPath string `yaml:"kubeconfig_path" json:"kubeconfig_path" mapstructure:"kubeconfig_path"` HelmAwsProfilePattern string `yaml:"helm_aws_profile_pattern" json:"helm_aws_profile_pattern" mapstructure:"helm_aws_profile_pattern"` ClusterNamePattern string `yaml:"cluster_name_pattern" json:"cluster_name_pattern" mapstructure:"cluster_name_pattern"` + Command string `yaml:"command" json:"command" mapstructure:"command"` } type Components struct { @@ -111,7 +113,9 @@ type ArgsAndFlagsInfo struct { SubCommand2 string ComponentFromArg string GlobalOptions []string + TerraformCommand string TerraformDir string + HelmfileCommand string HelmfileDir string ConfigDir string StacksDir string @@ -161,7 +165,9 @@ type ConfigAndStacksInfo struct { AdditionalArgsAndFlags []string GlobalOptions []string BasePath string + TerraformCommand string TerraformDir string + HelmfileCommand string HelmfileDir string ConfigDir string StacksDir string diff --git a/pkg/spacelift/spacelift_stack_processor.go b/pkg/spacelift/spacelift_stack_processor.go index 6c6ddb6a4..a7e2c8351 100644 --- a/pkg/spacelift/spacelift_stack_processor.go +++ b/pkg/spacelift/spacelift_stack_processor.go @@ -194,16 +194,16 @@ func TransformStackConfigToSpaceliftStacks( contextPrefix = strings.Replace(stackName, "/", "-", -1) } - spaceliftConfig["component"] = component + spaceliftConfig[cfg.ComponentSectionName] = component spaceliftConfig["stack"] = contextPrefix spaceliftConfig["imports"] = imports - spaceliftConfig["vars"] = componentVars - spaceliftConfig["settings"] = componentSettings - spaceliftConfig["env"] = componentEnv + spaceliftConfig[cfg.VarsSectionName] = componentVars + spaceliftConfig[cfg.SettingsSectionName] = componentSettings + spaceliftConfig[cfg.EnvSectionName] = componentEnv spaceliftConfig["stacks"] = componentStacks spaceliftConfig["inheritance"] = componentInheritance spaceliftConfig["base_component"] = baseComponentName - spaceliftConfig["metadata"] = componentMetadata + spaceliftConfig[cfg.MetadataSectionName] = componentMetadata // backend backendTypeName := "" diff --git a/pkg/stack/stack_processor.go b/pkg/stack/stack_processor.go index d61795f28..dac382780 100644 --- a/pkg/stack/stack_processor.go +++ b/pkg/stack/stack_processor.go @@ -98,6 +98,7 @@ func ProcessYAMLConfigFiles( componentStackMap := map[string]map[string][]string{} finalConfig, err := ProcessStackConfig( + cliConfig, stackBasePath, terraformComponentsBasePath, helmfileComponentsBasePath, @@ -462,6 +463,7 @@ func ProcessYAMLConfigFile( // ProcessStackConfig takes a stack manifest, deep-merges all variables, settings, environments and backends, // and returns the final stack configuration for all Terraform and helmfile components func ProcessStackConfig( + cliConfig schema.CliConfiguration, stacksBasePath string, terraformComponentsBasePath string, helmfileComponentsBasePath string, @@ -548,7 +550,7 @@ func ProcessStackConfig( } // Terraform section - if i, ok := globalTerraformSection["command"]; ok { + if i, ok := globalTerraformSection[cfg.CommandSectionName]; ok { terraformCommand, ok = i.(string) if !ok { return nil, fmt.Errorf("invalid 'terraform.command' section in the file '%s'", stackName) @@ -635,7 +637,7 @@ func ProcessStackConfig( } // Helmfile section - if i, ok := globalHelmfileSection["command"]; ok { + if i, ok := globalHelmfileSection[cfg.CommandSectionName]; ok { helmfileCommand, ok = i.(string) if !ok { return nil, fmt.Errorf("invalid 'helmfile.command' section in the file '%s'", stackName) @@ -781,7 +783,7 @@ func ProcessStackConfig( } componentTerraformCommand := "" - if i, ok := componentMap["command"]; ok { + if i, ok := componentMap[cfg.CommandSectionName]; ok { componentTerraformCommand, ok = i.(string) if !ok { return nil, fmt.Errorf("invalid 'components.terraform.%s.command' attribute in the file '%s'", component, stackName) @@ -819,7 +821,7 @@ func ProcessStackConfig( } } - if i, ok = componentOverrides["command"]; ok { + if i, ok = componentOverrides[cfg.CommandSectionName]; ok { if componentOverridesTerraformCommand, ok = i.(string); !ok { return nil, fmt.Errorf("invalid 'components.terraform.%s.overrides.command' in the manifest '%s'", component, stackName) } @@ -848,7 +850,7 @@ func ProcessStackConfig( var baseComponents []string // Inheritance using the top-level `component` attribute - if baseComponent, baseComponentExist := componentMap["component"]; baseComponentExist { + if baseComponent, baseComponentExist := componentMap[cfg.ComponentSectionName]; baseComponentExist { baseComponentName, ok = baseComponent.(string) if !ok { return nil, fmt.Errorf("invalid 'components.terraform.%s.component' attribute in the file '%s'", component, stackName) @@ -893,7 +895,7 @@ func ProcessStackConfig( // will deep-merge all the base components of `componentA` (each component overriding its base), // then all the base components of `componentB` (each component overriding its base), // then the two results are deep-merged together (`componentB` inheritance chain will override values from 'componentA' inheritance chain). - if baseComponentFromMetadata, baseComponentFromMetadataExist := componentMetadata["component"]; baseComponentFromMetadataExist { + if baseComponentFromMetadata, baseComponentFromMetadataExist := componentMetadata[cfg.ComponentSectionName]; baseComponentFromMetadataExist { baseComponentName, ok = baseComponentFromMetadata.(string) if !ok { return nil, fmt.Errorf("invalid 'components.terraform.%s.metadata.component' attribute in the file '%s'", component, stackName) @@ -1111,17 +1113,26 @@ func ProcessStackConfig( } // Final binary to execute + // Check for the binary in the following order: + // - `components.terraform.command` section in `atmos.yaml` CLI config file + // - global `terraform.command` section + // - base component(s) `command` section + // - component `command` section + // - `overrides.command` section finalComponentTerraformCommand := "terraform" - if len(terraformCommand) > 0 { + if cliConfig.Components.Terraform.Command != "" { + finalComponentTerraformCommand = cliConfig.Components.Terraform.Command + } + if terraformCommand != "" { finalComponentTerraformCommand = terraformCommand } - if len(baseComponentTerraformCommand) > 0 { + if baseComponentTerraformCommand != "" { finalComponentTerraformCommand = baseComponentTerraformCommand } - if len(componentTerraformCommand) > 0 { + if componentTerraformCommand != "" { finalComponentTerraformCommand = componentTerraformCommand } - if len(componentOverridesTerraformCommand) > 0 { + if componentOverridesTerraformCommand != "" { finalComponentTerraformCommand = componentOverridesTerraformCommand } @@ -1155,14 +1166,14 @@ func ProcessStackConfig( comp[cfg.BackendSectionName] = finalComponentBackend comp["remote_state_backend_type"] = finalComponentRemoteStateBackendType comp["remote_state_backend"] = finalComponentRemoteStateBackend - comp["command"] = finalComponentTerraformCommand + comp[cfg.CommandSectionName] = finalComponentTerraformCommand comp["inheritance"] = componentInheritanceChain comp[cfg.MetadataSectionName] = componentMetadata comp[cfg.OverridesSectionName] = componentOverrides comp[cfg.ProvidersSectionName] = finalComponentProviders if baseComponentName != "" { - comp["component"] = baseComponentName + comp[cfg.ComponentSectionName] = baseComponentName } terraformComponents[component] = comp @@ -1222,7 +1233,7 @@ func ProcessStackConfig( } componentHelmfileCommand := "" - if i, ok := componentMap["command"]; ok { + if i, ok := componentMap[cfg.CommandSectionName]; ok { componentHelmfileCommand, ok = i.(string) if !ok { return nil, fmt.Errorf("invalid 'components.helmfile.%s.command' attribute in the file '%s'", component, stackName) @@ -1259,7 +1270,7 @@ func ProcessStackConfig( } } - if i, ok = componentOverrides["command"]; ok { + if i, ok = componentOverrides[cfg.CommandSectionName]; ok { if componentOverridesHelmfileCommand, ok = i.(string); !ok { return nil, fmt.Errorf("invalid 'components.helmfile.%s.overrides.command' in the manifest '%s'", component, stackName) } @@ -1277,7 +1288,7 @@ func ProcessStackConfig( var baseComponents []string // Inheritance using the top-level `component` attribute - if baseComponent, baseComponentExist := componentMap["component"]; baseComponentExist { + if baseComponent, baseComponentExist := componentMap[cfg.ComponentSectionName]; baseComponentExist { baseComponentName, ok = baseComponent.(string) if !ok { return nil, fmt.Errorf("invalid 'components.helmfile.%s.component' attribute in the file '%s'", component, stackName) @@ -1317,7 +1328,7 @@ func ProcessStackConfig( // will deep-merge all the base components of `componentA` (each component overriding its base), // then all the base components of `componentB` (each component overriding its base), // then the two results are deep-merged together (`componentB` inheritance chain will override values from 'componentA' inheritance chain). - if baseComponentFromMetadata, baseComponentFromMetadataExist := componentMetadata["component"]; baseComponentFromMetadataExist { + if baseComponentFromMetadata, baseComponentFromMetadataExist := componentMetadata[cfg.ComponentSectionName]; baseComponentFromMetadataExist { baseComponentName, ok = baseComponentFromMetadata.(string) if !ok { return nil, fmt.Errorf("invalid 'components.helmfile.%s.metadata.component' attribute in the file '%s'", component, stackName) @@ -1404,17 +1415,26 @@ func ProcessStackConfig( } // Final binary to execute + // Check for the binary in the following order: + // - `components.helmfile.command` section in `atmos.yaml` CLI config file + // - global `helmfile.command` section + // - base component(s) `command` section + // - component `command` section + // - `overrides.command` section finalComponentHelmfileCommand := "helmfile" - if len(helmfileCommand) > 0 { + if cliConfig.Components.Helmfile.Command != "" { + finalComponentHelmfileCommand = cliConfig.Components.Helmfile.Command + } + if helmfileCommand != "" { finalComponentHelmfileCommand = helmfileCommand } - if len(baseComponentHelmfileCommand) > 0 { + if baseComponentHelmfileCommand != "" { finalComponentHelmfileCommand = baseComponentHelmfileCommand } - if len(componentHelmfileCommand) > 0 { + if componentHelmfileCommand != "" { finalComponentHelmfileCommand = componentHelmfileCommand } - if len(componentOverridesHelmfileCommand) > 0 { + if componentOverridesHelmfileCommand != "" { finalComponentHelmfileCommand = componentOverridesHelmfileCommand } @@ -1422,13 +1442,13 @@ func ProcessStackConfig( comp[cfg.VarsSectionName] = finalComponentVars comp[cfg.SettingsSectionName] = finalComponentSettings comp[cfg.EnvSectionName] = finalComponentEnv - comp["command"] = finalComponentHelmfileCommand + comp[cfg.CommandSectionName] = finalComponentHelmfileCommand comp["inheritance"] = componentInheritanceChain comp[cfg.MetadataSectionName] = componentMetadata comp[cfg.OverridesSectionName] = componentOverrides if baseComponentName != "" { - comp["component"] = baseComponentName + comp[cfg.ComponentSectionName] = baseComponentName } helmfileComponents[component] = comp diff --git a/pkg/stack/stack_processor_utils.go b/pkg/stack/stack_processor_utils.go index 2660eafe2..7d077eb51 100644 --- a/pkg/stack/stack_processor_utils.go +++ b/pkg/stack/stack_processor_utils.go @@ -285,6 +285,7 @@ func CreateComponentStackMap( } finalConfig, err := ProcessStackConfig( + cliConfig, stacksBasePath, terraformComponentsBasePath, helmfileComponentsBasePath, @@ -533,7 +534,7 @@ func ProcessBaseComponentConfig( } // Base component `command` - if baseComponentCommandSection, baseComponentCommandSectionExist := baseComponentMap["command"]; baseComponentCommandSectionExist { + if baseComponentCommandSection, baseComponentCommandSectionExist := baseComponentMap[cfg.CommandSectionName]; baseComponentCommandSectionExist { baseComponentCommand, ok = baseComponentCommandSection.(string) if !ok { return fmt.Errorf("invalid '%s.command' section in the stack '%s'", baseComponent, stack) @@ -627,7 +628,7 @@ func FindComponentsDerivedFromBaseComponents( return nil, fmt.Errorf("invalid '%s' component section in the file '%s'", component, stack) } - if base, baseComponentExist := componentSection["component"]; baseComponentExist { + if base, baseComponentExist := componentSection[cfg.ComponentSectionName]; baseComponentExist { baseComponent, ok := base.(string) if !ok { return nil, fmt.Errorf("invalid 'component' attribute in the component '%s' in the file '%s'", component, stack) diff --git a/website/docs/cli/commands/terraform/terraform-generate-varfiles.mdx b/website/docs/cli/commands/terraform/terraform-generate-varfiles.mdx index 0a35785fe..37a8c5b4d 100644 --- a/website/docs/cli/commands/terraform/terraform-generate-varfiles.mdx +++ b/website/docs/cli/commands/terraform/terraform-generate-varfiles.mdx @@ -5,6 +5,7 @@ sidebar_class_name: command id: generate-varfiles description: Use this command to generate the Terraform varfiles (`.tfvar`) for all Atmos terraform components in all stacks. --- + import Screengrab from '@site/src/components/Screengrab' :::note Purpose diff --git a/website/docs/cli/configuration.mdx b/website/docs/cli/configuration.mdx index f7385700c..f5bd6a188 100644 --- a/website/docs/cli/configuration.mdx +++ b/website/docs/cli/configuration.mdx @@ -130,6 +130,17 @@ Specify the default behaviors for components. ```yaml title="atmos.yaml" components: terraform: + # Optional `command` specifies the executable to be called by `atmos` when running Terraform commands + # If not defined, `terraform` is used + # Examples: + # command: terraform + # command: /usr/local/bin/terraform + # command: /usr/local/bin/terraform-1.8 + # command: tofu + # command: /usr/local/bin/tofu-1.7.1 + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_COMMAND' ENV var, or '--terraform-command' command-line argument + command: terraform + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument # Supports both absolute and relative paths base_path: "components/terraform" @@ -147,6 +158,14 @@ components: auto_generate_backend_file: true helmfile: + # Optional `command` specifies the executable to be called by `atmos` when running Helmfile commands + # If not defined, `helmfile` is used + # Examples: + # command: helmfile + # command: /usr/local/bin/helmfile + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_COMMAND' ENV var, or '--helmfile-command' command-line argument + command: helmfile + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_BASE_PATH' ENV var, or '--helmfile-dir' command-line argument # Supports both absolute and relative paths base_path: "components/helmfile" @@ -812,11 +831,13 @@ setting `ATMOS_STACKS_BASE_PATH` to a path in `/localhost` to your local develop |:------------------------------------------------------|:------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ATMOS_CLI_CONFIG_PATH | N/A | Where to find `atmos.yaml`. Path to a folder where `atmos.yaml` CLI config file is located (e.g. `/config`) | | ATMOS_BASE_PATH | base_path | Base path to `components` and `stacks` folders | +| ATMOS_COMPONENTS_TERRAFORM_COMMAND | components.terraform.command | The executable to be called by `atmos` when running Terraform commands | | ATMOS_COMPONENTS_TERRAFORM_BASE_PATH | components.terraform.base_path | Base path to Terraform components | | ATMOS_COMPONENTS_TERRAFORM_APPLY_AUTO_APPROVE | components.terraform.apply_auto_approve | If set to `true`, auto-generate Terraform backend config files when executing `atmos terraform` commands | | ATMOS_COMPONENTS_TERRAFORM_DEPLOY_RUN_INIT | components.terraform.deploy_run_init | Run `terraform init` when executing `atmos terraform deploy` command | | ATMOS_COMPONENTS_TERRAFORM_INIT_RUN_RECONFIGURE | components.terraform.init_run_reconfigure | Run `terraform init -reconfigure` when executing `atmos terraform` commands | | ATMOS_COMPONENTS_TERRAFORM_AUTO_GENERATE_BACKEND_FILE | components.terraform.auto_generate_backend_file | If set to `true`, auto-generate Terraform backend config files when executing `atmos terraform` commands | +| ATMOS_COMPONENTS_HELMFILE_COMMAND | components.helmfile.command | The executable to be called by `atmos` when running Helmfile commands | | ATMOS_COMPONENTS_HELMFILE_BASE_PATH | components.helmfile.base_path | Path to helmfile components | | ATMOS_COMPONENTS_HELMFILE_USE_EKS | components.helmfile.use_eks | If set to `true`, download `kubeconfig` from EKS by running `aws eks update-kubeconfig` command before executing `atmos helmfile` commands | | ATMOS_COMPONENTS_HELMFILE_KUBECONFIG_PATH | components.helmfile.kubeconfig_path | Path to write the `kubeconfig` file when executing `aws eks update-kubeconfig` command | diff --git a/website/docs/core-concepts/stacks/templating.md b/website/docs/core-concepts/stacks/templating.md index d3769d1f9..9c69500f0 100644 --- a/website/docs/core-concepts/stacks/templating.md +++ b/website/docs/core-concepts/stacks/templating.md @@ -305,11 +305,12 @@ You can use `Go` templates in the following Atmos sections to refer to values in - `vars` - `settings` - `env` - - `metadata` - `providers` - `overrides` - `backend` - `backend_type` + - `component` + - `metadata.component`
diff --git a/website/docs/integrations/atlantis.mdx b/website/docs/integrations/atlantis.mdx index c9283bd5e..aca05f927 100644 --- a/website/docs/integrations/atlantis.mdx +++ b/website/docs/integrations/atlantis.mdx @@ -1,9 +1,10 @@ --- title: Atlantis Integration -sidebar_position: 11 +sidebar_position: 10 sidebar_label: Atlantis --- -import Terminal from '@site/src/components/Screengrab' + +import Terminal from '@site/src/components/Terminal' Atmos natively supports [Atlantis](https://runatlantis.io) for Terraform Pull Request Automation. @@ -76,7 +77,7 @@ integrations: name: "{tenant}-{environment}-{stage}-{component}" workspace: "{workspace}" dir: "{component-path}" - terraform_version: v1.2 + terraform_version: v1.8 delete_source_branch_on_merge: true autoplan: enabled: true @@ -128,7 +129,7 @@ projects: workspace: test-component-override-3-workspace workflow: workflow-1 dir: examples/tests/components/terraform/test/test-component - terraform_version: v1.2 + terraform_version: v1.8 delete_source_branch_on_merge: true autoplan: enabled: true @@ -141,7 +142,7 @@ projects: workspace: tenant1-ue2-staging workflow: workflow-1 dir: examples/tests/components/terraform/infra/vpc - terraform_version: v1.2 + terraform_version: v1.8 delete_source_branch_on_merge: true autoplan: enabled: true @@ -215,7 +216,7 @@ Configuring the Atlantis Integration in the `settings.atlantis` sections in the delete_source_branch_on_merge: false dir: '{component-path}' name: '{tenant}-{environment}-{stage}-{component}' - terraform_version: v1.3 + terraform_version: v1.8 workflow: workflow-1 workspace: '{workspace}' project_template_name: project-1 @@ -353,7 +354,7 @@ The Atlantis config template and project template can be defined in the `setting name: "{tenant}-{environment}-{stage}-{component}" workspace: "{workspace}" dir: "{component-path}" - terraform_version: v1.2 + terraform_version: v1.8 delete_source_branch_on_merge: true autoplan: enabled: true @@ -411,7 +412,7 @@ The Atlantis config template and project template can be defined in the `setting workspace: "{workspace}" workflow: "workflow-1" dir: "{component-path}" - terraform_version: v1.3 + terraform_version: v1.8 delete_source_branch_on_merge: false autoplan: enabled: true @@ -686,7 +687,7 @@ on: branches: [ main ] env: - ATMOS_VERSION: 1.72.0 + ATMOS_VERSION: 1.73.0 ATMOS_CLI_CONFIG_PATH: ./ jobs: diff --git a/website/docs/integrations/aws.mdx b/website/docs/integrations/aws.mdx index 31e74a63a..1bead836b 100644 --- a/website/docs/integrations/aws.mdx +++ b/website/docs/integrations/aws.mdx @@ -1,6 +1,6 @@ --- title: Amazon Web Services (AWS) Integration -sidebar_position: 10 +sidebar_position: 5 sidebar_label: Amazon Web Services --- diff --git a/website/docs/integrations/github-actions/setup-atmos.md b/website/docs/integrations/github-actions/setup-atmos.md index f240418c4..0bb49e437 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.72.0 + atmos_version: 1.73.0 ``` diff --git a/website/docs/integrations/helmfile.md b/website/docs/integrations/helmfile.md index aef3659a5..52316ad50 100644 --- a/website/docs/integrations/helmfile.md +++ b/website/docs/integrations/helmfile.md @@ -1,6 +1,6 @@ --- title: Helmfile Integration -sidebar_position: 9 +sidebar_position: 4 sidebar_label: Helmfile --- diff --git a/website/docs/integrations/opentofu.mdx b/website/docs/integrations/opentofu.mdx new file mode 100644 index 000000000..19be2b09d --- /dev/null +++ b/website/docs/integrations/opentofu.mdx @@ -0,0 +1,111 @@ +--- +title: OpenTofu Integration +sidebar_position: 3 +sidebar_label: OpenTofu +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +OpenTofu + +Atmos natively supports [OpenTofu](https://opentofu.org), +similar to the way it supports [Terraform](/integrations/terraform). It's compatible with every version of `opentofu` and designed to +work with multiple different versions of it concurrently, and can even work alongside with [HashiCorp Terraform](/integrations/terraform). + +Please see the complete configuration options for [Terraform](/integrations/terraform), as they are the same for OpenTofu. We'll focus +only on what's different in this document, in order to utilize OpenTofu. Keep in mind that Atmos does not handle the downloading or installation +of OpenTofu; it assumes that any required binaries for the commands are already installed on your system. + +Additionally, if using Spacelift together with Atmos, make sure you review the [Spacelift Integration](/integrations/spacelift) to make any necessary changes. + +## CLI Configuration + +All the default configuration settings to support OpenTofu are defined in the [Atmos CLI Configuration](/cli/configuration), +but can also be overridden at any level of the [Stack](/core-concepts/stacks/#schema) configuration. + +To make OpenTofu the default command when running "terraform", modify [`atmos.yaml`](/cli/configuration) to configure the following global settings: + +```yaml +components: + terraform: + # Use the `tofu` command when calling "terraform" in Atmos. + command: "/usr/bin/tofu" # or just `tofu` + + # Optionally, specify a different path for OpenTofu components + base_path: "components/tofu" +``` + +:::important +Atmos consistently utilizes the `terraform` keyword across all configurations, rather than `tofu` or `opentofu`. +::: + +Additionally, if you prefer to run `atmos tofu` instead of `atmos terraform`, you can configure an alias. +Just add the following configuration somewhere in the `atmos.yaml` CLI config file: + +```yaml +aliases: + tofu: terraform +``` + +:::important +Creating aliases for `tofu` only changes the CLI invocation of `atmos terraform` and does not directly +influence the actual command that atmos executes when running Terraform. Atmos strictly adheres to the +specific `command` set in the Stack configurations. +::: + +## Stack Configuration for Components + +Settings for Terraform or OpenTofu can also be specified in stack configurations, where they are compatible with inheritance. +This feature allows projects to tailor behavior according to individual component needs. + +While defaults for everything are defined in the `atmos.yaml`, the same settings, can be overridden by Stack configurations at any level: + +- `terraform` +- `components.terraform` +- `components.terraform._component_` + +For instance, you can modify the command executed for a specific component by overriding the `command` parameter. +This flexibility is particularly valuable for gradually transitioning to OpenTofu or managing components that are +compatible only with HashiCorp Terraform. + +```yaml +components: + terraform: + vpc: + command: "/usr/local/bin/tofu-1.7" +``` + +## Example: Provision a Terraform Component with OpenTofu + +:::note +In the following examples, we'll assume that `tofu` is an Atmos alias for the `terraform` command. + +```yaml +aliases: + tofu: terraform +``` + +::: + +Once you've configured Atmos to utilize `tofu` — either by adjusting the default `terraform.command` in the `atmos.yaml` +or by specifying the `command` for an individual component — provisioning any component follows the same procedure as +you would typically use for Terraform. + +For example, to provision a Terraform component using OpenTofu, run the following commands: + +```console +atmos tofu plan eks --stack=ue2-dev +atmos tofu apply eks --stack=ue2-dev +``` + +where: + +- `eks` is the Terraform component to provision (from the `components/terraform` folder) +- `--stack=ue2-dev` is the stack to provision the component into + +Short versions of all command-line arguments can be used: + +```console +atmos tofu plan eks -s ue2-dev +atmos tofu apply eks -s ue2-dev +``` diff --git a/website/docs/integrations/spacelift.md b/website/docs/integrations/spacelift.md index 2d74d23a5..329a45a09 100644 --- a/website/docs/integrations/spacelift.md +++ b/website/docs/integrations/spacelift.md @@ -1,6 +1,6 @@ --- title: Spacelift Integration -sidebar_position: 3 +sidebar_position: 6 sidebar_label: Spacelift --- @@ -60,6 +60,35 @@ components:
+ +## OpenTofu Support + +Spacelift is compatible with [OpenTofu](https://opentofu.org) and configurable on a global and per stack or component basis. + +To make OpenTofu the default, add the following to your top-level stack manifest: + +```yaml +settings: + spacelift: + # Use OpenTofu + terraform_workflow_tool: OPEN_TOFU +``` + +Similarly, to override this behavior, or to only configure it on specific components, add the following to the component +configuration: + +```yaml +components: + terraform: + my-component: + settings: + spacelift: + # Use OpenTofu + terraform_workflow_tool: OPEN_TOFU +``` + +For more details on [Atmos support for OpenTofu](/integrations/opentofu) see our integration page. + ## Spacelift Stack Dependencies Atmos supports [Spacelift Stack Dependencies](https://docs.spacelift.io/concepts/stack/stack-dependencies) in component configurations. diff --git a/website/docs/integrations/terraform.md b/website/docs/integrations/terraform.md index 7cfc60b67..4b93c92af 100644 --- a/website/docs/integrations/terraform.md +++ b/website/docs/integrations/terraform.md @@ -1,40 +1,45 @@ --- title: Terraform Integration -sidebar_position: 8 +sidebar_position: 2 sidebar_label: Terraform --- -Atmos natively supports opinionated workflows for Terraform. It's compatible with every version of terraform and designed to work with multiple -different versions of Terraform concurrently. +Atmos natively supports opinionated workflows for Terraform and [OpenTofu](/integrations/opentofu). +It's compatible with every version of terraform and designed to work with multiple different versions of Terraform +concurrently. Keep in mind that Atmos does not handle the downloading or installation of Terraform; it assumes that any +required commands are already installed on your system. -Atmos provides many settings that are specific to Terraform. +Atmos provides many settings that are specific to Terraform and OpenTofu. -## Settings +## CLI Configuration -All of these settings are defined by default in the [Atmos CLI Configuration](/cli/configuration), but can be overridden at any level of -the [Stack](/core-concepts/stacks/#schema) configuration. +All of these settings are defined by default in the [Atmos CLI Configuration](/cli/configuration) found in `atmos.yaml`, +but can also be overridden at any level of the [Stack](/core-concepts/stacks/#schema) configuration. ```yaml -# The executable to be called by `atmos` when running terraform commands. -command: "/usr/bin/terraform-1" -# Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument -# Supports both absolute and relative paths -base_path: "components/terraform" -# Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_APPLY_AUTO_APPROVE' ENV var -apply_auto_approve: false -# Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_DEPLOY_RUN_INIT' ENV var, or '--deploy-run-init' command-line argument -deploy_run_init: true -# Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_INIT_RUN_RECONFIGURE' ENV var, or '--init-run-reconfigure' command-line argument -init_run_reconfigure: true -# Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_AUTO_GENERATE_BACKEND_FILE' ENV var, or '--auto-generate-backend-file' command-line argument -auto_generate_backend_file: false +components: + terraform: + # The executable to be called by `atmos` when running Terraform commands + command: "/usr/bin/terraform-1" + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_BASE_PATH' ENV var, or '--terraform-dir' command-line argument + # Supports both absolute and relative paths + base_path: "components/terraform" + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_APPLY_AUTO_APPROVE' ENV var + apply_auto_approve: false + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_DEPLOY_RUN_INIT' ENV var, or '--deploy-run-init' command-line argument + deploy_run_init: true + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_INIT_RUN_RECONFIGURE' ENV var, or '--init-run-reconfigure' command-line argument + init_run_reconfigure: true + # Can also be set using 'ATMOS_COMPONENTS_TERRAFORM_AUTO_GENERATE_BACKEND_FILE' ENV var, or '--auto-generate-backend-file' command-line argument + auto_generate_backend_file: false ``` ## Configuration -The settings for terraform can be defined in multiple places and support inheritance. This ensures that projects can override the behavior. +The settings for terraform can be defined in multiple places and support inheritance. This ensures that projects can +override the behavior. -The defaults everything are defined in the `atmos.yaml`. +The defaults for everything are defined in the `atmos.yaml`. ```yaml components: @@ -59,7 +64,8 @@ components: ## Terraform Provider -A Terraform provider (`cloudposse/terraform-provider-utils`) implements a `data` source that can read the YAML Stack configurations natively from +A Terraform provider (`cloudposse/terraform-provider-utils`) implements a `data` source that can read the YAML Stack +configurations natively from within terraform. ## Terraform Module @@ -103,7 +109,8 @@ atmos terraform plan eks -s ue2-dev atmos terraform apply eks -s ue2-dev ``` -`terraform deploy` command executes `terraform apply -auto-approve` to provision components in stacks without user interaction: +The `atmos terraform deploy` command executes `terraform apply -auto-approve` to provision components in stacks without +user interaction: ```console atmos terraform deploy eks -s ue2-dev diff --git a/website/static/img/opentofu-icon.svg b/website/static/img/opentofu-icon.svg new file mode 100644 index 000000000..2b48529a5 --- /dev/null +++ b/website/static/img/opentofu-icon.svg @@ -0,0 +1,61 @@ + + + + + + + + + +