Skip to content

Commit a746787

Browse files
author
Neumoin, Konstantin
committed
fix: new logic VS/VCS
1 parent 01fbc27 commit a746787

File tree

6 files changed

+775
-71
lines changed

6 files changed

+775
-71
lines changed

images/snapshot-controller/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,13 @@ spec:
217217
type: object
218218
x-kubernetes-map-type: atomic
219219
x-kubernetes-validations:
220-
- message: both spec.volumeSnapshotRef.name and spec.volumeSnapshotRef.namespace
221-
must be set
222-
rule: has(self.name) && has(self.__namespace__)
220+
- message: if spec.volumeSnapshotRef is set, both name and namespace must be specified together, or volumeSnapshotRef must be completely empty (VSC-only model)
221+
rule: '(has(self.name) && has(self.__namespace__)) || (!has(self.name) && !has(self.__namespace__) && !has(self.uid))'
223222
required:
224223
- deletionPolicy
225224
- driver
226225
- source
227-
- volumeSnapshotRef
226+
# volumeSnapshotRef is optional to support VSC-only model (VSC without VolumeSnapshot)
228227
type: object
229228
x-kubernetes-validations:
230229
- message: sourceVolumeMode is required once set
@@ -382,7 +381,7 @@ spec:
382381
description: name of the VolumeSnapshotClass from which this snapshot was (or will be) created. Note that after provisioning, the VolumeSnapshotClass may be deleted or recreated with different set of values, and as such, should not be referenced post-snapshot creation.
383382
type: string
384383
volumeSnapshotRef:
385-
description: volumeSnapshotRef specifies the VolumeSnapshot object to which this VolumeSnapshotContent object is bound. VolumeSnapshot.Spec.VolumeSnapshotContentName field must reference to this VolumeSnapshotContent's name for the bidirectional binding to be valid. For a pre-existing VolumeSnapshotContent object, name and namespace of the VolumeSnapshot object MUST be provided for binding to happen. This field is immutable after creation. Required.
384+
description: volumeSnapshotRef specifies the VolumeSnapshot object to which this VolumeSnapshotContent object is bound. VolumeSnapshot.Spec.VolumeSnapshotContentName field must reference to this VolumeSnapshotContent's name for the bidirectional binding to be valid. For a pre-existing VolumeSnapshotContent object, name and namespace of the VolumeSnapshot object MUST be provided for binding to happen. This field is immutable after creation. Optional - can be empty for VSC-only model (VSC created without VolumeSnapshot, e.g., by VCR controller).
386385
properties:
387386
apiVersion:
388387
description: API version of the referent.
@@ -410,7 +409,7 @@ spec:
410409
- deletionPolicy
411410
- driver
412411
- source
413-
- volumeSnapshotRef
412+
# volumeSnapshotRef is optional to support VSC-only model (VSC without VolumeSnapshot)
414413
type: object
415414
status:
416415
description: status represents the current information of a snapshot.

images/snapshot-controller/pkg/common-controller/snapshot_controller.go

Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,15 @@ const (
8686
const controllerUpdateFailMsg = "snapshot controller failed to update"
8787

8888
// syncContent deals with one key off the queue
89+
// VSC-only model: This function now works with VolumeSnapshotContent independently,
90+
// without requiring VolumeSnapshot. VolumeSnapshotRef may be empty (VSC-only) or
91+
// present (legacy backward compatibility).
8992
func (ctrl *csiSnapshotCommonController) syncContent(content *crdv1.VolumeSnapshotContent) error {
90-
snapshotName := utils.SnapshotRefKey(&content.Spec.VolumeSnapshotRef)
91-
klog.V(4).Infof("synchronizing VolumeSnapshotContent[%s]: content is bound to snapshot %s", content.Name, snapshotName)
93+
klog.V(4).Infof("synchronizing VolumeSnapshotContent[%s]", content.Name)
9294

9395
klog.V(5).Infof("syncContent[%s]: check if we should add invalid label on content", content.Name)
9496

97+
// Validate that exactly one of VolumeHandle and SnapshotHandle is specified
9598
if (content.Spec.Source.VolumeHandle == nil && content.Spec.Source.SnapshotHandle == nil) ||
9699
(content.Spec.Source.VolumeHandle != nil && content.Spec.Source.SnapshotHandle != nil) {
97100
err := fmt.Errorf("Exactly one of VolumeHandle and SnapshotHandle should be specified")
@@ -100,71 +103,56 @@ func (ctrl *csiSnapshotCommonController) syncContent(content *crdv1.VolumeSnapsh
100103
return err
101104
}
102105

103-
// The VolumeSnapshotContent is reserved for a VolumeSnapshot;
104-
// that VolumeSnapshot has not yet been bound to this VolumeSnapshotContent;
105-
// syncSnapshot will handle it.
106-
if content.Spec.VolumeSnapshotRef.UID == "" {
107-
klog.V(4).Infof("syncContent [%s]: VolumeSnapshotContent is pre-bound to VolumeSnapshot %s", content.Name, snapshotName)
106+
// Check if this is VSC-only (VolumeSnapshotRef completely empty) or legacy (VolumeSnapshotRef set)
107+
isVSCOnly := content.Spec.VolumeSnapshotRef.UID == "" &&
108+
content.Spec.VolumeSnapshotRef.Name == "" &&
109+
content.Spec.VolumeSnapshotRef.Namespace == ""
110+
111+
if isVSCOnly {
112+
// VSC-only model: VolumeSnapshotRef is completely empty - this is normal.
113+
// VSC is the source of truth, no VolumeSnapshot involved.
114+
klog.V(4).Infof("syncContent [%s]: VolumeSnapshotContent is VSC-only (no VolumeSnapshotRef)", content.Name)
115+
116+
// VSC-only: Add finalizer if needed (same as legacy)
117+
if utils.NeedToAddContentFinalizer(content) {
118+
klog.V(5).Infof("syncContent [%s]: Add Finalizer for VolumeSnapshotContent (VSC-only)", content.Name)
119+
return ctrl.addContentFinalizer(content)
120+
}
121+
122+
// VSC-only: No VolumeSnapshot to manage, no status updates needed.
123+
// Sidecar-controller handles CSI operations and updates VSC.Status.
124+
// Deletion is handled by DeletionTimestamp on VSC itself.
108125
return nil
109126
}
110127

128+
// Legacy mode: VolumeSnapshotRef is set - maintain backward compatibility
129+
// Keep original behavior for legacy snapshots to avoid regressions
130+
klog.V(4).Infof("syncContent [%s]: VolumeSnapshotContent is legacy (has VolumeSnapshotRef)", content.Name)
131+
132+
// Legacy: Add finalizer if needed (same as VSC-only)
111133
if utils.NeedToAddContentFinalizer(content) {
112-
// Content is not being deleted -> it should have the finalizer.
113-
klog.V(5).Infof("syncContent [%s]: Add Finalizer for VolumeSnapshotContent", content.Name)
134+
klog.V(5).Infof("syncContent [%s]: Add Finalizer for VolumeSnapshotContent (legacy)", content.Name)
114135
return ctrl.addContentFinalizer(content)
115136
}
116137

117-
// Check if snapshot exists in cache store
118-
// If getSnapshotFromStore returns (nil, nil), it means snapshot not found
119-
// and it may have already been deleted, and it will fall into the
120-
// snapshot == nil case below
121-
var snapshot *crdv1.VolumeSnapshot
138+
// Legacy: Check if VolumeSnapshot exists and handle deletion
139+
// This preserves original behavior for backward compatibility
140+
snapshotName := utils.SnapshotRefKey(&content.Spec.VolumeSnapshotRef)
122141
snapshot, err := ctrl.getSnapshotFromStore(snapshotName)
123142
if err != nil {
124-
return err
143+
// Error getting snapshot - log but continue (may be transient)
144+
klog.V(4).Infof("syncContent [%s]: error getting snapshot %s (legacy mode): %v", content.Name, snapshotName, err)
145+
return nil
125146
}
126147

127-
if snapshot != nil && snapshot.UID != content.Spec.VolumeSnapshotRef.UID {
128-
// The snapshot that the content was pointing to was deleted, and another
129-
// with the same name created.
130-
klog.V(4).Infof("syncContent [%s]: snapshot %s has different UID, the old one must have been deleted", content.Name, snapshotName)
131-
// Treat the content as bound to a missing snapshot.
132-
snapshot = nil
133-
} else {
134-
// Check if snapshot.Status is different from content.Status and add snapshot to queue
135-
// if there is a difference and it is worth triggering an snapshot status update.
136-
if snapshot != nil && ctrl.needsUpdateSnapshotStatus(snapshot, content) {
137-
klog.V(4).Infof("synchronizing VolumeSnapshotContent for snapshot [%s]: update snapshot status to true if needed.", snapshotName)
138-
// Manually trigger a snapshot status update to happen
139-
// right away so that it is in-sync with the content status
140-
ctrl.snapshotQueue.Add(snapshotName)
148+
if snapshot != nil && snapshot.UID == content.Spec.VolumeSnapshotRef.UID {
149+
// Legacy: If snapshot has deletion timestamp, set annotation to trigger deletion
150+
if utils.IsSnapshotDeletionCandidate(snapshot) {
151+
_, err = ctrl.setAnnVolumeSnapshotBeingDeleted(content)
152+
return err
141153
}
142154
}
143155

144-
// NOTE(xyang): Do not trigger content deletion if
145-
// snapshot is nil. This is to avoid data loss if
146-
// the user copied the yaml files and expect it to work
147-
// in a different setup. In this case snapshot is nil.
148-
// If we trigger content deletion, it will delete
149-
// physical snapshot resource on the storage system
150-
// and result in data loss!
151-
//
152-
// Trigger content deletion if snapshot is not nil
153-
// and snapshot has deletion timestamp.
154-
// If snapshot has deletion timestamp and finalizers, set
155-
// AnnVolumeSnapshotBeingDeleted annotation on the content.
156-
// This may trigger the deletion of the content in the
157-
// sidecar controller depending on the deletion policy
158-
// on the content.
159-
// Snapshot won't be deleted until content is deleted
160-
// due to the finalizer.
161-
if snapshot != nil && utils.IsSnapshotDeletionCandidate(snapshot) {
162-
// Do not need to use the returned content here, as syncContent will get
163-
// the correct version from the cache next time. It is also not used after this.
164-
_, err = ctrl.setAnnVolumeSnapshotBeingDeleted(content)
165-
return err
166-
}
167-
168156
return nil
169157
}
170158

images/snapshot-controller/pkg/sidecar-controller/csi_handler.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,25 @@ func (handler *csiHandler) CreateSnapshot(content *crdv1.VolumeSnapshotContent,
7676
ctx, cancel := context.WithTimeout(context.Background(), handler.timeout)
7777
defer cancel()
7878

79-
if content.Spec.VolumeSnapshotRef.UID == "" {
80-
return "", "", time.Time{}, 0, false, fmt.Errorf("cannot create snapshot. Snapshot content %s not bound to a snapshot", content.Name)
79+
// VSC-only model: VolumeSnapshotRef.UID may be empty. In this case, use VSC.UID for snapshot name.
80+
// This allows VSC-only snapshots (created by VCR) to work without VolumeSnapshot.
81+
// NOTE: We use content.UID (not content.Name) to match legacy behavior where VolumeSnapshotRef.UID is used.
82+
// content.UID is stable for the lifetime of the object, but changes if VSC is recreated.
83+
// Alternative: Using content.Name would be more stable across VSC recreations, but would diverge from legacy behavior.
84+
var snapshotUID string
85+
if content.Spec.VolumeSnapshotRef.UID != "" {
86+
// Legacy mode: VolumeSnapshotRef is set - use it
87+
snapshotUID = string(content.Spec.VolumeSnapshotRef.UID)
88+
} else {
89+
// VSC-only mode: VolumeSnapshotRef is empty - use VSC.UID
90+
snapshotUID = string(content.UID)
8191
}
8292

8393
if content.Spec.Source.VolumeHandle == nil {
8494
return "", "", time.Time{}, 0, false, fmt.Errorf("cannot create snapshot. Volume handle not found in snapshot content %s", content.Name)
8595
}
8696

87-
snapshotName, err := makeSnapshotName(handler.snapshotNamePrefix, string(content.Spec.VolumeSnapshotRef.UID), handler.snapshotNameUUIDLength)
97+
snapshotName, err := makeSnapshotName(handler.snapshotNamePrefix, snapshotUID, handler.snapshotNameUUIDLength)
8898
if err != nil {
8999
return "", "", time.Time{}, 0, false, err
90100
}

images/snapshot-controller/pkg/sidecar-controller/framework_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ func (r *snapshotReactor) checkContents(expectedContents []*crdv1.VolumeSnapshot
333333
v := v.DeepCopy()
334334
v.ResourceVersion = ""
335335
v.Spec.VolumeSnapshotRef.ResourceVersion = ""
336+
// Clear DeletionTimestamp to avoid time precision issues in comparison
337+
v.DeletionTimestamp = nil
336338
if v.Status != nil {
337339
v.Status.CreationTime = nil
338340
}
@@ -347,6 +349,8 @@ func (r *snapshotReactor) checkContents(expectedContents []*crdv1.VolumeSnapshot
347349
v := v.DeepCopy()
348350
v.ResourceVersion = ""
349351
v.Spec.VolumeSnapshotRef.ResourceVersion = ""
352+
// Clear DeletionTimestamp to avoid time precision issues in comparison
353+
v.DeletionTimestamp = nil
350354
if v.Status != nil {
351355
v.Status.CreationTime = nil
352356
if v.Status.Error != nil {

images/snapshot-controller/pkg/sidecar-controller/snapshot_controller.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ func (ctrl *csiSnapshotSideCarController) syncContent(content *crdv1.VolumeSnaps
105105
}
106106
return false, nil
107107
}
108+
109+
// VSC-only model: If VolumeHandle is nil, we can't create snapshot and can't check status.
110+
// Skip processing such VSC (it may be pre-provisioned with SnapshotHandle, but that's handled separately).
111+
if content.Spec.Source.VolumeHandle == nil && (content.Status == nil || content.Status.SnapshotHandle == nil) {
112+
// No VolumeHandle and no SnapshotHandle - nothing to do
113+
klog.V(4).Infof("syncContent [%s]: skipping VSC without VolumeHandle and without SnapshotHandle", content.Name)
114+
return false, nil
115+
}
116+
108117
return ctrl.checkandUpdateContentStatus(content)
109118
}
110119

@@ -360,8 +369,13 @@ func (ctrl *csiSnapshotSideCarController) createSnapshotWrapper(content *crdv1.V
360369
return content, fmt.Errorf("failed to remove CSI Parameters of prefixed keys: %v", err)
361370
}
362371
if ctrl.extraCreateMetadata {
363-
parameters[utils.PrefixedVolumeSnapshotNameKey] = content.Spec.VolumeSnapshotRef.Name
364-
parameters[utils.PrefixedVolumeSnapshotNamespaceKey] = content.Spec.VolumeSnapshotRef.Namespace
372+
// VSC-only model: VolumeSnapshotRef may be empty. If it's set, use it for backward compatibility.
373+
// If empty, we still provide VSC name as metadata.
374+
if content.Spec.VolumeSnapshotRef.Name != "" {
375+
parameters[utils.PrefixedVolumeSnapshotNameKey] = content.Spec.VolumeSnapshotRef.Name
376+
parameters[utils.PrefixedVolumeSnapshotNamespaceKey] = content.Spec.VolumeSnapshotRef.Namespace
377+
}
378+
// Always provide VSC name - this is the source of truth in VSC-only model
365379
parameters[utils.PrefixedVolumeSnapshotContentNameKey] = content.Name
366380
}
367381

@@ -628,25 +642,35 @@ func (ctrl *csiSnapshotSideCarController) shouldDelete(content *crdv1.VolumeSnap
628642
if content.ObjectMeta.DeletionTimestamp == nil {
629643
return false
630644
}
631-
// 1) shouldDelete returns true if a content is not bound
632-
// (VolumeSnapshotRef.UID == "") for pre-provisioned snapshot
633-
if content.Spec.Source.SnapshotHandle != nil && content.Spec.VolumeSnapshotRef.UID == "" {
634-
return true
635-
}
636645

637646
// NOTE(xyang): Handle create snapshot timeout
638-
// 2) shouldDelete returns false if AnnVolumeSnapshotBeingCreated
647+
// shouldDelete returns false if AnnVolumeSnapshotBeingCreated
639648
// annotation is set. This indicates a CreateSnapshot CSI RPC has
640649
// not responded with success or failure.
641650
// We need to keep waiting for a response from the CSI driver.
642651
if metav1.HasAnnotation(content.ObjectMeta, utils.AnnVolumeSnapshotBeingCreated) {
643652
return false
644653
}
645654

646-
// 3) shouldDelete returns true if AnnVolumeSnapshotBeingDeleted annotation is set
655+
// Legacy: For pre-provisioned snapshots (SnapshotHandle in Source and VolumeSnapshotRef.UID == ""), delete immediately.
656+
if content.Spec.Source.SnapshotHandle != nil && content.Spec.VolumeSnapshotRef.UID == "" {
657+
return true
658+
}
659+
660+
// VSC-only or legacy: shouldDelete returns true if AnnVolumeSnapshotBeingDeleted annotation is set.
661+
// This annotation is set by common-controller when deletion should proceed.
647662
if metav1.HasAnnotation(content.ObjectMeta, utils.AnnVolumeSnapshotBeingDeleted) {
648663
return true
649664
}
665+
666+
// VSC-only model: If DeletionTimestamp is set and we have SnapshotHandle in status,
667+
// we can proceed with deletion. This covers VSC-only snapshots created by VCR.
668+
// Common-controller may not set the annotation for VSC-only, so we check status directly.
669+
if content.Status != nil && content.Status.SnapshotHandle != nil {
670+
// We have a snapshot to delete - proceed with deletion
671+
return true
672+
}
673+
650674
return false
651675
}
652676

0 commit comments

Comments
 (0)