setplan

package
v0.22.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MPL-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package setplan plans a set of estate roots in one invocation (GitHub issue #1752, part of epic #1749): every root gets its own saved plan file, one root's failure never stops the others, and the whole set is reported in one JSON document.

One invocation, not one process

The issue asked for one process. Planning two roots in one process is not something this fork's commands can do, because every one of them reaches its root module through the process working directory: upstream's -chdir is an os.Chdir in main, [workdir.Dir.NormalizePath] answers relative to the root module on the assumption the two are the same directory, and the live pipeline loads "." directly (live_mode.go's liveSettings, live_plan.go's loadConfig, the record store opened at "." in live_record_store_open.go, the variable files read from "." in meta_vars.go, the operation's ConfigDir in plan.go). The working directory is one per process, so N roots planned concurrently in one process would all read whichever root was the working directory last.

So one orchestrating process runs each root's three steps (init, plan -out, show -json) as child processes of the same binary, each with -chdir=ROOT, under a bound of -parallel-estates. What the issue's "one process" was for survives intact:

  • Providers are installed once per invocation, not once per root. Every child shares one global plugin cache (TF_PLUGIN_CACHE_DIR), whose installs are serialised by a file lock per provider release (providercache.Dir.lock), so the first root to need a release unpacks it and every other root links it. The children also carry TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=1, without which a root with no dependency lock file re-downloads a release the cache already holds, because it has no checksum to match the cached copy against. The cost of that variable is the one it is named for: a root with no lock file gets one recording only the running platform's checksum.
  • One document, one exit code, one place to look.

The document

Document is what -json prints. Its contract with the grouped summary (#1753) and the set digest and wave apply (#1754) is that the top level carries "roots", and each root carries at least "root", "estate", "status", "error" and "plan", where "plan" is exactly the object "choudoufu show -json PLANFILE" prints for that root's plan file - stock OpenTofu's machine-readable plan, resource_changes included. Fields are added, never renamed or dropped; FormatVersion moves when one is. testdata/document.golden.json pins the shape.

Exit codes

0  every root planned, and no root has changes
2  every root planned, and at least one root has changes
   (the meaning plan -detailed-exitcode gives a single root)
4  at least one root failed; the others are still planned and reported
1  the command itself could not run (bad flags, no roots, a root given
   twice); no root was planned

3 is skipped on purpose: "choudoufu apply PLANFILE" already exits 3 when the live system moved under an approved plan, and #1754's wave apply extends that refusal to a set. A pipeline routing on exit codes should never have to ask which command produced a 3.

Index

Constants

View Source
const (
	EnvPluginCacheDir    = "TF_PLUGIN_CACHE_DIR"
	EnvCacheMayBreakLock = "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE"
	EnvDataDir           = "TF_DATA_DIR"
)

Environment variables the children are given. See the package documentation for why each one is there.

View Source
const (
	ExitClean      = 0
	ExitError      = 1
	ExitChanges    = 2
	ExitRootFailed = 4
)

The exit codes. See the package documentation.

View Source
const (
	TraceNameRoot   = "Set plan root"
	TraceNameStage  = "Set plan stage"
	TraceNameDigest = "Set digest"
)

GitHub issue #1898. Spans for a set plan: one per root, one per stage under it (each stage's child process gets the stage span as its TRACEPARENT, see Exec), and one for the set digest.

View Source
const FormatVersion = "1"

FormatVersion is the document's format version. It moves when a field is renamed or dropped, never when one is added.

Variables

This section is empty.

Functions

func ChildEnv

func ChildEnv(parent []string, pluginCacheDir string) []string

ChildEnv is parent with the shared plugin cache set and TF_DATA_DIR removed. TF_DATA_DIR names one data directory for a whole process; every root sharing one would install every root's modules over each other, so the command refuses it before this is called, and this drops it again in case a caller did not.

func ExitCode

func ExitCode(doc *Document) int

ExitCode is the code a document earns. See the package documentation.

func PlanName

func PlanName(rel string) string

PlanName is the name a root's plan file and log take inside the output directory, before their extensions: the root's own relative path, so estates/e01 plans to OUT/estates/e01.tfplan. The base directory itself has no path to mirror and is named "_root".

Types

type Document

type Document struct {
	FormatVersion string `json:"format_version"`
	// Roots are in the order the command was given them, whatever order
	// they finished in.
	Roots   []Root  `json:"roots"`
	Summary Summary `json:"summary"`
	// Digest is the set digest (#1754): one digest over every root's
	// planned changes, independent of root order, moving whenever any one
	// root's changes move. It is what an approval names and what
	// live-wave-apply checks the set against.
	Digest string `json:"digest"`
	// ExitCode is the code the command exits with, so a reader of a saved
	// document does not have to have watched the process.
	ExitCode int `json:"exit_code"`
	// PluginCacheDir is the one provider cache every root shared.
	PluginCacheDir string `json:"plugin_cache_dir"`
	// ParallelEstates is the bound the roots ran under.
	ParallelEstates int `json:"parallel_estates"`
}

Document is the -json output: one object, printed once.

func Run

func Run(ctx context.Context, opts Options) (*Document, error)

Run plans every root and returns the document. Its error is the command's own (see resolve); every root-level failure is in the document instead.

type Exec

type Exec struct {
	// Bin is the choudoufu executable, normally os.Executable().
	Bin string
	// Env is the children's whole environment. Build it with [ChildEnv].
	Env []string
	// EstateOf reads a root's live block. It is a function rather than a
	// child process because it is a parse, not a run: the command layer
	// owns the configuration loader.
	EstateOf func(dir string) (estate string, ok bool, err error)
}

Exec is the real Runner: every stage is a child process of Bin with -chdir=ROOT.

func (Exec) Apply

func (e Exec) Apply(ctx context.Context, dir, planFile string, log io.Writer) error

Apply applies planFile in dir: "apply PLANFILE", which under a live block plans the live system again and exits 3 when that plan differs from the file's (internal/command's ExitApprovalRefused). Used by live-wave-apply (#1754).

func (Exec) Estate

func (e Exec) Estate(_ context.Context, dir string) (string, bool, error)

func (Exec) Init

func (e Exec) Init(ctx context.Context, dir string, log io.Writer) error

func (Exec) Plan

func (e Exec) Plan(ctx context.Context, dir, planFile string, log io.Writer) (bool, error)

func (Exec) Show

func (e Exec) Show(ctx context.Context, dir, planFile string, log io.Writer) (json.RawMessage, error)

type Options

type Options struct {
	// Roots are the root directories, as given.
	Roots []string
	// BaseDir is the directory Roots, OutDir and the document's paths are
	// relative to: the command's working directory.
	BaseDir string
	// OutDir receives one plan file and one log per root, named by root.
	OutDir string
	// Parallel bounds how many roots run at once. At least 1.
	Parallel int
	// PluginCacheDir is reported in the document; the Runner is what uses
	// it.
	PluginCacheDir string
	Runner         Runner
	// Now is the clock, for tests. Defaults to time.Now.
	Now func() time.Time
}

Options are one set plan's inputs.

type Root

type Root struct {
	// Root is the root directory relative to the directory the command ran
	// in, with forward slashes.
	Root string `json:"root"`
	// Estate is the estate the root's live block names. Empty when the
	// root failed before its live block was read, or when the block names
	// none and the run derives it from the configuration's markers.
	Estate string `json:"estate"`
	Status Status `json:"status"`
	// Error is the failure, set when Status is failed and empty otherwise.
	Error string `json:"error"`
	// Stage is the stage that failed, set when Status is failed.
	Stage Stage `json:"stage,omitempty"`
	// Changes is what plan -detailed-exitcode said: true when the plan
	// proposes any change. False for a failed root.
	Changes bool `json:"changes"`
	// PlanFile is the saved plan, relative to the command's directory.
	// Set whenever the plan stage succeeded.
	PlanFile string `json:"plan_file"`
	// LogFile holds every stage's combined output for this root.
	LogFile string `json:"log_file"`
	// DurationMS is the root's wall time across all its stages.
	DurationMS int64 `json:"duration_ms"`
	// Plan is stock OpenTofu's machine-readable plan for this root, the
	// object "choudoufu show -json PLANFILE" prints. null for a failed root.
	Plan json.RawMessage `json:"plan"`
	// Digest is this root's digest (#1754): what [Document.Digest] is
	// computed over. internal/live/setdigest's package doc says what it covers.
	Digest string `json:"digest"`
}

Root is one root's entry in the document.

type Runner

type Runner interface {
	// Estate reads dir's estate from its live block. ok is false when dir
	// has no live block at all.
	Estate(ctx context.Context, dir string) (estate string, ok bool, err error)
	Init(ctx context.Context, dir string, log io.Writer) error
	// Plan writes planFile and reports whether the plan has changes.
	Plan(ctx context.Context, dir, planFile string, log io.Writer) (changes bool, err error)
	// Show returns the machine-readable form of planFile.
	Show(ctx context.Context, dir, planFile string, log io.Writer) (json.RawMessage, error)
}

Runner does one root's stages. Exec is the real one; tests substitute their own to break a chosen root at a chosen stage.

type Stage

type Stage string

Stage is one step of a root's run, in order.

const (
	// StageEstate reads the root's live block (or sidecar) for its estate.
	// A root with no live block fails here: a plain "choudoufu plan" there
	// would be a stock state-backed plan, not a live one.
	StageEstate Stage = "estate"
	StageInit   Stage = "init"
	StagePlan   Stage = "plan"
	StageShow   Stage = "show"
)

type StageError

type StageError struct {
	Stage  string
	Code   int
	Stderr string
}

StageError is a stage that exited non-zero.

func (*StageError) Error

func (e *StageError) Error() string

type Status

type Status string

Status is a root's outcome.

const (
	// StatusPlanned is a root whose plan file was written and read back.
	StatusPlanned Status = "planned"
	// StatusFailed is a root that stopped at one of its stages; Error says
	// why and Stage says where.
	StatusFailed Status = "failed"
)

type Summary

type Summary struct {
	Roots   int `json:"roots"`
	Planned int `json:"planned"`
	Changed int `json:"changed"`
	Failed  int `json:"failed"`
}

Summary counts the document's roots.

Jump to

Keyboard shortcuts

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