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
- func CheckConfig[T any](raw json.RawMessage) (T, error)
- func DecodeConfig[T any](sc *StepContext) (T, error)
- func DialFides(ctx context.Context, c client.Client, namespace, defaultServer string, ...) (*fides.Client, error)
- func Fail(reason, format string, args ...any) error
- func FailTerminal(reason, format string, args ...any) error
- func ImageDigests(bundle *v1alpha1.Bundle) []string
- func IsTerminal(err error) bool
- func ReasonOf(err error) string
- func Terminalf(format string, args ...any) error
- type ConfigChecker
- type Engine
- type Outcome
- type Reconciler
- type Registry
- type Runner
- type StepContext
- type StepError
- type StepProblem
- type StepResult
Constants ¶
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" )
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.
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 FailTerminal ¶
FailTerminal builds a step failure that must not be retried.
func ImageDigests ¶
ImageDigests are the Bundle's pinned image digests, in spec order.
func IsTerminal ¶
IsTerminal reports whether an error should stop retrying.
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) 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 (*Registry) MustRegister ¶
MustRegister is Register, panicking on error. For startup wiring.
func (*Registry) Register ¶
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.
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.