Skip to content

Preserve the ARC retirement fence while migrating it to ValidatingPolicy #5834

Open

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

Audience and problem

Whoever retires or recovers the ARC runner pool. The fence is what stops a Flux reconcile or a manual apply from recreating the release, or reopening runner admission, while a retirement is in progress. A migration that changes its decision even in one phase either blocks a legitimate recovery step or lets a retired pool come back. The existing cases feed the Namespace through a JMESPath apiCall context. A ValidatingPolicy reads it through CEL resource.Get instead, so the harness must give the new kind the same Namespace responses, or the old expectations prove nothing about it.

Hypothesis

If the existing engine cases run unchanged in their expected results against a ValidatingPolicy that reads the Namespace through CEL resource.Get, with the same Namespace responses supplied to it, then any decision the rewrite changes shows up as a failing case before deploy, not as a blocked recovery.

Success signal

  • scripts/tests/test-arc-retirement-fence.sh and the test(kyverno): pin the ARC listener and retirement policies before their ValidatingPolicy rewrite #5031 fixtures pass against the new kind with unchanged expected results, and a deliberate mutation of the new policy turns at least one case red (the harness is not vacuous for the new kind).
  • The ValidatingPolicy passes the same test unchanged in its expected results, keeps Deny, the same match scope and failurePolicy: Fail, and replaces the ClusterPolicy in the same PR.
  • After deploy, a read-only check shows the new policy ready and no ClusterPolicy of that name.

Guardrail: an unreadable Namespace must still deny (fail closed), as it does today through failurePolicy: Fail.

Smallest useful change

  1. Spike: confirm how the pinned Kyverno CLI (1.19.2) supplies a Namespace to a ValidatingPolicy's resource.Get (the apiCallResponses mechanism, or another context input), and how request.oldObject and user groups reach it. Record the answer here.
  2. Migration PR: the ValidatingPolicy and deletion of the ClusterPolicy, with the same tests pointed at the new kind.

Activity

  1. added theissue type on Nov 3, 2026
  2. moved this to 📥 Backlog in 🌊 Project Boardon Nov 3, 2026
  3. devantler commented on Nov 10, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer

    Folding in #6026, which was filed for the same policy on 10 November and is closed as a duplicate of this issue. What it added:

    • Production read, 10 November ~17:25Z: restrict-arc-retirement is one of the two Migrate the tenant and organisation validation ClusterPolicies to ValidatingPolicy #4995 policies still deployed as a ClusterPolicy. The arc-runners Namespace carries no retirement journal, so most rules are inactive today; the guard still has to work once a retirement starts.
    • Extra acceptance points: confirm at the running Kyverno version that a ValidatingPolicy can express each rule (the Namespace lookup, the Namespace DELETE rule and the system:masters check), and record any rule it cannot express on Migrate Kyverno ClusterPolicy and Policy before Kyverno 1.20 removes them #4984 so it blocks only that rule; keep Enforce, the same match scope and failurePolicy: Fail as recorded in tests/policy-failure-actions.json; add the replacement and delete the ClusterPolicy in the same change, then confirm after deploy that only the new policy remains.
  4. devantler commented on Nov 11, 2026

    @devantler
    ContributorAuthor

    🤖 Generated by the Agentic Engineer (interactive Claude Code session)

    Spike (step 1) result, at Kyverno CLI and engine 1.19.2. Short version: the CLI can feed the Namespace to a ValidatingPolicy, but it cannot feed a previous object, and the retirement tooling depends on the ClusterPolicy's kind and denial format. So the migration is bigger than one PR. I measured each point with small throwaway policies run through kyverno test:

    1. Namespace lookup. A Context file (context: in the Test, kind: Context, spec.resources) answers resource.Get('v1','namespaces','','arc-runners'), and it works inside matchConditions too. json.unmarshal parses the journal. A bad journal comes back as an error row, and a false match condition as skip, the same results the harness expects today.
    2. Lookup failures. A Context can only leave the Namespace out. The CLI then reports error (no such key: metadata). It cannot produce a 403 or a 500, so in the CLI the three lookup-failure cases become one "not found" case. The repository's offline engine driver (scripts/tests/tenant-webapp-admission, already used for the tenant and GitHub-team policies) can inject a refused read, so that is where those cases should live.
    3. request.oldObject. For a ValidatingPolicy, kyverno test makes the previous object a copy of the new one. Neither global nor per-resource request.oldObject values reach oldObject, and on DELETE it also leaves the old object without the values. The listener-cleanup, credential-rewrite, journal and namespace-delete cases all depend on a separate previous object, so they cannot stay kyverno test fixtures. They need the same offline engine driver, given an explicit oldObject and the user's groups (the driver has no groups field yet; the system:masters check needs one).
    4. User groups do reach the CEL request.userInfo.groups through a CLI UserInfo file.
    5. Coupling the issue does not list yet: scripts/retire-arc-pool.sh reads the live clusterpolicy restrict-arc-retirement and compares it field by field with the declared rules. Both it and scripts/retire-arc-pool/writer.go recognise a fence denial by the ClusterPolicy webhook name and by the restrict-arc-retirement: / <rule>: lines in the message. Its contract test also requires a rendered ClusterPolicy with eight rules. In the engine source (pkg/webhooks/resource/vpol/handler.go), a ValidatingPolicy denial reads Policy restrict-arc-retirement failed: <message>, with no rule name, and comes from the vpol.validate.kyverno.svc webhook family. So the migration also has to change those readers, or the tooling would refuse a real retirement. Each validation's message should name its old rule, so the denial stays tied to a specific fence.

    Suggested split: (a) add groups to the offline engine driver and port the existing ~75 engine cases to it, unchanged in their expected results and still running against today's ClusterPolicy where the CLI allows; (b) the ValidatingPolicy itself, with the cases pointed at it, the retirement tooling's readers moved to the new kind and denial format, tests/policy-failure-actions.json and the fixture-evaluation self-test (which uses these fixtures as its examples) updated, and the ClusterPolicy deleted in the same change. The after-deploy check is still open: kubectl get validatingpolicy restrict-arc-retirement reports ready and kubectl get clusterpolicy restrict-arc-retirement returns NotFound.

    I am not starting the migration in this session. The issue stays open.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions