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 }