github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/lorry/operations/volume/protect_test.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package volume 21 22 import ( 23 "context" 24 "encoding/json" 25 "fmt" 26 27 "github.com/golang/mock/gomock" 28 . "github.com/onsi/ginkgo/v2" 29 . "github.com/onsi/gomega" 30 "github.com/spf13/viper" 31 "k8s.io/apimachinery/pkg/util/rand" 32 statsv1alpha1 "k8s.io/kubelet/pkg/apis/stats/v1alpha1" 33 34 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 35 "github.com/1aal/kubeblocks/pkg/constant" 36 "github.com/1aal/kubeblocks/pkg/lorry/engines" 37 "github.com/1aal/kubeblocks/pkg/lorry/engines/register" 38 ) 39 40 type mockVolumeStatsRequester struct { 41 summary []byte 42 } 43 44 var _ volumeStatsRequester = &mockVolumeStatsRequester{} 45 46 func (r *mockVolumeStatsRequester) init(ctx context.Context) error { 47 return nil 48 } 49 50 func (r *mockVolumeStatsRequester) request(ctx context.Context) ([]byte, error) { 51 return r.summary, nil 52 } 53 54 type mockErrorVolumeStatsRequester struct { 55 initErr bool 56 requestErr bool 57 } 58 59 var _ volumeStatsRequester = &mockErrorVolumeStatsRequester{} 60 61 func (r *mockErrorVolumeStatsRequester) init(ctx context.Context) error { 62 if r.initErr { 63 return fmt.Errorf("error") 64 } 65 return nil 66 } 67 68 func (r *mockErrorVolumeStatsRequester) request(ctx context.Context) ([]byte, error) { 69 if r.requestErr { 70 return nil, fmt.Errorf("error") 71 } 72 return nil, nil 73 } 74 75 var _ = Describe("Volume Protection Operation", func() { 76 var ( 77 podName = rand.String(8) 78 volumeName = rand.String(8) 79 defaultThreshold = 90 80 zeroThreshold = 0 81 fullThreshold = 100 82 invalidThresholdLower = -1 83 invalidThresholdHigher = 101 84 capacityBytes = uint64(10 * 1024 * 1024 * 1024) 85 usedBytesUnderThreshold = capacityBytes * uint64(defaultThreshold-3) / 100 86 usedBytesOverThreshold = capacityBytes * uint64(defaultThreshold+3) / 100 87 usedBytesOverThresholdHigher = capacityBytes * uint64(defaultThreshold+4) / 100 88 volumeProtectionSpec = &appsv1alpha1.VolumeProtectionSpec{ 89 HighWatermark: defaultThreshold, 90 Volumes: []appsv1alpha1.ProtectedVolume{ 91 { 92 Name: volumeName, 93 HighWatermark: &defaultThreshold, 94 }, 95 }, 96 } 97 ) 98 99 setup := func() { 100 raw, _ := json.Marshal(volumeProtectionSpec) 101 viper.SetDefault(constant.KBEnvVolumeProtectionSpec, string(raw)) 102 viper.SetDefault(constant.KBEnvPodName, podName) 103 } 104 105 cleanAll := func() { 106 } 107 108 BeforeEach(setup) 109 110 AfterEach(cleanAll) 111 112 resetVolumeProtectionSpecEnv := func(spec appsv1alpha1.VolumeProtectionSpec) { 113 raw, _ := json.Marshal(spec) 114 viper.SetDefault(constant.KBEnvVolumeProtectionSpec, string(raw)) 115 } 116 117 newProtection := func() *Protection { 118 protection := &Protection{ 119 Requester: &mockVolumeStatsRequester{}, 120 } 121 Expect(protection.Init(context.Background())).Should(Succeed()) 122 protection.SendEvent = false 123 return protection 124 } 125 126 Context("Volume Protection", func() { 127 It("init - succeed", func() { 128 protection := &Protection{ 129 Requester: &mockVolumeStatsRequester{}, 130 } 131 Expect(protection.Init(context.Background())).Should(Succeed()) 132 Expect(protection.Pod).Should(Equal(podName)) 133 Expect(protection.HighWatermark).Should(Equal(volumeProtectionSpec.HighWatermark)) 134 Expect(len(protection.Volumes)).Should(Equal(len(volumeProtectionSpec.Volumes))) 135 }) 136 137 It("init - invalid volume protection spec env", func() { 138 viper.SetDefault(constant.KBEnvVolumeProtectionSpec, "") 139 protection := &Protection{ 140 Requester: &mockVolumeStatsRequester{}, 141 } 142 Expect(protection.initVolumes()).Should(HaveOccurred()) 143 }) 144 145 It("init - init requester error", func() { 146 protection := &Protection{ 147 Requester: &mockErrorVolumeStatsRequester{initErr: true}, 148 } 149 150 Expect(protection.Init(context.Background())).Should(HaveOccurred()) 151 }) 152 153 It("init - normalize watermark", func() { 154 By("normalize global watermark") 155 for _, val := range []int{-1, 0, 101} { 156 resetVolumeProtectionSpecEnv(appsv1alpha1.VolumeProtectionSpec{ 157 HighWatermark: val, 158 }) 159 obj := newProtection() 160 Expect(obj.initVolumes()).Should(Succeed()) 161 Expect(obj.HighWatermark).Should(Equal(0)) 162 } 163 164 By("normalize volume watermark") 165 spec := appsv1alpha1.VolumeProtectionSpec{ 166 HighWatermark: defaultThreshold, 167 Volumes: []appsv1alpha1.ProtectedVolume{ 168 { 169 Name: "01", 170 HighWatermark: &invalidThresholdLower, 171 }, 172 { 173 Name: "02", 174 HighWatermark: &invalidThresholdHigher, 175 }, 176 { 177 Name: "03", 178 HighWatermark: &zeroThreshold, 179 }, 180 { 181 Name: "04", 182 HighWatermark: &fullThreshold, 183 }, 184 }, 185 } 186 resetVolumeProtectionSpecEnv(spec) 187 obj := newProtection() 188 Expect(obj.initVolumes()).Should(Succeed()) 189 Expect(obj.HighWatermark).Should(Equal(spec.HighWatermark)) 190 for _, v := range spec.Volumes { 191 if *v.HighWatermark >= 0 && *v.HighWatermark <= 100 { 192 Expect(obj.Volumes[v.Name].HighWatermark).Should(Equal(*v.HighWatermark)) 193 } else { 194 Expect(obj.Volumes[v.Name].HighWatermark).Should(Equal(obj.HighWatermark)) 195 } 196 } 197 }) 198 199 It("disabled - empty pod name", func() { 200 viper.SetDefault(constant.KBEnvPodName, "") 201 obj := newProtection() 202 obj.Pod = viper.GetString(constant.KBEnvPodName) 203 Expect(obj.disabled()).Should(BeTrue()) 204 _, err := obj.Do(context.Background(), nil) 205 Expect(err).ShouldNot(HaveOccurred()) 206 }) 207 208 It("disabled - empty volume", func() { 209 resetVolumeProtectionSpecEnv(appsv1alpha1.VolumeProtectionSpec{ 210 HighWatermark: defaultThreshold, 211 Volumes: []appsv1alpha1.ProtectedVolume{}, 212 }) 213 obj := newProtection() 214 obj.Volumes = nil 215 Expect(obj.initVolumes()).Should(Succeed()) 216 Expect(obj.disabled()).Should(BeTrue()) 217 _, err := obj.Do(context.Background(), nil) 218 Expect(err).ShouldNot(HaveOccurred()) 219 }) 220 221 It("disabled - volumes with zero watermark", func() { 222 resetVolumeProtectionSpecEnv(appsv1alpha1.VolumeProtectionSpec{ 223 HighWatermark: defaultThreshold, 224 Volumes: []appsv1alpha1.ProtectedVolume{ 225 { 226 Name: "data", 227 HighWatermark: &zeroThreshold, 228 }, 229 }, 230 }) 231 obj := newProtection() 232 obj.Volumes = nil 233 Expect(obj.initVolumes()).Should(Succeed()) 234 Expect(obj.disabled()).Should(BeTrue()) 235 _, err := obj.Do(context.Background(), nil) 236 Expect(err).ShouldNot(HaveOccurred()) 237 238 }) 239 240 It("query stats summary - request error", func() { 241 obj := newProtection() 242 obj.Requester = &mockErrorVolumeStatsRequester{requestErr: true} 243 _, err := obj.Do(context.Background(), nil) 244 Expect(err).Should(HaveOccurred()) 245 }) 246 247 It("query stats summary - format error", func() { 248 // default summary is empty string 249 obj := newProtection() 250 _, err := obj.Do(context.Background(), nil) 251 Expect(err).Should(HaveOccurred()) 252 }) 253 254 It("query stats summary - ok", func() { 255 obj := newProtection() 256 mock := obj.Requester.(*mockVolumeStatsRequester) 257 stats := statsv1alpha1.Summary{} 258 mock.summary, _ = json.Marshal(stats) 259 Expect(obj.Init(context.Background())).Should(Succeed()) 260 261 _, err := obj.Do(context.Background(), nil) 262 Expect(err).ShouldNot(HaveOccurred()) 263 }) 264 265 It("update volume stats summary", func() { 266 obj := newProtection() 267 mock := obj.Requester.(*mockVolumeStatsRequester) 268 stats := statsv1alpha1.Summary{ 269 Pods: []statsv1alpha1.PodStats{ 270 { 271 PodRef: statsv1alpha1.PodReference{ 272 Name: podName, 273 }, 274 VolumeStats: []statsv1alpha1.VolumeStats{ 275 { 276 Name: volumeName, 277 FsStats: statsv1alpha1.FsStats{ 278 CapacityBytes: &capacityBytes, 279 UsedBytes: &usedBytesUnderThreshold, 280 }, 281 }, 282 }, 283 }, 284 }, 285 } 286 mock.summary, _ = json.Marshal(stats) 287 288 // nil capacity and used bytes 289 stats.Pods[0].VolumeStats[0].CapacityBytes = nil 290 stats.Pods[0].VolumeStats[0].UsedBytes = nil 291 292 _, err := obj.Do(context.Background(), nil) 293 Expect(err).ShouldNot(HaveOccurred()) 294 295 stats.Pods[0].VolumeStats[0].CapacityBytes = &capacityBytes 296 stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesUnderThreshold 297 _, err = obj.Do(context.Background(), nil) 298 Expect(err).ShouldNot(HaveOccurred()) 299 Expect(obj.Volumes[volumeName].Stats).Should(Equal(stats.Pods[0].VolumeStats[0])) 300 Expect(obj.Readonly).Should(BeFalse()) 301 }) 302 303 It("volume over high watermark", func() { 304 ctrl := gomock.NewController(GinkgoT()) 305 mockDBManager := engines.NewMockDBManager(ctrl) 306 mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil) 307 register.SetDBManager(mockDBManager) 308 309 obj := newProtection() 310 mock := obj.Requester.(*mockVolumeStatsRequester) 311 stats := statsv1alpha1.Summary{ 312 Pods: []statsv1alpha1.PodStats{ 313 { 314 PodRef: statsv1alpha1.PodReference{ 315 Name: podName, 316 }, 317 VolumeStats: []statsv1alpha1.VolumeStats{ 318 { 319 Name: volumeName, 320 FsStats: statsv1alpha1.FsStats{ 321 CapacityBytes: &capacityBytes, 322 UsedBytes: &usedBytesOverThreshold, 323 }, 324 }, 325 }, 326 }, 327 }, 328 } 329 mock.summary, _ = json.Marshal(stats) 330 331 _, err := obj.Do(context.Background(), nil) 332 Expect(err).ShouldNot(HaveOccurred()) 333 Expect(obj.Readonly).Should(BeTrue()) 334 335 // check again and the usage is higher, no lock action triggered again 336 stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesOverThresholdHigher 337 mock.summary, _ = json.Marshal(stats) 338 _, err = obj.Do(context.Background(), nil) 339 Expect(err).ShouldNot(HaveOccurred()) 340 Expect(obj.Readonly).Should(BeTrue()) 341 }) 342 343 It("volume under high watermark", func() { 344 ctrl := gomock.NewController(GinkgoT()) 345 mockDBManager := engines.NewMockDBManager(ctrl) 346 mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil) 347 mockDBManager.EXPECT().Unlock(gomock.Any()).Return(nil) 348 register.SetDBManager(mockDBManager) 349 350 obj := newProtection() 351 mock := obj.Requester.(*mockVolumeStatsRequester) 352 stats := statsv1alpha1.Summary{ 353 Pods: []statsv1alpha1.PodStats{ 354 { 355 PodRef: statsv1alpha1.PodReference{ 356 Name: podName, 357 }, 358 VolumeStats: []statsv1alpha1.VolumeStats{ 359 { 360 Name: volumeName, 361 FsStats: statsv1alpha1.FsStats{ 362 CapacityBytes: &capacityBytes, 363 UsedBytes: &usedBytesOverThreshold, 364 }, 365 }, 366 }, 367 }, 368 }, 369 } 370 mock.summary, _ = json.Marshal(stats) 371 _, err := obj.Do(context.Background(), nil) 372 Expect(err).ShouldNot(HaveOccurred()) 373 Expect(obj.Readonly).Should(BeTrue()) 374 375 // drops down the usage, and trigger unlock action 376 stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesUnderThreshold 377 mock.summary, _ = json.Marshal(stats) 378 _, err = obj.Do(context.Background(), nil) 379 Expect(err).ShouldNot(HaveOccurred()) 380 Expect(obj.Readonly).Should(BeFalse()) 381 }) 382 383 It("lock/unlock error", func() { 384 ctrl := gomock.NewController(GinkgoT()) 385 mockDBManager := engines.NewMockDBManager(ctrl) 386 mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(fmt.Errorf("test")) 387 mockDBManager.EXPECT().Unlock(gomock.Any()).Return(fmt.Errorf("test")) 388 register.SetDBManager(mockDBManager) 389 390 obj := newProtection() 391 mock := obj.Requester.(*mockVolumeStatsRequester) 392 stats := statsv1alpha1.Summary{ 393 Pods: []statsv1alpha1.PodStats{ 394 { 395 PodRef: statsv1alpha1.PodReference{ 396 Name: podName, 397 }, 398 VolumeStats: []statsv1alpha1.VolumeStats{ 399 { 400 Name: volumeName, 401 FsStats: statsv1alpha1.FsStats{ 402 CapacityBytes: &capacityBytes, 403 UsedBytes: &usedBytesOverThreshold, 404 }, 405 }, 406 }, 407 }, 408 }, 409 } 410 mock.summary, _ = json.Marshal(stats) 411 _, err := obj.Do(context.Background(), nil) 412 Expect(err).Should(HaveOccurred()) 413 Expect(obj.Readonly).Should(BeFalse()) // unchanged 414 415 // drops down the usage, and trigger unlock action 416 stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesUnderThreshold 417 mock.summary, _ = json.Marshal(stats) 418 obj.Readonly = true // hack it as locked 419 _, err = obj.Do(context.Background(), nil) 420 Expect(err).Should(HaveOccurred()) 421 Expect(obj.Readonly).Should(BeTrue()) // unchanged 422 }) 423 }) 424 })