Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions charms/garm/src/garm_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions charms/garm/tests/unit/test_garm_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading