feat(ansible): add lineinfile backup support
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
3eca6b15cb
commit
1e5bdc08dd
3 changed files with 105 additions and 2 deletions
|
|
@ -890,13 +890,34 @@ func moduleLineinfileWithClient(_ *Executor, client sshFileRunner, args map[stri
|
||||||
line := getStringArg(args, "line", "")
|
line := getStringArg(args, "line", "")
|
||||||
regexpArg := getStringArg(args, "regexp", "")
|
regexpArg := getStringArg(args, "regexp", "")
|
||||||
state := getStringArg(args, "state", "present")
|
state := getStringArg(args, "state", "present")
|
||||||
|
backup := getBoolArg(args, "backup", false)
|
||||||
backrefs := getBoolArg(args, "backrefs", false)
|
backrefs := getBoolArg(args, "backrefs", false)
|
||||||
insertBefore := getStringArg(args, "insertbefore", "")
|
insertBefore := getStringArg(args, "insertbefore", "")
|
||||||
insertAfter := getStringArg(args, "insertafter", "")
|
insertAfter := getStringArg(args, "insertafter", "")
|
||||||
firstMatch := getBoolArg(args, "firstmatch", false)
|
firstMatch := getBoolArg(args, "firstmatch", false)
|
||||||
|
var backupPath string
|
||||||
|
ensureBackup := func() error {
|
||||||
|
if !backup || backupPath != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
before, hasBefore := mockRemoteFileText(client, path)
|
||||||
|
if !hasBefore {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
backupPath = sprintf("%s.%s.bak", path, time.Now().UTC().Format("20060102T150405Z"))
|
||||||
|
if err := client.Upload(context.Background(), bytes.NewReader([]byte(before)), backupPath, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
if regexpArg != "" {
|
if regexpArg != "" {
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cmd := sprintf("sed -i '/%s/d' %q", regexpArg, path)
|
cmd := sprintf("sed -i '/%s/d' %q", regexpArg, path)
|
||||||
_, stderr, rc, _ := client.Run(context.Background(), cmd)
|
_, stderr, rc, _ := client.Run(context.Background(), cmd)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
|
|
@ -917,22 +938,34 @@ func moduleLineinfileWithClient(_ *Executor, client sshFileRunner, args map[stri
|
||||||
}
|
}
|
||||||
sedFlags = "-E -i"
|
sedFlags = "-E -i"
|
||||||
}
|
}
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cmd := sprintf("sed %s 's/%s/%s/' %q", sedFlags, regexpArg, escapedLine, path)
|
cmd := sprintf("sed %s 's/%s/%s/' %q", sedFlags, regexpArg, escapedLine, path)
|
||||||
_, _, rc, _ := client.Run(context.Background(), cmd)
|
_, _, rc, _ := client.Run(context.Background(), cmd)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
if backrefs {
|
if backrefs {
|
||||||
return &TaskResult{Changed: false}, nil
|
return &TaskResult{Changed: false}, nil
|
||||||
}
|
}
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if inserted, err := mockInsertLineRelativeToMatch(context.Background(), client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
if inserted, err := mockInsertLineRelativeToMatch(context.Background(), client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if inserted {
|
} else if inserted {
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
// Line not found, append.
|
// Line not found, append.
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cmd = sprintf("echo %q >> %q", line, path)
|
cmd = sprintf("echo %q >> %q", line, path)
|
||||||
_, _, _, _ = client.Run(context.Background(), cmd)
|
_, _, _, _ = client.Run(context.Background(), cmd)
|
||||||
}
|
}
|
||||||
} else if line != "" {
|
} else if line != "" {
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if inserted, err := mockInsertLineRelativeToMatch(context.Background(), client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
if inserted, err := mockInsertLineRelativeToMatch(context.Background(), client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if inserted {
|
} else if inserted {
|
||||||
|
|
@ -946,6 +979,9 @@ func moduleLineinfileWithClient(_ *Executor, client sshFileRunner, args map[stri
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &TaskResult{Changed: true}
|
result := &TaskResult{Changed: true}
|
||||||
|
if backupPath != "" {
|
||||||
|
result.Data = map[string]any{"backup_file": backupPath}
|
||||||
|
}
|
||||||
if after, ok := mockRemoteFileText(client, path); ok && before != after {
|
if after, ok := mockRemoteFileText(client, path); ok && before != after {
|
||||||
if result.Data == nil {
|
if result.Data == nil {
|
||||||
result.Data = make(map[string]any)
|
result.Data = make(map[string]any)
|
||||||
|
|
|
||||||
47
modules.go
47
modules.go
|
|
@ -831,12 +831,31 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
||||||
line := getStringArg(args, "line", "")
|
line := getStringArg(args, "line", "")
|
||||||
regexp := getStringArg(args, "regexp", "")
|
regexp := getStringArg(args, "regexp", "")
|
||||||
state := getStringArg(args, "state", "present")
|
state := getStringArg(args, "state", "present")
|
||||||
|
backup := getBoolArg(args, "backup", false)
|
||||||
backrefs := getBoolArg(args, "backrefs", false)
|
backrefs := getBoolArg(args, "backrefs", false)
|
||||||
create := getBoolArg(args, "create", false)
|
create := getBoolArg(args, "create", false)
|
||||||
insertBefore := getStringArg(args, "insertbefore", "")
|
insertBefore := getStringArg(args, "insertbefore", "")
|
||||||
insertAfter := getStringArg(args, "insertafter", "")
|
insertAfter := getStringArg(args, "insertafter", "")
|
||||||
firstMatch := getBoolArg(args, "firstmatch", false)
|
firstMatch := getBoolArg(args, "firstmatch", false)
|
||||||
|
|
||||||
|
var backupPath string
|
||||||
|
ensureBackup := func() error {
|
||||||
|
if !backup || backupPath != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasCopy bool
|
||||||
|
var err error
|
||||||
|
backupPath, hasCopy, err = backupRemoteFile(ctx, client, path)
|
||||||
|
if err != nil {
|
||||||
|
return coreerr.E("Executor.moduleLineinfile", "backup remote file", err)
|
||||||
|
}
|
||||||
|
if !hasCopy {
|
||||||
|
backupPath = ""
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if state != "absent" && line != "" && regexp == "" && insertBefore == "" && insertAfter == "" {
|
if state != "absent" && line != "" && regexp == "" && insertBefore == "" && insertAfter == "" {
|
||||||
if hasBefore && fileContainsExactLine(before, line) {
|
if hasBefore && fileContainsExactLine(before, line) {
|
||||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", path)}, nil
|
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", path)}, nil
|
||||||
|
|
@ -848,6 +867,9 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
||||||
if content, ok := remoteFileText(ctx, client, path); !ok || !regexpMatchString(regexp, content) {
|
if content, ok := remoteFileText(ctx, client, path); !ok || !regexpMatchString(regexp, content) {
|
||||||
return &TaskResult{Changed: false}, nil
|
return &TaskResult{Changed: false}, nil
|
||||||
}
|
}
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cmd := sprintf("sed -i '/%s/d' %q", regexp, path)
|
cmd := sprintf("sed -i '/%s/d' %q", regexp, path)
|
||||||
_, stderr, rc, _ := client.Run(ctx, cmd)
|
_, stderr, rc, _ := client.Run(ctx, cmd)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
|
|
@ -863,7 +885,6 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
||||||
|
|
||||||
// state == present
|
// state == present
|
||||||
if regexp != "" {
|
if regexp != "" {
|
||||||
// Replace line matching regexp.
|
|
||||||
escapedLine := replaceAll(line, "/", "\\/")
|
escapedLine := replaceAll(line, "/", "\\/")
|
||||||
sedFlags := "-i"
|
sedFlags := "-i"
|
||||||
if backrefs {
|
if backrefs {
|
||||||
|
|
@ -876,22 +897,38 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
||||||
}
|
}
|
||||||
sedFlags = "-E -i"
|
sedFlags = "-E -i"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
cmd := sprintf("sed %s 's/%s/%s/' %q", sedFlags, regexp, escapedLine, path)
|
cmd := sprintf("sed %s 's/%s/%s/' %q", sedFlags, regexp, escapedLine, path)
|
||||||
_, _, rc, _ := client.Run(ctx, cmd)
|
_, _, rc, _ := client.Run(ctx, cmd)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
if backrefs {
|
if backrefs {
|
||||||
return &TaskResult{Changed: false}, nil
|
return &TaskResult{Changed: false}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if inserted, err := insertLineRelativeToMatch(ctx, client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
if inserted, err := insertLineRelativeToMatch(ctx, client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if inserted {
|
} else if inserted {
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Line not found, append.
|
// Line not found, append.
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cmd = sprintf("echo %q >> %q", line, path)
|
cmd = sprintf("echo %q >> %q", line, path)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
}
|
}
|
||||||
} else if line != "" {
|
} else if line != "" {
|
||||||
|
if err := ensureBackup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if inserted, err := insertLineRelativeToMatch(ctx, client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
if inserted, err := insertLineRelativeToMatch(ctx, client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if inserted {
|
} else if inserted {
|
||||||
|
|
@ -905,9 +942,15 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &TaskResult{Changed: true}
|
result := &TaskResult{Changed: true}
|
||||||
|
if backupPath != "" {
|
||||||
|
result.Data = map[string]any{"backup_file": backupPath}
|
||||||
|
}
|
||||||
if e.Diff {
|
if e.Diff {
|
||||||
if after, ok := remoteFileText(ctx, client, path); ok && before != after {
|
if after, ok := remoteFileText(ctx, client, path); ok && before != after {
|
||||||
result.Data = map[string]any{"diff": fileDiffData(path, before, after)}
|
if result.Data == nil {
|
||||||
|
result.Data = make(map[string]any)
|
||||||
|
}
|
||||||
|
result.Data["diff"] = fileDiffData(path, before, after)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -791,6 +791,30 @@ func TestModulesFile_ModuleLineinfile_Good_ExactLineAlreadyPresentIsNoOp(t *test
|
||||||
assert.Equal(t, 0, mock.commandCount())
|
assert.Equal(t, 0, mock.commandCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestModulesFile_ModuleLineinfile_Good_BackupExistingFile(t *testing.T) {
|
||||||
|
e, mock := newTestExecutorWithMock("host1")
|
||||||
|
path := "/etc/example.conf"
|
||||||
|
mock.addFile(path, []byte("setting=old\n"))
|
||||||
|
|
||||||
|
result, err := e.moduleLineinfile(context.Background(), mock, map[string]any{
|
||||||
|
"path": path,
|
||||||
|
"line": "setting=new",
|
||||||
|
"backup": true,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, result.Changed)
|
||||||
|
require.NotNil(t, result.Data)
|
||||||
|
|
||||||
|
backupPath, ok := result.Data["backup_file"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Contains(t, backupPath, "/etc/example.conf.")
|
||||||
|
|
||||||
|
backupContent, err := mock.Download(context.Background(), backupPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("setting=old\n"), backupContent)
|
||||||
|
}
|
||||||
|
|
||||||
func TestModulesFile_ModuleLineinfile_Good_DiffData(t *testing.T) {
|
func TestModulesFile_ModuleLineinfile_Good_DiffData(t *testing.T) {
|
||||||
e := NewExecutor("/tmp")
|
e := NewExecutor("/tmp")
|
||||||
e.Diff = true
|
e.Diff = true
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue