autoinit

package
v1.229.0 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

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

View Source
const MarkerFileName = "atmos-init.json"

MarkerFileName is the name of the init marker file, written inside the Terraform data directory.

View Source
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

func ApplyRecovery(args []string, rec Recovery) []string

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

func DataDir(componentPath string, lookup func(key string) (string, bool)) string

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

func InitArgs(d Decision, passVars bool, varFile string) []string

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

func InvalidateMarker(path string) error

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

func MarkerPath(dataDir string) string

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

func Record(in *Inputs, initArgs []string, atmosVersion string) error

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

func RecordFromInfo(in *Inputs, initArgs []string)

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

func WriteMarker(path string, m *Marker) error

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.

func Decide

func Decide(req *Request) Decision

Decide determines whether `terraform init` should run, and with which flags, given req. Decide never fails: any error while computing a fingerprint or reading the marker degrades to RunInit=true, so a fingerprinting bug can never permanently block a user from getting a working init.

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

func Classify(output string) Diagnosis

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

func ReadMarker(path string) (*Marker, error)

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`.

Jump to

Keyboard shortcuts

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