runtime

package
v0.8.0-a.16 Latest Latest
Warning

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

Go to latest
Published: Jun 20, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package runtime is the execution engine linked into every compiled factory binary.

Owns:

  • DAG construction (implicit deps via reference; explicit via @depends-on)
  • Plan computation: refresh + drift + change detection + replace-because chains
  • Apply execution: parallelism cap (default 10), per-resource state writes, apply error UX
  • State model (snapshots, content-addressed, encrypted at rest)
  • Action semantics (triggered with @trigger; 'always' literal; @lock; @timeout)

Companion packages:

  • pkg/sdk/state - Backend contract that provider libraries implement
  • pkg/state/local and pkg/state/s3 - the filesystem and S3 backends
  • pkg/runner - the factory CLI that invokes runtime entry points

Index

Constants

View Source
const DefaultParallelism = 10

DefaultParallelism is the in-flight cap apply uses when no explicit value is given on the Executor or in the plan file.

View Source
const PlanFormatVersion = 1

PlanFormatVersion is the schema version this package reads and writes for plan files.

View Source
const TriggerAlways = "always"

TriggerAlways is the literal an action uses to opt into running every time, regardless of stored state.

Variables

View Source
var ErrEvalNotFound = errors.New("not found")

ErrEvalNotFound is returned by Eval when an address or field cannot be resolved in the current scope. Plan callers may treat it as "known after apply"; apply re-evaluates against the live scope and surfaces a real failure when the reference truly is invalid.

View Source
var ErrInstanceGone = errors.New("instance no longer in iterable")

ErrInstanceGone is returned by ensureCompositeScope when a per- instance composite scope is requested for a key that the boundary's `@for-each` iterable no longer yields. Plan-time seeding of prior state treats this as a signal to skip rather than fail; orphan destroy steps for the missing instance still emit through the usual orphan path.

View Source
var ErrInterrupted = errors.New("apply: interrupted")

ErrInterrupted is returned by ApplyPlan when the executor's Drain channel was closed before all steps could be dispatched. The returned snapshot still reflects every step that completed before the drain, so re-plan plus apply will pick up the remainder.

View Source
var ErrNotFound = errors.New("resource not found")

ErrNotFound is returned by a resource's Read method when the resource is absent in the cloud. The runtime treats it as a request to recreate.

Functions

func ApplyBindings added in v0.6.0

func ApplyBindings(ctx *EvalContext, binds []lang.EachBinding)

ApplyBindings copies a constraint's iteration bindings onto the context so @each and any chained level name resolve during the element's evaluation.

func Changed

func Changed[T any](prior, current T) bool

Changed reports whether a field differs between its prior and current value. It compares by value, so a pointer field compares what it points at, and a state round trip that re-decodes an equal value is not a false positive.

func CompositeInputNames

func CompositeInputNames(n *Node) map[string]bool

CompositeInputNames returns the input names declared by a composite boundary.

func CoreFunctionSigs added in v0.6.0

func CoreFunctionSigs() map[string]typecheck.FuncSig

CoreFunctionSigs returns each @core function's signature, keyed by name, for compile-time existence, arity, and type checking.

func Decode

func Decode(v any, inputs map[string]any) error

Decode fills v's exported fields from the inputs map using `ub` struct tags. A field's key is the tag's name, or the kebab-cased field name when the tag has no name (or no tag at all). String values like "30s" decode into time.Duration fields. v must be a non-nil pointer to a struct.

func DirectParent

func DirectParent(addr string) string

DirectParent returns the substring before the last `/` in addr, or the empty string when addr has no `/`. Unlike templateAddress, DirectParent preserves `['key']` segments so the result names a per-instance composite call site when one is present.

func DotPathString added in v0.6.0

func DotPathString(p *lang.DotPath) string

dotPathString renders a dotted reference back to its source form. Named segments are joined with `.`; indexed segments preserve the `['<key>']` form when the index is a string literal, and otherwise collapse to `[...]` so the path stays readable.

func EncodePlan

func EncodePlan(p *Plan) ([]byte, error)

EncodePlan renders a plan as JSON bytes for on-disk storage.

func Eval

func Eval(e lang.Expr, ctx *EvalContext) (any, error)

Eval reduces a parsed expression to a Go value. Supported are literals, bare identifiers (as their name string); array and object literals (recursive); and the `var.X[.Y...]` address form.

func RefAddress added in v0.6.0

func RefAddress(p *lang.DotPath) string

func Refs

func Refs(e lang.Expr) []string

Refs returns the addresses an expression depends on, in source order with duplicates removed. Each returned address is the canonical form of another node: var.name, resource.name, data.name, or action.name. Field segments past the node address and @each.X bindings are skipped.

func ScopeRef added in v0.6.0

func ScopeRef(ref, callSite string) string

scopeRef rewrites a reference into a composite internal address. `resource.inner` under call site `resource.outer` becomes `resource.outer/resource.inner`; every segment keeps its own kind root, so resource, data, and action refs all join the same way. Var refs and unsupported kinds pass through unchanged so toposort skips them. An empty callSite means the ref is already in its target scope (a top-level boundary's body refs, or a no-op when walking up past the outermost scope) and the ref returns unchanged.

func SealPlan

func SealPlan(p *Plan, enc encrypt.Encrypter) ([]byte, error)

SealPlan encodes p and seals the body in the shared state.Envelope, ready for atomic write. The envelope records the encrypter's own description, whether the operator wrote an encryption block or the resolver chose a default.

func SplitInstanceAddress

func SplitInstanceAddress(addr string) (template, key string)

SplitInstanceAddress separates a `<template>['<key>']` address into its template part and the instance key. Non-instance addresses return unchanged with an empty key.

func UnknownRefAddress

func UnknownRefAddress(p *lang.DotPath, nodes map[string]*Node, scope string) string

Types

type ActionRegistration

type ActionRegistration interface {
	NewReceiver() any
	Run(ctx context.Context, receiver, cfg any) (any, error)
	OutputType() reflect.Type
}

ActionRegistration is the type-erased registration for actions.

func MakeAction

func MakeAction[T, Out, Config any, PT actionPtr[T, Out, Config]]() ActionRegistration

MakeAction produces an ActionRegistration that wraps a TypedAction[Out, Config] implemented by *T.

func MakeActionWith

func MakeActionWith[T, Out, Config any, PT actionPtr[T, Out, Config]](
	construct func() *T,
) ActionRegistration

MakeActionWith is the variant of MakeAction that captures external state through the constructor.

type ApplyError

type ApplyError struct {
	Address        string
	Kind           NodeKind
	Decision       Decision
	Library        string
	Elapsed        time.Duration
	Err            error
	SkippedCount   int
	SucceededCount int
}

ApplyError is the structured failure value runApplySchedule returns when a step's CRUD or action call reports an error. The original error is available via Unwrap so callers can use errors.Is and errors.As; the runner uses the structured fields to print a multi line report that names the failing address, its decision and library, the elapsed time, and the counts of steps that were skipped or completed alongside it.

func (*ApplyError) Error

func (e *ApplyError) Error() string

func (*ApplyError) Unwrap

func (e *ApplyError) Unwrap() error

type ApplyEvent

type ApplyEvent struct {
	Address string
	Kind    NodeKind

	// Composite marks an event for a composite call site (a boundary).
	// A boundary's Kind is its own resource/data/action kind, so this
	// is what tells a boundary apart from a leaf of that kind.
	Composite bool

	Decision Decision
	Stage    ApplyStage
	Time     time.Time
	Elapsed  time.Duration
	Err      error
}

ApplyEvent is one observation the scheduler hands to the optional Executor.Events channel during a run. The renderer in the runner consumes these to print live per-step progress on stderr or to emit one JSON object per event under --json.

type ApplyStage

type ApplyStage string

ApplyStage tags one moment in a step's apply lifecycle.

const (
	// StageStart fires when the scheduler hands the step to a worker.
	StageStart ApplyStage = "start"
	// StageDone fires when the worker reports a successful result.
	StageDone ApplyStage = "done"
	// StageFail fires when the worker returns an error. Apply will
	// halt further dispatch but already-running siblings still emit
	// their own done or fail events.
	StageFail ApplyStage = "fail"
)

type CompositeType

type CompositeType struct {
	Name       string
	Kind       NodeKind
	SyntaxBody *syntax.FactoryBody
	Libraries  map[string]*Library
}

CompositeType registers a UB-implemented type under a library. SyntaxBody is the grammar-first body used by graph extraction.

Libraries is the resolved import table for this composite's body, keyed by the alias declared in the body's `imports:` block. The runtime looks up composite-internal nodes against this table, not the stack root's, so a composite can be reused without the caller importing every library it transitively uses. A nil Libraries uses the executor's root Libraries table.

type DAG

type DAG struct {
	Nodes map[string]*Node
	Edges map[string][]string
}

DAG is a stack's runtime dependency graph: every addressable node indexed by its address, and the list of node addresses each one depends on, collected from references in the body and from any `@depends-on` meta key.

func BuildSyntaxDAG

func BuildSyntaxDAG(body syntax.FactoryBody, libs map[string]*Library) *DAG

BuildSyntaxDAG builds the dependency graph from a typed factory or composite body.

func (*DAG) TopologicalOrder

func (g *DAG) TopologicalOrder() ([]string, error)

TopologicalOrder returns the DAG's nodes in dependency order: every node appears after the nodes it references. Edges to non-node addresses such as `var.X` are skipped, since vars are bound from inputs and not block execution. Returns an error naming the involved addresses when the graph contains a cycle.

func (*DAG) UnderForEachComposite added in v0.6.0

func (g *DAG) UnderForEachComposite(n *Node) bool

UnderForEachComposite reports whether any composite call site in n's ancestry is itself a `@for-each` template.

type DataSourceRegistration

type DataSourceRegistration interface {
	NewReceiver() any
	Read(ctx context.Context, receiver, cfg any) (any, error)
	OutputType() reflect.Type
}

DataSourceRegistration is the type-erased registration for data sources.

func MakeDataSource

func MakeDataSource[T, Out, Config any, PT dataSourcePtr[T, Out, Config]]() DataSourceRegistration

MakeDataSource produces a DataSourceRegistration that wraps a TypedDataSource[Out, Config] implemented by *T.

func MakeDataSourceWith

func MakeDataSourceWith[T, Out, Config any, PT dataSourcePtr[T, Out, Config]](
	construct func() *T,
) DataSourceRegistration

MakeDataSourceWith is the variant of MakeDataSource that captures external state through the constructor.

type Decision

type Decision string

Decision tags one node's planned action.

const (
	DecisionCreate  Decision = "create"
	DecisionUpdate  Decision = "update"
	DecisionReplace Decision = "replace"
	DecisionDestroy Decision = "destroy"
	DecisionNoOp    Decision = "no-op"
	DecisionRerun   Decision = "rerun"
	DecisionSkip    Decision = "skip"
	DecisionRead    Decision = "read"
	DecisionEval    Decision = "eval"
)

type EvalContext

type EvalContext struct {
	Vars      map[string]any
	Resources map[string]any
	Data      map[string]any
	Actions   map[string]any
	Libraries map[string]*Library
	Bindings  map[string]any

	// Each holds named iteration bindings, @each for a @for-each body
	// and declared names like @rule for a chained constraint form,
	// each a key/value record read as @name.key and @name.value.
	Each map[string]lang.EachValue

	// MissingAsNull makes path navigation yield null instead of
	// ErrEvalNotFound, or a hard error, when a key is absent or a parent
	// is itself null. The constraint checkers set it so a predicate over
	// an unset optional input, including a nested one, reduces to a
	// boolean rather than collapsing the whole expression to null or
	// failing on a null parent. Navigating into a non-null scalar is
	// still an error. It stays false everywhere else, because the planner
	// relies on ErrEvalNotFound to detect forward references to upstreams
	// that have not run yet.
	MissingAsNull bool
	// contains filtered or unexported fields
}

EvalContext supplies the values that addresses resolve against. Vars is the validated `inputs:` map after the stack file and `UB_VAR_*` env overrides. Resources, Data, and Actions hold the outputs of nodes that have already executed, indexed by their source address path. Libraries is the import table the scope's `<alias>.<func>(...)` calls resolve against; nil disables library-qualified calls. Bindings holds comprehension-bound names, which resolve as bare values and as dot-path roots ahead of the reserved roots; validation keeps the names distinct across nesting.

func NewEvalContext added in v0.7.0

func NewEvalContext(f *lang.File) *EvalContext

NewEvalContext returns an EvalContext whose local.<name> references resolve against f's locals: block. Callers fill the remaining fields for their scope. A nil file, or one without locals, yields a context where any local reference reports the local as not declared.

func NewEvalContextFromLocals

func NewEvalContextFromLocals(exprs map[string]lang.Expr) *EvalContext

NewEvalContextFromLocals returns an EvalContext whose local.<name> references resolve against exprs. Callers fill the remaining fields for their scope.

type ExecResult

type ExecResult struct {
	Outputs    map[string]any
	Actions    map[string]any
	Data       map[string]any
	WrittenRev string
}

ExecResult is what the Executor produces: the outputs map, the Action and Data tables populated during the run, and the rev of the snapshot written (empty when no Store was configured).

type Executor

type Executor struct {
	DAG       *DAG
	Libraries map[string]*Library
	Inputs    map[string]any

	// SyntaxSource is the typed factory body for grammar-first callers.
	SyntaxSource *syntax.FactoryBody

	Store   state.Backend
	Factory state.FactoryInfo

	// Parallelism caps the number of in-flight resource, data, and
	// action steps during ApplyPlan. Zero or negative falls back to
	// DefaultParallelism.
	Parallelism int

	// Destroy makes Plan compute a teardown: every resource in prior
	// state is planned for destroy and no outputs are evaluated. The
	// source is still parsed and its configurations still resolve, so
	// the deletes use the right credentials.
	Destroy bool

	// Drain, when non-nil, lets the caller ask the scheduler to stop
	// dispatching new steps without canceling the apply context. The
	// runner closes this channel on SIGINT so in-flight CRUD calls
	// finish and their state writes commit; SIGTERM cancels the
	// context directly. A nil channel disables the drain signal.
	Drain <-chan struct{}

	// Events, when non-nil, receives one ApplyEvent per step stage
	// during ApplyPlan: start when the scheduler hands the step to a
	// worker, done or fail when the worker returns. The caller owns
	// the channel and is responsible for sizing the buffer and
	// closing it after ApplyPlan returns. A nil channel disables
	// event emission.
	Events chan<- ApplyEvent
	// contains filtered or unexported fields
}

Executor owns the parsed DAG, imported libraries, caller inputs, and state backend. It exposes three lifecycle methods: Plan computes a PlanStep slice against prior state without running any CRUD, ApplyPlan executes a previously computed plan, and Refresh reads each prior-state resource and writes back observed outputs. Store and Stack must always be set.

func (*Executor) ApplyPlan

func (e *Executor) ApplyPlan(ctx context.Context, pf *PlanFile) (*ExecResult, error)

ApplyPlan executes a previously computed PlanFile against the Executor's libraries, store, and parsed source. The DAG passed on the Executor is used for output expressions, while resource and action bodies come from the plan. The plan's stack identity must match the Executor's, and the prior state's rev must match what the plan was computed against. The stack's lock is held for the duration.

func (*Executor) CheckLibraryConfigs

func (e *Executor) CheckLibraryConfigs() error

CheckLibraryConfigs reports Go leaves whose import alias needs a config but has no library-configs entry in its scope.

func (*Executor) Plan

func (e *Executor) Plan(ctx context.Context) (*Plan, error)

Plan walks the DAG against prior state and returns the planned actions per node. Resources with prior state get their Read method invoked so the plan can report drift; no CRUD methods (Create, Update, Replace, Delete) are called and no actions run. Inputs that reference outputs of nodes about to run are evaluated against the prior state where available.

func (*Executor) Refresh

func (e *Executor) Refresh(ctx context.Context) (*RefreshResult, error)

Refresh reads every resource recorded in prior state and writes a fresh snapshot whose leaf outputs reflect the observation. Resources that are no longer present are dropped. Action and library-call entries, plus stack-level outputs, carry forward unchanged. No resource writes happen. The stack's lock is held for the duration.

type FactoryRef

type FactoryRef struct {
	Name            string `json:"name"`
	Version         string `json:"version"`
	ContentRevision string `json:"content-revision"`
}

FactoryRef identifies the stack a plan was computed against.

type FunctionType

type FunctionType struct {
	Name        string
	Description string
	ArgCount    int
	Variadic    bool
	Func        func(args []any) (any, error)
}

FunctionType registers a callable function under a Go library. Functions take pre-evaluated argument values and return a single value or an error. They run inline during expression evaluation and have no DAG node or state of their own.

ArgCount and Variadic declare the argument count the compiler enforces at each call site: a non-variadic function takes exactly ArgCount arguments, a variadic one takes ArgCount or more. A call's argument count is fixed in the source, so this check is compile-time only and the function body may assume it already holds.

func MakeFunc added in v0.6.0

func MakeFunc(name, description string, fn any) FunctionType

MakeFunc adapts a plain typed Go function into a FunctionType, the way text/template adapts a FuncMap entry. fn may take any number of parameters, variadic included, and must return (value, error); each parameter must be a type the evaluator's values convert to exactly: bool, int64, float64, string, any, or a slice or string-keyed map of those. Evaluated arguments convert to the parameter types on the way in and the result converts back to evaluator values on the way out, so the implementation stays plain typed Go and the argument count cannot disagree with the declaration.

MakeFunc panics on a fn that does not fit. Registration runs when a factory binary starts and when a library's own tests construct it; the compiler rejects the same signatures from source, so a malformed registration fails the importing factory's compile first.

type Library

type Library struct {
	Name          string
	Description   string
	Configuration cfg.Registration
	Actions       map[string]ActionRegistration
	Resources     map[string]ResourceRegistration
	DataSources   map[string]DataSourceRegistration
	// Composites are kept in one map per kind, mirroring the
	// Resources / DataSources / Actions Go-type maps. resource, data,
	// and action are distinct namespaces, so a library may declare
	// resource.foo and data.foo as separate composites.
	ResourceComposites map[string]*CompositeType
	DataComposites     map[string]*CompositeType
	ActionComposites   map[string]*CompositeType
	Functions          map[string]FunctionType
	// Schema carries the library's resource, data source, and action
	// output field sets. Populated by the dev CLI from a fetched Go
	// library's source for compile-time reference checking; nil at
	// runtime since the generated binary does not need it.
	Schema *LibrarySchema

	// Constraints holds each Go type's cross-field constraints in the
	// embeddable spec form, keyed by "<kind>.<type>" (e.g. "resource.vpc")
	// since resource, data, and action are distinct namespaces. codegen
	// sets it in the generated main.go from the constraints goschema
	// derived from the library's source; the plan checks a node against
	// Constraints[node.Kind + "." + node.Type]. UB composites carry their
	// own constraints in their bodies, so this stays empty for UB libraries.
	Constraints map[string][]lang.ConstraintSpec

	// Defaults holds each Go type's declared input defaults, keyed by
	// "<kind>.<type>" like Constraints and set the same way by codegen.
	// The runtime fills a Value default into a body's inputs wherever a
	// field is left out, before constraints, triggers, and decode read
	// them; an Optional marker fills nothing.
	Defaults map[string][]lang.DefaultSpec
}

Library is the registration record a library exports for its types, actions, and data sources. Go libraries supply Resources, Actions, and DataSources via the generic helpers (`MakeResource` and friends); UB libraries compiled by `unobin compile` contribute Composites with typed syntax bodies. The compiler links each imported library's record and aggregates it under the alias the calling source assigned to the import.

func (*Library) AddComposite

func (l *Library) AddComposite(ct *CompositeType)

AddComposite stores ct in the map for its kind, creating the map on first use.

func (*Library) Composite

func (l *Library) Composite(kind NodeKind, name string) *CompositeType

Composite returns the composite of the given kind and name, or nil when the library has none. resource, data, and action are independent namespaces, so the kind selects which map to consult.

type LibrarySchema

type LibrarySchema struct {
	Resources   map[string]*TypeSchema
	DataSources map[string]*TypeSchema
	Actions     map[string]*TypeSchema
	// Functions maps each function name the library exports to its
	// declared signature. The implementations live in the compiled Go
	// and cannot run at compile time, but the signature lets the
	// reference checker reject a call to an unknown function or one
	// given the wrong number of arguments, and the inferrer check each
	// argument's type and use the result type. A function registered
	// without declared types reads as all-Unknown, which counts
	// arguments but checks no types.
	Functions map[string]typecheck.FuncSig

	// Configuration describes the fields of the library's Configuration
	// struct, keyed by kebab-case field name. Nil when the library
	// declares no configuration or when the struct behind the
	// ConfigurationType's New function cannot be read from source.
	// HasConfiguration distinguishes the two: it is true whenever the
	// library declares a configuration, readable or not, so checks can
	// tell "no configuration" from "fields unknowable".
	Configuration         map[string]typecheck.Type
	ConfigurationFields   []typecheck.ObjectField
	ConfigurationDefaults []lang.DefaultSpec
	ConfigurationDigest   string
	ConfigurationEmpty    bool
	HasConfiguration      bool
}

LibrarySchema describes a Go library's registered resource, data source, and action types as the dev CLI sees them at compile time. Each entry is keyed by the type's kebab-case name (the same name used in factory source).

func (*LibrarySchema) ForType added in v0.6.0

func (s *LibrarySchema) ForType(kind NodeKind, typ string) *TypeSchema

ForType returns the schema for a node kind's type, or nil when the kind is not a resource, data, or action or the type is absent.

type MigrationState added in v0.6.0

type MigrationState struct {
	Inputs  map[string]any
	Outputs map[string]any
}

MigrationState is the pair of persisted maps a Migrator upgrades: the inputs the body evaluated to on the last apply and the outputs the resource returned then. Observed (see Prior) is plan-time only and is never persisted, so a migration covers inputs and outputs alone.

type Migrator

type Migrator interface {
	Migrate(oldVersion int, prior MigrationState) (MigrationState, error)
}

Migrator is an optional add-on a TypedResource may implement when its SchemaVersion has incremented past 1 and an older state entry needs upgrading. Migrate receives the whole recorded entry at the version it was written and returns it at the current version. Upgrading both halves together keeps the entry's single SchemaVersion stamp correct: were only the outputs upgraded, the entry would be stamped current while its inputs stayed at the old version, and a later input migration would never run.

type NoConfig

type NoConfig struct{}

NoConfig is the config parameter for libraries that declare no config.

type Node

type Node struct {
	Address             string
	Kind                NodeKind
	Alias               string
	Type                string
	Name                string
	Body                lang.Expr
	Composite           string
	CompositeSyntaxBody *syntax.FactoryBody
	Libraries           map[string]*Library

	ForEach lang.Expr

	// LockName is the value of a node body's `@lock:` field. Two nodes
	// sharing a non-empty LockName cannot run in parallel under apply's
	// scheduler, even on unrelated DAG branches. Empty means the node is
	// not under a named lock. It applies to any kind; since the
	// scheduler only runs nodes in parallel at apply, a lock has no
	// effect on a data source whose inputs are known and read at plan.
	LockName string

	// Timeout is the parsed value of a node body's `@timeout:` field: a
	// limit on how long the node's apply step may run. Zero means no
	// limit. On expiry the step's context is cancelled and the step
	// fails like any other apply error. Like @lock it only bites at
	// apply, so it does not bound a data source read at plan.
	Timeout time.Duration
}

Node is one addressable element of a stack: a single resource instance, data source, action, output, or composite call site. Address is the dotted form the language uses to reference the node from elsewhere, such as `resource.app`, `action.deploy`, or `output.cluster-arn`. Body is the source expression: an ObjectLit for resources, data, actions, and composites; any Expr for outputs.

A node inside a composite stores the call site address in Composite so the runtime evaluates its body against the composite's scope rather than the root. Its address looks like `resource.app/resource.inner`, with the call site as a prefix joined by a single `/`. For a composite that itself calls another composite the chain continues: `resource.outer/resource.inner/resource.leaf`, and each node's Composite names its direct enclosing call site.

CompositeSyntaxBody and Libraries are set only on a composite boundary (the call site node), and IsComposite reports that case. Libraries is the composite's resolved import table; the runtime resolves composite-internal node lookups against this map rather than the stack root's, so a composite can be reused without the caller importing every library it transitively uses.

func ExtractSyntaxNodes

func ExtractSyntaxNodes(body syntax.FactoryBody, libs map[string]*Library) []*Node

ExtractSyntaxNodes walks a typed factory or composite body and returns every addressable node in source order. The body is assumed to be validated.

func (*Node) IsComposite

func (n *Node) IsComposite() bool

IsComposite reports whether the node is a composite call site (a boundary) rather than a primitive leaf. A boundary has its own Kind (the call site's resource/data/action kind) just like a leaf; what sets it apart is the composite body populated only on boundaries.

type NodeKind

type NodeKind string

NodeKind tags a Node with its source block.

const (
	NodeResource      NodeKind = "resource"
	NodeData          NodeKind = "data"
	NodeAction        NodeKind = "action"
	NodeOutput        NodeKind = "output"
	NodeLibraryConfig NodeKind = "library-config"
)

type PanicError added in v0.6.0

type PanicError struct {
	Op      string
	Library string
	Value   any
	Stack   []byte
	Core    bool
}

PanicError reports that code the runtime called panicked. Every call into a library - a resource, action, data source, or function - is recovered at the boundary, so a defect there fails the step like any other error instead of crashing the process. A panic in unobin's own @core functions is recovered the same way but attributed to unobin rather than to a library.

Op names what was running when the panic happened. Library is the import alias to blame, filled in where the failing node is known and left empty when the runtime cannot place it. Value is whatever was passed to panic. Stack is the goroutine stack captured at the moment of recovery, kept for a verbose report.

func (*PanicError) Error added in v0.6.0

func (e *PanicError) Error() string

type PendingValue added in v0.6.0

type PendingValue struct {
	Refs []string
}

PendingValue marks a position in a plan step's recorded inputs where a forward reference kept the value from resolving at plan time. It holds the upstream source addresses the expression reads, so the plan renderer shows `<resource.X.field>` at the value's real position, including inside a list or object, rather than collapsing the whole field to one marker. It is recorded for display only: withoutPending resets every unresolved field for the plan walk's decisions, and knownFields removes it before seeding or the apply premise check, so a PendingValue never reaches a resource.

type Plan

type Plan struct {
	Factory  state.FactoryInfo
	Stack    string
	StateRev string
	Inputs   map[string]any
	Steps    []*PlanStep

	// Backend names the state backend the plan was computed against,
	// so apply can reconstruct the same backend without re-reading
	// the stack file. A nil value means the resolver's default (the local
	// backend under .unobin/state) was used.
	Backend *StateRef

	// Parallelism is the in-flight cap apply should honor. Zero means
	// the runtime's DefaultParallelism applies.
	Parallelism int

	// Destroy marks a teardown plan: every step is a destroy and apply
	// evaluates no outputs.
	Destroy bool
}

Plan is the readonly result of computing what an apply would do. StateRev is the snapshot rev the plan was computed against. Apply rejects the plan when the current rev no longer matches. Inputs captures the validated root inputs so apply can rebuild the same eval scope without re-reading the stack file.

type PlanFile

type PlanFile struct {
	FormatVersion int            `json:"format-version"`
	Factory       FactoryRef     `json:"factory"`
	Stack         string         `json:"stack"`
	StateRev      string         `json:"state-rev"`
	GeneratedAt   time.Time      `json:"generated-at"`
	Inputs        map[string]any `json:"inputs,omitempty"`

	Backend     *StateRef  `json:"backend,omitempty"`
	Parallelism int        `json:"parallelism,omitempty"`
	Destroy     bool       `json:"destroy,omitempty"`
	Steps       []PlanStep `json:"steps"`
}

PlanFile is the on-disk form of a plan. Steps in the file mirror the in-memory Plan.Steps. Inputs holds the validated root inputs the plan was computed against, so apply can seed them into its eval scope without reading the stack file again.

func DecodePlan

func DecodePlan(b []byte) (*PlanFile, error)

DecodePlan parses a plan file from JSON bytes. JSON has no native distinction between int and float, so the decoder reads numbers as json.Number and coerceNumbers walks the result, restoring int64 for values whose text has no decimal point or exponent and float64 for everything else. This preserves the typing that Plan emitted: a re-evaluation at apply time gets int64 back where the schema said integer.

func OpenPlan

func OpenPlan(
	b []byte,
	resolveEnc func(*StateRef) (encrypt.Encrypter, error),
) (*PlanFile, error)

OpenPlan opens a sealed plan envelope and returns its inner PlanFile. resolveEnc builds an encrypter from the envelope's ref; pass a function that consults the caller's resolver chain. resolveEnc receives nil when the envelope has no encrypter ref. A plan file may name its own encrypter because the decrypted content is exactly what apply executes, so the ref grants the file no authority its content does not already have.

type PlanStep

type PlanStep struct {
	Address  string          `json:"address"`
	Kind     NodeKind        `json:"node-kind"`
	Selector *state.Selector `json:"selector,omitempty"`

	// Composite marks a step whose apply finalizes a composite call
	// site (a boundary) rather than a primitive leaf. A boundary's Kind
	// is its own resource/data/action kind, so the runtime cannot
	// tell it from a leaf by Kind alone; Node.IsComposite has source
	// body metadata, but a step has no body, so the bit is stored
	// explicitly in the plan file.
	Composite bool `json:"composite,omitempty"`

	Decision         Decision            `json:"decision"`
	Inputs           map[string]any      `json:"inputs,omitempty"`
	UnresolvedInputs map[string][]string `json:"unresolved-inputs,omitempty"`

	// DeferredConfig names the library-config node whose pending evaluation
	// kept this node's read from running at plan. The stored state is taken as
	// current for the decision; apply and the next plan see real values.
	DeferredConfig string `json:"deferred-config,omitempty"`

	// PriorInputs is the body the last apply evaluated, recorded so the plan
	// can show a changed field as `old -> new` rather than the new value
	// alone. Nil for a create, where there is no prior to compare against.
	PriorInputs map[string]any `json:"prior-inputs,omitempty"`

	PriorSelector   *state.Selector `json:"prior-selector,omitempty"`
	PriorOutputs    map[string]any  `json:"prior-outputs,omitempty"`
	ObservedOutputs map[string]any  `json:"observed-outputs,omitempty"`
	TriggerHash     string          `json:"trigger-hash,omitempty"`

	// ReplaceTriggers names the replace-forcing fields whose value changed,
	// the reason a step replaces rather than updates. The renderer tags each
	// with `(forces replacement)`. Empty unless Decision is replace.
	ReplaceTriggers []string `json:"replace-triggers,omitempty"`

	// DependsOn carries a destroy step's recorded dependencies from
	// prior state. Apply reverses these edges so a resource is deleted
	// before the resources it depended on.
	DependsOn []string `json:"depends-on,omitempty"`

	// AlreadyGone marks a destroy step whose resource was already
	// absent when the plan read it. Apply drops the entry from state
	// without calling Delete.
	AlreadyGone bool `json:"already-gone,omitempty"`

	// SensitiveInputs names the input fields whose value expression
	// reads from any sensitive source. Renderers replace the value
	// with a placeholder rather than printing the secret.
	SensitiveInputs []string `json:"sensitive-inputs,omitempty"`

	// SensitiveOutputs names the output fields of this step that are
	// sensitive. For a primitive resource/action it comes from the
	// library schema's tagged fields; for a composite call site it
	// comes from the composite type's `@sensitive` markers and from
	// propagation through its body.
	SensitiveOutputs []string `json:"sensitive-outputs,omitempty"`
	// contains filtered or unexported fields
}

PlanStep records one node's planned action. For resources, Inputs is the evaluated body. PriorOutputs is what state holds (nil for create or destroy of a resource that is not found). ObservedOutputs is what Resource.Read returned at plan time; it differs from PriorOutputs when the resource has drifted out of band. For actions, TriggerHash is the hash that determines whether to rerun or skip.

UnresolvedInputs names the input fields whose plan-time evaluation hit a forward reference (an upstream node with no prior state). Each entry maps the field name to the source-side dot paths the body reads from. Apply re-evaluates these against the live scope.

func (*PlanStep) Drift

func (s *PlanStep) Drift() bool

Drift reports whether the resource's observed outputs differ from the outputs in prior state. False for steps with no prior or no observation (Create, Destroy, Gone).

func (*PlanStep) Gone

func (s *PlanStep) Gone() bool

Gone reports whether a resource with prior state was missing in the cloud at plan time. Encoded as Create with PriorOutputs set.

type Prior

type Prior[In, Out any] struct {
	Inputs   In
	Outputs  Out
	Observed Out
}

Prior is everything known before Update acts: the inputs the body evaluated to on the last apply, the outputs the resource returned then, and the reality a plan-time Read last saw. Compare current inputs against Inputs with Changed to decide what to reconcile, read Outputs for the prior handle (an id, an arn) the update acts against, and read Observed to patch from current reality rather than the recorded result when the two have drifted apart.

Inputs is the input struct by value, so it is never nil and needs no guard; a prior whose recorded inputs no longer decode into the current struct degrades to the zero value, which reads as "every field changed". Outputs and Observed keep the same pointer type the rest of the contract uses, so a resource with no recorded result or no plan-time read still passes nil.

Observed is plan-time, not apply-time: apply does not re-Read before Update, so between plan and apply reality can move further. A resource that needs apply-time truth must Read itself.

type RefMatch

type RefMatch struct {
	Address  string
	Segments int
}

func RefMatchInScope

func RefMatchInScope(
	p *lang.DotPath,
	nodes map[string]*Node,
	scope string,
) (RefMatch, bool)

type RefreshResult

type RefreshResult struct {
	WrittenRev string
	Refreshed  int
	Dropped    int
}

RefreshResult reports what Refresh did. Refreshed counts leaf entries whose outputs were updated to match what was observed; Dropped counts leaves whose Read returned ErrNotFound and were removed from state.

type ResourceRegistration

type ResourceRegistration interface {
	SchemaVersion() int
	Migrate(oldVersion int, prior MigrationState) (MigrationState, error)
	NewReceiver() any
	Create(ctx context.Context, receiver, cfg any) (any, error)
	Read(ctx context.Context, receiver, cfg, prior any) (any, error)
	Update(ctx context.Context, receiver, cfg, priorInputs, priorOutputs, observed any) (any, error)
	Delete(ctx context.Context, receiver, cfg, prior any) error
	ReplaceFields(receiver any) []string
	OutputType() reflect.Type
}

ResourceRegistration is the type-erased registration the runtime's resource map holds. A library author produces one via MakeResource; the runtime calls the methods on it to dispatch CRUD work without caring about the typed Out parameter.

func MakeResource

func MakeResource[T, Out, Config any, PT resourcePtr[T, Out, Config]]() ResourceRegistration

MakeResource produces a ResourceRegistration that wraps a TypedResource[Out, Config] implemented by *T. Use as `runtime.MakeResource[Vpc, *VpcOutput, any]()`. Each receiver is zero-constructed via new(T) when the runtime asks for one.

func MakeResourceWith

func MakeResourceWith[T, Out, Config any, PT resourcePtr[T, Out, Config]](
	construct func() *T,
) ResourceRegistration

MakeResourceWith is the variant of MakeResource for callers that need each receiver to capture external state. The constructor runs once per instance the runtime needs; Decode then fills it from the inputs.

type StateRef

type StateRef = state.Ref

StateRef is an alias for state.Ref, the resolver reference that both the plan envelope and the state-snapshot envelope use to name a backend or encrypter. The type lives in pkg/sdk/state so a state backend can use it without importing runtime; the alias keeps the runtime spelling for the plan body and the resolver code.

type StepNode added in v0.7.0

type StepNode struct {
	Address   string   `json:"address"`
	Kind      NodeKind `json:"node-kind"`
	Composite bool     `json:"composite,omitempty"`
	Decision  Decision `json:"decision"`
	DependsOn []string `json:"depends-on,omitempty"`
}

StepNode is one plan step in the instance-form dependency graph PlanGraph returns: the step's identity plus the addresses it waits for under apply's scheduler.

func PlanGraph added in v0.7.0

func PlanGraph(pf *PlanFile, dag *DAG) []StepNode

PlanGraph returns the dependency graph apply schedules pf with: one node per plan step, in plan order, each listing the instance-form addresses it waits for. The edges are the scheduler's own, so a destroy step already waits on the reversal of its recorded dependencies. Each node's DependsOn is sorted.

type TriggerDecision

type TriggerDecision struct {
	Hash        string
	AlwaysRerun bool
	HasExplicit bool
}

TriggerDecision is what the runtime computes for an action node before dispatching it: a hash to compare against state and a flag for the always-rerun escape hatch.

func ComputeTrigger

func ComputeTrigger(n *Node, inputs map[string]any, ec *EvalContext) (TriggerDecision, error)

ComputeTrigger evaluates the action node's `@trigger` meta key (if any) and returns the resulting decision. Without `@trigger`, the hash is taken over the action's selector and evaluated inputs so any visible change to the body causes a rerun. With `@trigger: 'always'`, AlwaysRerun is true and the hash is empty.

type TypeSchema

type TypeSchema struct {
	Inputs           map[string]typecheck.Type
	Outputs          map[string]typecheck.Type
	SensitiveInputs  []string
	SensitiveOutputs []string

	// Constraints holds the type's cross-field constraints, derived from
	// its Constraints method at compile time, in the embeddable string
	// form. A check parses them with lang.ParseSpecs and runs them through
	// lang.CheckConstraintEntries, the same path UB constraints take.
	Constraints []lang.ConstraintSpec

	// Defaults holds the type's declared input defaults, derived from
	// its Defaults method at compile time. A field with a Value default
	// is filled in when a body leaves it out; an Optional marker only
	// declares that absence is fine. Any other input is required.
	Defaults []lang.DefaultSpec
}

TypeSchema describes the input and output fields of one resource, data source, or action. Each map keys a kebab-case field name (the form factory source uses) to that field's semantic Type. Inputs lists the receiver type's exported fields; Outputs lists the output struct's. The walker that builds this schema (goschema) recursively expands named struct types so nested object fields can be type-checked too.

SensitiveInputs and SensitiveOutputs hold the kebab-case names of fields a library marked sensitive via a `ub:",sensitive"` struct tag. Both are top-level only; sensitivity does not descend into nested object fields.

type TypedAction

type TypedAction[Out, Config any] interface {
	Run(ctx context.Context, config Config) (Out, error)
}

TypedAction is the typed contract for actions. Out names the action's output struct.

type TypedDataSource

type TypedDataSource[Out, Config any] interface {
	Read(ctx context.Context, config Config) (Out, error)
}

TypedDataSource is the typed contract for read-only data sources.

type TypedResource

type TypedResource[In, Out, Config any] interface {
	SchemaVersion() int
	Create(ctx context.Context, config Config) (Out, error)
	Read(ctx context.Context, config Config, prior Out) (Out, error)
	Update(ctx context.Context, config Config, prior Prior[In, Out]) (Out, error)
	Delete(ctx context.Context, config Config, prior Out) error
	ReplaceFields() []string
}

TypedResource is the typed contract a library author implements for one primitive resource type. In names the input struct, which is the method receiver; Out names the output struct, usually a pointer (e.g. *VpcOutput) so a "no prior state" call passes nil. Config names the decoded library config type. Update receives a Prior bundling the last apply's inputs and outputs.

Jump to

Keyboard shortcuts

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