diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cfed9749..ee6525b0d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -232,6 +232,7 @@ jobs: # - demo-component-manifest - demo-component-versions - demo-context + - demo-configuration # - demo-custom-command # - demo-json-validation # - demo-opa-validation diff --git a/examples/demo-atmos-cli-imports/atmos.yaml b/examples/demo-atmos-cli-imports/atmos.yaml new file mode 100644 index 000000000..79b3ccd26 --- /dev/null +++ b/examples/demo-atmos-cli-imports/atmos.yaml @@ -0,0 +1,9 @@ +# Description: This is an example of a custom import configuration file. +# The import configuration file is used to load configurations from multiple files and directories. +# The configurations are merged together to create a single configuration object. +# The configurations are loaded in the order they are defined in the import section. +base_path: "./" +import: + - "https://raw.githubusercontent.com/cloudposse/atmos/refs/heads/main/atmos.yaml" # Load from a remote URL + - "configs.d/**/*" # Recursively load configurations from a directory + - "./logs.yaml" # Load a specific file diff --git a/examples/demo-atmos-cli-imports/configs.d/commands.yaml b/examples/demo-atmos-cli-imports/configs.d/commands.yaml new file mode 100644 index 000000000..8580ae9f0 --- /dev/null +++ b/examples/demo-atmos-cli-imports/configs.d/commands.yaml @@ -0,0 +1,7 @@ +# Custom CLI commands +commands: + - name: "test" + description: "Run all tests" + steps: + - atmos describe config + diff --git a/examples/demo-atmos-cli-imports/configs.d/tools/stack.yml b/examples/demo-atmos-cli-imports/configs.d/tools/stack.yml new file mode 100644 index 000000000..5b2994a33 --- /dev/null +++ b/examples/demo-atmos-cli-imports/configs.d/tools/stack.yml @@ -0,0 +1,7 @@ +stacks: + base_path: "stacks" + included_paths: + - "deploy/**/*" + excluded_paths: + - "**/_defaults.yaml" + name_pattern: "{dev}" diff --git a/examples/demo-atmos-cli-imports/configs.d/tools/terraform.yaml b/examples/demo-atmos-cli-imports/configs.d/tools/terraform.yaml new file mode 100644 index 000000000..adf9c72c2 --- /dev/null +++ b/examples/demo-atmos-cli-imports/configs.d/tools/terraform.yaml @@ -0,0 +1,7 @@ +components: + terraform: + base_path: "components/terraform" + apply_auto_approve: true + deploy_run_init: true + init_run_reconfigure: true + auto_generate_backend_file: false diff --git a/examples/demo-atmos-cli-imports/configs.d/vendor.yaml b/examples/demo-atmos-cli-imports/configs.d/vendor.yaml new file mode 100644 index 000000000..d6fe12e0c --- /dev/null +++ b/examples/demo-atmos-cli-imports/configs.d/vendor.yaml @@ -0,0 +1,9 @@ +vendor: + # Single file + base_path: "./vendor.yaml" + + # Directory with multiple files + #base_path: "./vendor" + + # Absolute path + #base_path: "vendor.d/vendor1.yaml" diff --git a/examples/demo-atmos-cli-imports/logs.yaml b/examples/demo-atmos-cli-imports/logs.yaml new file mode 100644 index 000000000..e389a26b8 --- /dev/null +++ b/examples/demo-atmos-cli-imports/logs.yaml @@ -0,0 +1,3 @@ +logs: + file: "/dev/stderr" + level: Debug diff --git a/examples/demo-configuration/atmos.d/commands.yaml b/examples/demo-configuration/atmos.d/commands.yaml new file mode 100644 index 000000000..c655ad569 --- /dev/null +++ b/examples/demo-configuration/atmos.d/commands.yaml @@ -0,0 +1,6 @@ +# Custom CLI commands +commands: +- name: "test" + description: "Run all tests with custom command" + steps: + - atmos describe config diff --git a/examples/demo-configuration/atmos.d/logs.yaml b/examples/demo-configuration/atmos.d/logs.yaml new file mode 100644 index 000000000..e389a26b8 --- /dev/null +++ b/examples/demo-configuration/atmos.d/logs.yaml @@ -0,0 +1,3 @@ +logs: + file: "/dev/stderr" + level: Debug diff --git a/examples/demo-configuration/atmos.d/tools/helmfile.yml b/examples/demo-configuration/atmos.d/tools/helmfile.yml new file mode 100644 index 000000000..78780b119 --- /dev/null +++ b/examples/demo-configuration/atmos.d/tools/helmfile.yml @@ -0,0 +1,17 @@ +components: + 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" + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_USE_EKS' ENV var + # If not specified, defaults to 'true' + use_eks: true + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_KUBECONFIG_PATH' ENV var + kubeconfig_path: "/dev/shm" + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_HELM_AWS_PROFILE_PATTERN' ENV var + helm_aws_profile_pattern: "{namespace}-{tenant}-gbl-{stage}-helm" + # Can also be set using 'ATMOS_COMPONENTS_HELMFILE_CLUSTER_NAME_PATTERN' ENV var + cluster_name_pattern: "{namespace}-{tenant}-{environment}-{stage}-eks-cluster" + + + diff --git a/examples/demo-configuration/atmos.d/tools/stack.yaml b/examples/demo-configuration/atmos.d/tools/stack.yaml new file mode 100644 index 000000000..5b2994a33 --- /dev/null +++ b/examples/demo-configuration/atmos.d/tools/stack.yaml @@ -0,0 +1,7 @@ +stacks: + base_path: "stacks" + included_paths: + - "deploy/**/*" + excluded_paths: + - "**/_defaults.yaml" + name_pattern: "{dev}" diff --git a/examples/demo-configuration/atmos.d/tools/terraform.yaml b/examples/demo-configuration/atmos.d/tools/terraform.yaml new file mode 100644 index 000000000..201d2ff8a --- /dev/null +++ b/examples/demo-configuration/atmos.d/tools/terraform.yaml @@ -0,0 +1,23 @@ +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" + # 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: true diff --git a/examples/demo-configuration/atmos.yaml b/examples/demo-configuration/atmos.yaml new file mode 100644 index 000000000..063685dbc --- /dev/null +++ b/examples/demo-configuration/atmos.yaml @@ -0,0 +1,5 @@ +# Description: Configuration file for the Atmos CLI +# default import path is ./atmos.d import .yaml files from the directory and merge them +base_path: "./" + + diff --git a/examples/demo-env/atmos.yaml b/examples/demo-env/atmos.yaml new file mode 100644 index 000000000..555d07515 --- /dev/null +++ b/examples/demo-env/atmos.yaml @@ -0,0 +1,21 @@ +base_path: "./" + +components: + terraform: + base_path: "components/terraform" + apply_auto_approve: false + deploy_run_init: true + init_run_reconfigure: true + auto_generate_backend_file: false + +stacks: + base_path: "stacks" + included_paths: + - "deploy/**/*" + excluded_paths: + - "**/_defaults.yaml" + name_pattern: "{stage}" + +logs: + file: "/dev/stderr" + level: Info diff --git a/examples/demo-env/components/terraform/example/main.tf b/examples/demo-env/components/terraform/example/main.tf new file mode 100644 index 000000000..7c6ff8406 --- /dev/null +++ b/examples/demo-env/components/terraform/example/main.tf @@ -0,0 +1,4 @@ +# Fetch the required environment variables using the `environment_variables` data source +data "environment_variables" "required" { + filter = "ATMOS_.*" # Fetches all variables starting with "ATMOS_" +} diff --git a/examples/demo-env/components/terraform/example/outputs.tf b/examples/demo-env/components/terraform/example/outputs.tf new file mode 100644 index 000000000..79a4b5a1a --- /dev/null +++ b/examples/demo-env/components/terraform/example/outputs.tf @@ -0,0 +1,15 @@ +# Output environment variables +output "atmos_cli_config_path" { + value = data.environment_variables.required.items["ATMOS_CLI_CONFIG_PATH"] + description = "The path to the Atmos CLI configuration file" +} + +output "atmos_base_path" { + value = data.environment_variables.required.items["ATMOS_BASE_PATH"] + description = "The base path used by Atmos" +} + +output "example" { + value = data.environment_variables.required.items["EXAMPLE"] + description = "Example environment variable" +} diff --git a/examples/demo-env/stacks/catalog/example.yaml b/examples/demo-env/stacks/catalog/example.yaml new file mode 100644 index 000000000..409e8718e --- /dev/null +++ b/examples/demo-env/stacks/catalog/example.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json +env: + EXAMPLE: "test" + +components: + terraform: + example: + metadata: + component: example + vars: {} diff --git a/examples/demo-env/stacks/deploy/dev.yaml b/examples/demo-env/stacks/deploy/dev.yaml new file mode 100644 index 000000000..d876c40a4 --- /dev/null +++ b/examples/demo-env/stacks/deploy/dev.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +vars: + stage: dev + +import: + - catalog/example + +components: + terraform: + example: + vars: {} diff --git a/examples/demo-env/stacks/deploy/prod.yaml b/examples/demo-env/stacks/deploy/prod.yaml new file mode 100644 index 000000000..75b7497b6 --- /dev/null +++ b/examples/demo-env/stacks/deploy/prod.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +vars: + stage: prod + +import: + - catalog/example + +components: + terraform: + example: + vars: {} diff --git a/examples/demo-env/stacks/deploy/staging.yaml b/examples/demo-env/stacks/deploy/staging.yaml new file mode 100644 index 000000000..beca3ad9f --- /dev/null +++ b/examples/demo-env/stacks/deploy/staging.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json + +vars: + stage: staging + +import: + - catalog/example + +components: + terraform: + example: + vars: {} diff --git a/internal/exec/helmfile.go b/internal/exec/helmfile.go index bb127902d..abfa5fb9f 100644 --- a/internal/exec/helmfile.go +++ b/internal/exec/helmfile.go @@ -244,6 +244,8 @@ func ExecuteHelmfile(info schema.ConfigAndStacksInfo) error { if cliConfig.Components.Helmfile.UseEKS { envVars = append(envVars, envVarsEKS...) } + envVars = append(envVars, fmt.Sprintf("ATMOS_CLI_CONFIG_PATH=%s", cliConfig.CliConfigPath)) + envVars = append(envVars, fmt.Sprintf("ATMOS_BASE_PATH=%s", cliConfig.BasePath)) u.LogTrace(cliConfig, "Using ENV vars:") for _, v := range envVars { diff --git a/internal/exec/terraform.go b/internal/exec/terraform.go index 88fd45b04..a35682e3d 100644 --- a/internal/exec/terraform.go +++ b/internal/exec/terraform.go @@ -231,6 +231,8 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error { } } + info.ComponentEnvList = append(info.ComponentEnvList, fmt.Sprintf("ATMOS_CLI_CONFIG_PATH=%s", cliConfig.CliConfigPath)) + info.ComponentEnvList = append(info.ComponentEnvList, fmt.Sprintf("ATMOS_BASE_PATH=%s", cliConfig.BasePath)) // Set `TF_IN_AUTOMATION` ENV var to `true` to suppress verbose instructions after terraform commands // https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_in_automation info.ComponentEnvList = append(info.ComponentEnvList, "TF_IN_AUTOMATION=true") diff --git a/pkg/config/config.go b/pkg/config/config.go index 73322b0ed..d21467019 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,13 +2,17 @@ package config import ( "bytes" + "context" "encoding/json" "fmt" + "net/url" "os" "path/filepath" "runtime" + "strings" + "time" - "github.com/fatih/color" + "github.com/hashicorp/go-getter" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/spf13/viper" @@ -96,16 +100,18 @@ var ( // https://medium.com/@bnprashanth256/reading-configuration-files-and-environment-variables-in-go-golang-c2607f912b63 func InitCliConfig(configAndStacksInfo schema.ConfigAndStacksInfo, processStacks bool) (schema.CliConfiguration, error) { // cliConfig is loaded from the following locations (from lower to higher priority): - // system dir (`/usr/local/etc/atmos` on Linux, `%LOCALAPPDATA%/atmos` on Windows) - // home dir (~/.atmos) - // current directory - // ENV vars - // Command-line arguments - + // 1. If ATMOS_CLI_CONFIG_PATH is defined, check only there + // 2. If ATMOS_CLI_CONFIG_PATH is not defined, proceed with other paths + // - Check system directory (optional) + // - Check user-specific configuration: + // - If XDG_CONFIG_HOME is defined, use it; otherwise fallback to ~/.config/atmos + // - Check current directory + // - If Terraform provider specified a path + // 3. If no config is found in any of the above locations, use the default config + // Check if no imports are defined var cliConfig schema.CliConfiguration var err error configFound := false - var found bool v := viper.New() v.SetConfigType("yaml") @@ -115,99 +121,109 @@ func InitCliConfig(configAndStacksInfo schema.ConfigAndStacksInfo, processStacks v.SetDefault("components.helmfile.use_eks", true) v.SetDefault("components.terraform.append_user_agent", fmt.Sprintf("Atmos/%s (Cloud Posse; +https://atmos.tools)", version.Version)) - // Process config in system folder - configFilePath1 := "" - - // https://pureinfotech.com/list-environment-variables-windows-10/ - // https://docs.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables - // https://softwareengineering.stackexchange.com/questions/299869/where-is-the-appropriate-place-to-put-application-configuration-files-for-each-p - // https://stackoverflow.com/questions/37946282/why-does-appdata-in-windows-7-seemingly-points-to-wrong-folder - if runtime.GOOS == "windows" { - appDataDir := os.Getenv(WindowsAppDataEnvVar) - if len(appDataDir) > 0 { - configFilePath1 = appDataDir + // 1. If ATMOS_CLI_CONFIG_PATH is defined, check only there + if atmosCliConfigPathEnv := os.Getenv("ATMOS_CLI_CONFIG_PATH"); atmosCliConfigPathEnv != "" { + u.LogTrace(cliConfig, fmt.Sprintf("Found ENV var ATMOS_CLI_CONFIG_PATH=%s", atmosCliConfigPathEnv)) + configFile := filepath.Join(atmosCliConfigPathEnv, CliConfigFileName) + found, configPath, err := processConfigFile(cliConfig, configFile, v) + if err != nil { + return cliConfig, err + } + if !found { + // If we want to error out if config not found in ATMOS_CLI_CONFIG_PATH + return cliConfig, fmt.Errorf("config not found in ATMOS_CLI_CONFIG_PATH: %s", configFile) + } else { + configFound = true + cliConfig.CliConfigPath = configPath } + + // Since ATMOS_CLI_CONFIG_PATH is to be the first and only check, we skip other paths if found + // If not found, we still skip other paths as per the requirement. } else { - configFilePath1 = SystemDirConfigFilePath - } + // 2. If ATMOS_CLI_CONFIG_PATH is not defined, proceed with other paths + // - Check system directory (optional) + configFilePathSystem := "" + if runtime.GOOS == "windows" { + appDataDir := os.Getenv(WindowsAppDataEnvVar) + if len(appDataDir) > 0 { + configFilePathSystem = appDataDir + } + } else { + configFilePathSystem = SystemDirConfigFilePath + } + + if len(configFilePathSystem) > 0 { + configFile := filepath.Join(configFilePathSystem, CliConfigFileName) + found, configPath, err := processConfigFile(cliConfig, configFile, v) + if err != nil { + return cliConfig, err + } + if found { + configFound = true + cliConfig.CliConfigPath = configPath + } + } + + // 3. Check user-specific configuration: + // If XDG_CONFIG_HOME is defined, use it; otherwise fallback to ~/.config/atmos + xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") + var userConfigDir string + if xdgConfigHome != "" { + userConfigDir = filepath.Join(xdgConfigHome, "atmos") + } else { + homeDir, err := homedir.Dir() + if err != nil { + return cliConfig, err + } + userConfigDir = filepath.Join(homeDir, ".config", "atmos") + } - if len(configFilePath1) > 0 { - configFile1 := filepath.Join(configFilePath1, CliConfigFileName) - found, err = processConfigFile(cliConfig, configFile1, v) + userConfigFile := filepath.Join(userConfigDir, CliConfigFileName) + found, configPath, err := processConfigFile(cliConfig, userConfigFile, v) if err != nil { return cliConfig, err } if found { configFound = true + cliConfig.CliConfigPath = configPath } - } - - // Process config in user's HOME dir - configFilePath2, err := homedir.Dir() - if err != nil { - return cliConfig, err - } - configFile2 := filepath.Join(configFilePath2, ".atmos", CliConfigFileName) - found, err = processConfigFile(cliConfig, configFile2, v) - if err != nil { - return cliConfig, err - } - if found { - configFound = true - } - // Process config in the current dir - configFilePath3, err := os.Getwd() - if err != nil { - return cliConfig, err - } - configFile3 := filepath.Join(configFilePath3, CliConfigFileName) - found, err = processConfigFile(cliConfig, configFile3, v) - if err != nil { - return cliConfig, err - } - if found { - configFound = true - } - - // Process config from the path in ENV var `ATMOS_CLI_CONFIG_PATH` - configFilePath4 := os.Getenv("ATMOS_CLI_CONFIG_PATH") - if len(configFilePath4) > 0 { - u.LogTrace(cliConfig, fmt.Sprintf("Found ENV var ATMOS_CLI_CONFIG_PATH=%s", configFilePath4)) - configFile4 := filepath.Join(configFilePath4, CliConfigFileName) - found, err = processConfigFile(cliConfig, configFile4, v) + // 4. Check current directory + configFilePathCwd, err := os.Getwd() + if err != nil { + return cliConfig, err + } + configFileCwd := filepath.Join(configFilePathCwd, CliConfigFileName) + found, configPath, err = processConfigFile(cliConfig, configFileCwd, v) if err != nil { return cliConfig, err } if found { configFound = true + cliConfig.CliConfigPath = configPath } - } - // Process config from the path specified in the Terraform provider (which calls into the atmos code) - if configAndStacksInfo.AtmosCliConfigPath != "" { - configFilePath5 := configAndStacksInfo.AtmosCliConfigPath - if len(configFilePath5) > 0 { - configFile5 := filepath.Join(configFilePath5, CliConfigFileName) - found, err = processConfigFile(cliConfig, configFile5, v) + // 5. If Terraform provider specified a path + if configAndStacksInfo.AtmosCliConfigPath != "" { + configFileTfProvider := filepath.Join(configAndStacksInfo.AtmosCliConfigPath, CliConfigFileName) + found, configPath, err := processConfigFile(cliConfig, configFileTfProvider, v) if err != nil { return cliConfig, err } if found { configFound = true + cliConfig.CliConfigPath = configPath } } } if !configFound { - // If `atmos.yaml` not found, use the default config - // Set `ATMOS_LOGS_LEVEL` ENV var to "Debug" to see the message about Atmos using the default CLI config + // Use default config if no config was found in any location logsLevelEnvVar := os.Getenv("ATMOS_LOGS_LEVEL") if logsLevelEnvVar == u.LogLevelDebug || logsLevelEnvVar == u.LogLevelTrace { - u.PrintMessageInColor("'atmos.yaml' CLI config was not found in any of the searched paths: system dir, home dir, current dir, ENV vars.\n"+ - "Refer to https://atmos.tools/cli/configuration for details on how to configure 'atmos.yaml'.\n"+ - "Using the default CLI config:\n\n", color.New(color.FgCyan)) - + u.LogTrace(cliConfig, "atmos.yaml' CLI config was not found.\n"+ + "Refer to https://atmos.tools/cli/configuration\n"+ + "Using the default CLI config:\n\n") err = u.PrintAsYAMLToFileDescriptor(cliConfig, defaultCliConfig) if err != nil { return cliConfig, err @@ -227,19 +243,60 @@ func InitCliConfig(configAndStacksInfo schema.ConfigAndStacksInfo, processStacks } } - // https://gist.github.com/chazcheadle/45bf85b793dea2b71bd05ebaa3c28644 - // https://sagikazarmark.hu/blog/decoding-custom-formats-with-viper/ + // Unmarshal, process environment variables, imports, and command-line arguments as needed. err = v.Unmarshal(&cliConfig) if err != nil { return cliConfig, err } - // Process ENV vars err = processEnvVars(&cliConfig) if err != nil { return cliConfig, err } + // Check if no imports are defined + if len(cliConfig.Import) == 0 { + basePath, err := filepath.Abs(cliConfig.BasePath) + if err != nil { + return cliConfig, err + } + // Check for an `atmos.d` directory and load the configs if found + atmosDPath := filepath.Join(basePath, "atmos.d") + // Ensure the joined path doesn't escape the intended directory + if !strings.HasPrefix(atmosDPath, basePath) { + u.LogWarning(cliConfig, "invalid atmos.d path: attempted directory traversal") + } + _, err = os.Stat(atmosDPath) + if err == nil { + cliConfig.Import = []string{"atmos.d/**/*.yaml", "atmos.d/**/*.yml"} + } else if !os.IsNotExist(err) { + return cliConfig, err // Handle unexpected errors + } + // Check for `.atmos.d` directory if `.atmos.d` directory is not found + atmosDPath = filepath.Join(atmosDPath, ".atmos.d") + _, err = os.Stat(atmosDPath) + if err == nil { + cliImport := []string{".atmos.d/**/*.yaml", ".atmos.d/**/*.yml"} + cliConfig.Import = append(cliConfig.Import, cliImport...) + } else if !os.IsNotExist(err) { + return cliConfig, err // Handle unexpected errors + } + + } + // Process imports if any + if len(cliConfig.Import) > 0 { + err = processImports(cliConfig, v) + if err != nil { + return cliConfig, err + } + + // Re-unmarshal the merged configuration into cliConfig + err = v.Unmarshal(&cliConfig) + if err != nil { + return cliConfig, err + } + } + // Process command-line args err = processCommandLineArgs(&cliConfig, configAndStacksInfo) if err != nil { @@ -351,15 +408,15 @@ func processConfigFile( cliConfig schema.CliConfiguration, path string, v *viper.Viper, -) (bool, error) { +) (bool, string, error) { // Check if the config file exists configPath, fileExists := u.SearchConfigFile(path) if !fileExists { - return false, nil + return false, "", nil } reader, err := os.Open(configPath) if err != nil { - return false, err + return false, "", err } defer func(reader *os.File) { @@ -371,8 +428,103 @@ func processConfigFile( err = v.MergeConfig(reader) if err != nil { - return false, err + return false, "", err + } + + return true, configPath, nil +} +func processImports(cliConfig schema.CliConfiguration, v *viper.Viper) error { + for _, importPath := range cliConfig.Import { + if importPath == "" { + continue + } + + var resolvedPaths []string + var err error + + if strings.HasPrefix(importPath, "http://") || strings.HasPrefix(importPath, "https://") { + // Handle remote URLs + // Validate the URL before downloading + parsedURL, err := url.Parse(importPath) + if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") { + u.LogWarning(cliConfig, fmt.Sprintf("unsupported URL '%s': %v", importPath, err)) + continue + } + + tempDir, tempFile, err := downloadRemoteConfig(importPath) + if err != nil { + u.LogWarning(cliConfig, fmt.Sprintf("failed to download remote config '%s': %v", importPath, err)) + continue + } + resolvedPaths = []string{tempFile} + defer os.RemoveAll(tempDir) + + } else { + + ext := filepath.Ext(importPath) + + basePath, err := filepath.Abs(cliConfig.BasePath) + if err != nil { + return err + } + if ext != "" { + imp := filepath.Join(basePath, importPath) + resolvedPaths, err = u.GetGlobMatches(imp) + if err != nil { + u.LogWarning(cliConfig, fmt.Sprintf("failed to resolve import path '%s': %v", imp, err)) + continue + } + } else { + impYaml := filepath.Join(basePath, importPath+".yaml") + impYml := filepath.Join(basePath, importPath+".yml") + // ensure the joined path doesn't escape the intended directory + if !strings.HasPrefix(impYaml, basePath) || !strings.HasPrefix(impYml, basePath) { + return fmt.Errorf("invalid import path: attempted directory traversal") + } + resolvedPathYaml, errYaml := u.GetGlobMatches(impYaml) + resolvedPathsYml, errYml := u.GetGlobMatches(impYml) + if errYaml != nil && errYml != nil { + u.LogWarning(cliConfig, fmt.Sprintf("failed to resolve import path '%s': %v", importPath, err)) + continue + } + resolvedPaths = append(resolvedPaths, resolvedPathYaml...) + resolvedPaths = append(resolvedPaths, resolvedPathsYml...) + + } + } + // print the resolved paths + u.LogTrace(cliConfig, fmt.Sprintf("Resolved import paths: %v", resolvedPaths)) + for _, path := range resolvedPaths { + // Process each configuration file + _, _, err = processConfigFile(cliConfig, path, v) + if err != nil { + // Log the error but continue processing other files + u.LogWarning(cliConfig, fmt.Sprintf("failed to merge configuration from '%s': %v", path, err)) + continue + } + } } + return nil +} - return true, nil +func downloadRemoteConfig(url string) (string, string, error) { + tempDir, err := os.MkdirTemp("", "atmos-import-*") + if err != nil { + return "", "", err + } + tempFile := filepath.Join(tempDir, "atmos.yaml") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + client := &getter.Client{ + Ctx: ctx, + Src: url, + Dst: tempFile, + Mode: getter.ClientModeFile, + } + err = client.Get() + if err != nil { + os.RemoveAll(tempDir) + return "", "", fmt.Errorf("failed to download remote config: %w", err) + } + return tempDir, tempFile, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 000000000..02e9e38b8 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,138 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cloudposse/atmos/pkg/schema" +) + +// Config loads successfully from ATMOS_CLI_CONFIG_PATH when env var is set +func TestInitCliConfigLoadsFromAtmosCliConfigPath(t *testing.T) { + // Setup test directory and config file + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "atmos.yaml") + + // Create test config content + configContent := []byte(` +components: + terraform: + base_path: terraform +stacks: + base_path: "stacks" + included_paths: + - "deploy/**/*" + excluded_paths: + - "**/_defaults.yaml" + name_pattern: "{stage}" +`) + err := os.WriteFile(configPath, configContent, 0644) + require.NoError(t, err) + + // Set env var to temp config path + t.Setenv("ATMOS_CLI_CONFIG_PATH", tmpDir) + + // Create test input + configInfo := schema.ConfigAndStacksInfo{ + Stack: "test-stack", + } + + // Call function under test + cfg, err := InitCliConfig(configInfo, false) + require.NoError(t, err) + + // Assert results + require.True(t, cfg.Initialized) + require.Equal(t, "terraform", cfg.Components.Terraform.BasePath) +} + +// Empty or invalid ATMOS_CLI_CONFIG_PATH environment variable +func TestInitCliConfigWithInvalidEnvPath(t *testing.T) { + // Set env var to non-existent path + t.Setenv("ATMOS_CLI_CONFIG_PATH", "/non/existent/path") + + // Create test input + configInfo := schema.ConfigAndStacksInfo{ + Stack: "test-stack", + } + + // Call function under test + _, err := InitCliConfig(configInfo, false) + + // Assert error is returned + require.Error(t, err) + require.Contains(t, err.Error(), "config not found in ATMOS_CLI_CONFIG_PATH") +} + +// Config loads from default locations when ATMOS_CLI_CONFIG_PATH is not set +func TestConfigLoadsFromDefaultLocations(t *testing.T) { + originalValue := os.Getenv("ATMOS_CLI_CONFIG_PATH") + defer os.Setenv("ATMOS_CLI_CONFIG_PATH", originalValue) + os.Unsetenv("ATMOS_CLI_CONFIG_PATH") + + // Setup temporary directory and default config file + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "atmos.yaml") + configContent := []byte(` +components: + helmfile: + use_eks: true + terraform: + append_user_agent: Atmos/1.0.0 (Cloud Posse; +https://atmos.tools) +stacks: + base_path: "stacks" + included_paths: + - "deploy/**/*" + excluded_paths: + - "**/_defaults.yaml" + name_pattern: "{stage}" +`) + err := os.WriteFile(configPath, configContent, 0644) + require.NoError(t, err) + + // Change working directory to temporary directory + err = os.Chdir(tmpDir) + require.NoError(t, err) + + configAndStacksInfo := schema.ConfigAndStacksInfo{} + cliConfig, err := InitCliConfig(configAndStacksInfo, false) + require.NoError(t, err) + require.True(t, cliConfig.Initialized) +} + +// Imports from atmos.d directory are processed automatically when no explicit imports defined +func TestImportsFromAtmosDProcessedAutomatically(t *testing.T) { + originalValue := os.Getenv("ATMOS_CLI_CONFIG_PATH") + defer os.Setenv("ATMOS_CLI_CONFIG_PATH", originalValue) + os.Unsetenv("ATMOS_CLI_CONFIG_PATH") + + // Setup temporary directory and atmos.d + tmpDir := t.TempDir() + atmosDPath := filepath.Join(tmpDir, "atmos.d") + err := os.Mkdir(atmosDPath, 0755) + require.NoError(t, err) + + // Create a sample import file + importFilePath := filepath.Join(atmosDPath, "sample.yaml") + importContent := []byte(` +imports: + - some/import/path.yaml +`) + err = os.WriteFile(importFilePath, importContent, 0644) + require.NoError(t, err) + + // Change working directory to temporary directory + err = os.Chdir(tmpDir) + require.NoError(t, err) + + configAndStacksInfo := schema.ConfigAndStacksInfo{} + cliConfig, err := InitCliConfig(configAndStacksInfo, false) + require.NoError(t, err) + + if len(cliConfig.Import) == 0 { + t.Fatalf("Expected imports to be processed from atmos.d directory") + } +} diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index e44396a38..c74de5abc 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -5,6 +5,7 @@ type AtmosSectionMapType = map[string]any // CliConfiguration structure represents schema for `atmos.yaml` CLI config type CliConfiguration struct { BasePath string `yaml:"base_path" json:"base_path" mapstructure:"base_path"` + CliConfigPath string `yaml:"cli_config_path" json:"cli_config_path,omitempty" mapstructure:"cli_config_path"` Components Components `yaml:"components" json:"components" mapstructure:"components"` Stacks Stacks `yaml:"stacks" json:"stacks" mapstructure:"stacks"` Workflows Workflows `yaml:"workflows,omitempty" json:"workflows,omitempty" mapstructure:"workflows"` @@ -26,6 +27,7 @@ type CliConfiguration struct { StackConfigFilesAbsolutePaths []string `yaml:"stackConfigFilesAbsolutePaths,omitempty" json:"stackConfigFilesAbsolutePaths,omitempty" mapstructure:"stackConfigFilesAbsolutePaths"` StackType string `yaml:"stackType,omitempty" json:"StackType,omitempty" mapstructure:"stackType"` Default bool `yaml:"default" json:"default" mapstructure:"default"` + Import []string `yaml:"import" json:"import" mapstructure:"import"` Version Version `yaml:"version,omitempty" json:"version,omitempty" mapstructure:"version"` } diff --git a/pkg/utils/glob_utils.go b/pkg/utils/glob_utils.go index 268473c53..c2e2f2d0a 100644 --- a/pkg/utils/glob_utils.go +++ b/pkg/utils/glob_utils.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "sync" @@ -38,6 +39,8 @@ func GetGlobMatches(pattern string) ([]string, error) { for _, match := range matches { fullMatches = append(fullMatches, filepath.Join(base, match)) } + // Sort matches lexicographically + sort.Strings(fullMatches) getGlobMatchesSyncMap.Store(pattern, strings.Join(fullMatches, ",")) diff --git a/website/docs/cli/configuration/configuration.mdx b/website/docs/cli/configuration/configuration.mdx index 9268c0cb3..97c1b9b2d 100644 --- a/website/docs/cli/configuration/configuration.mdx +++ b/website/docs/cli/configuration/configuration.mdx @@ -10,6 +10,7 @@ import Screengrab from '@site/src/components/Screengrab' import Terminal from '@site/src/components/Terminal' import File from '@site/src/components/File' import Intro from '@site/src/components/Intro' +import Admonition from '@theme/Admonition'; # CLI Configuration @@ -17,30 +18,203 @@ import Intro from '@site/src/components/Intro' Use the `atmos.yaml` configuration file to control the behavior of the [`atmos` CLI](/cli) -Everything in the [`atmos` CLI](/cli) is configurable. The defaults are established in the `atmos.yaml` configuration file. The CLI configuration should not -be confused with [Stack configurations](/core-concepts/stacks/), which have a different schema. +Everything in the [`atmos` CLI](/cli) is configurable. The defaults are established in the `atmos.yaml` configuration file. The CLI configuration should not be confused with [Stack configurations](/core-concepts/stacks/), which have a different schema. -Think of this file as where you [bootstrap the settings or configuration of your project](/core-concepts/projects). If you'll be using -[terraform](/core-concepts/components/terraform), then [this is where](/cli/configuration/components#terraform-component-behavior) -you'd specify the command to run (e.g. [`opentofu`](/core-concepts/projects/configuration/opentofu)), -the base path location of the components, and so forth. +Think of this file as where you [bootstrap the settings or configuration of your project](/core-concepts/projects). If you'll be using [terraform](/core-concepts/components/terraform), then [this is where](/cli/configuration/components#terraform-component-behavior) you'd specify the command to run (e.g. [`opentofu`](/core-concepts/projects/configuration/opentofu)), the base path location of the components, and so forth. ## Configuration File (`atmos.yaml`) -The CLI config is loaded from the following locations (from lowest to highest priority): +The Atmos configuration is processed in three stages, allowing you to define settings at different levels of precedence. -- System directory (`/usr/local/etc/atmos/atmos.yaml` on Linux, `%LOCALAPPDATA%/atmos/atmos.yaml` on Windows) -- Home directory (`~/.atmos/atmos.yaml`) -- Current directory (`./atmos.yaml`) -- Environment variable `ATMOS_CLI_CONFIG_PATH` (the ENV var should point to a folder without specifying the file name) +As part of this process, Atmos updates the `ATMOS_CLI_CONFIG_PATH` environment variable to include a list of the absolute configuration paths in the order they were processed. These paths are separated by the system's standard path separator (`:` on Unix-like systems and `;` on Windows). This ensures that subshells and Terraform providers like `terraform-provider-utils` can consistently access and process the merged configurations across different operating systems. -Each configuration file discovered is deep-merged with the preceding configurations. +### Stage 1: Load Explicit or System-Wide Configurations + +Atmos will search for the configuration file in the following locations, in order of precedence. The first location found is used: +
+
+ `--config ` flag +
+
+ The `--config` flag lets you provide a relative or absolute path to a valid configuration file or a directory that only contains Atmos configuration files. If a relative path is supplied, it will be relative to your current working directory. The `--config` flag has the highest priority, meaning it overrides all other configuration sources. + + + When you use the `--config` flag, all subsequent stages are skipped, and only the configuration files specified by the `--config` flag are loaded. + + + You can pass the `--config` flag multiple times in a single command; the configurations will be deep merged in the order you provide them. The first config you specify is the lowest priority, and the last one is the highest, allowing later configs to override settings from earlier ones. + + For example, to load multiple configuration files, you would run: + + ```bash + atmos --config /path/to/config1.yaml --config /path/to/config2.yaml ... + ``` + +
+ +
+ System directory +
+
+ When no `--config` flag is specified, Atmos will attempt to load the system-wide Atmos configuration (if found). + The system configuration has the lowest precedence of all configuration files, since it is loaded first. + + **On Windows:** + - `%PROGRAMDATA%/atmos/atmos.yaml` (e.g. `C:\ProgramData\atmos\atmos.yaml`) + + **On Linux or macOS:** + - `/usr/local/etc/atmos/atmos.yaml` + - `/etc/atmos/atmos.yaml` +
+
+ +### Stage 2: Discover Additional Configurations + +:::important +If the `--config` flag is used, this stage is skipped. +::: + +Atmos will search for additional configuration files in the following locations, in order of precedence, and deep merged the current configuration. The first location found is used: +
+ +
+ `ATMOS_CLI_CONFIG_PATH` environment variable +
+
+ The environment variable `ATMOS_CLI_CONFIG_PATH` should point to a folder (or a specific file). Multiple paths can be specified, separated by the system's standard path separator (`:` on Unix-like systems and `;` on Windows). + + Atmos will only search for the configuration file in the specified folder. Note that `.yaml` and `.yml` file extensions are treated interchangeably and are loaded in lexicographical order. Atmos will search for the following files, in order, relative to the `ATMOS_CLI_CONFIG_PATH` environment variable. + + If a path is a directory, Atmos will search for the following files in the directory, in order: (in order from highest to lowest priority): + + - `atmos.yaml` + - `atmos.d/**/*` +
+ +
+ Git Repository Root +
+
+ If a Git repository is detected, Atmos uses the root of the Git repository as the `ATMOS_CLI_CONFIG_PATH`. + + Atmos will search for the following files relative to the repository root, in order from highest to lowest priority: + + - `atmos.yaml` + - `.atmos.yaml` + - `atmos.d/**/*` + - `.atmos.d/**/*` + - `.github/atmos.yaml` + + Alternatively, set `ATMOS_CLI_CONFIG_PATH` to `!repo-root` to enforce this behavior. + + ```bash + export ATMOS_CLI_CONFIG_PATH="!repo-root" + ``` + +
+
+ +### Stage 3: Apply User Preferences + +:::important +If the `--config` flag is used, this stage is skipped. +::: + +After processing all configuration files, Atmos applies user preferences. Since user preferences are applied last, they have the highest precedence. This allows you to override any settings from previous stages to suit your specific needs, such as adjusting logging levels, configuring aliases, tweaking colors, or even adding your own custom commands. + +Preferences are deep-merged, meaning they override any conflicting sections defined in subsequent locations: + +
+
+ `$XDG_CONFIG_HOME/atmos/atmos.yaml` +
+
+ The `$XDG_CONFIG_HOME/atmos/atmos.yaml` file (if `$XDG_CONFIG_HOME` is set) allows the user to specify their preferences and overrides. +
+ +
+ User's Home Directory +
+
+ **On Windows:** + - `%LOCALAPPDATA%/atmos/atmos.yaml` (e.g., `C:\Users\\AppData\Roaming\atmos\atmos.yaml`) + + **On Linux or macOS:** + - `~/.config/atmos/atmos.yaml` + - `~/.atmos/atmos.yaml` +
+
+ + +## Imports + +Additionally, Atmos supports `imports` of other CLI configurations. Use imports to break large Atmos CLI configurations into smaller ones, such as organized by top-level section. File imports are relative to the base path (if `import` section is set in the config). All imports are processed at the time the configuration is loaded, and then deep-merged in order, so that the last file in the list supersedes settings in the preceding imports. For an example, see [`examples/demo-atmos-cli-imports`](https://github.com/cloudposse/atmos/tree/main/examples/demo-atmos-cli-imports). :::tip Pro-Tip Atmos supports [POSIX-style greedy Globs](https://en.wikipedia.org/wiki/Glob_(programming)) for all file names/paths (double-star/globstar `**` is supported as well) ::: +Imports can be any of the following: +- Remote URL (if `import` section is set in the config) +- Specific Path (if `import` section is set in the config) +- Wildcard globs (`*`), including recursive globs (`**`), can be combined (e.g., `**/*` matches all files and subfolders recursively). Only files ending in `.yml` or `.yaml` will be considered for import when using globs. + +For example, we can import from multiple locations like this: + +```yaml +import: + # Load the Atmos configuration from the main branch of the 'cloudposse/atmos' repository + - "https://raw.githubusercontent.com/cloudposse/atmos/refs/heads/main/atmos.yaml" + # Then load the repository root's configuration + - !repo-root + # Then merge the configs + - "configs.d/**/*" + # Finally, override some logging settings + - "./logs.yaml" +``` + +See [`examples/demo-configuration`](https://github.com/cloudposse/atmos/tree/main/examples/demo-configuration) for a demonstration. + +Note, templated imports of Atmos configuration are not supported (unlike stacks). + +:::warning Be Careful with Remote Imports +- Always use HTTPS URLs (currently correctly demonstrated in the example). +- Verify the authenticity of remote sources. +- Consider pinning to specific commit hashes instead of branch references +::: + +Each configuration file discovered is deep-merged with the preceding configurations. + +## Base Path + +The base path serves as the root directory for components, stacks, workflows, and Atmos configurations. It is determined only after all configuration files have been loaded and deep-merged. This sequence is essential because the configuration itself defines the base path. + +It can be set using the `ATMOS_BASE_PATH` environment variable or by passing the `--base-path` command-line argument. If both are set, the `--base-path` will take precedence. Both absolute and relative paths are supported. When a relative path is provided, it will be expanded to an absolute path relative to the current working directory. This is because Atmos executes commands that may change the working directory (e.g., Terraform). + +When `base_path` is explicitly set in the Atmos configuration, it is always resolved to an absolute path based on the current working directory from which `atmos` is executed, unless an absolute path is already provided. + +Atmos provides a function `!repo-root` to enforce that the root of the git repository is always used. + +For example, this will ensure Atmos always uses the root of the git repository as the base path: + +```yaml +base_path: !repo-root +``` + +When the `base_path` is not explicitly provided or is empty, Atmos will attempt to infer the appropriate path using the following strategy, in order of precedence: + + 1. Atmos will use the first matching condition to determine the `base_path`. If the current working directory contains any of the following files or directories: `atmos.yaml`, `.atmos.yaml`, any files matching the pattern `atmos.d/**/*`, or `.github/atmos.yaml`, Atmos will set the `base_path` to the directory that contains the first matching file or directory (e.g., the parent directory of `.github` if `.github/atmos.yaml` is the first match found). + 2. If a Git repository is detected, Atmos uses the root of the Git repository as the `base_path` (alternatively, use `!repo-root` to enforce this behavior). + 3. Lastly, Atmos defaults to the absolute path of `./` as the `base_path`. + +Once the `base_path` is determined, Atmos will use it as the root directory for components, stacks, workflows, and Atmos configurations and set the `ATMOS_BASE_PATH` environment variable to the resolved absolute path. + +Relative paths, such as `components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path`, `workflows.base_path`, `schemas.jsonschema.base_path`, and `schemas.opa.base_path`, are resolved relative to the specified `base_path`. Absolute paths for these settings will be respected and used as-is. + +Each setting (`components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path`, and `workflows.base_path`) operates independently and can accept either an absolute or a relative path. + + ## Default CLI Configuration If `atmos.yaml` is not found in any of the searched locations, Atmos will use the following default CLI configuration: @@ -117,22 +291,6 @@ If Atmos does not find an `atmos.yaml` file and the default CLI config is used, What follows are all the sections of the `atmos.yaml` configuration file. -## Base Path - -The base path for components, stacks, workflows and validation configurations. -It can also be set using `ATMOS_BASE_PATH` environment variable, or by passing the `--base-path` command-line argument. -It supports both absolute and relative paths. - -If not provided or is an empty string, `components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path` and `workflows.base_path` -are independent settings (supporting both absolute and relative paths). - -If `base_path` is provided, `components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path`, `workflows.base_path`, -`schemas.jsonschema.base_path` and `schemas.opa.base_path` are considered paths relative to `base_path`. - -```yaml -base_path: "." -``` - ## Settings The `settings` section configures Atmos global settings. @@ -178,7 +336,7 @@ The `settings` section configures Atmos global settings.
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.
- +
`settings.docs`
Specifies how component documentation is displayed in the terminal. @@ -624,7 +782,7 @@ Most YAML settings can also be defined by environment variables. This is helpful setting `ATMOS_STACKS_BASE_PATH` to a path in `/localhost` to your local development folder, will enable you to rapidly iterate. | Variable | YAML Path | Description | -|:------------------------------------------------------|:------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| :---------------------------------------------------- | :---------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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_VENDOR_BASE_PATH | vendor.base_path | Path to vendor configuration file or directory containing vendor files. If a directory is specified, all .yaml files in the directory will be processed in lexicographical order. Supports both absolute and relative paths. | @@ -648,26 +806,26 @@ setting `ATMOS_STACKS_BASE_PATH` to a path in `/localhost` to your local develop | ATMOS_WORKFLOWS_BASE_PATH | workflows.base_path | Base path to Atmos workflows | | ATMOS_SCHEMAS_JSONSCHEMA_BASE_PATH | schemas.jsonschema.base_path | Base path to JSON schemas for component validation | | ATMOS_SCHEMAS_OPA_BASE_PATH | schemas.opa.base_path | Base path to OPA policies for component validation | -| 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](/cli/schemas) | +| 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](/cli/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` | -| ATMOS_VERSION_CHECK_ENABLED | version.check.enabled | Enable/disable Atmos version checks for updates to the newest release | +| ATMOS_VERSION_CHECK_ENABLED | version.check.enabled | Enable/disable Atmos version checks for updates to the newest release | ### Context Some commands, like [`atmos terraform shell`](/cli/commands/terraform/shell), spawn an interactive shell with certain environment variables set, in order to enable the user to use other tools -(in the case of `atmos terraform shell`, the Terraform or Tofu CLI) natively, while still being configured for a -specific component and stack. To accomplish this, and to provide visibility and context to the user regarding the +(in the case of `atmos terraform shell`, the Terraform or Tofu CLI) natively, while still being configured for a +specific component and stack. To accomplish this, and to provide visibility and context to the user regarding the configuration, Atmos may set the following environment variables in the spawned shell: -| Variable | Description | -|:------------------------|:-------------------------------------------------------------------------------------------------------| -| ATMOS_COMPONENT | The name of the active component | -| ATMOS_SHELL_WORKING_DIR | The directory from which native commands should be run | -| ATMOS_SHLVL | The depth of Atmos shell nesting. When present, it indicates that the shell has been spawned by Atmos. | -| ATMOS_STACK | The name of the active stack | +| Variable | Description | +| :------------------------ | :----------------------------------------------------------------------------------------------------- | +| ATMOS_COMPONENT | The name of the active component | +| ATMOS_SHELL_WORKING_DIR | The directory from which native commands should be run | +| ATMOS_SHLVL | The depth of Atmos shell nesting. When present, it indicates that the shell has been spawned by Atmos. | +| ATMOS_STACK | The name of the active stack | | ATMOS_TERRAFORM_WORKSPACE | The name of the Terraform workspace in which Terraform comamnds should be run | -| PS1 | When a custom shell prompt has been configured in Atmos, the prompt will be set via `PS1` | -| TF_CLI_ARGS_* | Terraform CLI arguments to be passed to Terraform commands | +| PS1 | When a custom shell prompt has been configured in Atmos, the prompt will be set via `PS1` | +| TF_CLI_ARGS_* | Terraform CLI arguments to be passed to Terraform commands |