diff --git a/mock_ssh_test.go b/mock_ssh_test.go index 9c6c694..0fa35e0 100644 --- a/mock_ssh_test.go +++ b/mock_ssh_test.go @@ -931,12 +931,24 @@ func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[str marker := getStringArg(args, "marker", "# {mark} ANSIBLE MANAGED BLOCK") state := getStringArg(args, "state", "present") create := getBoolArg(args, "create", false) + backup := getBoolArg(args, "backup", false) prependNewline := getBoolArg(args, "prepend_newline", false) appendNewline := getBoolArg(args, "append_newline", false) beginMarker := replaceN(marker, "{mark}", "BEGIN", 1) endMarker := replaceN(marker, "{mark}", "END", 1) + var backupPath string + if backup { + before, hasBefore := mockRemoteFileText(client, path) + if hasBefore { + 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 nil, err + } + } + } + if state == "absent" { // Remove block cmd := sprintf("sed -i '/%s/,/%s/d' %q", @@ -975,7 +987,12 @@ BLOCK_EOF return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil } - return &TaskResult{Changed: true}, nil + result := &TaskResult{Changed: true} + if backupPath != "" { + result.Data = map[string]any{"backup_file": backupPath} + } + + return result, nil } func mockInsertLineRelativeToMatch(ctx context.Context, client commandRunner, path, line, insertBefore, insertAfter string, firstMatch bool) (bool, error) { diff --git a/modules.go b/modules.go index a1e3a61..cbeb1c0 100644 --- a/modules.go +++ b/modules.go @@ -2371,12 +2371,22 @@ func (e *Executor) moduleBlockinfile(ctx context.Context, client sshExecutorClie marker := getStringArg(args, "marker", "# {mark} ANSIBLE MANAGED BLOCK") state := getStringArg(args, "state", "present") create := getBoolArg(args, "create", false) + backup := getBoolArg(args, "backup", false) prependNewline := getBoolArg(args, "prepend_newline", false) appendNewline := getBoolArg(args, "append_newline", false) beginMarker := replaceN(marker, "{mark}", "BEGIN", 1) endMarker := replaceN(marker, "{mark}", "END", 1) + var backupPath string + if backup { + var hasBefore bool + backupPath, hasBefore, _ = backupRemoteFile(ctx, client, path) + if !hasBefore { + backupPath = "" + } + } + if state == "absent" { // Remove block cmd := sprintf("sed -i '/%s/,/%s/d' %q", @@ -2415,7 +2425,12 @@ BLOCK_EOF return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil } - return &TaskResult{Changed: true}, nil + result := &TaskResult{Changed: true} + if backupPath != "" { + result.Data = map[string]any{"backup_file": backupPath} + } + + return result, nil } func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) { diff --git a/modules_file_test.go b/modules_file_test.go index 4ce4ded..d3fd1f6 100644 --- a/modules_file_test.go +++ b/modules_file_test.go @@ -705,6 +705,30 @@ func TestModulesFile_ModuleBlockinfile_Good_CreateFile(t *testing.T) { assert.True(t, mock.hasExecuted(`touch "/etc/new-config"`)) } +func TestModulesFile_ModuleBlockinfile_Good_BackupExistingDest(t *testing.T) { + e, mock := newTestExecutorWithMock("host1") + mock.addFile("/etc/config", []byte("old block contents")) + + result, err := moduleBlockinfileWithClient(e, mock, map[string]any{ + "path": "/etc/config", + "block": "new block contents", + "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/config.") + assert.Equal(t, 1, mock.uploadCount()) + + backupContent, err := mock.Download(context.Background(), backupPath) + require.NoError(t, err) + assert.Equal(t, []byte("old block contents"), backupContent) +} + func TestModulesFile_ModuleBlockinfile_Bad_MissingPath(t *testing.T) { e, mock := newTestExecutorWithMock("host1")