source

package
v0.33.7 Latest Latest
Warning

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

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

Documentation

Overview

Package source provides the snapshot tree model and the BuildTree function that resolves a Snapshot hierarchy from the Kubernetes API.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCycle is returned when a cycle or duplicate reference is detected in the snapshot tree.
	ErrCycle = errors.New("cycle detected in snapshot tree")
	// ErrTreeBudget is returned when a snapshot tree exceeds a configured traversal limit.
	ErrTreeBudget = errors.New("snapshot tree traversal budget exceeded")
)
View Source
var DegradedReadyReasons = []string{"ChildSnapshotDeleted"}

DegradedReadyReasons is the canonical set of Ready=False reasons that represent a recoverable degradation: capture already completed, the captured data is intact in the content-layer recycle bin, and the snapshot degraded without failing outright.

This MUST be kept byte-identical to the SSOT in state-snapshotter api/storage/v1alpha1/conditions.go's DegradedReadyReasons. That set is explicitly documented there as free to grow, so this slice is not assumed to stay single-member forever — re-verify against the sibling repo before relying on its exact contents.

View Source
var ErrAmbiguousNode = errors.New("ambiguous node: multiple nodes match kind and name")

ErrAmbiguousNode is returned by FindNode when more than one node in the tree matches the supplied kind and name.

View Source
var ErrLeafNotBound = errors.New("VolumeSnapshot leaf not yet captured (no status.data)")

ErrLeafNotBound is returned when a VolumeSnapshot visibility-leaf has no namespaced status.data; the leaf is not yet captured and is not ready for download.

View Source
var ErrNodeNotFound = errors.New("node not found in snapshot tree")

ErrNodeNotFound is returned by FindNode when no node in the tree matches the supplied kind and name.

Functions

func CanonicalSnapshotIdentity

func CanonicalSnapshotIdentity(id SnapshotIdentity) string

CanonicalSnapshotIdentity returns an opaque, deterministic key for a snapshot node, used for resume matching and as the input to the archive collision discriminator (a short hash of it disambiguates two nodes that share a readable source-name directory base). It is NOT a filesystem path.

func CanonicalSourceIdentity

func CanonicalSourceIdentity(id SourceRefIdentity) string

CanonicalSourceIdentity returns an opaque, deterministic key for the captured source object (provenance). Same NUL-joined form as CanonicalSnapshotIdentity; not a path.

func FindNode

func FindNode(root *Node, kind, name string) (*Node, []*Node, error)

FindNode searches the tree rooted at root for the unique node matching the supplied kind and name. A node matches under EITHER of two rules: (1) its own snapshot-CR Kind/Name equal kind/name (the original, unchanged behavior), or (2) its captured source object's SourceRef.Kind/Name equal kind/name (new — resolves by the ORIGINAL object identity, back-compatible with the snapshot-CR-name form so both selector styles keep working). A node matching under both rules simultaneously is still one match, not two. It returns the node and the ordered ancestor chain from the root down to the node's parent (nil when the match is the root itself).

For domain snapshot nodes Name is the snapshot CR's metadata.name (e.g. nss-child-…). For VolumeSnapshot orphan leaf nodes Kind is "VolumeSnapshot" and Name is the captured PVC name (Node.Name == dataRef.Target.Name set by BuildTree).

Returns ErrNodeNotFound when no node matches; returns ErrAmbiguousNode when more than one node matches — including when two DIFFERENT nodes each match under a different one of the two rules (e.g. one node's CR name collides with another node's captured source name): that is a genuine ambiguity, not a special case to resolve silently.

FindNode operates solely on the in-memory tree; it never fetches from the cluster.

func IsDegradedReason

func IsDegradedReason(reason string) bool

IsDegradedReason reports whether a Ready=False reason is a recoverable degradation (capture done, data intact, recoverable by manual intervention). Mirrors state-snapshotter's api/storage/v1alpha1.IsReasonDegraded.

func ParseNodeStatus

ParseNodeStatus decodes a snapshot node's identity plus its self-contained namespaced status fragments (status.sourceRef and status.data) directly from the unstructured object, without ever reading cluster-scoped SnapshotContent. It is the single reader d8 uses to build the tree from the namespaced API (see docs/2026-06-29-unified-snapshots-overview.md).

It is fail-closed: an absent status.sourceRef or status.data is allowed (returns nil), but a present-yet-malformed fragment is a hard error (never silently treated as "no data"). Both fragments must be JSON objects with their required identity fields set; status.data.size, when present, must parse as a quantity. status.sourceRef is a full provenance identity, so its uid is REQUIRED here; its namespace is required for namespaced source kinds but intentionally absent for the cluster-scoped root source (v1/Namespace), so it is validated per source scope (see parseStatusSourceRef) rather than unconditionally. A present status.data is fully validated HERE, at parse time, by parseStatusData: source and artifact identity are required, while source.uid is intentionally optional/best-effort and matched name-first downstream (see WriteVolumeManifest/matchesVolumeTarget in volume/manifest_worker.go).

The node's own SnapshotIdentity is validated up front: it feeds the resume key, checksum/index and the collision discriminator, so a weak (partially empty) identity would silently corrupt those. Every snapshot node is namespaced in Stage 2, hence metadata.namespace is required here.

Types

type AggregatedManifestSource

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

AggregatedManifestSource is the production ManifestSource backed by the state-snapshotter aggregated subresource API. It performs a single manifests-download GET per node and returns the decoded objects with plumbing kinds removed. The server already returns a clean JSON array (status preserved, namespace made relative), so no chunk assembly, gzip decode, or checksum verification is needed on the client side.

func NewAggregatedManifestSource

func NewAggregatedManifestSource(c *aggapi.Client) *AggregatedManifestSource

NewAggregatedManifestSource constructs an AggregatedManifestSource backed by c.

func (*AggregatedManifestSource) FetchNodeManifests

FetchNodeManifests implements ManifestSource. It GETs the node's manifests-download subresource and returns user objects with plumbing kinds removed.

type ArtifactRef

type ArtifactRef struct {
	APIVersion string `json:"apiVersion"`
	Kind       string `json:"kind"`
	Name       string `json:"name"`
	UID        string `json:"uid,omitempty"`
}

ArtifactRef points to a durable data artifact (e.g. a VolumeSnapshotContent). It mirrors state-snapshotter api/storage/v1alpha1 SnapshotDataArtifactRef. UID is best-effort: the core fills it once known, so it is optional in the wire form and validated only on the data path.

type ManifestSource

type ManifestSource interface {
	FetchNodeManifests(ctx context.Context, ref aggapi.NodeRef) ([]unstructured.Unstructured, error)
}

ManifestSource retrieves the own-scope manifests for a snapshot node. The interface is intentionally narrow so that an aggregated-API backend can replace any other implementation without changing callers. Nodes are addressed by aggapi.NodeRef rather than by a low-level ManifestCheckpoint name.

type Node

type Node struct {
	// APIVersion is the apiVersion of the snapshot CR for this node
	// (e.g. "state-snapshotter.deckhouse.io/v1alpha1" or a domain-specific group).
	// Orphan leaf volume nodes use "snapshot.storage.k8s.io/v1".
	APIVersion string

	// Kind is the kind of the snapshot CR for this node
	// (e.g. "Snapshot", "DemoVirtualMachineSnapshot").
	// Orphan leaf volume nodes always have Kind == "VolumeSnapshot".
	Kind string

	// Name is the metadata.name of the snapshot CR.
	// For orphan leaf nodes it is the captured VolumeSnapshot CR name.
	Name string

	// Namespace is the namespace of the snapshot CR.
	// For the root it is the user-supplied namespace; children inherit it.
	Namespace string

	// UID is the metadata.uid of the snapshot CR. Together with APIVersion/Kind/Namespace/Name
	// it forms the node's SnapshotIdentity (see identity.go), the basis for the resume key and
	// the archive collision discriminator. The readable directory base is NOT derived from it
	// (it comes from the source name; see DirBaseName).
	UID types.UID

	// SourceRef is the identity of the original captured source object, parsed from the
	// namespaced status.sourceRef (see ParseNodeStatus). It is the readable-directory base
	// (SourceRef.Name) and the domain source identity persisted for import reconstruction.
	// Nil when the CR has no status.sourceRef (e.g. some import-mode nodes).
	SourceRef *SourceRefIdentity

	// Data is the node's captured volume payload parsed from the namespaced status.data
	// (Variant A: at most one per node), or nil for aggregators and manifest-only nodes.
	Data *NodeData

	// Ready is this node's own Ready condition (status/reason/message), read from the node's
	// own status.conditions — populated for every node (root and every descendant, including
	// orphan VolumeSnapshot leaves), not only the root. Zero value when the node carries no
	// Ready condition at all; see parseReadyCondition in conditions.go.
	Ready NodeReadyStatus

	// Parent is the parent node. Nil for the root.
	Parent *Node

	// Children are the direct child nodes: domain snapshot children first (in
	// childrenSnapshotRefs order), then orphan leaf volume children for aggregator
	// nodes (in VolumeSnapshot visibility-leaf ref order). Always nil for orphan leaf
	// volume nodes (they are leaves).
	Children []*Node
}

Node is one node in the resolved snapshot tree.

All nodes in a tree share the same namespace (the root Snapshot namespace). Cross-namespace references are structurally impossible: SnapshotChildRef carries no namespace field, and the tree builder always fetches children in the root namespace.

Every node is built solely from the snapshot CR's own namespaced status (status.sourceRef / status.data via ParseNodeStatus); the tree builder never reads cluster-scoped SnapshotContent. There are two flavours of node:

  • Snapshot nodes: a snapshot CR in the tree hierarchy. A non-aggregator domain node (e.g. DemoVirtualDiskSnapshot) carries its own captured volume in Data; an aggregator (which has VolumeSnapshot visibility-leaf children) has Data == nil and exposes data through those leaf children.
  • Orphan leaf volume nodes: one captured standalone PVC. Kind is always "VolumeSnapshot"; APIVersion is "snapshot.storage.k8s.io/v1"; Data holds the captured volume; Children is always nil.

func BuildTree

func BuildTree(ctx context.Context, c client.Client, namespace, rootName string) (*Node, error)

BuildTree fetches the root Snapshot by name and recursively resolves the full snapshot tree by following status.childrenSnapshotRefs.

Each node is built solely from its own namespaced status via ParseNodeStatus: status.sourceRef (the captured source identity) and status.data (the single captured volume, Variant A cardinality ≤1). The tree builder NEVER reads cluster-scoped SnapshotContent. Node manifests are fetched separately via the aggregated manifests-download subresource.

All snapshot nodes are namespace-local: child refs carry no namespace field and are always fetched in the same namespace as the root. The function does one Get per node and never lists.

childrenSnapshotRefs are partitioned into two sets:

  • Domain refs (apiVersion != "snapshot.storage.k8s.io/v1" or kind != "VolumeSnapshot") are recursed normally.
  • VolumeSnapshot visibility-leaf refs (apiVersion == "snapshot.storage.k8s.io/v1" and kind == "VolumeSnapshot") signal that this node is an aggregator. Each leaf is resolved via visitVisibilityLeaf: Get the VolumeSnapshot and read its own namespaced status.sourceRef/status.data. The leaf node's Name is the VS CR name (for ManifestScopeRef); its readable directory base comes from status.sourceRef.name.

A non-aggregator node's captured volume (if any) is its own status.data. An aggregator node has status.data == nil and exposes data through its leaf children.

Returns ErrCycle if a duplicate snapshot ref is encountered. Returns ErrLeafNotBound if a VolumeSnapshot leaf has no status.data.

func BuildTreeWithLimits

func BuildTreeWithLimits(
	ctx context.Context,
	c client.Client,
	namespace string,
	rootName string,
	limits TreeLimits,
) (*Node, error)

BuildTreeWithLimits builds a snapshot tree subject to explicit traversal limits.

func (*Node) DirBaseName

func (n *Node) DirBaseName() string

DirBaseName returns the human-readable base for this node's archive directory: the captured source object name (status.sourceRef.name) when present, else the snapshot CR name. Uniqueness and resume identity are NOT tied to this value — they use the node's SnapshotIdentity (incl UID) via the collision discriminator (see archive.NodeDirName / resume identity).

func (*Node) DisplayLabel

func (n *Node) DisplayLabel() string

DisplayLabel returns the human-readable "<Kind>/<Name>" label for this node, for user-facing output only (CLI messages, --node error text, tree/table rendering) — it never feeds identity, resume keys, or aggapi addressing (see Identity/Ref).

It prefers the captured source object's original identity (SourceRef.Kind/Name) over the snapshot CR's own Kind/Name, mirroring the same SourceRef-preference DirBaseName already applies to the on-disk directory name: a user who typed `d8 snapshot restore my-vm-disk` wants to see "DemoDiskSnapshot/my-vm-disk" in output, not the generated CR name "nss-child-...".

The root node is a deliberate exception: it always reports its own Kind/Name (the snapshot identity the user actually typed on the command line), never a SourceRef. A root Snapshot CR has no captured-source identity distinct from a namespace-level capture, so resolving it would show something the user never named; echoing the typed identity back is the least surprising choice for the tree's entry point.

func (*Node) Identity

func (n *Node) Identity() SnapshotIdentity

Identity returns the node's structural SnapshotIdentity (apiVersion/kind/namespace/name/uid), the basis for the resume key and the archive collision discriminator.

func (*Node) IsVolumeLeaf

func (n *Node) IsVolumeLeaf() bool

IsVolumeLeaf reports whether this node is a CSI VolumeSnapshot visibility-leaf (a captured standalone PVC exposed as a leaf child of an aggregator).

func (*Node) ManifestScopeRef

func (n *Node) ManifestScopeRef() aggapi.NodeRef

ManifestScopeRef returns the aggregated-API node reference used to fetch this node's own-scope manifests (manifests-download subresource).

For domain snapshot nodes this is the node's own ref (its snapshot CR identity → own ManifestCheckpoint).

For orphan leaf volume nodes this is also the node's own ref: APIVersion=snapshot.storage.k8s.io/v1, Kind=VolumeSnapshot, Name=VS CR name. The VolumeSnapshot connector (subresources.snapshot.storage.k8s.io) resolves this ref via VolumeSnapshot.status.boundSnapshotContentName to the leaf's own child SnapshotContent ManifestCheckpoint (which holds the captured PVC manifest).

func (*Node) Ref

func (n *Node) Ref() aggapi.NodeRef

Ref returns the aggregated-API node reference that addresses this node's own manifests-download subresource.

type NodeData

type NodeData struct {
	// SourceRef identifies the captured PersistentVolumeClaim backing this node's data (the
	// data-leaf PVC, distinct from the top-level status.sourceRef live domain object). Its uid is
	// the single volume identity (state-snapshotter dropped the standalone targetUID).
	SourceRef SourceRefIdentity `json:"sourceRef"`
	// ArtifactRef references the cluster-scoped durable data artifact.
	ArtifactRef ArtifactRef `json:"artifactRef"`
	// VolumeMode is the source volume mode (Block or Filesystem).
	VolumeMode string `json:"volumeMode,omitempty"`
	// FsType is the source filesystem type (Filesystem volumes only).
	FsType string `json:"fsType,omitempty"`
	// AccessModes records the source PVC access modes.
	AccessModes []string `json:"accessModes,omitempty"`
	// StorageClassName records the source StorageClass of the captured volume.
	StorageClassName string `json:"storageClassName,omitempty"`
	// Size is the allocated size of the captured volume as a resource.Quantity string (e.g. "10Gi").
	Size string `json:"size,omitempty"`
}

NodeData is the decoded namespaced status.data descriptor: a self-contained {sourceRef, artifactRef, volume metadata} block the core mirrors onto every snapshot node. Variant A (cardinality ≤1): a node carries at most one data binding; multiple volumes are modeled as child volume nodes.

It mirrors state-snapshotter api/storage/v1alpha1 SnapshotDataBinding (source-based). This is the correct, current contract; the legacy target/targetUID/dataRef shape in internal/snapshot/api/v1alpha1 is outdated and is retired during Stage 2b when the tree builder switches to ParseNodeStatus.

type NodeReadyStatus

type NodeReadyStatus struct {
	// Status is the condition's status field ("True"/"False"/"Unknown").
	Status string
	// Reason is the condition's reason field (CamelCase, e.g. "ChildSnapshotDeleted").
	Reason string
	// Message is the condition's human-readable message field.
	Message string
}

NodeReadyStatus is a node's own Ready condition, read verbatim from the snapshot CR's status.conditions entry whose type == "Ready". The zero value (all empty strings) means the node carries no Ready condition at all; that is not an error, just an unpopulated status.

type SnapshotIdentity

type SnapshotIdentity struct {
	APIVersion string
	Kind       string
	Namespace  string
	Name       string
	UID        types.UID
}

SnapshotIdentity is the structural identity of a snapshot CR node itself (apiVersion/kind/namespace/name/uid). Every node has one, unlike SourceRefIdentity (the captured source object), which may be absent (root, manifest-only). It is the node's resume identity and the input to the archive collision discriminator. The readable archive directory name itself is NOT derived from it: layout stays source-name based via archive.NodeDirName (readable base = captured source name, fallback CR name); only the collision suffix uses a short hash of CanonicalSnapshotIdentity so two nodes sharing a source-name base never mix.

type SourceRefIdentity

type SourceRefIdentity struct {
	APIVersion string `json:"apiVersion"`
	Kind       string `json:"kind"`
	Namespace  string `json:"namespace"`
	Name       string `json:"name"`
	UID        string `json:"uid"`
}

SourceRefIdentity is the decoded form of the state-snapshotter.deckhouse.io/source-ref annotation. The annotation value is a JSON object {apiVersion, kind, namespace, name, uid} matching the SnapshotSourceIdentity type in state-snapshotter common/source_ref_annotation.go.

func ParseSourceRef

func ParseSourceRef(raw string) (SourceRefIdentity, error)

ParseSourceRef decodes the raw source-ref annotation string into a SourceRefIdentity. An empty or malformed annotation is not fatal: callers that only need the Name field may ignore the returned error. On any parse failure the zero SourceRefIdentity is returned.

type TreeLimits

type TreeLimits struct {
	MaxDepth int
	MaxNodes int
}

TreeLimits bounds the number and depth of nodes fetched by BuildTreeWithLimits. The root is at depth zero and counts toward MaxNodes.

func DefaultTreeLimits

func DefaultTreeLimits() TreeLimits

DefaultTreeLimits returns the traversal limits used by BuildTree: at most 10,000 nodes and 64 child edges below the root.

Jump to

Keyboard shortcuts

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