fix(kanidm): remove the silently-discarded service-accounts block; correct the claim #267

Sammanfogat
supernaut sammanfogade 2 incheckningar från fix/kanidm-state-dead-service-accounts in i main 2026-07-30 21:04:56 +00:00
Ägare

kanidm-state.json.j2 declared a "service-accounts" block, and the role defaults stated "The
account + its group memberships are declared in kanidm-state.json.j2 so kanidm-provision manages
them."

Half of that was false. The block was dead. Verified against upstream v1.3.0 src/state.rs:

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct State {
    pub groups: HashMap<String, Group>,
    pub persons: HashMap<String, Person>,
    pub systems: Systems,
}

Three fields, and no #[serde(deny_unknown_fields)] — so serde parsed "service-accounts", found
no matching field, and discarded it without a warning. Service-account support is upstream PR #29,
unreviewed for 12 months.

The template's own comment already flagged it as unresolved — "⚠️ VERIFY the exact key name
service-accounts and the object shape against kanidm-provision's schema"
. That verification was
never done, and the answer turns out to be "it does not exist".

What is actually true

Managed?
The service account's group memberships Yes — _sa_group_members merges it into the groups block
The account object itself No — manual bootstrap, created by hand with its API token

This is the same shape of latent gap as the entry-managed-by delegation that broke sign-up for 13
days: something assumed declarative that never was. Better removed than left looking managed.

Two things now documented in the runbook

  • Rebuild-ordering hazard. Because groups names the account as a member, a fresh host must have
    the service account created before this role provisions, or those member writes reference a
    nonexistent entity. Full rebuild sequence added (recover idm_admin → create SA + token →
    set-entry-manager → apply → drill).
  • "present": false deletes regardless of --no-auto-remove. That flag only gates removal of
    orphans — entities dropped from the state file. Nothing here uses present: false today; the note
    exists so nobody reaches for it expecting the flag to protect them.

Verification

Rendered the template offline with representative vars (ansible's bundled Jinja2 + PyYAML):

rendered state: VALID JSON
  top-level keys   : ['groups', 'persons', 'systems']
  service-accounts : False
  groups declared  : 16
  'gitborg-web-provision' member of : ['idm_bitborg_ent_managers', 'idm_people_admins',
                                       'idm_people_on_boarding']

Top-level keys now match upstream's struct exactly, so nothing is silently dropped any more, and the
memberships that are managed still are.

--syntax-check passes; ansible-lint clean at production; markdownlint clean.

Note: an earlier attempt to verify this from the --check --diff output failed, because a unified diff
contains only changed hunks — parsing its + lines as standalone JSON cannot work. Rendering the
template is the correct test.

`kanidm-state.json.j2` declared a `"service-accounts"` block, and the role defaults stated *"The account + its group memberships are declared in kanidm-state.json.j2 so kanidm-provision manages them."* **Half of that was false. The block was dead.** Verified against upstream v1.3.0 `src/state.rs`: ```rust #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct State { pub groups: HashMap<String, Group>, pub persons: HashMap<String, Person>, pub systems: Systems, } ``` Three fields, and **no `#[serde(deny_unknown_fields)]`** — so serde parsed `"service-accounts"`, found no matching field, and discarded it without a warning. Service-account support is upstream PR #29, unreviewed for 12 months. The template's own comment already flagged it as unresolved — *"⚠️ VERIFY the exact key name `service-accounts` and the object shape against kanidm-provision's schema"*. That verification was never done, and the answer turns out to be "it does not exist". ## What is actually true | | Managed? | | --- | --- | | The service account's **group memberships** | **Yes** — `_sa_group_members` merges it into the `groups` block | | The **account object** itself | **No** — manual bootstrap, created by hand with its API token | This is the same shape of latent gap as the `entry-managed-by` delegation that broke sign-up for 13 days: **something assumed declarative that never was.** Better removed than left looking managed. ## Two things now documented in the runbook - **Rebuild-ordering hazard.** Because `groups` names the account as a member, a fresh host must have the service account created **before** this role provisions, or those member writes reference a nonexistent entity. Full rebuild sequence added (recover idm_admin → create SA + token → `set-entry-manager` → apply → drill). - **`"present": false` deletes regardless of `--no-auto-remove`.** That flag only gates removal of *orphans* — entities dropped from the state file. Nothing here uses `present: false` today; the note exists so nobody reaches for it expecting the flag to protect them. ## Verification Rendered the template offline with representative vars (ansible's bundled Jinja2 + PyYAML): ``` rendered state: VALID JSON top-level keys : ['groups', 'persons', 'systems'] service-accounts : False groups declared : 16 'gitborg-web-provision' member of : ['idm_bitborg_ent_managers', 'idm_people_admins', 'idm_people_on_boarding'] ``` Top-level keys now match upstream's struct exactly, so nothing is silently dropped any more, and the memberships that *are* managed still are. `--syntax-check` passes; `ansible-lint` clean at `production`; `markdownlint` clean. Note: an earlier attempt to verify this from the `--check --diff` output failed, because a unified diff contains only changed hunks — parsing its `+` lines as standalone JSON cannot work. Rendering the template is the correct test.
supernaut lade till 2 incheckningar 2026-07-30 20:45:31 +00:00
fix(kanidm): detect real changes, and make a failed build retry instead of hiding
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m28s
93d4c0a752
Three fixes in the kanidm-provision path, all found by researching upstream after
#265 landed.

1. changed_when: false was needlessly blind — a change signal DOES exist

#265 set `changed_when: false` on the reasoning that the tool emits no change
signal. That conclusion came from grepping the compiled binary for Created /
Updated / Modified — PAST TENSE. The tool uses GERUNDS. The grep could not match,
so a usable signal was missed.

Upstream reality: `update_entity_attrs()` wraps every mutation in
`if current_values != values`, so when values match, no request is sent and nothing
is printed. Only mutations emit a log_event, and only four verbs exist: Creating,
Updating, Deleting, Appending. None of the six unconditional phase headers contains
any of them, so a verb match is sound on v1.3.0 with no upstream change.

  changed_when: stdout is search('(Creating|Updating|Deleting)')

`Appending` is deliberately excluded. In append mode the comparison is against the
DECLARED members only, and every group here sets overwriteMembers:false — so any
group whose real membership is a superset of what we declare (idm_people_admins,
where we declare the service account but Kanidm also holds idm_admin) compares
unequal forever and logs Appending on every run. Same for the "Tracking provisioned
entities" step, which appends to ext_idm_provisioned_entities. Matching it would
pin changed:true permanently — recreating the bug #265 fixed.

Unanchored `search` on purpose: log_event colourises unconditionally (no
supports_color check), so ANSI escapes are present even when stdout is a pipe, and
the {:>12} padding is computed over the escape-laden string.

2. A failed build was invisible on retry

The Containerfile is staged BEFORE the build and `is changed` was the rebuild
trigger, so it is true only on the run that stages it. If that build then failed —
a Rust compile on a 2 vCPU / 4 GB host, and the Containerfile itself warns a link
step can be OOM-killed — the next apply found the staged file unchanged AND the old
image still tagged, silently skipped the build, and provisioned against the stale
image. A failure did not self-heal and became invisible on retry: the same shape as
the merged-but-unapplied Caddyfile in #250.

Now the trigger compares the staged Containerfile against a `.built-Containerfile`
marker written only AFTER the build task succeeds. A crashed build leaves the
mismatch, so the next run retries.

The marker is written on `is not skipped`, not `is changed`: podman_image reports ok
(not changed) when the tag exists and needs no rebuild, which is the steady state —
gating on `is changed` would never write the marker and leave the gate open forever.

3. The whole provisioning block was invisible to --check

`_kanidm_provision_ready` depends on a `uri` reachability probe, and `uri` does not
run under --check. So the build context, Containerfile staging, image build, state
render and provisioning run were ALL skipped in every dry-run. That is how #265's
base-image change reached production without appearing in a single --check: the
dry-run showed changed=1 (an unrelated artefact) while the real apply rebuilt an
image. Same trap as #257.

The probe and the image-exists check are read-only, so both now run under --check.
podman_image declares supports_check_mode, so the build reports what it would do
rather than compiling, and the provisioning `command` is still skipped — a dry-run
never mutates Kanidm.

Verified: `--check --tags kanidm` now shows the probe, image check, both stats and
the build as ok (previously all skipped), and the marker task as changed.

Also corrected the CARGO_BUILD_JOBS hint: `podman build -e` does not exist, so the
previous comment was not copy-pasteable. It needs ARG+ENV in the Containerfile or
--build-arg.
fix(kanidm): remove the silently-discarded service-accounts block; correct the claim
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m30s
7a3ae4d0bd
`kanidm-state.json.j2` declared a `"service-accounts"` block, and the role defaults
stated "The account + its group memberships are declared in kanidm-state.json.j2 so
kanidm-provision manages them."

Half of that was false. The block was DEAD. Verified against upstream v1.3.0
`src/state.rs`:

  #[derive(Debug, Deserialize)]
  #[serde(rename_all = "camelCase")]
  pub struct State { pub groups: …, pub persons: …, pub systems: … }

Three fields, and no `#[serde(deny_unknown_fields)]` — so serde parsed
`"service-accounts"`, found no matching field, and discarded it without a warning.
Service-account support is upstream PR #29, unreviewed for 12 months.

The template's own comment already flagged this as unresolved: "⚠️ VERIFY the exact
key name "service-accounts" and the object shape against kanidm-provision's schema".
That verification was never done, and the answer was "it does not exist".

What is actually true:

- The service account's GROUP MEMBERSHIPS are managed — _sa_group_members merges it
  into the `groups` block, and the rendered state declares gitborg-web-provision as a
  member of idm_people_on_boarding, idm_people_admins and idm_gitborg_ent_managers.
- The ACCOUNT OBJECT is not. It is a manual bootstrap, created by hand at the same
  time as its API token.

This is the same shape of latent gap as the entry-managed-by delegation that broke
sign-up for 13 days: something assumed declarative that never was. Worth removing
rather than leaving in place looking managed.

Also documents two things in the runbook:

- A rebuild-ordering hazard: because `groups` names the account as a member, a fresh
  host must have the service account created BEFORE this role provisions, or those
  member writes reference a nonexistent entity. Full rebuild sequence added.
- `"present": false` DELETES regardless of `--no-auto-remove` — that flag only gates
  removal of orphans (entities dropped from the state file). Nothing here uses
  present:false today; the note exists so nobody reaches for it expecting protection.

Verified by rendering the template offline with representative vars: the output is
valid JSON, its top-level keys are exactly groups/persons/systems (matching upstream's
struct, so nothing is silently dropped any more), `service-accounts` is gone, and the
service account is still declared as a member of the three privilege groups.
supernaut tvångsskickade fix/kanidm-state-dead-service-accounts från 7a3ae4d0bd
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m30s
till fbd691a515
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m34s
2026-07-30 20:59:20 +00:00
Jämför
supernaut sammanfogade incheckning cbfaa0c44c till main 2026-07-30 21:04:56 +00:00
supernaut tog bort grenen fix/kanidm-state-dead-service-accounts 2026-07-30 21:04:56 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-infra!267
Ingen beskrivning angiven.