Add replace module support
This commit is contained in:
parent
cffc35a973
commit
5609471945
5 changed files with 133 additions and 4 deletions
|
|
@ -32,7 +32,7 @@ Inventory YAML ──► Parser ──► Inventory Callbacks (OnPlayStar
|
|||
- **`types.go`** — Core structs (`Playbook`, `Play`, `Task`, `TaskResult`, `Inventory`, `Host`, `Facts`) and `KnownModules` registry (96 entries: both FQCN `ansible.builtin.*` and short forms, plus compatibility aliases).
|
||||
- **`parser.go`** — YAML parsing for playbooks, inventories, tasks, and roles. Custom `Task.UnmarshalYAML` scans map keys against `KnownModules` to extract the module name and args (since Ansible embeds the module name as a YAML key, not a fixed field). Free-form syntax (`shell: echo hello`) is stored as `Args["_raw_params"]`. Iterator variants (`ParsePlaybookIter`, `ParseTasksIter`, etc.) return `iter.Seq` values.
|
||||
- **`executor.go`** — Orchestration engine: host resolution from inventory, play execution order (gather facts → pre_tasks → roles → tasks → post_tasks → notified handlers), `when:` condition evaluation, `{{ }}` Jinja2-style templating with filter support, loop execution, block/rescue/always, handler notification.
|
||||
- **`modules.go`** — 49 module handler implementations dispatched via a `switch` on the normalised module name. Each handler extracts args via `getStringArg`/`getBoolArg`, constructs shell commands, runs them via SSH, and returns a `TaskResult`.
|
||||
- **`modules.go`** — 50 module handler implementations dispatched via a `switch` on the normalised module name. Each handler extracts args via `getStringArg`/`getBoolArg`, constructs shell commands, runs them via SSH, and returns a `TaskResult`.
|
||||
- **`ssh.go`** — SSH client with lazy connection, auth chain (key file → default keys → password), `known_hosts` verification, become/sudo wrapping, file transfer via `cat >` piped through stdin.
|
||||
- **`cmd/ansible/`** — CLI command registration via `core/cli`. Provides `ansible <playbook>` and `ansible test <host>` subcommands with flags for inventory, limit, tags, extra-vars, verbosity, and check mode.
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ go-ansible/
|
|||
types.go Core data types: Playbook, Play, Task, Inventory, Host, Facts
|
||||
parser.go YAML parser for playbooks, inventories, tasks, roles
|
||||
executor.go Execution engine: module dispatch, templating, conditions, loops
|
||||
modules.go 49 module implementations (shell, apt, docker-compose, setup, etc.)
|
||||
modules.go 50 module implementations (shell, apt, replace, docker-compose, setup, etc.)
|
||||
ssh.go SSH client with key/password auth, become/sudo, file transfer
|
||||
types_test.go Tests for data types and YAML unmarshalling
|
||||
parser_test.go Tests for the YAML parser
|
||||
|
|
@ -126,12 +126,12 @@ go-ansible/
|
|||
|
||||
## Supported Modules
|
||||
|
||||
49 module handlers are implemented, covering the most commonly used Ansible modules:
|
||||
50 module handlers are implemented, covering the most commonly used Ansible modules:
|
||||
|
||||
| Category | Modules |
|
||||
|----------|---------|
|
||||
| **Command execution** | `shell`, `command`, `raw`, `script` |
|
||||
| **File operations** | `copy`, `template`, `file`, `lineinfile`, `blockinfile`, `stat`, `slurp`, `fetch`, `get_url` |
|
||||
| **File operations** | `copy`, `template`, `file`, `lineinfile`, `replace`, `blockinfile`, `stat`, `slurp`, `fetch`, `get_url` |
|
||||
| **Package management** | `apt`, `apt_key`, `apt_repository`, `package`, `pip`, `rpm` |
|
||||
| **Service management** | `service`, `systemd` |
|
||||
| **User and group** | `user`, `group` |
|
||||
|
|
|
|||
64
modules.go
64
modules.go
|
|
@ -92,6 +92,8 @@ func (e *Executor) executeModule(ctx context.Context, host string, client sshExe
|
|||
return e.moduleFile(ctx, client, args)
|
||||
case "ansible.builtin.lineinfile":
|
||||
return e.moduleLineinfile(ctx, client, args)
|
||||
case "ansible.builtin.replace":
|
||||
return e.moduleReplace(ctx, client, args)
|
||||
case "ansible.builtin.stat":
|
||||
return e.moduleStat(ctx, client, args)
|
||||
case "ansible.builtin.slurp":
|
||||
|
|
@ -901,6 +903,68 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func (e *Executor) moduleReplace(ctx context.Context, client sshExecutorClient, args map[string]any) (*TaskResult, error) {
|
||||
path := getStringArg(args, "path", "")
|
||||
if path == "" {
|
||||
path = getStringArg(args, "dest", "")
|
||||
}
|
||||
if path == "" {
|
||||
return nil, coreerr.E("Executor.moduleReplace", "path required", nil)
|
||||
}
|
||||
|
||||
pattern := getStringArg(args, "regexp", "")
|
||||
if pattern == "" {
|
||||
return nil, coreerr.E("Executor.moduleReplace", "regexp required", nil)
|
||||
}
|
||||
|
||||
replacement := getStringArg(args, "replace", "")
|
||||
backup := getBoolArg(args, "backup", false)
|
||||
|
||||
before, ok := remoteFileText(ctx, client, path)
|
||||
if !ok {
|
||||
return &TaskResult{Failed: true, Msg: sprintf("file not found: %s", path)}, nil
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Executor.moduleReplace", "compile regexp", err)
|
||||
}
|
||||
|
||||
after := re.ReplaceAllString(before, replacement)
|
||||
if after == before {
|
||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", path)}, nil
|
||||
}
|
||||
|
||||
result := &TaskResult{Changed: true}
|
||||
if backup {
|
||||
backupPath, hasBefore, err := backupRemoteFile(ctx, client, path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("Executor.moduleReplace", "backup remote file", err)
|
||||
}
|
||||
if hasBefore {
|
||||
result.Data = map[string]any{"backup_file": backupPath}
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Upload(ctx, bytes.NewReader([]byte(after)), path, 0644); err != nil {
|
||||
return nil, coreerr.E("Executor.moduleReplace", "upload replacement", err)
|
||||
}
|
||||
|
||||
if e.Diff {
|
||||
result.Data = ensureTaskResultData(result.Data)
|
||||
result.Data["diff"] = fileDiffData(path, before, after)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ensureTaskResultData(data map[string]any) map[string]any {
|
||||
if data != nil {
|
||||
return data
|
||||
}
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
func fileContainsExactLine(content, line string) bool {
|
||||
if content == "" || line == "" {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -789,6 +789,69 @@ func TestModulesFile_ModuleLineinfile_Good_DiffData(t *testing.T) {
|
|||
assert.Contains(t, diff["after"], "setting=new")
|
||||
}
|
||||
|
||||
// --- replace module ---
|
||||
|
||||
func TestModulesFile_ModuleReplace_Good_RegexpReplacementWithBackupAndDiff(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.Diff = true
|
||||
client := newDiffFileClient(map[string]string{
|
||||
"/etc/app.conf": "port=8080\nmode=prod\n",
|
||||
})
|
||||
|
||||
result, err := e.moduleReplace(context.Background(), client, map[string]any{
|
||||
"path": "/etc/app.conf",
|
||||
"regexp": `port=(\d+)`,
|
||||
"replace": "port=9090",
|
||||
"backup": true,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
require.NotNil(t, result.Data)
|
||||
assert.Contains(t, result.Data, "backup_file")
|
||||
assert.Contains(t, result.Data, "diff")
|
||||
|
||||
after, err := client.Download(context.Background(), "/etc/app.conf")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "port=9090\nmode=prod\n", string(after))
|
||||
|
||||
backupPath, _ := result.Data["backup_file"].(string)
|
||||
require.NotEmpty(t, backupPath)
|
||||
backup, err := client.Download(context.Background(), backupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "port=8080\nmode=prod\n", string(backup))
|
||||
}
|
||||
|
||||
func TestModulesFile_ModuleReplace_Good_NoOpWhenPatternMissing(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
client := newDiffFileClient(map[string]string{
|
||||
"/etc/app.conf": "port=8080\n",
|
||||
})
|
||||
|
||||
result, err := e.moduleReplace(context.Background(), client, map[string]any{
|
||||
"path": "/etc/app.conf",
|
||||
"regexp": `mode=.+`,
|
||||
"replace": "mode=prod",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result.Changed)
|
||||
assert.Contains(t, result.Msg, "already up to date")
|
||||
}
|
||||
|
||||
func TestModulesFile_ModuleReplace_Bad_MissingPath(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
client := newDiffFileClient(nil)
|
||||
|
||||
_, err := e.moduleReplace(context.Background(), client, map[string]any{
|
||||
"regexp": `mode=.+`,
|
||||
"replace": "mode=prod",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path required")
|
||||
}
|
||||
|
||||
// --- blockinfile module ---
|
||||
|
||||
func TestModulesFile_ModuleBlockinfile_Good_InsertBlock(t *testing.T) {
|
||||
|
|
|
|||
2
types.go
2
types.go
|
|
@ -396,6 +396,7 @@ var KnownModules = []string{
|
|||
"ansible.builtin.template",
|
||||
"ansible.builtin.file",
|
||||
"ansible.builtin.lineinfile",
|
||||
"ansible.builtin.replace",
|
||||
"ansible.builtin.blockinfile",
|
||||
"ansible.builtin.stat",
|
||||
"ansible.builtin.slurp",
|
||||
|
|
@ -449,6 +450,7 @@ var KnownModules = []string{
|
|||
"template",
|
||||
"file",
|
||||
"lineinfile",
|
||||
"replace",
|
||||
"blockinfile",
|
||||
"stat",
|
||||
"slurp",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue