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/
.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"
"time"
ansible "forge.lthn.ai/core/go-ansible"
"forge.lthn.ai/core/cli/pkg/cli"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
"dappco.re/go/core"
"dappco.re/go/core/ansible"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
var (
ansibleInventory string
ansibleLimit string
ansibleTags string
ansibleSkipTags string
ansibleVars []string
ansibleVerbose int
ansibleCheck bool
)
var ansibleCmd = &cli.Command{
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,
// args extracts all positional arguments from Options.
func args(opts core.Options) []string {
var out []string
for _, o := range opts {
if o.Key == "_arg" {
if s, ok := o.Value.(string); ok {
out = append(out, s)
}
}
}
return out
}
var ansibleTestCmd = &cli.Command{
Use: "test <host>",
Short: "Test SSH connectivity to a host",
Long: `Test SSH connection and gather facts from a host.
Examples:
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]
func runAnsible(opts core.Options) core.Result {
positional := args(opts)
if len(positional) < 1 {
return core.Result{Value: coreerr.E("runAnsible", "usage: ansible <playbook>", nil)}
}
playbookPath := positional[0]
// Resolve playbook path
if !filepath.IsAbs(playbookPath) {
@ -96,7 +39,7 @@ func runAnsible(cmd *cli.Command, args []string) error {
}
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
@ -105,38 +48,38 @@ func runAnsible(cmd *cli.Command, args []string) error {
defer executor.Close()
// Set options
executor.Limit = ansibleLimit
executor.CheckMode = ansibleCheck
executor.Verbose = ansibleVerbose
executor.Limit = opts.String("limit")
executor.CheckMode = opts.Bool("check")
executor.Verbose = opts.Int("verbose")
if ansibleTags != "" {
executor.Tags = strings.Split(ansibleTags, ",")
if tags := opts.String("tags"); tags != "" {
executor.Tags = strings.Split(tags, ",")
}
if ansibleSkipTags != "" {
executor.SkipTags = strings.Split(ansibleSkipTags, ",")
if skipTags := opts.String("skip-tags"); skipTags != "" {
executor.SkipTags = strings.Split(skipTags, ",")
}
// Parse extra vars
for _, v := range ansibleVars {
parts := strings.SplitN(v, "=", 2)
if len(parts) == 2 {
executor.SetVar(parts[0], parts[1])
if extraVars := opts.String("extra-vars"); extraVars != "" {
for _, v := range strings.Split(extraVars, ",") {
parts := strings.SplitN(v, "=", 2)
if len(parts) == 2 {
executor.SetVar(parts[0], parts[1])
}
}
}
// Load inventory
if ansibleInventory != "" {
invPath := ansibleInventory
if invPath := opts.String("inventory"); invPath != "" {
if !filepath.IsAbs(invPath) {
invPath, _ = filepath.Abs(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) {
// Look for inventory.yml or hosts.yml
for _, name := range []string{"inventory.yml", "hosts.yml", "inventory.yaml", "hosts.yaml"} {
p := filepath.Join(invPath, name)
if coreio.Local.Exists(p) {
@ -147,13 +90,13 @@ func runAnsible(cmd *cli.Command, args []string) error {
}
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
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))
}
@ -162,41 +105,36 @@ func runAnsible(cmd *cli.Command, args []string) error {
if taskName == "" {
taskName = task.Module
}
fmt.Printf("\n%s %s\n", cli.TitleStyle.Render("TASK"), cli.BoldStyle.Render("["+taskName+"]"))
if ansibleVerbose > 0 {
fmt.Printf("%s\n", cli.DimStyle.Render("host: "+host))
fmt.Printf("\nTASK [%s]\n", taskName)
if executor.Verbose > 0 {
fmt.Printf("host: %s\n", host)
}
}
executor.OnTaskEnd = func(host string, task *ansible.Task, result *ansible.TaskResult) {
status := "ok"
style := cli.SuccessStyle
if result.Failed {
status = "failed"
style = cli.ErrorStyle
} else if result.Skipped {
status = "skipping"
style = cli.DimStyle
} else if result.Changed {
status = "changed"
style = cli.WarningStyle
}
fmt.Printf("%s: [%s]", style.Render(status), host)
if result.Msg != "" && ansibleVerbose > 0 {
fmt.Printf("%s: [%s]", status, host)
if result.Msg != "" && executor.Verbose > 0 {
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.Println()
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 != "" {
fmt.Printf("stdout: %s\n", strings.TrimSpace(result.Stdout))
}
@ -211,36 +149,38 @@ func runAnsible(cmd *cli.Command, args []string) error {
ctx := context.Background()
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 {
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",
cli.SuccessStyle.Render("✓"),
time.Since(start).Round(time.Millisecond))
fmt.Printf("\nPlaybook completed in %s\n", time.Since(start).Round(time.Millisecond))
return nil
return core.Result{OK: true}
}
func runAnsibleTest(cmd *cli.Command, args []string) error {
host := args[0]
func runAnsibleTest(opts core.Options) core.Result {
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{
Host: host,
Port: testPort,
User: testUser,
Password: testPassword,
KeyFile: testKeyFile,
Port: opts.Int("port"),
User: opts.String("user"),
Password: opts.String("password"),
KeyFile: opts.String("key"),
Timeout: 30 * time.Second,
}
client, err := ansible.NewSSHClient(cfg)
if err != nil {
return coreerr.E("runAnsibleTest", "create client", err)
return core.Result{Value: coreerr.E("runAnsibleTest", "create client", err)}
}
defer func() { _ = client.Close() }()
@ -250,58 +190,50 @@ func runAnsibleTest(cmd *cli.Command, args []string) error {
// Test connection
start := time.Now()
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)
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
fmt.Println("\nGathering facts...")
// 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")
if stdout != "" {
fmt.Printf(" OS: %s\n", strings.TrimSpace(stdout))
}
// Kernel
stdout, _, _, _ = client.Run(ctx, "uname -r")
fmt.Printf(" Kernel: %s\n", strings.TrimSpace(stdout))
// Architecture
stdout, _, _, _ = client.Run(ctx, "uname -m")
fmt.Printf(" Architecture: %s\n", strings.TrimSpace(stdout))
// Memory
stdout, _, _, _ = client.Run(ctx, "free -h | grep Mem | awk '{print $2}'")
fmt.Printf(" Memory: %s\n", strings.TrimSpace(stdout))
// Disk
stdout, _, _, _ = client.Run(ctx, "df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}'")
fmt.Printf(" Disk: %s\n", strings.TrimSpace(stdout))
// Docker
stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null")
if err == nil {
fmt.Printf(" Docker: %s\n", cli.SuccessStyle.Render(strings.TrimSpace(stdout)))
fmt.Printf(" Docker: %s\n", strings.TrimSpace(stdout))
} 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'")
if strings.TrimSpace(stdout) == "running" {
fmt.Printf(" Coolify: %s\n", cli.SuccessStyle.Render("running"))
fmt.Printf(" Coolify: running\n")
} 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
import (
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core"
)
func init() {
cli.RegisterCommands(AddAnsibleCommands)
}
// Register registers the 'ansible' command and all subcommands on the given Core instance.
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.
func AddAnsibleCommands(root *cli.Command) {
root.AddCommand(ansibleCmd)
c.Command("ansible/test", core.Command{
Description: "Test SSH connectivity to a host",
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.mod Module definition (forge.lthn.ai/core/go-ansible)
go.mod Module definition (dappco.re/go/core/ansible)
go.sum Dependency checksums
CLAUDE.md AI assistant context file
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
`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
```
forge.lthn.ai/core/go-ansible
dappco.re/go/core/ansible
```
Requires **Go 1.26+**.
@ -26,7 +26,7 @@ import (
"context"
"fmt"
ansible "forge.lthn.ai/core/go-ansible"
ansible "dappco.re/go/core/ansible"
)
func main() {
@ -148,8 +148,8 @@ Both fully-qualified collection names (e.g. `ansible.builtin.shell`) and short-f
| Module | Purpose |
|--------|---------|
| `forge.lthn.ai/core/cli` | CLI framework (command registration, flags, styled output) |
| `forge.lthn.ai/core/go-log` | Structured logging and contextual error helper (`log.E()`) |
| `dappco.re/go/core` | Core framework (command registration, flags) |
| `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`) |
| `gopkg.in/yaml.v3` | YAML parsing for playbooks, inventories, and role files |
| `github.com/stretchr/testify` | Test assertions (test-only) |

View file

@ -11,8 +11,8 @@ import (
"text/template"
"time"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// 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
require (
forge.lthn.ai/core/cli v0.3.7
forge.lthn.ai/core/go-io v0.1.7
forge.lthn.ai/core/go-log v0.0.4
dappco.re/go/core v0.5.0
dappco.re/go/core/io v0.2.0
dappco.re/go/core/log v0.1.0
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.49.0
gopkg.in/yaml.v3 v3.0.1
)
require (
forge.lthn.ai/core/go v0.3.3 // 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
forge.lthn.ai/core/go-log v0.0.4 // 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/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/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=
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
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=
dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U=
dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
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=
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/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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
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/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/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/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/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/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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/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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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"
"strings"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// executeModule dispatches to the appropriate module handler.

View file

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

4
ssh.go
View file

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