diff --git a/cmd/ansible/ansible.go b/cmd/ansible/ansible.go index aa1cf91..4795e79 100644 --- a/cmd/ansible/ansible.go +++ b/cmd/ansible/ansible.go @@ -2,9 +2,6 @@ package anscmd import ( "context" - "fmt" - "path/filepath" - "strings" "time" "dappco.re/go/core" @@ -16,7 +13,7 @@ import ( // args extracts all positional arguments from Options. func args(opts core.Options) []string { var out []string - for _, o := range opts { + for _, o := range opts.Items() { if o.Key == "_arg" { if s, ok := o.Value.(string); ok { out = append(out, s) @@ -34,16 +31,16 @@ func runAnsible(opts core.Options) core.Result { playbookPath := positional[0] // Resolve playbook path - if !filepath.IsAbs(playbookPath) { - playbookPath, _ = filepath.Abs(playbookPath) + if !pathIsAbs(playbookPath) { + playbookPath = absPath(playbookPath) } if !coreio.Local.Exists(playbookPath) { - return core.Result{Value: coreerr.E("runAnsible", fmt.Sprintf("playbook not found: %s", playbookPath), nil)} + return core.Result{Value: coreerr.E("runAnsible", sprintf("playbook not found: %s", playbookPath), nil)} } // Create executor - basePath := filepath.Dir(playbookPath) + basePath := pathDir(playbookPath) executor := ansible.NewExecutor(basePath) defer executor.Close() @@ -53,16 +50,16 @@ func runAnsible(opts core.Options) core.Result { executor.Verbose = opts.Int("verbose") if tags := opts.String("tags"); tags != "" { - executor.Tags = strings.Split(tags, ",") + executor.Tags = split(tags, ",") } if skipTags := opts.String("skip-tags"); skipTags != "" { - executor.SkipTags = strings.Split(skipTags, ",") + executor.SkipTags = split(skipTags, ",") } // Parse extra vars if extraVars := opts.String("extra-vars"); extraVars != "" { - for _, v := range strings.Split(extraVars, ",") { - parts := strings.SplitN(v, "=", 2) + for _, v := range split(extraVars, ",") { + parts := splitN(v, "=", 2) if len(parts) == 2 { executor.SetVar(parts[0], parts[1]) } @@ -71,17 +68,17 @@ func runAnsible(opts core.Options) core.Result { // Load inventory if invPath := opts.String("inventory"); invPath != "" { - if !filepath.IsAbs(invPath) { - invPath, _ = filepath.Abs(invPath) + if !pathIsAbs(invPath) { + invPath = absPath(invPath) } if !coreio.Local.Exists(invPath) { - return core.Result{Value: coreerr.E("runAnsible", fmt.Sprintf("inventory not found: %s", invPath), nil)} + return core.Result{Value: coreerr.E("runAnsible", sprintf("inventory not found: %s", invPath), nil)} } if coreio.Local.IsDir(invPath) { for _, name := range []string{"inventory.yml", "hosts.yml", "inventory.yaml", "hosts.yaml"} { - p := filepath.Join(invPath, name) + p := joinPath(invPath, name) if coreio.Local.Exists(p) { invPath = p break @@ -96,8 +93,9 @@ func runAnsible(opts core.Options) core.Result { // Set up callbacks executor.OnPlayStart = func(play *ansible.Play) { - fmt.Printf("\nPLAY [%s]\n", play.Name) - fmt.Println(strings.Repeat("*", 70)) + print("") + print("PLAY [%s]", play.Name) + print("%s", repeat("*", 70)) } executor.OnTaskStart = func(host string, task *ansible.Task) { @@ -105,9 +103,10 @@ func runAnsible(opts core.Options) core.Result { if taskName == "" { taskName = task.Module } - fmt.Printf("\nTASK [%s]\n", taskName) + print("") + print("TASK [%s]", taskName) if executor.Verbose > 0 { - fmt.Printf("host: %s\n", host) + print("host: %s", host) } } @@ -121,41 +120,42 @@ func runAnsible(opts core.Options) core.Result { status = "changed" } - fmt.Printf("%s: [%s]", status, host) + line := sprintf("%s: [%s]", status, host) if result.Msg != "" && executor.Verbose > 0 { - fmt.Printf(" => %s", result.Msg) + line = sprintf("%s => %s", line, result.Msg) } if result.Duration > 0 && executor.Verbose > 1 { - fmt.Printf(" (%s)", result.Duration.Round(time.Millisecond)) + line = sprintf("%s (%s)", line, result.Duration.Round(time.Millisecond)) } - fmt.Println() + print("%s", line) if result.Failed && result.Stderr != "" { - fmt.Printf("%s\n", result.Stderr) + print("%s", result.Stderr) } if executor.Verbose > 1 { if result.Stdout != "" { - fmt.Printf("stdout: %s\n", strings.TrimSpace(result.Stdout)) + print("stdout: %s", trimSpace(result.Stdout)) } } } executor.OnPlayEnd = func(play *ansible.Play) { - fmt.Println() + print("") } // Run playbook ctx := context.Background() start := time.Now() - fmt.Printf("Running playbook: %s\n", playbookPath) + print("Running playbook: %s", playbookPath) if err := executor.Run(ctx, playbookPath); err != nil { return core.Result{Value: coreerr.E("runAnsible", "playbook failed", err)} } - fmt.Printf("\nPlaybook completed in %s\n", time.Since(start).Round(time.Millisecond)) + print("") + print("Playbook completed in %s", time.Since(start).Round(time.Millisecond)) return core.Result{OK: true} } @@ -167,7 +167,7 @@ func runAnsibleTest(opts core.Options) core.Result { } host := positional[0] - fmt.Printf("Testing SSH connection to %s...\n", host) + print("Testing SSH connection to %s...", host) cfg := ansible.SSHConfig{ Host: host, @@ -194,46 +194,48 @@ func runAnsibleTest(opts core.Options) core.Result { } connectTime := time.Since(start) - fmt.Printf("Connected in %s\n", connectTime.Round(time.Millisecond)) + print("Connected in %s", connectTime.Round(time.Millisecond)) // Gather facts - fmt.Println("\nGathering facts...") + print("") + print("Gathering facts...") stdout, _, _, _ := client.Run(ctx, "hostname -f 2>/dev/null || hostname") - fmt.Printf(" Hostname: %s\n", strings.TrimSpace(stdout)) + print(" Hostname: %s", trimSpace(stdout)) 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)) + print(" OS: %s", trimSpace(stdout)) } stdout, _, _, _ = client.Run(ctx, "uname -r") - fmt.Printf(" Kernel: %s\n", strings.TrimSpace(stdout)) + print(" Kernel: %s", trimSpace(stdout)) stdout, _, _, _ = client.Run(ctx, "uname -m") - fmt.Printf(" Architecture: %s\n", strings.TrimSpace(stdout)) + print(" Architecture: %s", trimSpace(stdout)) stdout, _, _, _ = client.Run(ctx, "free -h | grep Mem | awk '{print $2}'") - fmt.Printf(" Memory: %s\n", strings.TrimSpace(stdout)) + print(" Memory: %s", trimSpace(stdout)) stdout, _, _, _ = client.Run(ctx, "df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}'") - fmt.Printf(" Disk: %s\n", strings.TrimSpace(stdout)) + print(" Disk: %s", trimSpace(stdout)) stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null") if err == nil { - fmt.Printf(" Docker: %s\n", strings.TrimSpace(stdout)) + print(" Docker: %s", trimSpace(stdout)) } else { - fmt.Printf(" Docker: not installed\n") + print(" Docker: not installed") } 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: running\n") + if trimSpace(stdout) == "running" { + print(" Coolify: running") } else { - fmt.Printf(" Coolify: not installed\n") + print(" Coolify: not installed") } - fmt.Printf("\nSSH test passed\n") + print("") + print("SSH test passed") return core.Result{OK: true} } diff --git a/cmd/ansible/cmd.go b/cmd/ansible/cmd.go index 470fb4f..7f84a99 100644 --- a/cmd/ansible/cmd.go +++ b/cmd/ansible/cmd.go @@ -9,25 +9,25 @@ 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}, - }, + Flags: core.NewOptions( + core.Option{Key: "inventory", Value: ""}, + core.Option{Key: "limit", Value: ""}, + core.Option{Key: "tags", Value: ""}, + core.Option{Key: "skip-tags", Value: ""}, + core.Option{Key: "extra-vars", Value: ""}, + core.Option{Key: "verbose", Value: 0}, + core.Option{Key: "check", Value: false}, + ), }) 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}, - }, + Flags: core.NewOptions( + core.Option{Key: "user", Value: "root"}, + core.Option{Key: "password", Value: ""}, + core.Option{Key: "key", Value: ""}, + core.Option{Key: "port", Value: 22}, + ), }) } diff --git a/cmd/ansible/core_primitives.go b/cmd/ansible/core_primitives.go new file mode 100644 index 0000000..683146d --- /dev/null +++ b/cmd/ansible/core_primitives.go @@ -0,0 +1,160 @@ +package anscmd + +import ( + "unicode" + "unicode/utf8" + + "dappco.re/go/core" +) + +func absPath(path string) string { + if path == "" { + return core.Env("DIR_CWD") + } + if core.PathIsAbs(path) { + return cleanPath(path) + } + + cwd := core.Env("DIR_CWD") + if cwd == "" { + cwd = "." + } + return joinPath(cwd, path) +} + +func joinPath(parts ...string) string { + ds := dirSep() + path := "" + for _, part := range parts { + if part == "" { + continue + } + if path == "" { + path = part + continue + } + + path = core.TrimSuffix(path, ds) + part = core.TrimPrefix(part, ds) + path = core.Concat(path, ds, part) + } + + if path == "" { + return "." + } + return core.CleanPath(path, ds) +} + +func cleanPath(path string) string { + if path == "" { + return "." + } + return core.CleanPath(path, dirSep()) +} + +func pathDir(path string) string { + return core.PathDir(path) +} + +func pathIsAbs(path string) bool { + return core.PathIsAbs(path) +} + +func sprintf(format string, args ...any) string { + return core.Sprintf(format, args...) +} + +func split(s, sep string) []string { + return core.Split(s, sep) +} + +func splitN(s, sep string, n int) []string { + return core.SplitN(s, sep, n) +} + +func trimSpace(s string) string { + return core.Trim(s) +} + +func repeat(s string, count int) string { + if count <= 0 { + return "" + } + + buf := core.NewBuilder() + for i := 0; i < count; i++ { + buf.WriteString(s) + } + return buf.String() +} + +func print(format string, args ...any) { + core.Print(nil, format, args...) +} + +func println(args ...any) { + core.Println(args...) +} + +func dirSep() string { + ds := core.Env("DS") + if ds == "" { + return "/" + } + return ds +} + +func containsRune(cutset string, target rune) bool { + for _, candidate := range cutset { + if candidate == target { + return true + } + } + return false +} + +func trimCutset(s, cutset string) string { + start := 0 + end := len(s) + + for start < end { + r, size := utf8.DecodeRuneInString(s[start:end]) + if !containsRune(cutset, r) { + break + } + start += size + } + + for start < end { + r, size := utf8.DecodeLastRuneInString(s[start:end]) + if !containsRune(cutset, r) { + break + } + end -= size + } + + return s[start:end] +} + +func fields(s string) []string { + var out []string + start := -1 + + for i, r := range s { + if unicode.IsSpace(r) { + if start >= 0 { + out = append(out, s[start:i]) + start = -1 + } + continue + } + if start < 0 { + start = i + } + } + + if start >= 0 { + out = append(out, s[start:]) + } + return out +} diff --git a/core_primitives.go b/core_primitives.go new file mode 100644 index 0000000..a262fa2 --- /dev/null +++ b/core_primitives.go @@ -0,0 +1,291 @@ +package ansible + +import ( + "unicode" + "unicode/utf8" + + core "dappco.re/go/core" +) + +type stringBuffer interface { + Write([]byte) (int, error) + WriteString(string) (int, error) + String() string +} + +func dirSep() string { + ds := core.Env("DS") + if ds == "" { + return "/" + } + return ds +} + +func corexAbsPath(path string) string { + if path == "" { + return core.Env("DIR_CWD") + } + if core.PathIsAbs(path) { + return corexCleanPath(path) + } + + cwd := core.Env("DIR_CWD") + if cwd == "" { + cwd = "." + } + return corexJoinPath(cwd, path) +} + +func corexJoinPath(parts ...string) string { + ds := dirSep() + path := "" + for _, part := range parts { + if part == "" { + continue + } + if path == "" { + path = part + continue + } + + path = core.TrimSuffix(path, ds) + part = core.TrimPrefix(part, ds) + path = core.Concat(path, ds, part) + } + + if path == "" { + return "." + } + return core.CleanPath(path, ds) +} + +func corexCleanPath(path string) string { + if path == "" { + return "." + } + return core.CleanPath(path, dirSep()) +} + +func corexPathDir(path string) string { + return core.PathDir(path) +} + +func corexPathBase(path string) string { + return core.PathBase(path) +} + +func corexPathIsAbs(path string) bool { + return core.PathIsAbs(path) +} + +func corexEnv(key string) string { + return core.Env(key) +} + +func corexSprintf(format string, args ...any) string { + return core.Sprintf(format, args...) +} + +func corexSprint(args ...any) string { + return core.Sprint(args...) +} + +func corexContains(s, substr string) bool { + return core.Contains(s, substr) +} + +func corexHasPrefix(s, prefix string) bool { + return core.HasPrefix(s, prefix) +} + +func corexHasSuffix(s, suffix string) bool { + return core.HasSuffix(s, suffix) +} + +func corexSplit(s, sep string) []string { + return core.Split(s, sep) +} + +func corexSplitN(s, sep string, n int) []string { + return core.SplitN(s, sep, n) +} + +func corexJoin(sep string, parts []string) string { + return core.Join(sep, parts...) +} + +func corexLower(s string) string { + return core.Lower(s) +} + +func corexReplaceAll(s, old, new string) string { + return core.Replace(s, old, new) +} + +func corexReplaceN(s, old, new string, n int) string { + if n == 0 || old == "" { + return s + } + if n < 0 { + return corexReplaceAll(s, old, new) + } + + result := s + for i := 0; i < n; i++ { + index := corexStringIndex(result, old) + if index < 0 { + break + } + result = core.Concat(result[:index], new, result[index+len(old):]) + } + return result +} + +func corexTrimSpace(s string) string { + return core.Trim(s) +} + +func corexTrimPrefix(s, prefix string) string { + return core.TrimPrefix(s, prefix) +} + +func corexTrimCutset(s, cutset string) string { + start := 0 + end := len(s) + + for start < end { + r, size := utf8.DecodeRuneInString(s[start:end]) + if !corexContainsRune(cutset, r) { + break + } + start += size + } + + for start < end { + r, size := utf8.DecodeLastRuneInString(s[start:end]) + if !corexContainsRune(cutset, r) { + break + } + end -= size + } + + return s[start:end] +} + +func corexRepeat(s string, count int) string { + if count <= 0 { + return "" + } + + buf := core.NewBuilder() + for i := 0; i < count; i++ { + buf.WriteString(s) + } + return buf.String() +} + +func corexFields(s string) []string { + var out []string + start := -1 + + for i, r := range s { + if unicode.IsSpace(r) { + if start >= 0 { + out = append(out, s[start:i]) + start = -1 + } + continue + } + if start < 0 { + start = i + } + } + + if start >= 0 { + out = append(out, s[start:]) + } + return out +} + +func corexNewBuilder() stringBuffer { + return core.NewBuilder() +} + +func corexNewReader(s string) interface { + Read([]byte) (int, error) +} { + return core.NewReader(s) +} + +func corexReadAllString(reader any) (string, error) { + result := core.ReadAll(reader) + if !result.OK { + if err, ok := result.Value.(error); ok { + return "", err + } + return "", core.NewError("read content") + } + + if data, ok := result.Value.(string); ok { + return data, nil + } + return corexSprint(result.Value), nil +} + +func corexWriteString(writer interface { + Write([]byte) (int, error) +}, value string) { + _, _ = writer.Write([]byte(value)) +} + +func corexContainsRune(cutset string, target rune) bool { + for _, candidate := range cutset { + if candidate == target { + return true + } + } + return false +} + +func corexStringIndex(s, needle string) int { + if needle == "" { + return 0 + } + if len(needle) > len(s) { + return -1 + } + + for i := 0; i+len(needle) <= len(s); i++ { + if s[i:i+len(needle)] == needle { + return i + } + } + return -1 +} + +func absPath(path string) string { return corexAbsPath(path) } +func joinPath(parts ...string) string { return corexJoinPath(parts...) } +func cleanPath(path string) string { return corexCleanPath(path) } +func pathDir(path string) string { return corexPathDir(path) } +func pathBase(path string) string { return corexPathBase(path) } +func pathIsAbs(path string) bool { return corexPathIsAbs(path) } +func env(key string) string { return corexEnv(key) } +func sprintf(format string, args ...any) string { return corexSprintf(format, args...) } +func sprint(args ...any) string { return corexSprint(args...) } +func contains(s, substr string) bool { return corexContains(s, substr) } +func hasSuffix(s, suffix string) bool { return corexHasSuffix(s, suffix) } +func split(s, sep string) []string { return corexSplit(s, sep) } +func splitN(s, sep string, n int) []string { return corexSplitN(s, sep, n) } +func join(sep string, parts []string) string { return corexJoin(sep, parts) } +func lower(s string) string { return corexLower(s) } +func replaceAll(s, old, new string) string { return corexReplaceAll(s, old, new) } +func replaceN(s, old, new string, n int) string { return corexReplaceN(s, old, new, n) } +func trimCutset(s, cutset string) string { return corexTrimCutset(s, cutset) } +func repeat(s string, count int) string { return corexRepeat(s, count) } +func fields(s string) []string { return corexFields(s) } +func newBuilder() stringBuffer { return corexNewBuilder() } +func newReader(s string) interface{ Read([]byte) (int, error) } { return corexNewReader(s) } +func readAllString(reader any) (string, error) { return corexReadAllString(reader) } +func writeString(writer interface{ Write([]byte) (int, error) }, value string) { + corexWriteString(writer, value) +} diff --git a/executor.go b/executor.go index f65c9bb..5b11f7f 100644 --- a/executor.go +++ b/executor.go @@ -2,11 +2,8 @@ package ansible import ( "context" - "fmt" - "os" "regexp" "slices" - "strings" "sync" "text/template" "time" @@ -86,7 +83,7 @@ func (e *Executor) Run(ctx context.Context, playbookPath string) error { for i := range plays { if err := e.runPlay(ctx, &plays[i]); err != nil { - return coreerr.E("Executor.Run", fmt.Sprintf("play %d (%s)", i, plays[i].Name), err) + return coreerr.E("Executor.Run", sprintf("play %d (%s)", i, plays[i].Name), err) } } @@ -180,7 +177,7 @@ func (e *Executor) runRole(ctx context.Context, hosts []string, roleRef *RoleRef // Parse role tasks tasks, err := e.parser.ParseRole(roleRef.Role, roleRef.TasksFrom) if err != nil { - return coreerr.E("executor.runRole", fmt.Sprintf("parse role %s", roleRef.Role), err) + return coreerr.E("executor.runRole", sprintf("parse role %s", roleRef.Role), err) } // Merge role vars @@ -267,7 +264,7 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, task *Task, p // Get SSH client client, err := e.getClient(host, play) if err != nil { - return coreerr.E("Executor.runTaskOnHost", fmt.Sprintf("get client for %s", host), err) + return coreerr.E("Executor.runTaskOnHost", sprintf("get client for %s", host), err) } // Handle loops @@ -485,7 +482,7 @@ func (e *Executor) getHosts(pattern string) []string { var filtered []string for _, h := range hosts { - if limitSet[h] || h == e.Limit || strings.Contains(h, e.Limit) { + if limitSet[h] || h == e.Limit || contains(h, e.Limit) { filtered = append(filtered, h) } } @@ -582,32 +579,32 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err // Hostname stdout, _, _, err := client.Run(ctx, "hostname -f 2>/dev/null || hostname") if err == nil { - facts.FQDN = strings.TrimSpace(stdout) + facts.FQDN = corexTrimSpace(stdout) } stdout, _, _, err = client.Run(ctx, "hostname -s 2>/dev/null || hostname") if err == nil { - facts.Hostname = strings.TrimSpace(stdout) + facts.Hostname = corexTrimSpace(stdout) } // OS info stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2") - for line := range strings.SplitSeq(stdout, "\n") { - if strings.HasPrefix(line, "ID=") { - facts.Distribution = strings.Trim(strings.TrimPrefix(line, "ID="), "\"") + for _, line := range split(stdout, "\n") { + if corexHasPrefix(line, "ID=") { + facts.Distribution = trimCutset(corexTrimPrefix(line, "ID="), "\"") } - if strings.HasPrefix(line, "VERSION_ID=") { - facts.Version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"") + if corexHasPrefix(line, "VERSION_ID=") { + facts.Version = trimCutset(corexTrimPrefix(line, "VERSION_ID="), "\"") } } // Architecture stdout, _, _, _ = client.Run(ctx, "uname -m") - facts.Architecture = strings.TrimSpace(stdout) + facts.Architecture = corexTrimSpace(stdout) // Kernel stdout, _, _, _ = client.Run(ctx, "uname -r") - facts.Kernel = strings.TrimSpace(stdout) + facts.Kernel = corexTrimSpace(stdout) e.mu.Lock() e.facts[host] = facts @@ -650,11 +647,11 @@ func normalizeConditions(when any) []string { // evalCondition evaluates a single condition. func (e *Executor) evalCondition(cond string, host string) bool { - cond = strings.TrimSpace(cond) + cond = corexTrimSpace(cond) // Handle negation - if strings.HasPrefix(cond, "not ") { - return !e.evalCondition(strings.TrimPrefix(cond, "not "), host) + if corexHasPrefix(cond, "not ") { + return !e.evalCondition(corexTrimPrefix(cond, "not "), host) } // Handle boolean literals @@ -667,10 +664,10 @@ func (e *Executor) evalCondition(cond string, host string) bool { // Handle registered variable checks // e.g., "result is success", "result.rc == 0" - if strings.Contains(cond, " is ") { - parts := strings.SplitN(cond, " is ", 2) - varName := strings.TrimSpace(parts[0]) - check := strings.TrimSpace(parts[1]) + if contains(cond, " is ") { + parts := splitN(cond, " is ", 2) + varName := corexTrimSpace(parts[0]) + check := corexTrimSpace(parts[1]) result := e.getRegisteredVar(host, varName) if result == nil { @@ -694,7 +691,7 @@ func (e *Executor) evalCondition(cond string, host string) bool { } // Handle simple var checks - if strings.Contains(cond, " | default(") { + if contains(cond, " | default(") { // Extract var name and check if defined re := regexp.MustCompile(`(\w+)\s*\|\s*default\([^)]*\)`) if match := re.FindStringSubmatch(cond); len(match) > 1 { @@ -730,7 +727,7 @@ func (e *Executor) getRegisteredVar(host string, name string) *TaskResult { defer e.mu.RUnlock() // Handle dotted access (e.g., "result.stdout") - parts := strings.SplitN(name, ".", 2) + parts := splitN(name, ".", 2) varName := parts[0] if hostResults, ok := e.results[host]; ok { @@ -748,7 +745,7 @@ func (e *Executor) templateString(s string, host string, task *Task) string { re := regexp.MustCompile(`\{\{\s*([^}]+)\s*\}\}`) return re.ReplaceAllStringFunc(s, func(match string) string { - expr := strings.TrimSpace(match[2 : len(match)-2]) + expr := corexTrimSpace(match[2 : len(match)-2]) return e.resolveExpr(expr, host, task) }) } @@ -756,20 +753,20 @@ func (e *Executor) templateString(s string, host string, task *Task) string { // resolveExpr resolves a template expression. func (e *Executor) resolveExpr(expr string, host string, task *Task) string { // Handle filters - if strings.Contains(expr, " | ") { - parts := strings.SplitN(expr, " | ", 2) + if contains(expr, " | ") { + parts := splitN(expr, " | ", 2) value := e.resolveExpr(parts[0], host, task) return e.applyFilter(value, parts[1]) } // Handle lookups - if strings.HasPrefix(expr, "lookup(") { + if corexHasPrefix(expr, "lookup(") { return e.handleLookup(expr) } // Handle registered vars - if strings.Contains(expr, ".") { - parts := strings.SplitN(expr, ".", 2) + if contains(expr, ".") { + parts := splitN(expr, ".", 2) if result := e.getRegisteredVar(host, parts[0]); result != nil { switch parts[1] { case "stdout": @@ -777,24 +774,24 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string { case "stderr": return result.Stderr case "rc": - return fmt.Sprintf("%d", result.RC) + return sprintf("%d", result.RC) case "changed": - return fmt.Sprintf("%t", result.Changed) + return sprintf("%t", result.Changed) case "failed": - return fmt.Sprintf("%t", result.Failed) + return sprintf("%t", result.Failed) } } } // Check vars if val, ok := e.vars[expr]; ok { - return fmt.Sprintf("%v", val) + return sprintf("%v", val) } // Check task vars if task != nil { if val, ok := task.Vars[expr]; ok { - return fmt.Sprintf("%v", val) + return sprintf("%v", val) } } @@ -802,7 +799,7 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string { if e.inventory != nil { hostVars := GetHostVars(e.inventory, host) if val, ok := hostVars[expr]; ok { - return fmt.Sprintf("%v", val) + return sprintf("%v", val) } } @@ -829,15 +826,15 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string { // applyFilter applies a Jinja2 filter. func (e *Executor) applyFilter(value, filter string) string { - filter = strings.TrimSpace(filter) + filter = corexTrimSpace(filter) // Handle default filter - if strings.HasPrefix(filter, "default(") { + if corexHasPrefix(filter, "default(") { if value == "" || value == "{{ "+filter+" }}" { // Extract default value re := regexp.MustCompile(`default\(([^)]*)\)`) if match := re.FindStringSubmatch(filter); len(match) > 1 { - return strings.Trim(match[1], "'\"") + return trimCutset(match[1], "'\"") } } return value @@ -845,8 +842,8 @@ func (e *Executor) applyFilter(value, filter string) string { // Handle bool filter if filter == "bool" { - lower := strings.ToLower(value) - if lower == "true" || lower == "yes" || lower == "1" { + lowered := lower(value) + if lowered == "true" || lowered == "yes" || lowered == "1" { return "true" } return "false" @@ -854,7 +851,7 @@ func (e *Executor) applyFilter(value, filter string) string { // Handle trim if filter == "trim" { - return strings.TrimSpace(value) + return corexTrimSpace(value) } // Handle b64decode @@ -880,7 +877,7 @@ func (e *Executor) handleLookup(expr string) string { switch lookupType { case "env": - return os.Getenv(arg) + return env(arg) case "file": if data, err := coreio.Local.Read(arg); err == nil { return data @@ -978,9 +975,9 @@ func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) { // Convert Jinja2 to Go template syntax (basic conversion) tmplContent := content - tmplContent = strings.ReplaceAll(tmplContent, "{{", "{{ .") - tmplContent = strings.ReplaceAll(tmplContent, "{%", "{{") - tmplContent = strings.ReplaceAll(tmplContent, "%}", "}}") + tmplContent = replaceAll(tmplContent, "{{", "{{ .") + tmplContent = replaceAll(tmplContent, "{%", "{{") + tmplContent = replaceAll(tmplContent, "%}", "}}") tmpl, err := template.New("template").Parse(tmplContent) if err != nil { @@ -1010,8 +1007,8 @@ func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) { context["ansible_kernel"] = facts.Kernel } - var buf strings.Builder - if err := tmpl.Execute(&buf, context); err != nil { + buf := newBuilder() + if err := tmpl.Execute(buf, context); err != nil { return e.templateString(content, host, task), nil } diff --git a/go.mod b/go.mod index 4e293a5..b0ea219 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module dappco.re/go/core/ansible go 1.26.0 require ( - dappco.re/go/core v0.5.0 + dappco.re/go/core v0.8.0-alpha.1 dappco.re/go/core/io v0.2.0 dappco.re/go/core/log v0.1.0 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index dcf1a1a..47503e9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -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 v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= +dappco.re/go/core v0.8.0-alpha.1/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= diff --git a/modules.go b/modules.go index d6bb983..2a0d815 100644 --- a/modules.go +++ b/modules.go @@ -3,11 +3,8 @@ package ansible import ( "context" "encoding/base64" - "fmt" "os" - "path/filepath" "strconv" - "strings" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" @@ -135,7 +132,7 @@ func (e *Executor) executeModule(ctx context.Context, host string, client *SSHCl default: // For unknown modules, try to execute as shell if it looks like a command - if strings.Contains(task.Module, " ") || task.Module == "" { + if contains(task.Module, " ") || task.Module == "" { return e.moduleShell(ctx, client, args) } return nil, coreerr.E("Executor.executeModule", "unsupported module: "+module, nil) @@ -186,7 +183,7 @@ func (e *Executor) moduleShell(ctx context.Context, client *SSHClient, args map[ // Handle chdir if chdir := getStringArg(args, "chdir", ""); chdir != "" { - cmd = fmt.Sprintf("cd %q && %s", chdir, cmd) + cmd = sprintf("cd %q && %s", chdir, cmd) } stdout, stderr, rc, err := client.RunScript(ctx, cmd) @@ -214,7 +211,7 @@ func (e *Executor) moduleCommand(ctx context.Context, client *SSHClient, args ma // Handle chdir if chdir := getStringArg(args, "chdir", ""); chdir != "" { - cmd = fmt.Sprintf("cd %q && %s", chdir, cmd) + cmd = sprintf("cd %q && %s", chdir, cmd) } stdout, stderr, rc, err := client.Run(ctx, cmd) @@ -305,20 +302,20 @@ func (e *Executor) moduleCopy(ctx context.Context, client *SSHClient, args map[s } } - err = client.Upload(ctx, strings.NewReader(content), dest, mode) + err = client.Upload(ctx, newReader(content), dest, mode) if err != nil { return nil, err } // Handle owner/group (best-effort, errors ignored) if owner := getStringArg(args, "owner", ""); owner != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, dest)) + _, _, _, _ = client.Run(ctx, sprintf("chown %s %q", owner, dest)) } if group := getStringArg(args, "group", ""); group != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, dest)) + _, _, _, _ = client.Run(ctx, sprintf("chgrp %s %q", group, dest)) } - return &TaskResult{Changed: true, Msg: fmt.Sprintf("copied to %s", dest)}, nil + return &TaskResult{Changed: true, Msg: sprintf("copied to %s", dest)}, nil } func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) { @@ -341,12 +338,12 @@ func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args m } } - err = client.Upload(ctx, strings.NewReader(content), dest, mode) + err = client.Upload(ctx, newReader(content), dest, mode) if err != nil { return nil, err } - return &TaskResult{Changed: true, Msg: fmt.Sprintf("templated to %s", dest)}, nil + return &TaskResult{Changed: true, Msg: sprintf("templated to %s", dest)}, nil } func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { @@ -363,21 +360,21 @@ func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[s switch state { case "directory": mode := getStringArg(args, "mode", "0755") - cmd := fmt.Sprintf("mkdir -p %q && chmod %s %q", path, mode, path) + cmd := sprintf("mkdir -p %q && chmod %s %q", path, mode, path) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil } case "absent": - cmd := fmt.Sprintf("rm -rf %q", path) + cmd := sprintf("rm -rf %q", path) _, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil } case "touch": - cmd := fmt.Sprintf("touch %q", path) + cmd := sprintf("touch %q", path) _, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil @@ -388,7 +385,7 @@ func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[s if src == "" { return nil, coreerr.E("Executor.moduleFile", "src required for link state", nil) } - cmd := fmt.Sprintf("ln -sf %q %q", src, path) + cmd := sprintf("ln -sf %q %q", src, path) _, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil @@ -397,20 +394,20 @@ func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[s case "file": // Ensure file exists and set permissions if mode := getStringArg(args, "mode", ""); mode != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, path)) + _, _, _, _ = client.Run(ctx, sprintf("chmod %s %q", mode, path)) } } // Handle owner/group (best-effort, errors ignored) if owner := getStringArg(args, "owner", ""); owner != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, path)) + _, _, _, _ = client.Run(ctx, sprintf("chown %s %q", owner, path)) } if group := getStringArg(args, "group", ""); group != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, path)) + _, _, _, _ = client.Run(ctx, sprintf("chgrp %s %q", group, path)) } if recurse := getBoolArg(args, "recurse", false); recurse { if owner := getStringArg(args, "owner", ""); owner != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chown -R %s %q", owner, path)) + _, _, _, _ = client.Run(ctx, sprintf("chown -R %s %q", owner, path)) } } @@ -432,7 +429,7 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client *SSHClient, args if state == "absent" { if regexp != "" { - cmd := fmt.Sprintf("sed -i '/%s/d' %q", regexp, path) + cmd := sprintf("sed -i '/%s/d' %q", regexp, path) _, stderr, rc, _ := client.Run(ctx, cmd) if rc != 0 { return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil @@ -442,17 +439,17 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client *SSHClient, args // state == present if regexp != "" { // Replace line matching regexp - escapedLine := strings.ReplaceAll(line, "/", "\\/") - cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path) + escapedLine := replaceAll(line, "/", "\\/") + cmd := sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path) _, _, rc, _ := client.Run(ctx, cmd) if rc != 0 { // Line not found, append - cmd = fmt.Sprintf("echo %q >> %q", line, path) + cmd = sprintf("echo %q >> %q", line, path) _, _, _, _ = client.Run(ctx, cmd) } } else if line != "" { // Ensure line is present - cmd := fmt.Sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path) + cmd := sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path) _, _, _, _ = client.Run(ctx, cmd) } } @@ -512,7 +509,7 @@ func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[ } // Create dest directory - if err := coreio.Local.EnsureDir(filepath.Dir(dest)); err != nil { + if err := coreio.Local.EnsureDir(pathDir(dest)); err != nil { return nil, err } @@ -520,7 +517,7 @@ func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[ return nil, err } - return &TaskResult{Changed: true, Msg: fmt.Sprintf("fetched %s to %s", src, dest)}, nil + return &TaskResult{Changed: true, Msg: sprintf("fetched %s to %s", src, dest)}, nil } func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { @@ -531,7 +528,7 @@ func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map } // Use curl or wget - cmd := fmt.Sprintf("curl -fsSL -o %q %q || wget -q -O %q %q", dest, url, dest, url) + cmd := sprintf("curl -fsSL -o %q %q || wget -q -O %q %q", dest, url, dest, url) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil @@ -539,7 +536,7 @@ func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map // Set mode if specified (best-effort) if mode := getStringArg(args, "mode", ""); mode != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, dest)) + _, _, _, _ = client.Run(ctx, sprintf("chmod %s %q", mode, dest)) } return &TaskResult{Changed: true}, nil @@ -561,12 +558,12 @@ func (e *Executor) moduleApt(ctx context.Context, client *SSHClient, args map[st switch state { case "present", "installed": if name != "" { - cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name) + cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name) } case "absent", "removed": - cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name) + cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name) case "latest": - cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name) + cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name) } if cmd == "" { @@ -588,7 +585,7 @@ func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map if state == "absent" { if keyring != "" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", keyring)) + _, _, _, _ = client.Run(ctx, sprintf("rm -f %q", keyring)) } return &TaskResult{Changed: true}, nil } @@ -599,9 +596,9 @@ func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map var cmd string if keyring != "" { - cmd = fmt.Sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring) + cmd = sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring) } else { - cmd = fmt.Sprintf("curl -fsSL %q | apt-key add -", url) + cmd = sprintf("curl -fsSL %q | apt-key add -", url) } stdout, stderr, rc, err := client.Run(ctx, cmd) @@ -623,19 +620,19 @@ func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, a if filename == "" { // Generate filename from repo - filename = strings.ReplaceAll(repo, " ", "-") - filename = strings.ReplaceAll(filename, "/", "-") - filename = strings.ReplaceAll(filename, ":", "") + filename = replaceAll(repo, " ", "-") + filename = replaceAll(filename, "/", "-") + filename = replaceAll(filename, ":", "") } - path := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", filename) + path := sprintf("/etc/apt/sources.list.d/%s.list", filename) if state == "absent" { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", path)) + _, _, _, _ = client.Run(ctx, sprintf("rm -f %q", path)) return &TaskResult{Changed: true}, nil } - cmd := fmt.Sprintf("echo %q > %q", repo, path) + cmd := sprintf("echo %q > %q", repo, path) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil @@ -652,9 +649,9 @@ func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, a func (e *Executor) modulePackage(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) { // Detect package manager and delegate stdout, _, _, _ := client.Run(ctx, "which apt-get yum dnf 2>/dev/null | head -1") - stdout = strings.TrimSpace(stdout) + stdout = corexTrimSpace(stdout) - if strings.Contains(stdout, "apt") { + if contains(stdout, "apt") { return e.moduleApt(ctx, client, args) } @@ -670,11 +667,11 @@ func (e *Executor) modulePip(ctx context.Context, client *SSHClient, args map[st var cmd string switch state { case "present", "installed": - cmd = fmt.Sprintf("%s install %s", executable, name) + cmd = sprintf("%s install %s", executable, name) case "absent", "removed": - cmd = fmt.Sprintf("%s uninstall -y %s", executable, name) + cmd = sprintf("%s uninstall -y %s", executable, name) case "latest": - cmd = fmt.Sprintf("%s install --upgrade %s", executable, name) + cmd = sprintf("%s install --upgrade %s", executable, name) } stdout, stderr, rc, err := client.Run(ctx, cmd) @@ -701,21 +698,21 @@ func (e *Executor) moduleService(ctx context.Context, client *SSHClient, args ma if state != "" { switch state { case "started": - cmds = append(cmds, fmt.Sprintf("systemctl start %s", name)) + cmds = append(cmds, sprintf("systemctl start %s", name)) case "stopped": - cmds = append(cmds, fmt.Sprintf("systemctl stop %s", name)) + cmds = append(cmds, sprintf("systemctl stop %s", name)) case "restarted": - cmds = append(cmds, fmt.Sprintf("systemctl restart %s", name)) + cmds = append(cmds, sprintf("systemctl restart %s", name)) case "reloaded": - cmds = append(cmds, fmt.Sprintf("systemctl reload %s", name)) + cmds = append(cmds, sprintf("systemctl reload %s", name)) } } if enabled != nil { if getBoolArg(args, "enabled", false) { - cmds = append(cmds, fmt.Sprintf("systemctl enable %s", name)) + cmds = append(cmds, sprintf("systemctl enable %s", name)) } else { - cmds = append(cmds, fmt.Sprintf("systemctl disable %s", name)) + cmds = append(cmds, sprintf("systemctl disable %s", name)) } } @@ -749,7 +746,7 @@ func (e *Executor) moduleUser(ctx context.Context, client *SSHClient, args map[s } if state == "absent" { - cmd := fmt.Sprintf("userdel -r %s 2>/dev/null || true", name) + cmd := sprintf("userdel -r %s 2>/dev/null || true", name) _, _, _, _ = client.Run(ctx, cmd) return &TaskResult{Changed: true}, nil } @@ -780,12 +777,12 @@ func (e *Executor) moduleUser(ctx context.Context, client *SSHClient, args map[s } // Try usermod first, then useradd - optsStr := strings.Join(opts, " ") + optsStr := join(" ", opts) var cmd string if optsStr == "" { - cmd = fmt.Sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name) + cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name) } else { - cmd = fmt.Sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s", + cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s", name, optsStr, name, optsStr, name) } @@ -806,7 +803,7 @@ func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[ } if state == "absent" { - cmd := fmt.Sprintf("groupdel %s 2>/dev/null || true", name) + cmd := sprintf("groupdel %s 2>/dev/null || true", name) _, _, _, _ = client.Run(ctx, cmd) return &TaskResult{Changed: true}, nil } @@ -819,8 +816,8 @@ func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[ opts = append(opts, "-r") } - cmd := fmt.Sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s", - name, strings.Join(opts, " "), name) + cmd := sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s", + name, join(" ", opts), name) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { @@ -847,7 +844,7 @@ func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[st // Headers if headers, ok := args["headers"].(map[string]any); ok { for k, v := range headers { - curlOpts = append(curlOpts, "-H", fmt.Sprintf("%s: %v", k, v)) + curlOpts = append(curlOpts, "-H", sprintf("%s: %v", k, v)) } } @@ -859,14 +856,14 @@ func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[st // Status code curlOpts = append(curlOpts, "-w", "\\n%{http_code}") - cmd := fmt.Sprintf("curl %s %q", strings.Join(curlOpts, " "), url) + cmd := sprintf("curl %s %q", join(" ", curlOpts), url) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil { return &TaskResult{Failed: true, Msg: err.Error()}, nil } // Parse status code from last line - lines := strings.Split(strings.TrimSpace(stdout), "\n") + lines := split(corexTrimSpace(stdout), "\n") statusCode := 0 if len(lines) > 0 { statusCode, _ = strconv.Atoi(lines[len(lines)-1]) @@ -895,7 +892,7 @@ func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[st func (e *Executor) moduleDebug(args map[string]any) (*TaskResult, error) { msg := getStringArg(args, "msg", "") if v, ok := args["var"]; ok { - msg = fmt.Sprintf("%v = %v", v, e.vars[fmt.Sprintf("%v", v)]) + msg = sprintf("%v = %v", v, e.vars[sprintf("%v", v)]) } return &TaskResult{ @@ -921,7 +918,7 @@ func (e *Executor) moduleAssert(args map[string]any, host string) (*TaskResult, conditions := normalizeConditions(that) for _, cond := range conditions { if !e.evalCondition(cond, host) { - msg := getStringArg(args, "fail_msg", fmt.Sprintf("Assertion failed: %s", cond)) + msg := getStringArg(args, "fail_msg", sprintf("Assertion failed: %s", cond)) return &TaskResult{Failed: true, Msg: msg}, nil } } @@ -999,7 +996,7 @@ func (e *Executor) moduleWaitFor(ctx context.Context, client *SSHClient, args ma } if port > 0 && state == "started" { - cmd := fmt.Sprintf("timeout %d bash -c 'until nc -z %s %d; do sleep 1; done'", + cmd := sprintf("timeout %d bash -c 'until nc -z %s %d; do sleep 1; done'", timeout, host, port) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { @@ -1025,9 +1022,9 @@ func (e *Executor) moduleGit(ctx context.Context, client *SSHClient, args map[st var cmd string if exists { // Fetch and checkout (force to ensure clean state) - cmd = fmt.Sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version) + cmd = sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version) } else { - cmd = fmt.Sprintf("git clone %q %q && cd %q && git checkout %q", + cmd = sprintf("git clone %q %q && cd %q && git checkout %q", repo, dest, dest, version) } @@ -1049,7 +1046,7 @@ func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args } // Create dest directory (best-effort) - _, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q", dest)) + _, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q", dest)) var cmd string if !remote { @@ -1058,28 +1055,28 @@ func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args if err != nil { return nil, coreerr.E("Executor.moduleUnarchive", "read src", err) } - tmpPath := "/tmp/ansible_unarchive_" + filepath.Base(src) - err = client.Upload(ctx, strings.NewReader(data), tmpPath, 0644) + tmpPath := "/tmp/ansible_unarchive_" + pathBase(src) + err = client.Upload(ctx, newReader(data), tmpPath, 0644) if err != nil { return nil, err } src = tmpPath - defer func() { _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", tmpPath)) }() + defer func() { _, _, _, _ = client.Run(ctx, sprintf("rm -f %q", tmpPath)) }() } // Detect archive type and extract - if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") { - cmd = fmt.Sprintf("tar -xzf %q -C %q", src, dest) - } else if strings.HasSuffix(src, ".tar.xz") { - cmd = fmt.Sprintf("tar -xJf %q -C %q", src, dest) - } else if strings.HasSuffix(src, ".tar.bz2") { - cmd = fmt.Sprintf("tar -xjf %q -C %q", src, dest) - } else if strings.HasSuffix(src, ".tar") { - cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) - } else if strings.HasSuffix(src, ".zip") { - cmd = fmt.Sprintf("unzip -o %q -d %q", src, dest) + if hasSuffix(src, ".tar.gz") || hasSuffix(src, ".tgz") { + cmd = sprintf("tar -xzf %q -C %q", src, dest) + } else if hasSuffix(src, ".tar.xz") { + cmd = sprintf("tar -xJf %q -C %q", src, dest) + } else if hasSuffix(src, ".tar.bz2") { + cmd = sprintf("tar -xjf %q -C %q", src, dest) + } else if hasSuffix(src, ".tar") { + cmd = sprintf("tar -xf %q -C %q", src, dest) + } else if hasSuffix(src, ".zip") { + cmd = sprintf("unzip -o %q -d %q", src, dest) } else { - cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) // Guess tar + cmd = sprintf("tar -xf %q -C %q", src, dest) // Guess tar } stdout, stderr, rc, err := client.Run(ctx, cmd) @@ -1097,7 +1094,7 @@ func getStringArg(args map[string]any, key, def string) string { if s, ok := v.(string); ok { return s } - return fmt.Sprintf("%v", v) + return sprintf("%v", v) } return def } @@ -1108,8 +1105,8 @@ func getBoolArg(args map[string]any, key string, def bool) bool { case bool: return b case string: - lower := strings.ToLower(b) - return lower == "true" || lower == "yes" || lower == "1" + lowered := lower(b) + return lowered == "true" || lowered == "yes" || lowered == "1" } } return def @@ -1124,14 +1121,14 @@ func (e *Executor) moduleHostname(ctx context.Context, client *SSHClient, args m } // Set hostname - cmd := fmt.Sprintf("hostnamectl set-hostname %q || hostname %q", name, name) + cmd := sprintf("hostnamectl set-hostname %q || hostname %q", name, name) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil } // Update /etc/hosts if needed (best-effort) - _, _, _, _ = client.Run(ctx, fmt.Sprintf("sed -i 's/127.0.1.1.*/127.0.1.1\t%s/' /etc/hosts", name)) + _, _, _, _ = client.Run(ctx, sprintf("sed -i 's/127.0.1.1.*/127.0.1.1\t%s/' /etc/hosts", name)) return &TaskResult{Changed: true}, nil } @@ -1147,13 +1144,13 @@ func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map if state == "absent" { // Remove from sysctl.conf - cmd := fmt.Sprintf("sed -i '/%s/d' /etc/sysctl.conf", name) + cmd := sprintf("sed -i '/%s/d' /etc/sysctl.conf", name) _, _, _, _ = client.Run(ctx, cmd) return &TaskResult{Changed: true}, nil } // Set value - cmd := fmt.Sprintf("sysctl -w %s=%s", name, value) + cmd := sprintf("sysctl -w %s=%s", name, value) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil @@ -1161,7 +1158,7 @@ func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map // Persist if requested (best-effort) if getBoolArg(args, "sysctl_set", true) { - cmd = fmt.Sprintf("grep -q '^%s' /etc/sysctl.conf && sed -i 's/^%s.*/%s=%s/' /etc/sysctl.conf || echo '%s=%s' >> /etc/sysctl.conf", + cmd = sprintf("grep -q '^%s' /etc/sysctl.conf && sed -i 's/^%s.*/%s=%s/' /etc/sysctl.conf || echo '%s=%s' >> /etc/sysctl.conf", name, name, name, value, name, value) _, _, _, _ = client.Run(ctx, cmd) } @@ -1184,7 +1181,7 @@ func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[s if state == "absent" { if name != "" { // Remove by name (comment marker) - cmd := fmt.Sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -", + cmd := sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -", user, name, job, user) _, _, _, _ = client.Run(ctx, cmd) } @@ -1192,11 +1189,11 @@ func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[s } // Build cron entry - schedule := fmt.Sprintf("%s %s %s %s %s", minute, hour, day, month, weekday) - entry := fmt.Sprintf("%s %s # %s", schedule, job, name) + schedule := sprintf("%s %s %s %s %s", minute, hour, day, month, weekday) + entry := sprintf("%s %s # %s", schedule, job, name) // Add to crontab - cmd := fmt.Sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -", + cmd := sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -", user, name, entry, user) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { @@ -1220,14 +1217,14 @@ func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, arg state := getStringArg(args, "state", "present") create := getBoolArg(args, "create", false) - beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1) - endMarker := strings.Replace(marker, "{mark}", "END", 1) + beginMarker := replaceN(marker, "{mark}", "BEGIN", 1) + endMarker := replaceN(marker, "{mark}", "END", 1) if state == "absent" { // Remove block - cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q", - strings.ReplaceAll(beginMarker, "/", "\\/"), - strings.ReplaceAll(endMarker, "/", "\\/"), + cmd := sprintf("sed -i '/%s/,/%s/d' %q", + replaceAll(beginMarker, "/", "\\/"), + replaceAll(endMarker, "/", "\\/"), path) _, _, _, _ = client.Run(ctx, cmd) return &TaskResult{Changed: true}, nil @@ -1235,20 +1232,20 @@ func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, arg // Create file if needed (best-effort) if create { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("touch %q", path)) + _, _, _, _ = client.Run(ctx, sprintf("touch %q", path)) } // Remove existing block and add new one - escapedBlock := strings.ReplaceAll(block, "'", "'\\''") - cmd := fmt.Sprintf(` + escapedBlock := replaceAll(block, "'", "'\\''") + cmd := sprintf(` sed -i '/%s/,/%s/d' %q 2>/dev/null || true cat >> %q << 'BLOCK_EOF' %s %s %s BLOCK_EOF -`, strings.ReplaceAll(beginMarker, "/", "\\/"), - strings.ReplaceAll(endMarker, "/", "\\/"), +`, replaceAll(beginMarker, "/", "\\/"), + replaceAll(endMarker, "/", "\\/"), path, path, beginMarker, escapedBlock, endMarker) stdout, stderr, rc, err := client.RunScript(ctx, cmd) @@ -1294,10 +1291,10 @@ func (e *Executor) moduleReboot(ctx context.Context, client *SSHClient, args map msg := getStringArg(args, "msg", "Reboot initiated by Ansible") if preRebootDelay > 0 { - cmd := fmt.Sprintf("sleep %d && shutdown -r now '%s' &", preRebootDelay, msg) + cmd := sprintf("sleep %d && shutdown -r now '%s' &", preRebootDelay, msg) _, _, _, _ = client.Run(ctx, cmd) } else { - _, _, _, _ = client.Run(ctx, fmt.Sprintf("shutdown -r now '%s' &", msg)) + _, _, _, _ = client.Run(ctx, sprintf("shutdown -r now '%s' &", msg)) } return &TaskResult{Changed: true, Msg: "Reboot initiated"}, nil @@ -1336,13 +1333,13 @@ func (e *Executor) moduleUFW(ctx context.Context, client *SSHClient, args map[st if rule != "" && port != "" { switch rule { case "allow": - cmd = fmt.Sprintf("ufw allow %s/%s", port, proto) + cmd = sprintf("ufw allow %s/%s", port, proto) case "deny": - cmd = fmt.Sprintf("ufw deny %s/%s", port, proto) + cmd = sprintf("ufw deny %s/%s", port, proto) case "reject": - cmd = fmt.Sprintf("ufw reject %s/%s", port, proto) + cmd = sprintf("ufw reject %s/%s", port, proto) case "limit": - cmd = fmt.Sprintf("ufw limit %s/%s", port, proto) + cmd = sprintf("ufw limit %s/%s", port, proto) } stdout, stderr, rc, err := client.Run(ctx, cmd) @@ -1364,11 +1361,11 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a } // Get user's home directory - stdout, _, _, err := client.Run(ctx, fmt.Sprintf("getent passwd %s | cut -d: -f6", user)) + stdout, _, _, err := client.Run(ctx, sprintf("getent passwd %s | cut -d: -f6", user)) if err != nil { return nil, coreerr.E("Executor.moduleAuthorizedKey", "get home dir", err) } - home := strings.TrimSpace(stdout) + home := corexTrimSpace(stdout) if home == "" { home = "/root" if user != "root" { @@ -1376,22 +1373,22 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a } } - authKeysPath := filepath.Join(home, ".ssh", "authorized_keys") + authKeysPath := joinPath(home, ".ssh", "authorized_keys") if state == "absent" { // Remove key - escapedKey := strings.ReplaceAll(key, "/", "\\/") - cmd := fmt.Sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath) + escapedKey := replaceAll(key, "/", "\\/") + cmd := sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath) _, _, _, _ = client.Run(ctx, cmd) return &TaskResult{Changed: true}, nil } // Ensure .ssh directory exists (best-effort) - _, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q", - filepath.Dir(authKeysPath), filepath.Dir(authKeysPath), user, user, filepath.Dir(authKeysPath))) + _, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q", + pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath))) // Add key if not present - cmd := fmt.Sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q", + cmd := sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q", key[:40], authKeysPath, key, authKeysPath) stdout, stderr, rc, err := client.Run(ctx, cmd) if err != nil || rc != 0 { @@ -1399,7 +1396,7 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a } // Fix permissions (best-effort) - _, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod 600 %q && chown %s:%s %q", + _, _, _, _ = client.Run(ctx, sprintf("chmod 600 %q && chown %s:%s %q", authKeysPath, user, user, authKeysPath)) return &TaskResult{Changed: true}, nil @@ -1416,13 +1413,13 @@ func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, a var cmd string switch state { case "present": - cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc) + cmd = sprintf("cd %q && docker compose up -d", projectSrc) case "absent": - cmd = fmt.Sprintf("cd %q && docker compose down", projectSrc) + cmd = sprintf("cd %q && docker compose down", projectSrc) case "restarted": - cmd = fmt.Sprintf("cd %q && docker compose restart", projectSrc) + cmd = sprintf("cd %q && docker compose restart", projectSrc) default: - cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc) + cmd = sprintf("cd %q && docker compose up -d", projectSrc) } stdout, stderr, rc, err := client.Run(ctx, cmd) @@ -1431,7 +1428,7 @@ func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, a } // Heuristic for changed - changed := !strings.Contains(stdout, "Up to date") && !strings.Contains(stderr, "Up to date") + changed := !contains(stdout, "Up to date") && !contains(stderr, "Up to date") return &TaskResult{Changed: changed, Stdout: stdout}, nil } diff --git a/parser.go b/parser.go index c3e8277..f5378c7 100644 --- a/parser.go +++ b/parser.go @@ -1,12 +1,9 @@ package ansible import ( - "fmt" "iter" "maps" - "path/filepath" "slices" - "strings" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" @@ -42,7 +39,7 @@ func (p *Parser) ParsePlaybook(path string) ([]Play, error) { // Process each play for i := range plays { if err := p.processPlay(&plays[i]); err != nil { - return nil, coreerr.E("Parser.ParsePlaybook", fmt.Sprintf("process play %d", i), err) + return nil, coreerr.E("Parser.ParsePlaybook", sprintf("process play %d", i), err) } } @@ -93,7 +90,7 @@ func (p *Parser) ParseTasks(path string) ([]Task, error) { for i := range tasks { if err := p.extractModule(&tasks[i]); err != nil { - return nil, coreerr.E("Parser.ParseTasks", fmt.Sprintf("task %d", i), err) + return nil, coreerr.E("Parser.ParseTasks", sprintf("task %d", i), err) } } @@ -124,21 +121,21 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) { // Search paths for roles (in order of precedence) searchPaths := []string{ // Relative to playbook - filepath.Join(p.basePath, "roles", name, "tasks", tasksFrom), + joinPath(p.basePath, "roles", name, "tasks", tasksFrom), // Parent directory roles - filepath.Join(filepath.Dir(p.basePath), "roles", name, "tasks", tasksFrom), + joinPath(pathDir(p.basePath), "roles", name, "tasks", tasksFrom), // Sibling roles directory - filepath.Join(p.basePath, "..", "roles", name, "tasks", tasksFrom), + joinPath(p.basePath, "..", "roles", name, "tasks", tasksFrom), // playbooks/roles pattern - filepath.Join(p.basePath, "playbooks", "roles", name, "tasks", tasksFrom), + joinPath(p.basePath, "playbooks", "roles", name, "tasks", tasksFrom), // Common DevOps structure - filepath.Join(filepath.Dir(filepath.Dir(p.basePath)), "roles", name, "tasks", tasksFrom), + joinPath(pathDir(pathDir(p.basePath)), "roles", name, "tasks", tasksFrom), } var tasksPath string for _, sp := range searchPaths { // Clean the path to resolve .. segments - sp = filepath.Clean(sp) + sp = cleanPath(sp) if coreio.Local.Exists(sp) { tasksPath = sp break @@ -146,11 +143,11 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) { } if tasksPath == "" { - return nil, coreerr.E("Parser.ParseRole", fmt.Sprintf("role %s not found in search paths: %v", name, searchPaths), nil) + return nil, coreerr.E("Parser.ParseRole", sprintf("role %s not found in search paths: %v", name, searchPaths), nil) } // Load role defaults - defaultsPath := filepath.Join(filepath.Dir(filepath.Dir(tasksPath)), "defaults", "main.yml") + defaultsPath := joinPath(pathDir(pathDir(tasksPath)), "defaults", "main.yml") if data, err := coreio.Local.Read(defaultsPath); err == nil { var defaults map[string]any if yaml.Unmarshal([]byte(data), &defaults) == nil { @@ -163,7 +160,7 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) { } // Load role vars - varsPath := filepath.Join(filepath.Dir(filepath.Dir(tasksPath)), "vars", "main.yml") + varsPath := joinPath(pathDir(pathDir(tasksPath)), "vars", "main.yml") if data, err := coreio.Local.Read(varsPath); err == nil { var roleVars map[string]any if yaml.Unmarshal([]byte(data), &roleVars) == nil { @@ -185,25 +182,25 @@ func (p *Parser) processPlay(play *Play) error { for i := range play.PreTasks { if err := p.extractModule(&play.PreTasks[i]); err != nil { - return coreerr.E("Parser.processPlay", fmt.Sprintf("pre_task %d", i), err) + return coreerr.E("Parser.processPlay", sprintf("pre_task %d", i), err) } } for i := range play.Tasks { if err := p.extractModule(&play.Tasks[i]); err != nil { - return coreerr.E("Parser.processPlay", fmt.Sprintf("task %d", i), err) + return coreerr.E("Parser.processPlay", sprintf("task %d", i), err) } } for i := range play.PostTasks { if err := p.extractModule(&play.PostTasks[i]); err != nil { - return coreerr.E("Parser.processPlay", fmt.Sprintf("post_task %d", i), err) + return coreerr.E("Parser.processPlay", sprintf("post_task %d", i), err) } } for i := range play.Handlers { if err := p.extractModule(&play.Handlers[i]); err != nil { - return coreerr.E("Parser.processPlay", fmt.Sprintf("handler %d", i), err) + return coreerr.E("Parser.processPlay", sprintf("handler %d", i), err) } } @@ -308,20 +305,20 @@ func isModule(key string) bool { return true } // Also check without ansible.builtin. prefix - if strings.HasPrefix(m, "ansible.builtin.") { - if key == strings.TrimPrefix(m, "ansible.builtin.") { + if corexHasPrefix(m, "ansible.builtin.") { + if key == corexTrimPrefix(m, "ansible.builtin.") { return true } } } // Accept any key with dots (likely a module) - return strings.Contains(key, ".") + return contains(key, ".") } // NormalizeModule normalizes a module name to its canonical form. func NormalizeModule(name string) string { // Add ansible.builtin. prefix if missing - if !strings.Contains(name, ".") { + if !contains(name, ".") { return "ansible.builtin." + name } return name diff --git a/ssh.go b/ssh.go index ae5d9a6..b0afb6e 100644 --- a/ssh.go +++ b/ssh.go @@ -3,12 +3,9 @@ package ansible import ( "bytes" "context" - "fmt" "io" "net" "os" - "path/filepath" - "strings" "sync" "time" @@ -87,9 +84,8 @@ func (c *SSHClient) Connect(ctx context.Context) error { // Try key-based auth first if c.keyFile != "" { keyPath := c.keyFile - if strings.HasPrefix(keyPath, "~") { - home, _ := os.UserHomeDir() - keyPath = filepath.Join(home, keyPath[1:]) + if corexHasPrefix(keyPath, "~") { + keyPath = joinPath(env("DIR_HOME"), keyPath[1:]) } if key, err := coreio.Local.Read(keyPath); err == nil { @@ -101,10 +97,10 @@ func (c *SSHClient) Connect(ctx context.Context) error { // Try default SSH keys if len(authMethods) == 0 { - home, _ := os.UserHomeDir() + home := env("DIR_HOME") defaultKeys := []string{ - filepath.Join(home, ".ssh", "id_ed25519"), - filepath.Join(home, ".ssh", "id_rsa"), + joinPath(home, ".ssh", "id_ed25519"), + joinPath(home, ".ssh", "id_rsa"), } for _, keyPath := range defaultKeys { if key, err := coreio.Local.Read(keyPath); err == nil { @@ -135,15 +131,15 @@ func (c *SSHClient) Connect(ctx context.Context) error { // Host key verification var hostKeyCallback ssh.HostKeyCallback - home, err := os.UserHomeDir() - if err != nil { - return coreerr.E("ssh.Connect", "failed to get user home dir", err) + home := env("DIR_HOME") + if home == "" { + return coreerr.E("ssh.Connect", "failed to get user home dir", nil) } - knownHostsPath := filepath.Join(home, ".ssh", "known_hosts") + knownHostsPath := joinPath(home, ".ssh", "known_hosts") // Ensure known_hosts file exists if !coreio.Local.Exists(knownHostsPath) { - if err := coreio.Local.EnsureDir(filepath.Dir(knownHostsPath)); err != nil { + if err := coreio.Local.EnsureDir(pathDir(knownHostsPath)); err != nil { return coreerr.E("ssh.Connect", "failed to create .ssh dir", err) } if err := coreio.Local.Write(knownHostsPath, ""); err != nil { @@ -164,19 +160,19 @@ func (c *SSHClient) Connect(ctx context.Context) error { Timeout: c.timeout, } - addr := fmt.Sprintf("%s:%d", c.host, c.port) + addr := sprintf("%s:%d", c.host, c.port) // Connect with context timeout var d net.Dialer conn, err := d.DialContext(ctx, "tcp", addr) if err != nil { - return coreerr.E("ssh.Connect", fmt.Sprintf("dial %s", addr), err) + return coreerr.E("ssh.Connect", sprintf("dial %s", addr), err) } sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config) if err != nil { // conn is closed by NewClientConn on error - return coreerr.E("ssh.Connect", fmt.Sprintf("ssh connect %s", addr), err) + return coreerr.E("ssh.Connect", sprintf("ssh connect %s", addr), err) } c.client = ssh.NewClient(sshConn, chans, reqs) @@ -219,33 +215,33 @@ func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string, becomeUser = "root" } // Escape single quotes in the command - escapedCmd := strings.ReplaceAll(cmd, "'", "'\\''") + escapedCmd := replaceAll(cmd, "'", "'\\''") if c.becomePass != "" { // Use sudo with password via stdin (-S flag) // We launch a goroutine to write the password to stdin - cmd = fmt.Sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd) + cmd = sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd) stdin, err := session.StdinPipe() if err != nil { return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err) } go func() { defer func() { _ = stdin.Close() }() - _, _ = io.WriteString(stdin, c.becomePass+"\n") + writeString(stdin, c.becomePass+"\n") }() } else if c.password != "" { // Try using connection password for sudo - cmd = fmt.Sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd) + cmd = sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd) stdin, err := session.StdinPipe() if err != nil { return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err) } go func() { defer func() { _ = stdin.Close() }() - _, _ = io.WriteString(stdin, c.password+"\n") + writeString(stdin, c.password+"\n") }() } else { // Try passwordless sudo - cmd = fmt.Sprintf("sudo -n -u %s bash -c '%s'", becomeUser, escapedCmd) + cmd = sprintf("sudo -n -u %s bash -c '%s'", becomeUser, escapedCmd) } } @@ -275,7 +271,7 @@ func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string, // RunScript runs a script on the remote host. func (c *SSHClient) RunScript(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error) { // Escape the script for heredoc - cmd := fmt.Sprintf("bash <<'ANSIBLE_SCRIPT_EOF'\n%s\nANSIBLE_SCRIPT_EOF", script) + cmd := sprintf("bash <<'ANSIBLE_SCRIPT_EOF'\n%s\nANSIBLE_SCRIPT_EOF", script) return c.Run(ctx, cmd) } @@ -286,23 +282,23 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string, } // Read content - content, err := io.ReadAll(local) + content, err := readAllString(local) if err != nil { return coreerr.E("ssh.Upload", "read content", err) } // Create parent directory - dir := filepath.Dir(remote) - dirCmd := fmt.Sprintf("mkdir -p %q", dir) + dir := pathDir(remote) + dirCmd := sprintf("mkdir -p %q", dir) if c.become { - dirCmd = fmt.Sprintf("sudo mkdir -p %q", dir) + dirCmd = sprintf("sudo mkdir -p %q", dir) } if _, _, _, err := c.Run(ctx, dirCmd); err != nil { return coreerr.E("ssh.Upload", "create parent dir", err) } // Use cat to write the file (simpler than SCP) - writeCmd := fmt.Sprintf("cat > %q && chmod %o %q", remote, mode, remote) + writeCmd := sprintf("cat > %q && chmod %o %q", remote, mode, remote) // If become is needed, we construct a command that reads password then content from stdin // But we need to be careful with handling stdin for sudo + cat. @@ -335,11 +331,11 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string, if pass != "" { // Use sudo -S with password from stdin - writeCmd = fmt.Sprintf("sudo -S -u %s bash -c 'cat > %q && chmod %o %q'", + writeCmd = sprintf("sudo -S -u %s bash -c 'cat > %q && chmod %o %q'", becomeUser, remote, mode, remote) } else { // Use passwordless sudo (sudo -n) to avoid consuming file content as password - writeCmd = fmt.Sprintf("sudo -n -u %s bash -c 'cat > %q && chmod %o %q'", + writeCmd = sprintf("sudo -n -u %s bash -c 'cat > %q && chmod %o %q'", becomeUser, remote, mode, remote) } @@ -350,9 +346,9 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string, go func() { defer func() { _ = stdin.Close() }() if pass != "" { - _, _ = io.WriteString(stdin, pass+"\n") + writeString(stdin, pass+"\n") } - _, _ = stdin.Write(content) + _, _ = stdin.Write([]byte(content)) }() } else { // Normal write @@ -362,12 +358,12 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string, go func() { defer func() { _ = stdin.Close() }() - _, _ = stdin.Write(content) + _, _ = stdin.Write([]byte(content)) }() } if err := session2.Wait(); err != nil { - return coreerr.E("ssh.Upload", fmt.Sprintf("write failed (stderr: %s)", stderrBuf.String()), err) + return coreerr.E("ssh.Upload", sprintf("write failed (stderr: %s)", stderrBuf.String()), err) } return nil @@ -379,14 +375,14 @@ func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error) return nil, err } - cmd := fmt.Sprintf("cat %q", remote) + cmd := sprintf("cat %q", remote) stdout, stderr, exitCode, err := c.Run(ctx, cmd) if err != nil { return nil, err } if exitCode != 0 { - return nil, coreerr.E("ssh.Download", fmt.Sprintf("cat failed: %s", stderr), nil) + return nil, coreerr.E("ssh.Download", sprintf("cat failed: %s", stderr), nil) } return []byte(stdout), nil @@ -394,7 +390,7 @@ func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error) // FileExists checks if a file exists on the remote host. func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) { - cmd := fmt.Sprintf("test -e %q && echo yes || echo no", path) + cmd := sprintf("test -e %q && echo yes || echo no", path) stdout, _, exitCode, err := c.Run(ctx, cmd) if err != nil { return false, err @@ -403,13 +399,13 @@ func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) { // test command failed but didn't error - file doesn't exist return false, nil } - return strings.TrimSpace(stdout) == "yes", nil + return corexTrimSpace(stdout) == "yes", nil } // Stat returns file info from the remote host. func (c *SSHClient) Stat(ctx context.Context, path string) (map[string]any, error) { // Simple approach - get basic file info - cmd := fmt.Sprintf(` + cmd := sprintf(` if [ -e %q ]; then if [ -d %q ]; then echo "exists=true isdir=true" @@ -427,9 +423,9 @@ fi } result := make(map[string]any) - parts := strings.Fields(strings.TrimSpace(stdout)) + parts := fields(corexTrimSpace(stdout)) for _, part := range parts { - kv := strings.SplitN(part, "=", 2) + kv := splitN(part, "=", 2) if len(kv) == 2 { result[kv[0]] = kv[1] == "true" }