provisioners

package
v1.19.0 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: Apache-2.0 Imports: 9 Imported by: 17

README

pkg/provisioners

Intention

pkg/provisioners defines the repository's reconciler-oriented provisioning contract. It is the abstraction layer used to drive resource lifecycle through idempotent Provision(ctx) and Deprovision(ctx) operations that can make partial progress, yield, and converge over repeated controller-runtime retries.

This package itself is intentionally small:

  • the Provisioner and ManagerProvisioner interfaces
  • the RemoteCluster interface for deriving remote kubeconfigs and identities
  • shared metadata for provisioner names
  • shared sentinel errors and error dispositions (ErrYield, ErrTerminal, ErrUserActionRequired) plus the Error carrier type

Most of the real behavior lives in the lower-level adapters and combinators under this directory.

Error Dispositions

A provisioner communicates what the controller should do next through the disposition of the error it returns. The disposition — not a human-readable message — is the contract; the manager branches on it with errors.Is. This is why dispositions are sentinels, never prose.

Disposition Returned by Manager behaviour Recovery
nil success mark Available: Provisioned n/a
ErrYield ErrYield, Yield(reason, message), DependencyNotReady/DependencyFailed write Available: False; requeue on the fixed yield timeout next reconcile
ErrTerminal Terminal(reason, message), DependencyNotFound write Available: False; stop requeuing operator intervention
ErrUserActionRequired UserActionRequired(reason, message) write Available: False; stop requeuing a spec change (generation bump) wakes the controller

ErrTerminal and ErrUserActionRequired are terminal: requeuing them just burns the workqueue on a failure that will not self-heal. Use provisioners.IsTerminal(err) to test for either. They differ only in how a resource is revived — that difference lives in the watch/predicate layer, not in the requeue decision.

The Error carrier: reason + message

Error pairs a disposition with two surfaced fields, both user-safe, that map one-to-one onto the Available condition:

  • reason — a closed-vocabulary unikornv1.ProvisioningConditionReason (e.g. DependencyNotReady). Machine-classifiable and written straight onto the condition's Reason field. Typed rather than a bare string so the vocabulary is explicit at every call site; the API projection classifies each reason by its coarse disposition (see the server conversion package).
  • message — user-safe human detail (e.g. Network "prod-net" (a1b2) is not ready), written onto the condition's Message field. It is shown to the user, so it must be safe: name the user's own resources, never internal topology.

The manager surfaces these directly via SetProvisioningCondition — reason to Reason, message to Message — through the typed accessors Reason() and Message(). There is no flattening into a single string: with metav1.Condition the reason is a first-class field, so tooling reads Reason natively and the human reads Message.

Genuinely operator-only detail (provider internals, raw upstream errors) must not go in message — wrap the Error with fmt.Errorf instead. Error() embeds the wrapping for logs, and the manager's errors.As recovers only the typed Reason()/Message(), so the wrapping never reaches the user surface (CWE-209). New failure modes should return a typed Error: a bare error falls back to the stringified error on the condition, a known fail-open leak.

Yield(reason, message) is the ErrYield sibling of Terminal and UserActionRequired. Dependency waits are raised only through the enforced constructors — DependencyNotReady / DependencyFailed (both yield) and DependencyNotFound (terminal) — which take the dependency object, bind the reason and disposition together, and compose a safe message via the unexported describeResource (Kind from the scheme's GVK, the mutable display name, and the durable id). Binding them in one call is deliberate: a caller cannot pair a DependencyNotFound with a yield that would spin forever, or forget the description.

Caveat: terminality is not yet wired everywhere

The dispositions exist and the manager honours them, but the providers that should emit them mostly still return bare errors or ErrYield. Adopting them is incremental. In particular, a controller that uses ErrUserActionRequired must also reset any per-resource retry bookkeeping on a generation change, or the resource will immediately re-enter the terminal state instead of retrying.

Invariants And Guard Rails

  • Provisioners are intended to run inside reconcilers. They are expected to be idempotent and safe to revisit across repeated reconcile passes.
  • ErrYield is a core part of the model, not an edge case. It means "stop now and let the controller retry later" when a provisioner would otherwise block for too long waiting on external progress.
  • The preferred progress model is framework-driven retry, not long local retry loops. Provisioners should generally fail or yield quickly and let controller-runtime handle fairness and requeue.
  • Deprovision(ctx) is part of the same convergence model. It may make partial progress and return ErrYield while waiting for external deletion or teardown to complete.
  • ManagerProvisioner is the top-level provisioner shape that bridges directly into the controller-runtime layer for managed resources.
  • RemoteCluster is the narrow interface used by remote-scope provisioners to derive the target cluster identity and kubeconfig.

Package Map

  • application: CD-managed Helm application provisioner. Resolves application/version, derives application identity from resource scope, applies generator customizations, and delegates lifecycle to the CD driver.
  • remotecluster: switches active provisioning scope into a remote cluster, constructs the remote client, and coordinates shared remote lifecycle for descendants.
  • serial: ordered composition combinator for dependency-sensitive children. Provision in order, deprovision in reverse order.
  • concurrent: parallel composition combinator for independent children that should make progress in the same reconcile pass.
  • conditional: binary desired-state gate where predicate false means actively deprovision the child, not merely skip it.
  • resource: legacy single-client.Object adapter. Historical hack, not a recommended pattern for new code.
  • util: small provisioner-side helper bucket, mostly scheduling/config-generation fragments and a couple of operational helpers.

Caveats

  • pkg/provisioners is a contract layer, not a clean architecture in itself. Much of the real complexity is pushed into the subpackages, especially application and remotecluster.
  • The whole model assumes reconciler-friendly behavior: idempotence, quick yields, and retry-based convergence. If child provisioners block internally or depend on one-shot success, the abstraction breaks down.
  • Several important subpackages are tightly coupled to the older context-scoping and in-tree CD model documented in pkg/client, especially application and remotecluster.
  • Not every subpackage here is equally healthy. resource is legacy baggage, and some of the extension seams in application are historical compromise rather than clean design.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrYield is raised when a provision/deprovision optation could
	// block for a long time, in particular the bits that wait for apllication
	// available status.  This will trigger a controller to requeue the request.
	// The key things are that workers are unblocked, allowing other reconciles
	// to be triggered, and we can pick up an modifications (e.g. the cluster is
	// gubbed - thanks CAPO - and we can delete it without waiting for 10m as the
	// case used to be in the old world.
	ErrYield = errors.New("controller timeout yield")

	// ErrNotFound is when a resource is not found.
	ErrNotFound = errors.New("resource not found")

	// ErrTerminal marks a provisioning failure that will not self-heal and must
	// stop being requeued.
	//
	// WHY: today a provisioner that returns any error other than ErrYield is
	// requeued at a fixed interval forever (see reconcileNormal). There is no
	// way to say "give up". That is correct for transient faults but wrong for
	// permanent ones: a server whose provider create has been retried to
	// exhaustion, or a request that can never succeed, will otherwise spin the
	// workqueue indefinitely (observed in the wild: a counter climbing into the
	// hundreds against a cap of 3). Revival of an ErrTerminal resource is an
	// operator concern only — nothing the owning user can do to their own spec
	// will unstick it; see ErrUserActionRequired for the recoverable variant.
	//
	// This sentinel is intentionally a disposition, not a message. It is meant
	// to be matched with errors.Is so the manager can branch on it (stop
	// requeuing, write a terminal condition) rather than parsing prose.
	ErrTerminal = errors.New("provisioning terminally failed")

	// ErrUserActionRequired marks a provisioning failure that is terminal *for
	// now* but that the owning user can clear by changing the resource spec.
	//
	// WHY: a deterministic, user-caused failure (an invalid flavour, image, or
	// configuration) should not retry forever — retrying identical bad input is
	// pointless and hides the real problem — but it is also not a support case.
	// The remedy is in the user's hands: edit the spec. Controllers already wake
	// on a generation bump, so an ErrUserActionRequired resource naturally
	// resumes when (and only when) the spec changes. Splitting this from
	// ErrTerminal keeps "you can fix this" distinct from "call an operator",
	// which are different things to tell a user and different recovery paths.
	//
	// NOTE: making this disposition actually recoverable requires the owning
	// controller to clear any retry bookkeeping (e.g. attempt counters) on a
	// generation change; the sentinel alone does not do that.
	ErrUserActionRequired = errors.New("provisioning requires user action")
)

Functions

func IsTerminal added in v1.18.0

func IsTerminal(err error) bool

IsTerminal reports whether an error is a terminal provisioning disposition, i.e. one that must NOT be requeued.

WHY: the manager's requeue decision needs a single, authoritative test for "stop retrying this" rather than open-coding the set of terminal sentinels at each call site (and drifting when a new one is added). Both ErrTerminal and ErrUserActionRequired are terminal in the requeue sense — neither will be helped by another immediate reconcile. They differ only in how they are revived (operator vs spec change), which is a concern for the watch/predicate layer, not for the requeue decision.

Types

type Error added in v1.18.0

type Error struct {
	// contains filtered or unexported fields
}

Error is a provisioning error that pairs a disposition with a user-safe, machine-classifiable surface.

The manager funnels every provisioner error through a single condition message. Stringifying a raw error there is a fail-open leak of internal detail (CWE-209); at the same time, a bare code is unhelpful to the human reading it. Error separates the pieces that serve the different audiences:

  • disposition: the machine-switchable outcome (ErrTerminal, ErrUserActionRequired, ErrYield), exposed via Unwrap so errors.Is keeps working. Lives in the type system, never in prose.
  • reason: a closed-vocabulary ProvisioningConditionReason, machine-classifiable and written straight onto the Available condition's Reason field, e.g. DependencyFailed.
  • message: user-safe human detail, written onto the condition's Message field, e.g. Network "prod-net" (a1b2) is not ready. It IS shown to the user, so it must be safe — name the user's own resources, never internal topology.

The manager surfaces reason and message directly onto the condition via SetProvisioningCondition (see Reason/Message); there is no flattening into a single string. Genuinely operator-only detail (provider internals, raw upstream errors) does NOT go in message: wrap the Error with fmt.Errorf instead. Error() embeds the wrapping for logs, and the manager's errors.As recovers the typed reason/message so the unsafe wrapping never reaches the user surface.

func DependencyFailed added in v1.19.0

func DependencyFailed(scheme *runtime.Scheme, o client.Object) *Error

DependencyFailed signals a wait on a dependency that is itself in an error state. It still yields (the dependency may recover), but names the failure so the wait is not mistaken for progress.

func DependencyNotFound added in v1.19.0

func DependencyNotFound(scheme *runtime.Scheme, o client.Object) *Error

DependencyNotFound signals a referenced dependency that does not exist. It is terminal: waiting cannot resolve it. A referenced, finalized dependency that is nonetheless gone is a consistency violation, not a transient wait.

func DependencyNotReady added in v1.19.0

func DependencyNotReady(scheme *runtime.Scheme, o client.Object) *Error

DependencyNotReady is the sanctioned way to signal a wait on a dependency that exists but is not yet provisioned. It binds the reason, the yield disposition, and a safe description together so a caller cannot mis-pair them.

func Terminal added in v1.18.0

func Terminal(reason unikornv1.ProvisioningConditionReason, message string) *Error

Terminal returns an ErrTerminal-dispositioned error: permanent, operator-only recovery. See ErrTerminal.

func UserActionRequired added in v1.18.0

func UserActionRequired(reason unikornv1.ProvisioningConditionReason, message string) *Error

UserActionRequired returns an ErrUserActionRequired-dispositioned error: terminal until the user changes the spec. See ErrUserActionRequired.

func Yield added in v1.19.0

func Yield(reason unikornv1.ProvisioningConditionReason, message string) *Error

Yield returns an ErrYield-dispositioned error carrying a reason code and user-safe message while still requeuing. It is the ErrYield sibling of Terminal and UserActionRequired.

func (*Error) Error added in v1.18.0

func (e *Error) Error() string

Error implements the error interface, embedding the reason and message so existing, unmodified logging surfaces them for free.

func (*Error) Message added in v1.19.0

func (e *Error) Message() string

Message returns the user-safe human detail to write onto the condition's Message field.

func (*Error) Reason added in v1.18.0

Reason returns the closed-vocabulary provisioning reason to write onto the Available condition's Reason field.

func (*Error) Unwrap added in v1.18.0

func (e *Error) Unwrap() error

Unwrap exposes the disposition sentinel so callers can branch with errors.Is(err, ErrTerminal) etc.

type ManagerProvisioner

type ManagerProvisioner interface {
	Provisioner

	// Object returns a reference to the generic object type, internally
	// the provisioner will have a type specific version.
	Object() unikornv1.ManagableResourceInterface
}

ManagerProvisioner top-level manager provisioners hook directly into the controller runtime layer, and are a little special in that they abstract away type specific things.

type Metadata

type Metadata struct {
	// Name is the name of the provisioner.
	Name string
}

Metadata is a container for geneirc provisioner metadata.

func (*Metadata) ProvisionerName

func (p *Metadata) ProvisionerName() string

ProvisionerName implements the Provisioner interface.

type Provisioner

type Provisioner interface {
	// ProvisionerName returns the provisioner name.
	ProvisionerName() string

	// Provision deploys the requested package.
	// Implementations should ensure this receiver is idempotent.
	Provision(ctx context.Context) error

	// Deprovision does any special handling of resource/component
	// removal.  In the general case, you should rely on cascading
	// deletion i.e. kill the namespace, use owner references.
	// Deprovisioners should be gating, waiting for their resources
	// to be removed before indicating success.
	Deprovision(ctx context.Context) error
}

Provisioner is an abstract type that allows provisioning of Kubernetes packages in a technology agnostic way. For example some things may be installed as a raw set of resources, a YAML manifest, Helm etc.

type RemoteCluster

type RemoteCluster interface {
	// ID is the unique resource identifier for this remote cluster.
	ID() *cd.ResourceIdentifier

	// Config returns the client configuration (aka parsed Kubeconfig.)
	Config(ctx context.Context) (*clientcmdapi.Config, error)
}

Generator is an abstraction around the sources of remote clusters e.g. a cluster API or vcluster Kubernetes instance.

Directories

Path Synopsis
Code generated by MockGen.
Code generated by MockGen.

Jump to

Keyboard shortcuts

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