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  })