k8s.io/kubernetes@v1.29.3/pkg/controller/volume/expand/expand_controller_test.go (about)

     1  /*
     2  Copyright 2019 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 expand
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"reflect"
    24  	"regexp"
    25  	"testing"
    26  
    27  	v1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/resource"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    33  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    34  	"k8s.io/client-go/informers"
    35  	coretesting "k8s.io/client-go/testing"
    36  	csitrans "k8s.io/csi-translation-lib"
    37  	csitranslationplugins "k8s.io/csi-translation-lib/plugins"
    38  	"k8s.io/kubernetes/pkg/controller"
    39  	controllervolumetesting "k8s.io/kubernetes/pkg/controller/volume/attachdetach/testing"
    40  	"k8s.io/kubernetes/pkg/volume"
    41  	"k8s.io/kubernetes/pkg/volume/csimigration"
    42  	"k8s.io/kubernetes/pkg/volume/util"
    43  	"k8s.io/kubernetes/pkg/volume/util/operationexecutor"
    44  	volumetypes "k8s.io/kubernetes/pkg/volume/util/types"
    45  )
    46  
    47  func TestSyncHandler(t *testing.T) {
    48  	tests := []struct {
    49  		name               string
    50  		pvcKey             string
    51  		pv                 *v1.PersistentVolume
    52  		pvc                *v1.PersistentVolumeClaim
    53  		expansionCalled    bool
    54  		hasError           bool
    55  		expectedAnnotation map[string]string
    56  	}{
    57  		{
    58  			name:     "when pvc has no PV binding",
    59  			pvc:      getFakePersistentVolumeClaim("no-pv-pvc", "", "1Gi", "1Gi", ""),
    60  			pvcKey:   "default/no-pv-pvc",
    61  			hasError: true,
    62  		},
    63  		{
    64  			name:               "when pvc and pv has everything for in-tree plugin",
    65  			pv:                 getFakePersistentVolume("vol-3", csitranslationplugins.AWSEBSInTreePluginName, "1Gi", "good-pvc-vol-3"),
    66  			pvc:                getFakePersistentVolumeClaim("good-pvc", "vol-3", "1Gi", "2Gi", "good-pvc-vol-3"),
    67  			pvcKey:             "default/good-pvc",
    68  			expansionCalled:    false,
    69  			expectedAnnotation: map[string]string{volumetypes.VolumeResizerKey: csitranslationplugins.AWSEBSDriverName},
    70  		},
    71  		{
    72  			name: "if pv has pre-resize capacity annotation, generate expand operation should not be called",
    73  			pv: func() *v1.PersistentVolume {
    74  				pv := getFakePersistentVolume("vol-4", csitranslationplugins.AWSEBSInTreePluginName, "2Gi", "good-pvc-vol-4")
    75  				pv.ObjectMeta.Annotations = make(map[string]string)
    76  				pv.ObjectMeta.Annotations[util.AnnPreResizeCapacity] = "1Gi"
    77  				return pv
    78  			}(),
    79  			pvc:             getFakePersistentVolumeClaim("good-pvc", "vol-4", "2Gi", "2Gi", "good-pvc-vol-4"),
    80  			pvcKey:          "default/good-pvc",
    81  			expansionCalled: false,
    82  		},
    83  		{
    84  			name:            "for csi plugin without migration path",
    85  			pv:              getFakePersistentVolume("vol-5", "com.csi.ceph", "1Gi", "ceph-csi-pvc-vol-6"),
    86  			pvc:             getFakePersistentVolumeClaim("ceph-csi-pvc", "vol-5", "1Gi", "2Gi", "ceph-csi-pvc-vol-6"),
    87  			pvcKey:          "default/ceph-csi-pvc",
    88  			expansionCalled: false,
    89  			hasError:        false,
    90  		},
    91  	}
    92  
    93  	for _, tc := range tests {
    94  		test := tc
    95  		fakeKubeClient := controllervolumetesting.CreateTestClient()
    96  		informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
    97  		pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
    98  
    99  		pvc := test.pvc
   100  		if tc.pv != nil {
   101  			informerFactory.Core().V1().PersistentVolumes().Informer().GetIndexer().Add(tc.pv)
   102  		}
   103  
   104  		if tc.pvc != nil {
   105  			informerFactory.Core().V1().PersistentVolumeClaims().Informer().GetIndexer().Add(pvc)
   106  		}
   107  		allPlugins := []volume.VolumePlugin{}
   108  		translator := csitrans.New()
   109  		expc, err := NewExpandController(fakeKubeClient, pvcInformer, nil, allPlugins, translator, csimigration.NewPluginManager(translator, utilfeature.DefaultFeatureGate))
   110  		if err != nil {
   111  			t.Fatalf("error creating expand controller : %v", err)
   112  		}
   113  
   114  		var expController *expandController
   115  		expController, _ = expc.(*expandController)
   116  		var expansionCalled bool
   117  		expController.operationGenerator = operationexecutor.NewFakeOGCounter(func() volumetypes.OperationContext {
   118  			expansionCalled = true
   119  			return volumetypes.NewOperationContext(nil, nil, false)
   120  		})
   121  
   122  		if test.pv != nil {
   123  			fakeKubeClient.AddReactor("get", "persistentvolumes", func(action coretesting.Action) (bool, runtime.Object, error) {
   124  				return true, test.pv, nil
   125  			})
   126  		}
   127  		fakeKubeClient.AddReactor("patch", "persistentvolumeclaims", func(action coretesting.Action) (bool, runtime.Object, error) {
   128  			if action.GetSubresource() == "status" {
   129  				patchActionaction, _ := action.(coretesting.PatchAction)
   130  				pvc, err = applyPVCPatch(pvc, patchActionaction.GetPatch())
   131  				if err != nil {
   132  					return false, nil, err
   133  				}
   134  				return true, pvc, nil
   135  			}
   136  			return true, pvc, nil
   137  		})
   138  
   139  		err = expController.syncHandler(context.TODO(), test.pvcKey)
   140  		if err != nil && !test.hasError {
   141  			t.Fatalf("for: %s; unexpected error while running handler : %v", test.name, err)
   142  		}
   143  
   144  		if err == nil && test.hasError {
   145  			t.Fatalf("for: %s; unexpected success", test.name)
   146  		}
   147  		if expansionCalled != test.expansionCalled {
   148  			t.Fatalf("for: %s; expected expansionCalled to be %v but was %v", test.name, test.expansionCalled, expansionCalled)
   149  		}
   150  
   151  		if len(test.expectedAnnotation) != 0 && !reflect.DeepEqual(test.expectedAnnotation, pvc.Annotations) {
   152  			t.Fatalf("for: %s; expected %v annotations, got %v", test.name, test.expectedAnnotation, pvc.Annotations)
   153  		}
   154  	}
   155  }
   156  
   157  func applyPVCPatch(originalPVC *v1.PersistentVolumeClaim, patch []byte) (*v1.PersistentVolumeClaim, error) {
   158  	pvcData, err := json.Marshal(originalPVC)
   159  	if err != nil {
   160  		return nil, fmt.Errorf("failed to marshal pvc with %v", err)
   161  	}
   162  	updated, err := strategicpatch.StrategicMergePatch(pvcData, patch, v1.PersistentVolumeClaim{})
   163  	if err != nil {
   164  		return nil, fmt.Errorf("failed to apply patch on pvc %v", err)
   165  	}
   166  	updatedPVC := &v1.PersistentVolumeClaim{}
   167  	if err := json.Unmarshal(updated, updatedPVC); err != nil {
   168  		return nil, fmt.Errorf("failed to unmarshal updated pvc : %v", err)
   169  	}
   170  	return updatedPVC, nil
   171  }
   172  
   173  func getFakePersistentVolume(volumeName, pluginName string, size string, pvcUID types.UID) *v1.PersistentVolume {
   174  	pv := &v1.PersistentVolume{
   175  		ObjectMeta: metav1.ObjectMeta{Name: volumeName},
   176  		Spec: v1.PersistentVolumeSpec{
   177  			PersistentVolumeSource: v1.PersistentVolumeSource{},
   178  			ClaimRef: &v1.ObjectReference{
   179  				Namespace: "default",
   180  			},
   181  			Capacity: map[v1.ResourceName]resource.Quantity{
   182  				v1.ResourceStorage: resource.MustParse(size),
   183  			},
   184  		},
   185  	}
   186  	if pvcUID != "" {
   187  		pv.Spec.ClaimRef.UID = pvcUID
   188  	}
   189  
   190  	if matched, _ := regexp.MatchString(`csi`, pluginName); matched {
   191  		pv.Spec.PersistentVolumeSource.CSI = &v1.CSIPersistentVolumeSource{
   192  			Driver:       pluginName,
   193  			VolumeHandle: volumeName,
   194  		}
   195  	} else {
   196  		pv.Spec.PersistentVolumeSource.AWSElasticBlockStore = &v1.AWSElasticBlockStoreVolumeSource{
   197  			VolumeID: volumeName,
   198  			FSType:   "ext4",
   199  		}
   200  	}
   201  	return pv
   202  }
   203  
   204  func getFakePersistentVolumeClaim(pvcName, volumeName, statusSize, requestSize string, uid types.UID) *v1.PersistentVolumeClaim {
   205  	pvc := &v1.PersistentVolumeClaim{
   206  		ObjectMeta: metav1.ObjectMeta{Name: pvcName, Namespace: "default", UID: uid},
   207  		Spec: v1.PersistentVolumeClaimSpec{
   208  			Resources: v1.VolumeResourceRequirements{
   209  				Requests: map[v1.ResourceName]resource.Quantity{
   210  					v1.ResourceStorage: resource.MustParse(requestSize),
   211  				},
   212  			},
   213  		},
   214  		Status: v1.PersistentVolumeClaimStatus{
   215  			Capacity: map[v1.ResourceName]resource.Quantity{
   216  				v1.ResourceStorage: resource.MustParse(statusSize),
   217  			},
   218  		},
   219  	}
   220  	if volumeName != "" {
   221  		pvc.Spec.VolumeName = volumeName
   222  	}
   223  
   224  	return pvc
   225  }