Files
computer-playbook/tests/unit/plugins/action/test_stack_host_copy.py
T
kevinveenbirkenbachandClaude Opus 4.8 b2a6cf11c1 feat(swarm): add stack_host_copy action wrapper and migrate compose-dir copies
stack_host_copy is the copy: analogue of stack_host_template: it folds the IS_STACK_HOST gate into the module so callers do not silently fail on hosts whose destination tree is owned only by the stack host. In swarm an app's compose / build-context directory (/opt/compose/<app>) exists only on the manager, so a bare copy: into directories.instance also ran on workers and aborted the play with 'Destination directory does not exist'.

Migrate the six compose-dir copy tasks (baserow, jenkins, bookwyrm, postmarks, yourls, openldap) to stack_host_copy and drop the now-redundant when: IS_STACK_HOST. A new drift-guard lint test (test_stack_host_copy_migration) flags any future bare copy: into the compose dir, and a unit test covers the wrapper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:44:37 +02:00

113 lines
3.7 KiB
Python

import unittest
from unittest.mock import patch
from plugins.action.stack_host_copy import ActionModule
class _FakeTask:
def __init__(self, *, args=None):
self.args = {} if args is None else dict(args)
class _FakeTemplar:
def __init__(self, *, is_stack_host=True):
self._is_stack_host = is_stack_host
self.available_variables: dict[str, object] = {}
def template(self, raw):
return self._is_stack_host
def _make_action(task, templar):
action = object.__new__(ActionModule)
action._task = task
action._templar = templar
return action
class TestStackHostCopy(unittest.TestCase):
def test_skips_silently_on_non_stack_host(self):
task = _FakeTask(args={"src": "x", "dest": "/opt/compose/app/x"})
action = _make_action(task, _FakeTemplar(is_stack_host=False))
result = action.run(task_vars={})
self.assertTrue(result["skipped"])
self.assertFalse(result["changed"])
self.assertIn("IS_STACK_HOST", result["skip_reason"])
def test_skips_when_templar_returns_string_false(self):
task = _FakeTask(args={"src": "x", "dest": "/opt/compose/app/x"})
action = _make_action(task, _FakeTemplar(is_stack_host="False"))
result = action.run(task_vars={})
self.assertTrue(result["skipped"])
def test_delegates_to_copy_on_stack_host(self):
task = _FakeTask(args={"src": "x", "dest": "/opt/compose/app/x"})
action = _make_action(task, _FakeTemplar(is_stack_host=True))
with patch(
"plugins.action.stack_host_copy.CopyActionModule.run",
return_value={"changed": True},
) as super_run:
action.run(task_vars={})
super_run.assert_called_once()
def test_seeds_templar_with_task_vars_before_rendering(self):
task = _FakeTask(args={"src": "x", "dest": "/opt/compose/app/x"})
templar = _FakeTemplar(is_stack_host=True)
action = _make_action(task, templar)
with patch(
"plugins.action.stack_host_copy.CopyActionModule.run",
return_value={"changed": True},
):
action.run(task_vars={"IS_STACK_HOST": True, "ANSIBLE_VERSION": "9.0"})
self.assertEqual(templar.available_variables.get("IS_STACK_HOST"), True)
self.assertEqual(templar.available_variables.get("ANSIBLE_VERSION"), "9.0")
def test_defaults_mode_to_0644_when_unset(self):
task = _FakeTask(args={"src": "x", "dest": "/opt/compose/app/x"})
action = _make_action(task, _FakeTemplar(is_stack_host=True))
with patch(
"plugins.action.stack_host_copy.CopyActionModule.run",
return_value={"changed": True},
):
action.run(task_vars={})
self.assertEqual(task.args["mode"], "0644")
def test_keeps_caller_supplied_mode(self):
task = _FakeTask(
args={"src": "x", "dest": "/opt/compose/app/x", "mode": "0600"}
)
action = _make_action(task, _FakeTemplar(is_stack_host=True))
with patch(
"plugins.action.stack_host_copy.CopyActionModule.run",
return_value={"changed": True},
):
action.run(task_vars={})
self.assertEqual(task.args["mode"], "0600")
def test_is_stack_host_expr_carries_trust_tag(self):
from ansible.template import is_trusted_as_template
from plugins.action.stack_host_copy import _IS_STACK_HOST_EXPR
self.assertTrue(
is_trusted_as_template(_IS_STACK_HOST_EXPR),
"The IS_STACK_HOST template expression must carry the TrustedAsTemplate "
"tag; otherwise Ansible 2.19+ returns the literal string unchanged.",
)
if __name__ == "__main__":
unittest.main()