Documentation
¶
Overview ¶
Package snapshot is wfctl's truth model: one versioned, self-describing picture of a Wavefront that every command renders from, however it was obtained.
Three providers yield the same Snapshot (decision "Truth model"): StatusSource reads what the controller published (status.members — the default, and the only tier a read-only viewer needs), DeriveSource re-derives it live through the shared internal/inputs pipeline (which works while the controller is down, and checks it when it is not), and FileSource replays a captured snapshot, preserving whichever of the two origins produced it. The Source interface is deliberately the only seam the renderers see, so a future --live provider backed by a controller API is a non-breaking addition.
A Snapshot never carries Secret data, auth material, kubeconfig, raw managedFields, or URL userinfo: it is written to files and pasted into incident channels.
Index ¶
- Constants
- func EventCount(e eventsv1.Event) int32
- func EventTime(e eventsv1.Event) time.Time
- func ListEvents(ctx context.Context, reader client.Reader, wavefront string, ...) ([]eventsv1.Event, error)
- func Observe(ctx context.Context, r client.Reader, targets []gitpoll.Target, ...) (map[types.NamespacedName]gitpoll.Observation, []string)
- func ParseNodeRef(s string) (adapter.NodeRef, error)
- func ParseSource(s string) (types.NamespacedName, error)
- func SelectWavefront(ctx context.Context, r client.Reader, name string) (*wavefrontv1alpha1.Wavefront, error)
- func SpecOwners(wf *wavefrontv1alpha1.Wavefront) map[string]string
- func Waves(nodes []NodeView) map[adapter.NodeRef]int
- type AdmissionView
- type ClusterIdent
- type DeriveSource
- type DerivedStatus
- type EventFilter
- type FileSource
- type GraphView
- type HoldView
- type NodeView
- type PollOptions
- type Snapshot
- type Source
- type SourceView
- type StatusSource
- type WavefrontView
Constants ¶
const ( // Version is the Snapshot's apiVersion. FileSource rejects anything else: // a renderer must never guess at a schema it does not know. Version = "wfctl.wavefront.as-code.io/v1alpha1" // KindSnapshot is the Snapshot's kind. KindSnapshot = "Snapshot" )
const ( // OriginStatus: read back from status.members. OriginStatus = "status" // OriginDerive: re-derived live from Kustomizations and GitRepositories. OriginDerive = "derive" )
Snapshot origins. A snapshot always names how it was obtained, because what it can prove differs: a status snapshot reports what the controller last published, a derive snapshot what is true right now.
There is deliberately no "file" origin. Replaying a snapshot does not change what it proved when it was captured, so a replayed derive snapshot must still render its DERIVED columns; Replayed says how the snapshot reached the renderer, Origin what it is.
const ( FieldMode = "spec.mode" FieldSuspend = "spec.suspend" )
The SpecOwners keys — the two Wavefront spec fields wfctl writes.
const DefaultPollTimeout = 30 * time.Second
DefaultPollTimeout bounds one ref listing (the --poll-timeout default).
const EventNamespace = "default"
EventNamespace is where every events.k8s.io/v1 Event regarding a Wavefront lands.
Wavefronts are cluster-scoped; client-go's own event recorder defaults a cluster-scoped regarding object's namespace to "default" — the same place actions.Audit writes wfctl's own audit trail — which the e2e suite's fleetEventNamespace constant confirms against a real apiserver (test/e2e/wavefront_test.go).
Variables ¶
This section is empty.
Functions ¶
func EventCount ¶
EventCount is the number of occurrences one row represents: the recorder aggregates repeats onto a single Event with a Series rather than creating a new object every time, and a converted core/v1 event carries the same aggregate in its deprecated count instead.
func EventTime ¶
EventTime resolves the instant an event is ordered and filtered by (plan B3, `history`): its own eventTime when the recorder set one, falling back to the series' last-observed heartbeat, falling back to the deprecated core/v1 firstTimestamp a converted event carries instead of eventTime.
func ListEvents ¶
func ListEvents(ctx context.Context, reader client.Reader, wavefront string, filter EventFilter) ([]eventsv1.Event, error)
ListEvents lists the controller's and wfctl's own events.k8s.io/v1 events regarding wavefront, oldest first (kubectl's own --sort-by=.lastTimestamp convention).
The controller's Eventf calls (wavefront_controller.go's event helper) and wfctl's own audit trail (actions.Audit) both record regarding the Wavefront in EventNamespace, distinguished only by ReportingController ("wavefront-controller" vs "wfctl"), so one selector sees the merged stream `history` promises. A per-source event also names that source's GitRepository as its related object (wavefront_controller.go's event helper) — required for the events.k8s.io/v1 recorder's own dedup key to distinguish one source's event from another's in the same pass — but `related` cannot be used in a field selector, so this filter still keys on regarding alone; the note carries the source for filterBySource below.
func Observe ¶
func Observe( ctx context.Context, r client.Reader, targets []gitpoll.Target, opts *PollOptions, now time.Time, ) (map[types.NamespacedName]gitpoll.Observation, []string)
Observe lists every target's advertised refs once and returns the observations the evaluation should run against.
It never returns an error. A CLI that refuses to report anything because one of forty sources has an expired deploy key is useless in the incident it exists for: each failure becomes a Diagnostic and leaves that target unobserved, which every renderer already knows how to mark.
now stamps both ObservedAt and FirstObserved. There is no history to draw a real first-observation from — this is a single sweep, not a running poller — so a pending node's wait measures from this run, which is why derived LAG is rendered as a lower bound.
func ParseNodeRef ¶
ParseNodeRef parses a node reference as typed on the command line: "ns/name", where the kind defaults to Kustomization, or the explicit "Kind/ns/name" (decision "Conventions"). The kind is spelled out from day one so that a future HelmRelease graph, reserved but not built in v1, needs no new syntax.
The kind is matched case-insensitively and returned in its canonical spelling, so "kustomization/apps/web" and "Kustomization/apps/web" name the same node. v1alpha1 graphs only Kustomizations, so any other kind is rejected rather than accepted into a lookup that could never match.
func ParseSource ¶
func ParseSource(s string) (types.NamespacedName, error)
ParseSource parses a GitRepository reference as typed on the command line: "ns/name", the same form SourceView.Name and status.held[].source use.
func SelectWavefront ¶
func SelectWavefront(ctx context.Context, r client.Reader, name string) (*wavefrontv1alpha1.Wavefront, error)
SelectWavefront resolves the Wavefront to operate on. A named one is fetched directly; with no name, a single Wavefront is auto-selected and anything else is an error that names the candidates — guessing which Wavefront an operator meant is exactly the mistake an incident cannot afford (decision "Conventions").
func SpecOwners ¶
func SpecOwners(wf *wavefrontv1alpha1.Wavefront) map[string]string
SpecOwners maps "spec.mode" and "spec.suspend" to their owning field manager, so a write command can warn that a GitOps applier owns the field and will revert the change. It is exported because that warning is built by internal/wfctl/actions, from a Wavefront it read itself.
The first entry to claim a field wins: managedFields is returned in a stable order by the apiserver, and co-ownership of a scalar is rare enough that reporting one manager beats inventing a list the schema has no room for. Subresource entries cannot own spec and are skipped, as are entries whose FieldsV1 will not parse — an unparseable entry proves no ownership.
func Waves ¶
Waves layers nodes by dependsOn depth, as `wfctl graph` renders them: 0 for a node with no dependencies, otherwise 1 + the deepest dependency.
It is Kahn's algorithm run over the snapshot's own edges rather than internal/graph, so it works identically for a status snapshot (whose edges come from status.members) and for a replayed file — neither of which has a cluster to rebuild a graph.Graph from.
A node that never dequeues is in, or behind, a cycle and gets -1: no depth is meaningful for it, and rendering one would invent an order the engine explicitly refuses to assume. A dependency that is not itself a node in the list is a missing gate: it layers at 0 and is included in the result, so a renderer can show it as the wave-0 blocker it behaves as.
Types ¶
type AdmissionView ¶
type AdmissionView struct {
Node adapter.NodeRef `json:"node"`
// Source is the "ns/name" of the GitRepository.
Source string `json:"source"`
From string `json:"from,omitempty"`
To string `json:"to"`
ObservedRef string `json:"observedRef,omitempty"`
Initial bool `json:"initial,omitempty"`
PendingSince *time.Time `json:"pendingSince,omitempty"`
}
AdmissionView is one pin advance the evaluation would perform.
type ClusterIdent ¶
type ClusterIdent struct {
Context string `json:"context,omitempty"`
Server string `json:"server,omitempty"`
WfctlVersion string `json:"wfctlVersion,omitempty"`
}
ClusterIdent identifies the cluster a snapshot came from. Server is the host only: an apiserver URL's path and userinfo are neither useful here nor safe to share.
type DeriveSource ¶
type DeriveSource struct {
Reader client.Reader
Wavefront string
// Poll, when set, lists each source's advertised refs before the
// evaluation, exactly as the controller's poller would.
Poll *PollOptions
// Observations, when non-nil, supplies the observation set directly and
// Poll is not consulted. It is the seam a future --live provider hands the
// controller's own coherent sweep through, and what a test injects to
// derive a pending fleet without a git host; a nil map (the default) means
// no observations at all.
Observations map[types.NamespacedName]gitpoll.Observation
// Now is the clock used for CapturedAt, Evaluated and the observation
// timestamps; nil means time.Now.
Now func() time.Time
}
DeriveSource re-derives the picture live, through the very pipeline the reconciler uses (decision "Seam"): inputs.Build for discovery, resolution, graph and evaluation, inputs.Summarise for the numbers.
That is what makes `--derive` two things at once — the answer when the controller is down, and the check on it when it is not: a REPORTED column that disagrees with a DERIVED one is evidence about the controller, not about two different algorithms.
Observations are opt-in. Without Poll (or an injected Observations map) the evaluation runs with no observed SHAs at all, which is honest but blind: nothing can be pending, so the snapshot says Observed=false and every renderer marks the observed columns unknown rather than empty.
type DerivedStatus ¶
type DerivedStatus struct {
Phase wavefrontv1alpha1.Phase `json:"phase,omitempty"`
Counts wavefrontv1alpha1.NodeCounts `json:"counts"`
Blocked []wavefrontv1alpha1.BlockedNode `json:"blocked,omitempty"`
Held []wavefrontv1alpha1.HeldNode `json:"held,omitempty"`
BlockedByReason map[string]int `json:"blockedByReason,omitempty"`
FetchFailures int `json:"fetchFailures,omitempty"`
// GraphValid, GraphReason and GraphMessage are inputs.GraphVerdict — the
// GraphValid condition in all but name.
GraphValid bool `json:"graphValid"`
GraphReason string `json:"graphReason,omitempty"`
GraphMessage string `json:"graphMessage,omitempty"`
// Admissions are the ancestor-gated pin advances this evaluation would
// perform; Initial the ungated pins for sources seen for the first time.
Admissions []AdmissionView `json:"admissions,omitempty"`
Initial []AdmissionView `json:"initial,omitempty"`
}
DerivedStatus is what only a live re-derivation proves: the same numbers the controller would publish, computed here and now. `wfctl status --derive` prints these beside the reported ones and flags disagreement.
type EventFilter ¶
type EventFilter struct {
// Reason restricts to one event reason; empty means every reason.
Reason string
// Warnings restricts to type=Warning events.
Warnings bool
// Source restricts to events whose note names this source (as returned
// by ParseSource's NamespacedName.String, "namespace/name") as a
// standalone token; empty means every source.
Source string
// Since restricts to events at or after this instant; the zero value
// means no lower bound.
Since time.Time
}
EventFilter narrows the events `history` lists.
type FileSource ¶
type FileSource struct {
Path string
}
FileSource replays a captured Snapshot (`--from file.json`). It needs no cluster at all, which is the point: an incident's evidence can be attached to a ticket and re-rendered by anyone, and the renderers' golden tests run against fixtures rather than a fake apiserver.
func (FileSource) Capture ¶
func (f FileSource) Capture(_ context.Context) (*Snapshot, error)
Capture implements Source.
A snapshot whose apiVersion or kind is not this package's is rejected outright rather than decoded on a best-effort basis: a renderer that silently shows zero values for fields a newer schema moved would report a quiescent fleet that is nothing of the sort.
The captured Origin is preserved and Replayed is set, so a renderer branches on what the snapshot proved, not on how it was delivered.
type GraphView ¶
type GraphView struct {
Cycles [][]adapter.NodeRef `json:"cycles,omitempty"`
Unknown []adapter.NodeRef `json:"unknown,omitempty"`
Missing []adapter.NodeRef `json:"missing,omitempty"`
}
GraphView is the structural verdict on the dependsOn DAG. Under the status origin it is rebuilt from the members' own dependsOn edges; Missing is derive-only, because status cannot distinguish a dangling dependency from an unready gate.
type HoldView ¶
type HoldView struct {
// Kind is HandPin or Suspend.
Kind string `json:"kind"`
// Manager is empty for a Suspend hold.
Manager string `json:"manager,omitempty"`
}
HoldView is one source's hold, in the unified form the engine reports (inputs.Hold): a foreign field manager owning spec.ref.commit, or spec.suspend, which names no actor.
type NodeView ¶
type NodeView struct {
Ref adapter.NodeRef `json:"ref"`
Role string `json:"role"`
State string `json:"state"`
Held bool `json:"held,omitempty"`
// Blocked attributes a pending node's non-admission to its nearest
// unsettled ancestor (or blocking sibling).
Blocked *wavefrontv1alpha1.BlockedRef `json:"blocked,omitempty"`
PendingSince *time.Time `json:"pendingSince,omitempty"`
Ready bool `json:"ready"`
// Failing is populated only under the derive origin (see above).
Failing bool `json:"failing,omitempty"`
ReadyMessage string `json:"readyMessage,omitempty"`
AppliedSHA string `json:"appliedSHA,omitempty"`
DependsOn []adapter.NodeRef `json:"dependsOn,omitempty"`
// Source is the "ns/name" of the backing GitRepository; nil for a gate,
// which by definition has none.
Source *string `json:"source,omitempty"`
Pin string `json:"pin,omitempty"`
ObservedSHA string `json:"observedSHA,omitempty"`
// Wave is the node's dependsOn depth; -1 means it is in, or behind, a
// cycle and therefore never layered.
Wave int `json:"wave"`
}
NodeView is one evaluated node's derived state.
ReadyMessage, AppliedSHA and Failing are derive-only: status.members carries neither the Ready condition's message nor its explicit-False distinction, so under the status origin they are zero and a renderer must not read anything into that.
A renderer decides what to show from Origin, never from Replayed: replaying a derive snapshot from a file loses none of these fields.
type PollOptions ¶
type PollOptions struct {
// Timeout bounds one listing; <= 0 means DefaultPollTimeout.
Timeout time.Duration
// PerHostConcurrency bounds concurrent listings per git host, the same
// courtesy the controller extends; <= 0 means the CRD default. The CLI
// passes the Wavefront's own value.
PerHostConcurrency int
// Lister lists advertised refs; nil means the production go-git lister,
// which fetches no objects and touches no disk.
Lister gitpoll.Lister
// Strategy selects the candidate SHA from an advertisement; nil means the
// v1 default, TrackRef. It must be the same strategy the evaluation uses,
// or the candidate and the tracking ref would come from different
// policies.
Strategy selection.Strategy
}
PollOptions turns on live ref-advertisement listing for DeriveSource.
Polling from a CLI is opt-in because it costs credentials: it reads each source's Secret and speaks to every git host in the fleet from wherever the operator is sitting, which is why it needs the wider derive+poll RBAC tier rather than the read-only viewer one. Without it the derived picture is honest but blind — nothing can be pending.
type Snapshot ¶
type Snapshot struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
// Origin is how the picture was obtained when it was captured: status or
// derive. It survives a round trip through a file unchanged.
Origin string `json:"origin"`
// Replayed marks a Snapshot that came from a file rather than a live
// cluster (`--from`). It is orthogonal to Origin: a replayed derive
// snapshot is still a derive snapshot, and still carries Derived.
Replayed bool `json:"replayed,omitempty"`
// CapturedAt is when this snapshot was taken.
CapturedAt time.Time `json:"capturedAt"`
// Evaluated is when the picture was last derived: status.lastEvaluated
// under the status origin, CapturedAt under derive. nil means the
// controller has never published an evaluation.
Evaluated *time.Time `json:"evaluated"`
// Cluster identifies where the snapshot came from. Filled by the CLI,
// which owns the kubeconfig; the Sources here never read one.
Cluster ClusterIdent `json:"cluster"`
// Observed reports whether observed SHAs are meaningful. False means
// UNKNOWN, not "nothing pending": a renderer must say so rather than
// present an empty ObservedSHA as a fact (renderers show `?` instead).
Observed bool `json:"observed"`
// Wavefront is the object itself, as the cluster holds it.
Wavefront WavefrontView `json:"wavefront"`
// Nodes is every evaluated node, sorted by Ref.String().
Nodes []NodeView `json:"nodes"`
// Sources is every managed GitRepository backing a pinned node, sorted by
// Name ("ns/name"). Entries may be partial under the status origin when
// RBAC denies the GitRepository read; a Diagnostic says so.
Sources []SourceView `json:"sources"`
// Graph is the structural verdict on the dependsOn DAG.
Graph GraphView `json:"graph"`
// Derived carries what only a live re-derivation can prove; zero under
// every other origin.
Derived DerivedStatus `json:"derived"`
// Diagnostics are human-readable degradations of this snapshot: selector
// overlap, unsupported ref styles, poll and credential errors, stale
// status, RBAC degradation. Never fatal — a degraded picture beats none.
Diagnostics []string `json:"diagnostics"`
}
Snapshot is one complete, renderable picture of a Wavefront.
type Source ¶
Source captures one Snapshot. It is the only seam every renderer and write command sees, so where a picture came from — published status, live re-derivation, a replayed file, or a future controller API — is a choice the CLI makes once, at the top.
type SourceView ¶
type SourceView struct {
// Name is "ns/name".
Name string `json:"name"`
// URL has any userinfo stripped, and is empty when the URL could not be
// parsed — never a URL that might still embed credentials.
URL string `json:"url,omitempty"`
SecretRefName string `json:"secretRefName,omitempty"`
TrackingRef string `json:"trackingRef,omitempty"`
Pin string `json:"pin,omitempty"`
Suspended bool `json:"suspended,omitempty"`
// CommitOwners lists every field manager owning spec.ref.commit — the
// evidence behind a hold, and what `release` has to unpick.
CommitOwners []pin.Owner `json:"commitOwners,omitempty"`
Hold *HoldView `json:"hold,omitempty"`
// Provenance carries the three wavefront.as-code.io pin annotations: the
// durable ledger, unlike events, which the apiserver eventually expires.
Provenance map[string]string `json:"provenance,omitempty"`
ArtifactSHA string `json:"artifactSHA,omitempty"`
FetchFailing bool `json:"fetchFailing,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
ObservedSHA string `json:"observedSHA,omitempty"`
FirstObserved *time.Time `json:"firstObserved,omitempty"`
// Nodes lists every node referencing this source, sorted.
Nodes []adapter.NodeRef `json:"nodes,omitempty"`
// Partial marks a source whose GitRepository could not be read (RBAC or
// deletion): every field beyond Name, Pin and Nodes is unproven.
Partial bool `json:"partial,omitempty"`
}
SourceView is one managed GitRepository as wfctl reports it.
type StatusSource ¶
type StatusSource struct {
Reader client.Reader
Wavefront string
// Now is the clock used for CapturedAt and the staleness verdict; nil
// means time.Now.
Now func() time.Time
}
StatusSource is the default provider: it reports what the controller published, and needs nothing beyond get/list on wavefronts — the entire footprint of the viewer RBAC tier.
The Wavefront's status.members is a complete, self-contained picture of the last evaluation — every node, its state, its edges, its pin and its observed SHA — so this is the exact inverse of inputs.Summarise's members mapping. What status cannot carry, it does not invent: the Ready condition's message, the applied SHA and the explicit-Failing distinction are left zero (see NodeView), and a snapshot older than the controller's own cadence earns a Diagnostic rather than a silent lie.
type WavefrontView ¶
type WavefrontView struct {
Name string `json:"name"`
Generation int64 `json:"generation"`
Spec wavefrontv1alpha1.WavefrontSpec `json:"spec"`
Status wavefrontv1alpha1.WavefrontStatus `json:"status"`
// SpecOwners maps "spec.mode" and "spec.suspend" to the field manager
// owning them, so a write command can warn that a GitOps applier will
// revert the change. Derived from managedFields; the raw
// managedFields never appear in a Snapshot.
SpecOwners map[string]string `json:"specOwners,omitempty"`
}
WavefrontView is the Wavefront object as the cluster holds it, plus who owns the two fields wfctl writes.