Extract variant branches into a generic extension mechanism - #4871
Conversation
Add WithContext/ComponentHandlerOption to NewComponentHandler (variadic, backward-compatible) and call operator.ApplyPatches in CreateOrUpdateOrDelete for components implementing render.Named.
Pulls the enterprise RBAC extra-rules and MULTI_INTERFACE_MODE env branches out of pkg/render/typha.go into a new pkg/enterprise package. The enterprise package registers a patch via operator.Patch on startup; pkg/render/typha.go now has zero IsEnterprise branches.
Calls enterprise.Register() at startup so the typha modifier is wired in. Builds an operator.Context in the installation reconciler and passes it to the component handler so registered modifiers receive reconcile-derived state.
Extracts the image override registry into a leaf pkg/imageoverride package (no render/operator transitive deps) to avoid the render→operator import cycle. operator.OverrideImage/ResolveImage now delegate there. Registers the enterprise node image override in pkg/enterprise. Removes the IsEnterprise image switch from render/node.go; FIPS handling is preserved via a post-resolve check.
…sion The OSS installation controller no longer directly creates the node-prometheus keypair or fetches the prometheus/esgw certs. Those are now handled by a registered InstallationExtension in pkg/enterprise. Port value derivation and the kube-controller TLS block remain in the OSS controller unchanged.
Moves the calico-node-metrics Service out of OSS node render and into the enterprise node modifier, where it derives ports from ctx.FelixConfiguration. Also exports NodeBGPReporterPort so the modifier can reference it.
# Conflicts: # pkg/controller/installation/core_controller_test.go # pkg/controller/utils/component.go # pkg/render/node.go
The registry package is renamed to extensions. The installation controller builds the render context through a registered factory, and the componentHandler applies registered modifiers to component output. The node and typha variant branches now live in enterprise modifiers, and the calico log directory is mounted for both variants.
Drop the functional-options builder for Inputs (one call site, all fields always set) in favor of a plain struct literal, and replace the single-method RenderContextFactory interface with a registered builder func. All three extension seams now register a func.
Register modifiers, image overrides, and the render context builder per variant. The registries now gate on the installation variant, so the enterprise funcs drop their self-gate guards (the IsEnterprise checks the PR set out to remove) and the image override drops its decline bool - it only runs for its own variant. Move the node prometheus reporter keypair mounting (volume, mount, cert-management init container, pod hash annotation) into the node modifier, and remove NodeConfiguration.PrometheusServerTLS along with the round-trip through the installation controller. Core node render no longer carries a prometheus mount; in calico the keypair is never created. Rename Extensible.Name() to ModifierKey() so an unrelated Name() method can't make a component modifier-eligible by accident.
Merge the per-component image override and modifier into a single
Extension{Image, Modify} registered once per (variant, component) via
extensions.Register, so all of a component's variance lives in one place.
The image half still lands in the imageoverride leaf so render resolves it
without an import cycle; the fan-out is internal to Register.
Rename the variant-level render context builder to Setup
(RegisterSetup/RunSetup). That names the two phases a reader has to hold:
Setup is the controller-side work that builds the RenderContext baton, and
Extension hooks are the pure render-time funcs. Three registries with three
key schemes become two concepts split by when they run.
modCtx read like "modifier context"; the value is an extensions.RenderContext, so name it for what it is.
Add a package doc that lays out the two-phase model (Setup vs Extension) so the whole seam is legible from `go doc`. Fix two comments that still called the setup a render context builder.
Add a per-component context channel: a component implements render.ExtensionContextProvider to hand its modifier config a modifier can't derive from the shared RenderContext (config only the component's controller has). The componentHandler reads it into RenderContext.Component before applying the modifier. node's setup-produced keypair keeps its own field; this is for component-config-derived inputs. Move windows's enterprise branches into a pkg/enterprise extension: the two windows image overrides, the node-metrics Service, the calico log volume (swapped in for the OSS cni-log mount), the enterprise felix env, the trusted DNS servers for openshift/rke2, and the prometheus reporter keypair mount. The windows component exposes its reporter port, keypair, and trusted bundle via ExtensionContext; the windows controller wires the render context into its handler. Core windows render is now OSS-only.
A controller called Validate and ExtendInputs back to back, and the API server extension read the same two CRs in each. Validation now happens inside ExtendInputs, which rejects unsupported configuration with an error matching ErrInvalidConfig so the controller still degrades with a validation reason rather than a create reason.
caseydavenport
left a comment
There was a problem hiding this comment.
Claude: A pass looking only for behavior that changed in the extraction, skipping the structural and naming ground already covered in the existing comments. Everything below was checked against origin/master rather than inferred.
The first three are RBAC and watches that master has and this branch doesn't. All three were already absent before today's master merges, so they came from earlier rounds: a merge hit a region the branch had deleted, took our side, and quietly dropped what master had added inside it. Worth a systematic diff of master's other extracted components against their pkg/enterprise copies before this lands. I did that sweep for the two API server user roles and the rule vocabulary comes back with exactly the two clusters below and nothing else.
The last two, on the decorator and the reporter gating, are judgement calls rather than defects - flagging for a decision, not asking for a change.
The API server user roles lost their Gateway API read rules and the rbacUI role management rules, the installation extension lost its Manager watch, and the typha modifier only reached the cluster Typha, not the non-cluster-host one. Also pass the clusterconnection handler the inputs ExtendInputs returned, match removeByRef on the concrete type rather than an always-empty Kind, gate the node prometheus mount and its env on the same condition, keep the GatewayAPI read diagnostic, and skip decoration when the Installation asks for a variant this process didn't boot as, matching how the image overrides already resolve.
The product version reported in status comes from a ProductVersion companion the variant implements. The controller watches the Calico CRDs whatever it is running as, and the enterprise extension watches the ones it adds. The management cluster CRs are read by the hook rather than the controller, which means the base kube-controllers render no longer decides between reaching the manager directly or through Guardian, nor whether to bind the managed-cluster watch role - both move to the modifiers. The kube-controllers image resolves through the override registry, so Calico Cloud registers its image instead of the render branching on a flag.
A controller took the whole registry and looked its extension up on every reconcile. It now resolves once when it is built, so the call sites read r.ext.ExtendInputs and nothing downstream carries the registry. The resolved surface is a struct whose zero value is inert, which replaces the per-lookup null objects.
Master removed the Compliance feature, which included four Compliance ingress rules from the guardian calico-system policy. That policy body now lives in the enterprise guardian extension, so drop them there too.
A component hands its modifier the config it rendered from, so the extension dispatches on component type and the component handler applies it.
…sions # Conflicts: # pkg/controller/apiserver/apiserver_controller.go # pkg/controller/installation/core_controller.go # pkg/render/apiserver.go # pkg/render/apiserver_test.go # pkg/render/kubecontrollers/kube-controllers.go # pkg/render/kubecontrollers/kube-controllers_test.go # pkg/render/node.go
The enterprise apiserver and installation extensions read the gate ConfigMap themselves.
The core controller no longer knows which of the connection's fields are enterprise-only.
Restores the status reasons the shared controllers reported before the extension split, including the not-ready wait for the Dex TLS secret.
…sions # Conflicts: # pkg/render/testutils/expected_policies/kubecontrollers.json # pkg/render/testutils/expected_policies/kubecontrollers_managed.json # pkg/render/testutils/expected_policies/kubecontrollers_managed_ocp.json # pkg/render/testutils/expected_policies/kubecontrollers_ocp.json
This is phase 1 of prepping the operator for the monorepo merge, where the Calico and Calico Enterprise code paths eventually live apart. Today they share one codebase with
IsEnterprise()checks sprinkled through the render and controller code, and that coupling is the thing that makes the split hard.This PR pulls the enterprise-specific behavior out of the core code and behind a generic extension mechanism, so the enterprise build registers its own additions and the core operator stays variant-blind. After this, core render and controller code has no idea enterprise exists.
How it works
Enterprise registers extensions against a set keyed by variant. There are two extension points, one per phase of a reconcile:
The core operator registers nothing and runs the base path. All the enterprise wiring lives in
pkg/enterprise, one subpackage per component. After the split, that package is what the enterprise build constructs and the core build drops.Every extracted component (node, typha, guardian, windows, apiserver, kube-controllers) and the clusterconnection controller now run their enterprise behavior through this mechanism, with no
IsEnterprise()left in their core paths. Behavior is unchanged: the test gate is the existing core tests plus the relocated enterprise tests, which now run against the real extension set.A few shared-code cleanups and ergonomic refactors are left as follow-ups, tracked in CORE-13042.
Related: CORE-13180