From 9223bd60d945efbb7e5e09b962c142f35238a4e6 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 13 Jul 2026 09:01:53 +0000 Subject: [PATCH 1/6] fix(garm): treat 401 from controller-info as initialised is_initialized() probes GET /controller-info without auth. GARM gates that endpoint on two middlewares: a 409 until first-run has created the admin user, then JWT auth. On an already-initialised GARM (e.g. after a charm upgrade with persisted PostgreSQL), the unauthenticated probe passes the init gate and hits the auth gate, returning 401, which was being raised as a fatal GarmApiError and failing the config-changed hook. 401 now means initialised, same as 200; only 409 means not-yet-initialised. --- charms/garm/src/garm_api.py | 9 ++++++++- charms/garm/tests/unit/test_garm_api.py | 8 +++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/charms/garm/src/garm_api.py b/charms/garm/src/garm_api.py index 9370e802..689ac98e 100644 --- a/charms/garm/src/garm_api.py +++ b/charms/garm/src/garm_api.py @@ -85,7 +85,9 @@ def is_initialized(self) -> bool: """Return True if GARM has already been initialised (first-run done). GARM returns 409 Conflict on ``GET /controller-info`` until the initial - admin user has been created via ``POST /first-run``. + admin user has been created via ``POST /first-run``. Once initialised, + the endpoint requires auth, so this deliberately unauthenticated probe + gets back either 200 or 401 — both mean initialised. Returns: True if GARM is initialised, False if it is waiting for first-run. @@ -101,6 +103,11 @@ def is_initialized(self) -> bool: except ApiException as exc: 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): From 97a6feab506d5fd1af9bbe295b32023a37b6c6be Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 13 Jul 2026 09:22:27 +0000 Subject: [PATCH 2/6] docs(changelog): note garm controller-info 401 fix --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index a150d175..6a7ac78b 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`: stop the config-changed hook from erroring on an already-initialised 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 initialised and requires auth) as a fatal error, failing the hook on any config or charm change that rewrote the workload config — e.g. a charm upgrade. The probe now treats `401` the same as `200`: both mean GARM is past its init gate and already initialised. - `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 From 9be5fc30c6c8ef27f7b0e98201a105b1c85aad82 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 13 Jul 2026 09:26:05 +0000 Subject: [PATCH 3/6] docs(changelog): use 'for example' instead of 'e.g.' --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 6a7ac78b..4f2fb3b7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,7 +10,7 @@ Each revision is versioned by the date of the revision. ## 2026-07-13 -- `garm`: stop the config-changed hook from erroring on an already-initialised 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 initialised and requires auth) as a fatal error, failing the hook on any config or charm change that rewrote the workload config — e.g. a charm upgrade. The probe now treats `401` the same as `200`: both mean GARM is past its init gate and already initialised. +- `garm`: stop the config-changed hook from erroring on an already-initialised 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 initialised and requires auth) as a fatal error, failing 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 initialised. - `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 From 5cffd64cb4b4965740848ade3f2117de7f54cc1f Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 13 Jul 2026 09:29:53 +0000 Subject: [PATCH 4/6] docs(garm): clarify is_initialized docstring on 401 vs 200 --- charms/garm/src/garm_api.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/charms/garm/src/garm_api.py b/charms/garm/src/garm_api.py index 689ac98e..ded3e594 100644 --- a/charms/garm/src/garm_api.py +++ b/charms/garm/src/garm_api.py @@ -84,10 +84,12 @@ def _api_client(self) -> ApiClient: def is_initialized(self) -> bool: """Return True if GARM has already been initialised (first-run done). - GARM returns 409 Conflict on ``GET /controller-info`` until the initial - admin user has been created via ``POST /first-run``. Once initialised, - the endpoint requires auth, so this deliberately unauthenticated probe - gets back either 200 or 401 — both mean initialised. + GARM's init gate returns 409 Conflict on ``GET /controller-info`` until + the initial admin user has been created via ``POST /first-run``. Past + that gate the endpoint is auth-gated, so this deliberately unauthenticated + probe sees 401 once GARM is initialised. Both 401 and 200 therefore mean + the init gate has been passed (initialised); only 409 means first-run is + still pending. Returns: True if GARM is initialised, False if it is waiting for first-run. From ae6fa58b706979f914f5d69979b0c35fdc845608 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 13 Jul 2026 09:33:33 +0000 Subject: [PATCH 5/6] docs(changelog): use US spelling to satisfy spellcheck gate --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 4f2fb3b7..1ba6c29f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,7 +10,7 @@ Each revision is versioned by the date of the revision. ## 2026-07-13 -- `garm`: stop the config-changed hook from erroring on an already-initialised 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 initialised and requires auth) as a fatal error, failing 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 initialised. +- `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 From 6b203f44bb267148ae0ea29942fc019aaa799002 Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Mon, 13 Jul 2026 10:45:10 +0000 Subject: [PATCH 6/6] docs(garm): trim is_initialized docstring to its interface --- charms/garm/src/garm_api.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/charms/garm/src/garm_api.py b/charms/garm/src/garm_api.py index ded3e594..d321864b 100644 --- a/charms/garm/src/garm_api.py +++ b/charms/garm/src/garm_api.py @@ -82,14 +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's init gate returns 409 Conflict on ``GET /controller-info`` until - the initial admin user has been created via ``POST /first-run``. Past - that gate the endpoint is auth-gated, so this deliberately unauthenticated - probe sees 401 once GARM is initialised. Both 401 and 200 therefore mean - the init gate has been passed (initialised); only 409 means first-run is - still pending. + 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. @@ -103,6 +98,8 @@ 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: