Skip to content

Conversation

RoseSecurity
Copy link
Contributor

@RoseSecurity RoseSecurity commented Sep 4, 2025

what

  • This introduces support for Ansible commands within Atmos! This includes new subcommands for playbook execution, inventory management, vault operations, and version display.
  • It also adds error handling for Ansible operations, usage documentation, and an example to illustrate best practices for integrating Ansible with Atmos stacks and components.

  • Added the base ansible command and subcommands for playbook, inventory, vault, and version in the cmd/ package, each with its own usage, help, and integration with Atmos stack/component configuration.
  • Each subcommand is registered with the base ansible command and delegates execution to a shared handler, supporting relevant flags and options for Ansible operations.
  • Added markdown usage files for each Ansible subcommand, providing clear CLI examples and explanations.
  • Linked new documentation pages in the examples map for help suggestions in the CLI.
  • Added unit tests for each new command to verify configuration, flag parsing, and command registration.
  • Introduced specific error variables for Ansible-related failures to improve diagnostics.
  • Added a demo in examples/demo-ansible showing how to structure Ansible components, stacks, settings, and environment variables within Atmos, including playbooks, inventory, and configuration files.

why

  • Add native support so that Atmos' powerful deep-merging abilities can be expanded to Ansible configuration management!

references

Note

This PR is primarily based on the Packer implementation in this PR

Summary by CodeRabbit

  • New Features

    • Introduced “atmos ansible” with subcommands: playbook, inventory, vault, and version.
    • Supports --stack (-s), --playbook (-p), and --inventory (-i) flags.
    • Ansible components are now discoverable in component listings.
  • Documentation

    • Added comprehensive CLI docs and embedded help for Ansible commands.
    • Included a full demo project showcasing Ansible integration and usage.
  • Tests

    • Added unit tests for Ansible commands, helpers, and path utilities.
  • Chores

    • Extended schemas and configuration to natively support Ansible components.

@RoseSecurity RoseSecurity added enhancement New feature or request feature New functionality needs-cloudposse Needs Cloud Posse assistance size/xl Extra large size PR labels Sep 4, 2025
Copy link

mergify bot commented Sep 4, 2025

Warning

This PR exceeds the recommended limit of 1,000 lines.

Large PRs are difficult to review and may be rejected due to their size.

Please verify that this PR does not address multiple issues.
Consider refactoring it into smaller, more focused PRs to facilitate a smoother review process.

Copy link

mergify bot commented Sep 4, 2025

Important

Cloud Posse Engineering Team Review Required

This pull request modifies files that require Cloud Posse's review. Please be patient, and a core maintainer will review your changes.

To expedite this process, reach out to us on Slack in the #pr-reviews channel.

Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

golangci-lint found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@cloudposse cloudposse deleted a comment from github-actions bot Sep 4, 2025
@RoseSecurity
Copy link
Contributor Author

For the Golang lint failures, should I suppress (//nolint:unused) or handle another way?

Add schema definition and implementation for Ansible components:
- Add ansible section to atmos-manifest.json schema
- Update demo-ansible example with proper schema reference
- Modify list_components.go to support extracting ansible components
- Refactor code to improve component type extraction
@RoseSecurity
Copy link
Contributor Author

Is there also a good way to view the website to ensure it looks good locally?

Replace strict component type validation with a more tolerant approach
that doesn't error when specific component types are missing. This
allows stacks to have any combination of component types without
causing parsing errors.

- Remove specific error types for each component type
- Initialize empty maps and only populate when components exist
- Update comment to reflect that function handles all component types
@osterman
Copy link
Member

osterman commented Sep 5, 2025

Is there also a good way to view the website to ensure it looks good locally?

Yep! Try this and let us know if you run into problems.

cd website/
make up

@osterman
Copy link
Member

osterman commented Sep 5, 2025

For the Golang lint failures, should I suppress (//nolint:unused) or handle another way?

Nono, those are legit. Taking a look at one of those, I can see ansibleUsageMarkdown is declared and never accessed. That means it should be deleted.

@osterman
Copy link
Member

osterman commented Sep 5, 2025

@coderabbitai review

Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

📝 Walkthrough

Walkthrough

Adds first-class Ansible support to Atmos: new CLI group and subcommands (playbook, inventory, vault, version), internal executor and utils, config/env/schema plumbing, stack processing integration, path/describe-affected updates, error types, list-components awareness, extensive docs, and a full demo under examples. Public APIs updated where stack processing signatures gained Ansible base-path parameters.

Changes

Cohort / File(s) Summary
CLI: Ansible command group
cmd/ansible.go, cmd/ansible_playbook.go, cmd/ansible_inventory.go, cmd/ansible_vault.go, cmd/ansible_version.go
Adds Cobra-based ansible root and subcommands; embeds usage; wires flags (--playbook, --inventory); delegates to shared runner.
CLI tests
cmd/ansible_playbook_test.go, cmd/ansible_inventory_test.go, cmd/ansible_vault_test.go, cmd/ansible_version_test.go
Verifies command metadata and handlers for each subcommand.
Executor core
internal/exec/ansible.go
Implements ExecuteAnsible, validation, settings resolution, arg/env prep, varfile handling, execution, cleanup; supports playbook/inventory/vault/version.
Executor helpers & tests
internal/exec/ansible_utils.go, internal/exec/ansible_utils_test.go, internal/exec/ansible_test.go, internal/exec/path_utils.go, internal/exec/path_utils_test.go
Adds getters for ansible settings, path/varfile constructors, and corresponding tests.
Stack processing: exec
internal/exec/stack_processor_utils.go, internal/exec/utils.go, internal/exec/validate_stacks.go, internal/exec/describe_affected_utils.go
Integrates Ansible as a component type; extends processing and validation flows; threads Ansible base path; updates describe-affected paths.
Stack processing: public API
pkg/stack/stack_processor.go, pkg/stack/stack_processor_test.go
Extends ProcessYAMLConfigFiles signature with ansibleComponentsBasePath; passes through to exec; updates tests.
Spacelift integration
pkg/spacelift/spacelift_stack_processor.go
Threads new ansibleComponentsBasePath arg through stack processing calls.
Config and schema
pkg/config/config.go, pkg/config/default.go, pkg/config/const.go, pkg/config/utils.go, pkg/schema/schema.go, pkg/component/atmos.yaml
Adds Ansible component config (command/base_path), absolute path resolution, env var overrides, constants, and schema fields.
Public JSON Schemas
pkg/datafetcher/schema/.../manifest/1.0.json, .../config/global/1.0.json, .../stacks/stack-config/1.0.json
Adds Ansible sections/definitions; includes in root validation branches and components mapping.
Errors
errors/errors.go
Adds exported Ansible-related error variables.
List components
pkg/list/list_components.go, pkg/list/list_components_test.go
Extends to include non-Terraform components (incl. Ansible); removes Terraform-specific parse error; updates tests.
Markdown help registry
cmd/markdown_help.go
Registers Ansible help topics in examples map.
Embedded CLI usage docs
cmd/markdown/*atmos_ansible*_usage.md
Adds usage markdown for ansible, playbook, inventory, vault, version.
Website docs
website/docs/cli/commands/ansible/_category_.json, website/docs/cli/commands/ansible/*.mdx
Adds Docusaurus pages for Ansible commands and usage.
Demo: examples
examples/demo-ansible/**
Adds a complete Ansible demo: atmos.yaml, stacks, component (webapp) with playbook, inventory, templates, vars, schema, and README.
Misc
internal/exec/... (no other), pkg/utils/type_utils.go
Minor Coalesce return style change.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as Cobra (ansible/*)
  participant Core as ansibleRun
  participant Cfg as Config/Stacks Resolver
  participant Exec as ExecuteAnsible
  participant OS as System Shell

  User->>CLI: atmos ansible playbook <component> -s <stack> [flags] -- [ansible-opts]
  CLI->>Core: RunE(cmd, subcmd, args)
  Core->>Cfg: getConfigAndStacksInfo("ansible", ...)
  Cfg-->>Core: ConfigAndStacksInfo
  Core->>Exec: ExecuteAnsible(info, flags)
  Exec->>Exec: Validate stack/component/permissions
  Exec->>Exec: Resolve playbook/inventory (flags > settings)
  Exec->>Exec: Write vars file (if needed)
  Exec->>Exec: Build args/env and working dir
  Exec->>OS: Run ansible[-playbook|-inventory|-vault] ...
  OS-->>Exec: Exit code/stdout/stderr
  Exec->>Exec: Cleanup vars file (on success/fail as applicable)
  Exec-->>Core: error/nil
  Core-->>CLI: error/nil
  CLI-->>User: Output/exit code
Loading
sequenceDiagram
  autonumber
  actor User
  participant CLI as Cobra (ansible version)
  participant Core as ansibleRun
  participant Cfg as Config Resolver
  participant Exec as ExecuteAnsible
  participant OS as System Shell

  User->>CLI: atmos ansible version
  CLI->>Core: RunE(cmd,"version",[])
  Core->>Cfg: getConfigAndStacksInfo(...)
  Cfg-->>Core: info(SubCommand="version")
  Core->>Exec: ExecuteAnsible(info)
  Exec->>OS: Run ansible --version
  OS-->>Exec: Version output
  Exec-->>Core: nil
  Core-->>User: Version output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-atmos-ansible-functionality

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or @auto-summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @auto-title anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
pkg/datafetcher/schema/config/global/1.0.json (1)

3-5: Update Global Config schema $id and title
In pkg/datafetcher/schema/config/global/1.0.json (lines 3–4), replace the manifest schema identifiers to avoid resolver collisions:

-"$id": "https://json.schemastore.org/atmos-manifest.json",
-"title": "JSON Schema for Atmos Stack Manifest files. Version 1.0. https://atmos.tools",
+"$id": "https://atmos.tools/schemas/config/global/1.0.json",
+"title": "JSON Schema for Atmos Global Config files. Version 1.0. https://atmos.tools",
pkg/datafetcher/schema/stacks/stack-config/1.0.json (1)

3-5: $id/title reference the manifest schema, not the stack-config schema.

Rename to the proper Stack Config identity to prevent ambiguity.

-"$id": "https://json.schemastore.org/atmos-manifest.json",
-"title": "JSON Schema for Atmos Stack Manifest files. Version 1.0. https://atmos.tools",
+"$id": "https://atmos.tools/schemas/stacks/stack-config/1.0.json",
+"title": "JSON Schema for Atmos Stack Config files. Version 1.0. https://atmos.tools",
internal/exec/describe_affected_utils.go (1)

667-876: Ansible components are not considered in affected detection

findAffected handles Terraform/Helmfile/Packer, but there’s no analogous block for cfg.AnsibleComponentType. That means Ansible changes won’t surface in describe affected. Add a section mirroring Packer/Helmfile (metadata/env/vars/settings checks + component folder diff).

Here’s a minimal insertion (just after the Packer section):

+               // Ansible
+               if ansibleSection, ok := componentsSection[cfg.AnsibleComponentType].(map[string]any); ok {
+                 for componentName, compSection := range ansibleSection {
+                   if componentSection, ok := compSection.(map[string]any); ok {
+                     if metadataSection, ok := componentSection["metadata"].(map[string]any); ok {
+                       if metadataType, ok := metadataSection["type"].(string); ok && metadataType == "abstract" { continue }
+                       if !isComponentEnabled(metadataSection, componentName) { continue }
+                       if excludeLocked && isComponentLocked(metadataSection) { continue }
+                       if !isEqual(remoteStacks, stackName, cfg.AnsibleComponentType, componentName, metadataSection, "metadata") {
+                         affected := schema.Affected{ ComponentType: cfg.AnsibleComponentType, Component: componentName, Stack: stackName, Affected: "stack.metadata" }
+                         if err = appendToAffected(atmosConfig, componentName, stackName, &componentSection, &res, &affected, false, nil, includeSettings); err != nil { return nil, err }
+                       }
+                     }
+                     if component, ok := componentSection[cfg.ComponentSectionName].(string); ok && component != "" {
+                       changed, err := isComponentFolderChanged(component, cfg.AnsibleComponentType, atmosConfig, changedFiles)
+                       if err != nil { return nil, err }
+                       if changed {
+                         affected := schema.Affected{ ComponentType: cfg.AnsibleComponentType, Component: componentName, Stack: stackName, Affected: "component" }
+                         if err = appendToAffected(atmosConfig, componentName, stackName, &componentSection, &res, &affected, false, nil, includeSettings); err != nil { return nil, err }
+                       }
+                     }
+                     if varSection, ok := componentSection["vars"].(map[string]any); ok {
+                       if !isEqual(remoteStacks, stackName, cfg.AnsibleComponentType, componentName, varSection, "vars") {
+                         affected := schema.Affected{ ComponentType: cfg.AnsibleComponentType, Component: componentName, Stack: stackName, Affected: "stack.vars" }
+                         if err = appendToAffected(atmosConfig, componentName, stackName, &componentSection, &res, &affected, false, nil, includeSettings); err != nil { return nil, err }
+                       }
+                     }
+                     if envSection, ok := componentSection["env"].(map[string]any); ok {
+                       if !isEqual(remoteStacks, stackName, cfg.AnsibleComponentType, componentName, envSection, "env") {
+                         affected := schema.Affected{ ComponentType: cfg.AnsibleComponentType, Component: componentName, Stack: stackName, Affected: "stack.env" }
+                         if err = appendToAffected(atmosConfig, componentName, stackName, &componentSection, &res, &affected, false, nil, includeSettings); err != nil { return nil, err }
+                       }
+                     }
+                     if settingsSection, ok := componentSection[cfg.SettingsSectionName].(map[string]any); ok {
+                       if !isEqual(remoteStacks, stackName, cfg.AnsibleComponentType, componentName, settingsSection, cfg.SettingsSectionName) {
+                         affected := schema.Affected{ ComponentType: cfg.AnsibleComponentType, Component: componentName, Stack: stackName, Affected: "stack.settings" }
+                         if err = appendToAffected(atmosConfig, componentName, stackName, &componentSection, &res, &affected, false, nil, includeSettings); err != nil { return nil, err }
+                       }
+                       var stackComponentSettings schema.Settings
+                       if err = mapstructure.Decode(settingsSection, &stackComponentSettings); err != nil { return nil, err }
+                       if !reflect.ValueOf(stackComponentSettings).IsZero() && !reflect.ValueOf(stackComponentSettings.DependsOn).IsZero() {
+                         isChanged, changedType, changedFoF, err := isComponentDependentFolderOrFileChanged(changedFiles, stackComponentSettings.DependsOn)
+                         if err != nil { return nil, err }
+                         if isChanged {
+                           affected := schema.Affected{
+                             ComponentType: cfg.AnsibleComponentType, Component: componentName, Stack: stackName, Affected: changedType,
+                             File: func() string { if changedType == "file" { return changedFoF } ; return "" }(),
+                             Folder: func() string { if changedType == "folder" { return changedFoF } ; return "" }(),
+                           }
+                           if err = appendToAffected(atmosConfig, componentName, stackName, &componentSection, &res, &affected, false, nil, includeSettings); err != nil { return nil, err }
+                         }
+                       }
+                     }
+                   }
+                 }
+               }

Also ensure isComponentFolderChanged supports cfg.AnsibleComponentType by resolving atmosConfig.AnsibleDirAbsolutePath.

I can open a follow-up PR adding tests proving Ansible components are detected as affected.

pkg/list/list_components.go (1)

23-62: Generalize: don’t hardcode component types

Hardcoding terraform/helmfile/packer/ansible will require code changes for every new type. Iterate all entries under "components" and collect map keys.

Apply this refactor in-place:

-	// Initialize empty maps for each component type
-	terraformComponents := make(map[string]any)
-	helmfileComponents := make(map[string]any)
-	packerComponents := make(map[string]any)
-	ansibleComponents := make(map[string]any)
-
-	// Only try to get components if they exist, no error if they don't
-	if comp, ok := componentsMap["terraform"].(map[string]any); ok {
-		terraformComponents = comp
-	}
-
-	if comp, ok := componentsMap["helmfile"].(map[string]any); ok {
-		helmfileComponents = comp
-	}
-
-	if comp, ok := componentsMap["packer"].(map[string]any); ok {
-		packerComponents = comp
-	}
-
-	if comp, ok := componentsMap["ansible"].(map[string]any); ok {
-		ansibleComponents = comp
-	}
-
-	// Merge all component maps into one.
-	allComponents := lo.Assign(terraformComponents, helmfileComponents, packerComponents, ansibleComponents)
-
-	return lo.Keys(allComponents), nil
+	// Aggregate keys from all known/unknown component groups
+	keys := make([]string, 0, 16)
+	for _, v := range componentsMap {
+		if m, ok := v.(map[string]any); ok {
+			keys = append(keys, lo.Keys(m)...)
+		}
+	}
+	return lo.Uniq(keys), nil
internal/exec/stack_processor_utils_test.go (1)

630-637: Include the new Ansible component path and environment args in all ProcessStackConfig calls

  • internal/exec/validate_stacks.go (around line 177): current invocation only passes atmosConfig and StacksBaseAbsolutePath; it must also include the Terraform, Helmfile, Packer and Ansible component paths plus the environment string.
  • pkg/stack/stack_processor.go (around line 99): wrapper for exec.ProcessStackConfig needs to forward the additional arguments.
  • internal/exec/stack_processor_utils.go (around line 108): nested call to ProcessStackConfig must be updated to match the new signature.
🧹 Nitpick comments (72)
pkg/utils/type_utils.go (1)

5-6: Tighten the comment (“non-empty” → “non-zero” for comparable T).

Since T is comparable and this wraps lo.Coalesce, it returns the first non-zero value; if none, zero value. Consider updating the doc to reflect that.

-// Coalesce returns the first non-empty argument. Arguments must be comparable
+// Coalesce returns the first non-zero argument for T (or the zero value if none). T must be comparable.
examples/demo-ansible/components/ansible/webapp/README.md (4)

3-9: Normalize NGINX casing for consistency.

Use a single casing throughout (recommend “NGINX”).

-This is a demo Ansible component that configures a simple web application using nginx.
+This is a demo Ansible component that configures a simple web application using NGINX.
@@
-- `templates/app.conf.j2` - Nginx configuration template
+- `templates/app.conf.j2` - NGINX configuration template

8-8: Clarify inventory group naming.

If webservers is a literal group, format it as code to avoid confusion with “web servers.”

-- `inventory.yml` - Default inventory with webservers and load balancers
+- `inventory.yml` - Default inventory with `webservers` and load balancers

15-20: Document defaults and sources for vars.

Please state where environment derives its value (e.g., from the Atmos stack) and confirm the default for target_hosts is indeed webservers if not overridden.


11-21: Add a short “Prerequisites” section.

Suggest listing required tools (Atmos, Ansible), and any expected inventory/plugin assumptions. Keeps newcomers on the happy path.

Example to add after “Configuration”:

  • Atmos installed and configured
  • Ansible installed (version X+)
  • SSH access to target hosts
cmd/markdown/atmos_ansible_version_usage.md (1)

1-5: Fix markdownlint nits (code fence language, prompt symbol) and minor grammar.

This addresses MD040/MD014 and reads a bit clearer.

Apply:

-– Show the version of ansible command
+Show the version of the Ansible command
-
-```
-$ atmos ansible version
-```
+```sh
+atmos ansible version
+```

Also add a trailing newline at EOF.

cmd/markdown/atmos_ansible_inventory_usage.md (1)

1-11: Normalize headings and fix markdownlint issues (language + prompt).

Keeps style consistent and satisfies MD040/MD014.

Apply:

-– Display the configured inventory
+Display the configured inventory
-
-```
-$ atmos ansible inventory <component-name> -s <stack-name> --list
-```
+```sh
+atmos ansible inventory <component-name> -s <stack-name> --list
+```
-
-– Display inventory for specific host
+Display inventory for a specific host
-
-```
-$ atmos ansible inventory <component-name> -s <stack-name> --host <hostname>
-```
+```sh
+atmos ansible inventory <component-name> -s <stack-name> --host <hostname>
+```
cmd/markdown/atmos_ansible_vault_usage.md (1)

1-17: Add code fence languages and drop prompt symbol to satisfy markdownlint; keep headings concise.

Resolves MD040/MD014 across all blocks.

Apply:

-– Encrypt and decrypt files with Ansible Vault
+Encrypt and decrypt files with Ansible Vault
-
-```
-$ atmos ansible vault <component-name> -s <stack-name> encrypt <file>
-```
+```sh
+atmos ansible vault <component-name> -s <stack-name> encrypt <file>
+```
-
-– Decrypt a vault file
+Decrypt a vault file
-
-```
-$ atmos ansible vault <component-name> -s <stack-name> decrypt <file>
-```
+```sh
+atmos ansible vault <component-name> -s <stack-name> decrypt <file>
+```
-
-– Create a new encrypted file
+Create a new encrypted file
-
-```
-$ atmos ansible vault <component-name> -s <stack-name> create <file>
-```
+```sh
+atmos ansible vault <component-name> -s <stack-name> create <file>
+```
cmd/markdown/atmos_ansible_usage.md (1)

1-13: Fix markdownlint and style (add language, drop prompts, capitalize Ansible).

Add code-fence languages and remove $ to satisfy MD040/MD014. Also capitalize “Ansible.”

-– Execute ansible commands for configuration management
+Execute Ansible commands for configuration management

-```
-$ atmos ansible playbook <component-name> -s <stack-name>
-```
+```bash
+atmos ansible playbook <component-name> -s <stack-name>
+```

-```
-$ atmos ansible inventory <component-name> -s <stack-name> --list
-```
+```bash
+atmos ansible inventory <component-name> -s <stack-name> --list
+```

-```
-$ atmos ansible vault <component-name> -s <stack-name> encrypt <file>
-```
+```bash
+atmos ansible vault <component-name> -s <stack-name> encrypt <file>
+```
examples/demo-ansible/components/ansible/webapp/ansible.cfg (2)

2-2: Host key checking disabled — call out as demo-only.

Leaving this False is risky in real environments. Add a warning comment or flip to True for non-demo use.

 [defaults]
- host_key_checking = False
+ # WARNING: Demo setting. Set to True in production to verify SSH host keys.
+ host_key_checking = False

13-15: Use hashed ControlPath to avoid “too long” UNIX socket paths.

Switch to %%C to hash the ControlPath (safer on long paths).

 [ssh_connection]
 ssh_args = -o ControlMaster=auto -o ControlPersist=60s
 pipelining = True
-control_path = ~/.ansible/cp/ansible-ssh-%%h-%%p-%%r
+control_path = ~/.ansible/cp/%%C
cmd/markdown/atmos_ansible_playbook_usage.md (1)

1-17: Fix markdownlint and style (add language, drop prompts, normalize headings).

Add bash fence language and remove $ to satisfy MD040/MD014. Normalize headings.

-– Execute an Ansible playbook
+Execute an Ansible playbook

-```
-$ atmos ansible playbook <component-name> -s <stack-name>
-```
+```bash
+atmos ansible playbook <component-name> -s <stack-name>
+```

-– Execute with custom playbook
+Execute with custom playbook

-```
-$ atmos ansible playbook <component-name> -s <stack-name> --playbook custom.yml
-```
+```bash
+atmos ansible playbook <component-name> -s <stack-name> --playbook custom.yml
+```

-– Execute with custom inventory
+Execute with custom inventory

-```
-$ atmos ansible playbook <component-name> -s <stack-name> --inventory inventory.yml
-```
+```bash
+atmos ansible playbook <component-name> -s <stack-name> --inventory inventory.yml
+```
pkg/config/utils.go (1)

348-359: Nice: ENV overrides for Ansible; add CLI flag parity + viper binding.

  • Parity: Terraform/Helmfile/Packer have CLI override handlers; add analogous Ansible handling (e.g., setAnsibleConfig) and wire it into processCommandLineArgs.
  • Binding: ensure ATMOS_COMPONENTS_ANSIBLE_COMMAND and ATMOS_COMPONENTS_ANSIBLE_BASE_PATH are bound via the selective setEnv/bindEnv pattern (per repo convention), not just read via os.Getenv here.

I can draft the setAnsibleConfig helper and the bindEnv entries if you confirm the intended flag names.

examples/demo-ansible/components/ansible/webapp/group_vars/webservers.yml (1)

22-22: Add newline at EOF.

Satisfy YAMLlint’s new-line-at-end-of-file rule.

 status_page_location: "/nginx_status"
+ 
examples/demo-ansible/components/ansible/webapp/templates/app.conf.j2 (4)

12-16: Prefer default_type over add_header for /health Content-Type

Set default_type and drop the header; simpler and avoids surprises.

-    location /health {
-        access_log off;
-        return 200 "OK - {{ app_name }} v{{ app_version }} ({{ app_env }})\n";
-        add_header Content-Type text/plain;
-    }
+    location /health {
+        access_log off;
+        default_type text/plain;
+        return 200 "OK - {{ app_name }} v{{ app_version }} ({{ app_env }})\n";
+    }

2-2: Add IPv6 listener for parity

Expose the same port on IPv6.

-    listen {{ app_port }};
+    listen {{ app_port }};
+    listen [::]:{{ app_port }};

18-21: Harden headers (optional)

Consider adding CSP, Referrer-Policy, Permissions-Policy, and HSTS (effective over HTTPS).

     # Security headers
     add_header X-Frame-Options "SAMEORIGIN" always;
     add_header X-Content-Type-Options "nosniff" always;
     add_header X-XSS-Protection "1; mode=block" always;
+    add_header Referrer-Policy "no-referrer" always;
+    add_header Permissions-Policy "geolocation=()" always;
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+    server_tokens off;

22-22: Add trailing newline

Ends without a newline; some linters complain.

examples/demo-ansible/components/ansible/webapp/inventory.yml (3)

17-17: Fix YAML lint nits: trailing spaces + EOF newline

Trim trailing spaces (Line 17) and add a newline at EOF (Line 30).

-        
+
@@
-    ansible_python_interpreter: /usr/bin/python3
+    ansible_python_interpreter: /usr/bin/python3
+

Also applies to: 30-30


14-16: Prefer group_vars over inline vars in inventory

Move group-level vars to group_vars/{webservers,loadbalancers}.yml; keeps inventory focused on host/group membership.

Also applies to: 23-25, 27-30


27-30: Host key checking disabled — confirm intent

'-o StrictHostKeyChecking=no' is fine for demos; avoid in real environments. Consider gating by environment or using Ansible's host key checking setting.

Would you like me to propose a minimal prod-safe variant that keeps this only for non-prod stacks?

examples/demo-ansible/components/ansible/webapp/site.yml (2)

35-41: Make the symlink creation resilient

Force replacement if the dest exists as a file/dir.

     - name: Enable app site
       file:
         src: "/etc/nginx/sites-available/{{ app_name }}"
         dest: "/etc/nginx/sites-enabled/{{ app_name }}"
         state: link
+        force: yes
       notify: reload nginx

60-60: Add trailing newline

Ends without a newline; minor lint warning.

examples/demo-ansible/components/ansible/webapp/group_vars/all.yml (1)

21-21: Add trailing newline

Minor lint fix.

examples/demo-ansible/stacks/catalog/demo.yaml (1)

22-30: Consider documenting pass-through flags for Ansible.

Readers may expect to pass Ansible flags using -- (e.g., --check, --list). Suggest adding a short note here mirroring the usage shown later for playbook.

examples/demo-ansible/README.md (1)

17-36: Add a language to the fenced code block (fixes MD040).

Use text for the directory tree.

Apply:

-```
+```text
 demo-ansible/
 ├── atmos.yaml              # Atmos configuration
 ...
-```
+```
pkg/datafetcher/schema/config/global/1.0.json (1)

607-611: Outdated component description now that Ansible/Packer are supported.

Broaden the metadata.component description.

- "description": "Terraform/OpenTofu/Helmfile component"
+ "description": "Component path (e.g., Terraform/OpenTofu/Helmfile/Packer/Ansible)"
examples/demo-ansible/schemas/atmos-manifest.json (2)

3-5: Example schema $id should not reuse the published manifest $id.

Use a distinct $id for the example to avoid clashes during validation or tooling.

-"$id": "https://json.schemastore.org/atmos-manifest.json",
+"$id": "https://atmos.tools/schemas/examples/atmos-manifest-demo.json",

576-580: Update component description to include Ansible/Packer.

Keep wording consistent across schemas.

- "description": "Terraform/OpenTofu/Helmfile component"
+ "description": "Component path (e.g., Terraform/OpenTofu/Helmfile/Packer/Ansible)"
pkg/datafetcher/schema/stacks/stack-config/1.0.json (1)

607-611: Outdated component description.

Align with added Ansible support.

- "description": "Terraform/OpenTofu/Helmfile component"
+ "description": "Component path (e.g., Terraform/OpenTofu/Helmfile/Packer/Ansible)"
pkg/datafetcher/schema/atmos/manifest/1.0.json (1)

488-573: Update metadata component description to include Ansible.

Minor doc fix: now that Ansible is first‑class, the metadata description should include it (and Packer, if missing) for clarity.

Apply this diff (outside this hunk) to the metadata.component description:

-              "description": "Terraform/OpenTofu/Helmfile component"
+              "description": "Terraform/OpenTofu/Helmfile/Packer/Ansible component"
internal/exec/utils.go (1)

511-521: Add missing Ansible branch to componentInfo for UX parity

In internal/exec/utils.go (around line 511), the switch covers Terraform, Helmfile, and Packer but omits Ansible. Since constructAnsibleComponentWorkingDir exists, add:

 case cfg.PackerComponentType:
   componentInfo[cfg.ComponentPathSectionName] = constructPackerComponentWorkingDir(atmosConfig, &configAndStacksInfo)
+case cfg.AnsibleComponentType:
+  componentInfo[cfg.ComponentPathSectionName] = constructAnsibleComponentWorkingDir(atmosConfig, &configAndStacksInfo)
 }

This aligns Ansible with other component types.

cmd/ansible_vault_test.go (1)

1-16: Nice coverage of static command config.

Add t.Parallel() to speed up tests and mirror other command tests if applicable.

Apply:

 func TestAnsibleVaultCmd(t *testing.T) {
+	t.Parallel()
 	// Test that the command is properly configured
cmd/ansible_inventory_test.go (1)

1-16: Solid assertions for Cobra wiring.

Same minor: add t.Parallel().

Apply:

 func TestAnsibleInventoryCmd(t *testing.T) {
+	t.Parallel()
 	// Test that the command is properly configured
cmd/ansible_version.go (1)

14-15: Polish phrasing.

Consider tightening the copy.

-	ansibleVersionShort = "Show the version of ansible command."
-	ansibleVersionLong  = `This command shows the version of ansible command.
+	ansibleVersionShort = "Show the Ansible command version."
+	ansibleVersionLong  = `This command shows the version of the Ansible command.
cmd/ansible_version_test.go (1)

9-16: Add an assertion for Examples to prevent future regressions.

If you adopt the Example wiring, assert it here.

 func TestAnsibleVersionCmd(t *testing.T) {
   // Test that the command is properly configured
   assert.Equal(t, "version", ansibleVersionCmd.Use)
   assert.Contains(t, ansibleVersionCmd.Short, "version")
   assert.Contains(t, ansibleVersionCmd.Long, "version")
+  assert.Equal(t, ansibleVersionUsageMarkdown, ansibleVersionCmd.Example)
   assert.True(t, ansibleVersionCmd.FParseErrWhitelist.UnknownFlags)
   assert.NotNil(t, ansibleVersionCmd.RunE)
 }
examples/demo-ansible/stacks/deploy/staging/demo.yaml (1)

14-26: Clean trailing whitespace and add final newline.

This quiets yamllint and avoids noisy diffs.

-        log_level: "WARNING"
-        
+        log_level: "WARNING"
         # Staging uses different hosts
-        target_hosts: "webservers"
-        
+        target_hosts: "webservers"
         # Staging environment variables
-        webapp_environment: "staging"
-        webapp_version: "1.1.0"
-        
+        webapp_environment: "staging"
+        webapp_version: "1.1.0"
       settings:
         ansible:
           playbook: "site.yml"
           inventory: "inventory.yml"
-          
       env:
         ANSIBLE_VERBOSITY: "0"
         ANSIBLE_GATHERING: "explicit"
+```


Also applies to: 29-29

</blockquote></details>
<details>
<summary>pkg/config/config.go (1)</summary><blockquote>

`197-204`: **Avoid defaulting to CWD when Ansible base path is unset.**

`filepath.Abs("")` yields the current working directory, which can mispoint discovery if `components.ansible.base_path` (or `base_path`) is empty. Prefer leaving it blank unless configured.


```diff
-	// Convert Ansible dir to an absolute path.
-	ansibleBasePath := filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Ansible.BasePath)
-	ansibleDirAbsPath, err := filepath.Abs(ansibleBasePath)
-	if err != nil {
-		return err
-	}
-	atmosConfig.AnsibleDirAbsolutePath = ansibleDirAbsPath
+	// Convert Ansible dir to an absolute path (only if configured).
+	if atmosConfig.Components.Ansible.BasePath != "" || atmosConfig.BasePath != "" {
+		ansibleBasePath := filepath.Join(atmosConfig.BasePath, atmosConfig.Components.Ansible.BasePath)
+		ansibleDirAbsPath, err := filepath.Abs(ansibleBasePath)
+		if err != nil {
+			return err
+		}
+		atmosConfig.AnsibleDirAbsolutePath = ansibleDirAbsPath
+	}

If schema defaults guarantee a non-empty base path for Ansible, disregard.

internal/exec/validate_stacks.go (1)

84-94: Also validate duplicate Ansible components across manifests.

Parity with Terraform/Helmfile helps catch misconfigurations early.

  helmfileComponentStackMap, err := createComponentStackMap(atmosConfig, stacksMap, cfg.HelmfileSectionName)
  if err != nil {
    return err
  }

  errorList, err = checkComponentStackMap(helmfileComponentStackMap)
  if err != nil {
    return err
  }
  validationErrorMessages = append(validationErrorMessages, errorList...)

+ // Check Ansible components for duplicates across the same stack
+ ansibleComponentStackMap, err := createComponentStackMap(atmosConfig, stacksMap, cfg.AnsibleSectionName)
+ if err != nil {
+   return err
+ }
+ errorList, err = checkComponentStackMap(ansibleComponentStackMap)
+ if err != nil {
+   return err
+ }
+ validationErrorMessages = append(validationErrorMessages, errorList...)
pkg/component/atmos.yaml (1)

51-55: Ansible block added — confirm command semantics across subcommands

Looks good. One check: components.ansible.command is set to ansible-playbook, but inventory and vault use different binaries. If the field is intended as a single “base” command, consider documenting that only playbook uses it, or rename to playbook_command to avoid confusion. Also verify the ENV overrides are wired: ATMOS_COMPONENTS_ANSIBLE_COMMAND and ATMOS_COMPONENTS_ANSIBLE_BASE_PATH.

Would you like a small docs snippet clarifying the command’s scope?

internal/exec/describe_affected_utils.go (2)

78-91: Guard against empty base paths (low risk)

If Components.Ansible.BasePath isn’t set, filepath.Join(remoteRepoFileSystemPath, basePath, "") still works, but produces remote root. If that’s unintended, add a quick non-empty check to mirror other defaults logic.


176-191: Sanity check for changed files list

Not Ansible-specific: if patch.Stats() grows large, consider short-circuiting early when no files match any component roots to save work.

internal/exec/ansible_test.go (2)

11-20: Flags struct smoke test is fine; consider table-driven coverage

Add a small table test covering empty values and unusual filenames to catch regressions in flag plumbing.


22-36: Exercise ExecuteAnsible paths via a stub runner

Right now we just assert values. Introduce a minimal exec stub and call ExecuteAnsible for subcommands: version, playbook, inventory, vault, asserting it selects the correct binary/args without running external processes.

I can provide a stubbed runner interface and tests if helpful.

examples/demo-ansible/stacks/deploy/dev/demo.yaml (1)

14-14: Clean up trailing whitespace and add newline at EOF

Yamllint flags trailing spaces and missing newline. Trim and add EOF newline.

-        
+
-        
+
-        
+
-          
+
-        ANSIBLE_DISPLAY_SKIPPED_HOSTS: "True"
+        ANSIBLE_DISPLAY_SKIPPED_HOSTS: "True"
+

Also applies to: 17-17, 20-20, 26-26, 29-29

website/docs/cli/commands/ansible/ansible-version.mdx (1)

1-7: Polish description grammar

Minor copyedit for clarity and consistency with Ansible capitalization.

-description: Show the version of ansible command
+description: Show the version of the Ansible command
website/docs/cli/commands/ansible/ansible-playbook.mdx (1)

44-55: Document pass-through of unknown flags to ansible-playbook

The CLI enables UnknownFlags; add a note so users know they can pass native ansible-playbook flags through Atmos.

 </dl>

+:::note
+Any flags unknown to Atmos are passed through to the underlying `ansible-playbook` command.
+:::
cmd/ansible_vault.go (1)

16-26: Microcopy nit: tighten phrasing.

Consider “Encrypt and decrypt files with Ansible Vault.” for the short description.

internal/exec/path_utils_test.go (1)

148-179: Optional: add working-dir test for Ansible to match Packer coverage.

Parity improves confidence in path utils.

I can draft TestConstructAnsibleComponentWorkingDir mirroring TestConstructPackerComponentWorkingDir.

pkg/list/list_components_test.go (1)

120-125: Nice: fixtures now include packer and ansible

This broadens coverage across component types.

Add a small negative test with an unknown component type (e.g., "custom": {}) to ensure it’s harmlessly ignored or explicitly handled, matching intended behavior.

pkg/list/list_components.go (1)

23-62: Centralize error vars per repo convention

Per team learning, static errors should live in errors/errors.go. Move ErrParseStacks, ErrParseComponents, ErrStackNotFound, ErrProcessStack there and import as needed to avoid drift across packages.

I can generate a targeted diff moving these to errors/errors.go if you confirm the target package name/path.

pkg/stack/stack_processor_test.go (1)

387-388: Add a minimal assertion that ansible wiring is effective

Since you supply a real ansible components path, assert components["ansible"] presence (when fixtures include it) to exercise the new path.

If fixtures lack ansible for this stack, add a separate test that points at an ansible-enabled stack and asserts non-empty ansible components.

website/docs/cli/commands/ansible/ansible-vault.mdx (3)

1-7: Avoid potential doc ID collisions

"id: vault" might clash with other sections named "vault". Prefer a unique id like "ansible-vault" to keep routes stable.

Apply this tiny change and adjust any inbound links if present.


9-13: Nit: align import style

Either add semicolons to all import lines or remove the trailing one for consistency with the repo’s MDX style.


25-36: Consider adding prerequisites and common flags

A short prerequisites note (requires ansible + ansible-vault) and examples with --vault-id/--vault-password-file would make this page immediately actionable.

Happy to draft a snippet aligned with the implemented flags.

internal/exec/path_utils.go (1)

118-145: Reduce duplication with a small helper

The varfile-name pattern repeats across types. Consider a helper like makeVarfileName(prefix, replaced, comp, suffix string) to centralize formatting.

Example:

func makeVarfileName(ctx, replaced, comp, suffix string) string {
	if replaced == "" {
		return fmt.Sprintf("%s-%s.%s", ctx, comp, suffix)
	}
	return fmt.Sprintf("%s-%s-%s.%s", ctx, replaced, comp, suffix)
}
internal/exec/ansible_utils_test.go (3)

80-91: Prevent subtest loop-capture gotcha

Shadow the loop variable inside t.Run to avoid future parallelization issues.

-	for _, tt := range testCases {
-		t.Run(tt.name, func(t *testing.T) {
+	for _, tt := range testCases {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {

12-18: Drop unused wantErr or add an error-path case

All cases set wantErr=false; either remove the field or add at least one case asserting an error.


21-76: Avoid hardcoded section keys; use config constants

Using literals like "ansible" risks drift. Prefer cfg.AnsibleSectionName.

@@
-import (
+import (
 	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 
 	"github.com/cloudposse/atmos/pkg/schema"
+	cfg "github.com/cloudposse/atmos/pkg/config"
 )
@@
-			settings: &schema.AtmosSectionMapType{
-				"ansible": map[string]any{
+			settings: &schema.AtmosSectionMapType{
+				cfg.AnsibleSectionName: map[string]any{
 					settingKey: validValue,
 				},
 			},
@@
-			settings: &schema.AtmosSectionMapType{
-				"ansible": map[string]any{
+			settings: &schema.AtmosSectionMapType{
+				cfg.AnsibleSectionName: map[string]any{
 					"other": "value",
 				},
 			},
@@
-			settings: &schema.AtmosSectionMapType{
-				"ansible": "invalid",
+			settings: &schema.AtmosSectionMapType{
+				cfg.AnsibleSectionName: "invalid",
 			},
@@
-			settings: &schema.AtmosSectionMapType{
-				"ansible": map[string]any{
+			settings: &schema.AtmosSectionMapType{
+				cfg.AnsibleSectionName: map[string]any{
 					settingKey: 123,
 				},
 			},
cmd/ansible_inventory.go (1)

28-38: Optional: richer help rendering

If desired, render the markdown in a custom help func.

 func init() {
   ansibleCmd.AddCommand(ansibleInventoryCmd)
+  ansibleInventoryCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
+    // utils.PrintfMarkdown(ansibleInventoryUsageMarkdown) // uncomment if utils is available
+    cmd.Parent().HelpFunc()(cmd, args) // fallback to default help
+  })
 }
website/docs/cli/commands/ansible/usage.mdx (1)

6-9: Remove unused imports

Screengrab and File aren’t used; trim to avoid bundler noise.

-import Screengrab from '@site/src/components/Screengrab'
-import DocCardList from '@theme/DocCardList'
-import File from '@site/src/components/File'
-import Terminal from '@site/src/components/Terminal'
+import DocCardList from '@theme/DocCardList'
+import Terminal from '@site/src/components/Terminal'
 import useBaseUrl from '@docusaurus/useBaseUrl';
internal/exec/stack_processor_utils_test.go (1)

630-645: Label newly added path arg for readability

Comment the base-path params to avoid misordering with the new Ansible arg.

-	config, err := ProcessStackConfig(
+	config, err := ProcessStackConfig(
 		&atmosConfig,
-		stacksBasePath,
-		filepath.Join(basePath, "components", "terraform"),
-		filepath.Join(basePath, "components", "helmfile"),
-		filepath.Join(basePath, "components", "packer"),
-		filepath.Join(basePath, "components", "ansible"),
+		stacksBasePath,                                   // stacksBasePath
+		filepath.Join(basePath, "components", "terraform"), // terraformComponentsBasePath
+		filepath.Join(basePath, "components", "helmfile"),  // helmfileComponentsBasePath
+		filepath.Join(basePath, "components", "packer"),    // packerComponentsBasePath
+		filepath.Join(basePath, "components", "ansible"),   // ansibleComponentsBasePath
 		"nonprod",
 		deepMergedStackConfig,
 		false,
 		false,
 		"",
 		map[string]map[string][]string{},
 		importsConfig,
 		true,
 	)
pkg/stack/stack_processor.go (1)

104-105: Pass real base paths instead of empty strings
Replace the empty string placeholders with the actual AtmosConfiguration fields, which are present in the schema.

Apply:

-        "", // packerComponentsBasePath - not used in this context
-        "", // ansibleComponentsBasePath - not used in this context
+        atmosConfig.PackerDirAbsolutePath,
+        atmosConfig.AnsibleDirAbsolutePath,
internal/exec/ansible_utils.go (2)

9-25: Return type doesn’t match behavior; also duplicate logic

  • The error is always nil; return (string, bool) or emit a typed error when the value exists but is the wrong type.
  • The map-walk pattern is duplicated; factor to a tiny helper.

Apply:

 package exec
@@
-// GetAnsiblePlaybookFromSettings returns an Ansible playbook name from the `settings.ansible.playbook` section in the Atmos component manifest.
-func GetAnsiblePlaybookFromSettings(settings *schema.AtmosSectionMapType) (string, error) {
-	if settings == nil {
-		return "", nil
-	}
-
-	var ansibleSection schema.AtmosSectionMapType
-	var ansiblePlaybook string
-	var ok bool
-
-	if ansibleSection, ok = (*settings)[cfg.AnsibleSectionName].(map[string]any); !ok {
-		return "", nil
-	}
-	if ansiblePlaybook, ok = ansibleSection[cfg.AnsiblePlaybookSectionName].(string); !ok {
-		return "", nil
-	}
-	return ansiblePlaybook, nil
-}
+// GetAnsiblePlaybookFromSettings returns settings.ansible.playbook if present.
+func GetAnsiblePlaybookFromSettings(settings *schema.AtmosSectionMapType) (string, error) {
+	return getAnsibleString(settings, cfg.AnsiblePlaybookSectionName)
+}
+
+func getAnsibleString(settings *schema.AtmosSectionMapType, key string) (string, error) {
+	if settings == nil {
+		return "", nil
+	}
+	section, ok := (*settings)[cfg.AnsibleSectionName].(map[string]any)
+	if !ok || section == nil {
+		return "", nil
+	}
+	val, ok := section[key]
+	if !ok {
+		return "", nil
+	}
+	s, ok := val.(string)
+	if !ok {
+		return "", fmt.Errorf("settings.ansible.%s must be a string", key)
+	}
+	return s, nil
+}

(Add fmt import.)


28-44: Same concerns for inventory getter

Leverage the shared helper to keep behavior consistent and remove duplication.

Apply:

-// GetAnsibleInventoryFromSettings returns an Ansible inventory from the `settings.ansible.inventory` section in the Atmos component manifest.
-func GetAnsibleInventoryFromSettings(settings *schema.AtmosSectionMapType) (string, error) {
-	if settings == nil {
-		return "", nil
-	}
-
-	var ansibleSection schema.AtmosSectionMapType
-	var ansibleInventory string
-	var ok bool
-
-	if ansibleSection, ok = (*settings)[cfg.AnsibleSectionName].(map[string]any); !ok {
-		return "", nil
-	}
-	if ansibleInventory, ok = ansibleSection[cfg.AnsibleInventorySectionName].(string); !ok {
-		return "", nil
-	}
-	return ansibleInventory, nil
-}
+// GetAnsibleInventoryFromSettings returns settings.ansible.inventory if present.
+func GetAnsibleInventoryFromSettings(settings *schema.AtmosSectionMapType) (string, error) {
+	return getAnsibleString(settings, cfg.AnsibleInventorySectionName)
+}
examples/demo-ansible/stacks/deploy/prod/demo.yaml (1)

14-14: Fix YAML lint: trailing spaces and missing newline

  • Remove trailing spaces on lines 14, 19, 23, 28.
  • Add a newline at EOF.

Also applies to: 19-19, 23-23, 28-28, 32-32

internal/exec/ansible.go (4)

96-96: Remove unnecessary error return

The function always returns nil for error - consider removing the error from the return signature or documenting why it might return an error in the future.

-func prepareAnsibleArgs(info *schema.ConfigAndStacksInfo, playbook, inventory, varFile string) ([]string, error) {
+func prepareAnsibleArgs(info *schema.ConfigAndStacksInfo, playbook, inventory, varFile string) []string {

Then update the call site at line 235:

-	allArgsAndFlags, err := prepareAnsibleArgs(info, playbook, inventory, varFile)
-	if err != nil {
-		return err
-	}
+	allArgsAndFlags := prepareAnsibleArgs(info, playbook, inventory, varFile)

207-207: Define named constant for file permissions

Magic number should be a named constant for clarity.

Add at the top of the file:

const (
    // AnsibleVarFilePermissions defines the file permissions for Ansible variable files
    AnsibleVarFilePermissions = 0o644
)

Then use it:

-		err = u.WriteToFileAsYAML(varFilePath, info.ComponentVarsSection, 0o644)
+		err = u.WriteToFileAsYAML(varFilePath, info.ComponentVarsSection, AnsibleVarFilePermissions)

260-260: Define constant for "playbook" string

The string "playbook" appears multiple times - extract to a constant.

Add constants at the top:

const (
    AnsiblePlaybookSubCommand = "playbook"
    AnsibleInventorySubCommand = "inventory"
    AnsibleVaultSubCommand = "vault"
    AnsibleVersionSubCommand = "version"
)

Then use throughout the file.


135-268: Consider reducing function complexity

The ExecuteAnsible function has high cognitive complexity (23). Consider extracting the variable file handling into a separate function.

Extract lines 199-211 into a helper:

func writeComponentVariables(atmosConfig *schema.AtmosConfiguration, info *schema.ConfigAndStacksInfo, varFilePath string) error {
    if info.DryRun {
        log.Debug("Dry run: would write variables to file", "file", varFilePath)
        return nil
    }
    
    log.Debug("Writing the variables to file:", "file", varFilePath)
    return u.WriteToFileAsYAML(varFilePath, info.ComponentVarsSection, AnsibleVarFilePermissions)
}
internal/exec/stack_processor_utils.go (2)

682-682: Use wrapped static errors instead of dynamic errors

Define static errors and wrap them with additional context instead of using fmt.Errorf directly.

Add at the package level:

var (
    ErrInvalidAnsibleSection = errors.New("invalid 'ansible' section")
    ErrInvalidAnsibleCommandSection = errors.New("invalid 'ansible.command' section")  
    ErrInvalidAnsibleVarsSection = errors.New("invalid 'ansible.vars' section")
    ErrInvalidAnsibleSettingsSection = errors.New("invalid 'ansible.settings' section")
    ErrInvalidAnsibleEnvSection = errors.New("invalid 'ansible.env' section")
)

Then wrap with context:

-			return nil, fmt.Errorf("invalid 'ansible' section in the file '%s'", stackName)
+			return nil, fmt.Errorf("%w in file '%s'", ErrInvalidAnsibleSection, stackName)

Also applies to: 891-891, 898-898, 910-910, 922-922


2063-2063: Remove unnecessary blank line

There's an unnecessary leading newline after the comment.

 	// Process all Ansible components
-
 	if componentTypeFilter == "" || componentTypeFilter == cfg.AnsibleComponentType {

Comment on lines +19 to +21
"ansible": {
"$ref": "#/definitions/ansible"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm import vs imports usage across schema and code.

The schema exposes root import, while code paths (e.g., internal/exec/utils.go) reference imports. Ensure the loader populates imports as intended, or align names to avoid confusion.

Run to spot occurrences in repo:

Also applies to: 81-85


🏁 Script executed:

#!/bin/bash
rg -nP '^\s*(import|imports)\s*:' -g '!**/vendor/**' -g '!**/node_modules/**' website examples pkg internal

Length of output: 13340


Align import vs imports naming
Manifest schema currently defines a singular import, but code (e.g. pkg/stack/stack_processor_test.go) and examples reference imports. Pick one form—singular or plural—and update the JSON schema, loader (v.GetStringSlice), tests, and examples to match.

🤖 Prompt for AI Agents
In pkg/datafetcher/schema/atmos/manifest/1.0.json around lines 19-21, the schema
currently uses the singular "import" but the codebase and examples expect
"imports"; change the schema property to "imports" (and update any
$ref/definitions accordingly) so it validates a string array, then update the
loader code that reads this field to use v.GetStringSlice("imports") if it does
not already, and adjust all tests (e.g., pkg/stack/stack_processor_test.go) and
example manifests to use "imports" instead of "import" so names are consistent
across schema, loader, tests, and examples.

terraformComponentsBasePath string,
helmfileComponentsBasePath string,
packerComponentsBasePath string,
ansibleComponentsBasePath string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public API just grew again — consider an options struct

Adding ansibleComponentsBasePath here makes the already-long signature harder to maintain. Prefer a single options struct (or functional options) to avoid churn and breaking changes as more component types are added.

Run to find external callsites that may still use the old signature:


🏁 Script executed:

#!/bin/bash
rg -nP '\bProcessYAMLConfigFiles\s*\(' -g '!**/vendor/**' -C2

Length of output: 3990


Update all ProcessYAMLConfigFiles calls and consider an options struct

  • Update every invocation of ProcessYAMLConfigFiles(...) in pkg/stack, pkg/spacelift, internal/exec (and their tests) to pass the new ansibleComponentsBasePath argument.
  • To keep the API manageable as you add more paths, refactor to accept a single options struct or use functional options.
🤖 Prompt for AI Agents
In pkg/stack/stack_processor.go around line 15 the function signature gained a
new parameter ansibleComponentsBasePath, but all callers must be updated to
supply this argument and the API will become harder to manage as more paths are
added; update every invocation of ProcessYAMLConfigFiles(...) across pkg/stack,
pkg/spacelift, internal/exec and their tests to pass the new
ansibleComponentsBasePath value, and refactor the function signature to accept a
single options struct (or implement functional options) instead of many
positional parameters so adding future paths is backwards-compatible; update
call sites to construct and pass the options struct (or use the option
functions) and run tests to ensure compilation and behavior remain correct.

Comment on lines +1 to +11
{
"label": "ansible",
"position": 8,
"className": "command",
"collapsible": true,
"collapsed": true,
"link": {
"type": "doc",
"id": "usage"
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Doc ID may be too short; prefer full doc ID.

Unless usage.mdx sets id: usage in front matter, Docusaurus expects the full ID (path-based). Use the absolute ID to be safe.

Apply:

-  "link": {
-    "type": "doc",
-    "id": "usage"
-  }
+  "link": {
+    "type": "doc",
+    "id": "cli/commands/ansible/usage"
+  }

Check whether usage.mdx declares id: usage:


🏁 Script executed:

#!/bin/bash
fd -a usage.mdx website/docs | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,20p" {} | rg -n "^id:\s*usage\b" || true'

Length of output: 939


Use full Docusaurus doc ID for Ansible usage link
The usage.mdx file doesn’t declare id: usage, so Docusaurus will use its path-based ID (cli/commands/ansible/usage). Update the JSON to reference the absolute ID:

-  "link": {
-    "type": "doc",
-    "id": "usage"
-  }
+  "link": {
+    "type": "doc",
+    "id": "cli/commands/ansible/usage"
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"label": "ansible",
"position": 8,
"className": "command",
"collapsible": true,
"collapsed": true,
"link": {
"type": "doc",
"id": "usage"
}
}
{
"label": "ansible",
"position": 8,
"className": "command",
"collapsible": true,
"collapsed": true,
"link": {
"type": "doc",
"id": "cli/commands/ansible/usage"
}
}
🤖 Prompt for AI Agents
In website/docs/cli/commands/ansible/_category_.json around lines 1-11, the
category link uses a relative doc id "usage" which doesn't match the actual
Docusaurus path-based id for usage.mdx; update the link.id to the full absolute
doc id "cli/commands/ansible/usage" so the category correctly links to the
Ansible usage page.

@osterman
Copy link
Member

osterman commented Sep 5, 2025

Also, it's good to have an example as you have, but I want to make sure we don't overload the purpose of the examples. We have a tests/fixtures directory where we create our reusable fixtures, specifically for testing different scenarios. Reach out if you need more help understanding how we organize that. Also note that we keep component fixtures deliberately separate from stack fixtures. This allows us to reuse component fixtures in multiple scenarios, without duplicating the boilerplate.

@osterman
Copy link
Member

osterman commented Sep 5, 2025

Make sure the following commands work with the implementation.

  • atmos list components
  • atmos describe components
  • atmos describe affected

- Remove trailing whitespace throughout CLAUDE.md and other files
- Add Example field to ansible-related commands
- Implement setAnsibleConfig function to handle command-line args
- Update component tests to include nginx component
- Fix typo in ansible_vault.go long description
- Add fields for AnsibleCommand and AnsibleDir to schema structs
Comment on lines +1 to +13
– Execute ansible commands for configuration management

```
$ atmos ansible playbook <component-name> -s <stack-name>
```

```
$ atmos ansible inventory <component-name> -s <stack-name> --list
```

```
$ atmos ansible vault <component-name> -s <stack-name> encrypt <file>
``` No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't just list off a bunch of commands, list what each command is doing in plain English following the format of the other usage examples.


components:
ansible:
webapp:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it support a first-class inventory?

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>
atmosConfig.Components.Packer.BasePath = componentsPackerBasePath
}

componentsAnsibleCommand := os.Getenv("ATMOS_COMPONENTS_ANSIBLE_COMMAND")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs switching out for the equivalent viper call

atmosConfig.Components.Ansible.Command = componentsAnsibleCommand
}

componentsAnsibleBasePath := os.Getenv("ATMOS_COMPONENTS_ANSIBLE_BASE_PATH")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs switching out for the equivalent viper call

// ErrParseComponents is returned when component data cannot be parsed.
ErrParseComponents = errors.New("could not parse components")
// ErrParseTerraformComponents is returned when terraform component data cannot be parsed.
ErrParseTerraformComponents = errors.New("could not parse Terraform components")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this intended to be lost?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

many whitespace diffs in this file, if it is only whitespace and nothing else, revert the diffs

@osterman osterman changed the title Add native Ansible support (Atmos loves Ansible) Add Native Support for Ansible Components Sep 5, 2025
@mergify mergify bot removed the needs-cloudposse Needs Cloud Posse assistance label Sep 11, 2025
@RoseSecurity
Copy link
Contributor Author

Closing this draft PR, for now, as we mature the PRD and vision for what an Atmos/Ansible integration looks like. I'll gladly work with any other members of the community who are interested in developing this capability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request feature New functionality minor New features that do not break anything size/xl Extra large size PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants