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

     1  /*
     2  Copyright 2022 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 runtime
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	. "github.com/onsi/gomega"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	utilfeature "k8s.io/component-base/featuregate/testing"
    27  	"k8s.io/utils/ptr"
    28  	ctrl "sigs.k8s.io/controller-runtime"
    29  
    30  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    31  	"sigs.k8s.io/cluster-api/feature"
    32  	"sigs.k8s.io/cluster-api/internal/webhooks/util"
    33  )
    34  
    35  var (
    36  	ctx        = ctrl.SetupSignalHandler()
    37  	fakeScheme = runtime.NewScheme()
    38  )
    39  
    40  func init() {
    41  	_ = runtimev1.AddToScheme(fakeScheme)
    42  }
    43  
    44  func TestExtensionConfigValidationFeatureGated(t *testing.T) {
    45  	extension := &runtimev1.ExtensionConfig{
    46  		ObjectMeta: metav1.ObjectMeta{
    47  			Name: "test-extension",
    48  		},
    49  		Spec: runtimev1.ExtensionConfigSpec{
    50  			ClientConfig: runtimev1.ClientConfig{
    51  				URL: ptr.To("https://extension-address.com"),
    52  			},
    53  			NamespaceSelector: &metav1.LabelSelector{},
    54  		},
    55  	}
    56  	updatedExtension := extension.DeepCopy()
    57  	updatedExtension.Spec.ClientConfig.URL = ptr.To("https://a-new-extension-address.com")
    58  	tests := []struct {
    59  		name        string
    60  		new         *runtimev1.ExtensionConfig
    61  		old         *runtimev1.ExtensionConfig
    62  		featureGate bool
    63  		expectErr   bool
    64  	}{
    65  		{
    66  			name:        "creation should fail if feature flag is disabled",
    67  			new:         extension,
    68  			featureGate: false,
    69  			expectErr:   true,
    70  		},
    71  		{
    72  			name:        "update should fail if feature flag is disabled",
    73  			old:         extension,
    74  			new:         updatedExtension,
    75  			featureGate: false,
    76  			expectErr:   true,
    77  		},
    78  		{
    79  			name:        "creation should succeed if feature flag is enabled",
    80  			new:         extension,
    81  			featureGate: true,
    82  			expectErr:   false,
    83  		},
    84  		{
    85  			name:        "update should fail if feature flag is enabled",
    86  			old:         extension,
    87  			new:         updatedExtension,
    88  			featureGate: true,
    89  			expectErr:   false,
    90  		},
    91  	}
    92  
    93  	for _, tt := range tests {
    94  		t.Run(tt.name, func(t *testing.T) {
    95  			defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, tt.featureGate)()
    96  			webhook := ExtensionConfig{}
    97  			g := NewWithT(t)
    98  			warnings, err := webhook.validate(context.TODO(), tt.old, tt.new)
    99  			if tt.expectErr {
   100  				g.Expect(err).To(HaveOccurred())
   101  				g.Expect(warnings).To(BeEmpty())
   102  				return
   103  			}
   104  			g.Expect(err).ToNot(HaveOccurred())
   105  			g.Expect(warnings).To(BeEmpty())
   106  		})
   107  	}
   108  }
   109  
   110  func TestExtensionConfigDefault(t *testing.T) {
   111  	g := NewWithT(t)
   112  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)()
   113  
   114  	extensionConfig := &runtimev1.ExtensionConfig{
   115  		ObjectMeta: metav1.ObjectMeta{
   116  			Name: "test-extension",
   117  		},
   118  		Spec: runtimev1.ExtensionConfigSpec{
   119  			ClientConfig: runtimev1.ClientConfig{
   120  				Service: &runtimev1.ServiceReference{
   121  					Name:      "name",
   122  					Namespace: "namespace",
   123  				},
   124  			},
   125  		},
   126  	}
   127  
   128  	extensionConfigWebhook := &ExtensionConfig{}
   129  	t.Run("for Extension", util.CustomDefaultValidateTest(ctx, extensionConfig, extensionConfigWebhook))
   130  
   131  	g.Expect(extensionConfigWebhook.Default(ctx, extensionConfig)).To(Succeed())
   132  	g.Expect(extensionConfig.Spec.NamespaceSelector).To(BeComparableTo(&metav1.LabelSelector{}))
   133  	g.Expect(extensionConfig.Spec.ClientConfig.Service.Port).To(BeComparableTo(ptr.To[int32](443)))
   134  }
   135  
   136  func TestExtensionConfigValidate(t *testing.T) {
   137  	extensionWithURL := &runtimev1.ExtensionConfig{
   138  		ObjectMeta: metav1.ObjectMeta{
   139  			Name: "test-extension",
   140  		},
   141  		Spec: runtimev1.ExtensionConfigSpec{
   142  			ClientConfig: runtimev1.ClientConfig{
   143  				URL: ptr.To("https://extension-address.com"),
   144  			},
   145  		},
   146  	}
   147  
   148  	extensionWithService := &runtimev1.ExtensionConfig{
   149  		ObjectMeta: metav1.ObjectMeta{
   150  			Name: "test-extension",
   151  		},
   152  		Spec: runtimev1.ExtensionConfigSpec{
   153  			ClientConfig: runtimev1.ClientConfig{
   154  				Service: &runtimev1.ServiceReference{
   155  					Path:      ptr.To("/path/to/handler"),
   156  					Port:      ptr.To[int32](1),
   157  					Name:      "foo",
   158  					Namespace: "bar",
   159  				}},
   160  		},
   161  	}
   162  
   163  	extensionWithServiceAndURL := extensionWithURL.DeepCopy()
   164  	extensionWithServiceAndURL.Spec.ClientConfig.Service = extensionWithService.Spec.ClientConfig.Service
   165  
   166  	extensionWithBadName := extensionWithURL.DeepCopy()
   167  	extensionWithBadName.Name = "bad.name"
   168  
   169  	// Valid updated Extension
   170  	updatedExtension := extensionWithURL.DeepCopy()
   171  	updatedExtension.Spec.ClientConfig.URL = ptr.To("https://a-in-extension-address.com")
   172  
   173  	extensionWithoutURLOrService := extensionWithURL.DeepCopy()
   174  	extensionWithoutURLOrService.Spec.ClientConfig.URL = nil
   175  
   176  	extensionWithInvalidServicePath := extensionWithService.DeepCopy()
   177  	extensionWithInvalidServicePath.Spec.ClientConfig.Service.Path = ptr.To("https://example.com")
   178  
   179  	extensionWithNoServiceName := extensionWithService.DeepCopy()
   180  	extensionWithNoServiceName.Spec.ClientConfig.Service.Name = ""
   181  
   182  	extensionWithBadServiceName := extensionWithService.DeepCopy()
   183  	extensionWithBadServiceName.Spec.ClientConfig.Service.Name = "NOT_ALLOWED"
   184  
   185  	extensionWithNoServiceNamespace := extensionWithService.DeepCopy()
   186  	extensionWithNoServiceNamespace.Spec.ClientConfig.Service.Namespace = ""
   187  
   188  	extensionWithBadServiceNamespace := extensionWithService.DeepCopy()
   189  	extensionWithBadServiceNamespace.Spec.ClientConfig.Service.Namespace = "INVALID"
   190  
   191  	badURLExtension := extensionWithURL.DeepCopy()
   192  	badURLExtension.Spec.ClientConfig.URL = ptr.To("https//extension-address.com")
   193  
   194  	badSchemeExtension := extensionWithURL.DeepCopy()
   195  	badSchemeExtension.Spec.ClientConfig.URL = ptr.To("unknown://extension-address.com")
   196  
   197  	extensionWithInvalidServicePort := extensionWithService.DeepCopy()
   198  	extensionWithInvalidServicePort.Spec.ClientConfig.Service.Port = ptr.To[int32](90000)
   199  
   200  	extensionWithInvalidNamespaceSelector := extensionWithService.DeepCopy()
   201  	extensionWithInvalidNamespaceSelector.Spec.NamespaceSelector = &metav1.LabelSelector{
   202  		MatchExpressions: []metav1.LabelSelectorRequirement{
   203  			{
   204  				Key:      "foo",
   205  				Operator: "bad-operator",
   206  				Values:   []string{"foo", "bar"},
   207  			},
   208  		},
   209  	}
   210  	extensionWithValidNamespaceSelector := extensionWithService.DeepCopy()
   211  	extensionWithValidNamespaceSelector.Spec.NamespaceSelector = &metav1.LabelSelector{
   212  		MatchExpressions: []metav1.LabelSelectorRequirement{
   213  			{
   214  				Key:      "foo",
   215  				Operator: metav1.LabelSelectorOpExists,
   216  			},
   217  		},
   218  	}
   219  
   220  	tests := []struct {
   221  		name        string
   222  		in          *runtimev1.ExtensionConfig
   223  		old         *runtimev1.ExtensionConfig
   224  		featureGate bool
   225  		expectErr   bool
   226  	}{
   227  		{
   228  			name:        "creation should fail if feature flag is disabled",
   229  			in:          extensionWithURL,
   230  			featureGate: false,
   231  			expectErr:   true,
   232  		},
   233  		{
   234  			name:        "update should fail if feature flag is disabled",
   235  			old:         extensionWithURL,
   236  			in:          updatedExtension,
   237  			featureGate: false,
   238  			expectErr:   true,
   239  		},
   240  		{
   241  			name:        "creation should fail if no Service Name is defined",
   242  			in:          extensionWithNoServiceName,
   243  			featureGate: true,
   244  			expectErr:   true,
   245  		},
   246  		{
   247  			name:        "creation should fail if extensionConfig Name violates Kubernetes naming rules",
   248  			in:          extensionWithBadName,
   249  			featureGate: true,
   250  			expectErr:   true,
   251  		},
   252  		{
   253  			name:        "creation should fail if Service Name violates Kubernetes naming rules",
   254  			in:          extensionWithBadServiceName,
   255  			featureGate: true,
   256  			expectErr:   true,
   257  		},
   258  		{
   259  			name:        "creation should fail if no Service Namespace is defined",
   260  			in:          extensionWithNoServiceNamespace,
   261  			featureGate: true,
   262  			expectErr:   true,
   263  		},
   264  		{
   265  			name:        "creation should fail if Service Namespace violates Kubernetes naming rules",
   266  			in:          extensionWithBadServiceNamespace,
   267  			featureGate: true,
   268  			expectErr:   true,
   269  		},
   270  		{
   271  			name:        "creation should succeed if NamespaceSelector is correctly defined",
   272  			in:          extensionWithValidNamespaceSelector,
   273  			featureGate: true,
   274  			expectErr:   false,
   275  		},
   276  
   277  		{
   278  			name:        "creation should fail if NamespaceSelector is incorrectly defined",
   279  			in:          extensionWithInvalidNamespaceSelector,
   280  			featureGate: true,
   281  			expectErr:   true,
   282  		},
   283  		{
   284  			name:        "update should fail if URL is invalid",
   285  			old:         extensionWithURL,
   286  			in:          badURLExtension,
   287  			featureGate: true,
   288  			expectErr:   true,
   289  		},
   290  		{
   291  			name:        "update should fail if URL scheme is invalid",
   292  			old:         extensionWithURL,
   293  			in:          badSchemeExtension,
   294  			featureGate: true,
   295  			expectErr:   true,
   296  		},
   297  		{
   298  			name:        "update should fail if URL and Service are both nil",
   299  			old:         extensionWithURL,
   300  			in:          extensionWithoutURLOrService,
   301  			featureGate: true,
   302  			expectErr:   true,
   303  		},
   304  		{
   305  			name:        "update should fail if both URL and Service are defined",
   306  			old:         extensionWithService,
   307  			in:          extensionWithServiceAndURL,
   308  			featureGate: true,
   309  			expectErr:   true,
   310  		},
   311  		{
   312  			name:        "update should fail if Service Path is invalid",
   313  			old:         extensionWithService,
   314  			in:          extensionWithInvalidServicePath,
   315  			featureGate: true,
   316  			expectErr:   true,
   317  		},
   318  		{
   319  			name:        "update should fail if Service Port is invalid",
   320  			old:         extensionWithService,
   321  			in:          extensionWithInvalidServicePort,
   322  			featureGate: true,
   323  			expectErr:   true,
   324  		},
   325  		{
   326  			name:        "update should pass if updated Extension is valid",
   327  			old:         extensionWithService,
   328  			in:          extensionWithService,
   329  			featureGate: true,
   330  			expectErr:   false,
   331  		},
   332  	}
   333  
   334  	for _, tt := range tests {
   335  		t.Run(tt.name, func(t *testing.T) {
   336  			defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, tt.featureGate)()
   337  			g := NewWithT(t)
   338  			webhook := &ExtensionConfig{}
   339  			// Default the objects so we're not handling defaulted cases.
   340  			g.Expect(webhook.Default(ctx, tt.in)).To(Succeed())
   341  			if tt.old != nil {
   342  				g.Expect(webhook.Default(ctx, tt.old)).To(Succeed())
   343  			}
   344  
   345  			warnings, err := webhook.validate(ctx, tt.old, tt.in)
   346  			if tt.expectErr {
   347  				g.Expect(err).To(HaveOccurred())
   348  				g.Expect(warnings).To(BeEmpty())
   349  				return
   350  			}
   351  			g.Expect(err).ToNot(HaveOccurred())
   352  			g.Expect(warnings).To(BeEmpty())
   353  		})
   354  	}
   355  }