From c89f194932e075609ead34254ae4ba1cd9e71f61 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Thu, 17 Jul 2025 15:35:15 +0200 Subject: [PATCH 1/2] Add webhook-operator bundle and catalog entry Signed-off-by: Per Goncalves da Silva --- ...horization.k8s.io_v1beta1_clusterrole.yaml | 10 + ...ebhook-operator.clusterserviceversion.yaml | 234 ++++++++++++++++++ ...hook.operators.coreos.io_webhooktests.yaml | 105 ++++++++ .../v0.0.1/metadata/annotations.yaml | 12 + .../test-catalog/v1/configs/catalog.yaml | 20 ++ 5 files changed, 381 insertions(+) create mode 100644 testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator-metrics-reader_rbac.authorization.k8s.io_v1beta1_clusterrole.yaml create mode 100644 testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator.clusterserviceversion.yaml create mode 100644 testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook.operators.coreos.io_webhooktests.yaml create mode 100644 testdata/images/bundles/webhook-operator/v0.0.1/metadata/annotations.yaml diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator-metrics-reader_rbac.authorization.k8s.io_v1beta1_clusterrole.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator-metrics-reader_rbac.authorization.k8s.io_v1beta1_clusterrole.yaml new file mode 100644 index 000000000..20f88a159 --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator-metrics-reader_rbac.authorization.k8s.io_v1beta1_clusterrole.yaml @@ -0,0 +1,10 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: webhook-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator.clusterserviceversion.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator.clusterserviceversion.yaml new file mode 100644 index 000000000..0b8976f92 --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator.clusterserviceversion.yaml @@ -0,0 +1,234 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "webhook.operators.coreos.io/v1", + "kind": "WebhookTest", + "metadata": { + "name": "webhooktest-sample", + "namespace": "webhook-operator-system" + }, + "spec": { + "valid": true + } + } + ] + capabilities: Basic Install + operators.operatorframework.io/builder: operator-sdk-v1.0.0 + operators.operatorframework.io/project_layout: go + name: webhook-operator.v0.0.1 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - kind: WebhookTest + name: webhooktests.webhook.operators.coreos.io + version: v1 + description: Webhook Operator description. TODO. + displayName: Webhook Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - webhook.operators.coreos.io + resources: + - webhooktests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - webhook.operators.coreos.io + resources: + - webhooktests/status + verbs: + - get + - patch + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: default + deployments: + - name: webhook-operator-webhook + spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + strategy: {} + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + resources: {} + - args: + - --metrics-addr=127.0.0.1:8080 + - --enable-leader-election + command: + - /manager + image: quay.io/olmtest/webhook-operator:0.0.3 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create + serviceAccountName: default + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - webhook-operator + links: + - name: Webhook Operator + url: https://webhook-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.1 + webhookdefinitions: + - admissionReviewVersions: + - v1beta1 + - v1 + containerPort: 443 + targetPort: 4343 + deploymentName: webhook-operator-webhook + failurePolicy: Fail + generateName: vwebhooktest.kb.io + rules: + - apiGroups: + - webhook.operators.coreos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - webhooktests + sideEffects: None + type: ValidatingAdmissionWebhook + webhookPath: /validate-webhook-operators-coreos-io-v1-webhooktest + - admissionReviewVersions: + - v1beta1 + - v1 + containerPort: 443 + targetPort: 4343 + deploymentName: webhook-operator-webhook + failurePolicy: Fail + generateName: mwebhooktest.kb.io + rules: + - apiGroups: + - webhook.operators.coreos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - webhooktests + sideEffects: None + type: MutatingAdmissionWebhook + webhookPath: /mutate-webhook-operators-coreos-io-v1-webhooktest + - admissionReviewVersions: + - v1beta1 + - v1 + containerPort: 443 + targetPort: 4343 + deploymentName: webhook-operator-webhook + failurePolicy: Fail + generateName: cwebhooktest.kb.io + rules: + - apiGroups: + - webhook.operators.coreos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - webhooktests + sideEffects: None + type: ConversionWebhook + webhookPath: /convert + conversionCRDs: + - webhooktests.webhook.operators.coreos.io diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook.operators.coreos.io_webhooktests.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook.operators.coreos.io_webhooktests.yaml new file mode 100644 index 000000000..9c5262039 --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook.operators.coreos.io_webhooktests.yaml @@ -0,0 +1,105 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.3.0 + creationTimestamp: null + name: webhooktests.webhook.operators.coreos.io +spec: + preserveUnknownFields: false + group: webhook.operators.coreos.io + names: + kind: WebhookTest + listKind: WebhookTestList + plural: webhooktests + singular: webhooktest + scope: Namespaced + version: v1 + versions: + - name: v1 + schema: + openAPIV3Schema: + description: WebhookTest is the Schema for the webhooktests API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: WebhookTestSpec defines the desired state of WebhookTest + properties: + mutate: + description: Mutate is a field that will be set to true by the mutating + webhook. + type: boolean + valid: + description: Valid must be set to true or the validation webhook will + reject the resource. + type: boolean + required: + - valid + type: object + status: + description: WebhookTestStatus defines the observed state of WebhookTest + type: object + type: object + served: true + storage: true + - name: v2 + schema: + openAPIV3Schema: + description: WebhookTest is the Schema for the webhooktests API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: WebhookTestSpec defines the desired state of WebhookTest + properties: + conversion: + description: Conversion is an example field of WebhookTest. Edit WebhookTest_types.go + to remove/update + properties: + mutate: + description: Mutate is a field that will be set to true by the + mutating webhook. + type: boolean + valid: + description: Valid must be set to true or the validation webhook + will reject the resource. + type: boolean + required: + - valid + type: object + required: + - conversion + type: object + status: + description: WebhookTestStatus defines the observed state of WebhookTest + type: object + type: object + served: true + storage: false +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/metadata/annotations.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/metadata/annotations.yaml new file mode 100644 index 000000000..e59521886 --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/metadata/annotations.yaml @@ -0,0 +1,12 @@ +annotations: + operators.operatorframework.io.bundle.channel.default.v1: "" + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: webhook-operator + operators.operatorframework.io.metrics.builder: operator-sdk-v1.0.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: go + operators.operatorframework.io.test.config.v1: tests/scorecard/ + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 diff --git a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml index 576870595..69553dbcc 100644 --- a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml +++ b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml @@ -89,3 +89,23 @@ properties: value: packageName: dynamic version: 1.2.0 +--- +schema: olm.package +name: webhook-operator +defaultChannel: alpha +--- +schema: olm.channel +name: alpha +package: webhook-operator +entries: + - name: webhook-operator.v0.0.1 +--- +schema: olm.bundle +name: webhook-operator.v0.0.1 +package: webhook-operator +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/webhook-operator:v0.0.1 +properties: + - type: olm.package + value: + packageName: webhook-operator + version: 0.0.1 From b9cb9c4e55ec42d473600fd3711720dfe9ce633c Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Thu, 17 Jul 2025 09:50:50 +0200 Subject: [PATCH 2/2] Add basic webhook support e2e Signed-off-by: Per Goncalves da Silva --- .../experimental-e2e/experimental_e2e_test.go | 216 +++++++++++++++++- 1 file changed, 214 insertions(+), 2 deletions(-) diff --git a/test/experimental-e2e/experimental_e2e_test.go b/test/experimental-e2e/experimental_e2e_test.go index bc8c5ab69..f3832bd70 100644 --- a/test/experimental-e2e/experimental_e2e_test.go +++ b/test/experimental-e2e/experimental_e2e_test.go @@ -1,26 +1,45 @@ package experimental_e2e import ( + "context" + "fmt" "os" "testing" + "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + 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/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" "github.com/operator-framework/operator-controller/test/utils" ) const ( artifactName = "operator-controller-experimental-e2e" + pollDuration = time.Minute + pollInterval = time.Second ) var ( - cfg *rest.Config - c client.Client + cfg *rest.Config + c client.Client + dynamicClient dynamic.Interface ) func TestMain(m *testing.M) { @@ -31,6 +50,9 @@ func TestMain(m *testing.M) { c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) utilruntime.Must(err) + dynamicClient, err = dynamic.NewForConfig(cfg) + utilruntime.Must(err) + os.Exit(m.Run()) } @@ -38,3 +60,193 @@ func TestNoop(t *testing.T) { t.Log("Running experimental-e2e tests") defer utils.CollectTestArtifacts(t, artifactName, c, cfg) } + +func TestWebhookSupport(t *testing.T) { + t.Log("Test support for bundles with webhooks") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + t.Log("By creating install namespace, and necessary rbac resources") + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator", + }, + } + require.NoError(t, c.Create(t.Context(), &namespace)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &namespace)) + }) + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-installer", + Namespace: namespace.GetName(), + }, + } + require.NoError(t, c.Create(t.Context(), &serviceAccount)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &serviceAccount)) + }) + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-installer", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: corev1.GroupName, + Name: serviceAccount.GetName(), + Namespace: serviceAccount.GetNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + } + require.NoError(t, c.Create(t.Context(), clusterRoleBinding)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterRoleBinding)) + }) + + t.Log("By creating the webhook-operator ClusterCatalog") + extensionCatalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-catalog", + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: fmt.Sprintf("%s/e2e/test-catalog:v1", os.Getenv("LOCAL_REGISTRY_HOST")), + PollIntervalMinutes: ptr.To(1), + }, + }, + }, + } + require.NoError(t, c.Create(t.Context(), extensionCatalog)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), extensionCatalog)) + }) + + t.Log("By waiting for the catalog to serve its metadata") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + assert.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog)) + cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing) + assert.NotNil(t, cond) + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, ocv1.ReasonAvailable, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("By installing the webhook-operator ClusterExtension") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-extension", + }, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "webhook-operator", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, + }, + }, + }, + Namespace: namespace.GetName(), + ServiceAccount: ocv1.ServiceAccountReference{ + Name: serviceAccount.GetName(), + }, + }, + } + require.NoError(t, c.Create(t.Context(), clusterExtension)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterExtension)) + }) + + t.Log("By waiting for webhook-operator extension to be installed successfully") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + assert.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + if assert.NotNil(ct, cond) { + assert.Equal(ct, metav1.ConditionTrue, cond.Status) + assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + assert.Contains(ct, cond.Message, "Installed bundle") + assert.NotEmpty(ct, clusterExtension.Status.Install.Bundle) + } + }, pollDuration, pollInterval) + + t.Log("By waiting for webhook-operator deployment to be available") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + deployment := &appsv1.Deployment{} + assert.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "webhook-operator-webhook"}, deployment)) + available := false + for _, cond := range deployment.Status.Conditions { + if cond.Type == appsv1.DeploymentAvailable { + available = cond.Status == corev1.ConditionTrue + } + } + assert.True(ct, available) + }, pollDuration, pollInterval) + + v1Gvr := schema.GroupVersionResource{ + Group: "webhook.operators.coreos.io", + Version: "v1", + Resource: "webhooktests", + } + v1Client := dynamicClient.Resource(v1Gvr).Namespace(namespace.GetName()) + + t.Log("By checking an invalid CR is rejected by the validating webhook") + obj := getWebhookOperatorResource("invalid-test-cr", namespace.GetName(), false) + _, err := v1Client.Create(t.Context(), obj, metav1.CreateOptions{}) + require.Error(t, err) + require.Contains(t, err.Error(), "Invalid value: false: Spec.Valid must be true") + + t.Log("By checking a valid CR is mutated by the mutating webhook") + obj = getWebhookOperatorResource("valid-test-cr", namespace.GetName(), true) + _, err = v1Client.Create(t.Context(), obj, metav1.CreateOptions{}) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, dynamicClient.Resource(v1Gvr).Namespace(namespace.GetName()).Delete(context.Background(), obj.GetName(), metav1.DeleteOptions{})) + }) + res, err := v1Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, map[string]interface{}{ + "valid": true, + "mutate": true, + }, res.Object["spec"]) + + t.Log("By checking a valid CR is converted to v2 by the conversion webhook") + v2Gvr := schema.GroupVersionResource{ + Group: "webhook.operators.coreos.io", + Version: "v2", + Resource: "webhooktests", + } + v2Client := dynamicClient.Resource(v2Gvr).Namespace(namespace.GetName()) + + res, err = v2Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, map[string]interface{}{ + "conversion": map[string]interface{}{ + "valid": true, + "mutate": true, + }, + }, res.Object["spec"]) +} + +func getWebhookOperatorResource(name string, namespace string, valid bool) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "webhook.operators.coreos.io/v1", + "kind": "webhooktests", + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "valid": valid, + }, + }, + } +}