feat(ansible): reset cached ssh connections

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 20:10:56 +00:00
parent f1a52e777e
commit 02cb9273c5
3 changed files with 56 additions and 3 deletions

View file

@ -529,7 +529,7 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin
}
if NormalizeModule(task.Module) == "ansible.builtin.meta" {
if err := e.handleMetaAction(ctx, hosts, play, result); err != nil {
if err := e.handleMetaAction(ctx, host, hosts, play, result); err != nil {
return err
}
}
@ -1629,7 +1629,7 @@ func (e *Executor) runNotifiedHandlers(ctx context.Context, hosts []string, play
// handleMetaAction applies module meta side effects after the task result has
// been recorded and callbacks have fired.
func (e *Executor) handleMetaAction(ctx context.Context, hosts []string, play *Play, result *TaskResult) error {
func (e *Executor) handleMetaAction(ctx context.Context, host string, hosts []string, play *Play, result *TaskResult) error {
if result == nil || result.Data == nil {
return nil
}
@ -1643,6 +1643,9 @@ func (e *Executor) handleMetaAction(ctx context.Context, hosts []string, play *P
return nil
case "end_play":
return errEndPlay
case "reset_connection":
e.resetConnection(host)
return nil
default:
return nil
}
@ -1658,6 +1661,24 @@ func (e *Executor) clearFacts(hosts []string) {
}
}
// resetConnection closes and removes the cached SSH client for a host.
func (e *Executor) resetConnection(host string) {
if host == "" {
return
}
e.mu.Lock()
client, ok := e.clients[host]
if ok {
delete(e.clients, host)
}
e.mu.Unlock()
if ok {
_ = client.Close()
}
}
// Close closes all SSH connections.
//
// Example:

View file

@ -324,13 +324,23 @@ func TestExecutorExtra_ModuleMeta_Good_ClearFacts(t *testing.T) {
assert.Equal(t, "clear_facts", result.Data["action"])
}
func TestExecutorExtra_ModuleMeta_Good_ResetConnection(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleMeta(map[string]any{"_raw_params": "reset_connection"})
require.NoError(t, err)
assert.False(t, result.Changed)
require.NotNil(t, result.Data)
assert.Equal(t, "reset_connection", result.Data["action"])
}
func TestExecutorExtra_HandleMetaAction_Good_ClearFacts(t *testing.T) {
e := NewExecutor("/tmp")
e.facts["host1"] = &Facts{Hostname: "web01"}
e.facts["host2"] = &Facts{Hostname: "web02"}
result := &TaskResult{Data: map[string]any{"action": "clear_facts"}}
require.NoError(t, e.handleMetaAction(context.Background(), []string{"host1"}, nil, result))
require.NoError(t, e.handleMetaAction(context.Background(), "host1", []string{"host1"}, nil, result))
_, ok := e.facts["host1"]
assert.False(t, ok)
@ -338,6 +348,22 @@ func TestExecutorExtra_HandleMetaAction_Good_ClearFacts(t *testing.T) {
assert.Equal(t, "web02", e.facts["host2"].Hostname)
}
func TestExecutorExtra_HandleMetaAction_Good_ResetConnection(t *testing.T) {
e := NewExecutor("/tmp")
mock := NewMockSSHClient()
e.clients["host1"] = mock
e.clients["host2"] = NewMockSSHClient()
result := &TaskResult{Data: map[string]any{"action": "reset_connection"}}
require.NoError(t, e.handleMetaAction(context.Background(), "host1", []string{"host1", "host2"}, nil, result))
_, ok := e.clients["host1"]
assert.False(t, ok)
_, ok = e.clients["host2"]
assert.True(t, ok)
assert.True(t, mock.closed)
}
// ============================================================
// Tests for handleLookup (0% coverage)
// ============================================================

View file

@ -36,6 +36,9 @@ type MockSSHClient struct {
becomeUser string
becomePass string
// Lifecycle tracking
closed bool
// Execution log: every command that was executed
executed []executedCommand
@ -266,6 +269,9 @@ func (m *MockSSHClient) SetBecome(become bool, user, password string) {
//
// _ = mock.Close()
func (m *MockSSHClient) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
m.closed = true
return nil
}