diff --git a/Ansible-Executor.-.md b/Ansible-Executor.-.md new file mode 100644 index 0000000..44fd427 --- /dev/null +++ b/Ansible-Executor.-.md @@ -0,0 +1,226 @@ +# Ansible Executor + +> Playbook execution, inventory management, and SSH connection pooling. + +## Overview + +The `ansible` package provides a pure Go implementation for executing Ansible-style playbooks. It parses YAML inventories, manages SSH connections with pooling, and returns structured results for each task. + +## Executor + +The `Executor` is the primary entry point for running playbooks. + +```go +type Executor struct { + // Internal: basePath, inventory, vars, facts, results +} +``` + +### Creating an Executor + +```go +import "forge.lthn.ai/core/go-devops/ansible" + +// Create an executor rooted at the playbooks directory +executor := ansible.NewExecutor("/path/to/playbooks") +``` + +### Loading Inventory + +```go +// Load inventory from a YAML file +executor.SetInventory("/path/to/inventory.yml") +``` + +### Running a Playbook + +```go +ctx := context.Background() +result, err := executor.Run(ctx, "deploy.yml") +if err != nil { + log.Fatalf("playbook failed: %v", err) +} + +// Check results +for _, taskResult := range result.Tasks { + fmt.Printf("Task: %s — Changed: %t, RC: %d\n", + taskResult.Name, taskResult.Changed, taskResult.RC) +} +``` + +## Inventory + +The `Inventory` type models Ansible's host group structure with per-host and per-group variables. + +```go +type Inventory struct { + Groups map[string]HostGroup // Named groups of hosts + Vars map[string]any // Global inventory variables +} + +type HostGroup struct { + Hosts []Host // Hosts in this group + Vars map[string]any // Group-level variables +} + +type Host struct { + Name string // Hostname or IP + Address string // Connection address + Port int // SSH port (default: 22) + User string // SSH user + Vars map[string]any // Host-level variables +} +``` + +### Example Inventory + +```yaml +all: + vars: + ansible_user: deploy + children: + webservers: + hosts: + web1: + ansible_host: 10.0.1.10 + ansible_port: 4819 + web2: + ansible_host: 10.0.1.11 + databases: + hosts: + db1: + ansible_host: 10.0.2.10 + vars: + db_engine: postgresql +``` + +## Plays and Tasks + +### Play + +A play targets a group of hosts with a list of tasks. + +```go +type Play struct { + Name string // Play name + Hosts string // Target host group + Tasks []Task // Ordered task list + Vars map[string]any +} +``` + +### Task + +Each task executes a single module with arguments. + +```go +type Task struct { + Name string // Human-readable task name + Module string // Module to execute (e.g. "shell", "copy", "service") + Args map[string]any // Module arguments + Register string // Variable name to store result + Notify []string // Handlers to trigger on change + Tags []string // Tags for selective execution +} +``` + +### TaskResult + +Every task execution produces a structured result. + +```go +type TaskResult struct { + Name string // Task name + Changed bool // Whether the task made changes + Output string // stdout/stderr combined + RC int // Return code (0 = success) +} +``` + +### Example: Checking Results + +```go +result, err := executor.Run(ctx, "provision.yml") +if err != nil { + log.Fatal(err) +} + +for _, tr := range result.Tasks { + if tr.RC != 0 { + fmt.Printf("FAILED: %s (rc=%d)\nOutput: %s\n", tr.Name, tr.RC, tr.Output) + } else if tr.Changed { + fmt.Printf("CHANGED: %s\n", tr.Name) + } else { + fmt.Printf("OK: %s\n", tr.Name) + } +} +``` + +## SSH Connection Pooling + +The `SSHClient` manages persistent SSH connections to avoid repeated handshakes during playbook execution. + +```go +type SSHClient struct { + // Internal: connection pool, config, timeout +} +``` + +### How Pooling Works + +1. On first connection to a host, the client establishes an SSH session +2. Subsequent tasks reuse the existing connection +3. Connections are automatically closed when the executor finishes +4. Failed connections are evicted from the pool and re-established + +This significantly reduces execution time for playbooks with many tasks per host, as the SSH handshake (key exchange, authentication) only happens once. + +### Connection Configuration + +The SSH client reads connection parameters from the inventory: + +| Inventory Variable | Purpose | +|-------------------|---------| +| `ansible_host` | Connection address | +| `ansible_port` | SSH port (default: 22) | +| `ansible_user` | SSH username | +| `ansible_ssh_private_key_file` | Path to private key | +| `ansible_ssh_common_args` | Additional SSH arguments | + +## Example: Full Workflow + +```go +import ( + "forge.lthn.ai/core/go-devops/ansible" + "forge.lthn.ai/core/go-devops/build" +) + +// Build the project first +builder := build.New(build.Config{ + ProjectDir: "./my-service", + OutputDir: "./dist", +}) +artifact, err := builder.Build(ctx) + +// Then deploy with Ansible +executor := ansible.NewExecutor("./playbooks") +executor.SetInventory("./inventory/production.yml") + +// Set extra vars including the build artefact path +executor.SetVar("artifact_path", artifact.Path) +executor.SetVar("service_version", "1.2.0") + +result, err := executor.Run(ctx, "deploy.yml") +if err != nil { + log.Fatalf("deployment failed: %v", err) +} + +fmt.Printf("Deployment complete: %d tasks, %d changed\n", + len(result.Tasks), result.ChangedCount()) +``` + +## See Also + +- [[Home]] — Package overview +- [[Build-System]] — Building artefacts before deployment +- [[Infrastructure]] — Provisioning servers to deploy to