Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions docs/draft/howto/enable-webhook-support.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
## Installation of Bundles containing Webhooks

!!! note
This feature is still in *alpha*. Either the `WebhookProviderCertManager`, or the `WebhookProviderOpenshiftServiceCA`, feature-gate
must be enabled to make use of it. See the instructions below on how to enable the feature-gate.
OLMv1 supports the installation of bundles containing webhooks by default.
The controller uses the `WebhookProviderCertManager`
feature-gate unless you override it. To switch to the OpenShift Service CA provider,
start the controller with `--feature-gates=WebhookProviderCertManager=false` and enable `--feature-gates=WebhookProviderOpenshiftServiceCA=true`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
OLMv1 supports the installation of bundles containing webhooks by default.
The controller uses the `WebhookProviderCertManager`
feature-gate unless you override it. To switch to the OpenShift Service CA provider,
start the controller with `--feature-gates=WebhookProviderCertManager=false` and enable `--feature-gates=WebhookProviderOpenshiftServiceCA=true`.
OLM v1 supports installing bundles that use admission webhooks.
By default, OLM v1 uses the community Cert Manager package for admission webhook validation. To use the OpenShift Service CA provider, set the `--feature-gates=WebhookProviderOpenshiftServiceCA=true` flag at startup.

Copy link
Contributor Author

@camilamacedo86 camilamacedo86 Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have more than one kind of webhook that is supported.
certManager is used only for the admission webhook
So, I think it would not be very accurate.

The other suggestion was accepted :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, isn't that what I wrote?
"By default, OLM v1 uses the community Cert Manager package for admission webhook validation."

(I'm totally fine if you don't take the suggestion, but I thought we are both saying the same thing?)

Copy link
Contributor Author

@camilamacedo86 camilamacedo86 Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to have "OLMv1 supports the installation of bundles containing webhooks by default."
(because we support more than admission webhooks)
Then, below, we explain that for the specific case, admission, certmanager is used.

So, I updated it with a mix of your suggestions.


OLMv1 currently does not support the installation of bundles containing webhooks. The webhook support feature enables this capability.
Webhooks, or more concretely Admission Webhooks, are part of Kuberntes' [Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
Webhooks, or more concretely Admission Webhooks, are part of Kubernetes' [Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
feature. Webhooks run as services called by the kube-apiservice in due course of processing a resource related request. They can be used to validate resources, ensure reasonable default values,
are set, or aid in the migration to new CustomResourceDefinition schema. The communication with the webhook service is secured by TLS. In OLMv1, the TLS certificate is managed by a
certificate provider. Currently, two certificate providers are supported: CertManager and Openshift-ServiceCA. The certificate provider to use given by the feature-gate:
Expand All @@ -15,14 +16,12 @@ certificate provider. Currently, two certificate providers are supported: CertMa

As CertManager is already installed with OLMv1, we suggest using `WebhookProviderCertManager`.

### Run OLM v1with Experimental Features Enabled
### Run OLM v1 with Webhook Support

```terminal title=Enable Experimental Features in a New Kind Cluster
make run-experimental
```terminal title=Start the controller with webhook support
make run
```

This will enable only the `WebhookProviderCertManager` feature-gate, which works with cert-manager.

Then,

```terminal title=Wait for rollout to complete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to
```

!!! important
Currently, OLM 1.0 does not support the installation of extensions that use webhooks or that target a single or specified set of namespaces.
OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate.

3. Return list of packages which support `AllNamespaces` install mode, do not use webhooks, and where the channel head version uses `olm.csv.metadata` format:

Expand Down
3 changes: 1 addition & 2 deletions docs/project/olmv1_limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ hide:
Currently, OLM v1 only supports installing operators packaged in [OLM v0 bundles](https://olm.operatorframework.io/docs/tasks/creating-operator-bundle/)
, also known as `registry+v1` bundles. Additionally, the bundled operator, or cluster extension:

* **must** support installation via the `AllNamespaces` install mode.
* **must not** use webhooks.
* **must** support installation via the `AllNamespaces` install mode
* **must not** declare dependencies using any of the following file-based catalog properties:
* `olm.gvk.required`
* `olm.package.required`
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/explore-available-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to
```

!!! important
Currently, OLM 1.0 does not support the installation of extensions that use webhooks or that target a single or specified set of namespaces.
OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate.

3. Return list of packages that support `AllNamespaces` install mode and do not use webhooks:

Expand Down
1 change: 0 additions & 1 deletion helm/experimental.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ options:
operatorController:
features:
enabled:
- WebhookProviderCertManager
- SingleOwnNamespaceInstallSupport
- PreflightPermissions
- HelmChartSupport
Expand Down
1 change: 0 additions & 1 deletion helm/tilt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ options:
operatorController:
features:
enabled:
- WebhookProviderCertManager
- SingleOwnNamespaceInstallSupport
- PreflightPermissions
- HelmChartSupport
Expand Down
8 changes: 4 additions & 4 deletions internal/operator-controller/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
// mutating, and/or conversion webhooks with CertManager
// as the certificate provider.
WebhookProviderCertManager: {
Default: false,
PreRelease: featuregate.Alpha,
Default: true,
PreRelease: featuregate.GA,
LockToDefault: false,
},

Expand All @@ -61,8 +61,8 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
// mutating, and/or conversion webhooks with Openshift Service CA
// as the certificate provider.
WebhookProviderOpenshiftServiceCA: {
Default: false,
PreRelease: featuregate.Alpha,
Default: true,
PreRelease: featuregate.GA,
LockToDefault: false,
},

Expand Down
1 change: 0 additions & 1 deletion manifests/experimental-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2183,7 +2183,6 @@ spec:
- --health-probe-bind-address=:8081
- --metrics-bind-address=:8443
- --leader-elect
- --feature-gates=WebhookProviderCertManager=true
- --feature-gates=SingleOwnNamespaceInstallSupport=true
- --feature-gates=PreflightPermissions=true
- --feature-gates=HelmChartSupport=true
Expand Down
1 change: 0 additions & 1 deletion manifests/experimental.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2096,7 +2096,6 @@ spec:
- --health-probe-bind-address=:8081
- --metrics-bind-address=:8443
- --leader-elect
- --feature-gates=WebhookProviderCertManager=true
- --feature-gates=SingleOwnNamespaceInstallSupport=true
- --feature-gates=PreflightPermissions=true
- --feature-gates=HelmChartSupport=true
Expand Down
240 changes: 240 additions & 0 deletions test/e2e/webhook_support_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package e2e

import (
"context"
"fmt"
"os"
"testing"

"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"
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"
"k8s.io/client-go/dynamic"
"k8s.io/utils/ptr"

ocv1 "github.com/operator-framework/operator-controller/api/v1"
utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils"
)

var dynamicClient dynamic.Interface

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)

if dynamicClient == nil {
var err error
dynamicClient, err = dynamic.NewForConfig(cfg)
require.NoError(t, err)
}

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("CLUSTER_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) {
require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog))
cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing)
require.NotNil(ct, cond)
require.Equal(ct, metav1.ConditionTrue, cond.Status)
require.Equal(ct, 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) {
require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
require.NotNil(ct, cond)
require.Equal(ct, metav1.ConditionTrue, cond.Status)
require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
require.Contains(ct, cond.Message, "Installed bundle")
require.NotNil(ct, clusterExtension.Status.Install)
require.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{}
require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "webhook-operator-controller-manager"}, deployment))
available := false
for _, cond := range deployment.Status.Conditions {
if cond.Type == appsv1.DeploymentAvailable {
available = cond.Status == corev1.ConditionTrue
}
}
require.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 eventually seeing that invalid CR creation is rejected by the validating webhook")
require.EventuallyWithT(t, func(ct *assert.CollectT) {
obj := getWebhookOperatorResource("invalid-test-cr", namespace.GetName(), false)
_, err := v1Client.Create(t.Context(), obj, metav1.CreateOptions{})
require.Error(ct, err)
require.Contains(ct, err.Error(), "Invalid value: false: Spec.Valid must be true")
}, pollDuration, pollInterval)

var (
res *unstructured.Unstructured
err error
obj = getWebhookOperatorResource("valid-test-cr", namespace.GetName(), true)
)

t.Log("By eventually creating a valid CR")
require.EventuallyWithT(t, func(ct *assert.CollectT) {
res, err = v1Client.Create(t.Context(), obj, metav1.CreateOptions{})
require.NoError(ct, err)
}, pollDuration, pollInterval)
t.Cleanup(func() {
require.NoError(t, v1Client.Delete(context.Background(), obj.GetName(), metav1.DeleteOptions{}))
})

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())

t.Log("By eventually getting the valid CR with a v2 client")
require.EventuallyWithT(t, func(ct *assert.CollectT) {
res, err = v2Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{})
require.NoError(ct, err)
}, pollDuration, pollInterval)

t.Log("and verifying that the CR is correctly converted")
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,
},
},
}
}
Loading
Loading