diff --git a/Makefile b/Makefile index ecfd5f292..afe27822f 100644 --- a/Makefile +++ b/Makefile @@ -90,9 +90,6 @@ EXPERIMENTAL_MANIFEST := ./manifests/experimental.yaml EXPERIMENTAL_E2E_MANIFEST := ./manifests/experimental-e2e.yaml CATALOGS_MANIFEST := ./manifests/default-catalogs.yaml -# Manifest used by kind-deploy, which may be overridden by other targets -SOURCE_MANIFEST := $(STANDARD_MANIFEST) - # Disable -j flag for make .NOTPARALLEL: @@ -277,14 +274,16 @@ test-e2e: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) test-e2e: KIND_CLUSTER_NAME := operator-controller-e2e test-e2e: GO_BUILD_EXTRA_FLAGS := -cover test-e2e: COVERAGE_NAME := e2e -test-e2e: run image-registry prometheus e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster +test-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +test-e2e: run-internal image-registry prometheus e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster .PHONY: test-experimental-e2e test-experimental-e2e: SOURCE_MANIFEST := $(EXPERIMENTAL_E2E_MANIFEST) test-experimental-e2e: KIND_CLUSTER_NAME := operator-controller-e2e test-experimental-e2e: GO_BUILD_EXTRA_FLAGS := -cover test-experimental-e2e: COVERAGE_NAME := experimental-e2e -test-experimental-e2e: run image-registry prometheus experimental-e2e e2e e2e-coverage kind-clean #HELP Run experimental e2e test suite on local kind cluster +test-experimental-e2e: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +test-experimental-e2e: run-internal image-registry prometheus experimental-e2e e2e e2e-coverage kind-clean #HELP Run experimental e2e test suite on local kind cluster .PHONY: prometheus prometheus: PROMETHEUS_NAMESPACE := olmv1-system @@ -293,13 +292,15 @@ prometheus: #EXHELP Deploy Prometheus into specified namespace ./hack/test/install-prometheus.sh $(PROMETHEUS_NAMESPACE) $(PROMETHEUS_VERSION) $(KUSTOMIZE) $(VERSION) .PHONY: test-extension-developer-e2e +test-extension-developer-e2e: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) test-extension-developer-e2e: KIND_CLUSTER_NAME := operator-controller-ext-dev-e2e test-extension-developer-e2e: export INSTALL_DEFAULT_CATALOGS := false -test-extension-developer-e2e: run image-registry extension-developer-e2e kind-clean #HELP Run extension-developer e2e on local kind cluster +test-extension-developer-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +test-extension-developer-e2e: run-internal image-registry extension-developer-e2e kind-clean #HELP Run extension-developer e2e on local kind cluster .PHONY: run-latest-release run-latest-release: - curl -L -s https://github.com/operator-framework/operator-controller/releases/latest/download/$(notdir $(STANDARD_RELEASE_INSTALL)) | bash -s + curl -L -s https://github.com/operator-framework/operator-controller/releases/latest/download/$(notdir $(RELEASE_INSTALL)) | bash -s .PHONY: pre-upgrade-setup pre-upgrade-setup: @@ -309,11 +310,27 @@ pre-upgrade-setup: post-upgrade-checks: go test -count=1 -v ./test/upgrade-e2e/... + +TEST_UPGRADE_E2E_TASKS := kind-cluster run-latest-release image-registry pre-upgrade-setup docker-build kind-load kind-deploy post-upgrade-checks kind-clean + .PHONY: test-upgrade-e2e +test-upgrade-e2e: SOURCE_MANIFEST := $(STANDARD_MANIFEST) +test-upgrade-e2e: RELEASE_INSTALL := $(STANDARD_RELEASE_INSTALL) test-upgrade-e2e: KIND_CLUSTER_NAME := operator-controller-upgrade-e2e +test-upgrade-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) test-upgrade-e2e: export TEST_CLUSTER_CATALOG_NAME := test-catalog test-upgrade-e2e: export TEST_CLUSTER_EXTENSION_NAME := test-package -test-upgrade-e2e: kind-cluster run-latest-release image-registry pre-upgrade-setup docker-build kind-load kind-deploy post-upgrade-checks kind-clean #HELP Run upgrade e2e tests on a local kind cluster +test-upgrade-e2e: $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade e2e tests on a local kind cluster + +.PHONY: test-upgrade-experimental-e2e +test-upgrade-experimental-e2e: SOURCE_MANIFEST := $(EXPERIMENTAL_MANIFEST) +test-upgrade-experimental-e2e: RELEASE_INSTALL := $(EXPERIMENTAL_RELEASE_INSTALL) +test-upgrade-experimental-e2e: KIND_CLUSTER_NAME := operator-controller-upgrade-experimental-e2e +test-upgrade-experimental-e2e: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +test-upgrade-experimental-e2e: export TEST_CLUSTER_CATALOG_NAME := test-catalog +test-upgrade-experimental-e2e: export TEST_CLUSTER_EXTENSION_NAME := test-package +test-upgrade-experimental-e2e: $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade e2e tests on a local kind cluster + .PHONY: e2e-coverage e2e-coverage: @@ -327,7 +344,6 @@ kind-load: $(KIND) #EXHELP Loads the currently constructed images into the KIND $(CONTAINER_RUNTIME) save $(CATD_IMG) | $(KIND) load image-archive /dev/stdin --name $(KIND_CLUSTER_NAME) .PHONY: kind-deploy -kind-deploy: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) kind-deploy: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) kind-deploy: manifests @echo -e "\n\U1F4D8 Using $(SOURCE_MANIFEST) as source manifest\n" @@ -401,12 +417,18 @@ go-build-linux: export GOOS=linux go-build-linux: export GOARCH=amd64 go-build-linux: $(BINARIES) +.PHONY: run-internal +run-internal: docker-build kind-cluster kind-load kind-deploy wait + .PHONY: run -run: docker-build kind-cluster kind-load kind-deploy wait #HELP Build the operator-controller then deploy it into a new kind cluster. +run: SOURCE_MANIFEST := $(STANDARD_MANIFEST) +run: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +run: run-internal #HELP Build operator-controller then deploy it with the standard manifest into a new kind cluster. .PHONY: run-experimental run-experimental: SOURCE_MANIFEST := $(EXPERIMENTAL_MANIFEST) -run-experimental: run #HELP Build the operator-controller then deploy it with the experimental manifest into a new kind cluster. +run-experimental: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +run-experimental: run-internal #HELP Build the operator-controller then deploy it with the experimental manifest into a new kind cluster. CATD_NAMESPACE := olmv1-system wait: diff --git a/commitchecker.yaml b/commitchecker.yaml index 497def527..c4762e5b8 100644 --- a/commitchecker.yaml +++ b/commitchecker.yaml @@ -1,4 +1,4 @@ -expectedMergeBase: 0e0e70605e09910fca8cb701c9e9b436c0f23826 +expectedMergeBase: eebdcea5ba8b91c07719a5342c50f9fabd0ebf83 upstreamBranch: main upstreamOrg: operator-framework upstreamRepo: operator-controller diff --git a/hack/test/pre-upgrade-setup.sh b/hack/test/pre-upgrade-setup.sh index d60c9f03c..669f9da37 100755 --- a/hack/test/pre-upgrade-setup.sh +++ b/hack/test/pre-upgrade-setup.sh @@ -109,8 +109,10 @@ rules: verbs: - get - list + - watch - create - update + - patch - delete - apiGroups: - "olm.operatorframework.io" diff --git a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-certified-operators.yaml b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-certified-operators.yaml index efe7d15f4..7750f4aba 100644 --- a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-certified-operators.yaml +++ b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-certified-operators.yaml @@ -8,4 +8,4 @@ spec: type: Image image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/certified-operator-index:v4.20 + ref: registry.redhat.io/redhat/certified-operator-index:v4.19 diff --git a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-community-operators.yaml b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-community-operators.yaml index eff103c6b..4ec0c36fe 100644 --- a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-community-operators.yaml +++ b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-community-operators.yaml @@ -8,4 +8,4 @@ spec: type: Image image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/community-operator-index:v4.20 + ref: registry.redhat.io/redhat/community-operator-index:v4.19 diff --git a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-marketplace.yaml b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-marketplace.yaml index c2715bc98..99dd78f86 100644 --- a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-marketplace.yaml +++ b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-marketplace.yaml @@ -8,4 +8,4 @@ spec: type: Image image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/redhat-marketplace-index:v4.20 + ref: registry.redhat.io/redhat/redhat-marketplace-index:v4.19 diff --git a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-operators.yaml b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-operators.yaml index cf2030c62..7a1a35071 100644 --- a/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-operators.yaml +++ b/openshift/catalogd/kustomize/overlays/openshift/catalogs/openshift-redhat-operators.yaml @@ -8,4 +8,4 @@ spec: type: Image image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/redhat-operator-index:v4.20 + ref: registry.redhat.io/redhat/redhat-operator-index:v4.19 diff --git a/openshift/catalogd/manifests-experimental/22-clustercatalog-openshift-certified-operators.yml b/openshift/catalogd/manifests-experimental/22-clustercatalog-openshift-certified-operators.yml index 0cf968ad1..abc80ff21 100644 --- a/openshift/catalogd/manifests-experimental/22-clustercatalog-openshift-certified-operators.yml +++ b/openshift/catalogd/manifests-experimental/22-clustercatalog-openshift-certified-operators.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/certified-operator-index:v4.20 + ref: registry.redhat.io/redhat/certified-operator-index:v4.19 type: Image diff --git a/openshift/catalogd/manifests-experimental/23-clustercatalog-openshift-community-operators.yml b/openshift/catalogd/manifests-experimental/23-clustercatalog-openshift-community-operators.yml index d6d7715bd..bf8b9f5d2 100644 --- a/openshift/catalogd/manifests-experimental/23-clustercatalog-openshift-community-operators.yml +++ b/openshift/catalogd/manifests-experimental/23-clustercatalog-openshift-community-operators.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/community-operator-index:v4.20 + ref: registry.redhat.io/redhat/community-operator-index:v4.19 type: Image diff --git a/openshift/catalogd/manifests-experimental/24-clustercatalog-openshift-redhat-marketplace.yml b/openshift/catalogd/manifests-experimental/24-clustercatalog-openshift-redhat-marketplace.yml index e80742c44..2e57fc867 100644 --- a/openshift/catalogd/manifests-experimental/24-clustercatalog-openshift-redhat-marketplace.yml +++ b/openshift/catalogd/manifests-experimental/24-clustercatalog-openshift-redhat-marketplace.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/redhat-marketplace-index:v4.20 + ref: registry.redhat.io/redhat/redhat-marketplace-index:v4.19 type: Image diff --git a/openshift/catalogd/manifests-experimental/25-clustercatalog-openshift-redhat-operators.yml b/openshift/catalogd/manifests-experimental/25-clustercatalog-openshift-redhat-operators.yml index 2e5c28773..34e8180bd 100644 --- a/openshift/catalogd/manifests-experimental/25-clustercatalog-openshift-redhat-operators.yml +++ b/openshift/catalogd/manifests-experimental/25-clustercatalog-openshift-redhat-operators.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/redhat-operator-index:v4.20 + ref: registry.redhat.io/redhat/redhat-operator-index:v4.19 type: Image diff --git a/openshift/catalogd/manifests/22-clustercatalog-openshift-certified-operators.yml b/openshift/catalogd/manifests/22-clustercatalog-openshift-certified-operators.yml index 0cf968ad1..abc80ff21 100644 --- a/openshift/catalogd/manifests/22-clustercatalog-openshift-certified-operators.yml +++ b/openshift/catalogd/manifests/22-clustercatalog-openshift-certified-operators.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/certified-operator-index:v4.20 + ref: registry.redhat.io/redhat/certified-operator-index:v4.19 type: Image diff --git a/openshift/catalogd/manifests/23-clustercatalog-openshift-community-operators.yml b/openshift/catalogd/manifests/23-clustercatalog-openshift-community-operators.yml index d6d7715bd..bf8b9f5d2 100644 --- a/openshift/catalogd/manifests/23-clustercatalog-openshift-community-operators.yml +++ b/openshift/catalogd/manifests/23-clustercatalog-openshift-community-operators.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/community-operator-index:v4.20 + ref: registry.redhat.io/redhat/community-operator-index:v4.19 type: Image diff --git a/openshift/catalogd/manifests/24-clustercatalog-openshift-redhat-marketplace.yml b/openshift/catalogd/manifests/24-clustercatalog-openshift-redhat-marketplace.yml index e80742c44..2e57fc867 100644 --- a/openshift/catalogd/manifests/24-clustercatalog-openshift-redhat-marketplace.yml +++ b/openshift/catalogd/manifests/24-clustercatalog-openshift-redhat-marketplace.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/redhat-marketplace-index:v4.20 + ref: registry.redhat.io/redhat/redhat-marketplace-index:v4.19 type: Image diff --git a/openshift/catalogd/manifests/25-clustercatalog-openshift-redhat-operators.yml b/openshift/catalogd/manifests/25-clustercatalog-openshift-redhat-operators.yml index 2e5c28773..34e8180bd 100644 --- a/openshift/catalogd/manifests/25-clustercatalog-openshift-redhat-operators.yml +++ b/openshift/catalogd/manifests/25-clustercatalog-openshift-redhat-operators.yml @@ -8,5 +8,5 @@ spec: source: image: pollIntervalMinutes: 10 - ref: registry.redhat.io/redhat/redhat-operator-index:v4.20 + ref: registry.redhat.io/redhat/redhat-operator-index:v4.19 type: Image 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 d23d9e581..d838eda39 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/Makefile b/openshift/tests-extension/Makefile index a6b62bac6..05693ee40 100644 --- a/openshift/tests-extension/Makefile +++ b/openshift/tests-extension/Makefile @@ -84,7 +84,7 @@ pkg/bindata/catalog/catalog.go: $(shell find testdata/catalog -type f) # It prevents various FIPS compliance policies from being applied to this compilation. # Do not set globally. .PHONY: build -build: #HELP Build the extended tests binary +build: bindata #HELP Build the extended tests binary @mkdir -p $(TOOLS_BIN_DIR) GO_COMPLIANCE_POLICY="exempt_all" go build -ldflags "$(LDFLAGS)" -mod=vendor -o $(TOOLS_BIN_DIR)/olmv1-tests-ext ./cmd/... @@ -125,7 +125,7 @@ update-metadata: #HELP Build and run 'update-metadata' to generate test metadata # This will regenerate the metadata without the test entry. #──────────────────────────────────────────────────────────────────── .PHONY: build-update -build-update: bindata build update-metadata #HELP Build and update metadata and sanitize output +build-update: build update-metadata #HELP Build and update metadata and sanitize output #SECTION Metadata 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)) + } + } +}