k8s.io/kubernetes@v1.29.3/test/e2e/storage/framework/snapshot_resource.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package framework
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	"github.com/onsi/ginkgo/v2"
    24  
    25  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	"k8s.io/apimachinery/pkg/util/uuid"
    31  	"k8s.io/kubernetes/test/e2e/framework"
    32  	"k8s.io/kubernetes/test/e2e/storage/utils"
    33  )
    34  
    35  // SnapshotResource represents a snapshot class, a snapshot and its bound snapshot contents for a specific test case
    36  type SnapshotResource struct {
    37  	Config  *PerTestConfig
    38  	Pattern TestPattern
    39  
    40  	Vs        *unstructured.Unstructured
    41  	Vscontent *unstructured.Unstructured
    42  	Vsclass   *unstructured.Unstructured
    43  }
    44  
    45  // CreateSnapshot creates a VolumeSnapshotClass with given SnapshotDeletionPolicy and a VolumeSnapshot
    46  // from the VolumeSnapshotClass using a dynamic client.
    47  // Returns the unstructured VolumeSnapshotClass and VolumeSnapshot objects.
    48  func CreateSnapshot(ctx context.Context, sDriver SnapshottableTestDriver, config *PerTestConfig, pattern TestPattern, pvcName string, pvcNamespace string, timeouts *framework.TimeoutContext, parameters map[string]string) (*unstructured.Unstructured, *unstructured.Unstructured) {
    49  	defer ginkgo.GinkgoRecover()
    50  	var err error
    51  	if pattern.SnapshotType != DynamicCreatedSnapshot && pattern.SnapshotType != PreprovisionedCreatedSnapshot {
    52  		err = fmt.Errorf("SnapshotType must be set to either DynamicCreatedSnapshot or PreprovisionedCreatedSnapshot")
    53  		framework.ExpectNoError(err)
    54  	}
    55  	dc := config.Framework.DynamicClient
    56  
    57  	ginkgo.By("creating a SnapshotClass")
    58  	sclass := sDriver.GetSnapshotClass(ctx, config, parameters)
    59  	if sclass == nil {
    60  		framework.Failf("Failed to get snapshot class based on test config")
    61  	}
    62  	sclass.Object["deletionPolicy"] = pattern.SnapshotDeletionPolicy.String()
    63  
    64  	sclass, err = dc.Resource(utils.SnapshotClassGVR).Create(ctx, sclass, metav1.CreateOptions{})
    65  	framework.ExpectNoError(err)
    66  
    67  	sclass, err = dc.Resource(utils.SnapshotClassGVR).Get(ctx, sclass.GetName(), metav1.GetOptions{})
    68  	framework.ExpectNoError(err)
    69  
    70  	ginkgo.By("creating a dynamic VolumeSnapshot")
    71  	// prepare a dynamically provisioned volume snapshot with certain data
    72  	snapshot := getSnapshot(pvcName, pvcNamespace, sclass.GetName())
    73  
    74  	snapshot, err = dc.Resource(utils.SnapshotGVR).Namespace(snapshot.GetNamespace()).Create(ctx, snapshot, metav1.CreateOptions{})
    75  	framework.ExpectNoError(err)
    76  
    77  	return sclass, snapshot
    78  }
    79  
    80  // CreateSnapshotResource creates a snapshot resource for the current test. It knows how to deal with
    81  // different test pattern snapshot provisioning and deletion policy
    82  func CreateSnapshotResource(ctx context.Context, sDriver SnapshottableTestDriver, config *PerTestConfig, pattern TestPattern, pvcName string, pvcNamespace string, timeouts *framework.TimeoutContext, parameters map[string]string) *SnapshotResource {
    83  	var err error
    84  	r := SnapshotResource{
    85  		Config:  config,
    86  		Pattern: pattern,
    87  	}
    88  	r.Vsclass, r.Vs = CreateSnapshot(ctx, sDriver, config, pattern, pvcName, pvcNamespace, timeouts, parameters)
    89  
    90  	dc := r.Config.Framework.DynamicClient
    91  
    92  	r.Vscontent = utils.GetSnapshotContentFromSnapshot(ctx, dc, r.Vs, timeouts.SnapshotCreate)
    93  
    94  	if pattern.SnapshotType == PreprovisionedCreatedSnapshot {
    95  		// prepare a pre-provisioned VolumeSnapshotContent with certain data
    96  		// Because this could be run with an external CSI driver, we have no way
    97  		// to pre-provision the snapshot as we normally would using their API.
    98  		// We instead dynamically take a snapshot (above step), delete the old snapshot,
    99  		// and create another snapshot using the first snapshot's snapshot handle.
   100  
   101  		ginkgo.By("updating the snapshot content deletion policy to retain")
   102  		r.Vscontent.Object["spec"].(map[string]interface{})["deletionPolicy"] = "Retain"
   103  
   104  		r.Vscontent, err = dc.Resource(utils.SnapshotContentGVR).Update(ctx, r.Vscontent, metav1.UpdateOptions{})
   105  		framework.ExpectNoError(err)
   106  
   107  		ginkgo.By("recording properties of the preprovisioned snapshot")
   108  		snapshotHandle := r.Vscontent.Object["status"].(map[string]interface{})["snapshotHandle"].(string)
   109  		framework.Logf("Recording snapshot content handle: %s", snapshotHandle)
   110  		snapshotContentAnnotations := r.Vscontent.GetAnnotations()
   111  		framework.Logf("Recording snapshot content annotations: %v", snapshotContentAnnotations)
   112  		csiDriverName := r.Vsclass.Object["driver"].(string)
   113  		framework.Logf("Recording snapshot driver: %s", csiDriverName)
   114  
   115  		// If the deletion policy is retain on vscontent:
   116  		// when vs is deleted vscontent will not be deleted
   117  		// when the vscontent is manually deleted then the underlying snapshot resource will not be deleted.
   118  		// We exploit this to create a snapshot resource from which we can create a preprovisioned snapshot
   119  		ginkgo.By("deleting the snapshot and snapshot content")
   120  		err = dc.Resource(utils.SnapshotGVR).Namespace(r.Vs.GetNamespace()).Delete(ctx, r.Vs.GetName(), metav1.DeleteOptions{})
   121  		if apierrors.IsNotFound(err) {
   122  			err = nil
   123  		}
   124  		framework.ExpectNoError(err)
   125  
   126  		ginkgo.By("checking the Snapshot has been deleted")
   127  		err = utils.WaitForNamespacedGVRDeletion(ctx, dc, utils.SnapshotGVR, r.Vs.GetName(), r.Vs.GetNamespace(), framework.Poll, timeouts.SnapshotDelete)
   128  		framework.ExpectNoError(err)
   129  
   130  		err = dc.Resource(utils.SnapshotContentGVR).Delete(ctx, r.Vscontent.GetName(), metav1.DeleteOptions{})
   131  		if apierrors.IsNotFound(err) {
   132  			err = nil
   133  		}
   134  		framework.ExpectNoError(err)
   135  
   136  		ginkgo.By("checking the Snapshot content has been deleted")
   137  		err = utils.WaitForGVRDeletion(ctx, dc, utils.SnapshotContentGVR, r.Vscontent.GetName(), framework.Poll, timeouts.SnapshotDelete)
   138  		framework.ExpectNoError(err)
   139  
   140  		ginkgo.By("creating a snapshot content with the snapshot handle")
   141  		uuid := uuid.NewUUID()
   142  
   143  		snapName := getPreProvisionedSnapshotName(uuid)
   144  		snapcontentName := getPreProvisionedSnapshotContentName(uuid)
   145  
   146  		r.Vscontent = getPreProvisionedSnapshotContent(snapcontentName, snapshotContentAnnotations, snapName, pvcNamespace, snapshotHandle, pattern.SnapshotDeletionPolicy.String(), csiDriverName)
   147  		r.Vscontent, err = dc.Resource(utils.SnapshotContentGVR).Create(ctx, r.Vscontent, metav1.CreateOptions{})
   148  		framework.ExpectNoError(err)
   149  
   150  		ginkgo.By("creating a snapshot with that snapshot content")
   151  		r.Vs = getPreProvisionedSnapshot(snapName, pvcNamespace, snapcontentName)
   152  		r.Vs, err = dc.Resource(utils.SnapshotGVR).Namespace(r.Vs.GetNamespace()).Create(ctx, r.Vs, metav1.CreateOptions{})
   153  		framework.ExpectNoError(err)
   154  
   155  		err = utils.WaitForSnapshotReady(ctx, dc, r.Vs.GetNamespace(), r.Vs.GetName(), framework.Poll, timeouts.SnapshotCreate)
   156  		framework.ExpectNoError(err)
   157  
   158  		ginkgo.By("getting the snapshot and snapshot content")
   159  		r.Vs, err = dc.Resource(utils.SnapshotGVR).Namespace(r.Vs.GetNamespace()).Get(ctx, r.Vs.GetName(), metav1.GetOptions{})
   160  		framework.ExpectNoError(err)
   161  
   162  		r.Vscontent, err = dc.Resource(utils.SnapshotContentGVR).Get(ctx, r.Vscontent.GetName(), metav1.GetOptions{})
   163  		framework.ExpectNoError(err)
   164  	}
   165  	return &r
   166  }
   167  
   168  // CleanupResource cleans up the snapshot resource and ignores not found errors
   169  func (sr *SnapshotResource) CleanupResource(ctx context.Context, timeouts *framework.TimeoutContext) error {
   170  	var err error
   171  	var cleanupErrs []error
   172  
   173  	dc := sr.Config.Framework.DynamicClient
   174  
   175  	if sr.Vs != nil {
   176  		framework.Logf("deleting snapshot %q/%q", sr.Vs.GetNamespace(), sr.Vs.GetName())
   177  
   178  		sr.Vs, err = dc.Resource(utils.SnapshotGVR).Namespace(sr.Vs.GetNamespace()).Get(ctx, sr.Vs.GetName(), metav1.GetOptions{})
   179  		switch {
   180  		case err == nil:
   181  			snapshotStatus := sr.Vs.Object["status"].(map[string]interface{})
   182  			snapshotContentName := snapshotStatus["boundVolumeSnapshotContentName"].(string)
   183  			framework.Logf("received snapshotStatus %v", snapshotStatus)
   184  			framework.Logf("snapshotContentName %s", snapshotContentName)
   185  
   186  			boundVsContent, err := dc.Resource(utils.SnapshotContentGVR).Get(ctx, snapshotContentName, metav1.GetOptions{})
   187  			switch {
   188  			case err == nil:
   189  				if boundVsContent.Object["spec"].(map[string]interface{})["deletionPolicy"] != "Delete" {
   190  					// The purpose of this block is to prevent physical snapshotContent leaks.
   191  					// We must update the SnapshotContent to have Delete Deletion policy,
   192  					// or else the physical snapshot content will be leaked.
   193  					boundVsContent.Object["spec"].(map[string]interface{})["deletionPolicy"] = "Delete"
   194  					boundVsContent, err = dc.Resource(utils.SnapshotContentGVR).Update(ctx, boundVsContent, metav1.UpdateOptions{})
   195  					framework.ExpectNoError(err)
   196  				}
   197  				err = dc.Resource(utils.SnapshotGVR).Namespace(sr.Vs.GetNamespace()).Delete(ctx, sr.Vs.GetName(), metav1.DeleteOptions{})
   198  				if apierrors.IsNotFound(err) {
   199  					err = nil
   200  				}
   201  				framework.ExpectNoError(err)
   202  
   203  				err = utils.WaitForGVRDeletion(ctx, dc, utils.SnapshotContentGVR, boundVsContent.GetName(), framework.Poll, timeouts.SnapshotDelete)
   204  				framework.ExpectNoError(err)
   205  
   206  			case apierrors.IsNotFound(err):
   207  				// the volume snapshot is not bound to snapshot content yet
   208  				err = dc.Resource(utils.SnapshotGVR).Namespace(sr.Vs.GetNamespace()).Delete(ctx, sr.Vs.GetName(), metav1.DeleteOptions{})
   209  				if apierrors.IsNotFound(err) {
   210  					err = nil
   211  				}
   212  				framework.ExpectNoError(err)
   213  
   214  				err = utils.WaitForNamespacedGVRDeletion(ctx, dc, utils.SnapshotGVR, sr.Vs.GetName(), sr.Vs.GetNamespace(), framework.Poll, timeouts.SnapshotDelete)
   215  				framework.ExpectNoError(err)
   216  			default:
   217  				cleanupErrs = append(cleanupErrs, err)
   218  			}
   219  		case apierrors.IsNotFound(err):
   220  			// Hope that the underlying snapshot content and resource is gone already
   221  		default:
   222  			cleanupErrs = append(cleanupErrs, err)
   223  		}
   224  	}
   225  	if sr.Vscontent != nil {
   226  		framework.Logf("deleting snapshot content %q", sr.Vscontent.GetName())
   227  
   228  		sr.Vscontent, err = dc.Resource(utils.SnapshotContentGVR).Get(ctx, sr.Vscontent.GetName(), metav1.GetOptions{})
   229  		switch {
   230  		case err == nil:
   231  			if sr.Vscontent.Object["spec"].(map[string]interface{})["deletionPolicy"] != "Delete" {
   232  				// The purpose of this block is to prevent physical snapshotContent leaks.
   233  				// We must update the SnapshotContent to have Delete Deletion policy,
   234  				// or else the physical snapshot content will be leaked.
   235  				sr.Vscontent.Object["spec"].(map[string]interface{})["deletionPolicy"] = "Delete"
   236  				sr.Vscontent, err = dc.Resource(utils.SnapshotContentGVR).Update(ctx, sr.Vscontent, metav1.UpdateOptions{})
   237  				framework.ExpectNoError(err)
   238  			}
   239  			err = dc.Resource(utils.SnapshotContentGVR).Delete(ctx, sr.Vscontent.GetName(), metav1.DeleteOptions{})
   240  			if apierrors.IsNotFound(err) {
   241  				err = nil
   242  			}
   243  			framework.ExpectNoError(err)
   244  
   245  			err = utils.WaitForGVRDeletion(ctx, dc, utils.SnapshotContentGVR, sr.Vscontent.GetName(), framework.Poll, timeouts.SnapshotDelete)
   246  			framework.ExpectNoError(err)
   247  		case apierrors.IsNotFound(err):
   248  			// Hope the underlying physical snapshot resource has been deleted already
   249  		default:
   250  			cleanupErrs = append(cleanupErrs, err)
   251  		}
   252  	}
   253  	if sr.Vsclass != nil {
   254  		framework.Logf("deleting snapshot class %q", sr.Vsclass.GetName())
   255  		// typically this snapshot class has already been deleted
   256  		err = dc.Resource(utils.SnapshotClassGVR).Delete(ctx, sr.Vsclass.GetName(), metav1.DeleteOptions{})
   257  		if err != nil && !apierrors.IsNotFound(err) {
   258  			framework.Failf("Error deleting snapshot class %q. Error: %v", sr.Vsclass.GetName(), err)
   259  		}
   260  		err = utils.WaitForGVRDeletion(ctx, dc, utils.SnapshotClassGVR, sr.Vsclass.GetName(), framework.Poll, timeouts.SnapshotDelete)
   261  		framework.ExpectNoError(err)
   262  	}
   263  	return utilerrors.NewAggregate(cleanupErrs)
   264  }
   265  
   266  func getSnapshot(claimName string, ns, snapshotClassName string) *unstructured.Unstructured {
   267  	snapshot := &unstructured.Unstructured{
   268  		Object: map[string]interface{}{
   269  			"kind":       "VolumeSnapshot",
   270  			"apiVersion": utils.SnapshotAPIVersion,
   271  			"metadata": map[string]interface{}{
   272  				"generateName": "snapshot-",
   273  				"namespace":    ns,
   274  			},
   275  			"spec": map[string]interface{}{
   276  				"volumeSnapshotClassName": snapshotClassName,
   277  				"source": map[string]interface{}{
   278  					"persistentVolumeClaimName": claimName,
   279  				},
   280  			},
   281  		},
   282  	}
   283  
   284  	return snapshot
   285  }
   286  func getPreProvisionedSnapshot(snapName, ns, snapshotContentName string) *unstructured.Unstructured {
   287  	snapshot := &unstructured.Unstructured{
   288  		Object: map[string]interface{}{
   289  			"kind":       "VolumeSnapshot",
   290  			"apiVersion": utils.SnapshotAPIVersion,
   291  			"metadata": map[string]interface{}{
   292  				"name":      snapName,
   293  				"namespace": ns,
   294  			},
   295  			"spec": map[string]interface{}{
   296  				"source": map[string]interface{}{
   297  					"volumeSnapshotContentName": snapshotContentName,
   298  				},
   299  			},
   300  		},
   301  	}
   302  
   303  	return snapshot
   304  }
   305  func getPreProvisionedSnapshotContent(snapcontentName string, snapshotContentAnnotations map[string]string, snapshotName, snapshotNamespace, snapshotHandle, deletionPolicy, csiDriverName string) *unstructured.Unstructured {
   306  	snapshotContent := &unstructured.Unstructured{
   307  		Object: map[string]interface{}{
   308  			"kind":       "VolumeSnapshotContent",
   309  			"apiVersion": utils.SnapshotAPIVersion,
   310  			"metadata": map[string]interface{}{
   311  				"name":        snapcontentName,
   312  				"annotations": snapshotContentAnnotations,
   313  			},
   314  			"spec": map[string]interface{}{
   315  				"source": map[string]interface{}{
   316  					"snapshotHandle": snapshotHandle,
   317  				},
   318  				"volumeSnapshotRef": map[string]interface{}{
   319  					"name":      snapshotName,
   320  					"namespace": snapshotNamespace,
   321  				},
   322  				"driver":         csiDriverName,
   323  				"deletionPolicy": deletionPolicy,
   324  			},
   325  		},
   326  	}
   327  
   328  	return snapshotContent
   329  }
   330  
   331  func getPreProvisionedSnapshotContentName(uuid types.UID) string {
   332  	return fmt.Sprintf("pre-provisioned-snapcontent-%s", string(uuid))
   333  }
   334  
   335  func getPreProvisionedSnapshotName(uuid types.UID) string {
   336  	return fmt.Sprintf("pre-provisioned-snapshot-%s", string(uuid))
   337  }