Documentation
¶
Overview ¶
Package operator implements the controller-runtime-based reconciler half of JaaS, activated by --enable-flux-integration. The HTTP-only evaluator path does not import this package; it stays compiled-out unless the flag is set.
Index ¶
- Constants
- Variables
- func ExternalArtifactGVK() schema.GroupVersionKind
- func ParseExtVars(pairs []string) (map[string]string, error)
- func ParseRerenderRate(s string) (float64, error)
- func RecordSweepFailure()
- func RecordWebhookCertRenewalFailure()
- func Run(ctx context.Context, cfg Config, restCfg *rest.Config) error
- type Config
- type PublishResult
- type Publisher
- func (p *Publisher) PruneStored(ctx context.Context, namespace, name string, keepRevisions []string) error
- func (p *Publisher) Publish(ctx context.Context, c client.Client, snip *jaasv1.JsonnetSnippet, ...) (PublishResult, error)
- func (p *Publisher) Withdraw(ctx context.Context, c client.Client, snip *jaasv1.JsonnetSnippet) error
- type RateLimiter
- type SnippetReconciler
- func (r *SnippetReconciler) EngageFluxWatch(ctx context.Context, gvk schema.GroupVersionKind) error
- func (r *SnippetReconciler) MissingFluxSourceKinds() []schema.GroupVersionKind
- func (r *SnippetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)
- func (r *SnippetReconciler) SetupWithManager(mgr ctrl.Manager) error
- type SnippetValidator
- func (v *SnippetValidator) SetupWithManager(mgr ctrl.Manager) error
- func (v *SnippetValidator) ValidateCreate(ctx context.Context, snip *jaasv1.JsonnetSnippet) (admission.Warnings, error)
- func (v *SnippetValidator) ValidateDelete(_ context.Context, _ *jaasv1.JsonnetSnippet) (admission.Warnings, error)
- func (v *SnippetValidator) ValidateUpdate(ctx context.Context, _ *jaasv1.JsonnetSnippet, snip *jaasv1.JsonnetSnippet) (admission.Warnings, error)
- type SourceFetcher
Constants ¶
const ( // ReasonPending marks a snippet that has been observed but not yet // reconciled — primarily transient, between watch event and first // completed reconcile pass. ReasonPending = "Pending" // ReasonSynced is set on Ready=True when the most recent reconcile // completed end-to-end and produced a publishable artifact. ReasonSynced = "Synced" // ReasonInvalidSpec covers spec-level validation failures that // admission should have caught: missing main.jsonnet, both/neither of // files/sourceRef set, etc. ReasonInvalidSpec = "InvalidSpec" // ReasonLibraryNotFound is set when a spec.libraries entry references // a JsonnetLibrary CR that the controller cannot Get (deleted, wrong // name, wrong namespace). ReasonLibraryNotFound = "LibraryNotFound" // ReasonCrossNamespaceRefRejected fires when --no-cross-namespace-refs // is enabled and a snippet references a library or source outside its // own namespace. ReasonCrossNamespaceRefRejected = "CrossNamespaceRefRejected" // ReasonExternalVariableConflict fires when a CR's // spec.externalVariables names a key the operator already owns via // --ext-var. ReasonExternalVariableConflict = "ExternalVariableConflict" // ReasonServiceAccountMissing fires when neither spec.serviceAccountName // nor --default-service-account is set. ReasonServiceAccountMissing = "ServiceAccountMissing" // ReasonEvaluationFailed wraps any go-jsonnet diagnostic surfaced from // the eval package — syntax error, runtime error, etc. ReasonEvaluationFailed = "EvaluationFailed" // ReasonEvaluationTimeout fires when the eval-package deadline was // reached before the snippet finished. ReasonEvaluationTimeout = "EvaluationTimeout" // ReasonSourceRefNotYetSupported is set when a CR declares // spec.sourceRef but the operator was built without a Fetcher (tests // and the bare-bones binary path). Production wiring always installs // the Fetcher; users seeing this in real clusters indicate a // mis-deployed operator. ReasonSourceRefNotYetSupported = "SourceRefNotYetSupported" // ReasonSourceNotReady fires when the referenced Flux source CR // exists but its status.conditions[Ready] is not True yet, or its // status.artifact is unpopulated. Re-reconcile when the source flips // to Ready — controller-runtime currently doesn't watch source CRs so // the user may need to nudge the snippet to retrigger. ReasonSourceNotReady = "SourceNotReady" // ReasonSourceFetchFailed fires when the Fetcher can't materialise // the artifact: HTTP failure, digest mismatch, tar corruption, or any // other error not classified as "not ready". ReasonSourceFetchFailed = "SourceFetchFailed" // ReasonDependencyCycle fires when a snippet's spec.sourceRef chain // transitively points back at itself — directly (A → EA(A)) or // through other snippets (A → EA(B) → EA(A)). The reconciler refuses // to publish so chained snippets don't loop forever; the webhook // rejects new CRs that introduce the cycle at admission. ReasonDependencyCycle = "DependencyCycle" // ReasonArtifactTooLarge fires when a snippet's published content // (rendered output, or the whole source tree in Output=source mode) // exceeds Publisher.MaxArtifactBytes. Stops one runaway snippet // from filling the storage volume; operators tune the bound via // --max-artifact-bytes. ReasonArtifactTooLarge = "ArtifactTooLarge" // ReasonSuspended is set on Ready=False whenever spec.suspend is // true. The previous Status.Revision and the on-disk artifact are // preserved so downstream Flux consumers continue serving the last // published bytes — suspending is "pause writes", not "rollback". ReasonSuspended = "Suspended" // ReasonRBACDenied is set on Ready=False when an apiserver call // fails with Forbidden, or a source CR's kind is not registered // with the apiserver. Both cases are non-recoverable by retry — // the cluster operator has to grant the missing verb (Forbidden) // or install the missing CRD (NoMatchError). The reconciler stops // engaging backoff for these errors so the workqueue isn't // burning cycles on permanently-failing snippets. // // Distinct from ReasonSourceFetchFailed (network / 5xx / digest // mismatch — different remediation) and ReasonLibraryNotFound // (the library CR truly doesn't exist — different remediation). // The message always names the verb + resource the operator must // grant so kubectl describe sends them straight to the fix. ReasonRBACDenied = "RBACDenied" )
Wire-stable Reason strings on the Ready status condition. Programmatic callers and runbooks key off these values; renaming them is a breaking change.
const EntryFileName = "main.jsonnet"
EntryFileName is the snippet entry point inside spec.files. Snippets that omit it are rejected at reconcile time — the convention matches the HTTP path, where the resolver looks for main.jsonnet under each -snippet-directory.
const FinalizerName = "jaas.metio.wtf/finalizer"
FinalizerName is set on every JsonnetSnippet under management so the reconciler can clean up its published ExternalArtifact before the API removes the object. The string is part of the on-disk contract: changing it orphans finalizers on existing snippets and blocks their deletion.
const ReconcileRequestAnnotation = fluxmeta.ReconcileRequestAnnotation
ReconcileRequestAnnotation is the Flux-convention annotation key that `flux reconcile` (and a manual `kubectl annotate <cr> reconcile.fluxcd.io/requestedAt=<token> --overwrite`) stamps to force an out-of-band reconcile. The watch installs predicates.ReconcileRequestedPredicate so a change to this annotation enqueues the snippet; on a successful reconcile its value is copied to status.lastHandledReconcileAt so tooling can confirm the request was handled. Aliased to the canonical Flux constant so the reconciler reads the same key the predicate matches on.
Variables ¶
var AllReasons = []string{ ReasonPending, ReasonSynced, ReasonInvalidSpec, ReasonLibraryNotFound, ReasonCrossNamespaceRefRejected, ReasonExternalVariableConflict, ReasonServiceAccountMissing, ReasonEvaluationFailed, ReasonEvaluationTimeout, ReasonSourceRefNotYetSupported, ReasonSourceNotReady, ReasonSourceFetchFailed, ReasonDependencyCycle, ReasonArtifactTooLarge, ReasonSuspended, ReasonRBACDenied, }
AllReasons enumerates every wire-stable Reason the reconciler can set on the Ready condition. The drift-gate test in conditions_test.go asserts every entry has a matching docs/runbooks/<reason>.md, so a new Reason cannot ship without a remediation page.
var ErrArtifactTooLarge = errors.New("publisher: artifact exceeds MaxArtifactBytes")
ErrArtifactTooLarge is returned from Publish when the published content (summed across the tarball members) exceeds Publisher.MaxArtifactBytes. The reconciler surfaces this as ReasonArtifactTooLarge on the Ready condition.
var FluxSourceKinds = []string{"GitRepository", "OCIRepository", "Bucket", "ExternalArtifact"}
FluxSourceKinds are the Flux source-controller kinds the reconciler re-reconciles snippets against. ExternalArtifact is in this set so chained snippets re-render when an upstream snippet republishes; the cycle detector (detectSourceRefCycle) prevents the publish → watch → reconcile → publish loop a sourceRef cycle would otherwise create.
Drift gate: the chart's ClusterRole must grant `get` on every kind here or the Fetcher's first sourceRef lookup fails with Forbidden. The chart — and its ClusterRole drift-gate test — lives in the metio/helm-charts repo; when a new kind is added here, update the chart's source-CR rule and that test there in the same change.
Functions ¶
func ExternalArtifactGVK ¶
func ExternalArtifactGVK() schema.GroupVersionKind
ExternalArtifactGVK returns the GVK our reconciler upserts. Exported for tests and main.go-level scheme registration.
func ParseExtVars ¶
ParseExtVars converts KEY=VALUE strings into a map. Empty values are allowed (KEY= maps to ""); bare keys with no '=' are an error so a typo doesn't get silently swallowed.
func ParseRerenderRate ¶
ParseRerenderRate converts strings like "60/min" or "1/sec" into a per-second rate. The form is N/period where N is a non-negative float and period is one of sec|s|second|seconds, min|m|minute|minutes, hour|h|hr|hours.
Empty input is rejected so callers default explicitly.
func RecordSweepFailure ¶
func RecordSweepFailure()
RecordSweepFailure increments the sweep-failure counter. Called from main.go's runStorageSweep on every Sweep error. Exported so the main package can wire it without importing prometheus directly.
func RecordWebhookCertRenewalFailure ¶
func RecordWebhookCertRenewalFailure()
RecordWebhookCertRenewalFailure increments the cert-renewal failure counter. Exported so internal/webhook/selfsigned can wire it without pulling Prometheus into the package's import graph.
func Run ¶
Run boots a controller-runtime manager wired with the JaaS v1 scheme and blocks until ctx is canceled. The manager carries no reconcilers in Phase 1B; subsequent phases register them via the manager's builder API.
restCfg must be a valid *rest.Config; the kubeconfig-resolution chain lives in main.go so the operator package stays free of process-level concerns.
Types ¶
type Config ¶
type Config struct {
// DefaultServiceAccount names the ServiceAccount the operator
// impersonates when a JsonnetSnippet does not set
// spec.serviceAccountName. Empty means snippets without an explicit SA
// are rejected.
DefaultServiceAccount string
// NoCrossNamespaceRefs mirrors Flux's --no-cross-namespace-refs:
// when true (the default in Phase 1B's main.go wiring), a snippet
// referencing a SourceRef in a different namespace is rejected.
NoCrossNamespaceRefs bool
// LabelSelector narrows the operator's watch to objects matching this
// selector. Empty selects all objects in the watched scope, matching
// controller-runtime's default.
LabelSelector string
// WatchNamespaces restricts the manager's cache to the listed
// namespaces (Cache.DefaultNamespaces). Empty (the default) means
// cluster-wide watch — the historical behaviour. When set, the
// cache only observes CRs in these namespaces; CRs outside are
// invisible to the reconciler even when the SA's RBAC would
// otherwise grant access. Paired with per-namespace RoleBindings
// in the chart, this is the multi-tenant operator-instances
// pattern: one operator per tenant-group, disjoint watch sets.
//
// Each entry must be a valid Kubernetes namespace (DNS-1123
// label). main.go validates the list at flag-parse time and
// rejects an invalid entry with rc=2.
WatchNamespaces []string
// KnownLibraryAliases enumerates the OCI-mounted library aliases
// available cluster-wide (one per subdirectory of every -library-path).
// The reconciler + admission webhook use these to reject snippets
// whose spec.libraries[*].importPath shadows an OCI mount — the CR
// would silently win and the operator would never consult the OCI
// volume, surprising operators who set up the mount expecting it to
// be used. Empty disables the check.
//
// Derived at startup from OCILibraries (its keys); kept separately
// so the admission webhook validator can avoid the eval-package
// dependency.
KnownLibraryAliases []string
// OCILibraries holds the byte contents of every OCI-mounted
// library the operator was booted with, keyed by alias. The
// reconciler merges these into the per-snippet library map after
// the CR-based LibraryRef resolution so snippets can `import
// "<alias>/file"` against operator-shipped shared libraries
// without declaring a CR LibraryRef. CR aliases CAN'T collide
// (admission rejects shadow attempts); the merge is purely
// additive.
OCILibraries map[string]eval.Library
// RerenderRate is the steady-state per-snippet re-render budget, in
// tokens per second. Parsed from --rerender-rate=N/period via
// ParseRerenderRate.
RerenderRate float64
// RerenderBurst is the per-snippet token-bucket depth.
RerenderBurst int
// ExtVars is the operator-level std.extVar map. Keys here take
// precedence over CR-level spec.externalVariables; conflicts are
// rejected at admission, with a reconciler fallback that fails Ready.
ExtVars map[string]string
// EvaluationTimeout bounds a single reconcile's snippet eval; zero
// disables the bound. Mirrors the HTTP path's --evaluation-timeout.
EvaluationTimeout time.Duration
// MaxStack overrides go-jsonnet's default call-stack depth; zero keeps
// the default. Mirrors the HTTP path's --max-stack.
MaxStack int
// Store is the artifact backend the Publisher writes tarballs to.
// Nil leaves the reconciler in eval-only mode (no ExternalArtifact
// upserts) — useful for early bring-up and unit tests. In production
// main.go wires either a *storage.Store (filesystem, single-pod) or
// a *storage.S3Backend (object store, HA across replicas) depending
// on -storage-backend.
Store storage.Backend
// StorageBaseURL is the public URL prefix the operator's storage HTTP
// server serves tarballs from. Combined with Store.Path it forms each
// artifact's status.artifact.url. Required when Store is set.
StorageBaseURL string
// MaxArtifactBytes caps the published content size in bytes — the
// rendered output in Output=rendered mode, the whole source tree in
// Output=source mode. A snippet over the cap fails with
// ReasonArtifactTooLarge before any disk/S3 write. Zero disables.
MaxArtifactBytes int64
// ArtifactGCGrace is the minimum time a revision evicted from the
// keep-set remains fetchable before storage GC removes it. Closes
// the pin→fetch race in which a Flux consumer reads
// status.artifact a moment before the operator garbage-collects the
// superseded revision, then 404s on the dereference. Forwarded
// verbatim to Publisher.GCGrace and on to storage.Backend.Prune;
// zero restores the immediate-prune behavior. Default in main.go is
// 5m — wide enough to cover steady-state consumer fetch latencies
// (kustomize-controller, helm-controller, stageset-controller) while
// keeping storage growth bounded.
ArtifactGCGrace time.Duration
// EnableWebhook opts into running the validating admission webhook for
// JsonnetSnippet. The webhook checks operator-level ext-var conflicts
// at admission time; the reconciler enforces the same invariant as a
// fallback when the webhook is bypassed.
EnableWebhook bool
// WebhookCertDir tells controller-runtime where to find the TLS cert
// and key for the webhook server. Provisioning of these files is the
// helm chart's responsibility (cert-manager or an init container).
WebhookCertDir string
// WebhookPort is the port the webhook server binds to. Defaults to 9443
// when unset.
WebhookPort int
// SkipControllerNameValidation disables controller-runtime's
// once-per-process check that every controller name is unique. Only
// the envtest harness sets this — main.go-driven invocations always
// boot a single controller per process where the validation is a
// useful safety net.
SkipControllerNameValidation bool
// SkipImpersonation makes the reconciler use the manager's own client
// for tenant-side operations instead of building a per-snippet
// impersonating client. Only the envtest harness sets this so its
// reconcile tests can run without provisioning per-snippet SAs and
// RBAC; production must keep the default (impersonation on) so a
// compromised or buggy snippet can't reach beyond the tenant's SA
// permissions.
SkipImpersonation bool
// LeaderElection toggles controller-runtime's leader-election lock.
// When true the manager only runs reconcilers + cache + the webhook
// while holding the lease at LeaderElectionNamespace/LeaderElectionID —
// letting operators scale the Deployment without two replicas
// double-reconciling the same JsonnetSnippet. Defaults on in main.go
// when --enable-flux-integration is set; tests opt out via the
// zero-value (false) to avoid the cost of a lease per envtest case.
LeaderElection bool
// LeaderElectionID is the Lease object's name. Multiple installations
// in the same cluster must pick distinct IDs (helm uses the release
// name) so they don't fight over a shared lease.
LeaderElectionID string
// LeaderElectionNamespace holds the Lease. main.go fills this from a
// flag (typically the release namespace); empty falls back to
// controller-runtime's downward-API discovery.
LeaderElectionNamespace string
// MaxWithdrawWait bounds how long a deleted JsonnetSnippet may
// stay stuck in the finalizer while its Publisher.Withdraw keeps
// failing. Past the bound the reconciler force-drops the
// finalizer, emits a Warning WithdrawForced event, and lets the
// snippet be garbage-collected — possibly leaving an orphaned
// tarball in storage. Without this escape a permanently-broken
// backend (S3 perma-down, revoked RBAC, deleted bucket) makes
// snippets undeletable, which blocks namespace teardown.
//
// Zero falls back to defaultMaxWithdrawWait (1h). Negative is
// treated as zero; force-drop is unconditional only when an
// operator explicitly sets a very small value (test fixtures).
MaxWithdrawWait time.Duration
// MetricsBindAddress is the host:port the controller-runtime metrics
// server listens on. The default "" leaves controller-runtime's own
// default in place (":8080"), which collides with the jsonnet HTTP
// server, so main.go always sets this explicitly. "0" disables the
// metrics server entirely.
MetricsBindAddress string
// Logger receives operator-level events. nil falls back to slog.Default.
Logger *slog.Logger
// OnReady, when non-nil, is invoked exactly once after the manager's
// cache has synced — on every replica, leader or not (it is wired as a
// non-leader-election runnable). main.go threads `HealthState.SetReady`
// here so the pod's readiness probe stays 503 until the operator has
// booted and its cache is warm. Gating on leader election instead would
// leave standby replicas permanently NotReady even though they serve the
// HTTP renderer + storage and are ready to take over reconciliation.
OnReady func()
}
Config groups every operator-facing CLI flag in one struct so main.go can pass it to Run as a single argument and tests can build it without flag parsing.
type PublishResult ¶
PublishResult is what a successful Publish returns. Carries the revision (for status.revision) and the public URL the published tarball is served at (mirrored onto status.artifactURL so the snippet is self-describing — see SyncStatus.ArtifactURL).
type Publisher ¶
type Publisher struct {
Store storage.Backend
BaseURL string
Clock func() time.Time
// MaxArtifactBytes caps the published content size in bytes. The cap
// measures the sum of the tarball members' content — len(rendered) in
// Output=rendered mode, the whole sourceFiles tree in Output=source
// mode — so it bounds what actually lands on disk/S3 in either mode. A
// snippet over the cap fails with ErrArtifactTooLarge before any
// write, so one runaway snippet can't fill the storage volume. Zero
// disables.
MaxArtifactBytes int64
// GCGrace is the minimum time a revision evicted from the keep-set
// remains fetchable before Prune removes it. Closes the pin→fetch
// race in which a Flux consumer reads status.artifact a moment
// before the operator garbage-collects the superseded revision and
// then 404s on the dereference. Threaded through to
// storage.Backend.Prune verbatim; zero restores eager pruning.
GCGrace time.Duration
}
Publisher writes the snippet's tarball to the configured storage backend and upserts the matching ExternalArtifact CR. The API client is supplied per call so the reconciler can hand in a fresh per-snippet impersonating client without rebuilding the Publisher. Construct via NewPublisher.
The Store field is satisfied by any storage.Backend — currently the filesystem-backed *storage.Store and the object-store *storage.S3Backend. Tests substitute fakes here to inject Put / Prune / Delete failures.
func NewPublisher ¶
NewPublisher returns a Publisher whose Clock falls back to time.Now.
func (*Publisher) PruneStored ¶
func (p *Publisher) PruneStored(ctx context.Context, namespace, name string, keepRevisions []string) error
PruneStored runs a keep-set Prune on the snippet's stored revisions without a fresh Put or ExternalArtifact upsert. Used by the suspended reconcile path: a paused snippet still re-enters the reconciler on every watch tick + interval, so calling PruneStored there keeps grace-expired evicted revisions from leaking when the snippet stays suspended for the operator's lifetime. keepRevisions follows the same shape Publish wants — short SHA-256 hex strings, no "sha256:" prefix.
func (*Publisher) Publish ¶
func (p *Publisher) Publish(ctx context.Context, c client.Client, snip *jaasv1.JsonnetSnippet, rendered string, sourceFiles map[string]string, keepRevisions []string) (PublishResult, error)
Publish writes the artifact tarball, computes the URL the operator's storage HTTP server will serve it from, and upserts the ExternalArtifact CR with that URL on its status. The returned revision is the "sha256:<hex>" form ready to copy into JsonnetSnippet.Status.Revision. The supplied client is used for every API call — pass an impersonating client to bound the reconcile to the tenant's permissions.
sourceFiles is the resolved snippet source — for inline snippets this is snip.Spec.Files verbatim; for sourceRef snippets it's the file map the Fetcher extracted from the upstream tarball. Used only in Output=source mode; ignored otherwise.
keepRevisions lists the sha256 shortRevs (no "sha256:" prefix) to retain in storage after this publish; always include the just-published revision. An empty slice keeps only the just-published revision.
func (*Publisher) Withdraw ¶
func (p *Publisher) Withdraw(ctx context.Context, c client.Client, snip *jaasv1.JsonnetSnippet) error
Withdraw removes the ExternalArtifact CR and the snippet's stored tarballs. Called from the deletion path before the finalizer is dropped; the client is supplied per call for the same impersonation reason as Publish.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter bounds how frequently a single snippet can drive an end-to-end reconcile. The bucket is per-key (namespace/name) so a runaway snippet cannot starve unrelated ones.
The zero value is unusable; construct via NewRateLimiter. A nil receiver is allowed and always permits; callers can leave the field nil to disable rate limiting without conditional logic at the call site.
func NewRateLimiter ¶
func NewRateLimiter(perSec float64, burst int) *RateLimiter
NewRateLimiter returns a limiter whose per-key bucket refills at perSec tokens per second up to a maximum of burst. Both arguments must be > 0; values that don't satisfy that fall back to a permissive limiter.
func (*RateLimiter) Forget ¶
func (l *RateLimiter) Forget(key string)
Forget drops the bucket for key. Called when a snippet is deleted so the map doesn't grow unbounded.
func (*RateLimiter) Reserve ¶
func (l *RateLimiter) Reserve(key string) (bool, time.Duration)
Reserve consumes one token for key. Returns allowed=true with delay=0 when a token was available; returns allowed=false with delay set to the wait time until the next token is ready. The bucket is NOT drained on rejection so a runaway caller can't push the wait time past its natural value.
type SnippetReconciler ¶
type SnippetReconciler struct {
Client client.Client
Scheme *runtime.Scheme
// APIReader bypasses the manager's cache for the pre-publish
// staleness gate. The cache can lag behind the apiserver under
// load, so a fresh Get just before the downstream-visible commit
// catches a spec edit that landed during this reconcile's fetch +
// eval phase. nil falls back to Client — fine for tests but
// production wiring (defaultBuilder) sets it from mgr.GetAPIReader.
APIReader client.Reader
// RestConfig is the manager's rest.Config, cloned per-reconcile and
// stamped with a freshly-minted ServiceAccount token to mint a tenant
// client. nil disables impersonation — fake-client tests that don't
// model TokenRequest set this to nil and the reconciler falls back to
// Client for tenant operations.
RestConfig *rest.Config
// TokenCache mints + caches bearer tokens for the tenant SAs the
// reconciler impersonates. nil pairs with RestConfig nil — both off
// means "use r.Client". With RestConfig set, TokenCache must be set
// too; defaultBuilder wires the pair.
TokenCache *tokenCache
// ClientCache memoizes the impersonating controller-runtime client per
// (namespace, SA) so a reconcile against a cached, unchanged tenant
// token can skip client.New entirely (which builds a fresh RESTMapper
// + transport — non-trivial on the per-event hot path). nil disables
// caching; tenantClient still works, just constructs a client on every
// call. defaultBuilder wires this together with TokenCache.
ClientCache *tenantClientCache
// CycleCache memoizes the dependency-cycle verdict per snippet UID,
// keyed by snip.Generation. The watch handlers (mapJsonnetLibrary,
// mapFluxSource) Forget the entries they enqueue so a library or
// upstream-source change re-triggers the walk — generation alone does
// not catch a transitively-introduced cycle. nil disables caching;
// detectSourceRefCycle still works, just walks the graph every
// reconcile.
CycleCache *cycleCache
// DefaultServiceAccount fills in for snippets that omit
// spec.serviceAccountName. Empty leaves such snippets rejected with
// ReasonServiceAccountMissing.
DefaultServiceAccount string
// NoCrossNamespaceRefs mirrors Config.NoCrossNamespaceRefs; when true,
// a snippet referencing a library outside its own namespace fails with
// ReasonCrossNamespaceRefRejected.
NoCrossNamespaceRefs bool
// ExtVars is the operator-level external-variable map. Conflicting CR
// keys are rejected with ReasonExternalVariableConflict.
ExtVars map[string]string
// EvaluationTimeout bounds a single eval; zero disables the bound.
EvaluationTimeout time.Duration
// MaxStack overrides go-jsonnet's default; zero keeps the default.
MaxStack int
// Fetcher resolves spec.sourceRef into in-memory files via Flux source
// CRs. nil makes any sourceRef return ReasonSourceRefNotYetSupported
// — useful for tests that don't model source-controller. Production
// defaultBuilder always wires sources.New.
Fetcher SourceFetcher
// Publisher writes the artifact tarball and upserts the matching
// ExternalArtifact CR. nil disables publication — useful for unit tests
// that only exercise the eval pipeline.
Publisher *Publisher
// Limiter applies per-snippet rate limiting before each eval+publish.
// nil disables the limiter (tests, or operator started with
// --rerender-rate=0).
Limiter *RateLimiter
// Clock is the time source for RevisionEntry timestamps. nil falls
// back to time.Now — tests inject a fake.
Clock func() time.Time
// EventRecorder emits standard Kubernetes Events on Ready-condition
// transitions so Flux's notification-controller (or any other
// Event-sourced alerter) can route via Alert CRs targeting
// JsonnetSnippet. nil disables event emission. The reason and
// message mirror what's written to the Ready condition; severity
// is Normal for Synced and Warning for every other reason.
//
// Uses the events.v1 API (k8s.io/client-go/tools/events) — the
// older record.EventRecorder was deprecated in controller-runtime.
// notification-controller listens on both forms.
EventRecorder events.EventRecorder
// OCILibraries mirrors Config.OCILibraries — the byte contents of
// every operator-shipped (OCI-mounted) library, keyed by alias.
// resolveLibraries merges these into the per-snippet library map
// after the CR loop, so snippets can `import "<alias>/file"`
// against shared libraries without a CR LibraryRef.
OCILibraries map[string]eval.Library
// KnownLibraryAliases mirrors Config.KnownLibraryAliases — populated
// at SetupWithManager time. The reconciler consults it to reject
// LibraryRef.ImportPath values that collide with OCI-mounted
// library names.
KnownLibraryAliases []string
// MaxWithdrawWait bounds the time a deleted snippet's finalizer
// can hold while Publisher.Withdraw keeps failing. Past the
// bound, reconcileDelete force-drops the finalizer, emits a
// Warning WithdrawForced event, and the snippet is GC'd —
// possibly leaving an orphan tarball. Zero falls back to
// defaultMaxWithdrawWait. See Config.MaxWithdrawWait for the
// rationale.
MaxWithdrawWait time.Duration
// Logger receives reconcile-level logs. nil falls back to slog.Default.
Logger *slog.Logger
// contains filtered or unexported fields
}
SnippetReconciler is the controller-runtime reconciler for JsonnetSnippet.
Source resolution + eval + publish all flow through this reconciler. The Client field is the manager's broad-permission client and is used only for operations on the snippet itself (Get/Update/Status). Every tenant-side API call — library Gets, ExternalArtifact CRUD — runs through a fresh impersonating client built from RestConfig + spec.serviceAccountName so the operator can't read or write resources the tenant SA shouldn't reach.
func (*SnippetReconciler) EngageFluxWatch ¶
func (r *SnippetReconciler) EngageFluxWatch(ctx context.Context, gvk schema.GroupVersionKind) error
EngageFluxWatch wires a new Flux source kind into the live controller. Called by the crdWatcher when a previously-missing CRD becomes Established. The newly-installed kind's initial-list events fan out through mapFluxSource into snippets that reference any instance, so dependents get retried automatically — no process restart, no manual nudge needed.
Idempotent: re-engaging an already-watched GVK is a no-op (the source already exists in the controller's source list, and the underlying informer is shared per cache).
func (*SnippetReconciler) MissingFluxSourceKinds ¶
func (r *SnippetReconciler) MissingFluxSourceKinds() []schema.GroupVersionKind
MissingFluxSourceKinds returns a snapshot of the Flux source kinds that aren't installed in the cluster yet. Populated by SetupWithManager and pruned by EngageFluxWatch as the crdWatcher engages dynamic watches over time.
Returns a defensive copy so callers can iterate freely without holding the lock.
func (*SnippetReconciler) Reconcile ¶
Reconcile is invoked for every JsonnetSnippet create/update/delete event the manager surfaces to this controller.
func (*SnippetReconciler) SetupWithManager ¶
func (r *SnippetReconciler) SetupWithManager(mgr ctrl.Manager) error
SetupWithManager registers this reconciler with mgr, watching JsonnetSnippet objects plus three secondary kinds whose changes affect a snippet's rendered output:
- JsonnetLibrary — a referenced library's bytes change → re-render every snippet that imports it.
- Flux source CRs (GitRepository, OCIRepository, Bucket, ExternalArtifact) — a referenced source's status.artifact flips → re-fetch and re-render every snippet whose spec.sourceRef points at it. The Flux watches are gated on the RESTMapper resolving each GVK so the operator boots cleanly in clusters without source-controller installed; missing kinds are accumulated on the reconciler so a crdPoller can pick them up.
type SnippetValidator ¶
type SnippetValidator struct {
// OperatorExtVars is the operator-level external-variable set.
OperatorExtVars map[string]string
// KnownLibraryAliases enumerates OCI-mounted library aliases the
// operator was started with. A LibraryRef.ImportPath that shadows
// one is rejected at admission so the user notices the OCI mount is
// being silently overridden — empty disables the check.
KnownLibraryAliases []string
// Client reads the existing snippet graph for cycle detection. nil
// disables cycle checks at admission — the reconciler still enforces
// the invariant. defaultBuilder always wires the manager's client.
Client client.Client
}
SnippetValidator is the admission webhook for JsonnetSnippet. It rejects:
- CRs whose spec.externalVariables collide with the operator's --ext-var set
- CRs whose spec.sourceRef chain forms a dependency cycle through other JaaS-published ExternalArtifacts
The reconciler enforces the same invariants as fallbacks, so a bypassed webhook still produces Ready=False with the matching reason — but admission gives the user immediate feedback on `kubectl apply`.
func (*SnippetValidator) SetupWithManager ¶
func (v *SnippetValidator) SetupWithManager(mgr ctrl.Manager) error
SetupWithManager registers v as a validating webhook on mgr. The path is fixed; the helm chart's ValidatingWebhookConfiguration must point at it.
func (*SnippetValidator) ValidateCreate ¶
func (v *SnippetValidator) ValidateCreate(ctx context.Context, snip *jaasv1.JsonnetSnippet) (admission.Warnings, error)
ValidateCreate is called on every create request before persistence.
func (*SnippetValidator) ValidateDelete ¶
func (v *SnippetValidator) ValidateDelete(_ context.Context, _ *jaasv1.JsonnetSnippet) (admission.Warnings, error)
ValidateDelete is called on every delete request. We have no delete-time invariants to enforce, so this always passes.
func (*SnippetValidator) ValidateUpdate ¶
func (v *SnippetValidator) ValidateUpdate(ctx context.Context, _ *jaasv1.JsonnetSnippet, snip *jaasv1.JsonnetSnippet) (admission.Warnings, error)
ValidateUpdate is called on every update request before persistence.
type SourceFetcher ¶
type SourceFetcher interface {
Fetch(ctx context.Context, c client.Client, ref *jaasv1.SourceRef, ownerNs string) (*sources.Result, error)
}
SourceFetcher is the small interface SnippetReconciler depends on for resolving spec.sourceRef. *sources.Fetcher (production) and test stubs satisfy it; the indirection lets reconciler tests inject failure shapes without standing up an HTTP server.