github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controllerutil/pod_utils_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 controllerutil
    21  
    22  import (
    23  	"encoding/json"
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	. "github.com/onsi/ginkgo/v2"
    30  	. "github.com/onsi/gomega"
    31  
    32  	appsv1 "k8s.io/api/apps/v1"
    33  	corev1 "k8s.io/api/core/v1"
    34  	"k8s.io/apimachinery/pkg/api/resource"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	metautil "k8s.io/apimachinery/pkg/util/intstr"
    37  
    38  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    39  	"github.com/1aal/kubeblocks/pkg/constant"
    40  	testk8s "github.com/1aal/kubeblocks/pkg/testutil/k8s"
    41  )
    42  
    43  type TestResourceUnit struct {
    44  	pvc               corev1.PersistentVolumeClaimSpec
    45  	container         corev1.Container
    46  	expectMemorySize  int64
    47  	expectCPU         int
    48  	expectStorageSize int64
    49  }
    50  
    51  func TestPodIsReady(t *testing.T) {
    52  	set := testk8s.NewFakeStatefulSet("foo", 3)
    53  	pod := testk8s.NewFakeStatefulSetPod(set, 1)
    54  	pod.Status.Conditions = []corev1.PodCondition{
    55  		{
    56  			Type:   corev1.PodReady,
    57  			Status: corev1.ConditionTrue,
    58  		},
    59  	}
    60  	pod.Labels = map[string]string{constant.RoleLabelKey: "leader"}
    61  	if !PodIsReadyWithLabel(*pod) {
    62  		t.Errorf("isReady returned false negative")
    63  	}
    64  
    65  	pod.DeletionTimestamp = &metav1.Time{Time: time.Now()}
    66  	if PodIsReadyWithLabel(*pod) {
    67  		t.Errorf("isReady returned false positive")
    68  	}
    69  
    70  	pod.Labels = nil
    71  	if PodIsReadyWithLabel(*pod) {
    72  		t.Errorf("isReady returned false positive")
    73  	}
    74  
    75  	pod.Status.Conditions = nil
    76  	if PodIsReadyWithLabel(*pod) {
    77  		t.Errorf("isReady returned false positive")
    78  	}
    79  
    80  	pod.Status.Conditions = []corev1.PodCondition{}
    81  	if PodIsReadyWithLabel(*pod) {
    82  		t.Errorf("isReady returned false positive")
    83  	}
    84  }
    85  
    86  func TestPodIsControlledByLatestRevision(t *testing.T) {
    87  	set := testk8s.NewFakeStatefulSet("foo", 3)
    88  	pod := testk8s.NewFakeStatefulSetPod(set, 1)
    89  	pod.Labels = map[string]string{
    90  		appsv1.ControllerRevisionHashLabelKey: "test",
    91  	}
    92  	set.Generation = 1
    93  	set.Status.UpdateRevision = "test"
    94  	if PodIsControlledByLatestRevision(pod, set) {
    95  		t.Errorf("PodIsControlledByLatestRevision returned false positive")
    96  	}
    97  	set.Status.ObservedGeneration = 1
    98  	if !PodIsControlledByLatestRevision(pod, set) {
    99  		t.Errorf("PodIsControlledByLatestRevision returned false positive")
   100  	}
   101  }
   102  
   103  func TestGetPodRevision(t *testing.T) {
   104  	set := testk8s.NewFakeStatefulSet("foo", 3)
   105  	pod := testk8s.NewFakeStatefulSetPod(set, 1)
   106  	if GetPodRevision(pod) != "" {
   107  		t.Errorf("revision should be empty")
   108  	}
   109  
   110  	pod.Labels = make(map[string]string, 0)
   111  	pod.Labels[appsv1.StatefulSetRevisionLabel] = "bar"
   112  
   113  	if GetPodRevision(pod) != "bar" {
   114  		t.Errorf("revision not matched")
   115  	}
   116  }
   117  
   118  var _ = Describe("pod utils", func() {
   119  
   120  	var (
   121  		statefulSet     *appsv1.StatefulSet
   122  		pod             *corev1.Pod
   123  		configTemplates = []appsv1alpha1.ComponentConfigSpec{
   124  			{
   125  				ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   126  					Name:       "xxxxx",
   127  					VolumeName: "config1",
   128  				},
   129  			},
   130  			{
   131  				ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   132  					Name:       "xxxxx2",
   133  					VolumeName: "config2",
   134  				},
   135  			},
   136  		}
   137  
   138  		foundInitContainerConfigTemplates = []appsv1alpha1.ComponentConfigSpec{
   139  			{
   140  				ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   141  					Name:       "xxxxx",
   142  					VolumeName: "config1_init_container",
   143  				},
   144  			},
   145  			{
   146  				ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   147  					Name:       "xxxxx2",
   148  					VolumeName: "config2_init_container",
   149  				},
   150  			},
   151  		}
   152  
   153  		notFoundConfigTemplates = []appsv1alpha1.ComponentConfigSpec{
   154  			{
   155  				ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   156  					Name:       "xxxxx",
   157  					VolumeName: "config1_not_fount",
   158  				},
   159  			},
   160  			{
   161  				ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{
   162  					Name:       "xxxxx2",
   163  					VolumeName: "config2_not_fount",
   164  				},
   165  			},
   166  		}
   167  	)
   168  
   169  	const (
   170  		testContainers = `
   171  {
   172    "name": "mysql",
   173    "imagePullPolicy": "IfNotPresent",
   174    "ports": [
   175      {
   176        "containerPort": 3306,
   177        "protocol": "TCP",
   178        "name": "mysql"
   179      },
   180      {
   181        "containerPort": 13306,
   182        "protocol": "TCP",
   183        "name": "paxos"
   184      }
   185    ],
   186    "volumeMounts": [
   187      {
   188        "mountPath": "/data/config",
   189        "name": "config1"
   190      },
   191      {
   192        "mountPath": "/data/config",
   193        "name": "config2"
   194      },
   195      {
   196        "mountPath": "/data",
   197        "name": "data"
   198      },
   199      {
   200        "mountPath": "/log",
   201        "name": "log"
   202      }
   203    ],
   204    "env": [
   205      {
   206        "name": "MYSQL_ROOT_PASSWORD",
   207        "valueFrom": {
   208          "secretKeyRef": {
   209            "name": "$(CONN_CREDENTIAL_SECRET_NAME)",
   210            "key": "password"
   211          }
   212        }
   213      }
   214    ]
   215  }
   216  `
   217  	)
   218  
   219  	BeforeEach(func() {
   220  		// Add any steup steps that needs to be executed before each test
   221  		statefulSet = &appsv1.StatefulSet{}
   222  		statefulSet.ObjectMeta.Name = "stateful_test"
   223  		statefulSet.ObjectMeta.Namespace = "stateful_test_ns"
   224  
   225  		container := corev1.Container{}
   226  		if err := json.Unmarshal([]byte(testContainers), &container); err != nil {
   227  			Fail("convert container failed!")
   228  		}
   229  
   230  		container2 := container.DeepCopy()
   231  		container2.Name = "mysql2"
   232  		container2.VolumeMounts[1].Name += "_not_found"
   233  		container3 := container.DeepCopy()
   234  		container3.Name = "mysql3"
   235  		container3.VolumeMounts[0].Name += "_not_found"
   236  		container3.EnvFrom = []corev1.EnvFromSource{
   237  			{
   238  				ConfigMapRef: &corev1.ConfigMapEnvSource{
   239  					LocalObjectReference: corev1.LocalObjectReference{Name: "test-config-env"},
   240  				},
   241  			},
   242  		}
   243  
   244  		container4 := container.DeepCopy()
   245  		container4.Name = "mysql4"
   246  		container4.VolumeMounts = nil
   247  		container4.EnvFrom = []corev1.EnvFromSource{
   248  			{
   249  				ConfigMapRef: &corev1.ConfigMapEnvSource{
   250  					LocalObjectReference: corev1.LocalObjectReference{Name: "test-config-env"},
   251  				},
   252  			},
   253  		}
   254  
   255  		statefulSet.Spec.Template.Spec.Containers = []corev1.Container{
   256  			*container2, *container3, container}
   257  
   258  		// init container
   259  		initContainer := container.DeepCopy()
   260  		initContainer.Name = "init_mysql"
   261  		initContainer2 := container.DeepCopy()
   262  		initContainer2.Name = "init_mysql_2"
   263  		initContainer3 := container.DeepCopy()
   264  		initContainer3.Name = "init_mysql_3"
   265  		initContainer.VolumeMounts[0].Name += "_init_container"
   266  		initContainer.VolumeMounts[1].Name += "_init_container"
   267  		statefulSet.Spec.Template.Spec.InitContainers = []corev1.Container{
   268  			*initContainer, *initContainer2, *initContainer3}
   269  
   270  		// init pod
   271  		pod = &corev1.Pod{}
   272  		pod.ObjectMeta.Name = "pod_test"
   273  		pod.ObjectMeta.Namespace = "pod_test_ns"
   274  		pod.Spec.Containers = []corev1.Container{container, *container2, *container3, *container4}
   275  		pod.Spec.Volumes = []corev1.Volume{
   276  			{
   277  				Name: "config1",
   278  				VolumeSource: corev1.VolumeSource{
   279  					ConfigMap: &corev1.ConfigMapVolumeSource{
   280  						LocalObjectReference: corev1.LocalObjectReference{Name: "stateful_test-config1"},
   281  					},
   282  				},
   283  			},
   284  			{
   285  				Name: "config2",
   286  				VolumeSource: corev1.VolumeSource{
   287  					ConfigMap: &corev1.ConfigMapVolumeSource{
   288  						LocalObjectReference: corev1.LocalObjectReference{Name: "stateful_test-config2"},
   289  					},
   290  				},
   291  			},
   292  		}
   293  
   294  	})
   295  
   296  	// for test GetContainerByConfigSpec
   297  	Context("GetContainerByConfigSpec test", func() {
   298  		// found name: mysql3
   299  		It("Should succeed with no error", func() {
   300  			podSpec := &statefulSet.Spec.Template.Spec
   301  			Expect(GetContainerByConfigSpec(podSpec, configTemplates)).To(Equal(&podSpec.Containers[2]))
   302  		})
   303  		// found name: init_mysql
   304  		It("Should succeed with no error", func() {
   305  			podSpec := &statefulSet.Spec.Template.Spec
   306  			Expect(GetContainerByConfigSpec(podSpec, foundInitContainerConfigTemplates)).To(Equal(&podSpec.InitContainers[0]))
   307  		})
   308  		// not found container
   309  		It("Should fail", func() {
   310  			podSpec := &statefulSet.Spec.Template.Spec
   311  			Expect(GetContainerByConfigSpec(podSpec, notFoundConfigTemplates)).To(BeNil(), "get container is nil!")
   312  		})
   313  	})
   314  
   315  	// for test GetVolumeMountName
   316  	Context("GetPodContainerWithVolumeMount test", func() {
   317  		It("Should succeed with no error", func() {
   318  			mountedContainers := GetPodContainerWithVolumeMount(&pod.Spec, "config1")
   319  			Expect(len(mountedContainers)).To(Equal(2))
   320  			Expect(mountedContainers[0].Name).To(Equal("mysql"))
   321  			Expect(mountedContainers[1].Name).To(Equal("mysql2"))
   322  
   323  			//
   324  			mountedContainers = GetPodContainerWithVolumeMount(&pod.Spec, "config2")
   325  			Expect(len(mountedContainers)).To(Equal(2))
   326  			Expect(mountedContainers[0].Name).To(Equal("mysql"))
   327  			Expect(mountedContainers[1].Name).To(Equal("mysql3"))
   328  		})
   329  		It("Should fail", func() {
   330  			Expect(len(GetPodContainerWithVolumeMount(&pod.Spec, "not_exist_cm"))).To(Equal(0))
   331  
   332  			emptyPod := corev1.Pod{}
   333  			emptyPod.ObjectMeta.Name = "empty_test"
   334  			emptyPod.ObjectMeta.Namespace = "empty_test_ns"
   335  			Expect(GetPodContainerWithVolumeMount(&emptyPod.Spec, "not_exist_cm")).To(BeNil())
   336  
   337  		})
   338  	})
   339  
   340  	// for test GetContainerWithVolumeMount
   341  	Context("GetVolumeMountName test", func() {
   342  		It("Should succeed with no error", func() {
   343  			volume := GetVolumeMountName(pod.Spec.Volumes, "stateful_test-config1")
   344  			Expect(volume).NotTo(BeNil())
   345  			Expect(volume.Name).To(Equal("config1"))
   346  
   347  			Expect(GetVolumeMountName(pod.Spec.Volumes, "stateful_test-config1")).To(Equal(&pod.Spec.Volumes[0]))
   348  		})
   349  		It("Should fail", func() {
   350  			Expect(GetVolumeMountName(pod.Spec.Volumes, "not_exist_resource")).To(BeNil())
   351  		})
   352  	})
   353  
   354  	// for test MemorySize or CoreNum
   355  	Context("Get Resource test", func() {
   356  		It("Resource exists limit", func() {
   357  			testResources := []TestResourceUnit{
   358  				// memory unit: Gi
   359  				{
   360  					container: corev1.Container{
   361  						Resources: corev1.ResourceRequirements{
   362  							Limits: corev1.ResourceList{
   363  								corev1.ResourceMemory:  resource.MustParse("10Gi"),
   364  								corev1.ResourceCPU:     resource.MustParse("6"),
   365  								corev1.ResourceStorage: resource.MustParse("100G"),
   366  							},
   367  						},
   368  					},
   369  					expectMemorySize: 10 * 1024 * 1024 * 1024,
   370  					expectCPU:        6,
   371  				},
   372  				// memory unit: G
   373  				{
   374  					container: corev1.Container{
   375  						Resources: corev1.ResourceRequirements{
   376  							Limits: corev1.ResourceList{
   377  								corev1.ResourceMemory:  resource.MustParse("10G"),
   378  								corev1.ResourceCPU:     resource.MustParse("16"),
   379  								corev1.ResourceStorage: resource.MustParse("100G"),
   380  							},
   381  						},
   382  					},
   383  					expectMemorySize: 10 * 1000 * 1000 * 1000,
   384  					expectCPU:        16,
   385  				},
   386  				// memory unit: no
   387  				{
   388  					container: corev1.Container{
   389  						Resources: corev1.ResourceRequirements{
   390  							Limits: corev1.ResourceList{
   391  								corev1.ResourceMemory:  resource.MustParse("1024000"),
   392  								corev1.ResourceCPU:     resource.MustParse("26"),
   393  								corev1.ResourceStorage: resource.MustParse("100G"),
   394  							},
   395  						},
   396  					},
   397  					expectMemorySize: 1024000,
   398  					expectCPU:        26,
   399  				},
   400  			}
   401  
   402  			for i := range testResources {
   403  				Expect(GetMemorySize(testResources[i].container)).To(BeEquivalentTo(testResources[i].expectMemorySize))
   404  				Expect(GetCoreNum(testResources[i].container)).To(BeEquivalentTo(testResources[i].expectCPU))
   405  			}
   406  		})
   407  		It("Resource not limit", func() {
   408  			container := corev1.Container{}
   409  			Expect(GetMemorySize(container)).To(BeEquivalentTo(0))
   410  			Expect(GetCoreNum(container)).To(BeEquivalentTo(0))
   411  		})
   412  	})
   413  
   414  	// for test MemorySize or CoreNum
   415  	Context("Get pvc test", func() {
   416  		It("Resource exists request", func() {
   417  			testResources := []TestResourceUnit{
   418  				// memory unit: Gi
   419  				{
   420  					pvc: corev1.PersistentVolumeClaimSpec{
   421  						Resources: corev1.ResourceRequirements{
   422  							Requests: corev1.ResourceList{
   423  								corev1.ResourceStorage: resource.MustParse("100Gi"),
   424  							},
   425  						},
   426  					},
   427  					expectStorageSize: 100 * 1024 * 1024 * 1024,
   428  				},
   429  				// memory unit: G
   430  				{
   431  					pvc: corev1.PersistentVolumeClaimSpec{
   432  						Resources: corev1.ResourceRequirements{
   433  							Requests: corev1.ResourceList{
   434  								corev1.ResourceStorage: resource.MustParse("100G"),
   435  							},
   436  						},
   437  					},
   438  					expectStorageSize: 100 * 1000 * 1000 * 1000,
   439  				},
   440  				// memory unit: no
   441  				{
   442  					pvc: corev1.PersistentVolumeClaimSpec{
   443  						Resources: corev1.ResourceRequirements{
   444  							Requests: corev1.ResourceList{
   445  								corev1.ResourceStorage: resource.MustParse("10000"),
   446  							},
   447  						},
   448  					},
   449  					expectStorageSize: 10000,
   450  				},
   451  			}
   452  
   453  			for i := range testResources {
   454  				Expect(GetStorageSizeFromPersistentVolume(corev1.PersistentVolumeClaimTemplate{
   455  					Spec: testResources[i].pvc,
   456  				})).To(BeEquivalentTo(testResources[i].expectStorageSize))
   457  			}
   458  		})
   459  		It("Resource not request", func() {
   460  			pvcTpl := corev1.PersistentVolumeClaimTemplate{}
   461  			Expect(GetStorageSizeFromPersistentVolume(pvcTpl)).To(BeEquivalentTo(-1))
   462  		})
   463  	})
   464  
   465  	Context("testGetContainerID", func() {
   466  		It("Should succeed with no error", func() {
   467  			pods := []*corev1.Pod{{
   468  				Status: corev1.PodStatus{
   469  					ContainerStatuses: []corev1.ContainerStatus{
   470  						{
   471  							Name:        "a",
   472  							ContainerID: "docker://27d1586d53ef9a6af5bd983831d13b6a38128119fadcdc22894d7b2397758eb5",
   473  						},
   474  						{
   475  							Name:        "b",
   476  							ContainerID: "docker://6f5ca0f22cd151943ba1b70f618591ad482cdbbc019ed58d7adf4c04f6d0ca7a",
   477  						},
   478  					},
   479  				},
   480  			}, {
   481  				Status: corev1.PodStatus{
   482  					ContainerStatuses: []corev1.ContainerStatus{},
   483  				},
   484  			}}
   485  
   486  			type args struct {
   487  				pod           *corev1.Pod
   488  				containerName string
   489  			}
   490  			tests := []struct {
   491  				name string
   492  				args args
   493  				want string
   494  			}{{
   495  				name: "test1",
   496  				args: args{
   497  					pod:           pods[0],
   498  					containerName: "b",
   499  				},
   500  				want: "6f5ca0f22cd151943ba1b70f618591ad482cdbbc019ed58d7adf4c04f6d0ca7a",
   501  			}, {
   502  				name: "test2",
   503  				args: args{
   504  					pod:           pods[0],
   505  					containerName: "f",
   506  				},
   507  				want: "",
   508  			}, {
   509  				name: "test3",
   510  				args: args{
   511  					pod:           pods[1],
   512  					containerName: "a",
   513  				},
   514  				want: "",
   515  			}}
   516  			for _, tt := range tests {
   517  				Expect(GetContainerID(tt.args.pod, tt.args.containerName)).Should(BeEquivalentTo(tt.want))
   518  			}
   519  
   520  		})
   521  	})
   522  
   523  	Context("common funcs test", func() {
   524  		It("GetContainersByConfigmap Should succeed with no error", func() {
   525  			type args struct {
   526  				containers []corev1.Container
   527  				volumeName string
   528  				envFrom    string
   529  				filters    []containerNameFilter
   530  			}
   531  			tests := []struct {
   532  				name string
   533  				args args
   534  				want []string
   535  			}{{
   536  				name: "test1",
   537  				args: args{
   538  					containers: pod.Spec.Containers,
   539  					volumeName: "config1",
   540  				},
   541  				want: []string{"mysql", "mysql2"},
   542  			}, {
   543  				name: "test1",
   544  				args: args{
   545  					containers: pod.Spec.Containers,
   546  					volumeName: "config1",
   547  					filters: []containerNameFilter{
   548  						func(name string) bool {
   549  							return name != "mysql"
   550  						},
   551  					},
   552  				},
   553  				want: []string{"mysql"},
   554  			}, {
   555  				name: "test1",
   556  				args: args{
   557  					containers: pod.Spec.Containers,
   558  					volumeName: "config2",
   559  					filters: []containerNameFilter{
   560  						func(name string) bool {
   561  							return strings.HasPrefix(name, "mysql")
   562  						},
   563  					},
   564  				},
   565  				want: []string{},
   566  			}, {
   567  				name: "test_env",
   568  				args: args{
   569  					containers: pod.Spec.Containers,
   570  					volumeName: "not-config2",
   571  					envFrom:    "test-config-env",
   572  					filters: []containerNameFilter{
   573  						func(name string) bool {
   574  							return false
   575  						},
   576  					},
   577  				},
   578  				want: []string{"mysql3", "mysql4"},
   579  			}}
   580  			for _, tt := range tests {
   581  				Expect(GetContainersByConfigmap(tt.args.containers, tt.args.volumeName, tt.args.envFrom, tt.args.filters...)).Should(BeEquivalentTo(tt.want))
   582  			}
   583  
   584  		})
   585  
   586  		It("GetIntOrPercentValue Should succeed with no error", func() {
   587  			fn := func(v metautil.IntOrString) *metautil.IntOrString { return &v }
   588  			tests := []struct {
   589  				name      string
   590  				args      *metautil.IntOrString
   591  				want      int
   592  				isPercent bool
   593  				wantErr   bool
   594  			}{{
   595  				name:      "test",
   596  				args:      fn(metautil.FromString("10")),
   597  				want:      0,
   598  				isPercent: false,
   599  				wantErr:   true,
   600  			}, {
   601  				name:      "test",
   602  				args:      fn(metautil.FromString("10%")),
   603  				want:      10,
   604  				isPercent: true,
   605  				wantErr:   false,
   606  			}, {
   607  				name:      "test",
   608  				args:      fn(metautil.FromInt(60)),
   609  				want:      60,
   610  				isPercent: false,
   611  				wantErr:   false,
   612  			}}
   613  
   614  			for _, tt := range tests {
   615  				val, isPercent, err := GetIntOrPercentValue(tt.args)
   616  				Expect(err != nil).Should(BeEquivalentTo(tt.wantErr))
   617  				Expect(val).Should(BeEquivalentTo(tt.want))
   618  				Expect(isPercent).Should(BeEquivalentTo(tt.isPercent))
   619  			}
   620  		})
   621  	})
   622  	Context("test sort by pod name", func() {
   623  		It("Should succeed with no error", func() {
   624  			pods := []corev1.Pod{{
   625  				ObjectMeta: metav1.ObjectMeta{Name: "pod-2"},
   626  			}, {
   627  				ObjectMeta: metav1.ObjectMeta{Name: "pod-3"},
   628  			}, {
   629  				ObjectMeta: metav1.ObjectMeta{Name: "pod-0"},
   630  			}, {
   631  				ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
   632  			}}
   633  			sort.Sort(ByPodName(pods))
   634  			Expect(pods[0].Name).Should(Equal("pod-0"))
   635  			Expect(pods[3].Name).Should(Equal("pod-3"))
   636  		})
   637  	})
   638  })