Skip to content

Commit c059a59

Browse files
authored
fix(charm): check image readiness before starting runner manager service (#777)
Several event handlers (_on_upgrade_charm, _on_planner_relation_changed, _on_planner_relation_broken) called _reconcile() without first checking image readiness. This caused the runner manager service to start with empty runner combinations, erroring with "No runner combinations configured" instead of setting the charm to blocked/waiting status. Move _check_image_ready() into _reconcile() so all code paths that start the service are covered, and remove the now-redundant calls from individual handlers.
1 parent 1c08b2d commit c059a59

File tree

3 files changed

+42
-6
lines changed

3 files changed

+42
-6
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
This changelog documents user-relevant changes to the GitHub runner charm.
44

5+
## 2026-04-03
6+
7+
- 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."
8+
59
## 2026-03-30
610

711
- 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.

src/charm.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,6 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None:
354354
]
355355
flush_runners = True
356356

357-
self._check_image_ready()
358-
359357
self._reconcile(state)
360358

361359
if flush_runners:
@@ -389,6 +387,7 @@ def _reconcile(self, state: CharmState) -> None:
389387
Args:
390388
state: The charm state.
391389
"""
390+
self._check_image_ready()
392391
self._setup_service(state)
393392
self._update_planner_flavor(state)
394393

@@ -475,8 +474,6 @@ def _apt_install(self, packages: Sequence[str]) -> None:
475474
def _on_debug_ssh_relation_changed(self, _: ops.RelationChangedEvent) -> None:
476475
"""Handle debug ssh relation changed event."""
477476
self.unit.status = MaintenanceStatus("Added debug-ssh relation")
478-
self._check_image_ready()
479-
480477
state = self._setup_state()
481478
self._reconcile(state)
482479

@@ -498,8 +495,6 @@ def _on_image_relation_joined(self, _: ops.RelationJoinedEvent) -> None:
498495
def _on_image_relation_changed(self, _: ops.RelationChangedEvent) -> None:
499496
"""Handle image relation changed event."""
500497
self.unit.status = MaintenanceStatus("Update image for runners")
501-
self._check_image_ready()
502-
503498
state = self._setup_state()
504499
self._reconcile(state)
505500

tests/unit/test_charm.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,42 @@ def test_on_config_changed_openstack_clouds_yaml(mock_side_effects):
605605
assert harness.charm.unit.status == BlockedStatus("Please provide image integration.")
606606

607607

608+
@pytest.mark.parametrize(
609+
"handler_name, emit_event",
610+
[
611+
pytest.param(
612+
"_on_upgrade_charm",
613+
lambda h: h.charm.on.upgrade_charm.emit(),
614+
id="upgrade-charm",
615+
),
616+
pytest.param(
617+
"_on_planner_relation_changed",
618+
lambda h: h.charm._on_planner_relation_changed(MagicMock()),
619+
id="planner-relation-changed",
620+
),
621+
],
622+
)
623+
def test_reconcile_blocks_when_image_not_ready(
624+
harness: Harness, mock_side_effects, monkeypatch, handler_name, emit_event
625+
):
626+
"""
627+
arrange: Set up charm with no image integration.
628+
act: Fire an event that calls _reconcile without an explicit _check_image_ready call.
629+
assert: Charm enters blocked/waiting status and the service is stopped.
630+
"""
631+
harness.charm._setup_state = MagicMock()
632+
manager_client_mock = MagicMock(spec=GitHubRunnerManagerClient)
633+
harness.charm._manager_client = manager_client_mock
634+
mock_mgr_service = MagicMock()
635+
monkeypatch.setattr("charm.manager_service", mock_mgr_service)
636+
monkeypatch.setattr("charm.execute_command", MagicMock(return_value=(0, "Mock_stdout")))
637+
monkeypatch.setattr("charm.pathlib", MagicMock())
638+
639+
emit_event(harness)
640+
641+
assert harness.charm.unit.status.name == BlockedStatus.name
642+
643+
608644
def test_metric_log_ownership_for_upgrade(
609645
harness: Harness, mock_side_effects, tmp_path: Path, monkeypatch
610646
):
@@ -686,6 +722,7 @@ def test_planner_relation_changed_writes_flavor(monkeypatch: pytest.MonkeyPatch)
686722
state_mock.runner_config.openstack_image.tags = ["x64", "noble"]
687723
harness.charm._setup_state = MagicMock(return_value=state_mock)
688724
harness.charm._setup_service = MagicMock()
725+
harness.charm._check_image_ready = MagicMock()
689726

690727
harness.update_relation_data(relation_id, "planner-app/0", {"endpoint": "http://example.com"})
691728

0 commit comments

Comments
 (0)