The module lifecycle stage: General 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:
- Discovers every namespaced resource type in the cluster via the Kubernetes discovery API (not a hardcoded list), so even arbitrary custom resources are captured.
- Lists the objects of each type in the snapshot’s namespace and selects the ones that represent user desired-state (see What gets captured).
- Stores the selected manifests in a durable, cluster-scoped artifact (
SnapshotContent+ an internalManifestCheckpoint, chunked for large payloads). - Publishes status: when the capture is complete and stored, the
SnapshotbecomesReady=Trueandstatus.boundSnapshotContentNamepoints at the stored content. - Retains the stored content after the
Snapshotis 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(exceptdefault), standalonePod(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
VirtualDiskof a captured VM, or a companionSecret) is dropped alone; the owning node is still captured. - A
PersistentVolumeClaimis not captured by the volume-data leg, and a user-created CSIVolumeSnapshotover 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: {}
EOFThe spec is immutable.
spec: {}selects dynamic capture of themy-appnamespace. Do not add a target-namespace field — aSnapshotalways captures its own namespace.
Tracking status
d8 k -n my-app get snapNAME 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}' | jqKey status fields:
status.conditions[type=Ready]— overall readiness.Ready=Truemeans 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-downloadendpoint 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). Unlikemanifests-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, …) andstatusare 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
VolumeSnapshotkind, and its objects come back in the same response — you address this endpoint once, on theSnapshotnode, 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
Snapshotyou asked for, and a request that tries to address them into any other namespace is refused with400rather 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 throughPersistentVolumeClaim.spec.dataSourceRef, which carries no namespace and so is read inside the claim’s own, while theVolumeSnapshotit names stays with the snapshot.The body is a plain JSON array of objects — not a
Listobject and not a YAML stream — so a client iterates the array and applies the elements itself instead of piping the body intoapply -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.generationnever advances; there is no re-capture. - Retention after deletion. The stored
SnapshotContentis retained for a TTL (configured on the controller) after theSnapshotobject is deleted, anchored by anObjectKeeper. 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 CSIVolumeSnapshot. They carry the markerstate-snapshotter.deckhouse.io/delete-protected: "true". - What is NOT protected. The root
Snapshotis 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,ManifestCheckpointand its chunks,ObjectKeeper, the managed CSIVolumeSnapshotContent) 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
Snapshotcaptures only namespaced resources in its own namespace; the cluster-scopedNamespaceobject itself is not captured. - Manifests are captured as-is (including
status); field-level sanitization (strippingstatus,resourceVersion,uid, etc.) happens on the restore read-path, not on capture. Secretobjects 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.