iac

package
v0.8.6 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package iac turns multi-document YAML resource files into a plan of changes against a control plane and applies it through the ordinary REST API, so per-resource authorization applies to every item.

Index

Constants

View Source
const (
	StatusApplied = "applied"
	StatusFailed  = "failed"
	StatusDenied  = "denied"
	StatusSkipped = "skipped"
	StatusNoop    = "noop"
)

Item outcomes.

View Source
const (
	OpAdd    = "add"
	OpChange = "change"
	OpRemove = "remove"
	OpKeep   = "keep"
)

Field operations shown in a plan.

View Source
const DefaultDriftPolicy = DriftWarn

DefaultDriftPolicy is warn so hand edits keep working until an owner opts in.

View Source
const (
	LabelManagedBy = "managed-by"
)

Labels with a meaning of their own.

View Source
const SchemaVersion = 1

SchemaVersion is the only resource document version this package accepts.

Variables

View Source
var (
	ErrStackPinned     = errors.New("stack is pinned; only a rollback to the pinned revision may apply")
	ErrStackLocked     = errors.New("stack is locked by another apply")
	ErrLedgerConflict  = errors.New("resource is owned by another stack")
	ErrPruneNotAllowed = errors.New("prune needs both the manifest request and the stack allow_prune setting")
)

Sentinel errors the store and API map to 409 or 422 responses.

View Source
var ErrPlanChanged = errors.New("the plan changed since it was reviewed; plan again")

ErrPlanChanged means live state moved between the plan a user reviewed and the apply that followed.

Kinds lists every kind in the order changes are applied.

Functions

func Build

func Build(docs []*Document, opts Options) ([]*Resource, []Issue)

Build validates every document (schema first, then semantics) and turns them into canonical resources. It returns no resources when any issue exists, so nothing is planned from a partly valid input.

func IsDenied

func IsDenied(err error) bool

IsDenied reports whether err is an authorization failure.

func IsFeatureOff

func IsFeatureOff(err error) bool

IsFeatureOff reports whether err is the 404 a gated endpoint answers while its beta feature is disabled.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a 404.

func IsUnavailable

func IsUnavailable(err error) bool

IsUnavailable reports whether err says the feature is not configured.

func LeakedSecret

func LeakedSecret(docs []byte, secrets map[string]string) (string, bool)

LeakedSecret returns the name of a secret whose value appears in docs, so a revision can be refused before it is stored. Empty values are skipped.

func LedgerConflicts

func LedgerConflicts(p Plan) []string

LedgerConflicts returns the keys a plan refused for ownership reasons.

func MassPruneBlocked

func MassPruneBlocked(p Plan) bool

MassPruneBlocked reports whether the guard held back any delete.

func ParseDocuments

func ParseDocuments(sources []Source) ([]*Document, []Issue)

ParseDocuments splits sources into documents and decodes each envelope. It reports YAML syntax errors and missing kind or name, nothing deeper; Validate does the schema and semantic checks.

func SchemaJSON

func SchemaJSON() ([]byte, error)

SchemaJSON returns the published JSON Schema: the resource envelope schema with the app.yaml schema definitions merged in unchanged, except that an env value may also be a { secretRef } or a { from: { database, field } } object.

func ValidateStackName

func ValidateStackName(name string) error

ValidateStackName checks the stack name shape.

func WithLedger

func WithLedger(ctx context.Context, l *Ledger) context.Context

WithLedger makes Load and the planner enforce l. It is a server-side value; nothing a client sends can set it.

func WithStackScope

func WithStackScope(ctx context.Context, s StackScope) context.Context

WithStackScope returns ctx carrying the stack apply scope.

Types

type Action

type Action string

Action is what a plan item does.

const (
	ActionCreate Action = "create"
	ActionUpdate Action = "update"
	ActionDelete Action = "delete"
	ActionNoop   Action = "noop"
	ActionError  Action = "error"
)

Plan actions.

type ApplyResult

type ApplyResult struct {
	Plan    Plan         `json:"plan"`
	Results []ItemResult `json:"results"`
	Applied int          `json:"applied"`
	Failed  int          `json:"failed"`
	Skipped int          `json:"skipped"`
}

ApplyResult is the outcome of an apply.

func Apply

func Apply(ctx context.Context, d Doer, res []*Resource, opts Options, expectedHash string) (ApplyResult, error)

Apply plans and executes resources through doer. Validation and reading happen before any write; a plan holding an error item applies nothing unless ContinueOnError is set. Items run in dependency order and each goes through the ordinary endpoint, so a denied write is reported on its own item and the rest proceed only when ContinueOnError is set.

func (ApplyResult) OK

func (r ApplyResult) OK() bool

OK reports whether every item that had to change did.

type Change

type Change struct {
	Kind     Kind          `json:"kind"`
	Name     string        `json:"name"`
	Scope    string        `json:"scope,omitempty"`
	Action   Action        `json:"action"`
	Fields   []FieldChange `json:"fields,omitempty"`
	Kept     []FieldChange `json:"kept,omitempty"`
	Warnings []string      `json:"warnings,omitempty"`
	Reason   string        `json:"reason,omitempty"`
	// DependsOn lists the resources this item reads from, applied first.
	DependsOn []string `json:"dependsOn,omitempty"`
	Denied    bool     `json:"denied,omitempty"`
	File      string   `json:"file,omitempty"`
	Line      int      `json:"line,omitempty"`
}

Change is one line of a plan.

func (Change) Key

func (c Change) Key() string

Key is the change's stable identity.

type Document

type Document struct {
	File     string
	Line     int
	Kind     Kind
	Name     string
	Labels   map[string]string
	Root     *yaml.Node
	SpecNode *yaml.Node
}

Document is one parsed YAML document with its envelope decoded.

func FilterProject

func FilterProject(docs []*Document, project string) []*Document

FilterProject keeps the documents that belong to one project: the project itself, its environments, apps and databases, the domains, load balancers, pipelines and alert rules of those apps, and the tags those apps use.

type Doer

type Doer interface {
	Do(ctx context.Context, method, path string, body, out any) error
}

Doer performs one JSON request against the control plane REST API. The CLI implements it over HTTP with the caller's token; the server implements it in process with the caller's own credentials, so per resource authorization applies to every call either way.

type DriftPolicy

type DriftPolicy string

DriftPolicy says what happens when live state diverges from the stack.

const (
	DriftWarn DriftPolicy = "warn"
	DriftLock DriftPolicy = "lock"
)

Drift policies; revert is deliberately not offered.

func ParseDriftPolicy

func ParseDriftPolicy(s string) (DriftPolicy, error)

ParseDriftPolicy maps an empty value to the default and rejects unknown ones.

type ExportFile

type ExportFile struct {
	Name    string `json:"name"`
	Kind    Kind   `json:"kind"`
	Content string `json:"content"`
}

ExportFile is one exported document.

type ExportOptions

type ExportOptions struct {
	Project string
	App     string
	// IncludeEnvValues writes literal env values; when false every value
	// becomes a ${{ env.NAME }} placeholder.
	IncludeEnvValues bool
}

ExportOptions select what to export.

type ExportResult

type ExportResult struct {
	Files    []ExportFile `json:"files"`
	Warnings []string     `json:"warnings,omitempty"`
}

ExportResult is the exported documents in stable order plus warnings for live settings the documents cannot express.

func Export

func Export(ctx context.Context, d Doer, opts ExportOptions) (ExportResult, error)

Export reads live state and renders it as documents. Output is stable: no timestamps or generated ids, sorted keys and a fixed document order.

func (ExportResult) Join

func (r ExportResult) Join() string

Join concatenates exported files into one multi-document stream.

type FieldChange

type FieldChange struct {
	Path string `json:"path"`
	Op   string `json:"op"`
	Old  string `json:"old,omitempty"`
	New  string `json:"new,omitempty"`
}

FieldChange is one leaf difference. Values under env are never shown.

type GitMeta

type GitMeta struct {
	Repo     string `json:"git_repo,omitempty"`
	Ref      string `json:"git_ref,omitempty"`
	SHA      string `json:"git_sha,omitempty"`
	CIRunURL string `json:"ci_run_url,omitempty"`
}

GitMeta links a revision to the commit and CI run that produced it.

type Issue

type Issue struct {
	File    string `json:"file,omitempty"`
	Path    string `json:"path,omitempty"`
	Line    int    `json:"line"`
	Message string `json:"message"`
}

Issue is one validation problem, located by file, line and dotted path.

func (Issue) String

func (i Issue) String() string

type ItemResult

type ItemResult struct {
	Change
	Status string `json:"status"`
	Error  string `json:"error,omitempty"`
}

ItemResult is one plan item's outcome.

type Kind

type Kind string

Kind names a resource document type.

const (
	KindProject      Kind = "Project"
	KindEnvironment  Kind = "Environment"
	KindTag          Kind = "Tag"
	KindDatabase     Kind = "Database"
	KindApp          Kind = "App"
	KindDomain       Kind = "Domain"
	KindLoadBalancer Kind = "LoadBalancer"
	KindPipeline     Kind = "Pipeline"
	KindAlertRule    Kind = "AlertRule"
)

Resource kinds, in dependency order.

const KindStack Kind = "Stack"

KindStack is the optional stack document. It is not in Kinds because it configures the apply instead of describing a resource.

type Ledger

type Ledger struct {
	StackID   string
	StackName string
	// ScopeProject is the stack's own project; an admin bound it to the stack,
	// so declaring it claims it without an explicit adopt.
	ScopeProject string
	// Owned maps resource keys this stack owns to their entries.
	Owned map[string]LedgerEntry
	// Foreign maps keys owned by some other stack to that stack's id.
	Foreign map[string]string
	// Adopt lets the stack claim existing resources no stack owns.
	Adopt bool
	// AllowMassPrune confirms a prune above the guard thresholds.
	AllowMassPrune bool
	Guard          PruneGuard
}

Ledger is the stack ownership view a plan is checked against (ADR 042). With a ledger, ownership and prune come from it alone, never from tags.

func NewLedger

func NewLedger(stackID, stackName, scopeProject string, owned []LedgerEntry) *Ledger

NewLedger builds the view for one stack from its own ledger rows.

type LedgerEntry

type LedgerEntry struct {
	StackID       string `json:"stack_id"`
	Kind          Kind   `json:"kind"`
	Key           string `json:"key"`
	LiveID        string `json:"live_id"`
	SinceRevision int    `json:"since_revision"`
}

LedgerEntry records the single stack that owns a resource. Key is the resourceKey (kind/scope/name), unique per kind across all stacks.

type LedgerRef

type LedgerRef struct {
	Kind Kind
	Key  string
}

LedgerRef is one (kind, key) whose owner a caller must look up.

func LedgerRefs

func LedgerRefs(res []*Resource) []LedgerRef

LedgerRefs lists every key the plan checks ownership of: each resource plus the parent app of Domain, LoadBalancer, Pipeline and AlertRule documents.

type LedgerUpdate

type LedgerUpdate struct {
	Claim   []LedgerEntry
	Release []LedgerEntry
}

LedgerUpdate is what one apply changes in the ledger.

func LedgerChanges

func LedgerChanges(stackID string, rev int, res []*Resource, out ApplyResult) LedgerUpdate

LedgerChanges derives claims and releases from an apply. Only declared resources are claimed, and nothing is claimed from a plan refused for errors.

type Options

type Options struct {
	// Vars resolve ${{ env.NAME }} placeholders.
	Vars map[string]string
	// Source names this apply source; apps it creates carry a managed-by
	// tag and only those are ever pruned.
	Source string
	// Secrets maps "NAME" or "app/NAME" to a value to store at apply time.
	// Values never appear in a plan, result or export.
	Secrets map[string]string
	// Project limits prune to managed apps of this project, matching the
	// project filter applied to the files.
	Project string
	// Prune deletes managed resources absent from the files.
	Prune bool
	// NoDeploy skips the restart that applies an env change to a running app.
	NoDeploy bool
	// ContinueOnError keeps applying after an item fails.
	ContinueOnError bool
}

Options carry apply-time inputs that never live in a resource file.

type Plan

type Plan struct {
	Source  string   `json:"source,omitempty"`
	Prune   bool     `json:"prune,omitempty"`
	Changes []Change `json:"changes"`
	Summary Summary  `json:"summary"`
	Hash    string   `json:"hash"`
}

Plan is the ordered set of changes needed to reach the desired state.

func PlanFor

func PlanFor(st *State, res []*Resource, opts Options) Plan

PlanFor computes the plan for resources against live state.

func (Plan) Pending

func (p Plan) Pending() bool

Pending reports whether applying would change anything.

type PruneGuard

type PruneGuard struct {
	MaxPercent int
	MaxCount   int
}

PruneGuard bounds how much of a stack one prune may delete without an explicit confirmation.

func PruneGuardFromEnv

func PruneGuardFromEnv() PruneGuard

PruneGuardFromEnv reads APP_STACK_PRUNE_MAX_PERCENT and APP_STACK_PRUNE_MAX_COUNT; zero is valid and means any prune needs the flag.

type Resource

type Resource struct {
	Kind   Kind
	Name   string
	Scope  string
	Doc    *Document
	Fields map[string]any
	// SecretRefs are app env names backed by the secrets store.
	SecretRefs []string
	Warnings   []string
}

Resource is one desired object in canonical form, ready to compare with live state.

func (*Resource) Key

func (r *Resource) Key() string

Key is the stable identity used in plans: kind plus scope plus name.

type Revision

type Revision struct {
	StackID        string         `json:"stack_id"`
	Number         int            `json:"number"`
	Status         RevisionStatus `json:"status"`
	PlanHash       string         `json:"plan_hash"`
	PlanJSON       []byte         `json:"plan,omitempty"`
	RenderedDocs   []byte         `json:"rendered_docs,omitempty"`
	RollbackOf     int            `json:"rollback_of,omitempty"`
	IdempotencyKey string         `json:"idempotency_key,omitempty"`
	Actor          string         `json:"actor"`
	TokenID        string         `json:"token_id,omitempty"`
	Git            GitMeta        `json:"git"`
	CreatedAt      time.Time      `json:"created_at"`
	FinishedAt     *time.Time     `json:"finished_at,omitempty"`
	ResultJSON     []byte         `json:"result,omitempty"`
}

Revision is one numbered apply of a stack. RenderedDocs holds documents after env substitution only; secret values are never part of it.

func (*Revision) Validate

func (r *Revision) Validate() error

Validate checks a revision before it is first stored.

type RevisionStatus

type RevisionStatus string

RevisionStatus is the lifecycle of one stack apply.

const (
	RevisionPendingApproval RevisionStatus = "pending_approval"
	RevisionApplying        RevisionStatus = "applying"
	RevisionApplied         RevisionStatus = "applied"
	RevisionPartial         RevisionStatus = "partial"
	RevisionFailed          RevisionStatus = "failed"
	RevisionSuperseded      RevisionStatus = "superseded"
)

Revision statuses, in lifecycle order.

func (RevisionStatus) CanTransition

func (s RevisionStatus) CanTransition(next RevisionStatus) bool

CanTransition reports whether a revision may move from s to next.

func (RevisionStatus) Terminal

func (s RevisionStatus) Terminal() bool

Terminal statuses never change again, except applied becoming superseded.

func (RevisionStatus) Valid

func (s RevisionStatus) Valid() bool

Valid reports whether s is a known status.

type Source

type Source struct {
	Name string
	Data []byte
}

Source is one input file, or stdin, before parsing.

type Stack

type Stack struct {
	ID              string      `json:"id"`
	Name            string      `json:"name"`
	ProjectID       string      `json:"project_id"`
	EnvironmentID   string      `json:"environment_id,omitempty"`
	AllowPrune      bool        `json:"allow_prune"`
	DriftPolicy     DriftPolicy `json:"drift_policy"`
	PinnedRevision  int         `json:"pinned_revision,omitempty"` // 0 means not pinned
	RequireApproval bool        `json:"require_approval"`
	CreatedAt       time.Time   `json:"created_at"`
	CreatedBy       string      `json:"created_by"`
}

Stack is the server-side stack object. Settings here are admin-owned; a manifest can request but never grant them.

func (*Stack) CheckPin

func (s *Stack) CheckPin(rollbackOf int) error

CheckPin returns ErrStackPinned unless the apply is a rollback to the pin. rollbackOf is 0 for an ordinary apply.

func (*Stack) PruneAllowed

func (s *Stack) PruneAllowed(manifestPrune bool) error

PruneAllowed needs both sides so a manifest cannot grant itself prune.

func (*Stack) Validate

func (s *Stack) Validate() error

Validate checks the fields a store must never persist wrong.

type StackDeleteMode

type StackDeleteMode string

StackDeleteMode picks what deleting a stack does to its resources.

const (
	// StackForget releases the ledger and leaves every resource in place.
	StackForget StackDeleteMode = "forget"
	// StackDestroy deletes ledger apps, and databases only with delete_data; never from CI.
	StackDestroy StackDeleteMode = "destroy"
)

func ParseStackDeleteMode

func ParseStackDeleteMode(s string) (StackDeleteMode, error)

ParseStackDeleteMode defaults to forget, the mode that cannot lose data.

type StackLock

type StackLock struct {
	StackID   string    `json:"stack_id"`
	Holder    string    `json:"holder"`
	Revision  int       `json:"revision"`
	ExpiresAt time.Time `json:"expires_at"`
}

StackLock is the per-stack lease. Its length comes from APP_STACK_LOCK_TTL.

func (StackLock) Expired

func (l StackLock) Expired(now time.Time) bool

Expired reports whether another holder may take the lease over.

type StackScope

type StackScope struct {
	StackID   string
	StackName string
	Revision  int
}

StackScope is the apply context a stack handler attaches to loopback sub-requests. IAM trusts only this server-side value, never a header.

func StackScopeFrom

func StackScopeFrom(ctx context.Context) (StackScope, bool)

StackScopeFrom returns the stack apply scope, if the request runs inside one.

type StackSpec

type StackSpec struct {
	Project     string      `json:"project" yaml:"project"`
	Environment string      `json:"environment,omitempty" yaml:"environment,omitempty"`
	Prune       bool        `json:"prune,omitempty" yaml:"prune,omitempty"`
	Drift       DriftPolicy `json:"drift,omitempty" yaml:"drift,omitempty"`
}

StackSpec is the spec of a kind: Stack document.

func (*StackSpec) Validate

func (s *StackSpec) Validate() error

Validate checks the spec and fills the default drift policy.

type State

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

State is the live control plane data a plan needs, read through the API with the caller's own permissions. A read the caller may not perform is recorded in denied instead of failing the whole plan.

func Load

func Load(ctx context.Context, d Doer, res []*Resource, opts Options) (*State, error)

Load reads the live state the resources and prune scope need.

func (*State) ManagedApps

func (st *State) ManagedApps() []string

ManagedApps lists the apps this apply owns: the ledger's apps for a stack, otherwise those carrying the source's managed-by tag.

type StatusError

type StatusError struct {
	Status  int
	Message string
}

StatusError is a non-2xx API answer.

func (*StatusError) Error

func (e *StatusError) Error() string

type Summary

type Summary struct {
	Create int `json:"create"`
	Update int `json:"update"`
	Delete int `json:"delete"`
	Noop   int `json:"noop"`
	Error  int `json:"error"`
}

Summary counts a plan's items by action.

Jump to

Keyboard shortcuts

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