waves

package
v0.23.0 Latest Latest
Warning

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

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

Documentation

Overview

Package waves is GitHub issue #1754's primitives for rolling one change across many estate roots (epic #1749): a digest over a set of per-root plans, and a split of the set into ordered waves. The 2026-09-30 ruling bounds it: choudoufu supplies primitives, and where an approval lives is chant's side. Nothing here stores an approval.

The set digest

The digest lives in internal/live/setdigest, which imports nothing of this module so internal/live/setplan can print it; its package doc says what it covers. The names below are aliases of it.

Waves

Split orders a set of roots so that no root lands in a wave before a root whose estate it reads. A read is what ReadsOf finds in configuration: a data source filtered on the producer's tofu-estate marker (live/OUTPUTS.md's pattern), whether by a "filter" block or a "tags" argument, and a terraform_estate_outputs data source (#1371). A read of an estate outside the set orders nothing and is reported.

The explicit canaries, when there are any, are wave 1. Every other root lands in the earliest wave after every root it reads. A canary that reads a root of the set which is not itself a canary is refused, because wave 1 would then land a reader before what it reads; two canaries may read each other's estates in one direction, and wave 1 then has an order of its own, which Wave.Edges carries. A cycle among the roots is refused with the cycle named, since no order puts every reader after what it reads.

Wave apply

Apply applies one wave of a set whose digest was approved elsewhere. The set plan document must hash to the approved digest. Every root of the wave that has not already landed is planned again, and unless each fresh plan's root digest equals its approved one, nothing in the wave is applied and the result is exit 3 naming the roots that moved: a set extension of "apply PLANFILE"'s own refusal. A root whose fresh plan fails is not a moved set; it is a failed root.

Roots apply one at a time, producers first. A root that fails skips every root that reads its estate, in its wave and in later ones, and roots that read nothing that failed still apply. Each outcome is written to the resume file as it is decided, and a later run with that file plans and applies only roots that have not landed. A root in a later wave whose producer has not landed, for any reason including its wave never having run, is skipped rather than applied ahead of it.

A reader in a later wave was planned before its producer applied. When the producer's apply changes a value the reader reads, the reader's fresh plan differs from its approved one and its wave exits 3: the set has to be planned and approved again from that point. That is the refusal working, not a fault in it.

Index

Constants

View Source
const (
	ExitApplied    = 0
	ExitError      = 1
	ExitSetMoved   = 3
	ExitRootFailed = 4
)

Wave apply's exit codes. 3 is "apply <planfile>"'s own code for an approved plan the live system has moved under (internal/command's ExitApprovalRefused), extended here to a set; 4 is the set plan's code for "some root failed" (internal/live/setplan).

View Source
const (
	OutcomeLanded  = "landed"
	OutcomeFailed  = "failed"
	OutcomeSkipped = "skipped"
)

Outcomes a root can have in a resume file.

View Source
const (
	TraceNameDigest    = "Wave set digest"
	TraceNameResume    = "Wave resume check"
	TraceNameFreshPlan = "Wave fresh plan"
	TraceNameApplyRoot = "Wave apply root"

	RefusedStepDigest    = "digest"
	RefusedStepResume    = "resume"
	RefusedStepFreshPlan = "fresh-plan"
)

Apply applies one wave of an approved set. See the package documentation's "Wave apply" for the rules. GitHub issue #1898. Span names for the steps of a wave apply, and the choudoufu.refused.step each gate records when it refuses.

View Source
const (
	DigestPrefix  = setdigest.DigestPrefix
	StatusPlanned = setdigest.StatusPlanned
)
View Source
const EstateOutputsTypeName = "terraform_estate_outputs"

EstateOutputsTypeName is the builtin terraform provider's cross-estate output read (#1371), internal/builtin/providers/tf's EstateOutputsTypeName. It is repeated rather than imported because that package reaches internal/live/setplan, which digests through this one; internal/command's TestWavesEstateOutputsTypeName holds the two equal.

View Source
const FormatVersion = "1"

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

View Source
const ResumeFormatVersion = "1"

ResumeFormatVersion is the resume file's format version.

Variables

View Source
var (
	ParseSetDocument = setdigest.ParseSetDocument
	RootDigest       = setdigest.RootDigest
	SetDigest        = setdigest.SetDigest
	DocumentDigests  = setdigest.DocumentDigests
)

Functions

func EstatesRead

func EstatesRead(cfg *configs.Config) ([]string, error)

EstatesRead is the distinct estates ReadsOf finds, sorted: the form live-affected (GitHub issue #1751) reads, so the two commands share one cross-estate reader. Its error is ReadsOf's: a read whose estate cannot be told from configuration.

func RootName

func RootName(dir string) string

RootName is how a set names a root directory: cleaned, with forward slashes, the spelling #1752's set plan document uses.

func WaveLines

func WaveLines(w Wave) string

WaveLines is one wave's roots, one per line: the form another tool reads to open one change per wave.

func WriteResume

func WriteResume(path string, r *Resume) error

WriteResume writes r to path, through a temporary file so a reader never sees half of it.

Types

type ApplyOptions

type ApplyOptions struct {
	// Set is the approved set plan document.
	Set *SetDocument
	// Approved is the approved set digest. Set must hash to it.
	Approved string
	// Waves is the set's split (from [Build] over Set).
	Waves *Document
	// Wave is the wave to apply, from 1.
	Wave   int
	Resume *Resume
	// Save writes the resume file. It is called after every root's
	// outcome is decided.
	Save   func(*Resume) error
	Runner ApplyRunner
}

ApplyOptions are one wave apply's inputs.

type ApplyResult

type ApplyResult struct {
	ExitCode int `json:"exit_code"`
	Wave     int `json:"wave"`
	// Error is the refusal, for exit 1 and exit 3.
	Error string `json:"error,omitempty"`
	// Moved are the roots whose plans moved (exit 3); nothing was applied.
	Moved []Moved `json:"moved,omitempty"`
	// Outcomes are this run's outcomes for the wave's roots, including
	// those already landed before it ran.
	Outcomes []ResumeEntry `json:"outcomes"`
	// Applied are the roots this run applied, in order.
	Applied []string `json:"applied"`
}

ApplyResult is what a wave apply did.

func Apply

func Apply(ctx context.Context, o ApplyOptions) (*ApplyResult, error)

func (*ApplyResult) JSON

func (r *ApplyResult) JSON() (string, error)

JSON renders a wave apply's result.

func (*ApplyResult) Text

func (r *ApplyResult) Text() string

Text renders a wave apply's result for a reader.

type ApplyRunner

type ApplyRunner interface {
	// PlanRoots plans every root afresh and returns one entry per root,
	// keyed by root. A root that fails to plan is an entry whose Status is
	// not "planned".
	PlanRoots(ctx context.Context, roots []string) (map[string]FreshPlan, error)
	// Apply applies planFile in root. A non-nil error is the root failing.
	Apply(ctx context.Context, root, planFile string) error
}

ApplyRunner plans and applies roots. internal/command's is real: a set plan of the wave's roots, then "apply PLANFILE" in each root. Tests substitute their own.

type Document

type Document struct {
	FormatVersion string `json:"format_version"`
	// Roots are the roots read, in directory order, with what each reads.
	Roots []Root `json:"roots"`
	Waves []Wave `json:"waves"`
	// Edges are every ordering constraint between two roots of the set.
	Edges []Edge `json:"edges"`
	// ExternalReads are reads of estates no root of the set owns.
	ExternalReads []External `json:"external_reads"`
	// Digest is the set digest over every root, present when plans were
	// given.
	Digest string `json:"digest,omitempty"`
	// RootDigests are each root's digest, present when plans were given.
	RootDigests map[string]string `json:"root_digests,omitempty"`
}

Document is what live-waves -json prints.

func Build

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

Build reads every root's configuration, splits the set into waves and, when a set plan document is given, gives the set and each wave its digest.

func (*Document) JSON

func (d *Document) JSON() (string, error)

JSON renders d.

func (*Document) Text

func (d *Document) Text() string

Text renders d for a reader.

type Edge

type Edge struct {
	Reader   string `json:"reader"`
	Producer string `json:"producer"`
	Estate   string `json:"estate"`
	From     string `json:"from"`
}

Edge is one ordering constraint between two roots of the set: Reader lands after Producer.

type External

type External struct {
	Root   string `json:"root"`
	Estate string `json:"estate"`
	From   string `json:"from"`
}

External is a read of an estate no root of the set owns. It orders nothing.

type FreshPlan

type FreshPlan struct {
	RootPlan
	// PlanFile is the fresh plan's saved file, which is what is applied.
	PlanFile string
}

FreshPlan is one root planned again at apply time.

type Moved

type Moved struct {
	Root     string `json:"root"`
	Approved string `json:"approved_digest"`
	Fresh    string `json:"fresh_digest"`
}

Moved is a root whose fresh plan does not match the approved one.

type Options

type Options struct {
	// Roots are the root directories as given, relative to BaseDir or
	// absolute. Empty means the roots of Set.
	Roots []string
	// BaseDir is what a relative root is relative to.
	BaseDir string
	// Canaries name roots by directory or estate.
	Canaries []string
	// Set, when non-nil, is the set plan document whose plans give every
	// wave its digest. It must name exactly the roots planned.
	Set *SetDocument
}

Options are one wave planning's inputs.

type Read

type Read struct {
	// Estate is the producer estate.
	Estate string `json:"estate"`
	// From is the data source that reads it, module-qualified when it is
	// declared inside one.
	From string `json:"from"`
}

Read is one estate a root's configuration reads, and where.

func ReadsOf

func ReadsOf(cfg *configs.Config) ([]Read, error)

ReadsOf finds every estate cfg's configuration reads, over the whole static module tree, sorted. A read is:

  • a data source with a "filter" block named tag:tofu-estate, once per value (internal/live/check's own reading of live/OUTPUTS.md's pattern);
  • a data source whose "tags" argument is an object with a tofu-estate key;
  • a terraform_estate_outputs data source's "estate" argument (#1371).

A read whose estate is not a literal is an error naming it rather than a read silently missed: a missed read could land a reader before what it reads.

type Resume

type Resume struct {
	FormatVersion string `json:"format_version"`
	// SetDigest is the approved set this file belongs to. A file for
	// another set is refused.
	SetDigest string        `json:"set_digest"`
	Roots     []ResumeEntry `json:"roots"`
}

Resume is the resume file: what each root of an approved set came to. A wave apply reads it to learn what earlier waves landed, writes it after every root, and a re-run with it applies only what has not landed.

func ReadResume

func ReadResume(path string) (*Resume, error)

ReadResume reads path, or returns an empty Resume when it does not exist yet.

type ResumeEntry

type ResumeEntry struct {
	Root    string `json:"root"`
	Wave    int    `json:"wave"`
	Outcome string `json:"outcome"`
	// Reason says why a root failed or was skipped.
	Reason string `json:"reason,omitempty"`
}

ResumeEntry is one root's outcome.

type Root

type Root struct {
	// Root is the root's directory as the set names it.
	Root string `json:"root"`
	// Estate is the estate the root owns.
	Estate string `json:"estate"`
	// Reads are the estates the root's configuration reads.
	Reads []Read `json:"reads,omitempty"`
}

Root is one root of a set, as wave planning sees it.

func LoadRoot

func LoadRoot(ctx context.Context, name, dir string) (Root, error)

LoadRoot reads the configuration in dir, the whole static module tree, and returns the estate it owns and the estates it reads. name is what the result's Root is set to: the directory as the set names it.

It needs no "init" for a module called by a local path: that module is read from the path. Any other module is read from where "init" installed it (dir/.terraform/modules), and a module that is not installed is an error naming it, since a read inside it could not be seen.

The estate is the live block's (or estate.chdf.hcl's) estate argument, and otherwise the one tofu-estate value the configuration stamps; a configuration that names none, or several, is an error.

type RootDigestEntry

type RootDigestEntry = setdigest.RootDigestEntry

The set digest, from internal/live/setdigest. See the package doc.

type RootPlan

type RootPlan = setdigest.RootPlan

The set digest, from internal/live/setdigest. See the package doc.

type SetDocument

type SetDocument = setdigest.SetDocument

The set digest, from internal/live/setdigest. See the package doc.

type Wave

type Wave struct {
	// Number counts from 1.
	Number int `json:"wave"`
	// Canary is true for the wave the explicit canaries form.
	Canary bool `json:"canary"`
	// Roots are the wave's roots, sorted.
	Roots []string `json:"roots"`
	// Edges are ordering constraints between two roots of this same wave.
	// Only the canary wave can have any.
	Edges []Edge `json:"edges,omitempty"`
	// Digest is the set digest over this wave's roots, when the roots'
	// plans were given.
	Digest string `json:"digest,omitempty"`
}

Wave is one step of a rollout.

type Waves

type Waves struct {
	Waves    []Wave     `json:"waves"`
	Edges    []Edge     `json:"edges"`
	External []External `json:"external_reads"`
	// WaveOf maps each root to its wave number.
	WaveOf map[string]int `json:"-"`
}

Waves is a set split into waves.

func Split

func Split(roots []Root, canaries []string) (*Waves, error)

Split orders roots into waves. canaries name roots by directory or by estate; when there are any they form wave 1. See the package documentation for the rules and what is refused.

func (*Waves) AttachDigests

func (w *Waves) AttachDigests(byRoot map[string]string) error

AttachDigests sets every wave's digest from per-root digests. Every root of every wave must have one.

Jump to

Keyboard shortcuts

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