Conversation
slauger
force-pushed
the
claude/openvox-operator-review-maptcd
branch
from
September 8, 2026 14:51
29b4b88 to
32025b2
Compare
slauger
force-pushed
the
claude/openvox-operator-review-maptcd
branch
from
September 9, 2026 09:52
efb5285 to
6fecc1b
Compare
Three CRDs play the same role -- a policy resource the Config controller renders into a ConfigMap or Secret -- but only ReportProcessor owned its status. SigningPolicy and NodeClassifier had theirs written by the Config controller, from config_autosign.go and config_enc.go respectively. Both now follow the ReportProcessor pattern: a status-only observer derives Ready from whether the resource actually reached the servers, so no controller writes into another resource's status any more. The Config controller keeps ownership of the rendered Secrets and reports rendering failures as events on the Config (AutosignPolicyRenderFailed, ENCRenderFailed), matching what it already did for the report webhook. updateSigningPolicyStatus and updateNodeClassifierStatus are gone. Rendered Secrets carry an openvox.voxpupuli.org/rendered-from annotation listing the resources their content was built from and the generation each had at the time. The observers match on that rather than re-parsing the rendered file, which matters in three ways: - A failed re-render leaves the previous Secret in place. Matching on a name -- or, for enc.yaml, on the endpoint URL -- still finds the resource there, so the new, unrendered generation would report as Active. That case is RenderedConfigStale. - enc.yaml carries no resource name at all, so any spec change that keeps spec.url, an auth rotation above all, would be invisible to the observer. - The rendered schema is not declared twice, once with yaml.v3 tags on the render side and once with json tags on the parse side, where a key rename would compile cleanly and flip every resource to NotRendered in production. Cases that had no status at all before, because the Config controller only wrote status on the resources it happened to render: NoConfig for a SigningPolicy whose CertificateAuthority no Config references, NotReferenced for an unreferenced NodeClassifier, and the two override reasons for a replaced built-in binary. The override is checked before the Secret is read, since a Secret rendered before the override was set still exists, and it only counts when every referencing Config sets it. Watches and lookups: - nodeClassifiersForConfig reads nodeClassifierRef off the event object rather than re-fetching the Config. Both the old and the new object of an update run through the map function, so repointing or clearing the field enqueues the classifier that lost its Config, not only the one that gained it. - The Secret watches key off the annotation, so an unrelated Secret that merely ends in -enc does not fan out over every NodeClassifier in the namespace. - A Config whose Secret cannot be read does not mask another Config that did render the resource, so the verdict does not depend on listing order. Where several Configs render the same classifier, one still on an earlier generation holds it at RenderedConfigStale. - ObservedGeneration is captured before the observation. updateStatusWithRetry re-reads the object, so a spec edit landing in between would stamp the new generation onto a verdict derived from the old spec. - Reference lookups use the registered field indexes, with a new spec.nodeClassifierRef index for Config. findSigningPolicies, the duplicated Config filters and the twin override helpers collapse into shared functions. No API or RBAC change: config/rbac/role.yaml and the chart's ClusterRole already grant both status subresources. The kubebuilder markers narrow to what the new controllers do, and the Config controller loses its now-unused signingpolicies/status and nodeclassifiers/status markers.
AGENTS.md is the name coding agents look for by convention; the singular file was picked up by nothing. Nothing in the repository referenced the old path, so the rename is self-contained. While renaming, spell out that commits, pull request descriptions and comments carry no Co-Authored-By line for an AI assistant, no "Generated with Claude Code" footer and no session links.
The status tests built their Secret fixtures by calling renderedFromAnnotation themselves, so reader and writer agreed by construction rather than by observation. A mutation run showed how little that proved: reconcileSecret could stop writing the annotation entirely -- or write it only on create, or record generation zero -- and the whole suite still passed. New tests observe what the Config controller actually produces: the ENC and autosign Secrets carry the expected name=generation pairs, only the policies of the CA being rendered, refreshed on every re-render rather than frozen at create; and both halves now run against each other, Config controller rendering first and the status controller reading what it wrote. Each of those mutants is killed by at least one of them. Two defects the review turned up, both now covered: - A Secret rendered before this mechanism existed carries no annotation at all, which is not the same as being rendered from nothing. Reading it as "this resource is not in it" flipped every SigningPolicy and NodeClassifier to NotRendered on upgrade, and for a paused Config -- whose reconcile returns before the render step -- it stayed there. Those now report RenderSourceUnknown until the Config controller re-renders. - A NodeClassifier whose Config rendered a *different* classifier reported "no Secret rendered from X exists yet", which is false; a Secret exists, it just belongs to someone else. The SigningPolicy observer already distinguished the two cases. Also tightened: ObservedGeneration is now proven to come from the generation captured before the observation, by serving a bumped generation from the read updateStatusWithRetry performs -- the previous assertion could not tell the two apart. Error subtests assert Ready=False and not just the reason, which 15 of them failed to notice before. The Secret watches' suffix filters, the malformed annotation values and the request namespace all have tests; the namespace assertion no longer uses the default namespace, where a hardcoded value would have passed. The docs claimed a failed re-render always yields RenderedConfigStale. That holds only when a spec edit triggered it: a credential rotated underneath an unchanged spec leaves the generation untouched, so the resource keeps reporting Active while the old rendered file stays in effect. Both reference pages now say so and point at the Config's render-failure events, which is where that case is visible. This is long-standing behaviour, not new here -- the previous code also returned before writing status on a render error.
slauger
force-pushed
the
claude/openvox-operator-review-maptcd
branch
from
September 9, 2026 20:50
32b6608 to
a1d8135
Compare
The verdict ranked a current render above a Secret rendered from a different classifier and above one recording no source at all, while ranking a stale render below it. Those three are the same statement -- a server is running something that is not this generation -- so the ordering contradicted the rule it stated one line above, and one Config that renders cleanly hid the others. The case is persistent rather than a startup race. Two Configs reference the same classifier; the second fails its reconcile before the ENC step, so its Secret keeps whatever it held before. The classifier reported Ready=True and named only the healthy Secret in its message, leaving nothing to point at the Config that was actually stuck. A Secret that exists and does not match the current generation now holds the resource out of Ready whichever of the three forms it takes, and the message names it. A Config with no Secret at all still does not hold anything back: nothing is mounted there, so it contradicts nothing. Also from the review: - docs/reference/index.md still carried the claim the two reference pages had already been corrected for: that a failed re-render always yields RenderedConfigStale. It holds only when a spec edit caused the failure. The shared page now says so, and covers RenderSourceUnknown, which it omitted. - docs/reference/nodeclassifier.md stated the multi-Config rule for stale renders only, which read as a guarantee that Ready means every referencing Config is current. - The comment justifying requireErrorCondition's hardcoded condition type claimed every readiness condition is named "Ready". ConditionCAReady and ConditionConfigReady are not; the two types it is used on are.
…des errors Three independent reviews of this branch converged on the same two gaps. ReportProcessor is the third resource of this kind and was left matching by endpoint name alone, which is exactly what the rest of this branch argues against: a spec edit whose re-render fails leaves the previous file in place, the name still matches, and the processor reports Active while the servers post to the old endpoint. It now carries the rendered-from annotation and reads it like its two siblings, so all three answer the same question the same way and the last hand-rolled parser of a rendered file is gone. It also picks up the generation-captured-before-observation fix the other two got, and a test for it. A deliberate override is not a fault. Setting autosignCommand or externalNodesCommand replaces the built-in binary on purpose, and reporting the bypassed resource as phase Error made a supported configuration -- one the charts exercise in CI, see charts/openvox-stack/ci/puppet-command-overrides -values.yaml -- look permanently broken in the Phase column. Both CRDs gain a Disabled phase for that case. Ready stays False, because the resource genuinely is not in effect. Two more from the same reviews: - The Configs come back from the field index in map iteration order, so joining their Secret names into the condition Message made it a non-deterministic function of unchanged state. Every reordering was a real status write that re-enqueued this controller and fanned a Config re-render out behind it. The slices are sorted before they reach a message, which is what the render side already does. - The upgrade path -- adopting a Secret the previous version wrote, without the annotation -- was the most upgrade-critical behaviour in the branch and had no test. It does now, including the Config controller stamping the source and the resource going Active afterwards. Docs: docs/getting-started/installation.md gains an upgrade section covering the transient RenderedConfigSourceUnknown window, the paused-Config case, the renamed reason strings and the new Disabled phase. The reference pages get a reason table for ReportProcessor, which had none, and the shared mechanism on the index page is now true of all three resources rather than only two.
The rendered-file parsers were the only direct users of sigs.k8s.io/yaml. Reading the rendered-from annotation instead removed the last one, so the dependency moves to indirect and the CI tidy check goes green again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SigningPolicy and NodeClassifier reported a
Readystatus that could be wrong and stay wrong. A classifier whose Config repointed away from it kept reportingActiveuntil the next informer resync; a spec edit whose re-render failed reported the new, unrendered generation asActivewhile the servers ran the old one. Both resources had their status written by a controller that only ever touched the ones it happened to render, so anything outside that path got no status at all.The fix is to let them derive their own status from what was actually rendered. Structurally that is the same shape ReportProcessor already had.
Three CRDs play the same role — a policy resource that the Config controller renders into a ConfigMap or Secret — but only one of them owned its status.
config_autosign.go)config_enc.go)This moves SigningPolicy and NodeClassifier onto the ReportProcessor pattern, so no controller writes into another resource's status any more.
SigningPolicyReconcilerandNodeClassifierReconciler: status-only observers that deriveReadyfrom whether the resource actually reached the servers.AutosignPolicyRenderFailed,ENCRenderFailed), matching what it already did for the report webhook.updateSigningPolicyStatusandupdateNodeClassifierStatusare gone.How a resource is matched against the rendered file
Each rendered Secret carries an
openvox.voxpupuli.org/rendered-fromannotation listing the resources its content was built from and themetadata.generationeach had at the time. The observers match on that rather than re-parsing the rendered file, which is what letsReadyseparate my current spec is in effect from an earlier version of it is:enc.yaml, on the endpoint URL — still finds the resource there, so the new, unrendered generation would report asActive. That case is nowRenderedConfigStale.enc.yamlcarries no resource name at all, so any spec change that keepsspec.url(an auth rotation above all) would otherwise be invisible to the observer.yaml.v3tags on the render side and once withjsontags on the parse side, where a key rename would compile cleanly and flip every resource toNotRenderedin production.The annotation also drives the Secret watches, so an unrelated Secret that merely ends in
-encno longer fans out over every NodeClassifier in the namespace.Cases that were silent before
The Config controller only ever wrote status on the resources it happened to render, so anything outside that path got no status at all. Deriving it instead closes those gaps:
NoConfigNotReferencedOverriddenByAutosignCommand/OverriddenByExternalNodesCommandRenderedConfigStaleRenderSourceUnknownThe override is checked before the Secret is read: a Secret rendered before the override was set still exists, and reporting that as
Activewould claim an effect the resource no longer has. The override only counts when every referencing Config sets it — one Config opting out does not disable the policy for the one that did not.Where several Configs render the same NodeClassifier, any one of them holding a Secret that does not match the current generation holds the whole resource out of
Ready— whether that Secret is an earlier generation, one rendered from a different classifier, or one recording no source at all. All three are the same statement: a server is running something that is not this generation, so a Config that renders cleanly must not mask a Config that is stuck. A Config that has rendered no Secret at all is the exception, since nothing is mounted there to contradict anything, and the verdict never depends on listing order.No API or RBAC change —
config/rbac/role.yamland the chart's ClusterRole already grant both status subresources. The kubebuilder markers are narrowed to what the new controllers actually do (read +/status), and the Config controller's now-unusedsigningpolicies/statusandnodeclassifiers/statusmarkers are removed.Upgrade behaviour
A Secret rendered by an earlier operator version carries no annotation at all, which is not the same as being rendered from nothing. Reading it as "this resource is not in it" would flip every SigningPolicy and NodeClassifier to
NotRenderedon upgrade — and for a paused Config, whose reconcile returns before the render step, it would stay there. Those resources reportRenderSourceUnknowninstead, which resolves as soon as the Config controller re-renders.One limit worth naming: the annotation records
metadata.generation, so a re-render that fails without a spec edit — a credential rotated underneath an unchanged spec — leaves the generation matching and the resource reportingActivewhile the old rendered file stays in effect. That is long-standing behaviour rather than new here (the previous code also returned before writing status on a render error), and the reference pages now say so and point at the Config's render-failure events.Watches and lookups
nodeClassifiersForConfigreadsnodeClassifierRefoff the event object instead of re-fetching the Config. controller-runtime runs a map function against both the old and the new object of an update, so repointing or clearing the field now enqueues the classifier that lost its Config — which previously stayedActiveuntil the informer resync.ObservedGenerationis captured before the observation:updateStatusWithRetryre-reads the object, so a spec edit landing in between stamped the new generation onto a verdict derived from the old spec.spec.nodeClassifierRefindex for Config.findSigningPolicies, the duplicated Config filters and the twin override helpers collapse into shared functions.Also in this branch
A separate commit renames
AGENT.mdtoAGENTS.md— the name coding agents look for by convention; nothing in the repository referenced the old path — and spells out that commits, PR descriptions and comments carry no AI attribution trailers, footers or session links.Test plan
go build ./...,go vet ./...,gofmtcleango test ./...— pass, including./api/v1alpha1/golangci-lint run ./...(v2.13.2, built with the repo's Go 1.27 toolchain) — 0 issuesmake manifests generate— no CRD or deepcopy driftTestSigningPolicyReconcile_Status(9 subtests),TestNodeClassifierReconcile_Status(8 subtests),TestNodeClassifiersForConfig,TestRenderedFromAnnotationRoundTrip,TestRenderedSourceRequests— covering active, each error reason, the stale-render cases, the override and partial-override cases, the multi-Config ordering rule, and the watch mapping for a repointed or clearednodeClassifierRefnilfrom either render path, recording generation zero, writing the annotation only on create, readingObservedGenerationfrom the re-read object, forcingReady=Truein error cases, removing a Secret-watch suffix filter, and hardcoding the request namespace each turn at least one test redTestUpdate*Status_*tests, which asserted the cross-controller writes this PR deletes, and the two rendered-file parser tests, whose parsers are gonedocs/reference/signingpolicy.mdandnodeclassifier.md, plus the shared mechanism indocs/reference/index.md