Add diff-aware file module output
This commit is contained in:
parent
2965d93ca8
commit
bfbbd31f09
5 changed files with 141 additions and 4 deletions
|
|
@ -47,6 +47,7 @@ func runAnsible(opts core.Options) core.Result {
|
|||
// Set options
|
||||
executor.Limit = opts.String("limit")
|
||||
executor.CheckMode = opts.Bool("check")
|
||||
executor.Diff = opts.Bool("diff")
|
||||
executor.Verbose = opts.Int("verbose")
|
||||
|
||||
if tags := opts.String("tags"); tags != "" {
|
||||
|
|
@ -138,6 +139,18 @@ func runAnsible(opts core.Options) core.Result {
|
|||
print("stdout: %s", trimSpace(result.Stdout))
|
||||
}
|
||||
}
|
||||
|
||||
if executor.Diff {
|
||||
if diff, ok := result.Data["diff"].(map[string]any); ok {
|
||||
print("diff:")
|
||||
if before, ok := diff["before"].(string); ok && before != "" {
|
||||
print("- %s", before)
|
||||
}
|
||||
if after, ok := diff["after"].(string); ok && after != "" {
|
||||
print("+ %s", after)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executor.OnPlayEnd = func(play *ansible.Play) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ func Register(c *core.Core) {
|
|||
core.Option{Key: "extra-vars", Value: ""},
|
||||
core.Option{Key: "verbose", Value: 0},
|
||||
core.Option{Key: "check", Value: false},
|
||||
core.Option{Key: "diff", Value: false},
|
||||
),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -608,10 +608,19 @@ func moduleScriptWithClient(_ *Executor, client sshRunner, args map[string]any)
|
|||
type sshFileRunner interface {
|
||||
sshRunner
|
||||
Upload(ctx context.Context, local io.Reader, remote string, mode fs.FileMode) error
|
||||
Download(ctx context.Context, remote string) ([]byte, error)
|
||||
Stat(ctx context.Context, path string) (map[string]any, error)
|
||||
FileExists(ctx context.Context, path string) (bool, error)
|
||||
}
|
||||
|
||||
func mockRemoteFileText(client sshFileRunner, path string) (string, bool) {
|
||||
data, err := client.Download(context.Background(), path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(data), true
|
||||
}
|
||||
|
||||
// --- File module shims ---
|
||||
|
||||
func moduleCopyWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
||||
|
|
@ -641,6 +650,13 @@ func moduleCopyWithClient(e *Executor, client sshFileRunner, args map[string]any
|
|||
}
|
||||
}
|
||||
|
||||
before, hasBefore := mockRemoteFileText(client, dest)
|
||||
if hasBefore && before == string(content) {
|
||||
if getStringArg(args, "owner", "") == "" && getStringArg(args, "group", "") == "" {
|
||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", dest)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
err = client.Upload(context.Background(), newReader(string(content)), dest, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -654,7 +670,17 @@ func moduleCopyWithClient(e *Executor, client sshFileRunner, args map[string]any
|
|||
_, _, _, _ = client.Run(context.Background(), sprintf("chgrp %s %q", group, dest))
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true, Msg: sprintf("copied to %s", dest)}, nil
|
||||
result := &TaskResult{Changed: true, Msg: sprintf("copied to %s", dest)}
|
||||
if e.Diff && hasBefore {
|
||||
result.Data = map[string]any{
|
||||
"diff": map[string]any{
|
||||
"path": dest,
|
||||
"before": before,
|
||||
"after": string(content),
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func moduleTemplateWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
||||
|
|
@ -677,12 +703,27 @@ func moduleTemplateWithClient(e *Executor, client sshFileRunner, args map[string
|
|||
}
|
||||
}
|
||||
|
||||
before, hasBefore := mockRemoteFileText(client, dest)
|
||||
if hasBefore && before == content {
|
||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", dest)}, nil
|
||||
}
|
||||
|
||||
err = client.Upload(context.Background(), newReader(content), dest, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true, Msg: sprintf("templated to %s", dest)}, nil
|
||||
result := &TaskResult{Changed: true, Msg: sprintf("templated to %s", dest)}
|
||||
if e.Diff && hasBefore {
|
||||
result.Data = map[string]any{
|
||||
"diff": map[string]any{
|
||||
"path": dest,
|
||||
"before": before,
|
||||
"after": content,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any) (*TaskResult, error) {
|
||||
|
|
|
|||
44
modules.go
44
modules.go
|
|
@ -158,6 +158,22 @@ func (e *Executor) executeModule(ctx context.Context, host string, client sshExe
|
|||
}
|
||||
}
|
||||
|
||||
func remoteFileText(ctx context.Context, client sshExecutorClient, path string) (string, bool) {
|
||||
data, err := client.Download(ctx, path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(data), true
|
||||
}
|
||||
|
||||
func fileDiffData(path, before, after string) map[string]any {
|
||||
return map[string]any{
|
||||
"path": path,
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
}
|
||||
|
||||
// templateArgs templates all string values in args.
|
||||
func (e *Executor) templateArgs(args map[string]any, host string, task *Task) map[string]any {
|
||||
// Set inventory_hostname for templating
|
||||
|
|
@ -321,6 +337,13 @@ func (e *Executor) moduleCopy(ctx context.Context, client sshExecutorClient, arg
|
|||
}
|
||||
}
|
||||
|
||||
before, hasBefore := remoteFileText(ctx, client, dest)
|
||||
if hasBefore && before == content {
|
||||
if getStringArg(args, "owner", "") == "" && getStringArg(args, "group", "") == "" {
|
||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", dest)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
err = client.Upload(ctx, newReader(content), dest, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -334,7 +357,13 @@ func (e *Executor) moduleCopy(ctx context.Context, client sshExecutorClient, arg
|
|||
_, _, _, _ = client.Run(ctx, sprintf("chgrp %s %q", group, dest))
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true, Msg: sprintf("copied to %s", dest)}, nil
|
||||
result := &TaskResult{Changed: true, Msg: sprintf("copied to %s", dest)}
|
||||
if e.Diff {
|
||||
if hasBefore {
|
||||
result.Data = map[string]any{"diff": fileDiffData(dest, before, content)}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *Executor) moduleTemplate(ctx context.Context, client sshExecutorClient, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
||||
|
|
@ -357,12 +386,23 @@ func (e *Executor) moduleTemplate(ctx context.Context, client sshExecutorClient,
|
|||
}
|
||||
}
|
||||
|
||||
before, hasBefore := remoteFileText(ctx, client, dest)
|
||||
if hasBefore && before == content {
|
||||
return &TaskResult{Changed: false, Msg: sprintf("already up to date: %s", dest)}, nil
|
||||
}
|
||||
|
||||
err = client.Upload(ctx, newReader(content), dest, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true, Msg: sprintf("templated to %s", dest)}, nil
|
||||
result := &TaskResult{Changed: true, Msg: sprintf("templated to %s", dest)}
|
||||
if e.Diff {
|
||||
if hasBefore {
|
||||
result.Data = map[string]any{"diff": fileDiffData(dest, before, content)}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *Executor) moduleFile(ctx context.Context, client sshExecutorClient, args map[string]any) (*TaskResult, error) {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,22 @@ func TestModulesFile_ModuleCopy_Good_ContentTakesPrecedenceOverSrc(t *testing.T)
|
|||
assert.Equal(t, []byte("from_content"), up.Content)
|
||||
}
|
||||
|
||||
func TestModulesFile_ModuleCopy_Good_SkipsUnchangedContent(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
e.Diff = true
|
||||
mock.addFile("/etc/app/config", []byte("server_name=web01"))
|
||||
|
||||
result, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"content": "server_name=web01",
|
||||
"dest": "/etc/app/config",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result.Changed)
|
||||
assert.Equal(t, 0, mock.uploadCount())
|
||||
assert.Contains(t, result.Msg, "already up to date")
|
||||
}
|
||||
|
||||
// --- file module ---
|
||||
|
||||
func TestModulesFile_ModuleFile_Good_StateDirectory(t *testing.T) {
|
||||
|
|
@ -776,6 +792,32 @@ func TestModulesFile_ModuleTemplate_Good_PlainTextNoVars(t *testing.T) {
|
|||
assert.Equal(t, content, string(up.Content))
|
||||
}
|
||||
|
||||
func TestModulesFile_ModuleTemplate_Good_DiffData(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcPath := joinPath(tmpDir, "app.conf.j2")
|
||||
require.NoError(t, writeTestFile(srcPath, []byte("server_name={{ server_name }};"), 0644))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
e.Diff = true
|
||||
e.SetVar("server_name", "web01.example.com")
|
||||
mock.addFile("/etc/nginx/conf.d/app.conf", []byte("server_name=old.example.com;"))
|
||||
|
||||
result, err := moduleTemplateWithClient(e, mock, map[string]any{
|
||||
"src": srcPath,
|
||||
"dest": "/etc/nginx/conf.d/app.conf",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
require.NotNil(t, result.Data)
|
||||
|
||||
diff, ok := result.Data["diff"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "/etc/nginx/conf.d/app.conf", diff["path"])
|
||||
assert.Equal(t, "server_name=old.example.com;", diff["before"])
|
||||
assert.Contains(t, diff["after"], "web01.example.com")
|
||||
}
|
||||
|
||||
// --- Cross-module dispatch tests for file modules ---
|
||||
|
||||
func TestModulesFile_ExecuteModuleWithMock_Good_DispatchCopy(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue