diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4963a87 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main] + pull_request_review: + types: [submitted] + +jobs: + test: + if: github.event_name != 'pull_request_review' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dAppCore/build/actions/build/core@dev + with: + go-version: "1.26" + run-vet: "true" + + auto-fix: + if: > + github.event_name == 'pull_request_review' && + github.event.review.user.login == 'coderabbitai' && + github.event.review.state == 'changes_requested' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + - uses: dAppCore/build/actions/fix@dev + with: + go-version: "1.26" + + auto-merge: + if: > + github.event_name == 'pull_request_review' && + github.event.review.user.login == 'coderabbitai' && + github.event.review.state == 'approved' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Merge PR + run: gh pr merge ${{ github.event.pull_request.number }} --merge --delete-branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 815e1fa..cdc6f76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -.core/ .idea/ +.vscode/ +*.log +.core/ diff --git a/CONSUMERS.md b/CONSUMERS.md new file mode 100644 index 0000000..9f107b0 --- /dev/null +++ b/CONSUMERS.md @@ -0,0 +1,7 @@ +# Consumers of go-ansible + +These modules import `dappco.re/go/core/ansible`: + +- go-infra + +**Breaking change risk: 1 consumers.** diff --git a/cmd/ansible/ansible.go b/cmd/ansible/ansible.go index c561329..aa1cf91 100644 --- a/cmd/ansible/ansible.go +++ b/cmd/ansible/ansible.go @@ -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 ", - 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{ - Use: "test ", - 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) +// 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 } -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 ", 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 ", 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} } diff --git a/cmd/ansible/cmd.go b/cmd/ansible/cmd.go index d0b714f..470fb4f 100644 --- a/cmd/ansible/cmd.go +++ b/cmd/ansible/cmd.go @@ -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}, + }, + }) } diff --git a/docs/development.md b/docs/development.md index 03a3d27..7dd179e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 diff --git a/docs/index.md b/docs/index.md index 5f77363..505b0db 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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) | diff --git a/executor.go b/executor.go index 985171d..f65c9bb 100644 --- a/executor.go +++ b/executor.go @@ -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. diff --git a/go.mod b/go.mod index 72f1f05..4e293a5 100644 --- a/go.mod +++ b/go.mod @@ -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.5 - forge.lthn.ai/core/go-io v0.1.4 - 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.1 // indirect - forge.lthn.ai/core/go-i18n v0.1.5 // indirect - forge.lthn.ai/core/go-inference v0.1.4 // 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 ) diff --git a/go.sum b/go.sum index fd864ad..dcf1a1a 100644 --- a/go.sum +++ b/go.sum @@ -1,87 +1,29 @@ -forge.lthn.ai/core/cli v0.3.5 h1:P7yK0DmSA1QnUMFuCjJZf/fk/akKPIxopQ6OwD8Sar8= -forge.lthn.ai/core/cli v0.3.5/go.mod h1:SeArHx+hbpX5iZqgASCD7Q1EDoc6uaaGiGBotmNzIx4= -forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM= -forge.lthn.ai/core/go v0.3.1/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc= -forge.lthn.ai/core/go-i18n v0.1.5 h1:B4hV4eTl63akZiplM8lswuttctrcSOCWyFSGBZmu6Nc= -forge.lthn.ai/core/go-i18n v0.1.5/go.mod h1:hJsUxmqdPly73i3VkTDxvmbrpjxSd65hQVQqWA3+fnM= -forge.lthn.ai/core/go-inference v0.1.4 h1:fuAgWbqsEDajHniqAKyvHYbRcBrkGEiGSqR2pfTMRY0= -forge.lthn.ai/core/go-inference v0.1.4/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= -forge.lthn.ai/core/go-io v0.1.4 h1:eHH8UIQg2NRM5kR9c6b7plVgrHagK/bq8FwROE0Np6I= -forge.lthn.ai/core/go-io v0.1.4/go.mod h1:FRtXSsi8W+U9vewCU+LBAqqbIj3wjXA4dBdSv3SAtWI= +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= diff --git a/kb/Executor.md b/kb/Executor.md new file mode 100644 index 0000000..9e1627e --- /dev/null +++ b/kb/Executor.md @@ -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 diff --git a/kb/Home.md b/kb/Home.md new file mode 100644 index 0000000..a7c3294 --- /dev/null +++ b/kb/Home.md @@ -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 diff --git a/modules.go b/modules.go index da261a1..d6bb983 100644 --- a/modules.go +++ b/modules.go @@ -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. diff --git a/parser.go b/parser.go index 9f9d21a..c3e8277 100644 --- a/parser.go +++ b/parser.go @@ -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" ) diff --git a/ssh.go b/ssh.go index e296b93..ae5d9a6 100644 --- a/ssh.go +++ b/ssh.go @@ -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" )