istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/tag/generate_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package tag 16 17 import ( 18 "context" 19 "fmt" 20 "path/filepath" 21 "testing" 22 23 admitv1 "k8s.io/api/admissionregistration/v1" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/runtime" 26 "k8s.io/apimachinery/pkg/runtime/serializer" 27 28 "istio.io/api/label" 29 "istio.io/istio/pkg/kube" 30 "istio.io/istio/pkg/test/env" 31 "istio.io/istio/pkg/test/util/assert" 32 ) 33 34 var ( 35 defaultRevisionCanonicalWebhook = admitv1.MutatingWebhookConfiguration{ 36 ObjectMeta: metav1.ObjectMeta{ 37 Name: "istio-sidecar-injector", 38 Labels: map[string]string{label.IoIstioRev.Name: "default"}, 39 }, 40 Webhooks: []admitv1.MutatingWebhook{ 41 { 42 Name: fmt.Sprintf("namespace.%s", istioInjectionWebhookSuffix), 43 ClientConfig: admitv1.WebhookClientConfig{ 44 Service: &admitv1.ServiceReference{ 45 Namespace: "default", 46 Name: "istiod", 47 }, 48 CABundle: []byte("ca"), 49 }, 50 }, 51 { 52 Name: fmt.Sprintf("object.%s", istioInjectionWebhookSuffix), 53 ClientConfig: admitv1.WebhookClientConfig{ 54 Service: &admitv1.ServiceReference{ 55 Namespace: "default", 56 Name: "istiod", 57 }, 58 CABundle: []byte("ca"), 59 }, 60 }, 61 }, 62 } 63 samplePath = "/sample/path" 64 operatorManaged = operatorNamespace + "/managed" 65 revisionCanonicalWebhook = admitv1.MutatingWebhookConfiguration{ 66 ObjectMeta: metav1.ObjectMeta{ 67 Name: "istio-sidecar-injector-revision", 68 Labels: map[string]string{ 69 label.IoIstioRev.Name: "revision", 70 operatorManaged: "Reconcile", 71 }, 72 }, 73 Webhooks: []admitv1.MutatingWebhook{ 74 { 75 Name: fmt.Sprintf("namespace.%s", istioInjectionWebhookSuffix), 76 ClientConfig: admitv1.WebhookClientConfig{ 77 Service: &admitv1.ServiceReference{ 78 Namespace: "default", 79 Name: "istiod-revision", 80 Path: &samplePath, 81 }, 82 CABundle: []byte("ca"), 83 }, 84 }, 85 { 86 Name: fmt.Sprintf("object.%s", istioInjectionWebhookSuffix), 87 ClientConfig: admitv1.WebhookClientConfig{ 88 Service: &admitv1.ServiceReference{ 89 Namespace: "default", 90 Name: "istiod-revision", 91 }, 92 CABundle: []byte("ca"), 93 }, 94 }, 95 }, 96 } 97 remoteInjectionURL = "https://random.host.com/inject/cluster/cluster1/net/net1" 98 revisionCanonicalWebhookRemote = admitv1.MutatingWebhookConfiguration{ 99 ObjectMeta: metav1.ObjectMeta{ 100 Name: "istio-sidecar-injector-revision", 101 Labels: map[string]string{label.IoIstioRev.Name: "revision"}, 102 }, 103 Webhooks: []admitv1.MutatingWebhook{ 104 { 105 Name: fmt.Sprintf("namespace.%s", istioInjectionWebhookSuffix), 106 ClientConfig: admitv1.WebhookClientConfig{ 107 URL: &remoteInjectionURL, 108 CABundle: []byte("ca"), 109 }, 110 }, 111 { 112 Name: fmt.Sprintf("object.%s", istioInjectionWebhookSuffix), 113 ClientConfig: admitv1.WebhookClientConfig{ 114 URL: &remoteInjectionURL, 115 CABundle: []byte("ca"), 116 }, 117 }, 118 }, 119 } 120 remoteValidationURL = "https://random.host.com/validate" 121 ) 122 123 func TestGenerateValidatingWebhook(t *testing.T) { 124 tcs := []struct { 125 name string 126 istioNamespace string 127 webhook admitv1.MutatingWebhookConfiguration 128 whURL string 129 whSVC string 130 whCA string 131 userManaged bool 132 }{ 133 { 134 name: "webhook-pointing-to-service", 135 istioNamespace: "istio-system", 136 webhook: revisionCanonicalWebhook, 137 whURL: "", 138 whSVC: "istiod-revision", 139 whCA: "ca", 140 }, 141 { 142 name: "webhook-custom-istio-namespace", 143 istioNamespace: "istio-system-blue", 144 webhook: revisionCanonicalWebhook, 145 whURL: "", 146 whSVC: "istiod-revision", 147 whCA: "ca", 148 }, 149 { 150 name: "webhook-pointing-to-url", 151 istioNamespace: "istio-system", 152 webhook: revisionCanonicalWebhookRemote, 153 whURL: remoteValidationURL, 154 whSVC: "", 155 whCA: "ca", 156 }, 157 { 158 name: "webhook-process-failure-policy", 159 istioNamespace: "istio-system", 160 webhook: revisionCanonicalWebhook, 161 whURL: "", 162 whSVC: "istiod-revision", 163 whCA: "ca", 164 userManaged: true, 165 }, 166 } 167 scheme := runtime.NewScheme() 168 codecFactory := serializer.NewCodecFactory(scheme) 169 deserializer := codecFactory.UniversalDeserializer() 170 171 fail := admitv1.Fail 172 fakeClient := kube.NewFakeClient(&admitv1.ValidatingWebhookConfiguration{ 173 TypeMeta: metav1.TypeMeta{}, 174 ObjectMeta: metav1.ObjectMeta{ 175 Name: "istiod-default-validator", 176 }, 177 Webhooks: []admitv1.ValidatingWebhook{ 178 { 179 Name: "random", 180 }, 181 { 182 FailurePolicy: &fail, 183 Name: "validation.istio.io", 184 }, 185 }, 186 }) 187 for _, tc := range tcs { 188 t.Run(tc.name, func(t *testing.T) { 189 webhookConfig, err := tagWebhookConfigFromCanonicalWebhook(tc.webhook, "default", tc.istioNamespace) 190 if err != nil { 191 t.Fatalf("webhook parsing failed with error: %v", err) 192 } 193 webhookConfig, err = fixWhConfig(fakeClient, webhookConfig) 194 if err != nil { 195 t.Fatalf("webhook fixing failed with error: %v", err) 196 } 197 opts := &GenerateOptions{ 198 ManifestsPath: filepath.Join(env.IstioSrc, "manifests"), 199 } 200 if tc.userManaged { 201 opts.UserManaged = true 202 } 203 webhookYAML, err := generateValidatingWebhook(webhookConfig, opts) 204 if err != nil { 205 t.Fatalf("tag webhook YAML generation failed with error: %v", err) 206 } 207 208 vwhObject, _, err := deserializer.Decode([]byte(webhookYAML), nil, &admitv1.ValidatingWebhookConfiguration{}) 209 if err != nil { 210 t.Fatalf("could not parse webhook from generated YAML: %s", vwhObject) 211 } 212 wh := vwhObject.(*admitv1.ValidatingWebhookConfiguration) 213 214 if tc.userManaged { 215 // User created webhooks should not have operator labels, otherwise will be pruned. 216 _, ok := wh.GetLabels()[operatorManaged] 217 assert.Equal(t, ok, false) 218 } 219 220 for _, webhook := range wh.Webhooks { 221 validationWhConf := webhook.ClientConfig 222 223 // this is nil since we've already have one with failed FailurePolicy in the fake client 224 if webhook.FailurePolicy != nil { 225 t.Fatalf("expected FailurePolicy to be nil, got %v", *webhook.FailurePolicy) 226 } 227 228 if tc.whSVC != "" { 229 if validationWhConf.Service == nil { 230 t.Fatalf("expected validation service %s, got nil", tc.whSVC) 231 } 232 if validationWhConf.Service.Name != tc.whSVC { 233 t.Fatalf("expected validation service %s, got %s", tc.whSVC, validationWhConf.Service.Name) 234 } 235 if validationWhConf.Service.Namespace != tc.istioNamespace { 236 t.Fatalf("expected validation service namespace %s, got %s", tc.istioNamespace, validationWhConf.Service.Namespace) 237 } 238 } 239 if tc.whURL != "" { 240 if validationWhConf.URL == nil { 241 t.Fatalf("expected validation URL %s, got nil", tc.whURL) 242 } 243 if *validationWhConf.URL != tc.whURL { 244 t.Fatalf("expected validation URL %s, got %s", tc.whURL, *validationWhConf.URL) 245 } 246 } 247 if tc.whCA != "" { 248 if string(validationWhConf.CABundle) != tc.whCA { 249 t.Fatalf("expected CA bundle %q, got %q", tc.whCA, validationWhConf.CABundle) 250 } 251 } 252 } 253 }) 254 } 255 } 256 257 func TestGenerateMutatingWebhook(t *testing.T) { 258 tcs := []struct { 259 name string 260 webhook admitv1.MutatingWebhookConfiguration 261 tagName string 262 whURL string 263 whSVC string 264 whCA string 265 numWebhooks int 266 }{ 267 { 268 name: "webhook-pointing-to-service", 269 webhook: revisionCanonicalWebhook, 270 tagName: "canary", 271 whURL: "", 272 whSVC: "istiod-revision", 273 whCA: "ca", 274 numWebhooks: 2, 275 }, 276 { 277 name: "webhook-pointing-to-url", 278 webhook: revisionCanonicalWebhookRemote, 279 tagName: "canary", 280 whURL: remoteInjectionURL, 281 whSVC: "", 282 whCA: "ca", 283 numWebhooks: 2, 284 }, 285 { 286 name: "webhook-pointing-to-default-revision", 287 webhook: defaultRevisionCanonicalWebhook, 288 tagName: "canary", 289 whURL: "", 290 whSVC: "istiod", 291 whCA: "ca", 292 numWebhooks: 2, 293 }, 294 { 295 name: "webhook-pointing-to-default-revision", 296 webhook: defaultRevisionCanonicalWebhook, 297 tagName: "default", 298 whURL: "", 299 whSVC: "istiod", 300 whCA: "ca", 301 numWebhooks: 4, 302 }, 303 } 304 scheme := runtime.NewScheme() 305 codecFactory := serializer.NewCodecFactory(scheme) 306 deserializer := codecFactory.UniversalDeserializer() 307 308 for _, tc := range tcs { 309 webhookConfig, err := tagWebhookConfigFromCanonicalWebhook(tc.webhook, tc.tagName, "istio-system") 310 if err != nil { 311 t.Fatalf("webhook parsing failed with error: %v", err) 312 } 313 webhookYAML, err := generateMutatingWebhook(webhookConfig, &GenerateOptions{ 314 WebhookName: "", 315 ManifestsPath: filepath.Join(env.IstioSrc, "manifests"), 316 AutoInjectNamespaces: false, 317 CustomLabels: nil, 318 }) 319 if err != nil { 320 t.Fatalf("tag webhook YAML generation failed with error: %v", err) 321 } 322 323 whObject, _, err := deserializer.Decode([]byte(webhookYAML), nil, &admitv1.MutatingWebhookConfiguration{}) 324 if err != nil { 325 t.Fatalf("could not parse webhook from generated YAML: %s", webhookYAML) 326 } 327 wh := whObject.(*admitv1.MutatingWebhookConfiguration) 328 329 // expect both namespace.sidecar-injector.istio.io and object.sidecar-injector.istio.io webhooks 330 if len(wh.Webhooks) != tc.numWebhooks { 331 t.Errorf("expected %d webhook(s) in MutatingWebhookConfiguration, found %d", 332 tc.numWebhooks, len(wh.Webhooks)) 333 } 334 tag, exists := wh.ObjectMeta.Labels[IstioTagLabel] 335 if !exists { 336 t.Errorf("expected tag webhook to have %s label, did not find", IstioTagLabel) 337 } 338 if tag != tc.tagName { 339 t.Errorf("expected tag webhook to have istio.io/tag=%s, found %s instead", tc.tagName, tag) 340 } 341 342 // ensure all webhooks have the correct client config 343 for _, webhook := range wh.Webhooks { 344 injectionWhConf := webhook.ClientConfig 345 if tc.whSVC != "" { 346 if injectionWhConf.Service == nil { 347 t.Fatalf("expected injection service %s, got nil", tc.whSVC) 348 } 349 if injectionWhConf.Service.Name != tc.whSVC { 350 t.Fatalf("expected injection service %s, got %s", tc.whSVC, injectionWhConf.Service.Name) 351 } 352 } 353 if tc.whURL != "" { 354 if injectionWhConf.URL == nil { 355 t.Fatalf("expected injection URL %s, got nil", tc.whURL) 356 } 357 if *injectionWhConf.URL != tc.whURL { 358 t.Fatalf("expected injection URL %s, got %s", tc.whURL, *injectionWhConf.URL) 359 } 360 } 361 if tc.whCA != "" { 362 if string(injectionWhConf.CABundle) != tc.whCA { 363 t.Fatalf("expected CA bundle %q, got %q", tc.whCA, injectionWhConf.CABundle) 364 } 365 } 366 } 367 } 368 } 369 370 func testGenerateOption(t *testing.T, generate bool, assertFunc func(*testing.T, []admitv1.MutatingWebhook, []admitv1.MutatingWebhook)) { 371 defaultWh := defaultRevisionCanonicalWebhook.DeepCopy() 372 fakeClient := kube.NewFakeClient(defaultWh) 373 374 opts := &GenerateOptions{ 375 Generate: generate, 376 Tag: "default", 377 Revision: "default", 378 } 379 380 _, err := Generate(context.TODO(), fakeClient, opts, "istio-system") 381 assert.NoError(t, err) 382 383 wh, err := fakeClient.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations(). 384 Get(context.Background(), "istio-sidecar-injector", metav1.GetOptions{}) 385 assert.NoError(t, err) 386 387 assertFunc(t, wh.Webhooks, defaultWh.Webhooks) 388 } 389 390 func TestGenerateOptions(t *testing.T) { 391 // Test generate option 'true', should not modify webhooks 392 testGenerateOption(t, true, func(t *testing.T, actual, expected []admitv1.MutatingWebhook) { 393 assert.Equal(t, actual, expected) 394 }) 395 396 // Test generate option 'false', should modify webhooks 397 testGenerateOption(t, false, func(t *testing.T, actual, expected []admitv1.MutatingWebhook) { 398 if err := assert.Compare(actual, expected); err == nil { 399 t.Errorf("expected diff between webhooks, got none") 400 } 401 }) 402 }