Documentation
¶
Overview ¶
Package actions implements wfctl's write commands (plan B4): the seven operations that change a fleet, and nothing else.
Every action is two-phase. Plan reads the cluster and returns what the write would do — the object, the fields, the field manager, the before/after values, and every warning the operator should weigh — with the write itself sealed in a closure. Nothing is written until the caller has printed the plan and obtained consent (confirm.go).
That split is what makes --dry-run honest: a dry run is the real code path with its last step withheld, not a second implementation that might describe a write nobody would perform.
The writes themselves are chosen for what they leave behind in managedFields, because managedFields is what the controller reads (DESIGN §3.5.3): a hand-pin is an SSA apply under the wfctl field manager precisely so pin.Hold sees it, and a Wavefront spec change is a merge patch precisely so a GitOps applier keeps owning the spec.
Index ¶
- Constants
- Variables
- func Audit(ctx context.Context, c client.Client, wf *wavefrontv1alpha1.Wavefront, ...) error
- func Note(identity, command string) string
- func Partial(note string, err error) string
- type Action
- type Advertisement
- type Confirmer
- type ForceAdmit
- type Pin
- type Plan
- type Release
- type Strip
- type WavefrontChange
Constants ¶
const ( ReasonSuspended = "Suspended" ReasonResumed = "Resumed" ReasonModeChanged = "ModeChanged" ReasonHandPinned = "HandPinned" ReasonPinReleased = "PinReleased" ReasonPinStripped = "PinStripped" ReasonForceAdmitted = "ForceAdmitted" )
Audit reasons, one per write command (plan B4). They share the vocabulary of the controller's own events so that `wfctl history` reads as one stream: the controller says what it did, wfctl says what a human did.
Variables ¶
var ErrNotATerminal = errors.New("refusing to prompt: stdin is not a terminal (use --yes)")
ErrNotATerminal is the refusal that keeps a write command safe in a pipeline.
A prompt written to a non-terminal is read by nobody and answered by whatever happens to be on stdin, so the only safe reading of "no --yes and no terminal" is that consent was never given (plan B4).
Functions ¶
func Audit ¶
func Audit( ctx context.Context, c client.Client, wf *wavefrontv1alpha1.Wavefront, reason, note string, at time.Time, ) error
Audit records one completed write as an event on the Wavefront.
Best-effort by contract (plan B4): an operator whose RBAC covers patching a GitRepository but not creating events has still performed the write, and failing the command afterwards would report a lie. The caller turns the error into a warning; nothing here decides that.
Events are at-least-once and expire with the apiserver's --event-ttl (DESIGN §4.2) — this is a convenience trail, not the durable ledger. The durable record of a pin change is the provenance annotations.
func Note ¶
Note renders an audit note: who ran what.
The note is the whole audit value of the event — the reason says what happened, the note says who asked for it and in exactly which words — so it is built in one place rather than at each of seven call sites.
Types ¶
type Advertisement ¶
type Advertisement struct {
// Lister lists advertised refs; nil means the production go-git lister,
// which fetches no objects and touches no disk (DESIGN §3.1.1).
Lister gitpoll.Lister
// Strategy maps the source's ref spec to a tracking ref and picks the
// candidate; nil means the v1 default, TrackRef. It must be the strategy
// the controller uses, or wfctl would verify against a different policy.
Strategy selection.Strategy
// Timeout bounds the listing; <= 0 means snapshot.DefaultPollTimeout.
Timeout time.Duration
}
Advertisement is the ref-listing seam shared by `pin --poll` (which verifies a SHA against it) and `force-admit` (which cannot work without one).
Listing from a CLI costs credentials — it reads the source's Secret and speaks to the git host from wherever the operator is sitting — so it happens only where a command's contract says it must.
type Confirmer ¶
type Confirmer struct {
// In is where the answer to the prompt is read from — the same stream
// IsTTY reports on.
In io.Reader
// Out is where the plan and the prompt are written.
Out io.Writer
// Yes applies without asking (--yes).
Yes bool
// DryRun prints the plan and stops (--dry-run).
DryRun bool
// IsTTY reports whether In is an interactive terminal; nil means "not one",
// which is the safe reading.
IsTTY func() bool
}
Confirmer prints a Plan and applies it if consent is given.
It is a struct rather than four arguments because the terminal, the clock and the answer stream are exactly the things a test has to replace: with In, Out and IsTTY injected, every branch of the consent rule is exercisable without a tty, a cluster, or a human.
type ForceAdmit ¶
type ForceAdmit struct {
Client client.Client
Source types.NamespacedName
// Wavefront is the fleet, read for the suspend warning.
Wavefront *wavefrontv1alpha1.Wavefront
// SHA overrides the advertised tracking-ref SHA.
SHA string
// Unverified accepts SHA without listing the remote's refs.
Unverified bool
// Now stamps the admitted-at annotation; nil means the wall clock.
Now func() time.Time
Advertisement
}
ForceAdmit admits one source past its gate, once.
It writes exactly the pin the controller would have written — same field manager, same three provenance annotations — for the SHA the source's tracking ref currently advertises. The controller then finds pin == observed and walks the node through Converging to Settled without an admission of its own, so no PinAdvanced event is emitted: the ForceAdmitted audit event is the record that a human, not the graph, let this through.
Writing the pin is the whole point, and is why "just unpin it" is not the same thing: an unpinned source is initial-pinned to its *artifact* SHA (DESIGN §3.5.4), which during an incident is usually the stale revision the operator is trying to get past.
type Pin ¶
type Pin struct {
Client client.Client
Source types.NamespacedName
// SHA is the commit to pin.
SHA string
// Force displaces a third-party owner of spec.ref.commit.
Force bool
// Unverified pins a SHA nobody checked against the remote.
Unverified bool
// Verify lists the source's refs and checks SHA against them (--poll).
Verify bool
Advertisement
}
Pin hand-pins one source: it sets spec.ref.commit under the wfctl field manager, which is precisely how the controller comes to report the source as held (DESIGN §3.5.3, docs/runbook.md "Hand-pin etiquette").
The write carries no provenance annotations. Those three annotations are the controller's record of an *admission* it made, and forging them for a human decision would corrupt the one durable ledger the fleet has (§4.2). What it does record is the displaced pin, so that `wfctl release` can hand the value back to the controller with its history intact.
type Plan ¶
type Plan struct {
// Summary is one line: the operation, the object, the field manager.
Summary string
// Before and After are the fields the write touches, keyed identically so
// that the two render as a single table. A field the write creates appears
// in After alone, and one it deletes in Before alone.
Before map[string]string
After map[string]string
// Warnings are what the operator has to weigh before consenting: what the
// controller will do next, what will revert this, what is being bypassed.
Warnings []string
// Apply performs the write and reports whether anything reached the
// cluster. Never nil: an action with nothing to do says so in Summary and
// applies a no-op, so that every caller has one path.
//
// The two results are independent on purpose. A strip that patched thirty
// sources and was denied the thirty-first, or a release whose transfer
// landed and whose relinquish did not, has both changed the cluster and
// failed — and the audit trail has to say so, because the operator's next
// question is what state the fleet is in, not whether the command exited 0.
Apply func(ctx context.Context) (written bool, err error)
}
Plan is what a write would do, and the closure that does it.
type Release ¶
type Release struct {
Client client.Client
Source types.NamespacedName
// Float removes the pin instead of transferring it.
Float bool
// Strategy resolves the tracking ref recorded as provenance; nil means
// the v1 default.
Strategy selection.Strategy
// Now stamps the admitted-at annotation; nil means the wall clock.
Now func() time.Time
}
Release ends a hold on one source.
The default is a *transfer*, not a removal: the hand-pinned value is kept and handed to the controller, which then reports HoldReleased and admits from it normally. That is the honest end of an incident — the SHA an operator pinned by hand is the SHA the fleet is running, and unpinning would instead hand the source back to its tracking ref and let the next commit through the moment the gate opens.
It takes two writes, because SSA has no "disown" verb:
- pin.Writer.Advance under wavefront-controller with the *same* value. Applying a value identical to the live one is not a change, so the apiserver reports no conflict and simply adds the controller as a co-owner (DESIGN §3.5, plan B4).
- relinquish the holder's share, so the controller is left sole owner and pin.Hold stops reporting a hold.
--float takes the other road: delete the pin entirely and let the source float on its tracking ref until the controller initial-pins it from the artifact (§3.5.4).
type Strip ¶
type Strip struct {
Client client.Client
// Wavefront is the fleet whose suspend state governs whether the strip
// sticks, and the object --suspend flips.
Wavefront *wavefrontv1alpha1.Wavefront
// IncludeHeld strips hand-pinned sources too.
IncludeHeld bool
// Suspend suspends the fleet before stripping.
Suspend bool
}
Strip is the break-glass procedure of docs/runbook.md §8.5: remove every managed source's spec.ref.commit, restoring plain floating-ref Flux.
It is the runbook's own loop, made safe. The shell one-liner strips whatever it finds; this refuses to touch a source somebody is holding by hand unless told to, keeps going when one namespace denies it, and says out loud that the controller will re-pin everything on its next sweep unless the fleet is suspended — which --suspend does first, in the same command.
type WavefrontChange ¶
type WavefrontChange struct {
Client client.Client
// Wavefront is the object as it was read, and the optimistic-lock base.
Wavefront *wavefrontv1alpha1.Wavefront
// Suspend sets spec.suspend; nil leaves it alone.
Suspend *bool
// Mode sets spec.mode; nil leaves it alone.
Mode *wavefrontv1alpha1.Mode
}
WavefrontChange flips one of the two fleet-level brakes: spec.suspend (the gentle one) or spec.mode (Shadow ⇄ Enforce). See docs/runbook.md, "Modes and brakes".
The write is a merge patch, deliberately, and never a server-side apply: SSA would make wfctl an *applier* of the Wavefront spec, and the next GitOps reconcile would then have to fight it. A merge patch changes the value and leaves the shape of ownership alone, which is what lets a GitOps-managed Wavefront be suspended in an incident and then correctly reverted by git (plan B4).