Skip to content

Commit 87392e3

Browse files
Promote Webhook FeatureGates to GA
Promote to GA WebhookProviderOpenshiftServiceCA and WebhookProviderCertManager. For upstream WebhookProviderCertManager is used by default when WebhookProviderOpenshiftServiceCA is disabled by default.
1 parent 292c0db commit 87392e3

File tree

11 files changed

+257
-233
lines changed

11 files changed

+257
-233
lines changed

docs/draft/howto/enable-webhook-support.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
## Installation of Bundles containing Webhooks
22

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

7-
OLMv1 currently does not support the installation of bundles containing webhooks. The webhook support feature enables this capability.
8-
Webhooks, or more concretely Admission Webhooks, are part of Kuberntes' [Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
9+
Webhooks, or more concretely Admission Webhooks, are part of Kubernetes' [Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
910
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,
1011
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
1112
certificate provider. Currently, two certificate providers are supported: CertManager and Openshift-ServiceCA. The certificate provider to use given by the feature-gate:
@@ -15,14 +16,12 @@ certificate provider. Currently, two certificate providers are supported: CertMa
1516

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

18-
### Run OLM v1with Experimental Features Enabled
19+
### Run OLM v1 with Webhook Support
1920

20-
```terminal title=Enable Experimental Features in a New Kind Cluster
21-
make run-experimental
21+
```terminal title=Start the controller with webhook support
22+
make run
2223
```
2324

24-
This will enable only the `WebhookProviderCertManager` feature-gate, which works with cert-manager.
25-
2625
Then,
2726

2827
```terminal title=Wait for rollout to complete

docs/draft/tutorials/explore-available-content-metas-endpoint.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to
9292
```
9393
9494
!!! important
95-
Currently, OLM 1.0 does not support the installation of extensions that use webhooks or that target a single or specified set of namespaces.
95+
OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate.
9696
9797
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:
9898

docs/project/olmv1_limitations.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ hide:
88
Currently, OLM v1 only supports installing operators packaged in [OLM v0 bundles](https://olm.operatorframework.io/docs/tasks/creating-operator-bundle/)
99
, also known as `registry+v1` bundles. Additionally, the bundled operator, or cluster extension:
1010

11-
* **must** support installation via the `AllNamespaces` install mode.
12-
* **must not** use webhooks.
11+
* **must** support installation via the `AllNamespaces` install mode
1312
* **must not** declare dependencies using any of the following file-based catalog properties:
1413
* `olm.gvk.required`
1514
* `olm.package.required`

docs/tutorials/explore-available-content.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to
9292
```
9393
9494
!!! important
95-
Currently, OLM 1.0 does not support the installation of extensions that use webhooks or that target a single or specified set of namespaces.
95+
OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate.
9696
9797
3. Return list of packages that support `AllNamespaces` install mode and do not use webhooks:
9898

helm/experimental.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ options:
99
operatorController:
1010
features:
1111
enabled:
12-
- WebhookProviderCertManager
1312
- SingleOwnNamespaceInstallSupport
1413
- PreflightPermissions
1514
- HelmChartSupport

helm/tilt.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ options:
1414
operatorController:
1515
features:
1616
enabled:
17-
- WebhookProviderCertManager
1817
- SingleOwnNamespaceInstallSupport
1918
- PreflightPermissions
2019
- HelmChartSupport

internal/operator-controller/features/features.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
5151
// mutating, and/or conversion webhooks with CertManager
5252
// as the certificate provider.
5353
WebhookProviderCertManager: {
54-
Default: false,
55-
PreRelease: featuregate.Alpha,
54+
Default: true,
55+
PreRelease: featuregate.GA,
5656
LockToDefault: false,
5757
},
5858

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

manifests/experimental-e2e.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2183,7 +2183,6 @@ spec:
21832183
- --health-probe-bind-address=:8081
21842184
- --metrics-bind-address=:8443
21852185
- --leader-elect
2186-
- --feature-gates=WebhookProviderCertManager=true
21872186
- --feature-gates=SingleOwnNamespaceInstallSupport=true
21882187
- --feature-gates=PreflightPermissions=true
21892188
- --feature-gates=HelmChartSupport=true

manifests/experimental.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2096,7 +2096,6 @@ spec:
20962096
- --health-probe-bind-address=:8081
20972097
- --metrics-bind-address=:8443
20982098
- --leader-elect
2099-
- --feature-gates=WebhookProviderCertManager=true
21002099
- --feature-gates=SingleOwnNamespaceInstallSupport=true
21012100
- --feature-gates=PreflightPermissions=true
21022101
- --feature-gates=HelmChartSupport=true

test/e2e/webhook_support_test.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
appsv1 "k8s.io/api/apps/v1"
12+
corev1 "k8s.io/api/core/v1"
13+
rbacv1 "k8s.io/api/rbac/v1"
14+
apimeta "k8s.io/apimachinery/pkg/api/meta"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
"k8s.io/apimachinery/pkg/runtime/schema"
18+
"k8s.io/apimachinery/pkg/types"
19+
"k8s.io/client-go/dynamic"
20+
"k8s.io/utils/ptr"
21+
22+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
23+
utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils"
24+
)
25+
26+
var dynamicClient dynamic.Interface
27+
28+
func TestNoop(t *testing.T) {
29+
t.Log("Running experimental-e2e tests")
30+
defer utils.CollectTestArtifacts(t, artifactName, c, cfg)
31+
}
32+
33+
func TestWebhookSupport(t *testing.T) {
34+
t.Log("Test support for bundles with webhooks")
35+
defer utils.CollectTestArtifacts(t, artifactName, c, cfg)
36+
37+
if dynamicClient == nil {
38+
var err error
39+
dynamicClient, err = dynamic.NewForConfig(cfg)
40+
require.NoError(t, err)
41+
}
42+
43+
t.Log("By creating install namespace, and necessary rbac resources")
44+
namespace := corev1.Namespace{
45+
ObjectMeta: metav1.ObjectMeta{
46+
Name: "webhook-operator",
47+
},
48+
}
49+
require.NoError(t, c.Create(t.Context(), &namespace))
50+
t.Cleanup(func() {
51+
require.NoError(t, c.Delete(context.Background(), &namespace))
52+
})
53+
54+
serviceAccount := corev1.ServiceAccount{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Name: "webhook-operator-installer",
57+
Namespace: namespace.GetName(),
58+
},
59+
}
60+
require.NoError(t, c.Create(t.Context(), &serviceAccount))
61+
t.Cleanup(func() {
62+
require.NoError(t, c.Delete(context.Background(), &serviceAccount))
63+
})
64+
65+
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
66+
ObjectMeta: metav1.ObjectMeta{
67+
Name: "webhook-operator-installer",
68+
},
69+
Subjects: []rbacv1.Subject{
70+
{
71+
Kind: "ServiceAccount",
72+
APIGroup: corev1.GroupName,
73+
Name: serviceAccount.GetName(),
74+
Namespace: serviceAccount.GetNamespace(),
75+
},
76+
},
77+
RoleRef: rbacv1.RoleRef{
78+
APIGroup: rbacv1.GroupName,
79+
Kind: "ClusterRole",
80+
Name: "cluster-admin",
81+
},
82+
}
83+
require.NoError(t, c.Create(t.Context(), clusterRoleBinding))
84+
t.Cleanup(func() {
85+
require.NoError(t, c.Delete(context.Background(), clusterRoleBinding))
86+
})
87+
88+
t.Log("By creating the webhook-operator ClusterCatalog")
89+
extensionCatalog := &ocv1.ClusterCatalog{
90+
ObjectMeta: metav1.ObjectMeta{
91+
Name: "webhook-operator-catalog",
92+
},
93+
Spec: ocv1.ClusterCatalogSpec{
94+
Source: ocv1.CatalogSource{
95+
Type: ocv1.SourceTypeImage,
96+
Image: &ocv1.ImageSource{
97+
Ref: fmt.Sprintf("%s/e2e/test-catalog:v1", os.Getenv("CLUSTER_REGISTRY_HOST")),
98+
PollIntervalMinutes: ptr.To(1),
99+
},
100+
},
101+
},
102+
}
103+
require.NoError(t, c.Create(t.Context(), extensionCatalog))
104+
t.Cleanup(func() {
105+
require.NoError(t, c.Delete(context.Background(), extensionCatalog))
106+
})
107+
108+
t.Log("By waiting for the catalog to serve its metadata")
109+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
110+
require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog))
111+
cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing)
112+
require.NotNil(ct, cond)
113+
require.Equal(ct, metav1.ConditionTrue, cond.Status)
114+
require.Equal(ct, ocv1.ReasonAvailable, cond.Reason)
115+
}, pollDuration, pollInterval)
116+
117+
t.Log("By installing the webhook-operator ClusterExtension")
118+
clusterExtension := &ocv1.ClusterExtension{
119+
ObjectMeta: metav1.ObjectMeta{
120+
Name: "webhook-operator-extension",
121+
},
122+
Spec: ocv1.ClusterExtensionSpec{
123+
Source: ocv1.SourceConfig{
124+
SourceType: "Catalog",
125+
Catalog: &ocv1.CatalogFilter{
126+
PackageName: "webhook-operator",
127+
Selector: &metav1.LabelSelector{
128+
MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name},
129+
},
130+
},
131+
},
132+
Namespace: namespace.GetName(),
133+
ServiceAccount: ocv1.ServiceAccountReference{
134+
Name: serviceAccount.GetName(),
135+
},
136+
},
137+
}
138+
require.NoError(t, c.Create(t.Context(), clusterExtension))
139+
t.Cleanup(func() {
140+
require.NoError(t, c.Delete(context.Background(), clusterExtension))
141+
})
142+
143+
t.Log("By waiting for webhook-operator extension to be installed successfully")
144+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
145+
require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
146+
cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
147+
require.NotNil(ct, cond)
148+
require.Equal(ct, metav1.ConditionTrue, cond.Status)
149+
require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
150+
require.Contains(ct, cond.Message, "Installed bundle")
151+
require.NotNil(ct, clusterExtension.Status.Install)
152+
require.NotEmpty(ct, clusterExtension.Status.Install.Bundle)
153+
}, pollDuration, pollInterval)
154+
155+
t.Log("By waiting for webhook-operator deployment to be available")
156+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
157+
deployment := &appsv1.Deployment{}
158+
require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "webhook-operator-controller-manager"}, deployment))
159+
available := false
160+
for _, cond := range deployment.Status.Conditions {
161+
if cond.Type == appsv1.DeploymentAvailable {
162+
available = cond.Status == corev1.ConditionTrue
163+
}
164+
}
165+
require.True(ct, available)
166+
}, pollDuration, pollInterval)
167+
168+
v1Gvr := schema.GroupVersionResource{
169+
Group: "webhook.operators.coreos.io",
170+
Version: "v1",
171+
Resource: "webhooktests",
172+
}
173+
v1Client := dynamicClient.Resource(v1Gvr).Namespace(namespace.GetName())
174+
175+
t.Log("By eventually seeing that invalid CR creation is rejected by the validating webhook")
176+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
177+
obj := getWebhookOperatorResource("invalid-test-cr", namespace.GetName(), false)
178+
_, err := v1Client.Create(t.Context(), obj, metav1.CreateOptions{})
179+
require.Error(ct, err)
180+
require.Contains(ct, err.Error(), "Invalid value: false: Spec.Valid must be true")
181+
}, pollDuration, pollInterval)
182+
183+
var (
184+
res *unstructured.Unstructured
185+
err error
186+
obj = getWebhookOperatorResource("valid-test-cr", namespace.GetName(), true)
187+
)
188+
189+
t.Log("By eventually creating a valid CR")
190+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
191+
res, err = v1Client.Create(t.Context(), obj, metav1.CreateOptions{})
192+
require.NoError(ct, err)
193+
}, pollDuration, pollInterval)
194+
t.Cleanup(func() {
195+
require.NoError(t, v1Client.Delete(context.Background(), obj.GetName(), metav1.DeleteOptions{}))
196+
})
197+
198+
require.Equal(t, map[string]interface{}{
199+
"valid": true,
200+
"mutate": true,
201+
}, res.Object["spec"])
202+
203+
t.Log("By checking a valid CR is converted to v2 by the conversion webhook")
204+
v2Gvr := schema.GroupVersionResource{
205+
Group: "webhook.operators.coreos.io",
206+
Version: "v2",
207+
Resource: "webhooktests",
208+
}
209+
v2Client := dynamicClient.Resource(v2Gvr).Namespace(namespace.GetName())
210+
211+
t.Log("By eventually getting the valid CR with a v2 client")
212+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
213+
res, err = v2Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{})
214+
require.NoError(ct, err)
215+
}, pollDuration, pollInterval)
216+
217+
t.Log("and verifying that the CR is correctly converted")
218+
require.Equal(t, map[string]interface{}{
219+
"conversion": map[string]interface{}{
220+
"valid": true,
221+
"mutate": true,
222+
},
223+
}, res.Object["spec"])
224+
}
225+
226+
func getWebhookOperatorResource(name string, namespace string, valid bool) *unstructured.Unstructured {
227+
return &unstructured.Unstructured{
228+
Object: map[string]interface{}{
229+
"apiVersion": "webhook.operators.coreos.io/v1",
230+
"kind": "webhooktests",
231+
"metadata": map[string]interface{}{
232+
"name": name,
233+
"namespace": namespace,
234+
},
235+
"spec": map[string]interface{}{
236+
"valid": valid,
237+
},
238+
},
239+
}
240+
}

0 commit comments

Comments
 (0)