snapshot

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

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 (plan B1, 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

View Source
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"
)
View Source
const (
	// OriginStatus: read back from status.members.
	OriginStatus = "status"
	// OriginDerive: re-derived live from Kustomizations and GitRepositories.
	OriginDerive = "derive"
)

Snapshot origins (plan B2). 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.

View Source
const (
	FieldMode    = "spec.mode"
	FieldSuspend = "spec.suspend"
)

The SpecOwners keys — the two Wavefront spec fields wfctl writes.

View Source
const DefaultPollTimeout = 30 * time.Second

DefaultPollTimeout bounds one ref listing (plan B3, --poll-timeout).

View Source
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

func EventCount(e eventsv1.Event) int32

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

func EventTime(e eventsv1.Event) time.Time

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.

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 (plan B3).

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 (plan B3).

func ParseNodeRef

func ParseNodeRef(s string) (adapter.NodeRef, error)

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 a future HelmRelease plane needs no new syntax (DESIGN D12).

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 (plan B4). 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

func Waves(nodes []NodeView) map[adapter.NodeRef]int

Waves layers nodes by dependsOn depth (plan B3, `wfctl graph`): 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 (plan B3).

func (*DeriveSource) Capture

func (d *DeriveSource) Capture(ctx context.Context) (*Snapshot, error)

Capture implements Source.

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 initial pins (DESIGN §3.5.4).
	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 ListEvents (plan B3, `history`).

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 (DESIGN §3.1.1).
	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 (plan B3, the derive+poll RBAC tier). 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 (plan B3, `?` columns).
	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

type Source interface {
	Capture(ctx context.Context) (*Snapshot, error)
}

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 events are not (DESIGN §4.2).
	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 (plan B5's viewer 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 (DESIGN §4.1) — 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.

func (*StatusSource) Capture

func (s *StatusSource) Capture(ctx context.Context) (*Snapshot, error)

Capture implements Source.

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 (plan B4). 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.

Jump to

Keyboard shortcuts

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