passage

package
v0.4.2 Latest Latest
Warning

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

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

Documentation

Overview

Package passage runs the steps that move a Bundle through a Gate.

A step is invoked repeatedly rather than blocking: it reports what it sees and returns, and the engine calls it again later if it is still waiting. That keeps long waits (a Flux reconciliation, a pull request review) out of goroutines and in the Passage's persisted status, so a controller restart resumes rather than restarts.

Index

Constants

View Source
const (
	// ReasonUnregisteredStep is set when a Passage names a step that does not exist.
	ReasonUnregisteredStep = "UnregisteredStep"
	// ReasonBadExpression is set when a step's `with:` or `if:` cannot be
	// evaluated. A malformed expression will not become valid by retrying.
	ReasonBadExpression = "BadExpression"
)
View Source
const ActorController = "controller"

ActorController is what a crossing the controller started records as having asked for it.

Worth distinguishing from a person: an automatic crossing has no human deployer, and reporting a robot as one to a system evaluating segregation of duties would make four-eyes pass with three roles and two humans.

View Source
const ReasonUnknown = "Unknown"

ReasonUnknown is used when a step fails without naming a reason.

Variables

This section is empty.

Functions

func CheckConfig

func CheckConfig[T any](raw json.RawMessage) (T, error)

CheckConfig decodes a `with:` block strictly, and is how a step validates one without running.

Strict because the default is silent: json.Unmarshal drops fields it does not recognise, so `mesage:` for `message:` decodes cleanly into an empty struct and the step fails later complaining the message is missing — pointing at the field that is there rather than the one that is misspelt. Refusing the unknown field names the actual mistake.

func DecodeConfig

func DecodeConfig[T any](sc *StepContext) (T, error)

DecodeConfig unmarshals a step's config into a typed struct.

func DialFides

func DialFides(
	ctx context.Context,
	c client.Client,
	namespace, defaultServer string,
	evidence *v1alpha1.EvidenceConfig,
	dial func(fides.Config) (*fides.Client, error),
) (*fides.Client, error)

DialFides resolves the Fides server a Gate names and connects to it.

Shared with the evidence-gate step, which resolves exactly the same three things — server, credentials Secret, token — and had the only copy until the controller needed to attest. Two copies of "where is Fides" is how a Gate ends up checked against one server and recorded on another.

dial is injectable so tests can point at a fake Fides; nil means the real one.

func Fail

func Fail(reason, format string, args ...any) error

Fail builds a retryable step failure.

func FailTerminal

func FailTerminal(reason, format string, args ...any) error

FailTerminal builds a step failure that must not be retried.

func ImageDigests

func ImageDigests(bundle *v1alpha1.Bundle) []string

ImageDigests are the Bundle's pinned image digests, in spec order.

func IsTerminal

func IsTerminal(err error) bool

IsTerminal reports whether an error should stop retrying.

func ReasonOf

func ReasonOf(err error) string

ReasonOf extracts a failure's reason code, or ReasonUnknown.

func Terminalf

func Terminalf(format string, args ...any) error

Terminalf builds a terminal failure without a reason code.

Kept for the cases where no useful code exists, but prefer FailTerminal: a failure with no reason is one nothing downstream can act on.

Types

type ConfigChecker

type ConfigChecker interface {
	CheckConfig(raw json.RawMessage) error
}

Names lists the registered steps, sorted. ConfigChecker is implemented by a step that can judge its `with:` block without running.

Optional: a step that does not implement it is simply not checked, which is better than a registry that refuses to hold steps nobody has got to yet.

type Engine

type Engine struct {
	Registry *Registry
	// Now is the clock, injectable for tests.
	Now func() time.Time
}

Engine advances a Passage by running its steps in order.

It is deliberately not a long-running loop. Each call to Advance runs as far as it can and returns; the controller persists the resulting status and calls again. All progress therefore lives in the Passage object rather than in process memory, so a restart resumes mid-Passage instead of starting over.

func (*Engine) Advance

func (e *Engine) Advance(
	ctx context.Context, p *v1alpha1.Passage, bundle *v1alpha1.Bundle, workDir string,
) Outcome

Advance runs the Passage from wherever it left off.

workDir is the scratch directory shared by this Passage's steps; the caller owns its lifecycle. Passed in rather than resolved through a callback, because the caller already knows it and a callback would only add a way to forget to set it.

type Outcome

type Outcome struct {
	// Status is the Passage's updated status. The caller persists it.
	Status v1alpha1.PassageStatus
	// RequeueAfter is how long to wait before calling Advance again. Zero means
	// the Passage is finished.
	RequeueAfter time.Duration
	// Watch accumulates the health checks emitted by steps, for the Gate to
	// adopt once the Passage succeeds.
	Watch []v1alpha1.HealthCheck
}

Outcome is what Advance concluded.

type Reconciler

type Reconciler struct {
	client.Client
	Engine   *Engine
	Recorder events.EventRecorder
	// WorkRoot is the base directory for Passage scratch space. Empty means a
	// directory under the system temp dir.
	WorkRoot string
	// Now is the clock, injectable for tests.
	Now func() time.Time
	// FidesServer is the controller's --fides-server, used to record a finished
	// crossing when a Gate does not name one of its own.
	FidesServer string
	// DialFides is injectable so tests can point at a fake Fides. Nil is the
	// real client.
	DialFides func(fides.Config) (*fides.Client, error)
}

Reconciler drives a Passage's steps to completion.

It is a thin loop around Engine.Advance: the engine decides what happens, the controller persists the result and comes back when asked. Keeping the decision-making out of the controller is what lets the engine be tested exhaustively without a cluster.

func (*Reconciler) Reconcile

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)

Reconcile advances one Passage.

func (*Reconciler) SetupWithManager

func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error

SetupWithManager registers the controller.

func (*Reconciler) WorkDir

func (r *Reconciler) WorkDir(p *v1alpha1.Passage) string

WorkDir is the scratch directory shared by one Passage's steps — where one step clones a repository and a later one commits it.

Keyed by UID rather than name so a recreated Passage never inherits a previous one's leftovers.

It is deliberately **local and disposable**. A controller restart loses it, and that is acceptable: D5 already requires steps to be re-entrant, so a step must cope with an empty work dir — `git-clone` clones again. Making it durable would mean a PersistentVolume per Passage, which is a great deal of machinery to avoid re-running a clone.

type Registry

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

Registry holds the available step Runners.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry.

func (*Registry) Get

func (r *Registry) Get(name string) (Runner, bool)

Get returns the Runner registered under name.

func (*Registry) MustRegister

func (r *Registry) MustRegister(run Runner)

MustRegister is Register, panicking on error. For startup wiring.

func (*Registry) Names

func (r *Registry) Names() []string

func (*Registry) Register

func (r *Registry) Register(run Runner) error

Register adds a Runner. A duplicate name is an error, not a silent overwrite — two runners claiming one name would fail confusingly at runtime.

func (*Registry) Validate

func (r *Registry) Validate(steps []v1alpha1.Step) []StepProblem

Validate reports everything wrong with a step list.

Every problem, not the first: an author fixing a Gate wants the whole list, and returning one at a time turns a single mistake into several apply-and- wait cycles.

type Runner

type Runner interface {
	// Name is the value used in `steps[].uses`.
	Name() string
	// Run performs one invocation. It must return promptly: to wait, return
	// StepRunning with a RetryAfter rather than sleeping.
	Run(ctx context.Context, sc *StepContext) (StepResult, error)
}

Runner implements one kind of step.

type StepContext

type StepContext struct {
	// Namespace is where the Gate and Bundle live.
	Namespace string
	// Gate being crossed.
	Gate string
	// Passage performing the crossing.
	Passage string
	// Bundle being moved. Steps read artifact versions from here.
	Bundle *v1alpha1.Bundle
	// Actor initiated the Passage. ActorController when nobody did.
	Actor string
	// WorkDir is a scratch directory shared by every step in this Passage —
	// where one step clones a repo and a later one commits it.
	WorkDir string
	// Config is this step's `with:` block.
	Config json.RawMessage
	// Outputs holds prior steps' outputs, keyed by their `as:` alias.
	Outputs map[string]map[string]any
	// Attempt is how many times this step has already run, starting at 0.
	Attempt int32
	// Failed reports that an earlier step has already failed the Passage.
	//
	// A step only sees this if it asked to run anyway (`if: failed` or
	// `if: always`) — otherwise it is skipped and never invoked. It is here so
	// a step reporting the outcome can report the real one: the engine knows,
	// and making the user restate it in configuration would be a second place
	// for the truth to live (D46).
	Failed bool
	// Traceparent is the W3C trace context for this crossing, or empty when
	// tracing is off. Steps that write something durable should carry it, so a
	// promotion can be correlated end to end (D42).
	Traceparent string
	// StartedAt is when the Passage began.
	//
	// Steps that produce content should derive timestamps from this rather than
	// the wall clock, so re-running a Passage yields byte-identical output. A
	// commit stamped with time.Now() gets a new SHA on every attempt, which
	// turns a harmless retry into a second commit on the branch.
	StartedAt time.Time
}

StepContext is everything a step is given.

type StepError

type StepError struct {
	// Reason is a stable, machine-readable code in PascalCase, following the
	// convention Kubernetes uses for condition reasons — GitAuthFailed,
	// FluxStalled, InvalidConfig. Stable is the operative word: it is a
	// contract, so renaming one breaks whatever was matching on it.
	//
	// Deliberately not a closed enum. Each step names its own failures; a
	// central registry would be a bottleneck and a merge conflict, and steps
	// live in different packages.
	Reason string
	// Terminal marks a failure not worth retrying — bad configuration, a
	// missing reference, anything that will fail identically next time.
	Terminal bool
	Err      error
}

StepError is a step failure with a machine-readable reason.

The reason is what makes a failure something other than prose. `hecate diagnose`, a dashboard counting failure classes, and anything reasoning over a stuck Passage all need to distinguish "the git host rejected our credentials" from "Flux gave up" without parsing English.

Structured detail belongs in the step's Output, not here: the engine already records Output on failure, and a second place to put facts would only invite the two to disagree.

func (*StepError) Error

func (e *StepError) Error() string

func (*StepError) Unwrap

func (e *StepError) Unwrap() error

type StepProblem

type StepProblem struct {
	// Index is the step's position, which is how an author finds it: steps have
	// no required name, so "step 3" is often the only way to point at one.
	Index int
	Uses  string
	Err   error
}

StepProblem is one thing wrong with a Gate's step list.

func (StepProblem) Error

func (p StepProblem) Error() string

type StepResult

type StepResult struct {
	// Phase is the outcome. StepRunning means "call me again".
	Phase v1alpha1.StepPhase
	// Message explains the phase, and is shown in the UI and CLI while waiting.
	Message string
	// Output is exposed to later steps under this step's alias.
	Output map[string]any
	// Watch are health checks this step hands to the Gate, so the Gate keeps
	// monitoring what the step waited for. Without this a Gate goes blind the
	// moment its Passage finishes.
	Watch []v1alpha1.HealthCheck
	// Evidence is the compliance record this step produced or consulted, copied
	// onto the Passage's status.
	//
	// Carried here rather than dug out of Output by the controller, for the same
	// reason as Watch: a step knows what it recorded, and a controller matching
	// on well-known output keys would break the moment a step chose a different
	// name for them.
	Evidence *v1alpha1.EvidenceRef
	// RetryAfter suggests how long to wait before the next invocation.
	RetryAfter time.Duration
}

StepResult is the outcome of one invocation.

Directories

Path Synopsis
Package steps holds Hecate's built-in Passage steps.
Package steps holds Hecate's built-in Passage steps.

Jump to

Keyboard shortcuts

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