Repository navigation
Conversation
e8c06cb to
3064fde
Compare
…t is gone Removing a garm-configurator relation built GARM_PROVIDERS_JSON from the relations that remained, so the removed configurator's provider was dropped and GARM replanned without it before the drain started. GARM fixes its provider map per process, so the orphaned scaleset then had no scaleset worker to purge its instances and no provider worker to tear a VM down, while GARM refuses to delete a runner in the terminal `deleted` status or a scaleset that still lists runners. The teardown could never finish. restart() now runs the provider lifecycle in one pass: read the recorded set, add every provider the relations publish, start GARM with it, reconcile scalesets, then trim and re-record what no relation and no surviving scaleset names. The record is the charm's only knowledge of a departed configurator's credentials - Juju hides its unit from relation-departed onwards - so a record that cannot be read or written raises out of the hook instead of yielding a guessed set: publishing from an incomplete record, or running a set the record does not hold, is exactly what strands a scaleset. A reconcile pass that cannot establish which scalesets GARM holds trims nothing. The trim takes effect at the following replan, so a retirement does not restart GARM twice. Refs: ISD-7262
3064fde to
f8613b4
Compare
cbartz
left a comment
There was a problem hiding this comment.
The design is sound: keeping a departed configurator's provider published until its last scaleset has drained is the right fix, and failing safe on an unknown scaleset state is the right default. One gap needs to be closed before merge.
To address before merge
- Removing the last configurator bypasses the record. When no relation publishes provider data,
restart()returns before_published_providersreads the record. The drain then only works while the old Pebble layer survives: if the pod is recreated mid-drain,pebble-readyre-runsrestart(), which returns early again, so no provider is republished and the scaleset deadlocks as before. The record is also never trimmed, so the departed provider's credentials stay forever and are republished as soon as a new configurator relates. Suggest taking the early return only when there are no relations and the record is empty; otherwise publish the recorded set and trim as usual (with nothing related, the existing trim keeps the provider while its scaleset survives and drops it afterwards). Please add a unit test for this path. - Upgrade note. Please add a short note for operators (release notes or the upgrade docs) that deployments already stuck do not recover on upgrade, and are recovered by relating the same configurator again, as the description explains.
Suggestions
- Consider running the GARM E2E test from this branch before merge: the unit tests mock GARM, so they cannot show a real GARM purging the runners and deleting the scaleset once the provider is retained. Your call.
- Each change to the set writes a new revision of
garm-providers, and old revisions (holding old passwords) are never removed. Consider pruning superseded revisions afterset_content. - The hard fail on an unreadable/unwritable record is fine as is — a visible error beats a silently stranded scaleset.
Nits
get_content(refresh=True)on the charm's owngarm-providerssecret: the repo convention for owned secrets is plainget_content(), orpeek_content()to read the latest revision without changing tracking (which also covers non-leader units)._PROVIDER_RECORD_FIELDSduplicatesgarm_entrypoint._PROVIDER_REQUIRED_FIELDS, which lives in the samesrc/and could be imported, so the two can't drift._referenced_providersreturnsNoneas soon as one surviving scaleset has no provider name; if that scaleset persists, trimming is disabled indefinitely with only a per-hook warning. Fine as the safe default, but worth an explicit note or a clearer log line that says trimming stays off until it's gone.ISD-7262is referenced in code comments, the secret description andAGENTS.md. The tracker isn't public, so outside readers can't follow it; the surrounding text already explains the concern, so the id can be dropped from shipped text (it's fine in the PR title/commit).- Several docstrings carry design rationale and the bug's history (
ProviderRecordError,restart(),_read_provider_record,_trimmed_providers). Consider keeping docstrings to the interface and moving the rationale into one concise comment at the point of use (or theAGENTS.mdentry, which already has it). _by_provider_nameis a one-line wrapper aroundsorted; inlining it at its two call sites reads just as clearly.
AI-generated message.
- Publish the recorded provider set even when no relation publishes provider data, so a pod replaced mid-drain still starts GARM with the providers that drain needs. The waiting return now needs no related providers AND an empty record; a configurator that is related but silent stays a waiting state, so the sweep cannot prune the scalesets it is about to describe. - Read the record with peek_content() (latest revision, no tracking) and remove the revision each write supersedes: the leader is the only writer and every reader peeks, so no unit depends on an older revision. - Import PROVIDER_REQUIRED_FIELDS from the entrypoint instead of duplicating it, inline the one-line ordering helper, and keep docstrings to the interface. - Say in the log that a scaleset naming no provider keeps trimming off until it is gone, and drop the internal tracker id from shipped text. - Release note: stuck deployments do not recover on upgrade; relate the same configurator again to republish the provider and resume the drain. - Tests: the last-configurator path across a replaced pod, and the record being emptied once its last scaleset is gone.
|
Thanks for the review — all points are addressed in 1. Last configurator bypassing the record — fixed, with one extra condition Agreed, this was a real gap: the early return skipped the record entirely, so a replaced pod re-published nothing, and the record was never trimmed. The gate is now
Two tests cover it: 2. Upgrade note — added to 3. E2E — dispatched on this branch: https://github.com/canonical/github-runner-operators/actions/runs/39718034089. I will report the result. 4. Pruning superseded revisions — done, now that the single-writer assumption holds. The leader is the only writer and every reader uses 5–10. Nits — all applied:
Unit tests: 460 pass (with |
Replaces the copied-then-updated mapping with one literal, so the published set is a new value rather than an edited one.
What this PR does
Lets the operator pin the OpenStack security group a GARM scale set's runners are launched in, by passing an ID or name straight through to the provider.
garm-configurator: new optionalsecurity-groupconfiguration. It is published on thegarm-configuratorrelation assecurity_group, likeimage_visibility.garm: readssecurity_groupfrom the configurator's databag and puts it in the scale set'sextra_specs.security_groups, whichgarm-provider-openstack(v0.1.5, already pinned in the rock) maps to the instance port. A change is picked up by the existing drift detection, so the scale set is updated in place.Why we need it
Runners in one OpenStack project share its default security group, so they can reach each other, and a compromised job can attack its peers (ISD-7035). Pinning a locked-down group isolates them. The group itself stays the operator's to create and manage: the charms only pass the reference through.
This is the minimal version of ISD-7035. It does not create or converge the group. #436 manages a group's lifecycle from the charms; it is a separate, larger change and this PR neither builds on it nor modifies it. If both land,
security-groupcan name the group #436 manages.Test plan
tox -c tox.toml -e unit, per charm):garm454 passed,garm-configurator66 passed. The new tests assert the configured group reaches the provider:test_create_sends_security_group_as_an_extra_spec— the create call carriesextra_specs == {"security_groups": ["sg-runner"]}.test_update_converges_security_group— a changed or removed group converges in one update instead of re-updating on every reconcile.test_security_group_follows_the_configurator_databag— the databag value lands on the scale set spec, and an absent value stays unset for an older configurator.test_security_group_written_to_garm_configurator_relation— the charm config reaches the databag, empty when unset.fmt,lint,complexity,static: clean for both charms.tox -e garm-e2eagainst a tenant would be the check.Review focus
str(charm.config.get(..., "")).strip()rather than_get_optional_string_config(...)plusor "":ScalesetConfig.from_charmand_configure_garm_relationboth sit exactly at the cognitive-complexity limit, and either form pushes one of them over. That is why the field defaults to""instead ofNone.security_groups), matchinggarm-provider-openstack, but the configurator takes one group. A list-valued option is the obvious next step if operators need more than one.Potential breaking changes
None. The option is optional; while unset the charm publishes an empty
security_groupand the scale set's extra specs are unchanged, so the provider keeps attaching the project's default group.New dependencies, APIs, modules, or workflow changes
garm-configuratorsecurity-group.security_groupon thegarm-configuratorrelation.security_groupsis an existinggarm-provider-openstackextra spec.Checklist
Changes comply with the project's coding standards and guidelines (see CONTRIBUTING.md and STYLE.md)
CONTRIBUTING.mdhas been updated upon changes to the contribution/development process (n/a: no process change)Technical author has been assigned to review the PR in case of documentation changes (usually *.md files)
I updated
docs/changelog.mdwith user-relevant changesI used AI to assist with preparing this PR
I added or updated tests as needed (unit and integration)
If integration test modules are used: (n/a: no integration module change)
If this PR involves a Grafana dashboard: (n/a)
If this PR involves Terraform: (n/a: the module takes a generic config map)
If this PR involves Rockcraft: (n/a)
If this PR adds/removes a charm, or changes a charm's base class, conventions, tooling, or repo structure: (n/a)
If this PR changes
.copilot-collections.yamlor.github/instructions/: (n/a)GARM E2E dispatched on this branch: https://github.com/canonical/github-runner-operators/actions/runs/39718034089.