refactor(ansible): migrate module path to dappco.re/go/core/ansible
Some checks failed
CI / test (pull_request) Failing after 2s
CI / auto-fix (pull_request) Failing after 0s
CI / auto-merge (pull_request) Failing after 0s

Update go.mod module line and all dependencies from forge.lthn.ai to
dappco.re paths (core v0.5.0, log v0.1.0, io v0.2.0). Update all .go
import paths. Rewrite cmd/ansible/ for new core.Command API replacing
the cobra-based CLI integration. Update documentation references.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-22 01:50:56 +00:00
parent 1f2535bcfa
commit b3eed66230
14 changed files with 247 additions and 257 deletions

4
.gitignore vendored
View file

@ -1,2 +1,4 @@
.core/
.idea/ .idea/
.vscode/
*.log
.core/

7
CONSUMERS.md Normal file
View file

@ -0,0 +1,7 @@
# Consumers of go-ansible
These modules import `dappco.re/go/core/ansible`:
- go-infra
**Breaking change risk: 1 consumers.**

View file

@ -7,88 +7,31 @@ import (
"strings" "strings"
"time" "time"
ansible "forge.lthn.ai/core/go-ansible" "dappco.re/go/core"
"forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/ansible"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
) )
var ( // args extracts all positional arguments from Options.
ansibleInventory string func args(opts core.Options) []string {
ansibleLimit string var out []string
ansibleTags string for _, o := range opts {
ansibleSkipTags string if o.Key == "_arg" {
ansibleVars []string if s, ok := o.Value.(string); ok {
ansibleVerbose int out = append(out, s)
ansibleCheck bool }
) }
}
var ansibleCmd = &cli.Command{ return out
Use: "ansible <playbook>",
Short: "Run Ansible playbooks natively (no Python required)",
Long: `Execute Ansible playbooks using a pure Go implementation.
This command parses Ansible YAML playbooks and executes them natively,
without requiring Python or ansible-playbook to be installed.
Supported modules:
- shell, command, raw, script
- copy, template, file, lineinfile, stat, slurp, fetch, get_url
- apt, apt_key, apt_repository, package, pip
- service, systemd
- user, group
- uri, wait_for, git, unarchive
- debug, fail, assert, set_fact, pause
Examples:
core ansible playbooks/coolify/create.yml -i inventory/
core ansible site.yml -l production
core ansible deploy.yml -e "version=1.2.3" -e "env=prod"`,
Args: cli.ExactArgs(1),
RunE: runAnsible,
} }
var ansibleTestCmd = &cli.Command{ func runAnsible(opts core.Options) core.Result {
Use: "test <host>", positional := args(opts)
Short: "Test SSH connectivity to a host", if len(positional) < 1 {
Long: `Test SSH connection and gather facts from a host. return core.Result{Value: coreerr.E("runAnsible", "usage: ansible <playbook>", nil)}
}
Examples: playbookPath := positional[0]
core ansible test linux.snider.dev -u claude -p claude
core ansible test server.example.com -i ~/.ssh/id_rsa`,
Args: cli.ExactArgs(1),
RunE: runAnsibleTest,
}
var (
testUser string
testPassword string
testKeyFile string
testPort int
)
func init() {
// ansible command flags
ansibleCmd.Flags().StringVarP(&ansibleInventory, "inventory", "i", "", "Inventory file or directory")
ansibleCmd.Flags().StringVarP(&ansibleLimit, "limit", "l", "", "Limit to specific hosts")
ansibleCmd.Flags().StringVarP(&ansibleTags, "tags", "t", "", "Only run plays and tasks tagged with these values")
ansibleCmd.Flags().StringVar(&ansibleSkipTags, "skip-tags", "", "Skip plays and tasks tagged with these values")
ansibleCmd.Flags().StringArrayVarP(&ansibleVars, "extra-vars", "e", nil, "Set additional variables (key=value)")
ansibleCmd.Flags().CountVarP(&ansibleVerbose, "verbose", "v", "Increase verbosity")
ansibleCmd.Flags().BoolVar(&ansibleCheck, "check", false, "Don't make any changes (dry run)")
// test command flags
ansibleTestCmd.Flags().StringVarP(&testUser, "user", "u", "root", "SSH user")
ansibleTestCmd.Flags().StringVarP(&testPassword, "password", "p", "", "SSH password")
ansibleTestCmd.Flags().StringVarP(&testKeyFile, "key", "i", "", "SSH private key file")
ansibleTestCmd.Flags().IntVar(&testPort, "port", 22, "SSH port")
// Add subcommands
ansibleCmd.AddCommand(ansibleTestCmd)
}
func runAnsible(cmd *cli.Command, args []string) error {
playbookPath := args[0]
// Resolve playbook path // Resolve playbook path
if !filepath.IsAbs(playbookPath) { if !filepath.IsAbs(playbookPath) {
@ -96,7 +39,7 @@ func runAnsible(cmd *cli.Command, args []string) error {
} }
if !coreio.Local.Exists(playbookPath) { if !coreio.Local.Exists(playbookPath) {
return coreerr.E("runAnsible", fmt.Sprintf("playbook not found: %s", playbookPath), nil) return core.Result{Value: coreerr.E("runAnsible", fmt.Sprintf("playbook not found: %s", playbookPath), nil)}
} }
// Create executor // Create executor
@ -105,38 +48,38 @@ func runAnsible(cmd *cli.Command, args []string) error {
defer executor.Close() defer executor.Close()
// Set options // Set options
executor.Limit = ansibleLimit executor.Limit = opts.String("limit")
executor.CheckMode = ansibleCheck executor.CheckMode = opts.Bool("check")
executor.Verbose = ansibleVerbose executor.Verbose = opts.Int("verbose")
if ansibleTags != "" { if tags := opts.String("tags"); tags != "" {
executor.Tags = strings.Split(ansibleTags, ",") executor.Tags = strings.Split(tags, ",")
} }
if ansibleSkipTags != "" { if skipTags := opts.String("skip-tags"); skipTags != "" {
executor.SkipTags = strings.Split(ansibleSkipTags, ",") executor.SkipTags = strings.Split(skipTags, ",")
} }
// Parse extra vars // Parse extra vars
for _, v := range ansibleVars { if extraVars := opts.String("extra-vars"); extraVars != "" {
parts := strings.SplitN(v, "=", 2) for _, v := range strings.Split(extraVars, ",") {
if len(parts) == 2 { parts := strings.SplitN(v, "=", 2)
executor.SetVar(parts[0], parts[1]) if len(parts) == 2 {
executor.SetVar(parts[0], parts[1])
}
} }
} }
// Load inventory // Load inventory
if ansibleInventory != "" { if invPath := opts.String("inventory"); invPath != "" {
invPath := ansibleInventory
if !filepath.IsAbs(invPath) { if !filepath.IsAbs(invPath) {
invPath, _ = filepath.Abs(invPath) invPath, _ = filepath.Abs(invPath)
} }
if !coreio.Local.Exists(invPath) { if !coreio.Local.Exists(invPath) {
return coreerr.E("runAnsible", fmt.Sprintf("inventory not found: %s", invPath), nil) return core.Result{Value: coreerr.E("runAnsible", fmt.Sprintf("inventory not found: %s", invPath), nil)}
} }
if coreio.Local.IsDir(invPath) { if coreio.Local.IsDir(invPath) {
// Look for inventory.yml or hosts.yml
for _, name := range []string{"inventory.yml", "hosts.yml", "inventory.yaml", "hosts.yaml"} { for _, name := range []string{"inventory.yml", "hosts.yml", "inventory.yaml", "hosts.yaml"} {
p := filepath.Join(invPath, name) p := filepath.Join(invPath, name)
if coreio.Local.Exists(p) { if coreio.Local.Exists(p) {
@ -147,13 +90,13 @@ func runAnsible(cmd *cli.Command, args []string) error {
} }
if err := executor.SetInventory(invPath); err != nil { if err := executor.SetInventory(invPath); err != nil {
return coreerr.E("runAnsible", "load inventory", err) return core.Result{Value: coreerr.E("runAnsible", "load inventory", err)}
} }
} }
// Set up callbacks // Set up callbacks
executor.OnPlayStart = func(play *ansible.Play) { executor.OnPlayStart = func(play *ansible.Play) {
fmt.Printf("\n%s %s\n", cli.TitleStyle.Render("PLAY"), cli.BoldStyle.Render("["+play.Name+"]")) fmt.Printf("\nPLAY [%s]\n", play.Name)
fmt.Println(strings.Repeat("*", 70)) fmt.Println(strings.Repeat("*", 70))
} }
@ -162,41 +105,36 @@ func runAnsible(cmd *cli.Command, args []string) error {
if taskName == "" { if taskName == "" {
taskName = task.Module taskName = task.Module
} }
fmt.Printf("\n%s %s\n", cli.TitleStyle.Render("TASK"), cli.BoldStyle.Render("["+taskName+"]")) fmt.Printf("\nTASK [%s]\n", taskName)
if ansibleVerbose > 0 { if executor.Verbose > 0 {
fmt.Printf("%s\n", cli.DimStyle.Render("host: "+host)) fmt.Printf("host: %s\n", host)
} }
} }
executor.OnTaskEnd = func(host string, task *ansible.Task, result *ansible.TaskResult) { executor.OnTaskEnd = func(host string, task *ansible.Task, result *ansible.TaskResult) {
status := "ok" status := "ok"
style := cli.SuccessStyle
if result.Failed { if result.Failed {
status = "failed" status = "failed"
style = cli.ErrorStyle
} else if result.Skipped { } else if result.Skipped {
status = "skipping" status = "skipping"
style = cli.DimStyle
} else if result.Changed { } else if result.Changed {
status = "changed" status = "changed"
style = cli.WarningStyle
} }
fmt.Printf("%s: [%s]", style.Render(status), host) fmt.Printf("%s: [%s]", status, host)
if result.Msg != "" && ansibleVerbose > 0 { if result.Msg != "" && executor.Verbose > 0 {
fmt.Printf(" => %s", result.Msg) fmt.Printf(" => %s", result.Msg)
} }
if result.Duration > 0 && ansibleVerbose > 1 { if result.Duration > 0 && executor.Verbose > 1 {
fmt.Printf(" (%s)", result.Duration.Round(time.Millisecond)) fmt.Printf(" (%s)", result.Duration.Round(time.Millisecond))
} }
fmt.Println() fmt.Println()
if result.Failed && result.Stderr != "" { if result.Failed && result.Stderr != "" {
fmt.Printf("%s\n", cli.ErrorStyle.Render(result.Stderr)) fmt.Printf("%s\n", result.Stderr)
} }
if ansibleVerbose > 1 { if executor.Verbose > 1 {
if result.Stdout != "" { if result.Stdout != "" {
fmt.Printf("stdout: %s\n", strings.TrimSpace(result.Stdout)) fmt.Printf("stdout: %s\n", strings.TrimSpace(result.Stdout))
} }
@ -211,36 +149,38 @@ func runAnsible(cmd *cli.Command, args []string) error {
ctx := context.Background() ctx := context.Background()
start := time.Now() start := time.Now()
fmt.Printf("%s Running playbook: %s\n", cli.BoldStyle.Render("▶"), playbookPath) fmt.Printf("Running playbook: %s\n", playbookPath)
if err := executor.Run(ctx, playbookPath); err != nil { if err := executor.Run(ctx, playbookPath); err != nil {
return coreerr.E("runAnsible", "playbook failed", err) return core.Result{Value: coreerr.E("runAnsible", "playbook failed", err)}
} }
fmt.Printf("\n%s Playbook completed in %s\n", fmt.Printf("\nPlaybook completed in %s\n", time.Since(start).Round(time.Millisecond))
cli.SuccessStyle.Render("✓"),
time.Since(start).Round(time.Millisecond))
return nil return core.Result{OK: true}
} }
func runAnsibleTest(cmd *cli.Command, args []string) error { func runAnsibleTest(opts core.Options) core.Result {
host := args[0] positional := args(opts)
if len(positional) < 1 {
return core.Result{Value: coreerr.E("runAnsibleTest", "usage: ansible test <host>", nil)}
}
host := positional[0]
fmt.Printf("Testing SSH connection to %s...\n", cli.BoldStyle.Render(host)) fmt.Printf("Testing SSH connection to %s...\n", host)
cfg := ansible.SSHConfig{ cfg := ansible.SSHConfig{
Host: host, Host: host,
Port: testPort, Port: opts.Int("port"),
User: testUser, User: opts.String("user"),
Password: testPassword, Password: opts.String("password"),
KeyFile: testKeyFile, KeyFile: opts.String("key"),
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
} }
client, err := ansible.NewSSHClient(cfg) client, err := ansible.NewSSHClient(cfg)
if err != nil { if err != nil {
return coreerr.E("runAnsibleTest", "create client", err) return core.Result{Value: coreerr.E("runAnsibleTest", "create client", err)}
} }
defer func() { _ = client.Close() }() defer func() { _ = client.Close() }()
@ -250,58 +190,50 @@ func runAnsibleTest(cmd *cli.Command, args []string) error {
// Test connection // Test connection
start := time.Now() start := time.Now()
if err := client.Connect(ctx); err != nil { if err := client.Connect(ctx); err != nil {
return coreerr.E("runAnsibleTest", "connect failed", err) return core.Result{Value: coreerr.E("runAnsibleTest", "connect failed", err)}
} }
connectTime := time.Since(start) connectTime := time.Since(start)
fmt.Printf("%s Connected in %s\n", cli.SuccessStyle.Render("✓"), connectTime.Round(time.Millisecond)) fmt.Printf("Connected in %s\n", connectTime.Round(time.Millisecond))
// Gather facts // Gather facts
fmt.Println("\nGathering facts...") fmt.Println("\nGathering facts...")
// Hostname
stdout, _, _, _ := client.Run(ctx, "hostname -f 2>/dev/null || hostname") stdout, _, _, _ := client.Run(ctx, "hostname -f 2>/dev/null || hostname")
fmt.Printf(" Hostname: %s\n", cli.BoldStyle.Render(strings.TrimSpace(stdout))) fmt.Printf(" Hostname: %s\n", strings.TrimSpace(stdout))
// OS
stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2") stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2")
if stdout != "" { if stdout != "" {
fmt.Printf(" OS: %s\n", strings.TrimSpace(stdout)) fmt.Printf(" OS: %s\n", strings.TrimSpace(stdout))
} }
// Kernel
stdout, _, _, _ = client.Run(ctx, "uname -r") stdout, _, _, _ = client.Run(ctx, "uname -r")
fmt.Printf(" Kernel: %s\n", strings.TrimSpace(stdout)) fmt.Printf(" Kernel: %s\n", strings.TrimSpace(stdout))
// Architecture
stdout, _, _, _ = client.Run(ctx, "uname -m") stdout, _, _, _ = client.Run(ctx, "uname -m")
fmt.Printf(" Architecture: %s\n", strings.TrimSpace(stdout)) fmt.Printf(" Architecture: %s\n", strings.TrimSpace(stdout))
// Memory
stdout, _, _, _ = client.Run(ctx, "free -h | grep Mem | awk '{print $2}'") stdout, _, _, _ = client.Run(ctx, "free -h | grep Mem | awk '{print $2}'")
fmt.Printf(" Memory: %s\n", strings.TrimSpace(stdout)) fmt.Printf(" Memory: %s\n", strings.TrimSpace(stdout))
// Disk
stdout, _, _, _ = client.Run(ctx, "df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}'") stdout, _, _, _ = client.Run(ctx, "df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}'")
fmt.Printf(" Disk: %s\n", strings.TrimSpace(stdout)) fmt.Printf(" Disk: %s\n", strings.TrimSpace(stdout))
// Docker
stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null") stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null")
if err == nil { if err == nil {
fmt.Printf(" Docker: %s\n", cli.SuccessStyle.Render(strings.TrimSpace(stdout))) fmt.Printf(" Docker: %s\n", strings.TrimSpace(stdout))
} else { } else {
fmt.Printf(" Docker: %s\n", cli.DimStyle.Render("not installed")) fmt.Printf(" Docker: not installed\n")
} }
// Check if Coolify is running
stdout, _, _, _ = client.Run(ctx, "docker ps 2>/dev/null | grep -q coolify && echo 'running' || echo 'not running'") stdout, _, _, _ = client.Run(ctx, "docker ps 2>/dev/null | grep -q coolify && echo 'running' || echo 'not running'")
if strings.TrimSpace(stdout) == "running" { if strings.TrimSpace(stdout) == "running" {
fmt.Printf(" Coolify: %s\n", cli.SuccessStyle.Render("running")) fmt.Printf(" Coolify: running\n")
} else { } else {
fmt.Printf(" Coolify: %s\n", cli.DimStyle.Render("not installed")) fmt.Printf(" Coolify: not installed\n")
} }
fmt.Printf("\n%s SSH test passed\n", cli.SuccessStyle.Render("✓")) fmt.Printf("\nSSH test passed\n")
return nil return core.Result{OK: true}
} }

View file

@ -1,14 +1,33 @@
package anscmd package anscmd
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core"
) )
func init() { // Register registers the 'ansible' command and all subcommands on the given Core instance.
cli.RegisterCommands(AddAnsibleCommands) func Register(c *core.Core) {
} c.Command("ansible", core.Command{
Description: "Run Ansible playbooks natively (no Python required)",
Action: runAnsible,
Flags: core.Options{
{Key: "inventory", Value: ""},
{Key: "limit", Value: ""},
{Key: "tags", Value: ""},
{Key: "skip-tags", Value: ""},
{Key: "extra-vars", Value: ""},
{Key: "verbose", Value: 0},
{Key: "check", Value: false},
},
})
// AddAnsibleCommands registers the 'ansible' command and all subcommands. c.Command("ansible/test", core.Command{
func AddAnsibleCommands(root *cli.Command) { Description: "Test SSH connectivity to a host",
root.AddCommand(ansibleCmd) Action: runAnsibleTest,
Flags: core.Options{
{Key: "user", Value: "root"},
{Key: "password", Value: ""},
{Key: "key", Value: ""},
{Key: "port", Value: 22},
},
})
} }

View file

@ -155,7 +155,7 @@ func TestModuleHostname_Bad_MissingName(t *testing.T) {
``` ```
go-ansible/ go-ansible/
go.mod Module definition (forge.lthn.ai/core/go-ansible) go.mod Module definition (dappco.re/go/core/ansible)
go.sum Dependency checksums go.sum Dependency checksums
CLAUDE.md AI assistant context file CLAUDE.md AI assistant context file
types.go Core data types and KnownModules registry types.go Core data types and KnownModules registry

View file

@ -5,12 +5,12 @@ description: A pure Go Ansible playbook engine -- parses YAML playbooks, invento
# go-ansible # go-ansible
`forge.lthn.ai/core/go-ansible` is a pure Go implementation of an Ansible playbook engine. It parses standard Ansible YAML playbooks, inventories, and roles, then executes tasks against remote hosts over SSH -- with no dependency on Python or the upstream `ansible-playbook` binary. `dappco.re/go/core/ansible` is a pure Go implementation of an Ansible playbook engine. It parses standard Ansible YAML playbooks, inventories, and roles, then executes tasks against remote hosts over SSH -- with no dependency on Python or the upstream `ansible-playbook` binary.
## Module Path ## Module Path
``` ```
forge.lthn.ai/core/go-ansible dappco.re/go/core/ansible
``` ```
Requires **Go 1.26+**. Requires **Go 1.26+**.
@ -26,7 +26,7 @@ import (
"context" "context"
"fmt" "fmt"
ansible "forge.lthn.ai/core/go-ansible" ansible "dappco.re/go/core/ansible"
) )
func main() { func main() {
@ -148,8 +148,8 @@ Both fully-qualified collection names (e.g. `ansible.builtin.shell`) and short-f
| Module | Purpose | | Module | Purpose |
|--------|---------| |--------|---------|
| `forge.lthn.ai/core/cli` | CLI framework (command registration, flags, styled output) | | `dappco.re/go/core` | Core framework (command registration, flags) |
| `forge.lthn.ai/core/go-log` | Structured logging and contextual error helper (`log.E()`) | | `dappco.re/go/core/log` | Structured logging and contextual error helper (`log.E()`) |
| `golang.org/x/crypto` | SSH protocol implementation (`crypto/ssh`, `crypto/ssh/knownhosts`) | | `golang.org/x/crypto` | SSH protocol implementation (`crypto/ssh`, `crypto/ssh/knownhosts`) |
| `gopkg.in/yaml.v3` | YAML parsing for playbooks, inventories, and role files | | `gopkg.in/yaml.v3` | YAML parsing for playbooks, inventories, and role files |
| `github.com/stretchr/testify` | Test assertions (test-only) | | `github.com/stretchr/testify` | Test assertions (test-only) |

View file

@ -11,8 +11,8 @@ import (
"text/template" "text/template"
"time" "time"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
) )
// Executor runs Ansible playbooks. // Executor runs Ansible playbooks.

36
go.mod
View file

@ -1,45 +1,19 @@
module forge.lthn.ai/core/go-ansible module dappco.re/go/core/ansible
go 1.26.0 go 1.26.0
require ( require (
forge.lthn.ai/core/cli v0.3.7 dappco.re/go/core v0.5.0
forge.lthn.ai/core/go-io v0.1.7 dappco.re/go/core/io v0.2.0
forge.lthn.ai/core/go-log v0.0.4 dappco.re/go/core/log v0.1.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.49.0 golang.org/x/crypto v0.49.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
forge.lthn.ai/core/go v0.3.3 // indirect forge.lthn.ai/core/go-log v0.0.4 // indirect
forge.lthn.ai/core/go-i18n v0.1.7 // indirect
forge.lthn.ai/core/go-inference v0.1.6 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
) )

70
go.sum
View file

@ -1,87 +1,29 @@
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg= dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U=
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs= dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0= dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ= dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA= dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8= dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
forge.lthn.ai/core/go-inference v0.1.6 h1:ce42zC0zO8PuISUyAukAN1NACEdWp5wF1mRgnh5+58E=
forge.lthn.ai/core/go-inference v0.1.6/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

50
kb/Executor.md Normal file
View file

@ -0,0 +1,50 @@
# Executor
Module: `dappco.re/go/core/ansible`
The `Executor` is the main playbook runner. It manages SSH connections, variable resolution, conditional evaluation, loops, blocks, roles, handlers, and module execution.
## Execution Flow
1. Parse playbook YAML into `[]Play`
2. For each play:
- Resolve target hosts from inventory (apply `Limit` filter)
- Merge play variables
- Gather facts (SSH into hosts, collect OS/hostname/kernel info)
- Execute `pre_tasks`, `roles`, `tasks`, `post_tasks`
- Run notified handlers
3. Each task goes through:
- Tag matching (`Tags`, `SkipTags`)
- Block/rescue/always handling
- Include/import resolution
- `when` condition evaluation
- Loop expansion
- Module execution via SSH
- Result registration and handler notification
## Templating
Jinja2-like `{{ var }}` syntax is supported:
- Variable resolution from play vars, task vars, host vars, facts, registered results
- Dotted access: `{{ result.stdout }}`, `{{ result.rc }}`
- Filters: `| default(value)`, `| bool`, `| trim`
- Lookups: `lookup('env', 'HOME')`, `lookup('file', '/path')`
## Conditionals
`when` supports:
- Boolean literals: `true`, `false`
- Registered variable checks: `result is success`, `result is failed`, `result is changed`, `result is defined`
- Negation: `not condition`
- Variable truthiness checks
## SSH Client Features
- Key-based and password authentication
- Known hosts verification
- Privilege escalation (`become`/`sudo`) with password support
- File upload via `cat` (no SCP dependency)
- File download, stat, exists checks
- Context-based timeout and cancellation

64
kb/Home.md Normal file
View file

@ -0,0 +1,64 @@
# go-ansible
Module: `dappco.re/go/core/ansible`
Pure Go Ansible executor that parses and runs Ansible playbooks without requiring the Python ansible binary. Supports SSH-based remote execution, inventory parsing, Jinja2-like templating, module execution, roles, handlers, loops, blocks, and conditionals.
## Architecture
| File | Purpose |
|------|---------|
| `types.go` | Data types: `Playbook`, `Play`, `Task`, `TaskResult`, `Inventory`, `Host`, `Facts`, `KnownModules` |
| `parser.go` | YAML parsing for playbooks, inventory, roles, and task files |
| `executor.go` | Playbook execution engine with SSH client management, templating, conditionals |
| `ssh.go` | `SSHClient` for remote command execution, file upload/download |
| `modules.go` | Ansible module implementations (shell, copy, template, file, service, etc.) |
CLI registration in `cmd/ansible/`.
## Key Types
### Core Types
- **`Executor`** — Runs playbooks: `Run()`, `SetInventory()`, `SetVar()`. Supports callbacks: `OnPlayStart`, `OnTaskStart`, `OnTaskEnd`, `OnPlayEnd`. Options: `Limit`, `Tags`, `SkipTags`, `CheckMode`, `Diff`, `Verbose`
- **`Parser`** — Parses YAML: `ParsePlaybook()`, `ParseInventory()`, `ParseRole()`, `ParseTasks()`
- **`SSHClient`** — SSH operations: `Connect()`, `Run()`, `RunScript()`, `Upload()`, `Download()`, `FileExists()`, `Stat()`, `SetBecome()`
- **`SSHConfig`** — Connection config: `Host`, `Port`, `User`, `Password`, `KeyFile`, `Become`, `BecomeUser`, `BecomePass`, `Timeout`
### Playbook Types
- **`Play`** — Single play: `Name`, `Hosts`, `Become`, `Vars`, `PreTasks`, `Tasks`, `PostTasks`, `Roles`, `Handlers`
- **`Task`** — Single task: `Name`, `Module`, `Args`, `Register`, `When`, `Loop`, `LoopControl`, `Block`, `Rescue`, `Always`, `Notify`, `IncludeTasks`, `ImportTasks`
- **`TaskResult`** — Execution result: `Changed`, `Failed`, `Skipped`, `Msg`, `Stdout`, `Stderr`, `RC`, `Results` (for loops)
- **`RoleRef`** — Role reference with vars and conditions
### Inventory Types
- **`Inventory`** — Top-level with `All` group
- **`InventoryGroup`** — `Hosts`, `Children`, `Vars`
- **`Host`** — Connection details: `AnsibleHost`, `AnsiblePort`, `AnsibleUser`, `AnsibleSSHPrivateKeyFile`
- **`Facts`** — Gathered facts: `Hostname`, `FQDN`, `OS`, `Distribution`, `Architecture`, `Kernel`, `Memory`, `CPUs`
## Usage
```go
import "dappco.re/go/core/ansible"
executor := ansible.NewExecutor("/path/to/playbooks")
executor.SetInventory("inventory/hosts.yml")
executor.SetVar("deploy_version", "1.2.3")
executor.OnTaskStart = func(host string, task *ansible.Task) {
fmt.Printf("[%s] %s\n", host, task.Name)
}
err := executor.Run(ctx, "deploy.yml")
defer executor.Close()
```
## Dependencies
- `dappco.re/go/core/log` — Structured logging and errors
- `golang.org/x/crypto/ssh` — SSH client
- `golang.org/x/crypto/ssh/knownhosts` — Host key verification
- `gopkg.in/yaml.v3` — YAML parsing

View file

@ -9,8 +9,8 @@ import (
"strconv" "strconv"
"strings" "strings"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
) )
// executeModule dispatches to the appropriate module handler. // executeModule dispatches to the appropriate module handler.

View file

@ -8,8 +8,8 @@ import (
"slices" "slices"
"strings" "strings"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )

4
ssh.go
View file

@ -12,8 +12,8 @@ import (
"sync" "sync"
"time" "time"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh/knownhosts"
) )