diff --git a/charms/garm/src/garm_api.py b/charms/garm/src/garm_api.py index 9370e802..d321864b 100644 --- a/charms/garm/src/garm_api.py +++ b/charms/garm/src/garm_api.py @@ -82,10 +82,9 @@ def _api_client(self) -> ApiClient: return ApiClient(configuration=Configuration(host=self._base_url)) def is_initialized(self) -> bool: - """Return True if GARM has already been initialised (first-run done). + """Return whether GARM has completed first-run initialisation. - GARM returns 409 Conflict on ``GET /controller-info`` until the initial - admin user has been created via ``POST /first-run``. + Probes ``GET /controller-info``, GARM's only auth-free first-run signal. Returns: True if GARM is initialised, False if it is waiting for first-run. @@ -99,8 +98,15 @@ def is_initialized(self) -> bool: api.controller_info(_request_timeout=_REQUEST_TIMEOUT) return True except ApiException as exc: + # 409 is GARM's init gate rejecting the probe until first-run + # creates the admin user; getting past it means initialised. if exc.status == 409: return False + if exc.status == 401: + # Auth is only enforced once first-run has created the admin + # user, so a 401 on this deliberately unauthenticated probe + # means the init gate already let the request through. + return True raise GarmApiError( f"Unexpected response from GARM controller-info ({exc.status}): {exc.body}" ) from exc diff --git a/charms/garm/tests/unit/test_garm_api.py b/charms/garm/tests/unit/test_garm_api.py index 50baaa0a..09df38c2 100644 --- a/charms/garm/tests/unit/test_garm_api.py +++ b/charms/garm/tests/unit/test_garm_api.py @@ -33,14 +33,16 @@ def _stub_api_client(client): [ (None, True), (ApiException(status=409), False), + (ApiException(status=401), True), ], - ids=["200-ok", "409-not-initialised"], + ids=["200-ok", "409-not-initialised", "401-initialised-unauthenticated-probe"], ) def test_is_initialized(side_effect, expected): """ arrange: GarmApiClient pointed at BASE_URL with a stubbed api_client. - act: Call is_initialized(); ControllerInfoApi raises ApiException(409) or succeeds. - assert: Returns True on success, False on 409. + act: Call is_initialized(); ControllerInfoApi raises ApiException(409/401) or succeeds. + assert: Returns True on success or 401 (auth enforced means already initialised), + False on 409. """ client = GarmApiClient(BASE_URL) with _stub_api_client(client): diff --git a/docs/changelog.md b/docs/changelog.md index a150d175..1ba6c29f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,7 @@ Each revision is versioned by the date of the revision. ## 2026-07-13 +- `garm`: prevent the config-changed hook from failing on an already-initialized GARM. The charm's first-run check probed GARM's `controller-info` endpoint without a token and treated the resulting `401` (returned once GARM is initialized and requires auth) as a fatal error that aborted the hook on any config or charm change that rewrote the workload config — for example, a charm upgrade. The probe now treats `401` the same as `200`: both mean GARM is past its init gate and already initialized. - `garm`: reliably apply proxy changes to the GARM workload. The charm gated the workload's Pebble layer rewrite on a hash of the on-disk config file, which excluded the proxy environment *values* — so changing or clearing a proxy value while the variable set stayed the same never rewrote the layer, leaving the old value in the plan. The on-disk file could also drift from the layer and wedge it permanently. The charm now compares the freshly rendered hash (which includes the proxy values) against the `config_hash` stored in the container's current Pebble plan, so proxy value changes and clears reliably replan the workload. ## 2026-07-12