k8s.io/kubernetes@v1.29.3/pkg/controller/volume/persistentvolume/recycle_test.go (about) 1 /* 2 Copyright 2016 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 persistentvolume 18 19 import ( 20 "errors" 21 "testing" 22 23 v1 "k8s.io/api/core/v1" 24 storage "k8s.io/api/storage/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/component-helpers/storage/volume" 27 "k8s.io/klog/v2/ktesting" 28 pvtesting "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/testing" 29 ) 30 31 // Test single call to syncVolume, expecting recycling to happen. 32 // 1. Fill in the controller with initial data 33 // 2. Call the syncVolume *once*. 34 // 3. Compare resulting volumes with expected volumes. 35 func TestRecycleSync(t *testing.T) { 36 _, ctx := ktesting.NewTestContext(t) 37 runningPod := &v1.Pod{ 38 ObjectMeta: metav1.ObjectMeta{ 39 Name: "runningPod", 40 Namespace: testNamespace, 41 }, 42 Spec: v1.PodSpec{ 43 Volumes: []v1.Volume{ 44 { 45 Name: "vol1", 46 VolumeSource: v1.VolumeSource{ 47 PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ 48 ClaimName: "runningClaim", 49 }, 50 }, 51 }, 52 }, 53 }, 54 Status: v1.PodStatus{ 55 Phase: v1.PodRunning, 56 }, 57 } 58 59 pendingPod := runningPod.DeepCopy() 60 pendingPod.Name = "pendingPod" 61 pendingPod.Status.Phase = v1.PodPending 62 pendingPod.Spec.Volumes[0].PersistentVolumeClaim.ClaimName = "pendingClaim" 63 64 completedPod := runningPod.DeepCopy() 65 completedPod.Name = "completedPod" 66 completedPod.Status.Phase = v1.PodSucceeded 67 completedPod.Spec.Volumes[0].PersistentVolumeClaim.ClaimName = "completedClaim" 68 69 pods := []*v1.Pod{ 70 runningPod, 71 pendingPod, 72 completedPod, 73 } 74 75 tests := []controllerTest{ 76 { 77 // recycle volume bound by controller 78 name: "6-1 - successful recycle", 79 initialVolumes: newVolumeArray("volume6-1", "1Gi", "uid6-1", "claim6-1", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 80 expectedVolumes: newVolumeArray("volume6-1", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 81 initialClaims: noclaims, 82 expectedClaims: noclaims, 83 expectedEvents: noevents, 84 errors: noerrors, 85 // Inject recycler into the controller and call syncVolume. The 86 // recycler simulates one recycle() call that succeeds. 87 test: wrapTestWithReclaimCalls(operationRecycle, []error{nil}, testSyncVolume), 88 }, 89 { 90 // recycle volume bound by user 91 name: "6-2 - successful recycle with prebound volume", 92 initialVolumes: newVolumeArray("volume6-2", "1Gi", "uid6-2", "claim6-2", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 93 expectedVolumes: newVolumeArray("volume6-2", "1Gi", "", "claim6-2", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 94 initialClaims: noclaims, 95 expectedClaims: noclaims, 96 expectedEvents: noevents, 97 errors: noerrors, 98 // Inject recycler into the controller and call syncVolume. The 99 // recycler simulates one recycle() call that succeeds. 100 test: wrapTestWithReclaimCalls(operationRecycle, []error{nil}, testSyncVolume), 101 }, 102 { 103 // recycle failure - plugin not found 104 name: "6-3 - plugin not found", 105 initialVolumes: newVolumeArray("volume6-3", "1Gi", "uid6-3", "claim6-3", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 106 expectedVolumes: withMessage("No recycler plugin found for the volume!", newVolumeArray("volume6-3", "1Gi", "uid6-3", "claim6-3", v1.VolumeFailed, v1.PersistentVolumeReclaimRecycle, classEmpty)), 107 initialClaims: noclaims, 108 expectedClaims: noclaims, 109 expectedEvents: []string{"Warning VolumeFailedRecycle"}, 110 errors: noerrors, 111 test: testSyncVolume, 112 }, 113 { 114 // recycle failure - Recycle returns error 115 name: "6-4 - newRecycler returns error", 116 initialVolumes: newVolumeArray("volume6-4", "1Gi", "uid6-4", "claim6-4", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 117 expectedVolumes: withMessage("Recycle failed: Mock plugin error: no recycleCalls configured", newVolumeArray("volume6-4", "1Gi", "uid6-4", "claim6-4", v1.VolumeFailed, v1.PersistentVolumeReclaimRecycle, classEmpty)), 118 initialClaims: noclaims, 119 expectedClaims: noclaims, 120 expectedEvents: []string{"Warning VolumeFailedRecycle"}, 121 errors: noerrors, 122 test: wrapTestWithReclaimCalls(operationRecycle, []error{}, testSyncVolume), 123 }, 124 { 125 // recycle failure - recycle returns error 126 name: "6-5 - recycle returns error", 127 initialVolumes: newVolumeArray("volume6-5", "1Gi", "uid6-5", "claim6-5", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 128 expectedVolumes: withMessage("Recycle failed: Mock recycle error", newVolumeArray("volume6-5", "1Gi", "uid6-5", "claim6-5", v1.VolumeFailed, v1.PersistentVolumeReclaimRecycle, classEmpty)), 129 initialClaims: noclaims, 130 expectedClaims: noclaims, 131 expectedEvents: []string{"Warning VolumeFailedRecycle"}, 132 errors: noerrors, 133 test: wrapTestWithReclaimCalls(operationRecycle, []error{errors.New("Mock recycle error")}, testSyncVolume), 134 }, 135 { 136 // recycle success(?) - volume is deleted before doRecycle() starts 137 name: "6-6 - volume is deleted before recycling", 138 initialVolumes: newVolumeArray("volume6-6", "1Gi", "uid6-6", "claim6-6", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 139 expectedVolumes: novolumes, 140 initialClaims: noclaims, 141 expectedClaims: noclaims, 142 expectedEvents: noevents, 143 errors: noerrors, 144 test: wrapTestWithInjectedOperation(ctx, wrapTestWithReclaimCalls(operationRecycle, []error{}, testSyncVolume), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { 145 // Delete the volume before recycle operation starts 146 reactor.DeleteVolume("volume6-6") 147 }), 148 }, 149 { 150 // recycle success(?) - volume is recycled by previous recycler just 151 // at the time new doRecycle() starts. This simulates "volume no 152 // longer needs recycling, skipping". 153 name: "6-7 - volume is deleted before recycling", 154 initialVolumes: newVolumeArray("volume6-7", "1Gi", "uid6-7", "claim6-7", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 155 expectedVolumes: newVolumeArray("volume6-7", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 156 initialClaims: noclaims, 157 expectedClaims: noclaims, 158 expectedEvents: noevents, 159 errors: noerrors, 160 test: wrapTestWithInjectedOperation(ctx, wrapTestWithReclaimCalls(operationRecycle, []error{}, testSyncVolume), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { 161 // Mark the volume as Available before the recycler starts 162 reactor.MarkVolumeAvailable("volume6-7") 163 }), 164 }, 165 { 166 // recycle success(?) - volume bound by user is recycled by previous 167 // recycler just at the time new doRecycle() starts. This simulates 168 // "volume no longer needs recycling, skipping" with volume bound by 169 // user. 170 name: "6-8 - prebound volume is deleted before recycling", 171 initialVolumes: newVolumeArray("volume6-8", "1Gi", "uid6-8", "claim6-8", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 172 expectedVolumes: newVolumeArray("volume6-8", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 173 initialClaims: noclaims, 174 expectedClaims: noclaims, 175 expectedEvents: noevents, 176 errors: noerrors, 177 test: wrapTestWithInjectedOperation(ctx, wrapTestWithReclaimCalls(operationRecycle, []error{}, testSyncVolume), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { 178 // Mark the volume as Available before the recycler starts 179 reactor.MarkVolumeAvailable("volume6-8") 180 }), 181 }, 182 { 183 // recycle success - volume bound by user is recycled, while a new 184 // claim is created with another UID. 185 name: "6-9 - prebound volume is recycled while the claim exists", 186 initialVolumes: newVolumeArray("volume6-9", "1Gi", "uid6-9", "claim6-9", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 187 expectedVolumes: newVolumeArray("volume6-9", "1Gi", "", "claim6-9", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 188 initialClaims: newClaimArray("claim6-9", "uid6-9-x", "10Gi", "", v1.ClaimPending, nil), 189 expectedClaims: newClaimArray("claim6-9", "uid6-9-x", "10Gi", "", v1.ClaimPending, nil), 190 expectedEvents: noevents, 191 errors: noerrors, 192 // Inject recycler into the controller and call syncVolume. The 193 // recycler simulates one recycle() call that succeeds. 194 test: wrapTestWithReclaimCalls(operationRecycle, []error{nil}, testSyncVolume), 195 }, 196 { 197 // volume has unknown reclaim policy - failure expected 198 name: "6-10 - unknown reclaim policy", 199 initialVolumes: newVolumeArray("volume6-10", "1Gi", "uid6-10", "claim6-10", v1.VolumeBound, "Unknown", classEmpty), 200 expectedVolumes: withMessage("Volume has unrecognized PersistentVolumeReclaimPolicy", newVolumeArray("volume6-10", "1Gi", "uid6-10", "claim6-10", v1.VolumeFailed, "Unknown", classEmpty)), 201 initialClaims: noclaims, 202 expectedClaims: noclaims, 203 expectedEvents: []string{"Warning VolumeUnknownReclaimPolicy"}, 204 errors: noerrors, 205 test: testSyncVolume, 206 }, 207 { 208 // volume is used by a running pod - failure expected 209 name: "6-11 - used by running pod", 210 initialVolumes: newVolumeArray("volume6-11", "1Gi", "uid6-11", "runningClaim", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 211 expectedVolumes: newVolumeArray("volume6-11", "1Gi", "uid6-11", "runningClaim", v1.VolumeReleased, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 212 initialClaims: noclaims, 213 expectedClaims: noclaims, 214 expectedEvents: []string{"Normal VolumeFailedRecycle"}, 215 errors: noerrors, 216 test: testSyncVolume, 217 }, 218 { 219 // volume is used by a pending pod - failure expected 220 name: "6-12 - used by pending pod", 221 initialVolumes: newVolumeArray("volume6-12", "1Gi", "uid6-12", "pendingClaim", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 222 expectedVolumes: newVolumeArray("volume6-12", "1Gi", "uid6-12", "pendingClaim", v1.VolumeReleased, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 223 initialClaims: noclaims, 224 expectedClaims: noclaims, 225 expectedEvents: []string{"Normal VolumeFailedRecycle"}, 226 errors: noerrors, 227 test: testSyncVolume, 228 }, 229 { 230 // volume is used by a completed pod - recycle succeeds 231 name: "6-13 - used by completed pod", 232 initialVolumes: newVolumeArray("volume6-13", "1Gi", "uid6-13", "completedClaim", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 233 expectedVolumes: newVolumeArray("volume6-13", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 234 initialClaims: noclaims, 235 expectedClaims: noclaims, 236 expectedEvents: noevents, 237 errors: noerrors, 238 // Inject recycler into the controller and call syncVolume. The 239 // recycler simulates one recycle() call that succeeds. 240 test: wrapTestWithReclaimCalls(operationRecycle, []error{nil}, testSyncVolume), 241 }, 242 { 243 // volume is used by a completed pod, pod using claim with the same name bound to different pv is running, should recycle 244 name: "6-14 - seemingly used by running pod", 245 initialVolumes: newVolumeArray("volume6-14", "1Gi", "uid6-14", "completedClaim", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty, volume.AnnBoundByController), 246 expectedVolumes: newVolumeArray("volume6-14", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 247 initialClaims: newClaimArray("completedClaim", "uid6-14-x", "10Gi", "", v1.ClaimBound, nil), 248 expectedClaims: newClaimArray("completedClaim", "uid6-14-x", "10Gi", "", v1.ClaimBound, nil), 249 expectedEvents: noevents, 250 errors: noerrors, 251 test: wrapTestWithReclaimCalls(operationRecycle, []error{nil}, testSyncVolume), 252 }, 253 } 254 runSyncTests(t, ctx, tests, []*storage.StorageClass{}, pods) 255 } 256 257 // Test multiple calls to syncClaim/syncVolume and periodic sync of all 258 // volume/claims. The test follows this pattern: 259 // 0. Load the controller with initial data. 260 // 1. Call controllerTest.testCall() once as in TestSync() 261 // 2. For all volumes/claims changed by previous syncVolume/syncClaim calls, 262 // call appropriate syncVolume/syncClaim (simulating "volume/claim changed" 263 // events). Go to 2. if these calls change anything. 264 // 3. When all changes are processed and no new changes were made, call 265 // syncVolume/syncClaim on all volumes/claims (simulating "periodic sync"). 266 // 4. If some changes were done by step 3., go to 2. (simulation of 267 // "volume/claim updated" events, eventually performing step 3. again) 268 // 5. When 3. does not do any changes, finish the tests and compare final set 269 // of volumes/claims with expected claims/volumes and report differences. 270 // 271 // Some limit of calls in enforced to prevent endless loops. 272 func TestRecycleMultiSync(t *testing.T) { 273 _, ctx := ktesting.NewTestContext(t) 274 tests := []controllerTest{ 275 { 276 // recycle failure - recycle returns error. The controller should 277 // try again. 278 "7-1 - recycle returns error", 279 newVolumeArray("volume7-1", "1Gi", "uid7-1", "claim7-1", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classEmpty), 280 newVolumeArray("volume7-1", "1Gi", "", "claim7-1", v1.VolumeAvailable, v1.PersistentVolumeReclaimRecycle, classEmpty), 281 noclaims, 282 noclaims, 283 []string{"Warning VolumeFailedRecycle"}, noerrors, 284 wrapTestWithReclaimCalls(operationRecycle, []error{errors.New("Mock recycle error"), nil}, testSyncVolume), 285 }, 286 } 287 288 runMultisyncTests(t, ctx, tests, []*storage.StorageClass{}, "") 289 }