operatorkit

package
v1.0.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

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

func Run(ctx context.Context, logger *slog.Logger, opts Options, controllers []Controller) error

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
	// scaffolded RunOperators passes "<module>-leader". Two processes that
	// both run a manager and share this name contend for the SAME lease, so
	// a project running more than one gives each its own value here.
	LeaderElectionID string

	// LeaderElectionNamespace is the namespace holding the lease.
	//
	// Empty is correct IN-CLUSTER: controller-runtime infers the namespace
	// from the ServiceAccount mount. Out of cluster there is nothing to
	// infer and NewManager hard-errors, so Run treats "reachable cluster,
	// not in-cluster, no namespace" exactly like no-cluster — it warns and
	// continues WITHOUT operators rather than dying. Set this to run a
	// manager from a host process against a dev cluster; the value is the
	// opt-in.
	LeaderElectionNamespace 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 — which is itself projected
	// from the app's typed config, so a deployment that needs a probe port
	// declares one field and it arrives here. Empty leaves the manager
	// without a probe listener (the default — vanilla forge projects don't
	// bind one).
	HealthProbeBindAddress string

	// Leader-election timings. Zero takes the hardened defaults documented
	// at the top of this file (45s/30s/5s), which suit the single-replica
	// controller a forge project scaffolds. Raise or lower them together —
	// LeaseDuration > RenewDeadline > RetryPeriod is a controller-runtime
	// invariant, and a mismatched trio self-terminates a healthy leader.
	LeaseDuration time.Duration
	RenewDeadline time.Duration
	RetryPeriod   time.Duration

	// ClientQPS / ClientBurst raise the manager's own client-side request
	// ceiling. Zero takes the defaults above (50/100). They only apply when
	// the resolved rest.Config does not already carry a value, so an
	// explicit kubeconfig setting still wins.
	ClientQPS   float32
	ClientBurst int

	// 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

	// ByObjectLabels scopes the manager cache PER OBJECT TYPE by LABEL: the
	// informers for that type see ONLY objects matching the selector, in
	// every namespace (controller-runtime cache.ByObject.Label).
	//
	// This is the scoping primitive for CRs whose namespaces are not known up
	// front — one per deploy environment, created at runtime — where a
	// namespace list cannot be written down. A publisher stamps an ownership
	// label (e.g. forge.dev/stack=<id>) and each stack's operator watches only
	// its own value, so two stacks sharing a cluster cannot reconcile each
	// other's CRs.
	//
	// A type may appear in BOTH maps; the cache then intersects them. A nil
	// selector is dropped. UNLIKE ByObjectNamespaces there is no "empty means
	// cluster-wide" fallback to worry about here: the caller passes a real
	// selector or nothing, and refusing an empty scope is the caller's job.
	ByObjectLabels map[client.Object]labels.Selector
}

Options carries the per-project manager configuration the generated row table supplies.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL