fix(garm): make teardown lifecycle safe - #333
Conversation
1510c3a to
4992b8d
Compare
Lifecycle funnel safety auditI audited the current funnel against the unpatched Safety criterion: when
GARM-owned hooks
Inherited
|
| Hook | Unpatched operation | Teardown risk before patch | Patched active behavior | Decision |
|---|---|---|---|---|
config-changed |
Build state, then restart(). |
State/config/relation reads occur before restart(). |
Routes through GARM normal reconcile. | Safe. |
secret-changed |
Build state, then restart(). |
Secret and relation reads occur before restart(). |
Routes through GARM normal reconcile. | Safe. |
secret-storage-relation-changed |
Build state, then restart(). |
Peer state is read before the restart gate. | Routes through GARM normal reconcile. | Safe. |
secret-storage-relation-departed |
Build state, then restart(). |
Peer relation may be departing before state construction. | Routes through GARM normal reconcile. | Safe. |
PostgreSQL database-created |
Build state, then restart(rerun_migrations=True). |
PostgreSQL state is read before the gate. | Uses a separate gated migration path. | Safe; migration flag preserved. |
PostgreSQL endpoints-changed |
Same as database-created. |
Same PostgreSQL state-read risk. | Uses the same gated migration path. | Safe; migration flag preserved. |
PostgreSQL relation-broken |
Build state, then restart(). |
The relation is no longer valid. The data-platform helper documents that fetch_relation_data() cannot be used in relation-broken; its legacy PostgreSQL fetch path assumes a relation exists (data_interfaces.py:1696-1712, 2649-2653). |
Gate runs before the decorator. | Necessary and safe. |
Ingress ready |
Build state, then restart(). |
Late ingress event can arrive after relation/state teardown starts. | Routes through GARM normal reconcile. | Safe for the base callback. |
Ingress revoked |
Build state, then restart(). |
Same late relation/state risk. | Routes through GARM normal reconcile. | Safe for the base callback. |
App pebble-ready |
Build state, then restart(). |
A queued event may arrive after teardown starts. | Routes through GARM normal reconcile. | Safe for the base callback. |
rotate-secret-key action |
Build state, reset the peer secret, report success, then restart. | It could mutate peer state during teardown. | Rejects before calling the base decorator; delegates to base while active. | Safe if teardown must reject rotation. |
update-status base observer |
Build state, retry failed migrations, then call _ingress._publish_auto_data(). |
Ingress refresh can read/write relation data even if restart() returns. |
Calls base only while active. | Necessary and safe. |
Evidence and retained operations
- Base observer registration:
paas_charm/charm.py:195-225. - Decorator behavior:
paas_charm/charm_utils.py:36-71. - Framework state reconstruction:
paas_charm/charm.py:748-784. - Config validation errors:
paas_charm/charm_state.py:126-132andpaas_charm/charm.py:466-491. - PostgreSQL relation conversion:
paas_charm/databases.py:42-76. - PostgreSQL
relation-brokenhas no known remote units:ops/charm.py:775-789. planned_units()is local goal state current at hook start:ops/model.py:459-479.- Current GARM router and overrides:
charms/garm/src/charm.py:155-257. - Current GARM restart sequence is unchanged while active; the only new behavior is the early teardown return:
charms/garm/src/charm.py:348-421. - Scenario tests patch
GarmCharm._create_charm_stateto fail and cover teardown events; the current suite passes 257 tests.
Explicit limits
This patch protects GARM-owned and paas-charm-owned callbacks. It does not intercept direct callbacks registered on helper objects:
- Traefik ingress:
charms/garm/lib/charms/traefik_k8s/v2/ingress.py:386-392. - Prometheus metrics:
charms/garm/lib/charms/prometheus_k8s/v0/prometheus_scrape.py:1594-1623. - Loki logging:
charms/garm/lib/charms/loki_k8s/v1/loki_push_api.py:2383-2400. - Grafana dashboards:
charms/garm/lib/charms/grafana_k8s/v0/grafana_dashboard.py:1187-1198. - Data-platform PostgreSQL raw relation callbacks:
charms/garm/lib/charms/data_platform_libs/v0/data_interfaces.py:1801-1826.
The patch also does not add a raw postgresql-relation-departed handler. Early runner cleanup and helper-library lifecycle changes remain separate follow-up work.
yhaliaw
left a comment
There was a problem hiding this comment.
Major changes needed
🤝 Human review with AI assistance.
7f9655f to
e5b5193
Compare
Use Juju planned units as the local teardown signal before the paas-charm state decorator runs. Keep inherited handlers inert and prevent update-status from refreshing ingress after teardown begins.
Cover database migration and inherited guard routes, and fail loudly when the pinned paas-charm hook contract changes. Add the PR-scoped teardown decision-tree architecture diagram.
e5b5193 to
42b9923
Compare
Summary
This PR makes GARM teardown lifecycle-safe by stopping normal reconciliation before
paas-charmcan reconstruct charm state when local Juju reportsapp.planned_units() == 0.paas-charmcallbacks behind the pre-state teardown gate.rerun_migrations=Truefor PostgreSQL events.rotate-secret-keybefore the inherited decorator can mutate state during teardown.paas-charmlifecycle hook contract changes.mainand validate againstpaas-charm==1.12.3.The architecture diagram is generated locally at
~/tmp/garm-teardown-lifecycle.htmland is intentionally not included in this PR.Related to #332.
Behavior
Active application (
planned_units() > 0)database-createdandendpoints-changedretain the migration rerun flag.update-statusandrotate-secret-keydelegate to the inherited behavior.Local teardown (
planned_units() == 0)_teardown()beforeblock_if_invalid_dataor normal GARM state construction.update-statusreturns before the inherited ingress refresh.rotate-secret-keyfails before state construction, secret reset, or restart.restart()retains a second defensive planned-unit check.The aliases are an adapter for the released
paas-charm1.12.x API._validate_paas_charm_hook_contract()checks the expected base hooks beforepaas-charmregisters observers, so an incompatible dependency fails instead of silently bypassing the gate.Test coverage
Validation
tox -c tox.toml -e unit— 323 passedtox -c tox.toml -e lint— passedtox -c tox.toml -e complexity— passedtox -c tox.toml -e static— passedtox -c tox.toml -e coverage-report— passedgit diff --check— passedScope and follow-ups
This change protects GARM and
paas-charmnormal reconciliation. It does not change runner cleanup, secret migration, raw relation teardown, or callbacks registered directly by third-party helper libraries. Those require separate lifecycle review and removal integration coverage.