The module lifecycle stageGeneral Availability

The module has requirements for installation

What is a Snapshot?

A Snapshot (state-snapshotter.deckhouse.io/v1alpha1, short name snap) is a namespaced, point-in-time capture of a namespace’s desired state. You create a Snapshot in a namespace, and the module captures the manifests of the user-owned objects in that namespace into an immutable, durably stored artifact that you can read back later or restore into another namespace.

A Snapshot is one-shot and immutable: the spec is frozen on creation, the namespace is captured exactly once, and there is no re-capture. To take a new snapshot, create a new Snapshot.

Property Value
API group / version state-snapshotter.deckhouse.io/v1alpha1
Kind Snapshot (short name snap)
Scope Namespaced (captures its own namespace)
Spec Immutable after creation

How it works

When you create a Snapshot, the controller:

  1. Discovers every namespaced resource type in the cluster via the Kubernetes discovery API (not a hardcoded list), so even arbitrary custom resources are captured.
  2. Lists the objects of each type in the snapshot’s namespace and selects the ones that represent user desired-state (see What gets captured).
  3. Stores the selected manifests in a durable, cluster-scoped artifact (SnapshotContent + an internal ManifestCheckpoint, chunked for large payloads).
  4. Publishes status: when the capture is complete and stored, the Snapshot becomes Ready=True and status.boundSnapshotContentName points at the stored content.
  5. Retains the stored content after the Snapshot is deleted for a configurable TTL, so a captured state survives the deletion of the request object.

The capture is fail-closed: if some resource type cannot be listed (e.g. RBAC not yet propagated, or a broken aggregated API), the controller does not produce a partial snapshot — the Snapshot simply never reaches Ready=True, and the capture is retried until the type becomes readable. The diagnosis is published in two places: status.captureState.domainSpecificController.message names the resource types that could not be read, and a Warning event with reason NamespacePlanUnreadable is emitted on the Snapshot.

What gets captured

The model is default-include: every namespaced object in the target namespace is captured unless a provable exclusion signal applies. There is no “capture only these” allowlist.

Captured (examples):

  • User config and data objects: ConfigMap, Secret (any type except service-account tokens), Service, PersistentVolumeClaim, ServiceAccount (except default), standalone Pod (without a controller owner).
  • Top-level workloads you create: Deployment, StatefulSet, DaemonSet, Job, CronJob, etc. Their derived objects (ReplicaSet/Pod) are excluded because the owner recreates them on restore.
  • Any custom resources in the namespace, including aggregated APIs backed by real storage.
  • Networking and RBAC: Ingress, NetworkPolicy, Role, RoleBinding, etc.

Not captured:

Category Examples Why
Controller-owned derivatives objects with ownerReference.controller: true (ReplicaSet/Pod of a Deployment) the owner recreates them on restore
Control-plane noise Event, Endpoints, Lease, CiliumEndpoint, ConfigMap/kube-root-ca.crt, ServiceAccount/default, service-account-token Secret regenerated by the control plane / CNI, not user desired-state
Virtual / computed metrics.k8s.io (PodMetrics/NodeMetrics), custom/external.metrics.k8s.io not stored (no watch verb), cannot be restored
Snapshot machinery CSI VolumeSnapshot, snapshot/content kinds created by the module itself self-referential
Module machinery the state-snapshotter.deckhouse.io group and state-snapshotter.deckhouse.io request/transfer kinds (VolumeCaptureRequest, VolumeRestoreRequest, DataExport, DataImport) internal execution objects

There is no special “exclude objects managed by Deckhouse” rule. Deckhouse-managed objects are filtered out by the same generic signals (controller-owned, control-plane noise, or module machinery). Everything else in the namespace — including resources you merely configured on top of modules — is treated as desired-state and captured.

Excluding objects from a snapshot

A Snapshot captures its whole namespace. To keep an object out of the capture, label it with state-snapshotter.deckhouse.io/exclude before creating the Snapshot — a capture is one-shot, so labeling an object later does not change an existing snapshot:

d8 k -n myns label configmap scratch-cache state-snapshotter.deckhouse.io/exclude=""

The label’s presence is what excludes the object; the value is ignored ("", true — anything works).

The exclusion works at every level of the snapshot tree:

  • A plain namespaced object (ConfigMap, Secret, …) is dropped from the captured namespace manifests.
  • A domain resource (for example a VirtualMachine) is not expanded into a child snapshot at all — its whole subtree is skipped.
  • An object inside a domain subtree (for example one VirtualDisk of a captured VM, or a companion Secret) is dropped alone; the owning node is still captured.
  • A PersistentVolumeClaim is not captured by the volume-data leg, and a user-created CSI VolumeSnapshot over an excluded PVC is left as a plain CSI snapshot — the module does not adopt it.

The exclusion is not inherited: excluding a domain resource removes its snapshot subtree, but objects the other capture legs see on their own — its PersistentVolumeClaims, companion objects — keep being captured unless they carry the label themselves.

An excluded object is recorded, not silently dropped: the excluding snapshot node lists it in status.captureState.domainSpecificController.excludedRefs (exact apiVersion/kind/name), and the bound SnapshotContent aggregates the whole tree’s exclusions in status.excludedRefs. Check there to confirm the exclusion took effect.

Like the built-in exclusions above, the label can only narrow the capture. There is no per-snapshot include list: a capture is always “the namespace minus exclusions”. To capture different subsets, use separate namespaces.

Creating a Snapshot

Create a Snapshot in the namespace you want to capture. The default mode (spec.mode: Capture) performs dynamic namespace capture:

d8 k apply -f - <<EOF
apiVersion: state-snapshotter.deckhouse.io/v1alpha1
kind: Snapshot
metadata:
  name: my-namespace-snapshot
  namespace: my-app
spec: {}
EOF

The spec is immutable. spec: {} selects dynamic capture of the my-app namespace. Do not add a target-namespace field — a Snapshot always captures its own namespace.

Tracking status

d8 k -n my-app get snap
NAME                    READY   REASON      CONTENT                        AGE
my-namespace-snapshot   True    Completed   nss-content-1f4b0c9d2a3e5f76   30s

Inspect conditions for details:

d8 k -n my-app get snap my-namespace-snapshot -o jsonpath='{.status.conditions}' | jq

Key status fields:

  • status.conditions[type=Ready] — overall readiness. Ready=True means the capture is complete and stored.
  • status.boundSnapshotContentName — the durable, cluster-scoped content holding the captured state.
  • status.childrenSnapshotRefs — child snapshots when domain controllers contribute a snapshot tree (e.g. virtual machines/disks).

Reading the captured state

Captured manifests are read through a controlled aggregated subresource (raw payloads in internal chunks are never exposed directly):

d8 k get --raw \
  "/apis/subresources.state-snapshotter.deckhouse.io/v1alpha1/namespaces/my-app/snapshots/my-namespace-snapshot/manifests-download" \
  | jq 'length'

manifests-download returns the manifests of that node only — it does not walk the snapshot tree — and returns them as they were captured, including status and other runtime fields. Read the rest of the tree one request per node: status.childrenSnapshotRefs names the direct children of the node you just read, each child is read the same way through the aggregated group of its own kind, and its own status.childrenSnapshotRefs continues the walk.

The group and the version to address a child with follow that child’s own kind, not the group you used for its parent. A child of kind CSI VolumeSnapshot — the node a plain PVC’s data is captured into — has its node endpoints served by the module that owns that kind: group subresources.storage-foundation.deckhouse.io, version v1, resource volumesnapshots ({name} below is the child’s name, taken from the parent’s status.childrenSnapshotRefs):

d8 k get --raw \
  "/apis/subresources.storage-foundation.deckhouse.io/v1/namespaces/my-app/volumesnapshots/{name}/manifests-download" \
  | jq 'length'

The endpoint names are the same in both groups, so the group, the version and the resource are the only things that change between a Snapshot node and a volume node. Mind the version — the Snapshot node group is v1alpha1 and the volume-node group is v1; addressing one with the other’s version does not fall back to anything, it is simply a path the API server does not serve. A node contributed by a domain module instead — a virtual machine and its disks, say — is served by that module’s own endpoint group, which is not the volume node’s and cannot be derived from it. The whole table of groups, versions and endpoints is in Backup integration.

Restoring

A captured namespace is restored through the module’s controlled read path — you do not create internal request objects (such as VolumeRestoreRequest) by hand; those are module machinery and are not exposed to users.

  • Manifests — read the captured objects node by node via the manifests-download endpoint above and apply them into the target namespace.

  • State with data — to restore objects together with their persistent data, read the snapshot through the data-restoration path /apis/subresources.state-snapshotter.deckhouse.io/v1alpha1/namespaces/{namespace}/snapshots/{name}/manifests-with-data-restoration (GET). Unlike manifests-download, it is recursive — the addressed node and its children come back in one response — and the manifests are cleaned up for applying: runtime metadata (uid, resourceVersion, ownerReferences, managedFields, …) and status are stripped, and the objects are addressed into the namespace of the snapshot you asked for.

    The recursion crosses the module boundary for you: a volume node inside the tree is compiled by the module that owns the VolumeSnapshot kind, and its objects come back in the same response — you address this endpoint once, on the Snapshot node, and never assemble the tree yourself. It is fail-closed: if any node of the addressed subtree is not ready, the request fails instead of answering with a partial namespace.

    There is no parameter for choosing where the objects land: they always come back addressed into the namespace of the Snapshot you asked for, and a request that tries to address them into any other namespace is refused with 400 rather than quietly answered in the snapshot’s own. To restore a namespace’s contents somewhere else, move the snapshot there first — download it and load it into the destination — and then read it there. A claim holding data could not be moved by re-addressing anyway: it reaches its data through PersistentVolumeClaim.spec.dataSourceRef, which carries no namespace and so is read inside the claim’s own, while the VolumeSnapshot it names stays with the snapshot.

    The body is a plain JSON array of objects — not a List object and not a YAML stream — so a client iterates the array and applies the elements itself instead of piping the body into apply -f -. The module materializes the data into that namespace internally; you consume the result through the aggregated API, not by creating data-transfer objects yourself.

Snapshot modes

How a Snapshot obtains its content is selected by spec.mode, immutable like the rest of the spec:

Mode spec Behavior
Capture (default) spec: {}, or spec.mode: Capture explicitly The controller captures the live namespace.
Import spec.mode: Import The Snapshot is materialized from an uploaded payload instead of the live namespace — used by cross-cluster migration / restore tooling. The live namespace is never captured.

Spell the field name exactly. The API server prunes unknown fields before validation, so a misspelled or obsolete field is not rejected: the object is accepted, the field is dropped, and the snapshot silently falls back to Capture — a live namespace capture where an import was intended. Read the mode back from the created object to confirm what was stored:

d8 k -n my-app get snap my-namespace-snapshot -o jsonpath='{.spec.mode}'

Lifecycle and retention

  • One-shot / immutable. The spec is frozen at creation; metadata.generation never advances; there is no re-capture.
  • Retention after deletion. The stored SnapshotContent is retained for a TTL (configured on the controller) after the Snapshot object is deleted, anchored by an ObjectKeeper. This lets a captured state outlive the request object. After the TTL expires, the content and its checkpoints are garbage-collected.
  • No background load. The module only does work in response to explicit requests; it runs no continuous background capture.

Delete protection

The internal objects that make up a snapshot tree are protected from accidental direct deletion by an admission delete-guard. This closes an incident class where deleting a child object (e.g. a managed CSI VolumeSnapshot) from a UI silently degrades the root Snapshot while the durable data still exists.

  • What is protected. The namespaced nodes inside a tree: child Snapshots and the managed CSI VolumeSnapshot. They carry the marker state-snapshotter.deckhouse.io/delete-protected: "true".
  • What is NOT protected. The root Snapshot is not marked — deleting it is the normal way to tear down the whole tree (the controllers then remove the internal objects for you). The cluster-scoped objects of a tree (SnapshotContent, ManifestCheckpoint and its chunks, ObjectKeeper, the managed CSI VolumeSnapshotContent) are not marked either: they are not reachable from a namespace, and damage there surfaces as a degraded tree rather than being blocked by admission. Objects that are not part of a snapshot tree are never affected.
  • Force-delete (break-glass). To delete a protected object directly, set the annotation deckhouse.io/allow-delete: "true" on it, then delete. The marker itself cannot be removed or changed by regular users; the annotation is the only supported override and is reversible until the delete happens.
  • Always enforced. There is no module setting that disables or weakens the guard. Use the break-glass annotation above for an exceptional direct deletion.

Notes and limitations

  • A Snapshot captures only namespaced resources in its own namespace; the cluster-scoped Namespace object itself is not captured.
  • Manifests are captured as-is (including status); field-level sanitization (stripping status, resourceVersion, uid, etc.) happens on the restore read-path, not on capture.
  • Secret objects are captured verbatim (their data is stored as-is), except service-account-token secrets, which are excluded by the inclusion rule. At-rest encryption of the snapshot store is a separate, future concern.