Documentation
¶
Overview ¶
Package operatorkit owns the controller-manager runtime behind the generated App.RunOperators method in forge projects.
Pattern ¶
The generated pkg/app/bootstrap.go used to open-code the controller-runtime manager setup (kubeconfig resolution, leader election, scheme registration, controller setup, manager start). Following the "generated files are tables, not programs" rule, the generated RunOperators is now a single delegation to Run with one dumb Controller row per operator:
func (a *App) RunOperators(ctx context.Context, logger *slog.Logger, healthProbeAddr string) error {
return operatorkit.Run(ctx, logger, operatorkit.Options{
LeaderElectionID: "example.com/myproj-leader",
HealthProbeBindAddress: healthProbeAddr,
}, []operatorkit.Controller{
{Name: "scaler", AddToScheme: scaler.AddToScheme,
SetupWithManager: a.Operators.Scaler.SetupWithManager},
})
}
operatorkit lives in its own package (rather than appkit proper) so projects without operators never compile controller-runtime and its Kubernetes dependency tree — the generated import is conditional on the project having operators.
Behavioural fingerprint ¶
All observable strings from the pre-table generated RunOperators are preserved verbatim:
- warn "operators disabled: no Kubernetes cluster reachable" when kubeconfig resolution fails (vanilla docker-compose dev, fresh laptop, CI without a kind/k3d cluster) — the binary continues without operators rather than crashing, matching how NATS degrades.
- "creating controller manager: <wrapped error>".
- "adding <name> scheme: <wrapped error>".
- "setting up controller %q: <wrapped error>".
- info "registered operator controller" / "starting controller manager".
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
Run creates a controller manager, registers every controller's scheme and setup, and starts the manager. It blocks until ctx is cancelled or an error occurs; the caller runs it in a goroutine.
When no Kubernetes cluster is reachable, kubeconfig resolution fails and Run logs a warning and returns nil — the process continues without operators instead of crashing.
Types ¶
type Controller ¶
type Controller struct {
// Name is the operator's forge.yaml name — used in error messages
// and registration logs.
Name string
// AddToScheme registers the operator's CRD types on the manager's
// scheme. Optional (nil is skipped) for controllers that only watch
// built-in types.
AddToScheme func(s *runtime.Scheme) error
// SetupWithManager registers the controller with the manager.
SetupWithManager func(mgr ctrl.Manager) error
}
Controller is one generated operator row: the CRD scheme installer and the controller's manager hookup, both referenced straight off the generated operator package / constructed instance.
type Options ¶
type Options struct {
// LeaderElectionID is the lease name used for leader election —
// the generated table passes "<module>-leader". The LEADER_ELECTION_ID
// env var, when set, overrides this so distinct processes can take
// distinct leases (env > this default).
LeaderElectionID string
// HealthProbeBindAddress, when non-empty, binds a /healthz +
// /readyz listener on that address for the controller-runtime
// manager. The generated RunOperators forwards it from
// serverkit.Config.OperatorHealthProbeAddr. Empty leaves the
// manager without a probe listener (the default — vanilla forge
// projects don't bind one).
HealthProbeBindAddress string
// ByObjectNamespaces scopes the manager cache PER OBJECT TYPE: each entry
// maps an object example (e.g. &v1alpha1.Workspace{}) to the ONLY
// namespaces the manager's informers watch/list for that type
// (controller-runtime cache.ByObject.Namespaces). Types WITHOUT an entry
// keep the default cluster-wide watch, so a controller can confine its own
// CRD to the namespace its stack deploys into while still watching
// cross-namespace workload objects (Pods/PVCs in per-user namespaces)
// everywhere.
//
// Motivation: co-located stacks on one shared cluster (e.g. dev + e2e on
// one k3d node) each run their own copy of the same operator. With a
// cluster-wide CR watch, each copy also reconciles the OTHER stack's CRs —
// a derelict controller from one stack can then stamp its own config
// (image, env) onto a sibling stack's workloads. Scoping the CR watch to
// the stack's own namespace makes cross-stack reconciliation structurally
// impossible.
//
// Entries with no namespaces (or only empty strings) are dropped — that
// type stays cluster-wide, preserving the legacy behavior when the
// deployment namespace is unknown. The scoped types' GVKs are resolved
// against the manager scheme at manager construction, so every scoped
// type MUST be registered by one of the controllers' AddToScheme hooks
// (Run registers them all before creating the manager).
ByObjectNamespaces map[client.Object][]string
}
Options carries the per-project manager configuration the generated row table supplies.