github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/kube/ready_test.go (about)

     1  /*
     2  Copyright The Helm 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 kube // import "github.com/stefanmcshane/helm/pkg/kube"
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	appsv1 "k8s.io/api/apps/v1"
    24  	batchv1 "k8s.io/api/batch/v1"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/util/intstr"
    29  	"k8s.io/client-go/kubernetes/fake"
    30  )
    31  
    32  const defaultNamespace = metav1.NamespaceDefault
    33  
    34  func Test_ReadyChecker_deploymentReady(t *testing.T) {
    35  	type args struct {
    36  		rs  *appsv1.ReplicaSet
    37  		dep *appsv1.Deployment
    38  	}
    39  	tests := []struct {
    40  		name string
    41  		args args
    42  		want bool
    43  	}{
    44  		{
    45  			name: "deployment is ready",
    46  			args: args{
    47  				rs:  newReplicaSet("foo", 1, 1, true),
    48  				dep: newDeployment("foo", 1, 1, 0, true),
    49  			},
    50  			want: true,
    51  		},
    52  		{
    53  			name: "deployment is not ready",
    54  			args: args{
    55  				rs:  newReplicaSet("foo", 0, 0, true),
    56  				dep: newDeployment("foo", 1, 1, 0, true),
    57  			},
    58  			want: false,
    59  		},
    60  		{
    61  			name: "deployment is ready when maxUnavailable is set",
    62  			args: args{
    63  				rs:  newReplicaSet("foo", 2, 1, true),
    64  				dep: newDeployment("foo", 2, 1, 1, true),
    65  			},
    66  			want: true,
    67  		},
    68  		{
    69  			name: "deployment is not ready when replicaset generations are out of sync",
    70  			args: args{
    71  				rs:  newReplicaSet("foo", 1, 1, false),
    72  				dep: newDeployment("foo", 1, 1, 0, true),
    73  			},
    74  			want: false,
    75  		},
    76  		{
    77  			name: "deployment is not ready when deployment generations are out of sync",
    78  			args: args{
    79  				rs:  newReplicaSet("foo", 1, 1, true),
    80  				dep: newDeployment("foo", 1, 1, 0, false),
    81  			},
    82  			want: false,
    83  		},
    84  		{
    85  			name: "deployment is not ready when generations are out of sync",
    86  			args: args{
    87  				rs:  newReplicaSet("foo", 1, 1, false),
    88  				dep: newDeployment("foo", 1, 1, 0, false),
    89  			},
    90  			want: false,
    91  		},
    92  	}
    93  	for _, tt := range tests {
    94  		t.Run(tt.name, func(t *testing.T) {
    95  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
    96  			if got := c.deploymentReady(tt.args.rs, tt.args.dep); got != tt.want {
    97  				t.Errorf("deploymentReady() = %v, want %v", got, tt.want)
    98  			}
    99  		})
   100  	}
   101  }
   102  
   103  func Test_ReadyChecker_replicaSetReady(t *testing.T) {
   104  	type args struct {
   105  		rs *appsv1.ReplicaSet
   106  	}
   107  	tests := []struct {
   108  		name string
   109  		args args
   110  		want bool
   111  	}{
   112  		{
   113  			name: "replicaSet is ready",
   114  			args: args{
   115  				rs: newReplicaSet("foo", 1, 1, true),
   116  			},
   117  			want: true,
   118  		},
   119  		{
   120  			name: "replicaSet is not ready when generations are out of sync",
   121  			args: args{
   122  				rs: newReplicaSet("foo", 1, 1, false),
   123  			},
   124  			want: false,
   125  		},
   126  	}
   127  	for _, tt := range tests {
   128  		t.Run(tt.name, func(t *testing.T) {
   129  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
   130  			if got := c.replicaSetReady(tt.args.rs); got != tt.want {
   131  				t.Errorf("replicaSetReady() = %v, want %v", got, tt.want)
   132  			}
   133  		})
   134  	}
   135  }
   136  
   137  func Test_ReadyChecker_replicationControllerReady(t *testing.T) {
   138  	type args struct {
   139  		rc *corev1.ReplicationController
   140  	}
   141  	tests := []struct {
   142  		name string
   143  		args args
   144  		want bool
   145  	}{
   146  		{
   147  			name: "replicationController is ready",
   148  			args: args{
   149  				rc: newReplicationController("foo", true),
   150  			},
   151  			want: true,
   152  		},
   153  		{
   154  			name: "replicationController is not ready when generations are out of sync",
   155  			args: args{
   156  				rc: newReplicationController("foo", false),
   157  			},
   158  			want: false,
   159  		},
   160  	}
   161  	for _, tt := range tests {
   162  		t.Run(tt.name, func(t *testing.T) {
   163  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
   164  			if got := c.replicationControllerReady(tt.args.rc); got != tt.want {
   165  				t.Errorf("replicationControllerReady() = %v, want %v", got, tt.want)
   166  			}
   167  		})
   168  	}
   169  }
   170  
   171  func Test_ReadyChecker_daemonSetReady(t *testing.T) {
   172  	type args struct {
   173  		ds *appsv1.DaemonSet
   174  	}
   175  	tests := []struct {
   176  		name string
   177  		args args
   178  		want bool
   179  	}{
   180  		{
   181  			name: "daemonset is ready",
   182  			args: args{
   183  				ds: newDaemonSet("foo", 0, 1, 1, 1, true),
   184  			},
   185  			want: true,
   186  		},
   187  		{
   188  			name: "daemonset is not ready",
   189  			args: args{
   190  				ds: newDaemonSet("foo", 0, 0, 1, 1, true),
   191  			},
   192  			want: false,
   193  		},
   194  		{
   195  			name: "daemonset pods have not been scheduled successfully",
   196  			args: args{
   197  				ds: newDaemonSet("foo", 0, 0, 1, 0, true),
   198  			},
   199  			want: false,
   200  		},
   201  		{
   202  			name: "daemonset is ready when maxUnavailable is set",
   203  			args: args{
   204  				ds: newDaemonSet("foo", 1, 1, 2, 2, true),
   205  			},
   206  			want: true,
   207  		},
   208  		{
   209  			name: "daemonset is not ready when generations are out of sync",
   210  			args: args{
   211  				ds: newDaemonSet("foo", 0, 1, 1, 1, false),
   212  			},
   213  			want: false,
   214  		},
   215  	}
   216  	for _, tt := range tests {
   217  		t.Run(tt.name, func(t *testing.T) {
   218  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
   219  			if got := c.daemonSetReady(tt.args.ds); got != tt.want {
   220  				t.Errorf("daemonSetReady() = %v, want %v", got, tt.want)
   221  			}
   222  		})
   223  	}
   224  }
   225  
   226  func Test_ReadyChecker_statefulSetReady(t *testing.T) {
   227  	type args struct {
   228  		sts *appsv1.StatefulSet
   229  	}
   230  	tests := []struct {
   231  		name string
   232  		args args
   233  		want bool
   234  	}{
   235  		{
   236  			name: "statefulset is ready",
   237  			args: args{
   238  				sts: newStatefulSet("foo", 1, 0, 1, 1, true),
   239  			},
   240  			want: true,
   241  		},
   242  		{
   243  			name: "statefulset is not ready",
   244  			args: args{
   245  				sts: newStatefulSet("foo", 1, 0, 0, 1, true),
   246  			},
   247  			want: false,
   248  		},
   249  		{
   250  			name: "statefulset is ready when partition is specified",
   251  			args: args{
   252  				sts: newStatefulSet("foo", 2, 1, 2, 1, true),
   253  			},
   254  			want: true,
   255  		},
   256  		{
   257  			name: "statefulset is not ready when partition is set",
   258  			args: args{
   259  				sts: newStatefulSet("foo", 2, 1, 1, 0, true),
   260  			},
   261  			want: false,
   262  		},
   263  		{
   264  			name: "statefulset is ready when partition is set and no change in template",
   265  			args: args{
   266  				sts: newStatefulSet("foo", 2, 1, 2, 2, true),
   267  			},
   268  			want: true,
   269  		},
   270  		{
   271  			name: "statefulset is ready when partition is greater than replicas",
   272  			args: args{
   273  				sts: newStatefulSet("foo", 1, 2, 1, 1, true),
   274  			},
   275  			want: true,
   276  		},
   277  		{
   278  			name: "statefulset is not ready when generations are out of sync",
   279  			args: args{
   280  				sts: newStatefulSet("foo", 1, 0, 1, 1, false),
   281  			},
   282  			want: false,
   283  		},
   284  	}
   285  	for _, tt := range tests {
   286  		t.Run(tt.name, func(t *testing.T) {
   287  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
   288  			if got := c.statefulSetReady(tt.args.sts); got != tt.want {
   289  				t.Errorf("statefulSetReady() = %v, want %v", got, tt.want)
   290  			}
   291  		})
   292  	}
   293  }
   294  
   295  func Test_ReadyChecker_podsReadyForObject(t *testing.T) {
   296  	type args struct {
   297  		namespace string
   298  		obj       runtime.Object
   299  	}
   300  	tests := []struct {
   301  		name      string
   302  		args      args
   303  		existPods []corev1.Pod
   304  		want      bool
   305  		wantErr   bool
   306  	}{
   307  		{
   308  			name: "pods ready for a replicaset",
   309  			args: args{
   310  				namespace: defaultNamespace,
   311  				obj:       newReplicaSet("foo", 1, 1, true),
   312  			},
   313  			existPods: []corev1.Pod{
   314  				*newPodWithCondition("foo", corev1.ConditionTrue),
   315  			},
   316  			want:    true,
   317  			wantErr: false,
   318  		},
   319  		{
   320  			name: "pods not ready for a replicaset",
   321  			args: args{
   322  				namespace: defaultNamespace,
   323  				obj:       newReplicaSet("foo", 1, 1, true),
   324  			},
   325  			existPods: []corev1.Pod{
   326  				*newPodWithCondition("foo", corev1.ConditionFalse),
   327  			},
   328  			want:    false,
   329  			wantErr: false,
   330  		},
   331  	}
   332  	for _, tt := range tests {
   333  		t.Run(tt.name, func(t *testing.T) {
   334  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
   335  			for _, pod := range tt.existPods {
   336  				if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(context.TODO(), &pod, metav1.CreateOptions{}); err != nil {
   337  					t.Errorf("Failed to create Pod error: %v", err)
   338  					return
   339  				}
   340  			}
   341  			got, err := c.podsReadyForObject(context.TODO(), tt.args.namespace, tt.args.obj)
   342  			if (err != nil) != tt.wantErr {
   343  				t.Errorf("podsReadyForObject() error = %v, wantErr %v", err, tt.wantErr)
   344  				return
   345  			}
   346  			if got != tt.want {
   347  				t.Errorf("podsReadyForObject() got = %v, want %v", got, tt.want)
   348  			}
   349  		})
   350  	}
   351  }
   352  
   353  func Test_ReadyChecker_jobReady(t *testing.T) {
   354  	type args struct {
   355  		job *batchv1.Job
   356  	}
   357  	tests := []struct {
   358  		name string
   359  		args args
   360  		want bool
   361  	}{
   362  		{
   363  			name: "job is completed",
   364  			args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)},
   365  			want: true,
   366  		},
   367  		{
   368  			name: "job is incomplete",
   369  			args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)},
   370  			want: false,
   371  		},
   372  		{
   373  			name: "job is failed",
   374  			args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)},
   375  			want: false,
   376  		},
   377  		{
   378  			name: "job is completed with retry",
   379  			args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)},
   380  			want: true,
   381  		},
   382  		{
   383  			name: "job is failed with retry",
   384  			args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)},
   385  			want: false,
   386  		},
   387  		{
   388  			name: "job is completed single run",
   389  			args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)},
   390  			want: true,
   391  		},
   392  		{
   393  			name: "job is failed single run",
   394  			args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)},
   395  			want: false,
   396  		},
   397  		{
   398  			name: "job with null completions",
   399  			args: args{job: newJob("foo", 0, nil, 1, 0)},
   400  			want: true,
   401  		},
   402  	}
   403  	for _, tt := range tests {
   404  		t.Run(tt.name, func(t *testing.T) {
   405  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
   406  			if got := c.jobReady(tt.args.job); got != tt.want {
   407  				t.Errorf("jobReady() = %v, want %v", got, tt.want)
   408  			}
   409  		})
   410  	}
   411  }
   412  
   413  func Test_ReadyChecker_volumeReady(t *testing.T) {
   414  	type args struct {
   415  		v *corev1.PersistentVolumeClaim
   416  	}
   417  	tests := []struct {
   418  		name string
   419  		args args
   420  		want bool
   421  	}{
   422  		{
   423  			name: "pvc is bound",
   424  			args: args{
   425  				v: newPersistentVolumeClaim("foo", corev1.ClaimBound),
   426  			},
   427  			want: true,
   428  		},
   429  		{
   430  			name: "pvc is not ready",
   431  			args: args{
   432  				v: newPersistentVolumeClaim("foo", corev1.ClaimPending),
   433  			},
   434  			want: false,
   435  		},
   436  	}
   437  	for _, tt := range tests {
   438  		t.Run(tt.name, func(t *testing.T) {
   439  			c := NewReadyChecker(fake.NewSimpleClientset(), nil)
   440  			if got := c.volumeReady(tt.args.v); got != tt.want {
   441  				t.Errorf("volumeReady() = %v, want %v", got, tt.want)
   442  			}
   443  		})
   444  	}
   445  }
   446  
   447  func newDaemonSet(name string, maxUnavailable, numberReady, desiredNumberScheduled, updatedNumberScheduled int, generationInSync bool) *appsv1.DaemonSet {
   448  	var generation, observedGeneration int64 = 1, 1
   449  	if !generationInSync {
   450  		generation = 2
   451  	}
   452  	return &appsv1.DaemonSet{
   453  		ObjectMeta: metav1.ObjectMeta{
   454  			Name:       name,
   455  			Namespace:  defaultNamespace,
   456  			Generation: generation,
   457  		},
   458  		Spec: appsv1.DaemonSetSpec{
   459  			UpdateStrategy: appsv1.DaemonSetUpdateStrategy{
   460  				Type: appsv1.RollingUpdateDaemonSetStrategyType,
   461  				RollingUpdate: &appsv1.RollingUpdateDaemonSet{
   462  					MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromInt(maxUnavailable); return &i }(),
   463  				},
   464  			},
   465  			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}},
   466  			Template: corev1.PodTemplateSpec{
   467  				ObjectMeta: metav1.ObjectMeta{
   468  					Name:   name,
   469  					Labels: map[string]string{"name": name},
   470  				},
   471  				Spec: corev1.PodSpec{
   472  					Containers: []corev1.Container{
   473  						{
   474  							Image: "nginx",
   475  						},
   476  					},
   477  				},
   478  			},
   479  		},
   480  		Status: appsv1.DaemonSetStatus{
   481  			DesiredNumberScheduled: int32(desiredNumberScheduled),
   482  			NumberReady:            int32(numberReady),
   483  			UpdatedNumberScheduled: int32(updatedNumberScheduled),
   484  			ObservedGeneration:     observedGeneration,
   485  		},
   486  	}
   487  }
   488  
   489  func newStatefulSet(name string, replicas, partition, readyReplicas, updatedReplicas int, generationInSync bool) *appsv1.StatefulSet {
   490  	var generation, observedGeneration int64 = 1, 1
   491  	if !generationInSync {
   492  		generation = 2
   493  	}
   494  	return &appsv1.StatefulSet{
   495  		ObjectMeta: metav1.ObjectMeta{
   496  			Name:       name,
   497  			Namespace:  defaultNamespace,
   498  			Generation: generation,
   499  		},
   500  		Spec: appsv1.StatefulSetSpec{
   501  			UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
   502  				Type: appsv1.RollingUpdateStatefulSetStrategyType,
   503  				RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
   504  					Partition: intToInt32(partition),
   505  				},
   506  			},
   507  			Replicas: intToInt32(replicas),
   508  			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}},
   509  			Template: corev1.PodTemplateSpec{
   510  				ObjectMeta: metav1.ObjectMeta{
   511  					Name:   name,
   512  					Labels: map[string]string{"name": name},
   513  				},
   514  				Spec: corev1.PodSpec{
   515  					Containers: []corev1.Container{
   516  						{
   517  							Image: "nginx",
   518  						},
   519  					},
   520  				},
   521  			},
   522  		},
   523  		Status: appsv1.StatefulSetStatus{
   524  			UpdatedReplicas:    int32(updatedReplicas),
   525  			ReadyReplicas:      int32(readyReplicas),
   526  			ObservedGeneration: observedGeneration,
   527  		},
   528  	}
   529  }
   530  
   531  func newDeployment(name string, replicas, maxSurge, maxUnavailable int, generationInSync bool) *appsv1.Deployment {
   532  	var generation, observedGeneration int64 = 1, 1
   533  	if !generationInSync {
   534  		generation = 2
   535  	}
   536  	return &appsv1.Deployment{
   537  		ObjectMeta: metav1.ObjectMeta{
   538  			Name:       name,
   539  			Namespace:  defaultNamespace,
   540  			Generation: generation,
   541  		},
   542  		Spec: appsv1.DeploymentSpec{
   543  			Strategy: appsv1.DeploymentStrategy{
   544  				Type: appsv1.RollingUpdateDeploymentStrategyType,
   545  				RollingUpdate: &appsv1.RollingUpdateDeployment{
   546  					MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromInt(maxUnavailable); return &i }(),
   547  					MaxSurge:       func() *intstr.IntOrString { i := intstr.FromInt(maxSurge); return &i }(),
   548  				},
   549  			},
   550  			Replicas: intToInt32(replicas),
   551  			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}},
   552  			Template: corev1.PodTemplateSpec{
   553  				ObjectMeta: metav1.ObjectMeta{
   554  					Name:   name,
   555  					Labels: map[string]string{"name": name},
   556  				},
   557  				Spec: corev1.PodSpec{
   558  					Containers: []corev1.Container{
   559  						{
   560  							Image: "nginx",
   561  						},
   562  					},
   563  				},
   564  			},
   565  		},
   566  		Status: appsv1.DeploymentStatus{
   567  			ObservedGeneration: observedGeneration,
   568  		},
   569  	}
   570  }
   571  
   572  func newReplicationController(name string, generationInSync bool) *corev1.ReplicationController {
   573  	var generation, observedGeneration int64 = 1, 1
   574  	if !generationInSync {
   575  		generation = 2
   576  	}
   577  	return &corev1.ReplicationController{
   578  		ObjectMeta: metav1.ObjectMeta{
   579  			Name:       name,
   580  			Generation: generation,
   581  		},
   582  		Status: corev1.ReplicationControllerStatus{
   583  			ObservedGeneration: observedGeneration,
   584  		},
   585  	}
   586  }
   587  
   588  func newReplicaSet(name string, replicas int, readyReplicas int, generationInSync bool) *appsv1.ReplicaSet {
   589  	d := newDeployment(name, replicas, 0, 0, generationInSync)
   590  	return &appsv1.ReplicaSet{
   591  		ObjectMeta: metav1.ObjectMeta{
   592  			Name:            name,
   593  			Namespace:       defaultNamespace,
   594  			Labels:          d.Spec.Selector.MatchLabels,
   595  			OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(d, d.GroupVersionKind())},
   596  			Generation:      d.Generation,
   597  		},
   598  		Spec: appsv1.ReplicaSetSpec{
   599  			Selector: d.Spec.Selector,
   600  			Replicas: intToInt32(replicas),
   601  			Template: d.Spec.Template,
   602  		},
   603  		Status: appsv1.ReplicaSetStatus{
   604  			ReadyReplicas:      int32(readyReplicas),
   605  			ObservedGeneration: d.Status.ObservedGeneration,
   606  		},
   607  	}
   608  }
   609  
   610  func newPodWithCondition(name string, podReadyCondition corev1.ConditionStatus) *corev1.Pod {
   611  	return &corev1.Pod{
   612  		ObjectMeta: metav1.ObjectMeta{
   613  			Name:      name,
   614  			Namespace: defaultNamespace,
   615  			Labels:    map[string]string{"name": name},
   616  		},
   617  		Spec: corev1.PodSpec{
   618  			Containers: []corev1.Container{
   619  				{
   620  					Image: "nginx",
   621  				},
   622  			},
   623  		},
   624  		Status: corev1.PodStatus{
   625  			Conditions: []corev1.PodCondition{
   626  				{
   627  					Type:   corev1.PodReady,
   628  					Status: podReadyCondition,
   629  				},
   630  			},
   631  		},
   632  	}
   633  }
   634  
   635  func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPhase) *corev1.PersistentVolumeClaim {
   636  	return &corev1.PersistentVolumeClaim{
   637  		ObjectMeta: metav1.ObjectMeta{
   638  			Name:      name,
   639  			Namespace: defaultNamespace,
   640  		},
   641  		Status: corev1.PersistentVolumeClaimStatus{
   642  			Phase: phase,
   643  		},
   644  	}
   645  }
   646  
   647  func newJob(name string, backoffLimit int, completions *int32, succeeded int, failed int) *batchv1.Job {
   648  	return &batchv1.Job{
   649  		ObjectMeta: metav1.ObjectMeta{
   650  			Name:      name,
   651  			Namespace: defaultNamespace,
   652  		},
   653  		Spec: batchv1.JobSpec{
   654  			BackoffLimit: intToInt32(backoffLimit),
   655  			Completions:  completions,
   656  			Template: corev1.PodTemplateSpec{
   657  				ObjectMeta: metav1.ObjectMeta{
   658  					Name:   name,
   659  					Labels: map[string]string{"name": name},
   660  				},
   661  				Spec: corev1.PodSpec{
   662  					Containers: []corev1.Container{
   663  						{
   664  							Image: "nginx",
   665  						},
   666  					},
   667  				},
   668  			},
   669  		},
   670  		Status: batchv1.JobStatus{
   671  			Succeeded: int32(succeeded),
   672  			Failed:    int32(failed),
   673  		},
   674  	}
   675  }
   676  
   677  func intToInt32(i int) *int32 {
   678  	i32 := int32(i)
   679  	return &i32
   680  }