diff --git a/deploy/kubernetes/csi-snapshotter/rbac-csi-snapshotter.yaml b/deploy/kubernetes/csi-snapshotter/rbac-csi-snapshotter.yaml index 9ca4e00a7..141b93a88 100644 --- a/deploy/kubernetes/csi-snapshotter/rbac-csi-snapshotter.yaml +++ b/deploy/kubernetes/csi-snapshotter/rbac-csi-snapshotter.yaml @@ -35,10 +35,10 @@ rules: verbs: ["get", "list", "watch"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshotcontents"] - verbs: ["create", "get", "list", "watch", "update", "delete"] + verbs: ["create", "get", "list", "watch", "delete", "patch"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshotcontents/status"] - verbs: ["update"] + verbs: ["patch"] --- kind: ClusterRoleBinding diff --git a/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml b/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml index a6b8c7a9e..b3d5d248e 100644 --- a/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml +++ b/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml @@ -34,13 +34,16 @@ rules: verbs: ["get", "list", "watch"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshotcontents"] - verbs: ["create", "get", "list", "watch", "update", "delete"] + verbs: ["create", "get", "list", "watch", "delete", "patch"] + - apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotcontents/status"] + verbs: ["patch"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshots"] - verbs: ["get", "list", "watch", "update"] + verbs: ["get", "list", "watch", "patch"] - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshots/status"] - verbs: ["update"] + verbs: ["patch"] --- kind: ClusterRoleBinding diff --git a/go.mod b/go.mod index 53940ba56..055a31e08 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/container-storage-interface/spec v1.3.0 + github.com/evanphx/json-patch v4.9.0+incompatible github.com/fsnotify/fsnotify v1.4.9 github.com/go-logr/logr v0.3.0 // indirect github.com/golang/mock v1.4.4 diff --git a/pkg/common-controller/framework_test.go b/pkg/common-controller/framework_test.go index 81b340c38..b1e1d610d 100644 --- a/pkg/common-controller/framework_test.go +++ b/pkg/common-controller/framework_test.go @@ -17,8 +17,10 @@ limitations under the License. package common_controller import ( + "encoding/json" "errors" "fmt" + "reflect" sysruntime "runtime" "strconv" @@ -28,6 +30,7 @@ import ( "testing" "time" + patch "github.com/evanphx/json-patch" crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" clientset "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned/fake" @@ -255,6 +258,49 @@ func (r *snapshotReactor) React(action core.Action) (handled bool, ret runtime.O klog.V(4).Infof("saved updated content %s", content.Name) return true, content, nil + case action.Matches("patch", "volumesnapshotcontents"): + content := &crdv1.VolumeSnapshotContent{} + action := action.(core.PatchAction) + + // Check and bump object version + storedSnapshotContent, found := r.contents[action.GetName()] + if found { + // Apply patch + storedSnapshotBytes, err := json.Marshal(storedSnapshotContent) + if err != nil { + return true, nil, err + } + + mergedBytes, err := patch.MergePatch(storedSnapshotBytes, action.GetPatch()) + if err != nil { + return true, nil, err + } + if err = json.Unmarshal(mergedBytes, content); err != nil { + return true, nil, err + } + + storedVer, _ := strconv.Atoi(storedSnapshotContent.ResourceVersion) + + // Don't modify the existing object + content = content.DeepCopy() + content.ResourceVersion = strconv.Itoa(storedVer + 1) + + // If we were updating annotations and the new annotations are nil, leave as empty. + // This seems to be the behavior for merge-patching nil & empty annotations + if !reflect.DeepEqual(storedSnapshotContent.Annotations, content.Annotations) && content.Annotations == nil { + content.Annotations = make(map[string]string) + } + } else { + return true, nil, fmt.Errorf("cannot update snapshot content %s: snapshot content not found", action.GetName()) + } + + // Store the updated object to appropriate places. + r.contents[content.Name] = content + r.changedObjects = append(r.changedObjects, content) + r.changedSinceLastSync++ + klog.V(4).Infof("saved updated content %s", content.Name) + return true, content, nil + case action.Matches("update", "volumesnapshots"): obj := action.(core.UpdateAction).GetObject() snapshot := obj.(*crdv1.VolumeSnapshot) @@ -281,6 +327,69 @@ func (r *snapshotReactor) React(action core.Action) (handled bool, ret runtime.O klog.V(4).Infof("saved updated snapshot %s", snapshot.Name) return true, snapshot, nil + case action.Matches("patch", "volumesnapshots"): + snapshot := &crdv1.VolumeSnapshot{} + action := action.(core.PatchAction) + + // Check and bump object version + storedSnapshot, found := r.snapshots[action.GetName()] + if found { + // Apply patch + storedSnapshotBytes, err := json.Marshal(storedSnapshot) + if err != nil { + return true, nil, err + } + + mergedBytes, err := patch.MergePatch(storedSnapshotBytes, action.GetPatch()) + if err != nil { + return true, nil, err + } + if err = json.Unmarshal(mergedBytes, snapshot); err != nil { + return true, nil, err + } + + // We must re-assign the restore size as a new quantity. + // This is due to the behavior the json patch. The json serialization + // of a resource.Quantity is simply a number, i.e. + // "status": { + // "boundVolumeSnapshotContentName": "content4-4", + // "restoreSize": "5" + //} + // + // Upon de-serialization back into a resource.Quantity after + // the json merge occurs, this "restoreSize": "5" is parsed + // as a resource.DecimalSI of value 5, which is incorrect. + if snapshot.Status != nil && snapshot.Status.RestoreSize != nil { + size, ok := snapshot.Status.RestoreSize.AsInt64() + if !ok { + return true, nil, errors.New("failed to get restore size as int64") + } + + snapshot.Status.RestoreSize = resource.NewQuantity(size, resource.BinarySI) + } + + storedVer, _ := strconv.Atoi(storedSnapshot.ResourceVersion) + + // Don't modify the existing object + snapshot = snapshot.DeepCopy() + snapshot.ResourceVersion = strconv.Itoa(storedVer + 1) + + // If we were updating annotations and the new annotations are nil, leave as empty. + // This seems to be the behavior for merge-patching nil & empty annotations + if !reflect.DeepEqual(storedSnapshot.Annotations, snapshot.Annotations) && snapshot.Annotations == nil { + snapshot.Annotations = make(map[string]string) + } + } else { + return true, nil, fmt.Errorf("cannot update snapshot %s: snapshot not found", action.GetName()) + } + + // Store the updated object to appropriate places. + r.snapshots[snapshot.Name] = snapshot + r.changedObjects = append(r.changedObjects, snapshot) + r.changedSinceLastSync++ + klog.Infof("saved patched snapshot %s", snapshot.Name) + return true, snapshot, nil + case action.Matches("get", "volumesnapshotcontents"): name := action.(core.GetAction).GetName() content, found := r.contents[name] @@ -718,6 +827,8 @@ func newSnapshotReactor(kubeClient *kubefake.Clientset, client *fake.Clientset, client.AddReactor("delete", "volumesnapshotcontents", reactor.React) client.AddReactor("delete", "volumesnapshots", reactor.React) client.AddReactor("delete", "volumesnapshotclasses", reactor.React) + client.AddReactor("patch", "volumesnapshotcontents", reactor.React) + client.AddReactor("patch", "volumesnapshots", reactor.React) kubeClient.AddReactor("get", "persistentvolumeclaims", reactor.React) kubeClient.AddReactor("update", "persistentvolumeclaims", reactor.React) kubeClient.AddReactor("get", "persistentvolumes", reactor.React) @@ -1093,7 +1204,7 @@ func newVolumeArray(name, volumeUID, volumeHandle, capacity, boundToClaimUID, bo func newVolumeError(message string) *crdv1.VolumeSnapshotError { return &crdv1.VolumeSnapshotError{ - Time: &metav1.Time{}, + Time: nil, Message: &message, } } diff --git a/pkg/common-controller/snapshot_controller.go b/pkg/common-controller/snapshot_controller.go index 6f8693bda..33b7d8749 100644 --- a/pkg/common-controller/snapshot_controller.go +++ b/pkg/common-controller/snapshot_controller.go @@ -758,7 +758,7 @@ func (ctrl *csiSnapshotCommonController) storeContentUpdate(content interface{}) // given event on the snapshot. It saves the status and emits the event only when // the status has actually changed from the version saved in API server. // Parameters: -// snapshot - snapshot to update +// snapshot - snapshot to patch // setReadyToFalse bool - indicates whether to set the snapshot's ReadyToUse status to false. // if true, ReadyToUse will be set to false; // otherwise, ReadyToUse will not be changed. @@ -786,7 +786,7 @@ func (ctrl *csiSnapshotCommonController) updateSnapshotErrorStatusWithEvent(snap ready := false snapshotClone.Status.ReadyToUse = &ready } - newSnapshot, err := ctrl.clientset.SnapshotV1().VolumeSnapshots(snapshotClone.Namespace).UpdateStatus(context.TODO(), snapshotClone, metav1.UpdateOptions{}) + newSnapshot, err := utils.PatchVolumeSnapshot(snapshot, snapshotClone, ctrl.clientset, utils.StatusSubResource) // Emit the event even if the status update fails so that user can see the error ctrl.eventRecorder.Event(newSnapshot, eventtype, reason, message) @@ -810,7 +810,7 @@ func (ctrl *csiSnapshotCommonController) addContentFinalizer(content *crdv1.Volu contentClone := content.DeepCopy() contentClone.ObjectMeta.Finalizers = append(contentClone.ObjectMeta.Finalizers, utils.VolumeSnapshotContentFinalizer) - _, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().Update(context.TODO(), contentClone, metav1.UpdateOptions{}) + _, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset) if err != nil { return newControllerUpdateError(content.Name, err.Error()) } @@ -987,7 +987,8 @@ func (ctrl *csiSnapshotCommonController) checkandBindSnapshotContent(snapshot *c className := *(snapshot.Spec.VolumeSnapshotClassName) contentClone.Spec.VolumeSnapshotClassName = &className } - newContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().Update(context.TODO(), contentClone, metav1.UpdateOptions{}) + + newContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset) if err != nil { klog.V(4).Infof("updating VolumeSnapshotContent[%s] error status failed %v", contentClone.Name, err) return nil, err @@ -1163,7 +1164,7 @@ func (ctrl *csiSnapshotCommonController) updateSnapshotStatus(snapshot *crdv1.Vo ctrl.metricsManager.RecordMetrics(createAndReadyOperation, metrics.NewSnapshotOperationStatus(metrics.SnapshotStatusTypeSuccess), driverName) } - newSnapshotObj, err := ctrl.clientset.SnapshotV1().VolumeSnapshots(snapshotClone.Namespace).UpdateStatus(context.TODO(), snapshotClone, metav1.UpdateOptions{}) + newSnapshotObj, err := utils.PatchVolumeSnapshot(snapshot, snapshotClone, ctrl.clientset, utils.StatusSubResource) if err != nil { return nil, newControllerUpdateError(utils.SnapshotKey(snapshot), err.Error()) } @@ -1328,7 +1329,8 @@ func (ctrl *csiSnapshotCommonController) SetDefaultSnapshotClass(snapshot *crdv1 klog.V(5).Infof("setDefaultSnapshotClass [%s]: default VolumeSnapshotClassName [%s]", snapshot.Name, defaultClasses[0].Name) snapshotClone := snapshot.DeepCopy() snapshotClone.Spec.VolumeSnapshotClassName = &(defaultClasses[0].Name) - newSnapshot, err := ctrl.clientset.SnapshotV1().VolumeSnapshots(snapshotClone.Namespace).Update(context.TODO(), snapshotClone, metav1.UpdateOptions{}) + + newSnapshot, err := utils.PatchVolumeSnapshot(snapshot, snapshotClone, ctrl.clientset) if err != nil { klog.V(4).Infof("updating VolumeSnapshot[%s] default class failed %v", utils.SnapshotKey(snapshot), err) } @@ -1393,7 +1395,7 @@ func (ctrl *csiSnapshotCommonController) addSnapshotFinalizer(snapshot *crdv1.Vo if addBoundFinalizer { snapshotClone.ObjectMeta.Finalizers = append(snapshotClone.ObjectMeta.Finalizers, utils.VolumeSnapshotBoundFinalizer) } - _, err := ctrl.clientset.SnapshotV1().VolumeSnapshots(snapshotClone.Namespace).Update(context.TODO(), snapshotClone, metav1.UpdateOptions{}) + _, err := utils.PatchVolumeSnapshot(snapshot, snapshotClone, ctrl.clientset) if err != nil { return newControllerUpdateError(utils.SnapshotKey(snapshot), err.Error()) } @@ -1437,7 +1439,8 @@ func (ctrl *csiSnapshotCommonController) removeSnapshotFinalizer(snapshot *crdv1 if removeBoundFinalizer { snapshotClone.ObjectMeta.Finalizers = utils.RemoveString(snapshotClone.ObjectMeta.Finalizers, utils.VolumeSnapshotBoundFinalizer) } - _, err := ctrl.clientset.SnapshotV1().VolumeSnapshots(snapshotClone.Namespace).Update(context.TODO(), snapshotClone, metav1.UpdateOptions{}) + + _, err := utils.PatchVolumeSnapshot(snapshot, snapshotClone, ctrl.clientset) if err != nil { return newControllerUpdateError(snapshot.Name, err.Error()) } @@ -1483,9 +1486,10 @@ func (ctrl *csiSnapshotCommonController) setAnnVolumeSnapshotBeingDeleted(conten // Set AnnVolumeSnapshotBeingDeleted if it is not set yet if !metav1.HasAnnotation(content.ObjectMeta, utils.AnnVolumeSnapshotBeingDeleted) { klog.V(5).Infof("setAnnVolumeSnapshotBeingDeleted: set annotation [%s] on content [%s].", utils.AnnVolumeSnapshotBeingDeleted, content.Name) - metav1.SetMetaDataAnnotation(&content.ObjectMeta, utils.AnnVolumeSnapshotBeingDeleted, "yes") + contentClone := content.DeepCopy() + metav1.SetMetaDataAnnotation(&contentClone.ObjectMeta, utils.AnnVolumeSnapshotBeingDeleted, "yes") - updateContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().Update(context.TODO(), content, metav1.UpdateOptions{}) + updateContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset) if err != nil { return newControllerUpdateError(content.Name, err.Error()) } @@ -1525,7 +1529,8 @@ func (ctrl *csiSnapshotCommonController) checkAndSetInvalidContentLabel(content } contentClone.ObjectMeta.Labels[utils.VolumeSnapshotContentInvalidLabel] = "" } - updatedContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().Update(context.TODO(), contentClone, metav1.UpdateOptions{}) + + updatedContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset) if err != nil { return content, newControllerUpdateError(content.Name, err.Error()) } @@ -1567,7 +1572,7 @@ func (ctrl *csiSnapshotCommonController) checkAndSetInvalidSnapshotLabel(snapsho snapshotClone.ObjectMeta.Labels[utils.VolumeSnapshotInvalidLabel] = "" } - updatedSnapshot, err := ctrl.clientset.SnapshotV1().VolumeSnapshots(snapshot.Namespace).Update(context.TODO(), snapshotClone, metav1.UpdateOptions{}) + updatedSnapshot, err := utils.PatchVolumeSnapshot(snapshot, snapshotClone, ctrl.clientset) if err != nil { return snapshot, newControllerUpdateError(utils.SnapshotKey(snapshot), err.Error()) } diff --git a/pkg/common-controller/snapshot_create_test.go b/pkg/common-controller/snapshot_create_test.go index d07035088..1ea7f2f55 100644 --- a/pkg/common-controller/snapshot_create_test.go +++ b/pkg/common-controller/snapshot_create_test.go @@ -26,7 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var timeNow = time.Now() +var timeNow = time.Now().Round(time.Second) var timeNowStamp = timeNow.UnixNano() var False = false var True = true @@ -161,10 +161,10 @@ func TestCreateSnapshotSync(t *testing.T) { initialClaims: newClaimArray("claim7-9", "pvc-uid7-9", "1Gi", "volume7-9", v1.ClaimBound, &classGold), initialVolumes: newVolumeArray("volume7-9", "pv-uid7-9", "pv-handle7-9", "1Gi", "pvc-uid7-9", "claim7-9", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold), errors: []reactorError{ - {"update", "volumesnapshots", errors.New("mock update error")}, - {"update", "volumesnapshots", errors.New("mock update error")}, - {"update", "volumesnapshots", errors.New("mock update error")}, - {"update", "volumesnapshots", errors.New("mock update error")}, + {"patch", "volumesnapshots", errors.New("mock update error")}, + {"patch", "volumesnapshots", errors.New("mock update error")}, + {"patch", "volumesnapshots", errors.New("mock update error")}, + {"patch", "volumesnapshots", errors.New("mock update error")}, }, expectSuccess: false, test: testSyncSnapshot, diff --git a/pkg/common-controller/snapshot_delete_test.go b/pkg/common-controller/snapshot_delete_test.go index d780d8747..b2cc636ed 100644 --- a/pkg/common-controller/snapshot_delete_test.go +++ b/pkg/common-controller/snapshot_delete_test.go @@ -19,6 +19,7 @@ package common_controller import ( "errors" "testing" + "time" crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" "github.com/kubernetes-csi/external-snapshotter/v4/pkg/utils" @@ -49,7 +50,7 @@ var class5Parameters = map[string]string{ utils.PrefixedSnapshotterSecretNamespaceKey: "default", } -var timeNowMetav1 = metav1.Now() +var timeNowMetav1 = metav1.Time{Time: time.Now().Round(time.Second)} var content31 = "content3-1" var claim31 = "claim3-1" diff --git a/pkg/common-controller/snapshot_update_test.go b/pkg/common-controller/snapshot_update_test.go index 864fe81c2..1a2165967 100644 --- a/pkg/common-controller/snapshot_update_test.go +++ b/pkg/common-controller/snapshot_update_test.go @@ -19,7 +19,6 @@ package common_controller import ( "errors" "testing" - "time" crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" "github.com/kubernetes-csi/external-snapshotter/v4/pkg/utils" @@ -28,7 +27,7 @@ import ( ) var metaTimeNow = &metav1.Time{ - Time: time.Now(), + Time: timeNow, } var emptyString = "" @@ -117,9 +116,9 @@ func TestSync(t *testing.T) { initialVolumes: newVolumeArray("volume2-8", "pv-uid2-8", "pv-handle2-8", "1Gi", "pvc-uid2-8", "claim2-8", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classEmpty), initialSecrets: []*v1.Secret{secret()}, errors: []reactorError{ - // Inject error to the first client.VolumesnapshotV1().VolumeSnapshots().Update call. + // Inject error to the first client.VolumesnapshotV1().VolumeSnapshots().Patch call. // All other calls will succeed. - {"update", "volumesnapshots", errors.New("mock update error")}, + {"patch", "volumesnapshots", errors.New("mock update error")}, }, test: testSyncSnapshotError, }, @@ -162,7 +161,7 @@ func TestSync(t *testing.T) { expectedSnapshots: newSnapshotArray("snap2-12", "snapuid2-12", "", "content2-12", validSecretClass, "content2-12", &False, nil, nil, newVolumeError("Snapshot failed to bind VolumeSnapshotContent, mock update error"), false, true, nil), errors: []reactorError{ // Inject error to the forth client.VolumesnapshotV1().VolumeSnapshots().Update call. - {"update", "volumesnapshotcontents", errors.New("mock update error")}, + {"patch", "volumesnapshotcontents", errors.New("mock update error")}, }, test: testSyncSnapshot, }, @@ -312,7 +311,7 @@ func TestSync(t *testing.T) { initialSecrets: []*v1.Secret{secret()}, errors: []reactorError{ // Inject error to the forth client.VolumesnapshotV1().VolumeSnapshots().Update call. - {"update", "volumesnapshotcontents", errors.New("mock update error")}, + {"patch", "volumesnapshotcontents", errors.New("mock update error")}, }, expectSuccess: false, test: testSyncContentError, @@ -340,8 +339,8 @@ func TestSync(t *testing.T) { initialVolumes: newVolumeArray("volume5-4", "pv-uid5-4", "pv-handle5-4", "1Gi", "pvc-uid5-4", "claim5-4", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classEmpty), initialSecrets: []*v1.Secret{secret()}, errors: []reactorError{ - // Inject error to the forth client.VolumesnapshotV1().VolumeSnapshots().Update call. - {"update", "volumesnapshotcontents", errors.New("mock update error")}, + // Inject error to the forth client.VolumesnapshotV1().VolumeSnapshots().Patch call + {"patch", "volumesnapshots", errors.New("mock update error")}, }, expectSuccess: false, test: testSyncContentError, @@ -377,8 +376,8 @@ func TestSync(t *testing.T) { expectedContents: withContentAnnotations(newContentArray("content5-7", "snapuid5-7", "snap5-7", "sid5-7", validSecretClass, "sid5-7", "", deletionPolicy, nil, nil, true), map[string]string{utils.AnnVolumeSnapshotBeingDeleted: "yes"}), initialSecrets: []*v1.Secret{secret()}, errors: []reactorError{ - // Inject error to the forth client.VolumesnapshotV1().VolumeSnapshots().Update call. - {"update", "volumesnapshotcontents", errors.New("mock update error")}, + // Inject error to the forth client.VolumesnapshotV1().VolumeSnapshots().Patch call. + {"patch", "volumesnapshots", errors.New("mock update error")}, }, expectSuccess: false, test: testSyncContentError, diff --git a/pkg/sidecar-controller/framework_test.go b/pkg/sidecar-controller/framework_test.go index 7a8f0f0e8..1bda68cc0 100644 --- a/pkg/sidecar-controller/framework_test.go +++ b/pkg/sidecar-controller/framework_test.go @@ -15,6 +15,7 @@ package sidecar_controller import ( "context" + "encoding/json" "errors" "fmt" "reflect" @@ -25,6 +26,7 @@ import ( "testing" "time" + patch "github.com/evanphx/json-patch" crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" clientset "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned/fake" @@ -171,6 +173,50 @@ func (r *snapshotReactor) React(action core.Action) (handled bool, ret runtime.O // Test did not request to inject an error, continue simulating API server. switch { + + case action.Matches("patch", "volumesnapshotcontents"): + content := &crdv1.VolumeSnapshotContent{} + action := action.(core.PatchAction) + + // Check and bump object version + storedSnapshotContent, found := r.contents[action.GetName()] + if found { + // Apply patch + storedSnapshotContentBytes, err := json.Marshal(storedSnapshotContent) + if err != nil { + return true, nil, err + } + + mergedBytes, err := patch.MergePatch(storedSnapshotContentBytes, action.GetPatch()) + if err != nil { + return true, nil, err + } + if err = json.Unmarshal(mergedBytes, content); err != nil { + return true, nil, err + } + + storedVer, _ := strconv.Atoi(storedSnapshotContent.ResourceVersion) + + // Don't modify the existing object + content = content.DeepCopy() + content.ResourceVersion = strconv.Itoa(storedVer + 1) + + // If we were updating annotations and the new annotations are nil, leave as empty. + // This seems to be the behavior for merge-patching nil & empty annotations + if !reflect.DeepEqual(storedSnapshotContent.Annotations, content.Annotations) && content.Annotations == nil { + content.Annotations = make(map[string]string) + } + } else { + return true, nil, fmt.Errorf("cannot update snapshot content %s: snapshot content not found", action.GetName()) + } + + // Store the updated object to appropriate places. + r.contents[content.Name] = content + r.changedObjects = append(r.changedObjects, content) + r.changedSinceLastSync++ + klog.V(4).Infof("saved updated content %s", content.Name) + return true, content, nil + case action.Matches("create", "volumesnapshotcontents"): obj := action.(core.UpdateAction).GetObject() content := obj.(*crdv1.VolumeSnapshotContent) @@ -489,6 +535,8 @@ func newSnapshotReactor(kubeClient *kubefake.Clientset, client *fake.Clientset, client.AddReactor("update", "volumesnapshotcontents", reactor.React) client.AddReactor("get", "volumesnapshotcontents", reactor.React) client.AddReactor("delete", "volumesnapshotcontents", reactor.React) + client.AddReactor("patch", "volumesnapshot", reactor.React) + client.AddReactor("patch", "volumesnapshotcontents", reactor.React) kubeClient.AddReactor("get", "secrets", reactor.React) return reactor diff --git a/pkg/sidecar-controller/snapshot_controller.go b/pkg/sidecar-controller/snapshot_controller.go index b43c929b6..3292911a9 100644 --- a/pkg/sidecar-controller/snapshot_controller.go +++ b/pkg/sidecar-controller/snapshot_controller.go @@ -156,7 +156,7 @@ func (ctrl *csiSnapshotSideCarController) updateContentErrorStatusWithEvent(cont } ready := false contentClone.Status.ReadyToUse = &ready - newContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().UpdateStatus(context.TODO(), contentClone, metav1.UpdateOptions{}) + newContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset, utils.StatusSubResource) // Emit the event even if the status update fails so that user can see the error ctrl.eventRecorder.Event(newContent, eventtype, reason, message) @@ -370,13 +370,14 @@ func (ctrl *csiSnapshotSideCarController) clearVolumeContentStatus( if err != nil { return nil, fmt.Errorf("error get snapshot content %s from api server: %v", contentName, err) } - if content.Status != nil { - content.Status.SnapshotHandle = nil - content.Status.ReadyToUse = nil - content.Status.CreationTime = nil - content.Status.RestoreSize = nil + contentClone := content.DeepCopy() + if contentClone.Status != nil { + contentClone.Status.SnapshotHandle = nil + contentClone.Status.ReadyToUse = nil + contentClone.Status.CreationTime = nil + contentClone.Status.RestoreSize = nil } - newContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().UpdateStatus(context.TODO(), content, metav1.UpdateOptions{}) + newContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset, utils.StatusSubResource) if err != nil { return nil, newControllerUpdateError(contentName, err.Error()) } @@ -432,7 +433,7 @@ func (ctrl *csiSnapshotSideCarController) updateSnapshotContentStatus( if updated { contentClone := contentObj.DeepCopy() contentClone.Status = newStatus - newContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().UpdateStatus(context.TODO(), contentClone, metav1.UpdateOptions{}) + newContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset, utils.StatusSubResource) if err != nil { return nil, newControllerUpdateError(content.Name, err.Error()) } @@ -520,7 +521,7 @@ func (ctrl csiSnapshotSideCarController) removeContentFinalizer(content *crdv1.V contentClone := content.DeepCopy() contentClone.ObjectMeta.Finalizers = utils.RemoveString(contentClone.ObjectMeta.Finalizers, utils.VolumeSnapshotContentFinalizer) - _, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().Update(context.TODO(), contentClone, metav1.UpdateOptions{}) + _, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset) if err != nil { return newControllerUpdateError(content.Name, err.Error()) } @@ -577,7 +578,7 @@ func (ctrl *csiSnapshotSideCarController) setAnnVolumeSnapshotBeingCreated(conte contentClone := content.DeepCopy() metav1.SetMetaDataAnnotation(&contentClone.ObjectMeta, utils.AnnVolumeSnapshotBeingCreated, "yes") - updatedContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().Update(context.TODO(), contentClone, metav1.UpdateOptions{}) + updatedContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset) if err != nil { return newControllerUpdateError(content.Name, err.Error()) } @@ -603,7 +604,7 @@ func (ctrl csiSnapshotSideCarController) removeAnnVolumeSnapshotBeingCreated(con contentClone := content.DeepCopy() delete(contentClone.ObjectMeta.Annotations, utils.AnnVolumeSnapshotBeingCreated) - updatedContent, err := ctrl.clientset.SnapshotV1().VolumeSnapshotContents().Update(context.TODO(), contentClone, metav1.UpdateOptions{}) + updatedContent, err := utils.PatchVolumeSnapshotContent(content, contentClone, ctrl.clientset) if err != nil { return newControllerUpdateError(content.Name, err.Error()) } diff --git a/pkg/sidecar-controller/snapshot_delete_test.go b/pkg/sidecar-controller/snapshot_delete_test.go index 3daf5788f..3a3faa27a 100644 --- a/pkg/sidecar-controller/snapshot_delete_test.go +++ b/pkg/sidecar-controller/snapshot_delete_test.go @@ -34,7 +34,7 @@ var emptySize int64 var deletePolicy = crdv1.VolumeSnapshotContentDelete var retainPolicy = crdv1.VolumeSnapshotContentRetain var timeNow = time.Now() -var timeNowMetav1 = metav1.Now() +var timeNowMetav1 = metav1.Time{Time: time.Now().Round(time.Second)} var False = false var True = true @@ -345,5 +345,6 @@ func TestDeleteSync(t *testing.T) { test: testSyncContent, }, } + runSyncContentTests(t, tests, snapshotClasses) } diff --git a/pkg/utils/patch.go b/pkg/utils/patch.go new file mode 100644 index 000000000..bf289917b --- /dev/null +++ b/pkg/utils/patch.go @@ -0,0 +1,137 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package utils + +import ( + "context" + "encoding/json" + "fmt" + + patch "github.com/evanphx/json-patch" + crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + clientset "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +// PatchVolumeSnapshot patches a volume snapshot object +func PatchVolumeSnapshot( + existingSnapshot *crdv1.VolumeSnapshot, + updatedSnapshot *crdv1.VolumeSnapshot, + client clientset.Interface, + subresources ...string, +) (*crdv1.VolumeSnapshot, error) { + patch, err := createSnapshotPatch(existingSnapshot, updatedSnapshot) + if err != nil { + return updatedSnapshot, err + } + + newSnapshot, err := client.SnapshotV1().VolumeSnapshots(updatedSnapshot.Namespace).Patch(context.TODO(), existingSnapshot.Name, types.MergePatchType, patch, metav1.PatchOptions{}, subresources...) + if err != nil { + return updatedSnapshot, err + } + + return newSnapshot, nil +} + +// PatchVolumeSnapshotContent patches a volume snapshot content object +func PatchVolumeSnapshotContent( + existingSnapshotContent *crdv1.VolumeSnapshotContent, + updatedSnapshotContent *crdv1.VolumeSnapshotContent, + client clientset.Interface, + subresources ...string, +) (*crdv1.VolumeSnapshotContent, error) { + patch, err := createContentPatch(existingSnapshotContent, updatedSnapshotContent) + if err != nil { + return updatedSnapshotContent, err + } + + newSnapshotContent, err := client.SnapshotV1().VolumeSnapshotContents().Patch(context.TODO(), existingSnapshotContent.Name, types.MergePatchType, patch, metav1.PatchOptions{}, subresources...) + if err != nil { + return updatedSnapshotContent, err + } + + return newSnapshotContent, nil +} + +func addResourceVersion(patchBytes []byte, resourceVersion string) ([]byte, error) { + var patchMap map[string]interface{} + err := json.Unmarshal(patchBytes, &patchMap) + if err != nil { + return nil, fmt.Errorf("error unmarshalling patch: %v", err) + } + u := unstructured.Unstructured{Object: patchMap} + a, err := meta.Accessor(&u) + if err != nil { + return nil, fmt.Errorf("error creating accessor: %v", err) + } + a.SetResourceVersion(resourceVersion) + versionBytes, err := json.Marshal(patchMap) + if err != nil { + return nil, fmt.Errorf("error marshalling json patch: %v", err) + } + return versionBytes, nil +} + +func createSnapshotPatch(snapshot *crdv1.VolumeSnapshot, updatedSnapshot *crdv1.VolumeSnapshot) ([]byte, error) { + oldData, err := runtime.Encode(unstructured.UnstructuredJSONScheme, snapshot) + if err != nil { + return nil, fmt.Errorf("failed to marshal old data: %v", err) + } + newData, err := runtime.Encode(unstructured.UnstructuredJSONScheme, updatedSnapshot) + if err != nil { + return nil, fmt.Errorf("failed to marshal new data: %v", err) + } + + patchBytes, err := patch.CreateMergePatch(oldData, newData) + if err != nil { + return nil, fmt.Errorf("failed to create merge patch: %v", err) + } + + patchBytes, err = addResourceVersion(patchBytes, snapshot.ResourceVersion) + if err != nil { + return nil, fmt.Errorf("failed to add resource version: %v", err) + } + + return patchBytes, nil +} + +func createContentPatch(content *crdv1.VolumeSnapshotContent, updatedContent *crdv1.VolumeSnapshotContent) ([]byte, error) { + oldData, err := runtime.Encode(unstructured.UnstructuredJSONScheme, content) + if err != nil { + return nil, fmt.Errorf("failed to marshal old data: %v", err) + } + + newData, err := runtime.Encode(unstructured.UnstructuredJSONScheme, updatedContent) + if err != nil { + return nil, fmt.Errorf("failed to marshal new data: %v", err) + } + + patchBytes, err := patch.CreateMergePatch(oldData, newData) + if err != nil { + return nil, fmt.Errorf("failed to create merge patch: %v", err) + } + + patchBytes, err = addResourceVersion(patchBytes, content.ResourceVersion) + if err != nil { + return nil, fmt.Errorf("failed to add resource version: %v", err) + } + + return patchBytes, nil +} diff --git a/pkg/utils/patch_test.go b/pkg/utils/patch_test.go new file mode 100644 index 000000000..285118495 --- /dev/null +++ b/pkg/utils/patch_test.go @@ -0,0 +1,337 @@ +package utils + +import ( + "testing" + "time" + + crdv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var fixedTime = metav1.Time{Time: time.Date(2021, time.April, 1, 0, 0, 30, 0, time.UTC)} + +func TestCreateSnapshotPatch(t *testing.T) { + testcases := []struct { + name string + originalSnapshot *crdv1.VolumeSnapshot + updatedSnapshot *crdv1.VolumeSnapshot + + expectedPatch string + }{ + { + name: "1-1: empty case", + + originalSnapshot: &crdv1.VolumeSnapshot{}, + updatedSnapshot: &crdv1.VolumeSnapshot{}, + expectedPatch: `{}`, + }, + { + name: "1-2 patch new status", + + originalSnapshot: &crdv1.VolumeSnapshot{}, + updatedSnapshot: &crdv1.VolumeSnapshot{ + Status: &crdv1.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("test"), + }, + }, + expectedPatch: `{"status":{"boundVolumeSnapshotContentName":"test"}}`, + }, + { + name: "1-3: clear status", + + originalSnapshot: &crdv1.VolumeSnapshot{ + Status: &crdv1.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("test"), + CreationTime: nil, + ReadyToUse: nil, + RestoreSize: nil, + Error: nil, + }, + }, + updatedSnapshot: &crdv1.VolumeSnapshot{ + Status: &crdv1.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: nil, + CreationTime: nil, + ReadyToUse: nil, + RestoreSize: nil, + Error: nil, + }, + }, + // null json value unmarshals to a nil Go value. + // This is the expected patch for clearing a boundVolumeSnapshotContentName value. + expectedPatch: `{"status":{"boundVolumeSnapshotContentName":null}}`, + }, + { + name: "1-4: patch snapshotclass", + + originalSnapshot: &crdv1.VolumeSnapshot{ + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("old"), + }, + }, + updatedSnapshot: &crdv1.VolumeSnapshot{ + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("test"), + }, + }, + expectedPatch: `{"spec":{"volumeSnapshotClassName":"test"}}`, + }, + { + name: "1-5: patch snapshotclass no change", + originalSnapshot: &crdv1.VolumeSnapshot{ + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("test"), + }, + }, + updatedSnapshot: &crdv1.VolumeSnapshot{ + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("test"), + }, + }, + expectedPatch: `{}`, + }, + { + name: "1-6: patch snapshot status with non-nil values and other fields", + + originalSnapshot: &crdv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "snap-1-6", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-6"), + }, + Status: &crdv1.VolumeSnapshotStatus{ + ReadyToUse: boolPtr(false), + }, + }, + updatedSnapshot: &crdv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "snap-1-6", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-6"), + }, + Status: &crdv1.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("content-1-6"), + CreationTime: &fixedTime, + ReadyToUse: boolPtr(true), + RestoreSize: resource.NewQuantity(1, resource.BinarySI), + }, + }, + expectedPatch: `{"status":{"boundVolumeSnapshotContentName":"content-1-6","creationTime":"2021-04-01T00:00:30Z","readyToUse":true,"restoreSize":"1"}}`, + }, + { + name: "1-7: patch snapshot status with non-nil error and other fields", + + originalSnapshot: &crdv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "snap-1-7", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-7"), + }, + Status: &crdv1.VolumeSnapshotStatus{ + ReadyToUse: boolPtr(false), + }, + }, + updatedSnapshot: &crdv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "snap-1-7", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-7"), + }, + Status: &crdv1.VolumeSnapshotStatus{ + ReadyToUse: boolPtr(false), + Error: &crdv1.VolumeSnapshotError{ + Message: stringPtr("failed to patch"), + Time: &fixedTime, + }, + }, + }, + expectedPatch: `{"status":{"error":{"message":"failed to patch","time":"2021-04-01T00:00:30Z"}}}`, + }, + } + for _, tc := range testcases { + t.Logf("test: %v", tc.name) + + patch, err := createSnapshotPatch(tc.originalSnapshot, tc.updatedSnapshot) + if err != nil { + t.Fatalf("Encountered unexpected error: %v", err) + } + if string(patch) != tc.expectedPatch { + t.Errorf("Patch not equal to expected patch:\n GOT: %s\nEXPECTED: %s", patch, tc.expectedPatch) + } + } +} + +func TestCreateSnapshotContentPatch(t *testing.T) { + testcases := []struct { + name string + originalSnapshotContent *crdv1.VolumeSnapshotContent + updatedSnapshotContent *crdv1.VolumeSnapshotContent + + expectedPatch string + }{ + { + name: "1-1: empty case", + + originalSnapshotContent: &crdv1.VolumeSnapshotContent{}, + updatedSnapshotContent: &crdv1.VolumeSnapshotContent{}, + expectedPatch: `{}`, + }, + { + name: "1-2: patch new status", + + originalSnapshotContent: &crdv1.VolumeSnapshotContent{}, + updatedSnapshotContent: &crdv1.VolumeSnapshotContent{ + Status: &crdv1.VolumeSnapshotContentStatus{ + SnapshotHandle: stringPtr("test"), + }, + }, + expectedPatch: `{"status":{"snapshotHandle":"test"}}`, + }, + { + name: "1-3: clear status", + + originalSnapshotContent: &crdv1.VolumeSnapshotContent{ + Status: &crdv1.VolumeSnapshotContentStatus{ + SnapshotHandle: stringPtr("test"), + CreationTime: nil, + ReadyToUse: nil, + RestoreSize: nil, + Error: nil, + }, + }, + updatedSnapshotContent: &crdv1.VolumeSnapshotContent{ + Status: &crdv1.VolumeSnapshotContentStatus{ + SnapshotHandle: nil, + CreationTime: nil, + ReadyToUse: nil, + RestoreSize: nil, + Error: nil, + }, + }, + // null json value unmarshals to a nil Go value. + // This is the expected patch for clearing a snapshotHandle value. + expectedPatch: `{"status":{"snapshotHandle":null}}`, + }, + { + name: "1-4: patch snapshotclass", + + originalSnapshotContent: &crdv1.VolumeSnapshotContent{ + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("old"), + }, + }, + updatedSnapshotContent: &crdv1.VolumeSnapshotContent{ + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("test"), + }, + }, + expectedPatch: `{"spec":{"volumeSnapshotClassName":"test"}}`, + }, + { + name: "1-5: patch snapshotclass no change", + + originalSnapshotContent: &crdv1.VolumeSnapshotContent{ + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("test"), + }, + }, + updatedSnapshotContent: &crdv1.VolumeSnapshotContent{ + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("test"), + }, + }, + expectedPatch: `{}`, + }, + { + name: "1-6: patch snapshot content status with non-nil values and other fields", + + originalSnapshotContent: &crdv1.VolumeSnapshotContent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "content-1-6", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-6"), + }, + Status: &crdv1.VolumeSnapshotContentStatus{ + ReadyToUse: boolPtr(false), + }, + }, + updatedSnapshotContent: &crdv1.VolumeSnapshotContent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "content-1-6", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-6"), + }, + Status: &crdv1.VolumeSnapshotContentStatus{ + SnapshotHandle: stringPtr("snap-1-6"), + CreationTime: int64Ptr(100005000), + ReadyToUse: boolPtr(true), + RestoreSize: int64Ptr(500), + }, + }, + expectedPatch: `{"status":{"creationTime":100005000,"readyToUse":true,"restoreSize":500,"snapshotHandle":"snap-1-6"}}`, + }, + { + name: "1-7: patch snapshot content status with non-nil error and other fields", + + originalSnapshotContent: &crdv1.VolumeSnapshotContent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "content-1-7", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-7"), + }, + Status: &crdv1.VolumeSnapshotContentStatus{ + ReadyToUse: boolPtr(false), + }, + }, + updatedSnapshotContent: &crdv1.VolumeSnapshotContent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "content-1-7", + Namespace: "default", + }, + Spec: crdv1.VolumeSnapshotContentSpec{ + VolumeSnapshotClassName: stringPtr("snap-class-1-7"), + }, + Status: &crdv1.VolumeSnapshotContentStatus{ + ReadyToUse: boolPtr(false), + Error: &crdv1.VolumeSnapshotError{ + Message: stringPtr("failed to patch"), + Time: &fixedTime, + }, + }, + }, + expectedPatch: `{"status":{"error":{"message":"failed to patch","time":"2021-04-01T00:00:30Z"}}}`, + }, + } + for _, tc := range testcases { + t.Logf("test: %v", tc.name) + + patch, err := createContentPatch(tc.originalSnapshotContent, tc.updatedSnapshotContent) + if err != nil { + t.Fatalf("Encountered unexpected error: %v", err) + } + if string(patch) != tc.expectedPatch { + t.Errorf("Patch not equal to expected patch:\n GOT: %s\nEXPECTED: %s", patch, tc.expectedPatch) + } + } +} + +func stringPtr(s string) *string { return &s } + +func boolPtr(b bool) *bool { return &b } + +func int64Ptr(i int64) *int64 { return &i } diff --git a/pkg/utils/util.go b/pkg/utils/util.go index 632d397c4..0abfb2844 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -108,6 +108,10 @@ const ( // VolumeSnapshotInvalidLabel is applied to invalid snapshot as a label key. The value does not matter. // See https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/177-volume-snapshot/tighten-validation-webhook-crd.md#automatic-labelling-of-invalid-objects VolumeSnapshotInvalidLabel = "snapshot.storage.kubernetes.io/invalid-snapshot-resource" + + // StatusSubResource is the subresource used for interacting with + // a volumesnapshot or volumesnapshotcontent status subresource + StatusSubResource = "status" ) var SnapshotterSecretParams = secretParamsMap{ diff --git a/vendor/modules.txt b/vendor/modules.txt index e27b117a1..b3e35feca 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -10,6 +10,7 @@ github.com/container-storage-interface/spec/lib/go/csi # github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew/spew # github.com/evanphx/json-patch v4.9.0+incompatible +## explicit github.com/evanphx/json-patch # github.com/fsnotify/fsnotify v1.4.9 ## explicit