Help us shape the future of Go! Take the 2026 Go Developer Survey

record

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package record defines the on-disk run record Orchard writes after each `orchard run`. A record captures what a scenario did well enough that future tooling — starting with `orchard teardown` — can reason about it without re-executing the scenario.

The format is JSON, pretty-printed for inspectability. cty values (variables, node outputs, scenario outputs) serialise through pkg/protocol/ctyjson so they round-trip cleanly with the engine's own type system.

This is deliberately a simpler model than Terraform's state: no remote backends, no locking, no versioning, one file per run in the user's working directory. That keeps the surface small while the teardown story is being prototyped. A records-based teardown can grow sophistication when real-world use demands it.

Index

Constants

View Source
const (
	StatusSucceeded = "succeeded"
	StatusFailed    = "failed"
	StatusSkipped   = "skipped"
)

Status values for runs and nodes.

View Source
const (
	KindAction    = "action"
	KindComponent = "component"
)

Kind values for node entries.

View Source
const Format = "1"

Format is a monotonically-increasing record format version. Bump only for breaking changes to the on-disk shape: Load refuses any other format, so additive fields (omitempty, absent in older records) stay on the current one.

Variables

This section is empty.

Functions

func ConfigDrift added in v0.2.0

func ConfigDrift(r *Record, configs map[string]map[string]cty.Value) ([]string, error)

ConfigDrift lists, sorted, the "<provider>.<attribute>" configs that differ between the run r recorded and configs: changed, added or removed. Providers the run didn't record are left out, and a record without hashes reports no drift.

func DecodeCty

func DecodeCty(raw map[string]json.RawMessage) (map[string]cty.Value, error)

DecodeCty is the inverse of EncodeCty: turn a map of JSON-encoded cty values back into cty values. Types are inferred from the JSON structure (DynamicPseudoType) since the record doesn't carry a type hint per field — numbers become numbers, lists become tuples, objects become objects. Close enough for teardown expressions that typically read scalars and occasionally traverse an object.

func EncodeCty

func EncodeCty(values map[string]cty.Value) (map[string]json.RawMessage, error)

EncodeCty serialises a map of cty values using ctyjson, preserving type information across the JSON round-trip. Returns nil for an empty input so omitempty fires and the serialised record stays compact.

func Filename

func Filename(scenario, id string) string

Filename returns the on-disk filename for a record with the given scenario name and run ID. The scenario name is sanitised — anything outside [A-Za-z0-9_-] becomes an underscore so operators can still find records with familiar shell globs even if the scenario block uses spaces, slashes, or other metacharacters.

func HashConfig added in v0.2.0

func HashConfig(runID string, configs map[string]map[string]cty.Value) (map[string]map[string]string, error)

HashConfig fingerprints evaluated provider configs per attribute with HashValue.

func HashValue added in v0.2.0

func HashValue(runID, scope, name string, v cty.Value) (string, error)

HashValue fingerprints v as HMAC-SHA256 keyed by the run ID, so equal values in different records hash differently. scope and name say what v is ("var" and the variable, or a provider and its attribute), so equal values in one record differ too.

func IndexComponentVars added in v0.2.0

func IndexComponentVars(r *Record) (map[string]map[string]cty.Value, error)

IndexComponentVars decodes the record's component variables, keyed by component key, for engine.Teardown.

func IndexOutputs

func IndexOutputs(r *Record) (map[string]map[string]cty.Value, error)

IndexOutputs decodes each succeeded node's outputs into a map keyed by full node ID ("action.sql.create_merchant" etc.). Every succeeded node is present, with an empty map when it had no outputs (a DDL statement), since callers treat absence as "nothing to tear down". Failed and skipped nodes are omitted. Designed as the canonical way to hand a record off to engine.Teardown.

func IndexUndos

func IndexUndos(r *Record) (map[string]provider.Invocation, error)

IndexUndos decodes each succeeded node's undo invocation, keyed by full node ID, for engine.Teardown. Nodes without one are omitted.

func NewID

func NewID(now time.Time) string

NewID returns a run identifier of the form YYYYMMDDTHHMMSSZ_<8hex>. The timestamp prefix ensures lexicographic sort matches chronological order; the hex suffix handles collisions within the same second. UTC is always used so the ID is stable across machines.

8 hex characters give 2^32 values — ample for "same second" collision avoidance without bloating filenames.

func Save

func Save(path string, rec *Record) error

Save writes rec to path through a sibling temp file and a rename, so a partially-flushed record can't appear on disk.

func SensitiveDrift added in v0.2.0

func SensitiveDrift(r *Record, vars map[string]cty.Value) ([]string, error)

SensitiveDrift checks vars, the variables teardown loaded, against the hashes of the run's sensitive variables. It returns, sorted, the names whose value differs, and an error naming any the scenario no longer declares.

Types

type File

type File struct {
	Path   string
	Record *Record
}

File is a record and the path it was loaded from.

func List

func List(dir string, warn io.Writer) ([]File, error)

List loads every record in dir, newest first. Files that match the record name but can't be loaded (unreadable, not JSON, an unknown format) are skipped with a line on warn.

func Resolve

func Resolve(dir, arg string, warn io.Writer) (File, error)

Resolve finds a record from a command-line argument: a path to an existing file, or else a run ID looked up in dir. Records in dir that can't be loaded are reported on warn, as List does.

type FileWriter

type FileWriter struct {
	Dir string
}

FileWriter persists records to the filesystem. Dir is where the file lands; the filename is derived from the scenario name and run id. Writes go through a temp file + atomic rename so a partially-flushed record can't appear on disk.

func (FileWriter) Path

func (fw FileWriter) Path(r *Record) string

Path is where Write puts r.

func (FileWriter) Write

func (fw FileWriter) Write(r *Record) error

Write implements Writer.

type Node

type Node struct {
	ID         string                     `json:"id"`
	Kind       string                     `json:"kind"`
	StartedAt  time.Time                  `json:"started_at"`
	FinishedAt time.Time                  `json:"finished_at"`
	Status     string                     `json:"status"`
	Outputs    map[string]json.RawMessage `json:"outputs,omitempty"`
	Error      string                     `json:"error,omitempty"`
	// Undo is how the node's provider says to reverse it (a typed
	// insert: delete the row by its primary key). `orchard teardown`
	// runs it when the action has no explicit teardown block. Records
	// from before undo existed simply don't have it.
	Undo *Undo `json:"undo,omitempty"`
	// TornDown marks a node whose teardown succeeded, so re-running a
	// partly failed teardown skips it.
	TornDown bool `json:"torn_down,omitempty"`
}

Node captures one graph node's execution. Outputs is only populated for succeeded nodes; Error is only populated for failed ones. Skipped nodes (upstream of a failure) have neither.

type Options

type Options struct {
	Inner     engine.Reporter
	Writer    Writer
	Scenario  Scenario
	Variables map[string]cty.Value // resolved scenario variables
	Seed      uint64               // the run's random seed
	// ProviderConfigs are the evaluated provider configs, recorded as
	// hashes (see HashConfig).
	ProviderConfigs map[string]map[string]cty.Value
	// Sensitive names the scenario variables recorded as a hash
	// rather than a value (ParsedScenario.Sensitive).
	Sensitive map[string]bool
	LogOutput io.Writer // where write failures surface; defaults to os.Stderr
}

Options configures a Reporter. Most callers only need Writer and scenario context; LogOutput is where a write failure surfaces.

type Record

type Record struct {
	Format     string                     `json:"format"`
	ID         string                     `json:"id"`
	Scenario   Scenario                   `json:"scenario"`
	StartedAt  time.Time                  `json:"started_at"`
	FinishedAt time.Time                  `json:"finished_at"`
	Status     string                     `json:"status"`
	Error      string                     `json:"error,omitempty"`
	Variables  map[string]json.RawMessage `json:"variables,omitempty"`
	// Seed is the run's random seed; `orchard run --seed` with it
	// draws the same random values again. Absent in older records.
	Seed *uint64 `json:"seed,omitempty"`
	// ComponentVariables are each component's resolved inputs (its
	// var.* namespace), keyed by component key ("merchant.acme",
	// "merchant.acme[0]"), for its actions' teardown bodies. Absent
	// in older records.
	ComponentVariables map[string]map[string]json.RawMessage `json:"component_variables,omitempty"`
	Nodes              []Node                                `json:"nodes"`
	Outputs            map[string]json.RawMessage            `json:"outputs,omitempty"`
	// TornDownAt is set once `orchard teardown` has finished every
	// node's teardown; such a record is refused by a later teardown.
	TornDownAt *time.Time `json:"torn_down_at,omitempty"`
	// ConfigHashes fingerprints each provider config attribute the run
	// used (provider -> attribute -> HashConfig), so teardown can
	// refuse a config that now points somewhere else without the
	// record holding DSNs or keys. Absent in older records.
	ConfigHashes map[string]map[string]string `json:"config_hashes,omitempty"`
	// SensitiveVariables holds, for each scenario variable declared
	// `sensitive = true`, a hash of its value (see HashValue) in place
	// of the value, which Variables leaves out. Teardown needs the
	// value passed again and checks it against the hash.
	SensitiveVariables map[string]string `json:"sensitive_variables,omitempty"`
	// SensitiveComponentVariables names, per component key, the
	// component variables ComponentVariables leaves out because they
	// hold a sensitive value. Teardown evaluates them again.
	SensitiveComponentVariables map[string][]string `json:"sensitive_component_variables,omitempty"`
}

Record is the top-level document persisted per run.

func Load

func Load(path string) (*Record, error)

Load reads and parses a record file from disk, refusing a format other than Format.

type Reporter

type Reporter struct {

	// WriteErr is set if the record could not be persisted.
	WriteErr error
	// contains filtered or unexported fields
}

Reporter wraps another engine.Reporter and accumulates enough state to assemble a Record at the end of the run. Writer is called exactly once, from RunCompleted. WriteErr (if the write fails) is surfaced through the reporter's own Out writer so the user sees it, and the error is retained on the reporter so the caller can choose to fail the run.

Wrap a presentation reporter like so:

base := reporter.Select(reporter.Options{Out: os.Stdout})
rec := record.NewReporter(base, scenarioMeta, variables, record.FileWriter{Dir: "."}, os.Stderr)
engine.SetReporter(rec)

The inner reporter receives every event first; the recorder accumulates its own copy. This keeps presentation identical to a no-record run.

func NewReporter

func NewReporter(opts Options) (*Reporter, error)

NewReporter constructs a Reporter from options. The scenario variables are encoded up front so a later failure during the run doesn't lose the inputs that produced it.

func (*Reporter) ComponentVars added in v0.2.0

func (r *Reporter) ComponentVars(componentKey string, vars map[string]cty.Value)

ComponentVars implements engine.ComponentVarsReporter. Variables holding a sensitive value (marked parser.SensitiveMark) are named in the record but left out.

func (*Reporter) NodeCompleted

func (r *Reporter) NodeCompleted(nodeID string, outputs map[string]cty.Value, d time.Duration)

NodeCompleted implements engine.Reporter. Outputs are copied so the caller is free to mutate the map after the call returns.

func (*Reporter) NodeFailed

func (r *Reporter) NodeFailed(nodeID string, err error)

NodeFailed implements engine.Reporter.

func (*Reporter) NodeStarted

func (r *Reporter) NodeStarted(nodeID string)

NodeStarted implements engine.Reporter.

func (*Reporter) NodeUndo

func (r *Reporter) NodeUndo(nodeID string, undo provider.Invocation)

NodeUndo implements engine.UndoReporter: it attaches the undo invocation to the node that just completed.

func (*Reporter) Path

func (r *Reporter) Path() string

Path is the file the record was written to, or "" if it wasn't written (no writer, a failed write, or a writer that isn't a FileWriter).

func (*Reporter) RunCompleted

func (r *Reporter) RunCompleted(summary engine.RunSummary)

RunCompleted implements engine.Reporter. This is where the record is assembled and persisted.

func (*Reporter) RunStarted

func (r *Reporter) RunStarted(scenario string)

RunStarted implements engine.Reporter.

type Scenario

type Scenario struct {
	Name string `json:"name"`
	Path string `json:"path"`
}

Scenario is the subset of scenario metadata worth persisting. Path is absolute so a record is self-describing even when the consumer runs from a different cwd than the original `orchard run`.

func ScenarioFromParser

func ScenarioFromParser(ps *parser.ParsedScenario, path string) Scenario

ScenarioFromParser extracts the bits of scenario metadata the record needs from a ParsedScenario + its source path. Keeps the caller from reaching into parser internals.

type Undo

type Undo struct {
	Action string                     `json:"action"`
	Attrs  map[string]json.RawMessage `json:"attrs"`
}

Undo is a recorded provider.Invocation: an action and its evaluated attributes.

type Writer

type Writer interface {
	Write(r *Record) error
}

Writer accepts a rendered Record. The default implementation writes to a file in a configured directory; tests and future alternative backends (remote stores, stdout, etc.) implement it too.

Jump to

Keyboard shortcuts

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