feat(ansible): add lineinfile search_string support
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
92eaab75ba
commit
28ef1f3d85
3 changed files with 279 additions and 0 deletions
104
mock_ssh_test.go
104
mock_ssh_test.go
|
|
@ -900,6 +900,7 @@ func moduleLineinfileWithClient(_ *Executor, client sshFileRunner, args map[stri
|
|||
|
||||
line := getStringArg(args, "line", "")
|
||||
regexpArg := getStringArg(args, "regexp", "")
|
||||
searchString := getStringArg(args, "search_string", "")
|
||||
state := getStringArg(args, "state", "present")
|
||||
backup := getBoolArg(args, "backup", false)
|
||||
backrefs := getBoolArg(args, "backrefs", false)
|
||||
|
|
@ -925,6 +926,37 @@ func moduleLineinfileWithClient(_ *Executor, client sshFileRunner, args map[stri
|
|||
}
|
||||
|
||||
if state == "absent" {
|
||||
if searchString != "" {
|
||||
if before == "" || !strings.Contains(before, searchString) {
|
||||
return &TaskResult{Changed: false}, nil
|
||||
}
|
||||
if err := ensureBackup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated, changed := removeLinesContaining(before, searchString)
|
||||
if !changed {
|
||||
return &TaskResult{Changed: false}, nil
|
||||
}
|
||||
if err := client.Upload(context.Background(), bytes.NewReader([]byte(updated)), path, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &TaskResult{Changed: true}
|
||||
if backupPath != "" {
|
||||
result.Data = map[string]any{"backup_file": backupPath}
|
||||
}
|
||||
if after, ok := mockRemoteFileText(client, path); ok && before != after {
|
||||
if result.Data == nil {
|
||||
result.Data = make(map[string]any)
|
||||
}
|
||||
result.Data["diff"] = map[string]any{
|
||||
"path": path,
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if regexpArg != "" {
|
||||
if err := ensureBackup(); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -937,6 +969,78 @@ func moduleLineinfileWithClient(_ *Executor, client sshFileRunner, args map[stri
|
|||
}
|
||||
} else {
|
||||
// state == present
|
||||
if searchString != "" {
|
||||
if before != "" && fileContainsExactLine(before, line) {
|
||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", path)}, nil
|
||||
}
|
||||
|
||||
if before != "" {
|
||||
updated, changed := replaceFirstLineContaining(before, searchString, line)
|
||||
if changed {
|
||||
if err := ensureBackup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.Upload(context.Background(), bytes.NewReader([]byte(updated)), path, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &TaskResult{Changed: true}
|
||||
if backupPath != "" {
|
||||
result.Data = map[string]any{"backup_file": backupPath}
|
||||
}
|
||||
if after, ok := mockRemoteFileText(client, path); ok && before != after {
|
||||
if result.Data == nil {
|
||||
result.Data = make(map[string]any)
|
||||
}
|
||||
result.Data["diff"] = map[string]any{
|
||||
"path": path,
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := ensureBackup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inserted, err := mockInsertLineRelativeToMatch(context.Background(), client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
||||
return nil, err
|
||||
} else if inserted {
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
updated := line
|
||||
if before != "" {
|
||||
updated = before
|
||||
if updated != "" && !strings.HasSuffix(updated, "\n") {
|
||||
updated += "\n"
|
||||
}
|
||||
updated += line
|
||||
}
|
||||
if before == "" && line != "" {
|
||||
updated = line + "\n"
|
||||
}
|
||||
if err := client.Upload(context.Background(), bytes.NewReader([]byte(updated)), path, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &TaskResult{Changed: true}
|
||||
if backupPath != "" {
|
||||
result.Data = map[string]any{"backup_file": backupPath}
|
||||
}
|
||||
if after, ok := mockRemoteFileText(client, path); ok && before != after {
|
||||
if result.Data == nil {
|
||||
result.Data = make(map[string]any)
|
||||
}
|
||||
result.Data["diff"] = map[string]any{
|
||||
"path": path,
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if regexpArg != "" {
|
||||
// Replace line matching regexp.
|
||||
escapedLine := replaceAll(line, "/", "\\/")
|
||||
|
|
|
|||
139
modules.go
139
modules.go
|
|
@ -895,6 +895,7 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
|||
|
||||
line := getStringArg(args, "line", "")
|
||||
regexp := getStringArg(args, "regexp", "")
|
||||
searchString := getStringArg(args, "search_string", "")
|
||||
state := getStringArg(args, "state", "present")
|
||||
backup := getBoolArg(args, "backup", false)
|
||||
backrefs := getBoolArg(args, "backrefs", false)
|
||||
|
|
@ -928,6 +929,34 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
|||
}
|
||||
|
||||
if state == "absent" {
|
||||
if searchString != "" {
|
||||
if !hasBefore || !strings.Contains(before, searchString) {
|
||||
return &TaskResult{Changed: false}, nil
|
||||
}
|
||||
if err := ensureBackup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updated, changed := removeLinesContaining(before, searchString)
|
||||
if !changed {
|
||||
return &TaskResult{Changed: false}, nil
|
||||
}
|
||||
if err := client.Upload(ctx, newReader(updated), path, 0644); err != nil {
|
||||
return nil, coreerr.E("Executor.moduleLineinfile", "upload lineinfile search_string removal", err)
|
||||
}
|
||||
result := &TaskResult{Changed: true}
|
||||
if backupPath != "" {
|
||||
result.Data = map[string]any{"backup_file": backupPath}
|
||||
}
|
||||
if e.Diff {
|
||||
if after, ok := remoteFileText(ctx, client, path); ok && before != after {
|
||||
result.Data = ensureTaskResultData(result.Data)
|
||||
result.Data["diff"] = fileDiffData(path, before, after)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if regexp != "" {
|
||||
if content, ok := remoteFileText(ctx, client, path); !ok || !regexpMatchString(regexp, content) {
|
||||
return &TaskResult{Changed: false}, nil
|
||||
|
|
@ -949,6 +978,71 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client sshExecutorClien
|
|||
}
|
||||
|
||||
// state == present
|
||||
if searchString != "" {
|
||||
if hasBefore && fileContainsExactLine(before, line) {
|
||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", path)}, nil
|
||||
}
|
||||
|
||||
if hasBefore {
|
||||
updated, changed := replaceFirstLineContaining(before, searchString, line)
|
||||
if changed {
|
||||
if err := ensureBackup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.Upload(ctx, newReader(updated), path, 0644); err != nil {
|
||||
return nil, coreerr.E("Executor.moduleLineinfile", "upload lineinfile search_string replacement", err)
|
||||
}
|
||||
result := &TaskResult{Changed: true}
|
||||
if backupPath != "" {
|
||||
result.Data = map[string]any{"backup_file": backupPath}
|
||||
}
|
||||
if e.Diff {
|
||||
if after, ok := remoteFileText(ctx, client, path); ok && before != after {
|
||||
result.Data = ensureTaskResultData(result.Data)
|
||||
result.Data["diff"] = fileDiffData(path, before, after)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := ensureBackup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inserted, err := insertLineRelativeToMatch(ctx, client, path, line, insertBefore, insertAfter, firstMatch); err != nil {
|
||||
return nil, err
|
||||
} else if inserted {
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
updated := line
|
||||
if hasBefore {
|
||||
updated = before
|
||||
if updated != "" && !strings.HasSuffix(updated, "\n") {
|
||||
updated += "\n"
|
||||
}
|
||||
updated += line
|
||||
}
|
||||
if !hasBefore && line != "" {
|
||||
updated = line + "\n"
|
||||
}
|
||||
|
||||
if err := client.Upload(ctx, newReader(updated), path, 0644); err != nil {
|
||||
return nil, coreerr.E("Executor.moduleLineinfile", "upload lineinfile search_string append", err)
|
||||
}
|
||||
result := &TaskResult{Changed: true}
|
||||
if backupPath != "" {
|
||||
result.Data = map[string]any{"backup_file": backupPath}
|
||||
}
|
||||
if e.Diff {
|
||||
if after, ok := remoteFileText(ctx, client, path); ok && before != after {
|
||||
result.Data = ensureTaskResultData(result.Data)
|
||||
result.Data["diff"] = fileDiffData(path, before, after)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if regexp != "" {
|
||||
escapedLine := replaceAll(line, "/", "\\/")
|
||||
sedFlags := "-i"
|
||||
|
|
@ -1166,6 +1260,51 @@ func insertLineRelativeToMatch(ctx context.Context, client commandRunner, path,
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func replaceFirstLineContaining(content, needle, line string) (string, bool) {
|
||||
if content == "" || needle == "" {
|
||||
return content, false
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
changed := false
|
||||
for i, current := range lines {
|
||||
if changed {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(current, needle) {
|
||||
lines[i] = line
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return content, false
|
||||
}
|
||||
|
||||
return join("\n", lines), true
|
||||
}
|
||||
|
||||
func removeLinesContaining(content, needle string) (string, bool) {
|
||||
if content == "" || needle == "" {
|
||||
return content, false
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
kept := make([]string, 0, len(lines))
|
||||
removed := false
|
||||
for _, current := range lines {
|
||||
if strings.Contains(current, needle) {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, current)
|
||||
}
|
||||
if !removed {
|
||||
return content, false
|
||||
}
|
||||
|
||||
return join("\n", kept), true
|
||||
}
|
||||
|
||||
func buildLineinfileInsertCommand(path, line, anchor string, after, firstMatch bool) string {
|
||||
quotedLine := shellSingleQuote(line)
|
||||
quotedAnchor := shellSingleQuote(anchor)
|
||||
|
|
|
|||
|
|
@ -810,6 +810,42 @@ func TestModulesFile_ModuleLineinfile_Good_ExactLineAlreadyPresentIsNoOp(t *test
|
|||
assert.Equal(t, 0, mock.commandCount())
|
||||
}
|
||||
|
||||
func TestModulesFile_ModuleLineinfile_Good_SearchStringReplacesMatchingLine(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.addFile("/etc/ssh/sshd_config", []byte("PermitRootLogin yes\nPasswordAuthentication yes\n"))
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/ssh/sshd_config",
|
||||
"search_string": "PermitRootLogin",
|
||||
"line": "PermitRootLogin no",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
after, err := mock.Download(context.Background(), "/etc/ssh/sshd_config")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "PermitRootLogin no\nPasswordAuthentication yes\n", string(after))
|
||||
}
|
||||
|
||||
func TestModulesFile_ModuleLineinfile_Good_SearchStringRemovesMatchingLine(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.addFile("/etc/ssh/sshd_config", []byte("PermitRootLogin yes\nPasswordAuthentication yes\n"))
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/ssh/sshd_config",
|
||||
"search_string": "PermitRootLogin",
|
||||
"state": "absent",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
after, err := mock.Download(context.Background(), "/etc/ssh/sshd_config")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "PasswordAuthentication yes\n", string(after))
|
||||
}
|
||||
|
||||
func TestModulesFile_ModuleLineinfile_Good_BackupExistingFile(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
path := "/etc/example.conf"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue