jitsubst

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: May 5, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package jitsubst implements just-in-time substitution for an IaC interfaces.ResourceSpec at apply time.

Why JIT

Plan time computes a interfaces.IaCPlan over modules whose configs may reference values that don't exist yet — e.g. a database password that a sub-action will populate, or the ProviderID of a sibling resource that the same apply will create. wfctlhelpers.ApplyPlan calls ResolveSpec right before dispatching each interfaces.PlanAction so the driver sees fully-substituted Config.

Reference forms

Three substitution forms are recognized inside any string value of interfaces.ResourceSpec.Config (recursively, including nested maps and slices):

${VAR}             → envLookup(VAR)
${MODULE.id}       → replaceIDMap[MODULE], else syncedOutputs[MODULE]["id"]
${MODULE.field}    → syncedOutputs[MODULE][field]

The discriminator between ${VAR} and ${MODULE.field} is the presence of a "." inside the body. Env-var names cannot contain "." per POSIX, so the rule is unambiguous.

Source precedence for ${MODULE.id}

replaceIDMap is consulted FIRST. The map is populated by W-3b's doReplace interfaces.ApplyResult.ReplaceIDMap every time a Replace action successfully Delete-then-Creates a resource — its value is the post-Replace ProviderID. syncedOutputs holds outputs read from durable state (per W-5's design "JIT resolution reads from STATE … and from replaceIDMap"); state may have a stale ProviderID for a just-replaced resource until the apply loop persists the new one. Preferring replaceIDMap means dependents of a cascade-replaced parent see the new ID without depending on state-write ordering.

Strict resolution semantics

Every reference MUST resolve. An unset env var, missing module, or missing field returns an error — matching the JIT contract that unresolved-at-apply-time refs are operator-actionable bugs, NOT silent-empty-string substitutions like os.ExpandEnv. Plan-time has already collapsed every resolvable ${VAR} via config.ExpandEnvInMap; a surviving env-var reference at apply time is therefore meaningful.

Error handling and partial state

On error the original input spec is returned unmodified — callers MUST NOT use a partially-resolved spec since some fields may have substituted and others not. The first unresolved reference wins; resolution within a single string is left-to-right via Go's regexp.Regexp.ReplaceAllStringFunc. Across maps, keys are walked in sorted order so that error messages are deterministic across Go map-iteration randomization.

Mutation contract

ResolveSpec deep-copies Config (including nested maps and slices) before substitution. Caller-side mutation of the returned spec cannot poison the input.

Lifecycle

  • T5.1 (this file) defines the helper.
  • T5.2 wires it into wfctlhelpers.ApplyPlan's per-action dispatch — a single ResolveSpec call per PlanAction immediately before driver dispatch.
  • T5.3 verifies cascade behavior: doReplace populates ApplyResult.ReplaceIDMap when a Replace action successfully Delete-then-Creates, and the existing T5.2 ResolveSpec call site consumes that map on subsequent PlanActions in the same apply loop. T5.3 does NOT add a second ResolveSpec call inside doReplace — see ADR 008 for the rationale (single resolution boundary, ReplaceIDMap-first ordering).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func HasModuleRefs

func HasModuleRefs(v any) bool

HasModuleRefs returns true when any string value reachable from v (recursively walking map[string]any and []any) contains a JIT-style ${MODULE.field} reference — i.e., a ${...} reference whose body has a "." separator with non-empty segments on both sides. Plain ${VAR} env-var references (no dot in body) do NOT count.

Used by cmd/wfctl/infra.go::runInfraPlan (T5.4) to decide whether to stamp plan.SchemaVersion = 2 (JIT-required) and by the persisted-plan rejection in T5.5. Walking input is intentionally permissive: pass any value (a single map[string]any, a slice, a ResourceSpec.Config, even a raw string) — non-string scalars are ignored, nil yields false.

func ResolveSpec

func ResolveSpec(
	spec interfaces.ResourceSpec,
	replaceIDMap map[string]string,
	syncedOutputs map[string]map[string]any,
	envLookup func(string) (string, bool),
) (interfaces.ResourceSpec, error)

ResolveSpec returns a deep copy of spec where every ${...} reference in any string value of spec.Config (recursively, including nested maps and slices) has been resolved against the supplied substitution sources. See the package docstring for reference forms, source precedence, and strict-resolution semantics.

Inputs:

  • replaceIDMap: resource Name → new ProviderID for resources replaced earlier in this apply (W-3b/T3.4 doReplace).
  • syncedOutputs: resource Name → outputs map (typed via state).
  • envLookup: env-var resolver; pass os.LookupEnv in production. May be nil — every ${VAR} reference will then error as unset, but ${...} references that don't reach the env-var path will still resolve.

On error the input spec is returned unmodified; callers MUST NOT use a partially-resolved spec.

Types

This section is empty.

Jump to

Keyboard shortcuts

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