github.com/operator-framework/operator-lifecycle-manager@v0.30.0/test/e2e/webhook_e2e_test.go (about) 1 package e2e 2 3 import ( 4 "context" 5 "crypto/ecdsa" 6 "crypto/elliptic" 7 "crypto/rand" 8 "crypto/x509" 9 "crypto/x509/pkix" 10 "fmt" 11 "math" 12 "math/big" 13 "strings" 14 "time" 15 16 . "github.com/onsi/ginkgo/v2" 17 . "github.com/onsi/gomega" 18 "github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx" 19 "github.com/stretchr/testify/require" 20 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 21 corev1 "k8s.io/api/core/v1" 22 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 23 "k8s.io/apimachinery/pkg/api/errors" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 "k8s.io/apimachinery/pkg/labels" 27 28 v1 "github.com/operator-framework/api/pkg/operators/v1" 29 operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 30 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" 31 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/certs" 32 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" 33 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" 34 ) 35 36 // Global Variables 37 const ( 38 webhookName = "webhook.test.com" 39 ) 40 41 var _ = Describe("CSVs with a Webhook", func() { 42 var ( 43 generatedNamespace corev1.Namespace 44 c operatorclient.ClientInterface 45 crc versioned.Interface 46 nsLabels map[string]string 47 ) 48 49 BeforeEach(func() { 50 c = ctx.Ctx().KubeClient() 51 crc = ctx.Ctx().OperatorClient() 52 nsLabels = map[string]string{ 53 "foo": "bar", 54 } 55 generatedNamespace = corev1.Namespace{ 56 ObjectMeta: metav1.ObjectMeta{ 57 Name: genName("webhook-e2e-"), 58 Labels: map[string]string{ 59 "foo": "bar", 60 }, 61 }, 62 } 63 Eventually(func() error { 64 return ctx.Ctx().Client().Create(context.Background(), &generatedNamespace) 65 }).Should(Succeed()) 66 }) 67 68 AfterEach(func() { 69 TeardownNamespace(generatedNamespace.GetName()) 70 }) 71 72 When("Installed in an OperatorGroup that defines a selector", func() { 73 var cleanupCSV cleanupFunc 74 var ogSelector *metav1.LabelSelector 75 76 BeforeEach(func() { 77 ogSelector = &metav1.LabelSelector{ 78 MatchLabels: nsLabels, 79 } 80 81 og := newOperatorGroup(generatedNamespace.GetName(), genName("selector-og-"), nil, ogSelector, nil, false) 82 _, err := crc.OperatorsV1().OperatorGroups(generatedNamespace.GetName()).Create(context.TODO(), og, metav1.CreateOptions{}) 83 Expect(err).Should(BeNil()) 84 }) 85 86 AfterEach(func() { 87 if cleanupCSV != nil { 88 cleanupCSV() 89 } 90 }) 91 92 It("The webhook is scoped to the selector", func() { 93 sideEffect := admissionregistrationv1.SideEffectClassNone 94 webhook := operatorsv1alpha1.WebhookDescription{ 95 GenerateName: webhookName, 96 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 97 DeploymentName: genName("webhook-dep-"), 98 ContainerPort: 443, 99 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 100 SideEffects: &sideEffect, 101 } 102 103 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 104 var err error 105 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 106 Expect(err).Should(BeNil()) 107 108 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 109 Expect(err).Should(BeNil()) 110 111 actualWebhook, err := getWebhookWithGenerateName(c, webhook.GenerateName) 112 Expect(err).Should(BeNil()) 113 114 Expect(actualWebhook.Webhooks[0].NamespaceSelector).Should(Equal(ogSelector)) 115 }) 116 }) 117 When("Installed in a SingleNamespace OperatorGroup", func() { 118 var cleanupCSV cleanupFunc 119 var og *v1.OperatorGroup 120 121 BeforeEach(func() { 122 og = newOperatorGroup(generatedNamespace.GetName(), genName("single-namespace-og-"), nil, nil, []string{generatedNamespace.GetName()}, false) 123 var err error 124 og, err = crc.OperatorsV1().OperatorGroups(generatedNamespace.GetName()).Create(context.TODO(), og, metav1.CreateOptions{}) 125 Expect(err).Should(BeNil()) 126 }) 127 128 AfterEach(func() { 129 if cleanupCSV != nil { 130 cleanupCSV() 131 } 132 }) 133 134 It("Creates Webhooks scoped to a single namespace", func() { 135 sideEffect := admissionregistrationv1.SideEffectClassNone 136 webhook := operatorsv1alpha1.WebhookDescription{ 137 GenerateName: webhookName, 138 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 139 DeploymentName: genName("webhook-dep-"), 140 ContainerPort: 443, 141 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 142 SideEffects: &sideEffect, 143 } 144 145 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 146 var err error 147 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 148 Expect(err).Should(BeNil()) 149 150 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 151 Expect(err).Should(BeNil()) 152 153 actualWebhook, err := getWebhookWithGenerateName(c, webhook.GenerateName) 154 Expect(err).Should(BeNil()) 155 156 ogLabel, err := getOGLabelKey(og) 157 require.NoError(GinkgoT(), err) 158 159 expected := &metav1.LabelSelector{ 160 MatchLabels: map[string]string{ogLabel: ""}, 161 MatchExpressions: []metav1.LabelSelectorRequirement(nil), 162 } 163 Expect(actualWebhook.Webhooks[0].NamespaceSelector).Should(Equal(expected)) 164 165 By(`Ensure that changes to the WebhookDescription within the CSV trigger an update to on cluster resources`) 166 changedGenerateName := webhookName + "-changed" 167 Eventually(func() error { 168 existingCSV, err := crc.OperatorsV1alpha1().ClusterServiceVersions(generatedNamespace.GetName()).Get(context.TODO(), csv.GetName(), metav1.GetOptions{}) 169 if err != nil { 170 return err 171 } 172 existingCSV.Spec.WebhookDefinitions[0].GenerateName = changedGenerateName 173 174 existingCSV, err = crc.OperatorsV1alpha1().ClusterServiceVersions(generatedNamespace.GetName()).Update(context.TODO(), existingCSV, metav1.UpdateOptions{}) 175 return err 176 }, time.Minute, 5*time.Second).Should(Succeed()) 177 Eventually(func() bool { 178 By(`Previous Webhook should be deleted`) 179 _, err = getWebhookWithGenerateName(c, webhookName) 180 if err != nil && err.Error() != "NotFound" { 181 return false 182 } 183 184 By(`Current Webhook should exist`) 185 _, err = getWebhookWithGenerateName(c, changedGenerateName) 186 return err == nil 187 }, time.Minute, 5*time.Second).Should(BeTrue()) 188 }) 189 It("Reuses existing valid certs", func() { 190 sideEffect := admissionregistrationv1.SideEffectClassNone 191 webhook := operatorsv1alpha1.WebhookDescription{ 192 GenerateName: webhookName, 193 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 194 DeploymentName: genName("webhook-dep-"), 195 ContainerPort: 443, 196 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 197 SideEffects: &sideEffect, 198 } 199 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 200 201 var err error 202 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 203 Expect(err).Should(BeNil()) 204 205 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 206 Expect(err).Should(BeNil()) 207 208 By(`Get the existing secret`) 209 webhookSecretName := webhook.DeploymentName + "-service-cert" 210 existingSecret, err := c.KubernetesInterface().CoreV1().Secrets(generatedNamespace.GetName()).Get(context.TODO(), webhookSecretName, metav1.GetOptions{}) 211 require.NoError(GinkgoT(), err) 212 213 By(`Modify the phase`) 214 Eventually(func() bool { 215 fetchedCSV, err := crc.OperatorsV1alpha1().ClusterServiceVersions(generatedNamespace.GetName()).Get(context.TODO(), csv.GetName(), metav1.GetOptions{}) 216 if err != nil { 217 return false 218 } 219 220 fetchedCSV.Status.Phase = operatorsv1alpha1.CSVPhasePending 221 222 _, err = crc.OperatorsV1alpha1().ClusterServiceVersions(generatedNamespace.GetName()).UpdateStatus(context.TODO(), fetchedCSV, metav1.UpdateOptions{}) 223 return err == nil 224 }).Should(BeTrue(), "Unable to set CSV phase to Pending") 225 226 By(`Wait for webhook-operator to succeed`) 227 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.GetName(), csvSucceededChecker) 228 require.NoError(GinkgoT(), err) 229 230 By(`Get the updated secret`) 231 updatedSecret, err := c.KubernetesInterface().CoreV1().Secrets(generatedNamespace.GetName()).Get(context.TODO(), webhookSecretName, metav1.GetOptions{}) 232 require.NoError(GinkgoT(), err) 233 234 require.Equal(GinkgoT(), existingSecret.GetAnnotations()[install.OLMCAHashAnnotationKey], updatedSecret.GetAnnotations()[install.OLMCAHashAnnotationKey]) 235 require.Equal(GinkgoT(), existingSecret.Data[install.OLMCAPEMKey], updatedSecret.Data[install.OLMCAPEMKey]) 236 }) 237 It("Fails to install a CSV if multiple Webhooks share the same name", func() { 238 sideEffect := admissionregistrationv1.SideEffectClassNone 239 webhook := operatorsv1alpha1.WebhookDescription{ 240 GenerateName: webhookName, 241 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 242 DeploymentName: genName("webhook-dep-"), 243 ContainerPort: 443, 244 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 245 SideEffects: &sideEffect, 246 } 247 248 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 249 csv.Spec.WebhookDefinitions = append(csv.Spec.WebhookDefinitions, webhook) 250 var err error 251 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 252 Expect(err).Should(BeNil()) 253 254 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvFailedChecker) 255 Expect(err).Should(BeNil()) 256 }) 257 It("Fails if the webhooks intercepts all resources", func() { 258 sideEffect := admissionregistrationv1.SideEffectClassNone 259 webhook := operatorsv1alpha1.WebhookDescription{ 260 GenerateName: webhookName, 261 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 262 DeploymentName: genName("webhook-dep-"), 263 ContainerPort: 443, 264 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 265 SideEffects: &sideEffect, 266 Rules: []admissionregistrationv1.RuleWithOperations{ 267 { 268 Operations: []admissionregistrationv1.OperationType{}, 269 Rule: admissionregistrationv1.Rule{ 270 APIGroups: []string{"*"}, 271 APIVersions: []string{"*"}, 272 Resources: []string{"*"}, 273 }, 274 }, 275 }, 276 } 277 278 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 279 280 var err error 281 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 282 Expect(err).Should(BeNil()) 283 284 failedCSV, err := fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvFailedChecker) 285 Expect(err).Should(BeNil()) 286 Expect(failedCSV.Status.Message).Should(Equal("webhook rules cannot include all groups")) 287 }) 288 It("Fails if the webhook intercepts OLM resources", func() { 289 sideEffect := admissionregistrationv1.SideEffectClassNone 290 webhook := operatorsv1alpha1.WebhookDescription{ 291 GenerateName: webhookName, 292 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 293 DeploymentName: genName("webhook-dep-"), 294 ContainerPort: 443, 295 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 296 SideEffects: &sideEffect, 297 Rules: []admissionregistrationv1.RuleWithOperations{ 298 { 299 Operations: []admissionregistrationv1.OperationType{}, 300 Rule: admissionregistrationv1.Rule{ 301 APIGroups: []string{"operators.coreos.com"}, 302 APIVersions: []string{"*"}, 303 Resources: []string{"*"}, 304 }, 305 }, 306 }, 307 } 308 309 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 310 311 var err error 312 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 313 Expect(err).Should(BeNil()) 314 315 failedCSV, err := fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvFailedChecker) 316 Expect(err).Should(BeNil()) 317 Expect(failedCSV.Status.Message).Should(Equal("webhook rules cannot include the OLM group")) 318 }) 319 It("Fails if webhook intercepts Admission Webhook resources", func() { 320 sideEffect := admissionregistrationv1.SideEffectClassNone 321 webhook := operatorsv1alpha1.WebhookDescription{ 322 GenerateName: webhookName, 323 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 324 DeploymentName: genName("webhook-dep-"), 325 ContainerPort: 443, 326 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 327 SideEffects: &sideEffect, 328 Rules: []admissionregistrationv1.RuleWithOperations{ 329 { 330 Operations: []admissionregistrationv1.OperationType{}, 331 Rule: admissionregistrationv1.Rule{ 332 APIGroups: []string{"admissionregistration.k8s.io"}, 333 APIVersions: []string{"*"}, 334 Resources: []string{"*"}, 335 }, 336 }, 337 }, 338 } 339 340 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 341 342 var err error 343 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 344 Expect(err).Should(BeNil()) 345 346 failedCSV, err := fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvFailedChecker) 347 Expect(err).Should(BeNil()) 348 Expect(failedCSV.Status.Message).Should(Equal("webhook rules cannot include MutatingWebhookConfiguration or ValidatingWebhookConfiguration resources")) 349 }) 350 It("Succeeds if the webhook intercepts non Admission Webhook resources in admissionregistration group", func() { 351 sideEffect := admissionregistrationv1.SideEffectClassNone 352 webhook := operatorsv1alpha1.WebhookDescription{ 353 GenerateName: webhookName, 354 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 355 DeploymentName: genName("webhook-dep-"), 356 ContainerPort: 443, 357 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 358 SideEffects: &sideEffect, 359 Rules: []admissionregistrationv1.RuleWithOperations{ 360 { 361 Operations: []admissionregistrationv1.OperationType{ 362 admissionregistrationv1.OperationAll, 363 }, 364 Rule: admissionregistrationv1.Rule{ 365 APIGroups: []string{"admissionregistration.k8s.io"}, 366 APIVersions: []string{"*"}, 367 Resources: []string{"SomeOtherResource"}, 368 }, 369 }, 370 }, 371 } 372 373 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 374 375 var err error 376 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 377 Expect(err).Should(BeNil()) 378 379 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 380 Expect(err).Should(BeNil()) 381 }) 382 It("Can be installed and upgraded successfully", func() { 383 sideEffect := admissionregistrationv1.SideEffectClassNone 384 webhook := operatorsv1alpha1.WebhookDescription{ 385 GenerateName: "webhook.test.com", 386 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 387 DeploymentName: genName("webhook-dep-"), 388 ContainerPort: 443, 389 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 390 SideEffects: &sideEffect, 391 Rules: []admissionregistrationv1.RuleWithOperations{ 392 { 393 Operations: []admissionregistrationv1.OperationType{ 394 admissionregistrationv1.OperationAll, 395 }, 396 Rule: admissionregistrationv1.Rule{ 397 APIGroups: []string{"admissionregistration.k8s.io"}, 398 APIVersions: []string{"*"}, 399 Resources: []string{"SomeOtherResource"}, 400 }, 401 }, 402 }, 403 } 404 405 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 406 407 _, err := createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 408 Expect(err).Should(BeNil()) 409 By(`cleanup by upgrade`) 410 411 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 412 Expect(err).Should(BeNil()) 413 414 _, err = getWebhookWithGenerateName(c, webhook.GenerateName) 415 Expect(err).Should(BeNil()) 416 417 By(`Update the CSV so it it replaces the existing CSV`) 418 csv.Spec.Replaces = csv.GetName() 419 csv.Name = genName("csv-") 420 previousWebhookName := webhook.GenerateName 421 webhook.GenerateName = "webhook2.test.com" 422 csv.Spec.WebhookDefinitions[0] = webhook 423 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 424 Expect(err).Should(BeNil()) 425 426 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.GetName(), csvSucceededChecker) 427 Expect(err).Should(BeNil()) 428 429 _, err = getWebhookWithGenerateName(c, webhook.GenerateName) 430 Expect(err).Should(BeNil()) 431 432 By(`Make sure old resources are cleaned up.`) 433 err = waitForCsvToDelete(generatedNamespace.GetName(), csv.Spec.Replaces, crc) 434 Expect(err).ShouldNot(HaveOccurred()) 435 436 By(`Wait until previous webhook is cleaned up`) 437 Eventually(func() (bool, error) { 438 _, err := c.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), previousWebhookName, metav1.GetOptions{}) 439 if errors.IsNotFound(err) { 440 return true, nil 441 } 442 if err != nil { 443 return false, err 444 } 445 return false, nil 446 }).Should(BeTrue()) 447 }) 448 It("[FLAKE] Is updated when the CAs expire", func() { 449 By(`issue: https://github.com/operator-framework/operator-lifecycle-manager/issues/2629`) 450 sideEffect := admissionregistrationv1.SideEffectClassNone 451 webhook := operatorsv1alpha1.WebhookDescription{ 452 GenerateName: webhookName, 453 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 454 DeploymentName: genName("webhook-dep-"), 455 ContainerPort: 443, 456 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 457 SideEffects: &sideEffect, 458 } 459 460 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 461 462 var err error 463 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 464 Expect(err).Should(BeNil()) 465 466 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 467 Expect(err).Should(BeNil()) 468 469 actualWebhook, err := getWebhookWithGenerateName(c, webhook.GenerateName) 470 Expect(err).Should(BeNil()) 471 472 oldWebhookCABundle := actualWebhook.Webhooks[0].ClientConfig.CABundle 473 474 By(`Get the deployment`) 475 dep, err := c.KubernetesInterface().AppsV1().Deployments(generatedNamespace.GetName()).Get(context.TODO(), csv.Spec.WebhookDefinitions[0].DeploymentName, metav1.GetOptions{}) 476 Expect(err).Should(BeNil()) 477 478 //Store the ca sha annotation 479 oldCAAnnotation, ok := dep.Spec.Template.GetAnnotations()[install.OLMCAHashAnnotationKey] 480 Expect(ok).Should(BeTrue()) 481 482 caSecret, err := c.KubernetesInterface().CoreV1().Secrets(generatedNamespace.GetName()).Get(context.TODO(), install.SecretName(install.ServiceName(dep.Name)), metav1.GetOptions{}) 483 Expect(err).Should(BeNil()) 484 485 caPEM, certPEM, privPEM := generateExpiredCerts(install.HostnamesForService(install.ServiceName(dep.Name), generatedNamespace.GetName())) 486 By(`Induce a cert rotation`) 487 Eventually(Apply(caSecret, func(caSecret *corev1.Secret) error { 488 caSecret.Data[install.OLMCAPEMKey] = caPEM 489 caSecret.Data["tls.crt"] = certPEM 490 caSecret.Data["tls.key"] = privPEM 491 return nil 492 })).Should(Succeed()) 493 494 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, func(csv *operatorsv1alpha1.ClusterServiceVersion) bool { 495 By(`Should create deployment`) 496 dep, err = c.GetDeployment(generatedNamespace.GetName(), csv.Spec.WebhookDefinitions[0].DeploymentName) 497 if err != nil { 498 return false 499 } 500 501 By(`Should have a new ca hash annotation`) 502 newCAAnnotation, ok := dep.Spec.Template.GetAnnotations()[install.OLMCAHashAnnotationKey] 503 if !ok { 504 return false 505 } 506 507 if newCAAnnotation != oldCAAnnotation { 508 By(`Check for success`) 509 return csvSucceededChecker(csv) 510 } 511 512 return false 513 }) 514 Expect(err).Should(BeNil()) 515 516 By(`get new webhook`) 517 actualWebhook, err = getWebhookWithGenerateName(c, webhook.GenerateName) 518 Expect(err).Should(BeNil()) 519 520 newWebhookCABundle := actualWebhook.Webhooks[0].ClientConfig.CABundle 521 Expect(newWebhookCABundle).ShouldNot(Equal(oldWebhookCABundle)) 522 }) 523 }) 524 When("Installed in a Global OperatorGroup", func() { 525 var cleanupCSV cleanupFunc 526 527 BeforeEach(func() { 528 og := newOperatorGroup(generatedNamespace.GetName(), genName("global-og-"), nil, nil, []string{}, false) 529 og, err := crc.OperatorsV1().OperatorGroups(generatedNamespace.GetName()).Create(context.TODO(), og, metav1.CreateOptions{}) 530 Expect(err).Should(BeNil()) 531 }) 532 533 AfterEach(func() { 534 if cleanupCSV != nil { 535 cleanupCSV() 536 } 537 }) 538 539 It("The webhook is scoped to all namespaces", func() { 540 sideEffect := admissionregistrationv1.SideEffectClassNone 541 webhook := operatorsv1alpha1.WebhookDescription{ 542 GenerateName: webhookName, 543 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 544 DeploymentName: genName("webhook-dep-"), 545 ContainerPort: 443, 546 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 547 SideEffects: &sideEffect, 548 } 549 550 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 551 552 var err error 553 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 554 Expect(err).Should(BeNil()) 555 556 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 557 Expect(err).Should(BeNil()) 558 actualWebhook, err := getWebhookWithGenerateName(c, webhook.GenerateName) 559 Expect(err).Should(BeNil()) 560 561 expected := &metav1.LabelSelector{ 562 MatchLabels: map[string]string(nil), 563 MatchExpressions: []metav1.LabelSelectorRequirement(nil), 564 } 565 Expect(actualWebhook.Webhooks[0].NamespaceSelector).Should(Equal(expected)) 566 }) 567 }) 568 It("Allows multiple installs of the same webhook", func() { 569 namespace1, cleanupNS1 := newNamespace(c, genName("webhook-test-")) 570 defer cleanupNS1() 571 572 namespace2, cleanupNS2 := newNamespace(c, genName("webhook-test-")) 573 defer cleanupNS2() 574 575 og1 := newOperatorGroup(namespace1.Name, genName("test-og-"), nil, nil, []string{"test-go-"}, false) 576 Eventually(func() error { 577 og, err := crc.OperatorsV1().OperatorGroups(namespace1.Name).Create(context.TODO(), og1, metav1.CreateOptions{}) 578 if err != nil { 579 return err 580 } 581 582 og1 = og 583 584 return nil 585 }).Should(Succeed()) 586 587 og2 := newOperatorGroup(namespace2.Name, genName("test-og-"), nil, nil, []string{"test-go-"}, false) 588 Eventually(func() error { 589 og, err := crc.OperatorsV1().OperatorGroups(namespace2.Name).Create(context.TODO(), og2, metav1.CreateOptions{}) 590 if err != nil { 591 return err 592 } 593 594 og2 = og 595 596 return nil 597 }).Should(Succeed()) 598 599 sideEffect := admissionregistrationv1.SideEffectClassNone 600 webhook := operatorsv1alpha1.WebhookDescription{ 601 GenerateName: webhookName, 602 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 603 DeploymentName: genName("webhook-dep-"), 604 ContainerPort: 443, 605 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 606 SideEffects: &sideEffect, 607 } 608 609 csv := createCSVWithWebhook(generatedNamespace.GetName(), webhook) 610 611 csv.Namespace = namespace1.GetName() 612 var cleanupCSV cleanupFunc 613 Eventually(func() (err error) { 614 cleanupCSV, err = createCSV(c, crc, csv, namespace1.Name, false, false) 615 return 616 }).Should(Succeed()) 617 defer cleanupCSV() 618 619 Eventually(func() (err error) { 620 _, err = fetchCSV(crc, namespace1.Name, csv.Name, csvSucceededChecker) 621 return 622 }).Should(Succeed()) 623 624 csv.Namespace = namespace2.Name 625 Eventually(func() (err error) { 626 cleanupCSV, err = createCSV(c, crc, csv, namespace2.Name, false, false) 627 return 628 }).Should(Succeed()) 629 defer cleanupCSV() 630 631 Eventually(func() (err error) { 632 _, err = fetchCSV(crc, namespace2.Name, csv.Name, csvSucceededChecker) 633 return 634 }).Should(Succeed()) 635 636 Eventually(func() (count int, err error) { 637 var webhooks *admissionregistrationv1.ValidatingWebhookConfigurationList 638 webhooks, err = c.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{}) 639 if err != nil { 640 return 641 } 642 643 for _, w := range webhooks.Items { 644 if strings.HasPrefix(w.GetName(), webhook.GenerateName) { 645 count++ 646 } 647 } 648 649 return 650 }).Should(Equal(2)) 651 }) 652 When("Installed from a catalog Source", func() { 653 const csvName = "webhook-operator.v0.0.1" 654 var cleanupCSV cleanupFunc 655 var cleanupCatSrc cleanupFunc 656 var cleanupSubscription cleanupFunc 657 658 BeforeEach(func() { 659 og := newOperatorGroup(generatedNamespace.GetName(), genName("og-"), nil, nil, []string{}, false) 660 _, err := crc.OperatorsV1().OperatorGroups(generatedNamespace.GetName()).Create(context.TODO(), og, metav1.CreateOptions{}) 661 Expect(err).Should(BeNil()) 662 663 By(`Create a catalogSource which has the webhook-operator`) 664 sourceName := genName("catalog-") 665 packageName := "webhook-operator" 666 channelName := "alpha" 667 668 catSrcImage := "quay.io/operator-framework/webhook-operator-index" 669 670 By(`Create gRPC CatalogSource`) 671 source := &operatorsv1alpha1.CatalogSource{ 672 TypeMeta: metav1.TypeMeta{ 673 Kind: operatorsv1alpha1.CatalogSourceKind, 674 APIVersion: operatorsv1alpha1.CatalogSourceCRDAPIVersion, 675 }, 676 ObjectMeta: metav1.ObjectMeta{ 677 Name: sourceName, 678 Namespace: generatedNamespace.GetName(), 679 }, 680 Spec: operatorsv1alpha1.CatalogSourceSpec{ 681 SourceType: operatorsv1alpha1.SourceTypeGrpc, 682 Image: catSrcImage + ":0.0.3", 683 GrpcPodConfig: &operatorsv1alpha1.GrpcPodConfig{ 684 SecurityContextConfig: operatorsv1alpha1.Restricted, 685 }, 686 }, 687 } 688 689 source, err = crc.OperatorsV1alpha1().CatalogSources(source.GetNamespace()).Create(context.TODO(), source, metav1.CreateOptions{}) 690 require.NoError(GinkgoT(), err) 691 cleanupCatSrc = func() { 692 require.NoError(GinkgoT(), crc.OperatorsV1alpha1().CatalogSources(source.GetNamespace()).Delete(context.TODO(), source.GetName(), metav1.DeleteOptions{})) 693 } 694 695 By(`Wait for the CatalogSource to be ready`) 696 _, err = fetchCatalogSourceOnStatus(crc, source.GetName(), source.GetNamespace(), catalogSourceRegistryPodSynced()) 697 require.NoError(GinkgoT(), err) 698 699 By(`Create a Subscription for the webhook-operator`) 700 subscriptionName := genName("sub-") 701 cleanupSubscription := createSubscriptionForCatalog(crc, source.GetNamespace(), subscriptionName, source.GetName(), packageName, channelName, "", operatorsv1alpha1.ApprovalAutomatic) 702 defer cleanupSubscription() 703 704 _, err = fetchSubscription(crc, source.GetNamespace(), subscriptionName, subscriptionHasInstallPlanChecker()) 705 require.NoError(GinkgoT(), err) 706 707 By(`Wait for webhook-operator v2 csv to succeed`) 708 csv, err := fetchCSV(crc, source.GetNamespace(), csvName, csvSucceededChecker) 709 require.NoError(GinkgoT(), err) 710 711 cleanupCSV = buildCSVCleanupFunc(c, crc, *csv, source.GetNamespace(), true, true) 712 }) 713 714 AfterEach(func() { 715 if cleanupCSV != nil { 716 cleanupCSV() 717 } 718 if cleanupCatSrc != nil { 719 cleanupCatSrc() 720 } 721 if cleanupSubscription != nil { 722 cleanupSubscription() 723 } 724 }) 725 726 It("Validating, Mutating and Conversion webhooks work as intended", func() { 727 By(`An invalid custom resource is rejected by the validating webhook`) 728 invalidCR := &unstructured.Unstructured{ 729 Object: map[string]interface{}{ 730 "apiVersion": "webhook.operators.coreos.io/v1", 731 "kind": "webhooktests", 732 "metadata": map[string]interface{}{ 733 "namespace": generatedNamespace.GetName(), 734 "name": "my-cr-1", 735 }, 736 "spec": map[string]interface{}{ 737 "valid": false, 738 }, 739 }, 740 } 741 expectedErrorMessage := "admission webhook \"vwebhooktest.kb.io\" denied the request: WebhookTest.test.operators.coreos.com \"my-cr-1\" is invalid: spec.schedule: Invalid value: false: Spec.Valid must be true" 742 Eventually(func() bool { 743 err := c.CreateCustomResource(invalidCR) 744 if err == nil || expectedErrorMessage != err.Error() { 745 return false 746 } 747 return true 748 }).Should(BeTrue(), "The admission webhook should have rejected the invalid resource") 749 750 By(`An valid custom resource is acceoted by the validating webhook and the mutating webhook sets the CR's spec.mutate field to true.`) 751 validCR := &unstructured.Unstructured{ 752 Object: map[string]interface{}{ 753 "apiVersion": "webhook.operators.coreos.io/v1", 754 "kind": "webhooktests", 755 "metadata": map[string]interface{}{ 756 "namespace": generatedNamespace.GetName(), 757 "name": "my-cr-1", 758 }, 759 "spec": map[string]interface{}{ 760 "valid": true, 761 }, 762 }, 763 } 764 crCleanupFunc, err := createCR(c, validCR, "webhook.operators.coreos.io", "v1", generatedNamespace.GetName(), "webhooktests", "my-cr-1") 765 defer crCleanupFunc() 766 require.NoError(GinkgoT(), err, "The valid CR should have been approved by the validating webhook") 767 768 By(`Check that you can get v1 of the webhooktest cr`) 769 v1UnstructuredObject, err := c.GetCustomResource("webhook.operators.coreos.io", "v1", generatedNamespace.GetName(), "webhooktests", "my-cr-1") 770 require.NoError(GinkgoT(), err, "Unable to get the v1 of the valid CR") 771 v1Object := v1UnstructuredObject.Object 772 v1Spec, ok := v1Object["spec"].(map[string]interface{}) 773 require.True(GinkgoT(), ok, "Unable to get spec of v1 object") 774 v1SpecMutate, ok := v1Spec["mutate"].(bool) 775 require.True(GinkgoT(), ok, "Unable to get spec.mutate of v1 object") 776 v1SpecValid, ok := v1Spec["valid"].(bool) 777 require.True(GinkgoT(), ok, "Unable to get spec.valid of v1 object") 778 779 require.True(GinkgoT(), v1SpecMutate, "The mutating webhook should have set the valid CR's spec.mutate field to true") 780 require.True(GinkgoT(), v1SpecValid, "The validating webhook should have required that the CR's spec.valid field is true") 781 782 By(`Check that you can get v2 of the webhooktest cr`) 783 v2UnstructuredObject, err := c.GetCustomResource("webhook.operators.coreos.io", "v2", generatedNamespace.GetName(), "webhooktests", "my-cr-1") 784 require.NoError(GinkgoT(), err, "Unable to get the v2 of the valid CR") 785 v2Object := v2UnstructuredObject.Object 786 v2Spec := v2Object["spec"].(map[string]interface{}) 787 require.True(GinkgoT(), ok, "Unable to get spec of v2 object") 788 v2SpecConversion, ok := v2Spec["conversion"].(map[string]interface{}) 789 require.True(GinkgoT(), ok, "Unable to get spec.conversion of v2 object") 790 v2SpecConversionMutate := v2SpecConversion["mutate"].(bool) 791 require.True(GinkgoT(), ok, "Unable to get spec.conversion.mutate of v2 object") 792 v2SpecConversionValid := v2SpecConversion["valid"].(bool) 793 require.True(GinkgoT(), ok, "Unable to get spec.conversion.valid of v2 object") 794 require.True(GinkgoT(), v2SpecConversionMutate) 795 require.True(GinkgoT(), v2SpecConversionValid) 796 797 By(`Check that conversion strategies are disabled after uninstalling the operator.`) 798 err = crc.OperatorsV1alpha1().ClusterServiceVersions(generatedNamespace.GetName()).Delete(context.TODO(), csvName, metav1.DeleteOptions{}) 799 require.NoError(GinkgoT(), err) 800 801 Eventually(func() error { 802 crd, err := c.ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), "webhooktests.webhook.operators.coreos.io", metav1.GetOptions{}) 803 if err != nil { 804 return err 805 } 806 807 if crd.Spec.Conversion.Strategy != apiextensionsv1.NoneConverter { 808 return fmt.Errorf("conversion strategy is not NoneConverter") 809 } 810 if crd.Spec.Conversion.Webhook != nil { 811 return fmt.Errorf("webhook is not nil") 812 } 813 return nil 814 }).Should(Succeed()) 815 }) 816 }) 817 When("WebhookDescription has conversionCRDs field", func() { 818 var cleanupCSV cleanupFunc 819 820 BeforeEach(func() { 821 By(`global operator group`) 822 og := newOperatorGroup(generatedNamespace.GetName(), genName("global-og-"), nil, nil, []string{}, false) 823 og, err := crc.OperatorsV1().OperatorGroups(generatedNamespace.GetName()).Create(context.TODO(), og, metav1.CreateOptions{}) 824 Expect(err).Should(BeNil()) 825 }) 826 827 AfterEach(func() { 828 if cleanupCSV != nil { 829 cleanupCSV() 830 } 831 }) 832 833 It("The conversion CRD is not updated via webhook when CSV does not own this CRD", func() { 834 By(`create CRD (crdA)`) 835 crdAPlural := genName("mockcrda") 836 crdA := newV1CRD(crdAPlural) 837 cleanupCRD, er := createCRD(c, crdA) 838 require.NoError(GinkgoT(), er) 839 defer cleanupCRD() 840 841 By(`create another CRD (crdB)`) 842 crdBPlural := genName("mockcrdb") 843 crdB := newV1CRD(crdBPlural) 844 cleanupCRD2, er := createCRD(c, crdB) 845 require.NoError(GinkgoT(), er) 846 defer cleanupCRD2() 847 848 By(`describe webhook`) 849 sideEffect := admissionregistrationv1.SideEffectClassNone 850 webhook := operatorsv1alpha1.WebhookDescription{ 851 GenerateName: webhookName, 852 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 853 DeploymentName: genName("webhook-dep-"), 854 ContainerPort: 443, 855 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 856 SideEffects: &sideEffect, 857 ConversionCRDs: []string{crdA.GetName(), crdB.GetName()}, 858 } 859 860 ownedCRDDescs := make([]operatorsv1alpha1.CRDDescription, 0) 861 862 By(`create CSV`) 863 csv := createCSVWithWebhookAndCrds(generatedNamespace.GetName(), webhook, ownedCRDDescs) 864 865 var err error 866 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 867 Expect(err).Should(BeNil()) 868 869 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 870 Expect(err).Should(BeNil()) 871 actualWebhook, err := getWebhookWithGenerateName(c, webhook.GenerateName) 872 Expect(err).Should(BeNil()) 873 874 expected := &metav1.LabelSelector{ 875 MatchLabels: map[string]string(nil), 876 MatchExpressions: []metav1.LabelSelectorRequirement(nil), 877 } 878 Expect(actualWebhook.Webhooks[0].NamespaceSelector).Should(Equal(expected)) 879 880 expectedUpdatedCrdFields := &apiextensionsv1.CustomResourceConversion{ 881 Strategy: "Webhook", 882 } 883 884 By(`Read the updated crdA on cluster into the following crd`) 885 tempCrdA, err := c.ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crdA.GetName(), metav1.GetOptions{}) 886 887 By(`Read the updated crdB on cluster into the following crd`) 888 tempCrdB, err := c.ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crdB.GetName(), metav1.GetOptions{}) 889 890 Expect(tempCrdA.Spec.Conversion.Strategy).Should(Equal(expectedUpdatedCrdFields.Strategy)) 891 Expect(tempCrdB.Spec.Conversion.Strategy).Should(Equal(expectedUpdatedCrdFields.Strategy)) 892 893 var expectedTempPort int32 = 443 894 expectedConvertPath := "/convert" 895 expectedConvertNamespace := "system" 896 897 Expect(tempCrdA.Spec.Conversion.Webhook.ClientConfig.Service.Port).Should(Equal(&expectedTempPort)) 898 Expect(tempCrdA.Spec.Conversion.Webhook.ClientConfig.Service.Path).Should(Equal(&expectedConvertPath)) 899 Expect(tempCrdA.Spec.Conversion.Webhook.ClientConfig.Service.Name).Should(Equal("webhook-service")) 900 Expect(tempCrdA.Spec.Conversion.Webhook.ClientConfig.Service.Namespace).Should(Equal(expectedConvertNamespace)) 901 }) 902 It("The CSV is not created when dealing with conversionCRD and multiple installModes support exists", func() { 903 By(`create CRD (crdA)`) 904 crdAPlural := genName("mockcrda") 905 crdA := newV1CRD(crdAPlural) 906 cleanupCRD, er := createCRD(c, crdA) 907 require.NoError(GinkgoT(), er) 908 defer cleanupCRD() 909 910 By(`describe webhook`) 911 sideEffect := admissionregistrationv1.SideEffectClassNone 912 webhook := operatorsv1alpha1.WebhookDescription{ 913 GenerateName: webhookName, 914 Type: operatorsv1alpha1.ValidatingAdmissionWebhook, 915 DeploymentName: genName("webhook-dep-"), 916 ContainerPort: 443, 917 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 918 SideEffects: &sideEffect, 919 ConversionCRDs: []string{crdA.GetName()}, 920 } 921 922 ownedCRDDescs := make([]operatorsv1alpha1.CRDDescription, 0) 923 ownedCRDDescs = append(ownedCRDDescs, operatorsv1alpha1.CRDDescription{Name: crdA.GetName(), Version: crdA.Spec.Versions[0].Name, Kind: crdA.Spec.Names.Kind}) 924 925 By(`create CSV`) 926 csv := createCSVWithWebhookAndCrdsAndInvalidInstallModes(generatedNamespace.GetName(), webhook, ownedCRDDescs) 927 928 var err error 929 cleanupCSV, err = createCSV(c, crc, csv, generatedNamespace.GetName(), false, false) 930 Expect(err).Should(BeNil()) 931 932 _, err = fetchCSV(crc, generatedNamespace.GetName(), csv.Name, csvSucceededChecker) 933 Expect(err).Should(BeNil()) 934 actualWebhook, err := getWebhookWithGenerateName(c, webhook.GenerateName) 935 Expect(err).Should(BeNil()) 936 937 expected := &metav1.LabelSelector{ 938 MatchLabels: map[string]string(nil), 939 MatchExpressions: []metav1.LabelSelectorRequirement(nil), 940 } 941 Expect(actualWebhook.Webhooks[0].NamespaceSelector).Should(Equal(expected)) 942 943 expectedUpdatedCrdFields := &apiextensionsv1.CustomResourceConversion{ 944 Strategy: "Webhook", 945 } 946 947 By(`Read the updated crdA on cluster into the following crd`) 948 tempCrdA, err := c.ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crdA.GetName(), metav1.GetOptions{}) 949 950 Expect(tempCrdA.Spec.Conversion.Strategy).Should(Equal(expectedUpdatedCrdFields.Strategy)) 951 952 var expectedTempPort int32 = 443 953 expectedConvertPath := "/convert" 954 955 Expect(tempCrdA.Spec.Conversion.Webhook.ClientConfig.Service.Port).Should(Equal(&expectedTempPort)) 956 Expect(tempCrdA.Spec.Conversion.Webhook.ClientConfig.Service.Path).Should(Equal(&expectedConvertPath)) 957 By(`CRD namespace would not be updated, hence conversion webhook won't work for objects of this CRD's Kind`) 958 Expect(tempCrdA.Spec.Conversion.Webhook.ClientConfig.Service.Namespace).ShouldNot(Equal(csv.GetNamespace())) 959 }) 960 }) 961 }) 962 963 func getWebhookWithGenerateName(c operatorclient.ClientInterface, generateName string) (*admissionregistrationv1.ValidatingWebhookConfiguration, error) { 964 webhookSelector := labels.SelectorFromSet(map[string]string{install.WebhookDescKey: generateName}).String() 965 existingWebhooks, err := c.KubernetesInterface().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{LabelSelector: webhookSelector}) 966 if err != nil { 967 return nil, err 968 } 969 970 if len(existingWebhooks.Items) > 0 { 971 return &existingWebhooks.Items[0], nil 972 } 973 return nil, fmt.Errorf("NotFound") 974 } 975 976 func createCSVWithWebhook(namespace string, webhookDesc operatorsv1alpha1.WebhookDescription) operatorsv1alpha1.ClusterServiceVersion { 977 return operatorsv1alpha1.ClusterServiceVersion{ 978 TypeMeta: metav1.TypeMeta{ 979 Kind: operatorsv1alpha1.ClusterServiceVersionKind, 980 APIVersion: operatorsv1alpha1.ClusterServiceVersionAPIVersion, 981 }, 982 ObjectMeta: metav1.ObjectMeta{ 983 Name: genName("webhook-csv-"), 984 Namespace: namespace, 985 }, 986 Spec: operatorsv1alpha1.ClusterServiceVersionSpec{ 987 WebhookDefinitions: []operatorsv1alpha1.WebhookDescription{ 988 webhookDesc, 989 }, 990 InstallModes: []operatorsv1alpha1.InstallMode{ 991 { 992 Type: operatorsv1alpha1.InstallModeTypeOwnNamespace, 993 Supported: true, 994 }, 995 { 996 Type: operatorsv1alpha1.InstallModeTypeSingleNamespace, 997 Supported: true, 998 }, 999 { 1000 Type: operatorsv1alpha1.InstallModeTypeMultiNamespace, 1001 Supported: true, 1002 }, 1003 { 1004 Type: operatorsv1alpha1.InstallModeTypeAllNamespaces, 1005 Supported: true, 1006 }, 1007 }, 1008 InstallStrategy: newNginxInstallStrategy(webhookDesc.DeploymentName, nil, nil), 1009 }, 1010 } 1011 } 1012 1013 func createCSVWithWebhookAndCrds(namespace string, webhookDesc operatorsv1alpha1.WebhookDescription, ownedCRDDescs []operatorsv1alpha1.CRDDescription) operatorsv1alpha1.ClusterServiceVersion { 1014 return operatorsv1alpha1.ClusterServiceVersion{ 1015 TypeMeta: metav1.TypeMeta{ 1016 Kind: operatorsv1alpha1.ClusterServiceVersionKind, 1017 APIVersion: operatorsv1alpha1.ClusterServiceVersionAPIVersion, 1018 }, 1019 ObjectMeta: metav1.ObjectMeta{ 1020 Name: genName("webhook-csv-"), 1021 Namespace: namespace, 1022 }, 1023 Spec: operatorsv1alpha1.ClusterServiceVersionSpec{ 1024 WebhookDefinitions: []operatorsv1alpha1.WebhookDescription{ 1025 webhookDesc, 1026 }, 1027 CustomResourceDefinitions: operatorsv1alpha1.CustomResourceDefinitions{ 1028 Owned: ownedCRDDescs, 1029 }, 1030 InstallModes: []operatorsv1alpha1.InstallMode{ 1031 { 1032 Type: operatorsv1alpha1.InstallModeTypeOwnNamespace, 1033 Supported: false, 1034 }, 1035 { 1036 Type: operatorsv1alpha1.InstallModeTypeSingleNamespace, 1037 Supported: false, 1038 }, 1039 { 1040 Type: operatorsv1alpha1.InstallModeTypeMultiNamespace, 1041 Supported: false, 1042 }, 1043 { 1044 Type: operatorsv1alpha1.InstallModeTypeAllNamespaces, 1045 Supported: true, 1046 }, 1047 }, 1048 InstallStrategy: newNginxInstallStrategy(webhookDesc.DeploymentName, nil, nil), 1049 }, 1050 } 1051 } 1052 1053 func createCSVWithWebhookAndCrdsAndInvalidInstallModes(namespace string, webhookDesc operatorsv1alpha1.WebhookDescription, ownedCRDDescs []operatorsv1alpha1.CRDDescription) operatorsv1alpha1.ClusterServiceVersion { 1054 return operatorsv1alpha1.ClusterServiceVersion{ 1055 TypeMeta: metav1.TypeMeta{ 1056 Kind: operatorsv1alpha1.ClusterServiceVersionKind, 1057 APIVersion: operatorsv1alpha1.ClusterServiceVersionAPIVersion, 1058 }, 1059 ObjectMeta: metav1.ObjectMeta{ 1060 Name: genName("webhook-csv-"), 1061 Namespace: namespace, 1062 }, 1063 Spec: operatorsv1alpha1.ClusterServiceVersionSpec{ 1064 WebhookDefinitions: []operatorsv1alpha1.WebhookDescription{ 1065 webhookDesc, 1066 }, 1067 CustomResourceDefinitions: operatorsv1alpha1.CustomResourceDefinitions{ 1068 Owned: ownedCRDDescs, 1069 }, 1070 InstallModes: []operatorsv1alpha1.InstallMode{ 1071 { 1072 Type: operatorsv1alpha1.InstallModeTypeOwnNamespace, 1073 Supported: true, 1074 }, 1075 { 1076 Type: operatorsv1alpha1.InstallModeTypeSingleNamespace, 1077 Supported: false, 1078 }, 1079 { 1080 Type: operatorsv1alpha1.InstallModeTypeMultiNamespace, 1081 Supported: false, 1082 }, 1083 { 1084 Type: operatorsv1alpha1.InstallModeTypeAllNamespaces, 1085 Supported: true, 1086 }, 1087 }, 1088 InstallStrategy: newNginxInstallStrategy(webhookDesc.DeploymentName, nil, nil), 1089 }, 1090 } 1091 } 1092 1093 func newV1CRD(plural string) apiextensionsv1.CustomResourceDefinition { 1094 path := "/convert" 1095 var port int32 = 443 1096 var min float64 = 2 1097 var max float64 = 256 1098 crd := apiextensionsv1.CustomResourceDefinition{ 1099 ObjectMeta: metav1.ObjectMeta{ 1100 Name: plural + ".cluster.com", 1101 }, 1102 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1103 Group: "cluster.com", 1104 Scope: apiextensionsv1.NamespaceScoped, 1105 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1106 { 1107 Name: "v1alpha1", 1108 Served: true, 1109 Storage: true, 1110 Schema: &apiextensionsv1.CustomResourceValidation{ 1111 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1112 Type: "object", 1113 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1114 "spec": { 1115 Type: "object", 1116 Description: "Spec of a test object.", 1117 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1118 "scalar": { 1119 Type: "number", 1120 Description: "Scalar value that should have a min and max.", 1121 Minimum: &min, 1122 Maximum: &max, 1123 }, 1124 }, 1125 }, 1126 }, 1127 }, 1128 }, 1129 }, 1130 { 1131 Name: "v1alpha2", 1132 Served: true, 1133 Storage: false, 1134 Schema: &apiextensionsv1.CustomResourceValidation{ 1135 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1136 Type: "object", 1137 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1138 "spec": { 1139 Type: "object", 1140 Description: "Spec of a test object.", 1141 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1142 "scalar": { 1143 Type: "number", 1144 Description: "Scalar value that should have a min and max.", 1145 Minimum: &min, 1146 Maximum: &max, 1147 }, 1148 }, 1149 }, 1150 }, 1151 }, 1152 }, 1153 }, 1154 }, 1155 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1156 Plural: plural, 1157 Singular: plural, 1158 Kind: plural, 1159 ListKind: plural + "list", 1160 }, 1161 PreserveUnknownFields: false, 1162 Conversion: &apiextensionsv1.CustomResourceConversion{ 1163 Strategy: "Webhook", 1164 Webhook: &apiextensionsv1.WebhookConversion{ 1165 ClientConfig: &apiextensionsv1.WebhookClientConfig{ 1166 Service: &apiextensionsv1.ServiceReference{ 1167 Namespace: "system", 1168 Name: "webhook-service", 1169 Path: &path, 1170 Port: &port, 1171 }, 1172 }, 1173 ConversionReviewVersions: []string{"v1", "v1beta1"}, 1174 }, 1175 }, 1176 }, 1177 Status: apiextensionsv1.CustomResourceDefinitionStatus{ 1178 StoredVersions: []string{"v1alpha1", "v1alpha2"}, 1179 }, 1180 } 1181 1182 return crd 1183 } 1184 1185 func generateExpiredCerts(hosts []string) ([]byte, []byte, []byte) { 1186 caSerial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) 1187 Expect(err).Should(BeNil()) 1188 1189 caDetails := &x509.Certificate{ 1190 SerialNumber: caSerial, 1191 Subject: pkix.Name{ 1192 CommonName: fmt.Sprintf("olm-selfsigned-%x", caSerial), 1193 Organization: []string{install.Organization}, 1194 }, 1195 NotBefore: time.Now().Add(-5 * time.Minute), 1196 NotAfter: time.Now().Add(-5 * time.Minute), 1197 IsCA: true, 1198 KeyUsage: x509.KeyUsageCertSign, 1199 BasicConstraintsValid: true, 1200 } 1201 1202 caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 1203 Expect(err).Should(BeNil()) 1204 1205 caRaw, err := x509.CreateCertificate(rand.Reader, caDetails, caDetails, &caKey.PublicKey, caKey) 1206 Expect(err).Should(BeNil()) 1207 1208 caCert, err := x509.ParseCertificate(caRaw) 1209 Expect(err).Should(BeNil()) 1210 1211 caPEM, _, err := (&certs.KeyPair{ 1212 Cert: caCert, 1213 Priv: caKey, 1214 }).ToPEM() 1215 Expect(err).Should(BeNil()) 1216 1217 serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) 1218 Expect(err).Should(BeNil()) 1219 1220 certDetails := &x509.Certificate{ 1221 SerialNumber: serial, 1222 Subject: pkix.Name{ 1223 CommonName: hosts[0], 1224 Organization: []string{install.Organization}, 1225 }, 1226 NotBefore: time.Now(), 1227 NotAfter: time.Now(), 1228 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 1229 BasicConstraintsValid: true, 1230 DNSNames: hosts, 1231 } 1232 1233 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 1234 Expect(err).Should(BeNil()) 1235 1236 publicKey := &privateKey.PublicKey 1237 certRaw, err := x509.CreateCertificate(rand.Reader, certDetails, caCert, publicKey, caKey) 1238 Expect(err).Should(BeNil()) 1239 1240 cert, err := x509.ParseCertificate(certRaw) 1241 Expect(err).Should(BeNil()) 1242 1243 certPEM, privPEM, err := (&certs.KeyPair{ 1244 Cert: cert, 1245 Priv: privateKey, 1246 }).ToPEM() 1247 Expect(err).Should(BeNil()) 1248 1249 return caPEM, certPEM, privPEM 1250 }