From 19447d0cf4e24e2dc99d2030e22eeb2cfe02ec77 Mon Sep 17 00:00:00 2001 From: Camila Macedo <7708031+camilamacedo86@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:33:54 +0100 Subject: [PATCH] UPSTREAM: : [OTE] add webhook tests Migrates OLMv1 webhook operator tests from using external YAML files to defining resources in Go structs. This change removes file dependencies, improving test reliability and simplifying test setup. The migration is a refactoring of code from https://github.com/openshift/origin/pull/30059. The new code uses better naming conventions and adapts the tests to work with a controller-runtime client, enhancing test consistency and maintainability. The migration covers all core test scenarios: - Validating, mutating, and conversion webhooks. - Certificate and secret rotation tolerance. Assisted-by: Gemini --- .../openshift_payload_olmv1.json | 50 +++ .../tests-extension/pkg/helpers/catalogs.go | 52 +++ openshift/tests-extension/test/webhooks.go | 362 ++++++++++++++++++ 3 files changed, 464 insertions(+) create mode 100644 openshift/tests-extension/pkg/helpers/catalogs.go create mode 100644 openshift/tests-extension/test/webhooks.go diff --git a/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json b/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json index 87eb2453f..d0a2125c9 100644 --- a/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json +++ b/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json @@ -48,5 +48,55 @@ "source": "openshift:payload:olmv1", "lifecycle": "blocking", "environmentSelector": {} + }, + { + "name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working validating webhook", + "labels": {}, + "resources": { + "isolation": {} + }, + "source": "openshift:payload:olmv1", + "lifecycle": "blocking", + "environmentSelector": {} + }, + { + "name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working mutating webhook", + "labels": {}, + "resources": { + "isolation": {} + }, + "source": "openshift:payload:olmv1", + "lifecycle": "blocking", + "environmentSelector": {} + }, + { + "name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working conversion webhook", + "labels": {}, + "resources": { + "isolation": {} + }, + "source": "openshift:payload:olmv1", + "lifecycle": "blocking", + "environmentSelector": {} + }, + { + "name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should be tolerant to openshift-service-ca certificate rotation", + "labels": {}, + "resources": { + "isolation": {} + }, + "source": "openshift:payload:olmv1", + "lifecycle": "blocking", + "environmentSelector": {} + }, + { + "name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should be tolerant to tls secret deletion", + "labels": {}, + "resources": { + "isolation": {} + }, + "source": "openshift:payload:olmv1", + "lifecycle": "blocking", + "environmentSelector": {} } ] diff --git a/openshift/tests-extension/pkg/helpers/catalogs.go b/openshift/tests-extension/pkg/helpers/catalogs.go new file mode 100644 index 000000000..8409fed96 --- /dev/null +++ b/openshift/tests-extension/pkg/helpers/catalogs.go @@ -0,0 +1,52 @@ +package helpers + +import ( + "context" + "fmt" + "time" + + //nolint:staticcheck // ST1001: dot-imports for readability + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + olmv1 "github.com/operator-framework/operator-controller/api/v1" + + "github/operator-framework-operator-controller/openshift/tests-extension/pkg/env" +) + +// NewClusterCatalog returns a new ClusterCatalog object. +// It sets the image reference as source. +func NewClusterCatalog(name, imageRef string) *olmv1.ClusterCatalog { + return &olmv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: olmv1.ClusterCatalogSpec{ + Source: olmv1.CatalogSource{ + Type: olmv1.SourceTypeImage, + Image: &olmv1.ImageSource{ + Ref: imageRef, + }, + }, + }, + } +} + +// ExpectCatalogToBeServing checks that the catalog with the given name is installed +func ExpectCatalogToBeServing(ctx context.Context, name string) { + k8sClient := env.Get().K8sClient + Eventually(func(g Gomega) { + var catalog olmv1.ClusterCatalog + err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, &catalog) + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get catalog %q", name)) + + conditions := catalog.Status.Conditions + g.Expect(conditions).NotTo(BeEmpty(), fmt.Sprintf("catalog %q has empty status.conditions", name)) + + g.Expect(meta.IsStatusConditionPresentAndEqual(conditions, olmv1.TypeServing, metav1.ConditionTrue)). + To(BeTrue(), fmt.Sprintf("catalog %q is not serving", name)) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) +} diff --git a/openshift/tests-extension/test/webhooks.go b/openshift/tests-extension/test/webhooks.go new file mode 100644 index 000000000..3bb53567e --- /dev/null +++ b/openshift/tests-extension/test/webhooks.go @@ -0,0 +1,362 @@ +package test + +import ( + "context" + "fmt" + "strings" + "time" + + //nolint:staticcheck // ST1001: dot-imports for readability + . "github.com/onsi/ginkgo/v2" + //nolint:staticcheck // ST1001: dot-imports for readability + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/controller-runtime/pkg/client" + + olmv1 "github.com/operator-framework/operator-controller/api/v1" + + "github/operator-framework-operator-controller/openshift/tests-extension/pkg/env" + "github/operator-framework-operator-controller/openshift/tests-extension/pkg/helpers" +) + +const ( + openshiftServiceCANamespace = "openshift-service-ca" + openshiftServiceCASigningKeySecretName = "signing-key" + + webhookCatalogName = "webhook-operator-catalog" + webhookOperatorPackageName = "webhook-operator" + webhookOperatorCRDName = "webhooktests.webhook.operators.coreos.io" +) + +var _ = Describe("[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks", + Ordered, Serial, func() { + var ( + k8sClient client.Client + dynamicClient dynamic.Interface + webhookOperatorInstallNamespace string + cleanup func(ctx context.Context) + ) + + BeforeEach(func(ctx SpecContext) { + By("initializing Kubernetes client and dynamic client") + k8sClient = env.Get().K8sClient + restCfg := env.Get().RestCfg + var err error + dynamicClient, err = dynamic.NewForConfig(restCfg) + Expect(err).ToNot(HaveOccurred(), "failed to create dynamic client") + + By("requiring OLMv1 capability on OpenShift") + helpers.RequireOLMv1CapabilityOnOpenshift() + + By("ensuring no ClusterExtension and CRD from a previous run") + helpers.EnsureCleanupClusterExtension(ctx, webhookOperatorPackageName, webhookOperatorCRDName) + + By(fmt.Sprintf("checking if the %s exists", webhookCatalogName)) + catalog := &olmv1.ClusterCatalog{} + err = k8sClient.Get(ctx, client.ObjectKey{Name: webhookCatalogName}, catalog) + if apierrors.IsNotFound(err) { + By(fmt.Sprintf("creating the webhook-operator catalog with name %s", webhookCatalogName)) + catalog = helpers.NewClusterCatalog(webhookCatalogName, "quay.io/operator-framework/webhook-operator-index:0.0.3") + err = k8sClient.Create(ctx, catalog) + Expect(err).ToNot(HaveOccurred()) + + By("waiting for the webhook-operator catalog to be serving") + helpers.ExpectCatalogToBeServing(ctx, webhookCatalogName) + } else { + By(fmt.Sprintf("webhook-operator catalog %s already exists, skipping creation", webhookCatalogName)) + } + webhookOperatorInstallNamespace = fmt.Sprintf("webhook-operator-%s", rand.String(5)) + cleanup = setupWebhookOperator(ctx, k8sClient, webhookOperatorInstallNamespace) + }) + + AfterEach(func(ctx SpecContext) { + By("performing webhook operator cleanup") + if cleanup != nil { + cleanup(ctx) + } + }) + + It("should have a working validating webhook", func(ctx SpecContext) { + By("creating a webhook test resource that will be rejected by the validating webhook") + Eventually(func() error { + name := fmt.Sprintf("validating-webhook-test-%s", rand.String(5)) + obj := newWebhookTestV1(name, webhookOperatorInstallNamespace, false) + + _, err := dynamicClient. + Resource(webhookTestGVRV1). + Namespace(webhookOperatorInstallNamespace). + Create(ctx, obj, metav1.CreateOptions{}) + + switch { + case err == nil: + // Webhook not ready yet; clean up and keep polling. + _ = dynamicClient.Resource(webhookTestGVRV1). + Namespace(webhookOperatorInstallNamespace). + Delete(ctx, name, metav1.DeleteOptions{}) + return fmt.Errorf("webhook not rejecting yet") + case strings.Contains(err.Error(), "Invalid value: false: Spec.Valid must be true"): + return nil // got the expected validating-webhook rejection + default: + return fmt.Errorf("unexpected error: %v", err) + } + }).WithTimeout(2 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + }) + + It("should have a working mutating webhook", func(ctx SpecContext) { + By("creating a valid webhook test resource") + mutatingWebhookResourceName := "mutating-webhook-test" + resource := newWebhookTestV1(mutatingWebhookResourceName, webhookOperatorInstallNamespace, true) + Eventually(func(g Gomega) { + _, err := dynamicClient.Resource(webhookTestGVRV1).Namespace(webhookOperatorInstallNamespace).Create(ctx, resource, metav1.CreateOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + }).WithTimeout(1 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + + By("getting the created resource in v1 schema") + obj, err := dynamicClient.Resource(webhookTestGVRV1).Namespace(webhookOperatorInstallNamespace).Get(ctx, mutatingWebhookResourceName, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(obj).ToNot(BeNil()) + + By("validating the resource spec") + spec := obj.Object["spec"].(map[string]interface{}) + Expect(spec).To(Equal(map[string]interface{}{ + "valid": true, + "mutate": true, + })) + }) + + It("should have a working conversion webhook", func(ctx SpecContext) { + By("creating a conversion webhook test resource") + conversionWebhookResourceName := "conversion-webhook-test" + resourceV1 := newWebhookTestV1(conversionWebhookResourceName, webhookOperatorInstallNamespace, true) + Eventually(func(g Gomega) { + _, err := dynamicClient.Resource(webhookTestGVRV1).Namespace(webhookOperatorInstallNamespace).Create(ctx, resourceV1, metav1.CreateOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + }).WithTimeout(1 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + + By("getting the created resource in v2 schema") + obj, err := dynamicClient.Resource(webhookTestGVRV2).Namespace(webhookOperatorInstallNamespace).Get(ctx, conversionWebhookResourceName, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(obj).ToNot(BeNil()) + + By("validating the resource spec") + spec := obj.Object["spec"].(map[string]interface{}) + Expect(spec).To(Equal(map[string]interface{}{ + "conversion": map[string]interface{}{ + "valid": true, + "mutate": true, + }, + })) + }) + + It("should be tolerant to openshift-service-ca certificate rotation", func(ctx SpecContext) { + By("deleting the openshift-service-ca signing-key secret") + signingKeySecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: openshiftServiceCASigningKeySecretName, + Namespace: openshiftServiceCANamespace, + }, + } + err := k8sClient.Delete(ctx, signingKeySecret, client.PropagationPolicy(metav1.DeletePropagationBackground)) + Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) + + By("waiting for the webhook operator's service certificate secret to be recreated and populated") + certificateSecretName := "webhook-operator-webhook-service-cert" + Eventually(func(g Gomega) { + secret := &corev1.Secret{} + err := k8sClient.Get(ctx, client.ObjectKey{Name: certificateSecretName, Namespace: webhookOperatorInstallNamespace}, secret) + if apierrors.IsNotFound(err) { + GinkgoLogr.Info(fmt.Sprintf("Secret %s/%s not found yet (still polling for recreation)", webhookOperatorInstallNamespace, certificateSecretName)) + return + } + + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get webhook service certificate secret %s/%s: %v", webhookOperatorInstallNamespace, certificateSecretName, err)) + g.Expect(secret.Data).ToNot(BeEmpty(), "expected webhook service certificate secret data to not be empty after recreation") + }).WithTimeout(2*time.Minute).WithPolling(10*time.Second).Should(Succeed(), "webhook service certificate secret did not get recreated and populated within timeout") + + By("checking webhook is responsive through cert rotation") + Eventually(func(g Gomega) { + resourceName := fmt.Sprintf("cert-rotation-test-%s", rand.String(5)) + resource := newWebhookTestV1(resourceName, webhookOperatorInstallNamespace, true) + + _, err := dynamicClient.Resource(webhookTestGVRV1).Namespace(webhookOperatorInstallNamespace).Create(ctx, resource, metav1.CreateOptions{}) + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to create test resource %s: %v", resourceName, err)) + + err = dynamicClient.Resource(webhookTestGVRV1).Namespace(webhookOperatorInstallNamespace).Delete(ctx, resource.GetName(), metav1.DeleteOptions{}) + g.Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred(), fmt.Sprintf("failed to delete test resource %s: %v", resourceName, err)) + }).WithTimeout(2 * time.Minute).WithPolling(10 * time.Second).Should(Succeed()) + }) + + It("should be tolerant to tls secret deletion", func(ctx SpecContext) { + certificateSecretName := "webhook-operator-webhook-service-cert" + By("ensuring secret exists before deletion attempt") + Eventually(func(g Gomega) { + secret := &corev1.Secret{} + err := k8sClient.Get(ctx, client.ObjectKey{Name: certificateSecretName, Namespace: webhookOperatorInstallNamespace}, secret) + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get secret %s/%s", webhookOperatorInstallNamespace, certificateSecretName)) + }).WithTimeout(1 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + + By("checking webhook is responsive through secret recreation after manual deletion") + tlsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: certificateSecretName, + Namespace: webhookOperatorInstallNamespace, + }, + } + err := k8sClient.Delete(ctx, tlsSecret, client.PropagationPolicy(metav1.DeletePropagationBackground)) + Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) + + By("waiting for the webhook operator's service certificate secret to be recreated and populated") + Eventually(func(g Gomega) { + secret := &corev1.Secret{} + err := k8sClient.Get(ctx, client.ObjectKey{Name: certificateSecretName, Namespace: webhookOperatorInstallNamespace}, secret) + if apierrors.IsNotFound(err) { + GinkgoLogr.Info(fmt.Sprintf("Secret %s/%s not found yet (still polling for recreation)", webhookOperatorInstallNamespace, certificateSecretName)) + return + } + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get webhook service certificate secret %s/%s: %v", webhookOperatorInstallNamespace, certificateSecretName, err)) + g.Expect(secret.Data).ToNot(BeEmpty(), "expected webhook service certificate secret data to not be empty after recreation") + }).WithTimeout(2*time.Minute).WithPolling(10*time.Second).Should(Succeed(), "webhook service certificate secret did not get recreated and populated within timeout") + + Eventually(func(g Gomega) { + resourceName := fmt.Sprintf("tls-deletion-test-%s", rand.String(5)) + resource := newWebhookTestV1(resourceName, webhookOperatorInstallNamespace, true) + + _, err := dynamicClient.Resource(webhookTestGVRV1).Namespace(webhookOperatorInstallNamespace).Create(ctx, resource, metav1.CreateOptions{}) + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to create test resource %s: %v", resourceName, err)) + + err = dynamicClient.Resource(webhookTestGVRV1).Namespace(webhookOperatorInstallNamespace).Delete(ctx, resource.GetName(), metav1.DeleteOptions{}) + g.Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred(), fmt.Sprintf("failed to delete test resource %s: %v", resourceName, err)) + }).WithTimeout(2 * time.Minute).WithPolling(10 * time.Second).Should(Succeed()) + }) + }) + +var webhookTestGVRV1 = schema.GroupVersionResource{ + Group: "webhook.operators.coreos.io", + Version: "v1", + Resource: "webhooktests", +} + +var webhookTestGVRV2 = schema.GroupVersionResource{ + Group: "webhook.operators.coreos.io", + Version: "v2", + Resource: "webhooktests", +} + +func newWebhookTestV1(name, namespace string, valid bool) *unstructured.Unstructured { + mutateValue := valid + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "webhook.operators.coreos.io/v1", + "kind": "WebhookTest", + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "valid": valid, + "mutate": mutateValue, + }, + }, + } + return obj +} + +func setupWebhookOperator(ctx SpecContext, k8sClient client.Client, webhookOperatorInstallNamespace string) func(ctx context.Context) { + By(fmt.Sprintf("installing the webhook operator in namespace %s", webhookOperatorInstallNamespace)) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: webhookOperatorInstallNamespace}, + } + err := k8sClient.Create(ctx, ns) + Expect(err).ToNot(HaveOccurred()) + + saName := fmt.Sprintf("%s-installer", webhookOperatorInstallNamespace) + sa := helpers.NewServiceAccount(saName, webhookOperatorInstallNamespace) + err = k8sClient.Create(ctx, sa) + Expect(err).ToNot(HaveOccurred()) + helpers.ExpectServiceAccountExists(ctx, saName, webhookOperatorInstallNamespace) + + By("creating a ClusterRoleBinding to cluster-admin for the webhook operator") + operatorClusterRoleBindingName := fmt.Sprintf("%s-operator-crb", webhookOperatorInstallNamespace) + operatorClusterRoleBinding := helpers.NewClusterRoleBinding(operatorClusterRoleBindingName, "cluster-admin", saName, webhookOperatorInstallNamespace) + err = k8sClient.Create(ctx, operatorClusterRoleBinding) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to create ClusterRoleBinding %s", + operatorClusterRoleBindingName)) + helpers.ExpectClusterRoleBindingExists(ctx, operatorClusterRoleBindingName) + + ceName := webhookOperatorInstallNamespace + ce := helpers.NewClusterExtensionObject("webhook-operator", "0.0.1", ceName, saName, webhookOperatorInstallNamespace) + ce.Spec.Source.Catalog.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "olm.operatorframework.io/metadata.name": webhookCatalogName, + }, + } + err = k8sClient.Create(ctx, ce) + Expect(err).ToNot(HaveOccurred()) + + By("waiting for the webhook operator to be installed") + helpers.ExpectClusterExtensionToBeInstalled(ctx, ceName) + + // Reordered checks: Service -> Secret -> Deployment + By("waiting for the webhook operator's service to be ready") + serviceName := "webhook-operator-webhook-service" // Standard name for the service created by the operator + Eventually(func(g Gomega) { + svc := &corev1.Service{} + err := k8sClient.Get(ctx, client.ObjectKey{Name: serviceName, Namespace: webhookOperatorInstallNamespace}, svc) + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get webhook service %s/%s: %v", webhookOperatorInstallNamespace, serviceName, err)) + g.Expect(svc.Spec.ClusterIP).ToNot(BeEmpty(), "expected webhook service to have a ClusterIP assigned") + g.Expect(svc.Spec.Ports).ToNot(BeEmpty(), "expected webhook service to have ports defined") + }).WithTimeout(1*time.Minute).WithPolling(5*time.Second).Should(Succeed(), "webhook service did not become ready within timeout") + + By("waiting for the webhook operator's service certificate secret to exist and be populated") + certificateSecretName := "webhook-operator-webhook-service-cert" // Fixed to use the static name + Eventually(func(g Gomega) { + secret := &corev1.Secret{} + // Force bypassing the client cache for this Get operation + err := k8sClient.Get(ctx, client.ObjectKey{Name: certificateSecretName, Namespace: webhookOperatorInstallNamespace}, secret) // Removed client.WithCacheDisabled + + if apierrors.IsNotFound(err) { + GinkgoLogr.Info(fmt.Sprintf("Secret %s/%s not found yet (still polling)", webhookOperatorInstallNamespace, certificateSecretName)) + return // Keep polling if not found + } + + g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get webhook service certificate secret %s/%s: %v", + webhookOperatorInstallNamespace, certificateSecretName, err)) + g.Expect(secret.Data).ToNot(BeEmpty(), "expected webhook service certificate secret data to not be empty") + }).WithTimeout(5*time.Minute).WithPolling(5*time.Second).Should(Succeed(), "webhook service certificate secret did not become available within timeout") + + return func(ctx context.Context) { + By(fmt.Sprintf("cleanup: deleting ClusterExtension %s", ce.Name)) + _ = k8sClient.Delete(ctx, ce, client.PropagationPolicy(metav1.DeletePropagationBackground)) + By(fmt.Sprintf("cleanup: deleting ClusterRoleBinding %s", operatorClusterRoleBinding.Name)) + _ = k8sClient.Delete(ctx, operatorClusterRoleBinding, client.PropagationPolicy(metav1.DeletePropagationBackground)) + By(fmt.Sprintf("cleanup: deleting ServiceAccount %s in namespace %s", sa.Name, sa.Namespace)) + _ = k8sClient.Delete(ctx, sa, client.PropagationPolicy(metav1.DeletePropagationBackground)) + By(fmt.Sprintf("cleanup: deleting namespace %s", ns.Name)) + _ = k8sClient.Delete(ctx, ns, client.PropagationPolicy(metav1.DeletePropagationForeground)) + + By(fmt.Sprintf("waiting for namespace %s to be fully deleted", webhookOperatorInstallNamespace)) + pollErr := wait.PollUntilContextTimeout(ctx, 5*time.Second, 2*time.Minute, true, func(pollCtx context.Context) (bool, error) { + var currentNS corev1.Namespace + err := k8sClient.Get(pollCtx, client.ObjectKey{Name: webhookOperatorInstallNamespace}, ¤tNS) + if err != nil { + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + } + return false, nil + }) + if pollErr != nil { + GinkgoLogr.Info(fmt.Sprintf("Warning: namespace %s deletion wait failed: %v", webhookOperatorInstallNamespace, pollErr)) + } + } +}