sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machineset_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  	"context"
    21  	"strings"
    22  	"testing"
    23  
    24  	. "github.com/onsi/gomega"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/utils/ptr"
    28  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    29  
    30  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    31  	"sigs.k8s.io/cluster-api/internal/webhooks/util"
    32  )
    33  
    34  func TestMachineSetDefault(t *testing.T) {
    35  	g := NewWithT(t)
    36  	ms := &clusterv1.MachineSet{
    37  		ObjectMeta: metav1.ObjectMeta{
    38  			Name: "test-ms",
    39  		},
    40  		Spec: clusterv1.MachineSetSpec{
    41  			Template: clusterv1.MachineTemplateSpec{
    42  				Spec: clusterv1.MachineSpec{
    43  					Version: ptr.To("1.19.10"),
    44  				},
    45  			},
    46  		},
    47  	}
    48  
    49  	scheme := runtime.NewScheme()
    50  	g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed())
    51  	webhook := &MachineSet{
    52  		decoder: admission.NewDecoder(scheme),
    53  	}
    54  
    55  	reqCtx := admission.NewContextWithRequest(ctx, admission.Request{})
    56  	t.Run("for MachineSet", util.CustomDefaultValidateTest(reqCtx, ms, webhook))
    57  	g.Expect(webhook.Default(reqCtx, ms)).To(Succeed())
    58  
    59  	g.Expect(ms.Labels[clusterv1.ClusterNameLabel]).To(Equal(ms.Spec.ClusterName))
    60  	g.Expect(ms.Spec.DeletePolicy).To(Equal(string(clusterv1.RandomMachineSetDeletePolicy)))
    61  	g.Expect(ms.Spec.Selector.MatchLabels).To(HaveKeyWithValue(clusterv1.MachineSetNameLabel, "test-ms"))
    62  	g.Expect(ms.Spec.Template.Labels).To(HaveKeyWithValue(clusterv1.MachineSetNameLabel, "test-ms"))
    63  	g.Expect(*ms.Spec.Template.Spec.Version).To(Equal("v1.19.10"))
    64  }
    65  
    66  func TestCalculateMachineSetReplicas(t *testing.T) {
    67  	tests := []struct {
    68  		name             string
    69  		newMS            *clusterv1.MachineSet
    70  		oldMS            *clusterv1.MachineSet
    71  		expectedReplicas int32
    72  		expectErr        bool
    73  	}{
    74  		{
    75  			name: "if new MS has replicas set, keep that value",
    76  			newMS: &clusterv1.MachineSet{
    77  				Spec: clusterv1.MachineSetSpec{
    78  					Replicas: ptr.To[int32](5),
    79  				},
    80  			},
    81  			expectedReplicas: 5,
    82  		},
    83  		{
    84  			name:             "if new MS does not have replicas set and no annotations, use 1",
    85  			newMS:            &clusterv1.MachineSet{},
    86  			expectedReplicas: 1,
    87  		},
    88  		{
    89  			name: "if new MS only has min size annotation, fallback to 1",
    90  			newMS: &clusterv1.MachineSet{
    91  				ObjectMeta: metav1.ObjectMeta{
    92  					Annotations: map[string]string{
    93  						clusterv1.AutoscalerMinSizeAnnotation: "3",
    94  					},
    95  				},
    96  			},
    97  			expectedReplicas: 1,
    98  		},
    99  		{
   100  			name: "if new MS only has max size annotation, fallback to 1",
   101  			newMS: &clusterv1.MachineSet{
   102  				ObjectMeta: metav1.ObjectMeta{
   103  					Annotations: map[string]string{
   104  						clusterv1.AutoscalerMaxSizeAnnotation: "7",
   105  					},
   106  				},
   107  			},
   108  			expectedReplicas: 1,
   109  		},
   110  		{
   111  			name: "if new MS has min and max size annotation and min size is invalid, fail",
   112  			newMS: &clusterv1.MachineSet{
   113  				ObjectMeta: metav1.ObjectMeta{
   114  					Annotations: map[string]string{
   115  						clusterv1.AutoscalerMinSizeAnnotation: "abc",
   116  						clusterv1.AutoscalerMaxSizeAnnotation: "7",
   117  					},
   118  				},
   119  			},
   120  			expectErr: true,
   121  		},
   122  		{
   123  			name: "if new MS has min and max size annotation and max size is invalid, fail",
   124  			newMS: &clusterv1.MachineSet{
   125  				ObjectMeta: metav1.ObjectMeta{
   126  					Annotations: map[string]string{
   127  						clusterv1.AutoscalerMinSizeAnnotation: "3",
   128  						clusterv1.AutoscalerMaxSizeAnnotation: "abc",
   129  					},
   130  				},
   131  			},
   132  			expectErr: true,
   133  		},
   134  		{
   135  			name: "if new MS has min and max size annotation and new MS is a new MS, use min size",
   136  			newMS: &clusterv1.MachineSet{
   137  				ObjectMeta: metav1.ObjectMeta{
   138  					Annotations: map[string]string{
   139  						clusterv1.AutoscalerMinSizeAnnotation: "3",
   140  						clusterv1.AutoscalerMaxSizeAnnotation: "7",
   141  					},
   142  				},
   143  			},
   144  			expectedReplicas: 3,
   145  		},
   146  		{
   147  			name: "if new MS has min and max size annotation and old MS doesn't have replicas set, use min size",
   148  			newMS: &clusterv1.MachineSet{
   149  				ObjectMeta: metav1.ObjectMeta{
   150  					Annotations: map[string]string{
   151  						clusterv1.AutoscalerMinSizeAnnotation: "3",
   152  						clusterv1.AutoscalerMaxSizeAnnotation: "7",
   153  					},
   154  				},
   155  			},
   156  			oldMS:            &clusterv1.MachineSet{},
   157  			expectedReplicas: 3,
   158  		},
   159  		{
   160  			name: "if new MS has min and max size annotation and old MS replicas is below min size, use min size",
   161  			newMS: &clusterv1.MachineSet{
   162  				ObjectMeta: metav1.ObjectMeta{
   163  					Annotations: map[string]string{
   164  						clusterv1.AutoscalerMinSizeAnnotation: "3",
   165  						clusterv1.AutoscalerMaxSizeAnnotation: "7",
   166  					},
   167  				},
   168  			},
   169  			oldMS: &clusterv1.MachineSet{
   170  				Spec: clusterv1.MachineSetSpec{
   171  					Replicas: ptr.To[int32](1),
   172  				},
   173  			},
   174  			expectedReplicas: 3,
   175  		},
   176  		{
   177  			name: "if new MS has min and max size annotation and old MS replicas is above max size, use max size",
   178  			newMS: &clusterv1.MachineSet{
   179  				ObjectMeta: metav1.ObjectMeta{
   180  					Annotations: map[string]string{
   181  						clusterv1.AutoscalerMinSizeAnnotation: "3",
   182  						clusterv1.AutoscalerMaxSizeAnnotation: "7",
   183  					},
   184  				},
   185  			},
   186  			oldMS: &clusterv1.MachineSet{
   187  				Spec: clusterv1.MachineSetSpec{
   188  					Replicas: ptr.To[int32](15),
   189  				},
   190  			},
   191  			expectedReplicas: 7,
   192  		},
   193  		{
   194  			name: "if new MS has min and max size annotation and old MS replicas is between min and max size, use old MS replicas",
   195  			newMS: &clusterv1.MachineSet{
   196  				ObjectMeta: metav1.ObjectMeta{
   197  					Annotations: map[string]string{
   198  						clusterv1.AutoscalerMinSizeAnnotation: "3",
   199  						clusterv1.AutoscalerMaxSizeAnnotation: "7",
   200  					},
   201  				},
   202  			},
   203  			oldMS: &clusterv1.MachineSet{
   204  				Spec: clusterv1.MachineSetSpec{
   205  					Replicas: ptr.To[int32](4),
   206  				},
   207  			},
   208  			expectedReplicas: 4,
   209  		},
   210  	}
   211  
   212  	for _, tt := range tests {
   213  		t.Run(tt.name, func(t *testing.T) {
   214  			g := NewWithT(t)
   215  
   216  			replicas, err := calculateMachineSetReplicas(context.Background(), tt.oldMS, tt.newMS, false)
   217  
   218  			if tt.expectErr {
   219  				g.Expect(err).To(HaveOccurred())
   220  				return
   221  			}
   222  
   223  			g.Expect(err).ToNot(HaveOccurred())
   224  			g.Expect(replicas).To(Equal(tt.expectedReplicas))
   225  		})
   226  	}
   227  }
   228  
   229  func TestMachineSetLabelSelectorMatchValidation(t *testing.T) {
   230  	tests := []struct {
   231  		name      string
   232  		selectors map[string]string
   233  		labels    map[string]string
   234  		expectErr bool
   235  	}{
   236  		{
   237  			name:      "should return error on mismatch",
   238  			selectors: map[string]string{"foo": "bar"},
   239  			labels:    map[string]string{"foo": "baz"},
   240  			expectErr: true,
   241  		},
   242  		{
   243  			name:      "should return error on missing labels",
   244  			selectors: map[string]string{"foo": "bar"},
   245  			labels:    map[string]string{"": ""},
   246  			expectErr: true,
   247  		},
   248  		{
   249  			name:      "should return error if all selectors don't match",
   250  			selectors: map[string]string{"foo": "bar", "hello": "world"},
   251  			labels:    map[string]string{"foo": "bar"},
   252  			expectErr: true,
   253  		},
   254  		{
   255  			name:      "should not return error on match",
   256  			selectors: map[string]string{"foo": "bar"},
   257  			labels:    map[string]string{"foo": "bar"},
   258  			expectErr: false,
   259  		},
   260  		{
   261  			name:      "should return error for invalid selector",
   262  			selectors: map[string]string{"-123-foo": "bar"},
   263  			labels:    map[string]string{"-123-foo": "bar"},
   264  			expectErr: true,
   265  		},
   266  	}
   267  
   268  	for _, tt := range tests {
   269  		t.Run(tt.name, func(t *testing.T) {
   270  			g := NewWithT(t)
   271  			ms := &clusterv1.MachineSet{
   272  				Spec: clusterv1.MachineSetSpec{
   273  					Selector: metav1.LabelSelector{
   274  						MatchLabels: tt.selectors,
   275  					},
   276  					Template: clusterv1.MachineTemplateSpec{
   277  						ObjectMeta: clusterv1.ObjectMeta{
   278  							Labels: tt.labels,
   279  						},
   280  					},
   281  				},
   282  			}
   283  			webhook := &MachineSet{}
   284  
   285  			if tt.expectErr {
   286  				warnings, err := webhook.ValidateCreate(ctx, ms)
   287  				g.Expect(err).To(HaveOccurred())
   288  				g.Expect(warnings).To(BeEmpty())
   289  				warnings, err = webhook.ValidateUpdate(ctx, ms, ms)
   290  				g.Expect(err).To(HaveOccurred())
   291  				g.Expect(warnings).To(BeEmpty())
   292  			} else {
   293  				warnings, err := webhook.ValidateCreate(ctx, ms)
   294  				g.Expect(err).ToNot(HaveOccurred())
   295  				g.Expect(warnings).To(BeEmpty())
   296  				warnings, err = webhook.ValidateUpdate(ctx, ms, ms)
   297  				g.Expect(err).ToNot(HaveOccurred())
   298  				g.Expect(warnings).To(BeEmpty())
   299  			}
   300  		})
   301  	}
   302  }
   303  
   304  func TestMachineSetClusterNameImmutable(t *testing.T) {
   305  	tests := []struct {
   306  		name           string
   307  		oldClusterName string
   308  		newClusterName string
   309  		expectErr      bool
   310  	}{
   311  		{
   312  			name:           "when the cluster name has not changed",
   313  			oldClusterName: "foo",
   314  			newClusterName: "foo",
   315  			expectErr:      false,
   316  		},
   317  		{
   318  			name:           "when the cluster name has changed",
   319  			oldClusterName: "foo",
   320  			newClusterName: "bar",
   321  			expectErr:      true,
   322  		},
   323  	}
   324  
   325  	for _, tt := range tests {
   326  		t.Run(tt.name, func(t *testing.T) {
   327  			g := NewWithT(t)
   328  
   329  			newMS := &clusterv1.MachineSet{
   330  				Spec: clusterv1.MachineSetSpec{
   331  					ClusterName: tt.newClusterName,
   332  				},
   333  			}
   334  
   335  			oldMS := &clusterv1.MachineSet{
   336  				Spec: clusterv1.MachineSetSpec{
   337  					ClusterName: tt.oldClusterName,
   338  				},
   339  			}
   340  
   341  			warnings, err := (&MachineSet{}).ValidateUpdate(ctx, oldMS, newMS)
   342  			if tt.expectErr {
   343  				g.Expect(err).To(HaveOccurred())
   344  			} else {
   345  				g.Expect(err).ToNot(HaveOccurred())
   346  			}
   347  			g.Expect(warnings).To(BeEmpty())
   348  		})
   349  	}
   350  }
   351  
   352  func TestMachineSetVersionValidation(t *testing.T) {
   353  	tests := []struct {
   354  		name      string
   355  		version   string
   356  		expectErr bool
   357  	}{
   358  		{
   359  			name:      "should succeed when given a valid semantic version with prepended 'v'",
   360  			version:   "v1.19.2",
   361  			expectErr: false,
   362  		},
   363  		{
   364  			name:      "should return error when given a valid semantic version without 'v'",
   365  			version:   "1.19.2",
   366  			expectErr: true,
   367  		},
   368  		{
   369  			name:      "should return error when given an invalid semantic version",
   370  			version:   "1",
   371  			expectErr: true,
   372  		},
   373  		{
   374  			name:      "should return error when given an invalid semantic version",
   375  			version:   "v1",
   376  			expectErr: true,
   377  		},
   378  		{
   379  			name:      "should return error when given an invalid semantic version",
   380  			version:   "wrong_version",
   381  			expectErr: true,
   382  		},
   383  	}
   384  
   385  	for _, tt := range tests {
   386  		t.Run(tt.name, func(t *testing.T) {
   387  			g := NewWithT(t)
   388  
   389  			ms := &clusterv1.MachineSet{
   390  				Spec: clusterv1.MachineSetSpec{
   391  					Template: clusterv1.MachineTemplateSpec{
   392  						Spec: clusterv1.MachineSpec{
   393  							Version: ptr.To(tt.version),
   394  						},
   395  					},
   396  				},
   397  			}
   398  			webhook := &MachineSet{}
   399  
   400  			if tt.expectErr {
   401  				warnings, err := webhook.ValidateCreate(ctx, ms)
   402  				g.Expect(err).To(HaveOccurred())
   403  				g.Expect(warnings).To(BeEmpty())
   404  				warnings, err = webhook.ValidateUpdate(ctx, ms, ms)
   405  				g.Expect(err).To(HaveOccurred())
   406  				g.Expect(warnings).To(BeEmpty())
   407  			} else {
   408  				warnings, err := webhook.ValidateCreate(ctx, ms)
   409  				g.Expect(err).ToNot(HaveOccurred())
   410  				g.Expect(warnings).To(BeEmpty())
   411  				warnings, err = webhook.ValidateUpdate(ctx, ms, ms)
   412  				g.Expect(err).ToNot(HaveOccurred())
   413  				g.Expect(warnings).To(BeEmpty())
   414  			}
   415  		})
   416  	}
   417  }
   418  
   419  func TestValidateSkippedMachineSetPreflightChecks(t *testing.T) {
   420  	tests := []struct {
   421  		name      string
   422  		ms        *clusterv1.MachineSet
   423  		expectErr bool
   424  	}{
   425  		{
   426  			name:      "should pass if the machine set skip preflight checks annotation is not set",
   427  			ms:        &clusterv1.MachineSet{},
   428  			expectErr: false,
   429  		},
   430  		{
   431  			name: "should pass if not preflight checks are skipped",
   432  			ms: &clusterv1.MachineSet{
   433  				ObjectMeta: metav1.ObjectMeta{
   434  					Annotations: map[string]string{
   435  						clusterv1.MachineSetSkipPreflightChecksAnnotation: "",
   436  					},
   437  				},
   438  			},
   439  			expectErr: false,
   440  		},
   441  		{
   442  			name: "should pass if only valid preflight checks are skipped (single)",
   443  			ms: &clusterv1.MachineSet{
   444  				ObjectMeta: metav1.ObjectMeta{
   445  					Annotations: map[string]string{
   446  						clusterv1.MachineSetSkipPreflightChecksAnnotation: string(clusterv1.MachineSetPreflightCheckKubeadmVersionSkew),
   447  					},
   448  				},
   449  			},
   450  			expectErr: false,
   451  		},
   452  		{
   453  			name: "should pass if only valid preflight checks are skipped (multiple)",
   454  			ms: &clusterv1.MachineSet{
   455  				ObjectMeta: metav1.ObjectMeta{
   456  					Annotations: map[string]string{
   457  						clusterv1.MachineSetSkipPreflightChecksAnnotation: string(clusterv1.MachineSetPreflightCheckKubeadmVersionSkew) + "," + string(clusterv1.MachineSetPreflightCheckControlPlaneIsStable),
   458  					},
   459  				},
   460  			},
   461  			expectErr: false,
   462  		},
   463  		{
   464  			name: "should fail if invalid preflight checks are skipped",
   465  			ms: &clusterv1.MachineSet{
   466  				ObjectMeta: metav1.ObjectMeta{
   467  					Annotations: map[string]string{
   468  						clusterv1.MachineSetSkipPreflightChecksAnnotation: string(clusterv1.MachineSetPreflightCheckKubeadmVersionSkew) + ",invalid-preflight-check-name",
   469  					},
   470  				},
   471  			},
   472  			expectErr: true,
   473  		},
   474  	}
   475  
   476  	for _, tt := range tests {
   477  		t.Run(tt.name, func(t *testing.T) {
   478  			g := NewWithT(t)
   479  			err := validateSkippedMachineSetPreflightChecks(tt.ms)
   480  			if tt.expectErr {
   481  				g.Expect(err).To(HaveOccurred())
   482  			} else {
   483  				g.Expect(err).ToNot(HaveOccurred())
   484  			}
   485  		})
   486  	}
   487  }
   488  
   489  func TestMachineSetTemplateMetadataValidation(t *testing.T) {
   490  	tests := []struct {
   491  		name        string
   492  		labels      map[string]string
   493  		annotations map[string]string
   494  		expectErr   bool
   495  	}{
   496  		{
   497  			name: "should return error for invalid labels and annotations",
   498  			labels: map[string]string{
   499  				"foo":          "$invalid-key",
   500  				"bar":          strings.Repeat("a", 64) + "too-long-value",
   501  				"/invalid-key": "foo",
   502  			},
   503  			annotations: map[string]string{
   504  				"/invalid-key": "foo",
   505  			},
   506  			expectErr: true,
   507  		},
   508  	}
   509  
   510  	for _, tt := range tests {
   511  		t.Run(tt.name, func(t *testing.T) {
   512  			g := NewWithT(t)
   513  			ms := &clusterv1.MachineSet{
   514  				Spec: clusterv1.MachineSetSpec{
   515  					Template: clusterv1.MachineTemplateSpec{
   516  						ObjectMeta: clusterv1.ObjectMeta{
   517  							Labels:      tt.labels,
   518  							Annotations: tt.annotations,
   519  						},
   520  					},
   521  				},
   522  			}
   523  
   524  			webhook := &MachineSet{}
   525  
   526  			if tt.expectErr {
   527  				warnings, err := webhook.ValidateCreate(ctx, ms)
   528  				g.Expect(err).To(HaveOccurred())
   529  				g.Expect(warnings).To(BeEmpty())
   530  				warnings, err = webhook.ValidateUpdate(ctx, ms, ms)
   531  				g.Expect(err).To(HaveOccurred())
   532  				g.Expect(warnings).To(BeEmpty())
   533  			} else {
   534  				warnings, err := webhook.ValidateCreate(ctx, ms)
   535  				g.Expect(err).ToNot(HaveOccurred())
   536  				g.Expect(warnings).To(BeEmpty())
   537  				warnings, err = webhook.ValidateUpdate(ctx, ms, ms)
   538  				g.Expect(err).ToNot(HaveOccurred())
   539  				g.Expect(warnings).To(BeEmpty())
   540  			}
   541  		})
   542  	}
   543  }