provisioners

package
v1.18.0 Latest Latest
Warning

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

Go to latest
Published: Jun 28, 2026 License: Apache-2.0 Imports: 6 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, Blocked(...) requeue on the fixed yield timeout next reconcile
ErrTerminal Terminal(reason, why) write Errored, stop requeuing operator intervention
ErrUserActionRequired UserActionRequired(reason, why) write Errored, 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 and the why

Error wraps a disposition with two extra fields so a single returned value serves both audiences cleanly:

  • Reason() — a short, stable, closed-vocabulary code (e.g. insufficient_capacity). Safe to surface to users; this is what the manager writes into the Available condition message.
  • Why() — operator-facing detail. Log-safe, not user-safe. It is embedded in Error() so existing logging surfaces it for free, but it is deliberately kept out of the user-visible condition (CWE-209). Routing it to a user surface is a separate, explicit decision.

New failure modes should return a typed Error rather than a bare error: the manager's untyped fallback stringifies the raw error into the user-visible message, which is a known fail-open leak.

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 carries an explicit, structured why alongside its disposition.

WHY: the manager funnels every provisioner error through a single condition message, and the default path stringifies the raw error into user-visible text — a fail-open leak of internal detail (CWE-209). At the same time, an operator reading logs wants exactly that detail. One free-text field cannot honestly serve both audiences. This type separates the three things that actually matter:

  • disposition: the machine-switchable outcome (ErrTerminal, ErrUserActionRequired, ErrYield, ...), exposed via Unwrap so errors.Is keeps working. Disposition lives in the type system, never in prose.
  • reason: a short, stable code (e.g. "insufficient_capacity", "invalid_flavor") suitable for mapping to a closed user-facing vocabulary later, without re-parsing strings.
  • why: an operator-facing detail string for logs and debugging. It is NOT intended to reach the user verbatim; routing it to a user surface is a deliberate, separate decision.

The quick win it buys today, with no wiring: Error() embeds the why, so the existing log.Error(err, ...) call in the manager logs the reason and why for free — you get useful operator logs the moment a provisioner returns one of these, well before any condition-surfacing work lands.

func Blocked added in v1.18.0

func Blocked(kind, id string) *Error

Blocked returns a yield-dispositioned error that names the dependency being waited on.

WHY: dependency waits currently collapse into a single opaque "Provisioning" state — you cannot tell a resource that is doing work from one parked on a dependency. Blocked still requeues (it unwraps to ErrYield), but records which resource is the blocker. The kind/id is the user's own resource, so it is safe to surface, and it makes the wait self-explanatory in logs immediately.

func Terminal added in v1.18.0

func Terminal(reason, why string) *Error

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

func UserActionRequired added in v1.18.0

func UserActionRequired(reason, why string) *Error

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

func (*Error) Error added in v1.18.0

func (e *Error) Error() string

Error implements the error interface. The why is included so existing, unmodified logging surfaces it without any extra plumbing.

func (*Error) Reason added in v1.18.0

func (e *Error) Reason() string

Reason returns the short stable failure code.

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.

func (*Error) Why added in v1.18.0

func (e *Error) Why() string

Why returns the operator-facing detail. Treat as log-safe, not user-safe.

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