wfctlhelpers

package
v0.60.4 Latest Latest
Warning

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

Go to latest
Published: May 20, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package wfctlhelpers hosts the wfctl-side dispatch helper for v2 IaC plugins. wfctl calls ApplyPlanWithHooks (or the legacy ApplyPlan) when a plugin manifest declares iacProvider.computePlanVersion: v2 (see plugin/sdk.IaCProvider). The helper iterates plan.Actions, fetches the matching ResourceDriver from the provider, and dispatches each action to a per-action sub-function (doCreate, doUpdate, doReplace, doDelete).

Action lifecycle versions (workflow#640 migration)

Two callers can drive plan execution:

  • ApplyPlan (legacy, marked Deprecated) — empty-hooks equivalent of ApplyPlanWithHooks. State persistence happens at whole-plan completion only.

  • ApplyPlanWithHooks (v2, recommended) — caller-supplied per-action OnResourceApplied / OnResourceDeleted hooks fire at each successful cloud-mutation boundary. Required for #640's invariants.

See docs/migrations/2026-05-16-v2-lifecycle-phase1-inventory.md and decisions/0040-v2-action-lifecycle-provider-compatibility.md for the migration contract; ApplyPlan will be removed in Phase 5.

Lifecycle inside W-3a:

  • T3.1 (this file's ApplyPlan + dispatch + skeleton sub-functions)
  • T3.1.5 — wraps ApplyPlan with the input-drift postcondition
  • T3.2 — fills doCreate with UpsertSupporter recovery
  • T3.3 — fills doUpdate + doDelete (the latent doDelete bug fix)
  • T3.4 — fills doReplace and populates ApplyResult.ReplaceIDMap

Until W-3b lands the cmd/wfctl dispatch wiring, ApplyPlan has no in-tree caller — the helper ships in W-3a as foundation only and is exercised solely by this package's tests.

Per-action error-prefix policy

Sub-functions follow a "decompose-then-prefix" rule for the strings recorded in interfaces.ApplyResult.Errors[].Error:

  • doCreate, doUpdate, doDelete pass driver errors through unchanged. The ActionError struct already carries Resource + Action context fields, so a per-kind prefix would be redundant.
  • doCreate's upsert recovery path prefixes "upsert: " (e.g., "upsert: read after conflict: ...") because the failure is specifically about the recovery flow, not the original Create.
  • doReplace prefixes "replace: delete: " or "replace: create: " because a Replace decomposes into two driver calls — without the prefix, an operator reading result.Errors couldn't tell which sub-step failed.

Tests in apply_update_delete_test.go and apply_replace_test.go lock this contract via exact-string assertions; future refactors that drop or rename a prefix fail loudly.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyPlan deprecated

ApplyPlan dispatches each plan action to the matching ResourceDriver on the provider. Per-action errors are recorded on result.Errors and do NOT abort the loop — apply best-effort across actions, surface every failure for the operator to triage. Context cancellation between actions IS respected: when ctx is canceled or its deadline expires, the loop stops at the next iteration boundary and returns ctx.Err() as the top-level error so a long apply terminates promptly on Ctrl-C / SIGTERM.

At entry ApplyPlan captures result.InitialInputSnapshot by fingerprinting every name listed in plan.InputSnapshot through the OS env. After the dispatch loop completes — successfully or not — a deferred postcondition computes result.InputDriftReport against an apply-time snapshot taken through inputsnapshot.NewTolerantEnvProvider (sub-action env unsets are preserved, not flagged as drift). The postcondition is wrapped in recover() so a buggy env-provider closure cannot corrupt apply results; on panic, InputDriftReport is reset to nil and a warning is logged.

The function is concurrency-safe with respect to its inputs: result is owned by ApplyPlan for the duration of the call and is not shared with the provider or driver implementations.

T3.1 ships the dispatch skeleton; T3.1.5 added the postcondition above; T3.2/T3.3/T3.4 fill the per-action sub-functions with their full bodies.

Deprecated: use ApplyPlanWithHooks instead. ApplyPlan is the empty-hooks equivalent of ApplyPlanWithHooks and is preserved only for backwards compatibility during the workflow#640 v2 action lifecycle migration. New callers MUST use ApplyPlanWithHooks so per-action state-persistence hooks can fire at each cloud-mutation boundary. See docs/migrations/2026-05-16-v2-lifecycle-phase1-inventory.md and decisions/0040-v2-action-lifecycle-provider-compatibility.md for the migration contract; ApplyPlan will be removed in Phase 5.

func ApplyPlanWithHooks added in v0.51.3

ApplyPlanWithHooks is ApplyPlan plus action-boundary hooks for callers that need durable side effects as each cloud mutation succeeds.

func DefaultReplace added in v0.23.0

DefaultReplace is the engine's default Replace dispatcher: Delete the resource at action.Current, then Create from action.Resource. Public so drivers that opt into ResourceReplacer can delegate a particular spec to engine-default behavior without a sentinel-error round-trip.

Decomposes a Replace action into Delete-then-Create on the driver and propagates the new ProviderID through result.ReplaceIDMap[action.Resource.Name] so the JIT substitution wired into ApplyPlan's loop (T5.2) can patch dependent resources whose configs reference the replaced resource by name.

Cascade contract (T5.3)

When a plan has [Replace parent, X dependent] where dependent's Config carries ${parent.id}, the cascade lands automatically: DefaultReplace's post-Create write to result.ReplaceIDMap completes BEFORE the dispatch loop's next iteration calls jitsubst.ResolveSpec on the dependent's spec, so the dependent's driver call (Create or Replace's post-Delete Create) sees the freshly-resolved parent ProviderID. Delete continues to use action.Current.ProviderID via refFromAction — JIT substitution does NOT alter action.Current, so Replace's Delete still targets the pre-Replace cloud resource.

Verified by apply_replace_cascade_test.go.

Failure semantics:

  • Delete fails → return wrapped "replace: delete: <err>"; Create does NOT run; ReplaceIDMap is NOT populated for this resource.
  • Delete succeeds, ctx canceled before Create → return wrapped "replace: canceled after delete: <err>"; Create does NOT run; ReplaceIDMap is NOT populated. The half-replaced state is the operator's recovery surface (same as the Create-fails case).
  • Delete succeeds, Create fails → return wrapped "replace: create: <err>"; ReplaceIDMap stays empty for this resource. Operators inspect the apply log + the empty-for-this- name slot in ReplaceIDMap to know which resources are in a half-replaced state and need manual cloud restoration.
  • Both succeed → result.Resources gets the new output appended, result.ReplaceIDMap[action.Resource.Name] = new ProviderID. Map is lazily-initialized on first successful Replace so plans with no Replace actions don't carry an empty map through serialisation.

The "replace: ..." prefix is essential because Replace decomposes into two driver calls — without it, an operator reading result.Errors couldn't tell whether the Delete or the Create failed. Other sub-functions (doCreate non-recovery path, doUpdate, doDelete) pass driver errors through unchanged.

func ValidateAllowReplaceProtected

func ValidateAllowReplaceProtected(plan interfaces.IaCPlan, allow map[string]struct{}) error

ValidateAllowReplaceProtected gates dispatch on the per-resource `protected: true` annotation. It walks every replace or delete action in plan and aggregates ALL blockers (resources protected and not in `allow`) into a single error before returning, so the operator sees the full set of names in one apply attempt and can authorize them with one round-trip via the pre-formatted --allow-replace=<csv> value.

Format (W-6/T6.2):

plan would require destructive action on N protected resource(s):
  <name1> (replace)
  <name2> (delete)
  ...
to authorize, re-run with:
  --allow-replace=<name1>,<name2>,...

Both names and the csv preserve plan-action declaration order so the output is deterministic across runs.

`protected: true` is sourced from PlanAction.Resource.Config for replace actions (where Resource carries the desired spec) and from PlanAction.Current.AppliedConfig for delete actions (where platform.differ leaves Resource.Config empty and the protected status is preserved on the previously-applied state).

Promoted from cmd/wfctl in W-7/T7.9 so the iac/conformance suite can exercise the gate contract without importing package main. The cmd/wfctl validateAllowReplaceProtected wrapper now delegates here; behavior and error format are byte-identical with the pre-W-7 implementation, so all existing tests in cmd/wfctl continue to pass without modification.

Types

type ApplyPlanHooks added in v0.51.3

type ApplyPlanHooks struct {
	OnResourceApplied func(context.Context, interfaces.ResourceDriver, interfaces.PlanAction, interfaces.ResourceOutput) error
	OnResourceDeleted func(context.Context, interfaces.PlanAction) error
	// OnPlanComplete fires once after the per-action loop reaches its
	// natural success-exit return at the end of
	// applyPlanWithEnvProviderAndHooks, i.e., when the outer function is
	// about to return (result, nil). Used by the DO plugin's deferred-
	// flush integration via IaCProviderFinalizer.FinalizeApply RPC.
	//
	// Does NOT fire on:
	//   - The preflightProviderOwnedReplaceWithDeleteHooks early-return —
	//     loopReached=false, no cloud work happened.
	//   - The per-action loop's `if fatalErr != nil { return ... }`
	//     early-return — outer err != nil. v1 semantic preservation per
	//     cycle-1 plan-review C-3: DOProvider.Apply skips deferred-flush
	//     when wfctlhelpers.ApplyPlan returns a top-level err (the
	//     `if err != nil { return ... }` guard in DOProvider.Apply
	//     immediately after the ApplyPlan call in
	//     workflow-plugin-digitalocean internal/provider.go).
	//   - The post-loop length-invariant check that compares
	//     len(result.Actions) against len(plan.Actions) — outer err != nil.
	//
	// DOES fire on per-action driver-error paths (best-effort; driver
	// errors append to result.Errors but do NOT set fatalErr; the loop
	// continues and reaches the natural success-exit return). Mirrors v1
	// DOProvider.Apply behavior where the deferred-flush ran whenever
	// the wrapped Apply returned nil err, regardless of per-action
	// result.Errors entries.
	//
	// Per workflow#695 Phase 2.5 / ADR 0024 / ADR 0040.
	OnPlanComplete func(context.Context) error
}

ApplyPlanHooks are optional callbacks invoked immediately after a plan action successfully mutates cloud-side state. Hooks let wfctl persist state at the action boundary instead of waiting for the whole plan to finish.

Jump to

Keyboard shortcuts

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