GRM-16: feat: add --force flag to grm remove for unreachable runners
- Add force parameter to RunnerManager.remove() — skips remote Ansible playbook and only removes the local registry entry - Add --force/-f CLI flag to grm remove command - Add translations for --force help text across all 5 languages - Add unit tests for force skip and CLI flag propagation - 118 tests, 100% coverage, pyright clean, ruff clean
This commit is contained in:
@@ -306,6 +306,12 @@ def status(
|
|||||||
type=click.Choice(["docker", "binary"]),
|
type=click.Choice(["docker", "binary"]),
|
||||||
help=_("Override deployment mode from registry"),
|
help=_("Override deployment mode from registry"),
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force",
|
||||||
|
"-f",
|
||||||
|
is_flag=True,
|
||||||
|
help=_("Skip remote cleanup and only remove the local registry entry"),
|
||||||
|
)
|
||||||
@_handle_errors("Remove failed: {error}")
|
@_handle_errors("Remove failed: {error}")
|
||||||
def remove(
|
def remove(
|
||||||
runner_name: str,
|
runner_name: str,
|
||||||
@@ -314,6 +320,7 @@ def remove(
|
|||||||
key: str | None,
|
key: str | None,
|
||||||
token: str | None,
|
token: str | None,
|
||||||
mode: str | None,
|
mode: str | None,
|
||||||
|
force: bool,
|
||||||
ask_become_pass: bool,
|
ask_become_pass: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
manager = RunnerManager()
|
manager = RunnerManager()
|
||||||
@@ -326,6 +333,7 @@ def remove(
|
|||||||
gitea_url=os.getenv("GITEA_URL", ""),
|
gitea_url=os.getenv("GITEA_URL", ""),
|
||||||
mode=mode,
|
mode=mode,
|
||||||
ask_become_pass=ask_become_pass,
|
ask_become_pass=ask_become_pass,
|
||||||
|
force=force,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -467,6 +467,13 @@ TRANSLATIONS: dict[str, dict[str, str]] = {
|
|||||||
"ru": "Удалить runner '{name}' из локального реестра",
|
"ru": "Удалить runner '{name}' из локального реестра",
|
||||||
"zh": "从本地注册表移除 runner '{name}'",
|
"zh": "从本地注册表移除 runner '{name}'",
|
||||||
},
|
},
|
||||||
|
"Skip remote cleanup and only remove the local registry entry": {
|
||||||
|
"en": "Skip remote cleanup and only remove the local registry entry",
|
||||||
|
"bg": "Пропуснете отдалеченото почистване и премахнете само локалния запис",
|
||||||
|
"de": "Remote-Bereinigung überspringen und nur den lokalen Registrierungseintrag entfernen",
|
||||||
|
"ru": "Пропустить удаленную очистку и удалить только локальную запись реестра",
|
||||||
|
"zh": "跳过远程清理,仅删除本地注册表条目",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -240,8 +240,14 @@ class RunnerManager:
|
|||||||
gitea_url: str = "",
|
gitea_url: str = "",
|
||||||
mode: str | None = None,
|
mode: str | None = None,
|
||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
|
force: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Remove a runner instance completely."""
|
"""Remove a runner instance completely.
|
||||||
|
|
||||||
|
When *force* is ``True``, skip the remote Ansible playbook and
|
||||||
|
only remove the local registry entry. Use this when the remote
|
||||||
|
host is already gone or unreachable.
|
||||||
|
"""
|
||||||
if not gitea_url:
|
if not gitea_url:
|
||||||
raise AnsibleError(_("GITEA_URL must be set (or pass --url)"))
|
raise AnsibleError(_("GITEA_URL must be set (or pass --url)"))
|
||||||
actual_host, actual_user, actual_key, actual_mode = self._resolve_runner(name, host, user, key, mode)
|
actual_host, actual_user, actual_key, actual_mode = self._resolve_runner(name, host, user, key, mode)
|
||||||
@@ -249,15 +255,16 @@ class RunnerManager:
|
|||||||
raise AnsibleError(_("GITEA_REGISTRATION_TOKEN must be set (or pass --token)"))
|
raise AnsibleError(_("GITEA_REGISTRATION_TOKEN must be set (or pass --token)"))
|
||||||
with track_steps() as tracker:
|
with track_steps() as tracker:
|
||||||
tracker.begin(_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host))
|
tracker.begin(_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host))
|
||||||
extra_vars = (
|
if not force:
|
||||||
f"runner_name={name} registration_token={token} gitea_url={gitea_url} runner_mode={actual_mode}"
|
extra_vars = (
|
||||||
)
|
f"runner_name={name} registration_token={token} gitea_url={gitea_url} runner_mode={actual_mode}"
|
||||||
cmd = self._build_cmd(
|
)
|
||||||
"remove-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
cmd = self._build_cmd(
|
||||||
)
|
"remove-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
||||||
self._executor.run(
|
)
|
||||||
cmd, description=_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host)
|
self._executor.run(
|
||||||
)
|
cmd, description=_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host)
|
||||||
|
)
|
||||||
tracker.done()
|
tracker.done()
|
||||||
|
|
||||||
tracker.begin(_("Remove runner '{name}' from local registry", name=name))
|
tracker.begin(_("Remove runner '{name}' from local registry", name=name))
|
||||||
|
|||||||
@@ -375,6 +375,27 @@ class TestCLI:
|
|||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode=None,
|
mode=None,
|
||||||
ask_become_pass=False,
|
ask_become_pass=False,
|
||||||
|
force=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||||
|
def test_remove_force_flag(self, mock_manager_class: MagicMock) -> None:
|
||||||
|
mock_manager = MagicMock()
|
||||||
|
mock_manager_class.return_value = mock_manager
|
||||||
|
|
||||||
|
runner = CliRunner(env={"GITEA_URL": "https://git.example.com"})
|
||||||
|
result = runner.invoke(cli, ["remove", "r1", "--token", "tok", "--force"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_manager.remove.assert_called_once_with(
|
||||||
|
name="r1",
|
||||||
|
host=None,
|
||||||
|
user=None,
|
||||||
|
key=None,
|
||||||
|
token="tok",
|
||||||
|
gitea_url="https://git.example.com",
|
||||||
|
mode=None,
|
||||||
|
ask_become_pass=False,
|
||||||
|
force=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||||
|
|||||||
@@ -381,6 +381,17 @@ class TestRunnerManager:
|
|||||||
manager.remove("r1", token="tok")
|
manager.remove("r1", token="tok")
|
||||||
mock_registry.remove.assert_not_called()
|
mock_registry.remove.assert_not_called()
|
||||||
|
|
||||||
|
def test_remove_force_skips_playbook(self) -> None:
|
||||||
|
mock_registry = MagicMock()
|
||||||
|
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
||||||
|
manager = RunnerManager(registry=mock_registry)
|
||||||
|
mock_executor = MagicMock()
|
||||||
|
manager._executor = mock_executor
|
||||||
|
|
||||||
|
manager.remove("r1", token="tok", gitea_url="https://git.example.com", force=True)
|
||||||
|
mock_executor.run.assert_not_called()
|
||||||
|
mock_registry.remove.assert_called_once_with("r1")
|
||||||
|
|
||||||
def test_list_runners(self) -> None:
|
def test_list_runners(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
|
|||||||
Reference in New Issue
Block a user