state

package
v0.38.0 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const (
	// SchemaVersionCurrent is the state file schema version written by this build.
	// v3 replaced the single-purpose metadata.scafctlVersion field with a
	// metadata.runtime block that separates the execution engine identity
	// (runtime.engine) from the invoking CLI/frontend identity (runtime.cli).
	// Legacy v2 files still load (SchemaVersionMinimum stays 2) but their old
	// scafctlVersion value is intentionally NOT migrated into runtime -- the
	// runtime block is re-stamped from the current invocation on the next save.
	SchemaVersionCurrent = 3

	// SchemaVersionMinimum is the oldest state file schema version this build can
	// safely load. Bump this ONLY when a breaking change makes older files unsafe
	// to read -- e.g. schema v1 stored immutable locks under a legacy `immutables`
	// field that is now dropped on unmarshal, so loading a v1 file would silently
	// discard immutable enforcement. Additive, backward-compatible bumps should
	// raise SchemaVersionCurrent WITHOUT raising this floor so older files still load.
	SchemaVersionMinimum = 2
)
View Source
const (
	SectionKindKeyValue = "keyValue"
	SectionKindRows     = "rows"
)

Section kinds. Key/value sections describe a single object (e.g. metadata); row sections describe a collection rendered as a table (e.g. resolvers).

View Source
const SectionNameOverview = "overview"

SectionNameOverview is the name of the leading section that collects top-level scalar fields (e.g. schemaVersion). It exists so no schema field is silently dropped from the projection; presentation layers may choose to omit it.

Variables

View Source
var (
	// ErrInvalidBackend indicates the configured backend provider lacks CapabilityState.
	ErrInvalidBackend = errors.New("state backend provider does not have CapabilityState")

	// ErrKeyNotFound indicates a requested state key does not exist.
	ErrKeyNotFound = errors.New("state key not found")

	// ErrImmutableEntry indicates an attempt to overwrite an immutable state entry.
	ErrImmutableEntry = errors.New("cannot overwrite immutable state entry")

	// ErrUnsupportedSchemaVersion indicates the state file was written by a newer
	// version of scafctl and cannot be safely read by this version.
	ErrUnsupportedSchemaVersion = errors.New("unsupported state schema version")

	// ErrIncompatibleSchemaVersion indicates the state file was written by an
	// older, no-longer-supported schema version whose layout cannot be safely
	// read by this build (e.g. a breaking change dropped a field). The file must
	// be deleted and recreated.
	ErrIncompatibleSchemaVersion = errors.New("incompatible state schema version")
)

Functions

func MergeParameters

func MergeParameters(saved, cli map[string]any) map[string]any

MergeParameters combines saved parameters from state with CLI parameters. CLI parameters take precedence (overwrite existing keys). New CLI keys are added. Returns the effective parameter set for this execution.

func MissingParams added in v0.28.0

func MissingParams(ctx context.Context, config *Config, params map[string]any) []string

MissingParams returns the subset of RequiredParams that are absent from the supplied params map. Returns nil if all required params are present.

func PersistResolvers added in v0.34.0

func PersistResolvers(stateData *Data, resolverCtx *resolver.Context, resolvers []*resolver.Resolver, skip map[string]bool) error

PersistResolvers records resolver values into state after execution. For each resolver marked persist: true or immutable: true (immutable implies persist):

  • Persist-only resolvers have their value overwritten each run (UpdatedAt advances; CreatedAt is preserved from any prior entry).
  • Immutable resolvers are locked on first write; on subsequent runs the resolved value is verified against the locked value and an error is returned on mismatch.

Only resolvers that completed with status Success are recorded; skipped or failed resolvers leave any prior entry untouched.

Resolvers whose names are present in skip are not locked on first run: a new immutable value is not persisted when its deferred (cross-resolver) validation failed. Existing locks are still verified so an immutable value cannot drift undetected.

func RequiredParams added in v0.28.0

func RequiredParams(ctx context.Context, config *Config) []string

RequiredParams extracts the __params keys referenced by the state configuration. These are the CLI parameters (-r flags) that must be supplied for the state backend to resolve its inputs at load time.

It inspects Enabled and Backend.Inputs ValueRefs for:

  • CEL expressions: uses Expression.GetVariablesWithPrefix("__params.")
  • Go templates: uses GetGoTemplateReferences() and filters for .__params.*

Literal and resolver-ref ValueRefs are skipped (they don't use __params). SaveOverrides are also skipped (they are only evaluated at save time).

Returns a deduplicated, sorted list of parameter names (e.g., ["app_name", "project"]). Errors during parsing are silently ignored (best-effort extraction).

func ResolveStatePath

func ResolveStatePath(path, baseDir string) (string, error)

ResolveStatePath resolves a state file path. Absolute paths are used as-is. Relative paths are resolved against baseDir. Path traversal (../) is rejected.

func RuntimeProvenanceFromContext added in v0.34.0

func RuntimeProvenanceFromContext(ctx context.Context) settings.RuntimeProvenance

RuntimeProvenanceFromContext builds execution provenance from the ambient CLI settings. It is a thin wrapper over settings.RuntimeProvenanceFromContext so callers in the command layer need not import settings directly.

func SaveToFile

func SaveToFile(path, baseDir string, sd *Data) error

SaveToFile marshals and writes StateData to a JSON file using atomic write. Relative paths are resolved against baseDir.

func VerifyImmutables added in v0.34.0

func VerifyImmutables(stateData *Data, resolverCtx *resolver.Context, resolvers []*resolver.Resolver) error

VerifyImmutables checks resolved immutable values against previously locked state WITHOUT mutating state or locking new values. It is intended to run after resolver execution but BEFORE action execution, so that a violated immutable aborts the run before any side effects (file scaffolding, external calls, etc.) occur.

Only resolvers with an existing locked entry are verified. New immutables (no prior entry) are ignored here -- they are locked later by PersistResolvers at save time. Returns an error on the first mismatch.

func WithState

func WithState(ctx context.Context, data *Data) context.Context

WithState returns a new context with the StateData stored in it.

Types

type Backend

type Backend struct {
	// Provider is the name of a registered provider with CapabilityState (e.g., "file").
	Provider string `json:"provider" yaml:"provider" doc:"Provider name with CapabilityState" maxLength:"253" example:"file"`

	// Inputs are provider-specific inputs. Each value is a ValueRef for dynamic resolution.
	//
	// CEL expressions use __params for CLI parameters (e.g. __params.project) and _ for
	// resolver outputs (available at save time only, not load time).
	//
	// Go templates spread resolver data at top level (e.g. {{ .name }}) and expose CLI
	// parameters under __params (e.g. {{ .__params.project }}).
	Inputs map[string]*spec.ValueRef `json:"inputs" yaml:"inputs" doc:"Provider-specific inputs (ValueRef for dynamic resolution)"`

	// SaveOverrides are provider-specific inputs resolved only at save time when
	// resolver data (_) is available. Keys that overlap with Inputs override them
	// at save time. This enables patterns like loading state from a fixed branch
	// (via Inputs) and saving to a resolver-derived branch (via SaveOverrides).
	//
	// At load time, SaveOverrides are completely ignored -- no errors are raised
	// for resolver-dependent expressions.
	SaveOverrides map[string]*spec.ValueRef `json:"saveOverrides,omitempty" yaml:"saveOverrides,omitempty" doc:"Save-time-only inputs that override Inputs keys"`
}

Backend configures the state persistence backend.

type CommandInfo

type CommandInfo struct {
	// Subcommand is the CLI subcommand used (e.g., "run solution").
	Subcommand string `json:"subcommand" doc:"CLI subcommand used" maxLength:"100" example:"run solution"`

	// Parameters are the key-value pairs from --parameter flags for the most recent run.
	Parameters map[string]string `json:"parameters" doc:"Key-value pairs from --parameter flags"`
}

CommandInfo captures the most recent invocation for validation replay. Only the latest invocation is stored -- no history.

type Config

type Config struct {
	// Enabled controls whether state persistence is active. Supports literal bool, CEL expression, or template.
	// Resolver references (rslvr:) are not supported because state is loaded before resolver execution.
	// Use __params to reference CLI parameters (e.g. expr: "__params.enable_state == true").
	Enabled *spec.ValueRef `json:"enabled" yaml:"enabled" doc:"Dynamic activation of state persistence"`

	// Backend configures which provider handles state persistence.
	Backend Backend `json:"backend" yaml:"backend" doc:"Backend provider configuration"`
}

Config is the solution-level state configuration. It is a top-level peer to Spec, Catalog, Bundle, and Compose on the Solution struct.

CLI parameters passed via -r flags are available as __params in CEL expressions and Go templates used in Enabled and Backend.Inputs. This allows dynamic backend configuration without requiring resolver execution (which happens after state load).

Example:

state:
  enabled:
    expr: "__params.state_enabled == true"
  backend:
    provider: file
    inputs:
      path:
        expr: "'gcp/' + __params.project + '/state.json'"

type Data

type Data struct {
	// SchemaVersion enables forward-compatible format migrations.
	SchemaVersion int `json:"schemaVersion" doc:"Format version for migrations"`

	// Metadata identifies the solution and tracks timestamps.
	Metadata Metadata `json:"metadata" doc:"Solution identity and timestamps"`

	// Command captures the most recent invocation for validation replay.
	Command CommandInfo `json:"command" doc:"Most recent invocation"`

	// Parameters stores the merged parameter set used for replay. On each run,
	// CLI parameters are merged into this map (CLI wins on conflict, new keys added).
	// When no CLI params are provided, these saved parameters drive replay.
	Parameters map[string]any `json:"parameters" doc:"Merged parameter set for replay"`

	// Resolvers maps resolver names to their persisted outputs. Resolvers marked
	// persist: true have their value recorded here after each successful run for
	// later retrieval via the state provider. Resolvers marked immutable: true are
	// also stored here (with Immutable set) and verified on subsequent runs.
	Resolvers map[string]*PersistedEntry `json:"resolvers" doc:"Persisted resolver values"`

	// Fingerprints stores file and input hashes for action up-to-date checks.
	// Keys use the format "__fingerprint:<actionName>:<type>".
	Fingerprints map[string]*FingerprintEntry `json:"fingerprints" doc:"Action fingerprint hashes"`
}

Data is the complete persisted state structure. It is serialized as JSON to the backend storage.

func FromContext

func FromContext(ctx context.Context) (*Data, bool)

FromContext retrieves the StateData from the context. Returns the StateData and true if found, or nil and false if not present.

func LoadFromFile

func LoadFromFile(path, baseDir string) (*Data, error)

LoadFromFile reads and unmarshals a StateData JSON file. If the file does not exist, it returns an empty StateData. Relative paths are resolved against baseDir.

func NewData

func NewData() *Data

NewData returns an initialized empty StateData with the current schema version.

func NewMockData

func NewMockData(solution, version string, params map[string]any) *Data

NewMockData creates a StateData populated with test data. Use this in tests that need a pre-populated state.

func (*Data) Summarize added in v0.34.0

func (sd *Data) Summarize() Summary

Summarize projects state Data into a Summary for the header line. It is side-effect free and safe to call on a freshly created (empty) Data.

type FingerprintEntry

type FingerprintEntry struct {
	// Value is the stored hash string.
	Value string `json:"value" doc:"Stored hash value"`

	// UpdatedAt is when this fingerprint was last written.
	UpdatedAt time.Time `json:"updatedAt" doc:"When this fingerprint was last written"`
}

FingerprintEntry stores a fingerprint hash for action up-to-date checks.

type ListView added in v0.34.0

type ListView struct {
	// Sections are the ordered display groups derived from Data.
	Sections []Section `json:"sections"`
}

ListView is a schema-driven, display-oriented projection of state Data. It is built by reflecting over Data, so new top-level fields (and new fields on persisted-entry types) automatically surface as sections/columns without any change to this view. Struct fields become key/value sections, map fields become key-sorted row sections, and top-level scalars are grouped into a leading "overview" section so no schema field is silently dropped.

func BuildListView added in v0.34.0

func BuildListView(sd *Data) *ListView

BuildListView reflects over state Data and projects it into ordered display sections. The transformation is schema-driven and side-effect free:

  • each exported struct field becomes a key/value section,
  • each exported map field becomes a section of key-sorted rows,
  • top-level scalars are collected into a leading "overview" section,
  • time.Time values are formatted with displayTimeFormat (zero -> empty).

Because it is driven entirely by the Data type, adding a field to Data or to a persisted-entry type surfaces here automatically with no manual update.

func (*ListView) EntryCount added in v0.34.0

func (v *ListView) EntryCount() int

EntryCount returns the total number of rows across all collection (row) sections. Key/value identity sections (overview, metadata, command) are not counted, mirroring the notion of "how many stored entries exist".

func (*ListView) SectionByName added in v0.34.0

func (v *ListView) SectionByName(name string) *Section

SectionByName returns a pointer to the section with the given name, or nil if no such section exists.

type LoadResult

type LoadResult struct {
	// Ctx is the context enriched with state.WithState.
	Ctx context.Context

	// Data is the loaded (or empty) state data.
	Data *Data

	// MergedParams is the effective parameter set (saved params merged with CLI params).
	// CLI params override saved params; new CLI keys are added.
	MergedParams map[string]any

	// Skipped is true when state is disabled or the enabled ValueRef is falsy.
	Skipped bool
}

LoadResult is returned by Load with the loaded state and merged parameters.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager orchestrates the state lifecycle: pre-execution loading, parameter merging, and post-execution saving. It is called by the CLI command layer before and after resolver execution.

func NewManager

func NewManager(config *Config, registry *provider.Registry, runtime settings.RuntimeProvenance) *Manager

NewManager creates a state manager for the given state configuration. The provenance records both the engine (scafctl library) and invoking CLI/frontend identities; see settings.RuntimeProvenance.

func (*Manager) Load

func (m *Manager) Load(ctx context.Context, params map[string]any, command CommandInfo) (*LoadResult, error)

Load executes the pre-execution state lifecycle:

  1. Evaluates the enabled ValueRef using CLI params (available as __params in CEL expressions). No resolver data is available at load time.
  2. If disabled, returns LoadResult{Skipped: true}.
  3. Resolves backend inputs with params as __params.
  4. Calls the backend provider with operation=state_load.
  5. Merges saved parameters with CLI params (CLI wins on conflict).
  6. Captures command info.
  7. Injects state into context via WithState.

func (*Manager) Save

func (m *Manager) Save(ctx context.Context, stateData *Data, resolverCtx *resolver.Context, resolvers []*resolver.Resolver, mergedParams, resolverData map[string]any, solMeta SolutionMeta) error

Save executes the post-execution state lifecycle:

  1. Saves the merged parameter set.
  2. Checks immutable resolvers (saves new, verifies existing).
  3. Updates metadata timestamps.
  4. Calls the backend provider with operation=state_save.

params are the merged parameters for this execution. resolverData contains resolver outputs, available as _ in CEL expressions for backend inputs.

func (*Manager) SaveImmutables added in v0.34.0

func (m *Manager) SaveImmutables(ctx context.Context, stateData *Data, resolverCtx *resolver.Context, resolvers []*resolver.Resolver, mergedParams, resolverData map[string]any, solMeta SolutionMeta, skip map[string]bool) error

SaveImmutables checks and persists immutable resolver locks without touching the saved parameter set. It is used by entrypoints that run side-effecting actions: immutable locks are committed after deferred validation passes and before actions run, so a value that is now fixed is persisted even if a downstream action later fails.

skip contains resolver names whose deferred validation failed; their immutable values are not locked.

func (*Manager) SaveParams added in v0.34.0

func (m *Manager) SaveParams(ctx context.Context, stateData *Data, mergedParams, resolverData map[string]any, solMeta SolutionMeta) error

SaveParams persists the merged parameter set. It is called after actions complete so that ordinary (mutable) parameters are only saved on a fully successful run.

func (*Manager) VerifyImmutables added in v0.34.0

func (m *Manager) VerifyImmutables(stateData *Data, resolverCtx *resolver.Context, resolvers []*resolver.Resolver) error

VerifyImmutables checks that resolved immutable values have not changed relative to previously locked state, WITHOUT mutating state or locking new values. Call it after resolver execution but BEFORE action execution so that a locked-value violation aborts the run before any side effects occur.

It is a no-op when state is disabled or no state data is present.

type Metadata

type Metadata struct {
	// Solution is the solution name from metadata.name.
	Solution string `json:"solution" doc:"Solution name from metadata.name" maxLength:"253"`

	// Version is the solution semver string.
	Version string `json:"version" doc:"Solution semver" maxLength:"30"`

	// CreatedAt is when the state file was first created.
	CreatedAt time.Time `json:"createdAt" doc:"First state file creation"`

	// LastUpdatedAt is when the state file was most recently saved.
	LastUpdatedAt time.Time `json:"lastUpdatedAt" doc:"Most recent save"`

	// Runtime records the execution provenance that last wrote the state,
	// separating the engine (library) identity from the invoking CLI/frontend.
	Runtime Runtime `json:"runtime" doc:"Execution provenance that last wrote the state"`
}

Metadata identifies the solution and tracks state lifecycle timestamps.

type MissingParamsError added in v0.28.0

type MissingParamsError struct {
	// Missing is the sorted list of __params keys not found in the supplied params.
	Missing []string `json:"missing" yaml:"missing" doc:"Parameter names required by state configuration"`

	// Original is the underlying evaluation error.
	Original error `json:"-" yaml:"-" doc:"Underlying evaluation error"`
}

MissingParamsError is returned when state load fails because the state configuration references __params keys that were not supplied via CLI -r flags. It wraps the original evaluation error and includes the list of missing parameter names so callers can produce actionable messages.

func (*MissingParamsError) Error added in v0.28.0

func (e *MissingParamsError) Error() string

func (*MissingParamsError) Unwrap added in v0.28.0

func (e *MissingParamsError) Unwrap() error

type PersistedEntry added in v0.34.0

type PersistedEntry struct {
	// Value is the persisted resolver value.
	Value any `json:"value" doc:"Persisted resolver value"`

	// Type is the resolver's declared type.
	Type string `json:"type" doc:"Resolver declared type" maxLength:"30" example:"string"`

	// Immutable discriminates immutable entries (locked and verified) from
	// persist-only entries (overwritten each run).
	Immutable bool `json:"immutable,omitempty" doc:"Whether the entry is locked and verified across runs"`

	// CreatedAt is when the entry was first stored and never changes thereafter.
	// For entries created immutable this is the lock time. For an entry promoted
	// from persist-only to immutable it is the original persist time, not the
	// lock time.
	CreatedAt time.Time `json:"createdAt" doc:"When the value was first stored"`

	// UpdatedAt is when the entry was last written. For persist-only entries it
	// advances each run. For entries created immutable it equals CreatedAt. For
	// an entry promoted from persist-only to immutable it is the promotion (lock)
	// time and is later than CreatedAt.
	UpdatedAt time.Time `json:"updatedAt" doc:"When the value was last written"`
}

PersistedEntry is a resolver value persisted in state. Persist-only entries are overwritten on each run; immutable entries (Immutable set) are locked on first write and verified thereafter.

type Runtime added in v0.34.0

type Runtime struct {
	// Engine identifies the execution engine (the scafctl library) that
	// performed the run.
	Engine RuntimeComponent `json:"engine" doc:"Execution engine (scafctl library) identity"`

	// CLI identifies the CLI/frontend that invoked execution. For embedded
	// runners this is the wrapper binary; for direct scafctl use it mirrors
	// Engine.
	CLI RuntimeComponent `json:"cli" doc:"Invoking CLI/frontend identity"`
}

Runtime records execution provenance: which engine performed the run and which CLI/frontend invoked it. When scafctl is run directly (not embedded), CLI mirrors Engine.

type RuntimeComponent added in v0.34.0

type RuntimeComponent struct {
	// Name is the component's binary/identifier name.
	Name string `json:"name" doc:"Component name" maxLength:"64" example:"scafctl"`

	// Version is the component's version string. May be empty when unknown.
	Version string `json:"version" doc:"Component version" maxLength:"64" example:"0.5.0"`
}

RuntimeComponent identifies a named, versioned runtime participant.

type Section added in v0.34.0

type Section struct {
	// Name is the section identifier (json-tag name of the source field).
	Name string `json:"name"`

	// Kind is either SectionKindKeyValue or SectionKindRows.
	Kind string `json:"kind"`

	// Fields holds the key/value pairs for a SectionKindKeyValue section.
	Fields map[string]any `json:"fields,omitempty"`

	// Rows holds the ordered rows for a SectionKindRows section.
	Rows []map[string]any `json:"rows,omitempty"`
}

Section is one display group within a ListView. Key/value sections populate Fields; row sections populate Rows. The Name matches the json-tag name of the originating Data field so the display mirrors the on-disk layout.

func (Section) Title added in v0.34.0

func (s Section) Title() string

Title returns the section name capitalized for use as a display heading.

type SolutionMeta

type SolutionMeta struct {
	Name    string
	Version string
}

SolutionMeta contains solution identity for state metadata.

type Summary added in v0.34.0

type Summary struct {
	// Solution is the solution name the state belongs to (may be empty).
	Solution string `json:"solution"`

	// Version is the solution semver the state belongs to (may be empty).
	Version string `json:"version"`

	// SchemaVersion is the state file format version.
	SchemaVersion int `json:"schemaVersion"`

	// LastUpdated is when the state was most recently saved (zero if never).
	LastUpdated time.Time `json:"lastUpdated"`

	// ParameterCount is the number of stored replay parameters.
	ParameterCount int `json:"parameterCount"`

	// ResolverCount is the number of persisted resolver entries.
	ResolverCount int `json:"resolverCount"`

	// FingerprintCount is the number of stored action fingerprints.
	FingerprintCount int `json:"fingerprintCount"`
}

Summary is a compact, at-a-glance projection of a state document: its identity (solution, version), when it was last written, and the size of each stored collection. It backs the one-line header shown above the detailed sections in human output so a user can grasp "what is this state" without scanning tables.

Jump to

Keyboard shortcuts

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