Help us shape the future of Go! Take the 2026 Go Developer Survey

operatorkit

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 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