diff --git a/examples/locals/README.md b/examples/locals/README.md index 2ef55c4130..b1e86b254b 100644 --- a/examples/locals/README.md +++ b/examples/locals/README.md @@ -2,154 +2,34 @@ Reduce repetition and build computed values using file-scoped locals. -Learn more about [Locals](https://atmos.tools/core-concepts/stacks/locals/). +Learn more about [Locals](https://atmos.tools/stacks/locals). ## What You'll See - **Basic locals**: Define reusable values within a file - **Dependency resolution**: Locals can reference other locals -- **Context access**: Locals can access `settings`, `vars`, and `env` -- **Computed values**: Build complex values from simpler components +- **Context access**: Locals can access `settings`, `vars`, and `env` from the same file +- **File-scoped isolation**: Each stack file has independent locals ## Try It ```shell cd examples/locals -# View the resolved locals defined in the `dev` stack -atmos describe locals --stack dev +# View resolved locals for the dev stack +atmos describe locals -s dev -# View the resolved locals defined in the `prod` stack -atmos describe locals -s prod +# View resolved locals for a specific component +atmos describe locals myapp -s dev -# View the resolved locals for the `myapp` component in the `dev` stack -atmos describe locals myapp --stack dev - -# View the resolved locals for the `myapp` component in the `prod` stack -atmos describe locals myapp --stack prod - -# View the resolved variables for the `myapp` component in `dev` stack -atmos list vars myapp --stack dev - -# View the resolved variables for the `myapp` component in `prod` stack -atmos list vars myapp --stack prod - -# View the full `myapp` component configuration in `dev` stack -atmos describe component myapp -s dev - -# View the full `myapp` component configuration in `prod` stack -atmos describe component myapp -s prod -``` - -## Example Output - -The output below shows the resolved `locals` defined in the `dev` stack: - -```shell -$ atmos describe locals --stack dev -``` - -```yaml -locals: - app_version: v1 - default_tags: - Environment: development - ManagedBy: Atmos - Namespace: acme - Team: platform - environment: development - full_name: acme-development-dev - name_prefix: acme-development - namespace: acme - stage_name: dev -``` - -The output below shows the resolved `locals` for the `myapp` component in the `dev` stack: - -```shell -$ atmos describe locals myapp --stack dev -``` - -```yaml -components: - terraform: - myapp: - locals: - app_version: v1 - default_tags: - Environment: development - ManagedBy: Atmos - Namespace: acme - Team: platform - environment: development - full_name: acme-development-dev - name_prefix: acme-development - namespace: acme - stage_name: dev -``` - -The output below shows the resolved variables for the `myapp` component in the `dev` stack: - -```shell -$ atmos list vars myapp -s dev -``` - -```text -Key dev -──────────────────────────────────── - environment development - full_name acme-development-dev - name acme - stage dev +# Compare dev vs prod +atmos describe locals myapp -s prod ``` -## Key Concepts - -### Locals Reference Other Locals - -```yaml -locals: - namespace: acme - environment: dev - # Reference other locals - full_name: "{{ .locals.namespace }}-{{ .locals.environment }}" -``` - -### Locals Compose Values from Settings and Vars - -```yaml -settings: - team: platform - -vars: - stage: dev - -locals: - namespace: acme - - # Compose values from settings, vars, and other locals - resource_prefix: "{{ .locals.namespace }}-{{ .vars.stage }}" - owner_tag: "{{ .settings.team }}-team" - - # Build a tags map combining multiple sources - default_tags: - Namespace: "{{ .locals.namespace }}" - Stage: "{{ .vars.stage }}" - Owner: "{{ .locals.owner_tag }}" -``` - -### File-Scoped Isolation - -Locals are scoped to the file where they are defined. This means: - -- Locals defined in `dev.yaml` cannot be accessed from other files. -- Each file has its own independent `locals` scope. -- Use `vars` or `settings` for values that need to be shared across files. - ## Key Files -| File | Purpose | -|--------------------------------------|-------------------------------| -| `stacks/deploy/dev.yaml` | Development stack with locals | -| `stacks/deploy/prod.yaml` | Production stack with locals | -| `components/terraform/myapp/main.tf` | Terraform component | +| File | Purpose | +|------|---------| +| `stacks/deploy/dev.yaml` | Development stack with locals | +| `stacks/deploy/prod.yaml` | Production stack with locals | +| `components/terraform/myapp/main.tf` | Terraform component | diff --git a/website/docs/stacks/locals.mdx b/website/docs/stacks/locals.mdx index 4fb9b219f7..56c4d9ff3b 100644 --- a/website/docs/stacks/locals.mdx +++ b/website/docs/stacks/locals.mdx @@ -34,53 +34,15 @@ Locals are similar to [Terraform locals](https://developer.hashicorp.com/terrafo ## Configuration Scopes -The `locals` section can be defined at multiple levels within a single file. Each scope inherits from its parent scope within that file. +Locals can be defined at three levels within a file, each inheriting from its parent: -### Global Level - -Locals defined at the root level are available to all sections in the file: - -```yaml -# stacks/orgs/acme/plat/prod/us-east-1.yaml -locals: - namespace: acme - environment: prod - stage: us-east-1 - name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}" - -vars: - cluster_name: "{{ .locals.name_prefix }}-eks" ``` - -### Component-Type Level - -Locals defined under `terraform`, `helmfile`, or `packer` inherit from global locals and are available to all components of that type: - -```yaml -# stacks/orgs/acme/plat/prod/us-east-1.yaml -locals: - namespace: acme - environment: prod - -terraform: - locals: - # Inherits namespace and environment from global - backend_bucket: "{{ .locals.namespace }}-{{ .locals.environment }}-tfstate" - backend_key_prefix: "{{ .locals.environment }}" - - backend_type: s3 - backend: - s3: - bucket: "{{ .locals.backend_bucket }}" - key: "{{ .locals.backend_key_prefix }}/terraform.tfstate" +Global locals → Component-type locals (terraform/helmfile/packer) → Component-level locals ``` -### Using Locals in Components - -Components can reference merged locals (global + component-type) in their `vars`. When the same key is defined at multiple scopes, later scopes take precedence: component-type locals override global locals, and component-level locals override both. +When the same key exists at multiple levels, the most specific scope wins. ```yaml -# stacks/orgs/acme/plat/prod/us-east-1.yaml locals: namespace: acme environment: prod @@ -88,116 +50,54 @@ locals: terraform: locals: + # Inherits from global, adds terraform-specific locals backend_bucket: "{{ .locals.namespace }}-{{ .locals.environment }}-tfstate" components: terraform: vpc: + locals: + # Component-specific locals (inherits from global + terraform) + vpc_type: production vars: - # Uses merged locals (global + terraform section) - vpc_name: "{{ .locals.name_prefix }}-vpc" - bucket: "{{ .locals.backend_bucket }}" - - eks: - vars: - # Same merged locals available to all terraform components - cluster_name: "{{ .locals.name_prefix }}-eks" + name: "{{ .locals.name_prefix }}-{{ .locals.vpc_type }}-vpc" bucket: "{{ .locals.backend_bucket }}" ``` -:::tip Component-Level Locals -Components can also define their own `locals:` section. Component-level locals inherit from global and section-level locals, and also support inheritance from base components via `metadata.inherits`. See [Component-Level Locals](#component-level-locals) for details and the [describe locals command](/cli/commands/describe/locals) for inspecting them. -::: - -## Scope Inheritance (Within a File) - -Within a single file, locals follow this inheritance chain: - -``` -Global locals - ↓ -Component-type locals (terraform/helmfile/packer) - ↓ -Component-level locals (inside component definitions) -``` - -Each level can: -- Access locals from parent scopes -- Define new locals -- Override parent locals with new values - -Component-level locals also inherit from base components via `metadata.inherits` or `component` attribute. - -:::note Precedence -When the same key exists at multiple levels, the most specific scope wins. For terraform components, the full precedence chain is: - -**Global → Terraform Section → Base Component → Component** - -This means component-level locals override section-level, which override global locals. -::: - -### Example - -```yaml -locals: - env: prod # Global - -terraform: - locals: - env: production # Overrides global for terraform components - tf_version: "1.5" - -components: - terraform: - vpc: - vars: - # Uses merged locals: env = "production", tf_version = "1.5" - name: "{{ .locals.env }}-vpc" - terraform_version: "{{ .locals.tf_version }}" -``` - ## File-Scoped Isolation -**Important:** Unlike `vars`, `settings`, and `env`, locals do **not** inherit across file imports. Each file has its own isolated locals scope. +Unlike `vars`, `settings`, and `env`, locals do **not** inherit across file imports. Each file has its own isolated locals scope. ```yaml # These locals are ONLY available in this file locals: default_region: us-east-1 - default_tags: - ManagedBy: Atmos ``` - + ```yaml import: - catalog/defaults # The locals from catalog/defaults are NOT available here -# You must define your own locals in this file locals: - namespace: acme - environment: prod + namespace: acme # Define your own locals ``` -This design is intentional: -- **Predictability:** Locals in a file only come from that file -- **No Hidden Dependencies:** You can understand a file without tracing imports -- **Flexibility:** Each file can define locals that make sense for its context - -### Processing Order +This design ensures predictability—you can understand a file without tracing imports. -To understand why locals are file-scoped, it helps to know how Atmos processes stack files: +
+How does processing work? -1. **Per-File Processing:** Each file (including imported files) is processed independently -2. **Locals Resolution:** Within each file, locals are resolved first using only that file's context -3. **Import Merging:** After all files are processed, their sections (`vars`, `settings`, `env`, `components`) are merged according to import precedence -4. **Final Template Processing:** Templates in `vars`, `settings`, and other sections are processed with the fully merged context +Locals are resolved before imports are merged: -Since locals are resolved in step 2 (before imports are merged), they only have access to data from their own file. By the time imports are merged in step 3, locals resolution is already complete. +1. **Per-File Processing:** Each file is processed independently +2. **Locals Resolution:** Locals are resolved using only that file's context +3. **Import Merging:** Sections (`vars`, `settings`, `env`, `components`) are merged +4. **Final Template Processing:** Templates in other sections use the merged context ```mermaid flowchart TB @@ -214,70 +114,36 @@ flowchart TB end subgraph merge["3. Merge Phase"] - C1[Merge vars, settings, env, components from all files] - C2[locals are NOT merged - already resolved per-file] + C1[Merge vars, settings, env, components] + C2[locals already resolved per-file] C1 --> C2 end - subgraph templates["4. Template Processing"] - D1[Process templates in vars, settings, etc.] - D2[uses fully merged context] - D1 --> D2 - end - file1 --> merge file2 --> merge - merge --> templates + merge --> D1[4. Process templates in vars, settings, etc.] ``` -This architecture ensures that locals resolution is fast, predictable, and free of cross-file dependencies. +
## Dependency Resolution -Locals can reference other locals, and Atmos automatically resolves them in the correct order using topological sorting: +Locals can reference other locals in any order—Atmos resolves them automatically: ```yaml locals: - # These can be defined in any order - full_name: "{{ .locals.name_prefix }}-{{ .locals.component }}" - name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}" - namespace: acme - environment: prod - component: vpc + full_name: "{{ .locals.name_prefix }}-{{ .locals.component }}" # Resolved last + name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}" # Resolved second + namespace: acme # Resolved first + environment: prod # Resolved first + component: vpc # Resolved first ``` -Atmos resolves these in dependency order: -1. `namespace` and `environment` (no dependencies) -2. `component` (no dependencies) -3. `name_prefix` (depends on `namespace`, `environment`) -4. `full_name` (depends on `name_prefix`, `component`) - -## Circular Dependency Detection - -Atmos detects circular references and provides clear error messages: - -```yaml -locals: - a: "{{ .locals.b }}" - b: "{{ .locals.c }}" - c: "{{ .locals.a }}" # Creates a cycle! -``` - -Error output: -``` -circular dependency in locals at stacks/example.yaml - -Dependency cycle detected: - a → b → c → a -``` +Circular references are detected and reported with clear error messages showing the dependency cycle. ## Accessing Other Sections -Locals can access other sections defined in the **same file**. This is useful for building computed values based on your configuration. - -### Available Template Context - -Within a file, locals have access to: +Locals can access `settings`, `vars`, and `env` defined in the **same file**: | Section | Syntax | Description | |---------|--------|-------------| @@ -286,60 +152,35 @@ Within a file, locals have access to: | `vars` | `{{ .vars.key }}` | Variables defined in the same file | | `env` | `{{ .env.KEY }}` | Environment variables defined in the same file | -### Example: Accessing Settings - ```yaml settings: version: v1 - validation: - enabled: true - strict_mode: false - -locals: - # Access settings from the same file - api_version: "{{ .settings.version }}" - validation_level: '{{ if .settings.validation.strict_mode }}strict{{ else }}standard{{ end }}' - -vars: - version: "{{ .locals.api_version }}" -``` - -### Example: Accessing Vars -```yaml vars: - base_name: myapp - environment: production + stage: dev locals: - # Access vars from the same file - full_name: "{{ .vars.base_name }}-{{ .vars.environment }}" - log_path: "/var/log/{{ .vars.base_name }}" + namespace: acme + label: "{{ .locals.namespace }}-{{ .vars.stage }}-{{ .settings.version }}" ``` :::warning[Same-File Access Only] -Locals can only access `settings`, `vars`, and `env` defined in the **same file**. Values from imported files are not available during locals resolution because imports are processed separately. If you need to share values across files, use `vars` or `settings` instead of `locals`. +Locals cannot access `settings`, `vars`, or `env` from imported files. If you need values from imports, use `vars` instead—they inherit across files. ::: ```yaml -# ❌ This won't work - settings from imported file not available +# ❌ Won't work - imported settings not available to locals import: - catalog/defaults # Has settings.region = us-east-1 - locals: - region: "{{ .settings.region }}" # Error: settings.region not found -``` - -```yaml -# ✅ Instead, use vars which inherit across imports -import: - - catalog/defaults # Has vars.region = us-east-1 + region: "{{ .settings.region }}" # Error! +# ✅ Use vars instead - they inherit across imports vars: - computed_name: "{{ .vars.region }}-cluster" # Works! + computed_name: "{{ .vars.region }}-cluster" ``` -## Using Templates in Locals +## Using Templates Locals support full Go template syntax with [Sprig functions](http://masterminds.github.io/sprig/): @@ -347,251 +188,72 @@ Locals support full Go template syntax with [Sprig functions](http://masterminds locals: name: myapp environment: production - - # String manipulation upper_name: "{{ .locals.name | upper }}" - quoted_env: '{{ .locals.environment | quote }}' - - # Conditionals log_level: '{{ if eq .locals.environment "production" }}warn{{ else }}debug{{ end }}' - - # Complex expressions - resource_name: "{{ .locals.name }}-{{ .locals.environment | lower | trunc 4 }}" ``` ## Complex Values -Locals can contain maps and lists, not just strings: +Locals can contain maps and lists: ```yaml locals: namespace: acme environment: prod - # Map value default_tags: Namespace: "{{ .locals.namespace }}" Environment: "{{ .locals.environment }}" ManagedBy: Atmos - # List value availability_zones: - us-east-1a - us-east-1b - - us-east-1c - - # Nested structure - backend_config: - bucket: "{{ .locals.namespace }}-tfstate" - region: us-east-1 - encrypt: true vars: tags: "{{ .locals.default_tags }}" - azs: "{{ .locals.availability_zones }}" ``` -## Complete Example - - -```yaml -# Global locals - available throughout this file -locals: - # Base identifiers - namespace: acme - tenant: platform - environment: prod - stage: us-east-1 - - # Computed values - name_prefix: "{{ .locals.namespace }}-{{ .locals.tenant }}-{{ .locals.environment }}" - full_name: "{{ .locals.name_prefix }}-{{ .locals.stage }}" - - # Shared tags - default_tags: - Namespace: "{{ .locals.namespace }}" - Tenant: "{{ .locals.tenant }}" - Environment: "{{ .locals.environment }}" - Stage: "{{ .locals.stage }}" - ManagedBy: Atmos - -# Use locals in global vars -vars: - region: "{{ .locals.stage }}" - tags: "{{ .locals.default_tags }}" - -terraform: - # Terraform-specific locals (inherit from global) - locals: - backend_bucket: "{{ .locals.namespace }}-{{ .locals.environment }}-tfstate" - vpc_name: "{{ .locals.full_name }}-vpc" - cluster_name: "{{ .locals.full_name }}-eks" - - backend_type: s3 - backend: - s3: - bucket: "{{ .locals.backend_bucket }}" - region: "{{ .locals.stage }}" - key: terraform.tfstate - -# Components use merged locals (global + terraform section) -components: - terraform: - vpc: - vars: - name: "{{ .locals.vpc_name }}" - cidr_block: "10.0.0.0/16" - tags: - Name: "{{ .locals.vpc_name }}" - - eks: - vars: - cluster_name: "{{ .locals.cluster_name }}" - tags: - Name: "{{ .locals.cluster_name }}" -``` - - ## Component-Level Locals -Components can define their own `locals:` section that inherits from global and section-level locals. Component-level locals also support inheritance from base components. - -### Basic Component Locals - -```yaml -locals: - namespace: acme - -terraform: - locals: - backend_bucket: "{{ .locals.namespace }}-tfstate" - -components: - terraform: - vpc: - locals: - # Component-specific locals - vpc_type: production - cidr_prefix: "10.0" - vars: - name: "{{ .locals.namespace }}-{{ .locals.vpc_type }}-vpc" - cidr: "{{ .locals.cidr_prefix }}.0.0/16" -``` - -### Inheritance from Base Components - -Component-level locals support inheritance from base components via `metadata.inherits` or the `component` attribute. This follows the same inheritance pattern as `vars`: +Components can define their own locals that inherit from global and section-level locals. They also support inheritance from base components via `metadata.inherits`: ```yaml components: terraform: - # Base component (abstract) vpc/base: metadata: type: abstract locals: vpc_type: standard - cidr_prefix: "10.0" enable_nat: false - # Derived component - inherits and overrides locals vpc/production: metadata: inherits: - vpc/base locals: - vpc_type: production # Overrides base - enable_nat: true # Overrides base - # cidr_prefix inherited from base + vpc_type: production # Overrides base + enable_nat: true # Overrides base vars: name: "{{ .locals.vpc_type }}-vpc" - cidr: "{{ .locals.cidr_prefix }}.0.0/16" - nat_gateway_enabled: "{{ .locals.enable_nat }}" + nat_enabled: "{{ .locals.enable_nat }}" ``` -### Full Locals Resolution Order +Resolution order (later overrides earlier): **Global → Section → Base Component → Component** -For a component, locals are resolved in this order (later values override earlier): +## Debugging -``` -1. Global locals (root level) -2. Section locals (terraform/helmfile/packer) -3. Base component locals (from metadata.inherits chain) -4. Component locals (in the component definition) -``` - -:::note -Component-level locals appear in the final component output and can be inspected with `atmos describe locals -s `. They support the same template syntax as file-level locals. -::: - -## Scope Merging for Settings and Vars - -When locals access `settings` or `vars`, the values come from the **same file** with section-level values merged on top of global values. - -### How Merging Works - -```yaml -# Global settings -settings: - region: us-east-1 - team: platform - -# Global vars -vars: - stage: dev - instance_type: t3.medium - -terraform: - # Section-level settings (merged with global) - settings: - team: terraform-team # Overrides global - - # Section-level vars (merged with global) - vars: - instance_type: t3.large # Overrides global - - locals: - # Locals in terraform section see MERGED values: - # - .settings.region = "us-east-1" (from global) - # - .settings.team = "terraform-team" (section overrides global) - # - .vars.stage = "dev" (from global) - # - .vars.instance_type = "t3.large" (section overrides global) - resource_name: "{{ .settings.team }}-{{ .vars.stage }}" -``` - -This merging only happens within the same file. Values from imported files are not available. - -## Debugging Locals - -Use the `atmos describe locals` command to inspect resolved locals values. - -### View Stack Locals +Use `atmos describe locals` to inspect resolved values: ```shell -# Show all locals for a stack -atmos describe locals -s dev - -# Output as JSON -atmos describe locals -s dev --format json - -# Write to file -atmos describe locals -s dev --file locals.yaml +atmos describe locals -s dev # All locals for a stack +atmos describe locals vpc -s dev # Locals for a specific component +atmos describe locals -s dev --format json # Output as JSON ``` -### View Component Locals - -```shell -# Show locals available to a specific component -atmos describe locals vpc -s dev -``` - -This shows the merged locals (global + section + base component + component) that are available during template resolution for that component. - -### Example Output - -```shell -$ atmos describe locals -s dev -``` +
+Example output ```yaml locals: @@ -601,32 +263,9 @@ locals: terraform: locals: backend_bucket: acme-dev-tfstate -helmfile: - locals: - release_prefix: acme-helm ``` -### Debugging Template Errors - -If a local fails to resolve, check: - -1. **Is the referenced value defined in the same file?** - ```shell - # Look at the raw stack file - cat stacks/deploy/dev.yaml | grep -A5 "settings:" - ``` - -2. **Is the syntax correct?** - - Use `{{ .settings.key }}` not `{{ settings.key }}`. - - Use `{{ .locals.name }}` not `{{ locals.name }}`. - -3. **Are you trying to access imported values?** - - Locals cannot access settings/vars from imported files. - - Use `vars` in the template instead, which inherits across imports. - -4. **Is there a circular dependency?** - - Run with `--logs-level Debug` to see dependency resolution. - - Error messages show the dependency cycle if detected. +
## Locals vs Vars @@ -635,187 +274,53 @@ If a local fails to resolve, check: | **Scope** | File-scoped only | Inherits across imports | | **Purpose** | Temporary values for DRY | Input variables for components | | **Output** | Not passed to components | Passed to Terraform/Helmfile/Packer | -| **Dependencies** | Can reference other locals, settings, vars, env from same file | Can reference locals after they're resolved | -| **Visibility** | Internal to stack config | Visible in component execution | | **Cross-File Access** | Cannot access imported values | Can access merged values from all imports | -Use `locals` for intermediate computations within a single file, and `vars` for values that need to be passed to your components or shared across files. - -### When to Use Each - -**Use `locals` when:** -- You need temporary values to reduce repetition within a single file -- You're building computed values from other data in the same file -- The values are only relevant to template processing, not component execution - -**Use `vars` when:** -- Values need to be passed to Terraform/Helmfile/Packer components -- Values need to be shared across multiple files via imports -- You need to reference values from imported catalog files +Use `locals` for intermediate computations within a single file, and `vars` for values that need to be passed to components or shared across files. -## Template Processing Behavior +## Template Processing -When a file defines locals, template processing is automatically enabled for that file. This means any `{{ ... }}` syntax in the file will be processed as Go templates. +When a file defines locals, template processing is automatically enabled. Any `{{ ... }}` syntax will be processed. :::warning Conflicting Template Syntax -If your YAML files contain non-Atmos template syntax (such as Helm's `{{ ... }}`), adding locals will cause those templates to be processed incorrectly. To prevent this, you can disable template processing for specific imports: +If your YAML files contain Helm templates or other `{{ }}` syntax, use `skip_templates_processing`: ```yaml import: - path: catalog/helm-values - skip_templates_processing: true # Preserves {{ ... }} syntax as literal text + skip_templates_processing: true ``` - -This is useful when importing files that contain: -- Helm chart values with Go template syntax -- Other templating systems that use `{{ }}` -- Literal strings that happen to contain template-like patterns ::: ## Error Handling -When a template reference cannot be resolved, Atmos produces a clear error message rather than silently returning an empty string or passing through the literal template syntax. - -### Unresolved Template References - -If you reference a key that doesn't exist in the current scope, you'll get an error: +Unresolved template references produce clear error messages: ```yaml -# This file has NO settings section defined locals: - domain: "{{ .settings.region }}.example.com" # Error! + domain: "{{ .settings.region }}.example.com" # Error if no settings defined ``` -**Error output:** ``` -Error: failed to resolve local "domain" in stacks/deploy/example.yaml: - template: domain:1:12: executing "domain" at <.settings.region>: - map has no entry for key "settings" - -Available locals at this scope: (none) -Available context: - • .vars +Error: map has no entry for key "settings" ``` -The error message tells you: -- Which local failed to resolve -- Which key was missing -- What context is actually available - -### Common Error Scenarios - -| Scenario | Error Message | -|----------|---------------| -| Accessing `.settings` when no settings defined | `map has no entry for key "settings"` | -| Accessing `.settings.foo` when `foo` doesn't exist | `map has no entry for key "foo"` | -| Accessing `.vars` when no vars defined | `map has no entry for key "vars"` | -| Referencing `.locals.undefined` | `map has no entry for key "undefined"` | - -### Handling Optional Values - -If you need to handle cases where a value might not exist, use Go template conditionals: +For optional values, use Go template conditionals: ```yaml -settings: - # region might or might not be defined - region: us-east-1 - locals: - # Using 'with' for conditional access safe_region: '{{ with .settings }}{{ .region }}{{ else }}us-west-2{{ end }}' - - # Using index with default (requires the parent key to exist) - domain: '{{ index .settings "region" | default "us-west-2" }}.example.com' - - # Checking if a key exists before using it - log_path: '{{ if .settings }}{{ .settings.region }}{{ else }}default{{ end }}' ``` -### Cross-File Access Errors - -A common mistake is attempting to access `settings`, `vars`, or `env` from an imported file: - -```yaml -import: - - catalog/defaults # This file has settings.region = us-east-1 - -locals: - # ❌ This will fail with "map has no entry for key 'settings'" - # because the imported settings aren't available during locals resolution - region: "{{ .settings.region }}" -``` - -**Why this happens:** Locals are resolved *before* imports are merged. At the time locals are processed, only the current file's `settings`, `vars`, and `env` are available. - -**Solution:** Define the values you need in the same file, or use `vars` which properly inherit across imports: - -```yaml -import: - - catalog/defaults # Has vars.region = us-east-1 - -# Option 1: Define settings in this file -settings: - region: us-east-1 - -locals: - domain: "{{ .settings.region }}.example.com" # ✅ Works - -# Option 2: Use vars in templates (not locals) -vars: - # vars can access imported values after merge - domain: "{{ .vars.region }}.example.com" # ✅ Works -``` - -## Limitations - -Locals are file-scoped by design. Key limitations: - -- **Cannot access inherited values**: Locals only see `settings`, `vars`, and `env` from the same file, not from imports. -- **Cannot be shared across files**: Locals defined in one file aren't accessible from other files. -- **No lazy evaluation**: All locals resolve during file processing, before imports merge. - -**Workaround:** Use `vars` or `settings` for values that need cross-file access—they properly inherit across imports. - ## Best Practices -1. **Use for Repetition:** If you find yourself repeating the same value or expression, extract it to a local. - -2. **Build Incrementally:** Start with simple locals and compose them into more complex values: - ```yaml - locals: - namespace: acme - env: prod - prefix: "{{ .locals.namespace }}-{{ .locals.env }}" # Build on simpler locals - bucket: "{{ .locals.prefix }}-assets" # Build on prefix - ``` - -3. **Keep Locals Close:** Define locals near where they're used. If a local is only used in one component, define it at the component level. - -4. **Use Descriptive Names:** Choose names that describe what the value represents, not how it's computed. - -5. **Avoid Deep Nesting:** If you have many levels of local dependencies, consider simplifying or restructuring. - -6. **Remember File Scope:** Don't expect locals from imported files—define what you need in each file. - -7. **Use Vars for Cross-File Sharing:** If you need values available across multiple files, put them in `vars` or `settings` instead of `locals`. These sections properly merge across imports: - ```yaml - # stacks/catalog/defaults.yaml - vars: - shared_region: us-east-1 # Available to all files that import this - - # stacks/deploy/production.yaml - import: - - catalog/defaults - - vars: - # Can reference vars from imported files - bucket_name: "assets-{{ .vars.shared_region }}" - ``` +1. **Extract repetition** to locals instead of duplicating values. +2. **Build incrementally**—compose complex values from simpler locals. +3. **Keep locals close** to where they're used (component-level when possible). +4. **Use vars for cross-file sharing**—locals are file-scoped by design. ## Try It -Explore a working example that demonstrates locals in action. - ## Related @@ -824,4 +329,3 @@ Explore a working example that demonstrates locals in action. - [Environment Variables (env)](/stacks/env) - [Settings](/stacks/settings) - [Imports](/stacks/imports) -- [YAML Functions](/functions/yaml)