sigs.k8s.io/cluster-api@v1.6.3/exp/internal/webhooks/machinepool_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  	"strings"
    21  	"testing"
    22  
    23  	. "github.com/onsi/gomega"
    24  	corev1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	utilfeature "k8s.io/component-base/featuregate/testing"
    27  	"k8s.io/utils/pointer"
    28  	ctrl "sigs.k8s.io/controller-runtime"
    29  
    30  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    31  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    32  	"sigs.k8s.io/cluster-api/feature"
    33  	"sigs.k8s.io/cluster-api/internal/webhooks/util"
    34  )
    35  
    36  var ctx = ctrl.SetupSignalHandler()
    37  
    38  func TestMachinePoolDefault(t *testing.T) {
    39  	// NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool.
    40  	// Enabling the feature flag temporarily for this test.
    41  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)()
    42  
    43  	g := NewWithT(t)
    44  
    45  	mp := &expv1.MachinePool{
    46  		ObjectMeta: metav1.ObjectMeta{
    47  			Namespace: "foobar",
    48  		},
    49  		Spec: expv1.MachinePoolSpec{
    50  			Template: clusterv1.MachineTemplateSpec{
    51  				Spec: clusterv1.MachineSpec{
    52  					Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}},
    53  					Version:   pointer.String("1.20.0"),
    54  				},
    55  			},
    56  		},
    57  	}
    58  	webhook := &MachinePool{}
    59  	t.Run("for MachinePool", util.CustomDefaultValidateTest(ctx, mp, webhook))
    60  	g.Expect(webhook.Default(ctx, mp)).To(Succeed())
    61  
    62  	g.Expect(mp.Labels[clusterv1.ClusterNameLabel]).To(Equal(mp.Spec.ClusterName))
    63  	g.Expect(mp.Spec.Replicas).To(Equal(pointer.Int32(1)))
    64  	g.Expect(mp.Spec.MinReadySeconds).To(Equal(pointer.Int32(0)))
    65  	g.Expect(mp.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace).To(Equal(mp.Namespace))
    66  	g.Expect(mp.Spec.Template.Spec.InfrastructureRef.Namespace).To(Equal(mp.Namespace))
    67  	g.Expect(mp.Spec.Template.Spec.Version).To(Equal(pointer.String("v1.20.0")))
    68  }
    69  
    70  func TestMachinePoolBootstrapValidation(t *testing.T) {
    71  	// NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool.
    72  	// Enabling the feature flag temporarily for this test.
    73  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)()
    74  	tests := []struct {
    75  		name      string
    76  		bootstrap clusterv1.Bootstrap
    77  		expectErr bool
    78  	}{
    79  		{
    80  			name:      "should return error if configref and data are nil",
    81  			bootstrap: clusterv1.Bootstrap{ConfigRef: nil, DataSecretName: nil},
    82  			expectErr: true,
    83  		},
    84  		{
    85  			name:      "should not return error if dataSecretName is set",
    86  			bootstrap: clusterv1.Bootstrap{ConfigRef: nil, DataSecretName: pointer.String("test")},
    87  			expectErr: false,
    88  		},
    89  		{
    90  			name:      "should not return error if config ref is set",
    91  			bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}, DataSecretName: nil},
    92  			expectErr: false,
    93  		},
    94  	}
    95  
    96  	for _, tt := range tests {
    97  		t.Run(tt.name, func(t *testing.T) {
    98  			g := NewWithT(t)
    99  			webhook := &MachinePool{}
   100  			mp := &expv1.MachinePool{
   101  				Spec: expv1.MachinePoolSpec{
   102  					Template: clusterv1.MachineTemplateSpec{
   103  						Spec: clusterv1.MachineSpec{
   104  							Bootstrap: tt.bootstrap,
   105  						},
   106  					},
   107  				},
   108  			}
   109  
   110  			if tt.expectErr {
   111  				warnings, err := webhook.ValidateCreate(ctx, mp)
   112  				g.Expect(err).To(HaveOccurred())
   113  				g.Expect(warnings).To(BeEmpty())
   114  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   115  				g.Expect(err).To(HaveOccurred())
   116  				g.Expect(warnings).To(BeEmpty())
   117  			} else {
   118  				warnings, err := webhook.ValidateCreate(ctx, mp)
   119  				g.Expect(err).ToNot(HaveOccurred())
   120  				g.Expect(warnings).To(BeEmpty())
   121  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   122  				g.Expect(err).ToNot(HaveOccurred())
   123  				g.Expect(warnings).To(BeEmpty())
   124  			}
   125  		})
   126  	}
   127  }
   128  
   129  func TestMachinePoolNamespaceValidation(t *testing.T) {
   130  	// NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool.
   131  	// Enabling the feature flag temporarily for this test.
   132  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)()
   133  	tests := []struct {
   134  		name      string
   135  		expectErr bool
   136  		bootstrap clusterv1.Bootstrap
   137  		infraRef  corev1.ObjectReference
   138  		namespace string
   139  	}{
   140  		{
   141  			name:      "should succeed if all namespaces match",
   142  			expectErr: false,
   143  			namespace: "foobar",
   144  			bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar"}},
   145  			infraRef:  corev1.ObjectReference{Namespace: "foobar"},
   146  		},
   147  		{
   148  			name:      "should return error if namespace and bootstrap namespace don't match",
   149  			expectErr: true,
   150  			namespace: "foobar",
   151  			bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar123"}},
   152  			infraRef:  corev1.ObjectReference{Namespace: "foobar"},
   153  		},
   154  		{
   155  			name:      "should return error if namespace and infrastructure ref namespace don't match",
   156  			expectErr: true,
   157  			namespace: "foobar",
   158  			bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar"}},
   159  			infraRef:  corev1.ObjectReference{Namespace: "foobar123"},
   160  		},
   161  		{
   162  			name:      "should return error if no namespaces match",
   163  			expectErr: true,
   164  			namespace: "foobar1",
   165  			bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar2"}},
   166  			infraRef:  corev1.ObjectReference{Namespace: "foobar3"},
   167  		},
   168  	}
   169  
   170  	for _, tt := range tests {
   171  		t.Run(tt.name, func(t *testing.T) {
   172  			g := NewWithT(t)
   173  
   174  			webhook := &MachinePool{}
   175  			mp := &expv1.MachinePool{
   176  				ObjectMeta: metav1.ObjectMeta{Namespace: tt.namespace},
   177  				Spec: expv1.MachinePoolSpec{
   178  					Template: clusterv1.MachineTemplateSpec{
   179  						Spec: clusterv1.MachineSpec{
   180  							Bootstrap:         tt.bootstrap,
   181  							InfrastructureRef: tt.infraRef,
   182  						},
   183  					},
   184  				},
   185  			}
   186  
   187  			if tt.expectErr {
   188  				warnings, err := webhook.ValidateCreate(ctx, mp)
   189  				g.Expect(err).To(HaveOccurred())
   190  				g.Expect(warnings).To(BeEmpty())
   191  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   192  				g.Expect(err).To(HaveOccurred())
   193  				g.Expect(warnings).To(BeEmpty())
   194  			} else {
   195  				warnings, err := webhook.ValidateCreate(ctx, mp)
   196  				g.Expect(err).ToNot(HaveOccurred())
   197  				g.Expect(warnings).To(BeEmpty())
   198  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   199  				g.Expect(err).ToNot(HaveOccurred())
   200  				g.Expect(warnings).To(BeEmpty())
   201  			}
   202  		})
   203  	}
   204  }
   205  
   206  func TestMachinePoolClusterNameImmutable(t *testing.T) {
   207  	// NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool.
   208  	// Enabling the feature flag temporarily for this test.
   209  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)()
   210  	tests := []struct {
   211  		name           string
   212  		oldClusterName string
   213  		newClusterName string
   214  		expectErr      bool
   215  	}{
   216  		{
   217  			name:           "when the cluster name has not changed",
   218  			oldClusterName: "foo",
   219  			newClusterName: "foo",
   220  			expectErr:      false,
   221  		},
   222  		{
   223  			name:           "when the cluster name has changed",
   224  			oldClusterName: "foo",
   225  			newClusterName: "bar",
   226  			expectErr:      true,
   227  		},
   228  	}
   229  
   230  	for _, tt := range tests {
   231  		t.Run(tt.name, func(t *testing.T) {
   232  			g := NewWithT(t)
   233  
   234  			newMP := &expv1.MachinePool{
   235  				Spec: expv1.MachinePoolSpec{
   236  					ClusterName: tt.newClusterName,
   237  					Template: clusterv1.MachineTemplateSpec{
   238  						Spec: clusterv1.MachineSpec{
   239  							Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}},
   240  						},
   241  					},
   242  				},
   243  			}
   244  
   245  			oldMP := &expv1.MachinePool{
   246  				Spec: expv1.MachinePoolSpec{
   247  					ClusterName: tt.oldClusterName,
   248  					Template: clusterv1.MachineTemplateSpec{
   249  						Spec: clusterv1.MachineSpec{
   250  							Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}},
   251  						},
   252  					},
   253  				},
   254  			}
   255  
   256  			webhook := MachinePool{}
   257  			warnings, err := webhook.ValidateUpdate(ctx, oldMP, newMP)
   258  			if tt.expectErr {
   259  				g.Expect(err).To(HaveOccurred())
   260  			} else {
   261  				g.Expect(err).ToNot(HaveOccurred())
   262  			}
   263  			g.Expect(warnings).To(BeEmpty())
   264  		})
   265  	}
   266  }
   267  
   268  func TestMachinePoolVersionValidation(t *testing.T) {
   269  	// NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool.
   270  	// Enabling the feature flag temporarily for this test.
   271  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)()
   272  	tests := []struct {
   273  		name      string
   274  		expectErr bool
   275  		version   string
   276  	}{
   277  		{
   278  			name:      "should succeed version is a valid kube semver",
   279  			expectErr: false,
   280  			version:   "v1.23.3",
   281  		},
   282  		{
   283  			name:      "should succeed version is a valid pre-release",
   284  			expectErr: false,
   285  			version:   "v1.19.0-alpha.1",
   286  		},
   287  		{
   288  			name:      "should fail if version is not a valid semver",
   289  			expectErr: true,
   290  			version:   "v1.1",
   291  		},
   292  		{
   293  			name:      "should fail if version is missing a v prefix",
   294  			expectErr: true,
   295  			version:   "1.20.0",
   296  		},
   297  	}
   298  
   299  	for i := range tests {
   300  		tt := tests[i]
   301  		t.Run(tt.name, func(t *testing.T) {
   302  			g := NewWithT(t)
   303  
   304  			mp := &expv1.MachinePool{
   305  				Spec: expv1.MachinePoolSpec{
   306  					Template: clusterv1.MachineTemplateSpec{
   307  						Spec: clusterv1.MachineSpec{
   308  							Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}},
   309  							Version:   &tt.version,
   310  						},
   311  					},
   312  				},
   313  			}
   314  			webhook := &MachinePool{}
   315  
   316  			if tt.expectErr {
   317  				warnings, err := webhook.ValidateCreate(ctx, mp)
   318  				g.Expect(err).To(HaveOccurred())
   319  				g.Expect(warnings).To(BeEmpty())
   320  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   321  				g.Expect(err).To(HaveOccurred())
   322  				g.Expect(warnings).To(BeEmpty())
   323  			} else {
   324  				warnings, err := webhook.ValidateCreate(ctx, mp)
   325  				g.Expect(err).ToNot(HaveOccurred())
   326  				g.Expect(warnings).To(BeEmpty())
   327  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   328  				g.Expect(err).ToNot(HaveOccurred())
   329  				g.Expect(warnings).To(BeEmpty())
   330  			}
   331  		})
   332  	}
   333  }
   334  
   335  func TestMachinePoolMetadataValidation(t *testing.T) {
   336  	tests := []struct {
   337  		name        string
   338  		labels      map[string]string
   339  		annotations map[string]string
   340  		expectErr   bool
   341  	}{
   342  		{
   343  			name: "should return error for invalid labels and annotations",
   344  			labels: map[string]string{
   345  				"foo":          "$invalid-key",
   346  				"bar":          strings.Repeat("a", 64) + "too-long-value",
   347  				"/invalid-key": "foo",
   348  			},
   349  			annotations: map[string]string{
   350  				"/invalid-key": "foo",
   351  			},
   352  			expectErr: true,
   353  		},
   354  	}
   355  
   356  	for _, tt := range tests {
   357  		t.Run(tt.name, func(t *testing.T) {
   358  			g := NewWithT(t)
   359  			mp := &expv1.MachinePool{
   360  				Spec: expv1.MachinePoolSpec{
   361  					Template: clusterv1.MachineTemplateSpec{
   362  						ObjectMeta: clusterv1.ObjectMeta{
   363  							Labels:      tt.labels,
   364  							Annotations: tt.annotations,
   365  						},
   366  					},
   367  				},
   368  			}
   369  			webhook := &MachinePool{}
   370  			if tt.expectErr {
   371  				warnings, err := webhook.ValidateCreate(ctx, mp)
   372  				g.Expect(err).To(HaveOccurred())
   373  				g.Expect(warnings).To(BeEmpty())
   374  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   375  				g.Expect(err).To(HaveOccurred())
   376  				g.Expect(warnings).To(BeEmpty())
   377  			} else {
   378  				warnings, err := webhook.ValidateCreate(ctx, mp)
   379  				g.Expect(err).ToNot(HaveOccurred())
   380  				g.Expect(warnings).To(BeEmpty())
   381  				warnings, err = webhook.ValidateUpdate(ctx, mp, mp)
   382  				g.Expect(err).ToNot(HaveOccurred())
   383  				g.Expect(warnings).To(BeEmpty())
   384  			}
   385  		})
   386  	}
   387  }