diff --git a/mock_ssh_test.go b/mock_ssh_test.go index 459ea9f..2b56454 100644 --- a/mock_ssh_test.go +++ b/mock_ssh_test.go @@ -842,6 +842,17 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil } + case "hard": + src := getStringArg(args, "src", "") + if src == "" { + return nil, mockError("moduleFileWithClient", "file: src required for hard state") + } + cmd := sprintf("ln -f %q %q", src, path) + _, stderr, rc, err := client.Run(context.Background(), cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil + } + case "file": // Ensure file exists and set permissions if mode := getStringArg(args, "mode", ""); mode != "" { diff --git a/modules.go b/modules.go index fc56387..4b4c21f 100644 --- a/modules.go +++ b/modules.go @@ -783,6 +783,17 @@ func (e *Executor) moduleFile(ctx context.Context, client sshExecutorClient, arg return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil } + case "hard": + src := getStringArg(args, "src", "") + if src == "" { + return nil, coreerr.E("Executor.moduleFile", "src required for hard state", nil) + } + cmd := sprintf("ln -f %q %q", src, path) + _, stderr, rc, err := client.Run(ctx, cmd) + if err != nil || rc != 0 { + return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil + } + case "file": // Ensure file exists and set permissions if mode := getStringArg(args, "mode", ""); mode != "" { diff --git a/modules_file_test.go b/modules_file_test.go index 99b3d5b..f56965d 100644 --- a/modules_file_test.go +++ b/modules_file_test.go @@ -419,6 +419,32 @@ func TestModulesFile_ModuleFile_Good_StateLink(t *testing.T) { assert.True(t, mock.hasExecuted(`ln -sf "/opt/node/bin/node" "/usr/local/bin/node"`)) } +func TestModulesFile_ModuleFile_Good_StateHard(t *testing.T) { + e, mock := newTestExecutorWithMock("host1") + + result, err := moduleFileWithClient(e, mock, map[string]any{ + "path": "/usr/local/bin/node", + "state": "hard", + "src": "/opt/node/bin/node", + }) + + require.NoError(t, err) + assert.True(t, result.Changed) + assert.True(t, mock.hasExecuted(`ln -f "/opt/node/bin/node" "/usr/local/bin/node"`)) +} + +func TestModulesFile_ModuleFile_Bad_StateHardMissingSrc(t *testing.T) { + e, mock := newTestExecutorWithMock("host1") + + _, err := moduleFileWithClient(e, mock, map[string]any{ + "path": "/usr/local/bin/node", + "state": "hard", + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "src required for hard state") +} + func TestModulesFile_ModuleFile_Bad_LinkMissingSrc(t *testing.T) { e, mock := newTestExecutorWithMock("host1")