sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machinehealthcheck_test.go (about)

     1  /*
     2  Copyright 2021 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 webhooks
    18  
    19  import (
    20  	"testing"
    21  	"time"
    22  
    23  	. "github.com/onsi/gomega"
    24  	corev1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/util/intstr"
    27  
    28  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    29  	"sigs.k8s.io/cluster-api/internal/webhooks/util"
    30  )
    31  
    32  func TestMachineHealthCheckDefault(t *testing.T) {
    33  	g := NewWithT(t)
    34  	mhc := &clusterv1.MachineHealthCheck{
    35  		ObjectMeta: metav1.ObjectMeta{
    36  			Namespace: "foo",
    37  		},
    38  		Spec: clusterv1.MachineHealthCheckSpec{
    39  			Selector: metav1.LabelSelector{
    40  				MatchLabels: map[string]string{"foo": "bar"},
    41  			},
    42  			RemediationTemplate: &corev1.ObjectReference{},
    43  			UnhealthyConditions: []clusterv1.UnhealthyCondition{
    44  				{
    45  					Type:   corev1.NodeReady,
    46  					Status: corev1.ConditionFalse,
    47  				},
    48  			},
    49  		},
    50  	}
    51  	webhook := &MachineHealthCheck{}
    52  
    53  	t.Run("for MachineHealthCheck", util.CustomDefaultValidateTest(ctx, mhc, webhook))
    54  	g.Expect(webhook.Default(ctx, mhc)).To(Succeed())
    55  
    56  	g.Expect(mhc.Labels[clusterv1.ClusterNameLabel]).To(Equal(mhc.Spec.ClusterName))
    57  	g.Expect(mhc.Spec.MaxUnhealthy.String()).To(Equal("100%"))
    58  	g.Expect(mhc.Spec.NodeStartupTimeout).ToNot(BeNil())
    59  	g.Expect(*mhc.Spec.NodeStartupTimeout).To(BeComparableTo(metav1.Duration{Duration: 10 * time.Minute}))
    60  	g.Expect(mhc.Spec.RemediationTemplate.Namespace).To(Equal(mhc.Namespace))
    61  }
    62  
    63  func TestMachineHealthCheckLabelSelectorAsSelectorValidation(t *testing.T) {
    64  	tests := []struct {
    65  		name      string
    66  		selectors map[string]string
    67  		expectErr bool
    68  	}{
    69  		{
    70  			name:      "should not return error for valid selector",
    71  			selectors: map[string]string{"foo": "bar"},
    72  			expectErr: false,
    73  		},
    74  		{
    75  			name:      "should return error for invalid selector",
    76  			selectors: map[string]string{"-123-foo": "bar"},
    77  			expectErr: true,
    78  		},
    79  	}
    80  
    81  	for _, tt := range tests {
    82  		t.Run(tt.name, func(t *testing.T) {
    83  			g := NewWithT(t)
    84  			mhc := &clusterv1.MachineHealthCheck{
    85  				Spec: clusterv1.MachineHealthCheckSpec{
    86  					Selector: metav1.LabelSelector{
    87  						MatchLabels: tt.selectors,
    88  					},
    89  					UnhealthyConditions: []clusterv1.UnhealthyCondition{
    90  						{
    91  							Type:   corev1.NodeReady,
    92  							Status: corev1.ConditionFalse,
    93  						},
    94  					},
    95  				},
    96  			}
    97  			webhook := &MachineHealthCheck{}
    98  
    99  			if tt.expectErr {
   100  				warnings, err := webhook.ValidateCreate(ctx, mhc)
   101  				g.Expect(err).To(HaveOccurred())
   102  				g.Expect(warnings).To(BeEmpty())
   103  				warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   104  				g.Expect(err).To(HaveOccurred())
   105  				g.Expect(warnings).To(BeEmpty())
   106  			} else {
   107  				warnings, err := webhook.ValidateCreate(ctx, mhc)
   108  				g.Expect(err).ToNot(HaveOccurred())
   109  				g.Expect(warnings).To(BeEmpty())
   110  				warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   111  				g.Expect(err).ToNot(HaveOccurred())
   112  				g.Expect(warnings).To(BeEmpty())
   113  			}
   114  		})
   115  	}
   116  }
   117  
   118  func TestMachineHealthCheckClusterNameImmutable(t *testing.T) {
   119  	tests := []struct {
   120  		name           string
   121  		oldClusterName string
   122  		newClusterName string
   123  		expectErr      bool
   124  	}{
   125  		{
   126  			name:           "when the cluster name has not changed",
   127  			oldClusterName: "foo",
   128  			newClusterName: "foo",
   129  			expectErr:      false,
   130  		},
   131  		{
   132  			name:           "when the cluster name has changed",
   133  			oldClusterName: "foo",
   134  			newClusterName: "bar",
   135  			expectErr:      true,
   136  		},
   137  	}
   138  
   139  	for _, tt := range tests {
   140  		t.Run(tt.name, func(t *testing.T) {
   141  			g := NewWithT(t)
   142  
   143  			newMHC := &clusterv1.MachineHealthCheck{
   144  				Spec: clusterv1.MachineHealthCheckSpec{
   145  					ClusterName: tt.newClusterName,
   146  					Selector: metav1.LabelSelector{
   147  						MatchLabels: map[string]string{
   148  							"test": "test",
   149  						},
   150  					},
   151  					UnhealthyConditions: []clusterv1.UnhealthyCondition{
   152  						{
   153  							Type:   corev1.NodeReady,
   154  							Status: corev1.ConditionFalse,
   155  						},
   156  					},
   157  				},
   158  			}
   159  			oldMHC := &clusterv1.MachineHealthCheck{
   160  				Spec: clusterv1.MachineHealthCheckSpec{
   161  					ClusterName: tt.oldClusterName,
   162  					Selector: metav1.LabelSelector{
   163  						MatchLabels: map[string]string{
   164  							"test": "test",
   165  						},
   166  					},
   167  					UnhealthyConditions: []clusterv1.UnhealthyCondition{
   168  						{
   169  							Type:   corev1.NodeReady,
   170  							Status: corev1.ConditionFalse,
   171  						},
   172  					},
   173  				},
   174  			}
   175  
   176  			warnings, err := (&MachineHealthCheck{}).ValidateUpdate(ctx, oldMHC, newMHC)
   177  			if tt.expectErr {
   178  				g.Expect(err).To(HaveOccurred())
   179  			} else {
   180  				g.Expect(err).ToNot(HaveOccurred())
   181  			}
   182  			g.Expect(warnings).To(BeEmpty())
   183  		})
   184  	}
   185  }
   186  
   187  func TestMachineHealthCheckUnhealthyConditions(t *testing.T) {
   188  	tests := []struct {
   189  		name               string
   190  		unhealthConditions []clusterv1.UnhealthyCondition
   191  		expectErr          bool
   192  	}{
   193  		{
   194  			name: "pass with correctly defined unhealthyConditions",
   195  			unhealthConditions: []clusterv1.UnhealthyCondition{
   196  				{
   197  					Type:   corev1.NodeReady,
   198  					Status: corev1.ConditionFalse,
   199  				},
   200  			},
   201  			expectErr: false,
   202  		},
   203  		{
   204  			name:               "fail if the UnhealthCondition array is nil",
   205  			unhealthConditions: nil,
   206  			expectErr:          true,
   207  		},
   208  		{
   209  			name:               "fail if the UnhealthCondition array is empty",
   210  			unhealthConditions: []clusterv1.UnhealthyCondition{},
   211  			expectErr:          true,
   212  		},
   213  	}
   214  
   215  	for _, tt := range tests {
   216  		t.Run(tt.name, func(t *testing.T) {
   217  			g := NewWithT(t)
   218  			mhc := &clusterv1.MachineHealthCheck{
   219  				Spec: clusterv1.MachineHealthCheckSpec{
   220  					Selector: metav1.LabelSelector{
   221  						MatchLabels: map[string]string{
   222  							"test": "test",
   223  						},
   224  					},
   225  					UnhealthyConditions: tt.unhealthConditions,
   226  				},
   227  			}
   228  			webhook := &MachineHealthCheck{}
   229  
   230  			if tt.expectErr {
   231  				warnings, err := webhook.ValidateCreate(ctx, mhc)
   232  				g.Expect(err).To(HaveOccurred())
   233  				g.Expect(warnings).To(BeEmpty())
   234  				warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   235  				g.Expect(err).To(HaveOccurred())
   236  				g.Expect(warnings).To(BeEmpty())
   237  			} else {
   238  				warnings, err := webhook.ValidateCreate(ctx, mhc)
   239  				g.Expect(err).ToNot(HaveOccurred())
   240  				g.Expect(warnings).To(BeEmpty())
   241  				warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   242  				g.Expect(err).ToNot(HaveOccurred())
   243  				g.Expect(warnings).To(BeEmpty())
   244  			}
   245  		})
   246  	}
   247  }
   248  
   249  func TestMachineHealthCheckNodeStartupTimeout(t *testing.T) {
   250  	zero := metav1.Duration{Duration: 0}
   251  	twentyNineSeconds := metav1.Duration{Duration: 29 * time.Second}
   252  	thirtySeconds := metav1.Duration{Duration: 30 * time.Second}
   253  	oneMinute := metav1.Duration{Duration: 1 * time.Minute}
   254  	minusOneMinute := metav1.Duration{Duration: -1 * time.Minute}
   255  
   256  	tests := []struct {
   257  		name      string
   258  		timeout   *metav1.Duration
   259  		expectErr bool
   260  	}{
   261  		{
   262  			name:      "when the nodeStartupTimeout is not given",
   263  			timeout:   nil,
   264  			expectErr: false,
   265  		},
   266  		{
   267  			name:      "when the nodeStartupTimeout is greater than 30s",
   268  			timeout:   &oneMinute,
   269  			expectErr: false,
   270  		},
   271  		{
   272  			name:      "when the nodeStartupTimeout is 30s",
   273  			timeout:   &thirtySeconds,
   274  			expectErr: false,
   275  		},
   276  		{
   277  			name:      "when the nodeStartupTimeout is 29s",
   278  			timeout:   &twentyNineSeconds,
   279  			expectErr: true,
   280  		},
   281  		{
   282  			name:      "when the nodeStartupTimeout is less than 0",
   283  			timeout:   &minusOneMinute,
   284  			expectErr: true,
   285  		},
   286  		{
   287  			name:      "when the nodeStartupTimeout is 0 (disabled)",
   288  			timeout:   &zero,
   289  			expectErr: false,
   290  		},
   291  	}
   292  
   293  	for _, tt := range tests {
   294  		g := NewWithT(t)
   295  
   296  		mhc := &clusterv1.MachineHealthCheck{
   297  			Spec: clusterv1.MachineHealthCheckSpec{
   298  				NodeStartupTimeout: tt.timeout,
   299  				Selector: metav1.LabelSelector{
   300  					MatchLabels: map[string]string{
   301  						"test": "test",
   302  					},
   303  				},
   304  				UnhealthyConditions: []clusterv1.UnhealthyCondition{
   305  					{
   306  						Type:   corev1.NodeReady,
   307  						Status: corev1.ConditionFalse,
   308  					},
   309  				},
   310  			},
   311  		}
   312  		webhook := &MachineHealthCheck{}
   313  
   314  		if tt.expectErr {
   315  			warnings, err := webhook.ValidateCreate(ctx, mhc)
   316  			g.Expect(err).To(HaveOccurred())
   317  			g.Expect(warnings).To(BeEmpty())
   318  			warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   319  			g.Expect(err).To(HaveOccurred())
   320  			g.Expect(warnings).To(BeEmpty())
   321  		} else {
   322  			warnings, err := webhook.ValidateCreate(ctx, mhc)
   323  			g.Expect(err).ToNot(HaveOccurred())
   324  			g.Expect(warnings).To(BeEmpty())
   325  			warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   326  			g.Expect(err).ToNot(HaveOccurred())
   327  			g.Expect(warnings).To(BeEmpty())
   328  		}
   329  	}
   330  }
   331  
   332  func TestMachineHealthCheckMaxUnhealthy(t *testing.T) {
   333  	tests := []struct {
   334  		name      string
   335  		value     intstr.IntOrString
   336  		expectErr bool
   337  	}{
   338  		{
   339  			name:      "when the value is an integer",
   340  			value:     intstr.Parse("10"),
   341  			expectErr: false,
   342  		},
   343  		{
   344  			name:      "when the value is a percentage",
   345  			value:     intstr.Parse("10%"),
   346  			expectErr: false,
   347  		},
   348  		{
   349  			name:      "when the value is a random string",
   350  			value:     intstr.Parse("abcdef"),
   351  			expectErr: true,
   352  		},
   353  		{
   354  			name:      "when the value stringified integer",
   355  			value:     intstr.FromString("10"),
   356  			expectErr: true,
   357  		},
   358  	}
   359  
   360  	for _, tt := range tests {
   361  		g := NewWithT(t)
   362  
   363  		maxUnhealthy := tt.value
   364  		mhc := &clusterv1.MachineHealthCheck{
   365  			Spec: clusterv1.MachineHealthCheckSpec{
   366  				MaxUnhealthy: &maxUnhealthy,
   367  				Selector: metav1.LabelSelector{
   368  					MatchLabels: map[string]string{
   369  						"test": "test",
   370  					},
   371  				},
   372  				UnhealthyConditions: []clusterv1.UnhealthyCondition{
   373  					{
   374  						Type:   corev1.NodeReady,
   375  						Status: corev1.ConditionFalse,
   376  					},
   377  				},
   378  			},
   379  		}
   380  		webhook := &MachineHealthCheck{}
   381  
   382  		if tt.expectErr {
   383  			warnings, err := webhook.ValidateCreate(ctx, mhc)
   384  			g.Expect(err).To(HaveOccurred())
   385  			g.Expect(warnings).To(BeEmpty())
   386  			warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   387  			g.Expect(err).To(HaveOccurred())
   388  			g.Expect(warnings).To(BeEmpty())
   389  		} else {
   390  			warnings, err := webhook.ValidateCreate(ctx, mhc)
   391  			g.Expect(err).ToNot(HaveOccurred())
   392  			g.Expect(warnings).To(BeEmpty())
   393  			warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc)
   394  			g.Expect(err).ToNot(HaveOccurred())
   395  			g.Expect(warnings).To(BeEmpty())
   396  		}
   397  	}
   398  }
   399  
   400  func TestMachineHealthCheckSelectorValidation(t *testing.T) {
   401  	g := NewWithT(t)
   402  	mhc := &clusterv1.MachineHealthCheck{
   403  		Spec: clusterv1.MachineHealthCheckSpec{
   404  			UnhealthyConditions: []clusterv1.UnhealthyCondition{
   405  				{
   406  					Type:   corev1.NodeReady,
   407  					Status: corev1.ConditionFalse,
   408  				},
   409  			},
   410  		},
   411  	}
   412  	webhook := &MachineHealthCheck{}
   413  
   414  	err := webhook.validate(nil, mhc)
   415  	g.Expect(err).To(HaveOccurred())
   416  	g.Expect(err.Error()).To(ContainSubstring("selector must not be empty"))
   417  }
   418  
   419  func TestMachineHealthCheckClusterNameSelectorValidation(t *testing.T) {
   420  	g := NewWithT(t)
   421  	mhc := &clusterv1.MachineHealthCheck{
   422  		Spec: clusterv1.MachineHealthCheckSpec{
   423  			ClusterName: "foo",
   424  			Selector: metav1.LabelSelector{
   425  				MatchLabels: map[string]string{
   426  					clusterv1.ClusterNameLabel: "bar",
   427  					"baz":                      "qux",
   428  				},
   429  			},
   430  			UnhealthyConditions: []clusterv1.UnhealthyCondition{
   431  				{
   432  					Type:   corev1.NodeReady,
   433  					Status: corev1.ConditionFalse,
   434  				},
   435  			},
   436  		},
   437  	}
   438  	webhook := &MachineHealthCheck{}
   439  
   440  	err := webhook.validate(nil, mhc)
   441  	g.Expect(err).To(HaveOccurred())
   442  	g.Expect(err.Error()).To(ContainSubstring("cannot specify a cluster selector other than the one specified by ClusterName"))
   443  
   444  	mhc.Spec.Selector.MatchLabels[clusterv1.ClusterNameLabel] = "foo"
   445  	g.Expect(webhook.validate(nil, mhc)).To(Succeed())
   446  	delete(mhc.Spec.Selector.MatchLabels, clusterv1.ClusterNameLabel)
   447  	g.Expect(webhook.validate(nil, mhc)).To(Succeed())
   448  }
   449  
   450  func TestMachineHealthCheckRemediationTemplateNamespaceValidation(t *testing.T) {
   451  	valid := &clusterv1.MachineHealthCheck{
   452  		ObjectMeta: metav1.ObjectMeta{
   453  			Namespace: "foo",
   454  		},
   455  		Spec: clusterv1.MachineHealthCheckSpec{
   456  			Selector:            metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
   457  			RemediationTemplate: &corev1.ObjectReference{Namespace: "foo"},
   458  			UnhealthyConditions: []clusterv1.UnhealthyCondition{
   459  				{
   460  					Type:   corev1.NodeReady,
   461  					Status: corev1.ConditionFalse,
   462  				},
   463  			},
   464  		},
   465  	}
   466  	invalid := valid.DeepCopy()
   467  	invalid.Spec.RemediationTemplate.Namespace = "bar"
   468  
   469  	tests := []struct {
   470  		name      string
   471  		expectErr bool
   472  		c         *clusterv1.MachineHealthCheck
   473  	}{
   474  		{
   475  			name:      "should return error when MachineHealthCheck namespace and RemediationTemplate ref namespace mismatch",
   476  			expectErr: true,
   477  			c:         invalid,
   478  		},
   479  		{
   480  			name:      "should succeed when namespaces match",
   481  			expectErr: false,
   482  			c:         valid,
   483  		},
   484  	}
   485  
   486  	for _, tt := range tests {
   487  		t.Run(tt.name, func(t *testing.T) {
   488  			g := NewWithT(t)
   489  			webhook := &MachineHealthCheck{}
   490  
   491  			if tt.expectErr {
   492  				g.Expect(webhook.validate(nil, tt.c)).NotTo(Succeed())
   493  			} else {
   494  				g.Expect(webhook.validate(nil, tt.c)).To(Succeed())
   495  			}
   496  		})
   497  	}
   498  }