diff --git a/docs/changelog.md b/docs/changelog.md index fb6061fc40..1ee0fdcef3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ This changelog documents user-relevant changes to the GitHub runner charm. +## 2026-04-03 + +- Fixed charm not entering blocked/waiting status when the image integration has no image available. Previously, some event handlers (`upgrade-charm`, `planner-relation-changed`, `planner-relation-broken`) would start the runner manager service without checking image readiness, causing the service to error with "No runner combinations configured." + ## 2026-03-30 - Fixed stale `systemd` service files left behind when a unit is removed from a co-located machine. The service is now disabled, the service file removed, and the unit data cleaned up on stop. diff --git a/src/charm.py b/src/charm.py index 8d84eb1165..e155dcd5f0 100755 --- a/src/charm.py +++ b/src/charm.py @@ -354,8 +354,6 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None: ] flush_runners = True - self._check_image_ready() - self._reconcile(state) if flush_runners: @@ -389,6 +387,7 @@ def _reconcile(self, state: CharmState) -> None: Args: state: The charm state. """ + self._check_image_ready() self._setup_service(state) self._update_planner_flavor(state) @@ -475,8 +474,6 @@ def _apt_install(self, packages: Sequence[str]) -> None: def _on_debug_ssh_relation_changed(self, _: ops.RelationChangedEvent) -> None: """Handle debug ssh relation changed event.""" self.unit.status = MaintenanceStatus("Added debug-ssh relation") - self._check_image_ready() - state = self._setup_state() self._reconcile(state) @@ -498,8 +495,6 @@ def _on_image_relation_joined(self, _: ops.RelationJoinedEvent) -> None: def _on_image_relation_changed(self, _: ops.RelationChangedEvent) -> None: """Handle image relation changed event.""" self.unit.status = MaintenanceStatus("Update image for runners") - self._check_image_ready() - state = self._setup_state() self._reconcile(state) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index b50f9d5636..b870a37979 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -605,6 +605,42 @@ def test_on_config_changed_openstack_clouds_yaml(mock_side_effects): assert harness.charm.unit.status == BlockedStatus("Please provide image integration.") +@pytest.mark.parametrize( + "handler_name, emit_event", + [ + pytest.param( + "_on_upgrade_charm", + lambda h: h.charm.on.upgrade_charm.emit(), + id="upgrade-charm", + ), + pytest.param( + "_on_planner_relation_changed", + lambda h: h.charm._on_planner_relation_changed(MagicMock()), + id="planner-relation-changed", + ), + ], +) +def test_reconcile_blocks_when_image_not_ready( + harness: Harness, mock_side_effects, monkeypatch, handler_name, emit_event +): + """ + arrange: Set up charm with no image integration. + act: Fire an event that calls _reconcile without an explicit _check_image_ready call. + assert: Charm enters blocked/waiting status and the service is stopped. + """ + harness.charm._setup_state = MagicMock() + manager_client_mock = MagicMock(spec=GitHubRunnerManagerClient) + harness.charm._manager_client = manager_client_mock + mock_mgr_service = MagicMock() + monkeypatch.setattr("charm.manager_service", mock_mgr_service) + monkeypatch.setattr("charm.execute_command", MagicMock(return_value=(0, "Mock_stdout"))) + monkeypatch.setattr("charm.pathlib", MagicMock()) + + emit_event(harness) + + assert harness.charm.unit.status.name == BlockedStatus.name + + def test_metric_log_ownership_for_upgrade( harness: Harness, mock_side_effects, tmp_path: Path, monkeypatch ): @@ -686,6 +722,7 @@ def test_planner_relation_changed_writes_flavor(monkeypatch: pytest.MonkeyPatch) state_mock.runner_config.openstack_image.tags = ["x64", "noble"] harness.charm._setup_state = MagicMock(return_value=state_mock) harness.charm._setup_service = MagicMock() + harness.charm._check_image_ready = MagicMock() harness.update_relation_data(relation_id, "planner-app/0", {"endpoint": "http://example.com"})