Documentation
¶
Overview ¶
Package autoinit implements Atmos's "smart init" policy for Terraform/OpenTofu: skip the `terraform init` subprocess when nothing that init cares about has changed since the last successful init, add `-reconfigure` only when the backend configuration itself changed, add `-upgrade` only when explicitly requested or required, and recover automatically when terraform/tofu reports that init is required after all.
The three moving parts ¶
- Compute derives a Fingerprint from an Inputs value: a digest over every file and environment setting that can change what `terraform init` would do (root configuration files, the dependency lock file, var files, CLI config, the resolved binary, and relevant environment variables), plus a narrower Fingerprint.BackendHash over only the backend-relevant subset of those files.
- Marker is the small JSON record (WriteMarker / ReadMarker / Record) Atmos writes into the Terraform data directory after a successful init, capturing the fingerprint that was true at that moment.
- Decide compares a fresh Compute against the last Marker (and a handful of filesystem preconditions -- were providers actually installed, are modules present, does local state exist) to produce a Decision: whether to run init at all, and with which flags.
Recovering from a stale skip ¶
Skipping init is a bet that nothing relevant changed; Classify and ShouldRecover are the fallback when that bet is wrong. Classify inspects terraform/tofu's own diagnostic output for known "you need to run init" signatures, and ShouldRecover turns that diagnosis into a concrete recovery action -- respecting the caller's configured init policy, including erroring out (rather than silently re-running init) when the user explicitly disabled it.
All file paths that contribute to a fingerprint are recorded by name relative to Inputs.ComponentPath, never by absolute path, so the same component checked out at two different locations on disk produces identical fingerprints, and files written to a process-unique temporary path (e.g. Atmos's generated `TF_CLI_CONFIG_FILE`) are hashed by content rather than by their throwaway path.
Index ¶
- Constants
- func AnnounceSkipped(info *schema.ConfigAndStacksInfo, reason Reason)
- func ApplyRecovery(args []string, rec Recovery) []string
- func DataDir(componentPath string, lookup func(key string) (string, bool)) string
- func InitArgs(d Decision, passVars bool, varFile string) []string
- func InvalidateFromInfo(in *Inputs)
- func InvalidateMarker(path string) error
- func MarkerPath(dataDir string) string
- func OptedOut(atmosConfig *schema.AtmosConfiguration, info *schema.ConfigAndStacksInfo) bool
- func Record(in *Inputs, initArgs []string, atmosVersion string) error
- func RecordFromInfo(in *Inputs, initArgs []string)
- func Recover(p RecoverParams) error
- func WriteMarker(path string, m *Marker) error
- type Decision
- type Diagnosis
- type Fingerprint
- type Inputs
- type Marker
- type Reason
- type RecoverInitParams
- type RecoverParams
- type Recovery
- type Request
Constants ¶
const MarkerFileName = "atmos-init.json"
MarkerFileName is the name of the init marker file, written inside the Terraform data directory.
const MarkerSchemaVersion = 1
MarkerSchemaVersion is the current on-disk schema version for Marker. Bumping it forces every existing marker to be treated as stale (ReasonSchemaVersion) the next time Decide runs.
Variables ¶
This section is empty.
Functions ¶
func AnnounceSkipped ¶
func AnnounceSkipped(info *schema.ConfigAndStacksInfo, reason Reason)
AnnounceSkipped tells the user (on the UI/stderr channel, never stdout) that a fresh `terraform init` was determined to be unnecessary this run.
func ApplyRecovery ¶
ApplyRecovery returns a copy of args with -upgrade and/or -reconfigure appended when rec calls for them and they aren't already present.
func DataDir ¶
DataDir resolves the Terraform data directory the subprocess will actually use: lookup(TF_DATA_DIR) wins whenever it reports the key as present (its second return value), even when the value itself is an explicit empty string, falling back to os.Getenv(TF_DATA_DIR) only when lookup is nil or reports the key absent, and finally to the conventional ".terraform" default; lookup models the subprocess environment Atmos is about to launch terraform/tofu with. A relative result is joined to componentPath so callers always receive an absolute-ish path suitable for filesystem checks; the result is always filepath.Clean'ed.
func InitArgs ¶
InitArgs builds the `terraform init` argument list from a Decision: `init`, then `-reconfigure` when d.Reconfigure, then `-upgrade` when d.Upgrade, then `-var-file <varFile>` when passVars is enabled.
func InvalidateFromInfo ¶
func InvalidateFromInfo(in *Inputs)
InvalidateFromInfo best-effort invalidates the init marker immediately before an init attempt whose outcome isn't known yet: if that attempt fails, a stale-but-still-matching marker must not survive it, or a later, unrelated invocation could wrongly decide (ReasonUpToDate) that init is unnecessary when the last actual init attempt never succeeded. A nil in (dry run) is a no-op; callers re-record a fresh marker via RecordFromInfo once the attempt that follows this call actually succeeds.
func InvalidateMarker ¶
InvalidateMarker best-effort removes the marker at path so a subsequent Decide treats init as required (ReasonNoMarker) rather than trusting a marker recorded before an init attempt whose outcome isn't known yet. A missing file is not an error -- there is nothing to invalidate.
func MarkerPath ¶
MarkerPath returns the path of the init marker file inside dataDir.
func OptedOut ¶
func OptedOut(atmosConfig *schema.AtmosConfiguration, info *schema.ConfigAndStacksInfo) bool
OptedOut reports whether the caller explicitly disabled implicit init through any of Atmos's own opt-out signals: --skip-init, components.terraform.init.mode: never, or deploy_run_init: false on a deploy. Passed as Recover's OptedOut so a diagnosed init-required failure becomes a clear, actionable error (never a silent init) when the user asked not to initialize on their behalf.
func Record ¶
Record recomputes the fingerprint from in (the lock file, and anything else init may have changed, is re-read post-init) and writes the resulting marker to MarkerPath(dataDir).
func RecordFromInfo ¶
RecordFromInfo best-effort records the init marker after a successful init. A failure only means smart init may unnecessarily re-run next time -- it never fails the (already successful) command that just completed. A nil in (dry run) is a no-op.
func Recover ¶
func Recover(p RecoverParams) error
Recover is smart init's plan/apply-time safety net: when the main command a caller just ran failed, it classifies p.Output for a known "init is required" diagnostic (Classify) and, if policy allows it (ShouldRecover), calls p.RunInit with the flag(s) the diagnostic asked for, then p.Retry exactly once. Returns p.Err unchanged when p.Skip is set, when nothing was diagnosed, or when ShouldRecover declines to recover. Returns errors.Join(p.Err, policyErr) when ShouldRecover reports a policy error (e.g. the caller explicitly disabled implicit init) -- that policy error is never silently swallowed. Returns errors.Join(p.Err, initErr) when p.RunInit itself fails.
func WriteMarker ¶
WriteMarker writes m to path as JSON, creating parent directories as needed and writing atomically so a concurrent reader never observes a partially written marker.
Types ¶
type Decision ¶
type Decision struct {
// RunInit reports whether `terraform init` should run at all.
RunInit bool
// Reconfigure reports whether `-reconfigure` should be added.
Reconfigure bool
// Upgrade reports whether `-upgrade` should be added.
Upgrade bool
// Reason explains RunInit.
Reason Reason
// Fingerprint is the computed fingerprint; zero when it was never computed (e.g. dry run, or
// init.mode is never).
Fingerprint Fingerprint
// Marker is the previously recorded marker; nil when absent, malformed, or never read.
Marker *Marker
}
Decision is the outcome of Decide.
type Diagnosis ¶
type Diagnosis struct {
// InitRequired reports whether the output indicates `init` must run again.
InitRequired bool
// ReconfigureRequired reports whether the backend configuration changed.
ReconfigureRequired bool
// UpgradeRequired reports whether provider/module constraints require `-upgrade`.
UpgradeRequired bool
// Matched is the signature substring that produced this Diagnosis, for debug logs.
Matched string
}
Diagnosis summarizes what terraform/tofu output told us about the init state after a subcommand failed.
func Classify ¶
Classify inspects raw terraform/tofu output for known init-related diagnostics and returns the combined Diagnosis; output is ANSI-stripped first, since captured subprocess output may still carry color codes. Every matching signature is scanned (not just the first) and their flags are OR'd together, since a single failure can legitimately surface more than one diagnostic (e.g. both "Backend configuration changed" and a "-upgrade" notice) and recovery needs to know about all of them to run a single init with every required flag. Matched joins every matching signature's text (comma-separated) for debug logging. Returns a zero Diagnosis when nothing matches.
type Fingerprint ¶
type Fingerprint struct {
// Hash is the sha256 hex digest over every input that can affect `terraform init`.
Hash string
// BackendHash is the sha256 hex digest over only the backend-relevant subset of files.
BackendHash string
// Files lists the relative file names that contributed to Hash, sorted, for debug logs.
Files []string
}
Fingerprint is the computed digest of a set of Inputs.
func Compute ¶
func Compute(in *Inputs) (Fingerprint, error)
Compute derives a Fingerprint from in. Every file that contributes to the digest is recorded by name relative to in.ComponentPath, never by absolute path, so two checkouts of the same component at different locations on disk produce identical fingerprints.
type Inputs ¶
type Inputs struct {
// ComponentPath is the absolute component (or workdir) directory init would run in.
ComponentPath string
// DataDir is the Terraform data directory; if empty it is computed with
// DataDir(ComponentPath, EnvLookup).
DataDir string
// VarFile is the varfile path (absolute, or relative to ComponentPath); hashed only when
// PassVars is true.
VarFile string
// PassVars indicates whether Atmos is passing a varfile (and TF_VAR_* extras) to terraform.
PassVars bool
// Binary is the resolved terraform/tofu executable. Bare names are resolved via
// exec.LookPath; on failure only the name itself is recorded.
Binary string
// EnvLookup models the subprocess environment Atmos is about to launch terraform/tofu with.
// Its second return value distinguishes "key present with this value" (honored as-is, even
// when the value is "") from "key absent" (falls back to os.Getenv). A nil EnvLookup falls
// back to os.Getenv unconditionally.
EnvLookup func(key string) (string, bool)
// Extra holds caller-supplied records (e.g. TF_VAR_* values when PassVars is true) that
// should participate in the fingerprint.
Extra map[string]string
}
Inputs describes everything that determines whether a previously recorded init is still valid.
func InputsFromInfo ¶
func InputsFromInfo(atmosConfig *schema.AtmosConfiguration, info *schema.ConfigAndStacksInfo, componentPath, varFile string) *Inputs
InputsFromInfo builds the Inputs describing this invocation's init fingerprint inputs, or nil on a dry run -- there is no component on disk yet to fingerprint, and Decide treats a nil Inputs as "always run init" (ReasonNoInputs).
type Marker ¶
type Marker struct {
// SchemaVersion is the on-disk schema version of this marker; compared against
// MarkerSchemaVersion by Decide.
SchemaVersion int `json:"schema_version"`
// Fingerprint is the Fingerprint.Hash recorded at init time.
Fingerprint string `json:"fingerprint"`
// BackendFingerprint is the Fingerprint.BackendHash recorded at init time.
BackendFingerprint string `json:"backend_fingerprint"`
// InitArgs are the extra arguments (e.g. "-upgrade", "-reconfigure") the recorded init ran
// with.
InitArgs []string `json:"init_args"`
// AtmosVersion is the Atmos version that performed the init.
AtmosVersion string `json:"atmos_version"`
// Binary is the terraform/tofu executable that performed the init.
Binary string `json:"binary"`
// Timestamp is when the recorded init completed, in UTC.
Timestamp time.Time `json:"timestamp"`
}
Marker is the small JSON record Atmos writes after a successful `terraform init`, capturing the fingerprint that was true at that moment so a later run can decide whether init is still unnecessary.
func ReadMarker ¶
ReadMarker reads and parses the marker at path. A missing file or malformed JSON is not treated as an error -- both return (nil, nil), since either simply means "no usable marker yet". Any other I/O failure (e.g. a permissions error, or path referring to a directory) is a genuine failure and returns an ErrInitMarker-wrapped error.
type Reason ¶
type Reason string
Reason explains why Decide chose to run (or skip) init.
const ( ReasonModeAlways Reason = "init.mode is always" ReasonModeNever Reason = "init.mode is never" ReasonForced Reason = "init forced by caller" ReasonNoInputs Reason = "no fingerprint inputs (dry run)" ReasonNoMarker Reason = "no init marker found" ReasonSchemaVersion Reason = "init marker schema version changed" ReasonFingerprintChanged Reason = "init inputs changed" ReasonProvidersMissing Reason = "provider plugins are not installed" ReasonModulesMissing Reason = "modules are not installed" ReasonBackendStateMissing Reason = "backend is not initialized" ReasonFingerprintError Reason = "fingerprint could not be computed" ReasonUpToDate Reason = "init is up to date" )
Reasons returned by Decide, in the order they are checked.
type RecoverInitParams ¶
type RecoverInitParams struct {
// Output is the captured stdout+stderr (+ Err.Error()) from the failed init subprocess.
Output string
// Err is init's own error; RecoverInit never calls Rerun when Err is nil.
Err error
Mode schema.TerraformInitMode
Reconfigure schema.TerraformInitReconfigure
Upgrade schema.TerraformInitUpgrade
// Rerun retries init once with the recovery's flags.
Rerun func(rec Recovery) error
// Warn, if set, is called with a human-readable message (and structured key/value pairs,
// mirroring log.Warn) before Rerun runs.
Warn func(msg string, kv ...any)
}
RecoverInitParams bundles what RecoverInit needs to classify a failed `terraform init` subprocess's own output and, if it asked for -upgrade or -reconfigure that weren't already passed, retry init exactly once with the missing flag(s) added.
type RecoverParams ¶
type RecoverParams struct {
// Output is the captured stdout+stderr (+ Err.Error()) from the failed main command.
Output string
// Err is the main command's own error; Recover never calls RunInit/Retry when Err is nil.
Err error
// Mode, Reconfigure, Upgrade are the caller's resolved atmos.yaml init policy (see
// schema.Terraform's Effective* accessors).
Mode schema.TerraformInitMode
Reconfigure schema.TerraformInitReconfigure
Upgrade schema.TerraformInitUpgrade
// OptedOut reflects the caller's own opt-out signals (see OptedOut).
OptedOut bool
// Skip, when true, means recovery must not run at all (e.g. the command that failed was
// itself `init` -- there is no "init required" fallback for init).
Skip bool
// RunInit forces one `terraform init` re-run with the flags rec calls for.
RunInit func(rec Recovery) error
// Retry re-runs the original failed command once, after RunInit succeeds.
Retry func() error
// Warn, if set, is called with a human-readable message before RunInit/Retry run.
Warn func(string)
}
RecoverParams bundles what Recover needs to classify a failed main command's output and, if policy allows it, force one init re-run and retry the main command once.
type Recovery ¶
type Recovery struct {
// Run reports whether init should be re-run.
Run bool
// WithReconfigure reports whether the re-run should add `-reconfigure`.
WithReconfigure bool
// WithUpgrade reports whether the re-run should add `-upgrade`.
WithUpgrade bool
}
Recovery describes the init recovery Atmos should perform after Classify reports a Diagnosis.
func RecoverInit ¶
func RecoverInit(p RecoverInitParams) (Recovery, error)
RecoverInit is init's own (not plan/apply's) recovery path: when init itself just failed, it classifies p.Output and, if it asked for -upgrade/-reconfigure, calls p.Rerun once with those flags, returning the Recovery that was applied (zero value when none was) alongside the resulting error. There is no opt-out check here -- the caller is already initializing, so init.mode: never / --skip-init are not in play; a policy error (e.g. init.upgrade: never but the diagnostic demands -upgrade) is still surfaced, joined with p.Err.
func ShouldRecover ¶
func ShouldRecover( d Diagnosis, mode schema.TerraformInitMode, reconfigure schema.TerraformInitReconfigure, upgrade schema.TerraformInitUpgrade, optedOut bool, ) (Recovery, error)
ShouldRecover applies Atmos's init recovery policy to d: no recovery when nothing was diagnosed, an error (never a silent init) when the caller explicitly opted out of implicit init, an error when a required upgrade or reconfigure is itself disabled by policy, and otherwise a concrete Recovery describing the re-run; optedOut reflects the caller's own opt-out signals (--skip-init, components.terraform.init.mode: never, deploy_run_init: false); mode is accepted for troubleshooting context in the debug log even though the opt-out itself is the caller's responsibility to compute.
type Request ¶
type Request struct {
// Mode is the configured init mode; empty is treated as schema.TerraformInitModeAuto.
Mode schema.TerraformInitMode
// Reconfigure is the configured reconfigure policy; empty is treated as
// schema.TerraformInitReconfigureAuto.
Reconfigure schema.TerraformInitReconfigure
// Upgrade is the configured upgrade policy; empty is treated as
// schema.TerraformInitUpgradeAuto.
Upgrade schema.TerraformInitUpgrade
// Force short-circuits to RunInit=true regardless of the fingerprint, e.g. for the
// workspace subcommand, a re-provisioned workdir, or an explicit `atmos terraform init`.
Force bool
// Inputs is nil on a dry run (no component on disk to fingerprint yet).
Inputs *Inputs
}
Request captures everything Decide needs to determine whether init should run, and with which flags.
func RequestFromInfo ¶
func RequestFromInfo(atmosConfig *schema.AtmosConfiguration, info *schema.ConfigAndStacksInfo, in *Inputs, force bool) *Request
RequestFromInfo resolves the atmos.yaml init policy plus caller-agnostic always-force signals (the workspace subcommand, a re-provisioned workdir) into a Request ready for Decide. Force is the caller's own signal, e.g. an explicit `atmos terraform init`.