Skip to content

Commit dd9992b

Browse files
Promote Webhook FeatureGates to GA
1 parent 9eac616 commit dd9992b

File tree

9 files changed

+245
-229
lines changed

9 files changed

+245
-229
lines changed

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
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+
Webhook support is enabled by default. The controller uses the `WebhookProviderCertManager`
5+
feature gate unless you override it. To switch to the OpenShift Service CA provider,
6+
start the controller with `--feature-gates=WebhookProviderCertManager=false`.
67

78
OLMv1 currently does not support the installation of bundles containing webhooks. The webhook support feature enables this capability.
89
Webhooks, or more concretely Admission Webhooks, are part of Kuberntes' [Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
@@ -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

helm/experimental.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
# Use with {{- if has "FeatureGate" .Values.operatorControllerFeatures }}
77
# to pull in resources or additions
88
operatorControllerFeatures:
9-
- WebhookProviderCertManager
109
- SingleOwnNamespaceInstallSupport
1110
- PreflightPermissions
1211
- HelmChartSupport

helm/tilt.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ options:
1313
featureSet: experimental
1414

1515
operatorControllerFeatures:
16-
- WebhookProviderCertManager
1716
- SingleOwnNamespaceInstallSupport
1817
- PreflightPermissions
1918
- 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/e2e_suite_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1010
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
11+
"k8s.io/client-go/dynamic"
1112
"k8s.io/client-go/rest"
1213
ctrl "sigs.k8s.io/controller-runtime"
1314
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -18,8 +19,9 @@ import (
1819
)
1920

2021
var (
21-
cfg *rest.Config
22-
c client.Client
22+
cfg *rest.Config
23+
c client.Client
24+
dynamicClient dynamic.Interface
2325
)
2426

2527
const (
@@ -35,6 +37,9 @@ func TestMain(m *testing.M) {
3537
c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
3638
utilruntime.Must(err)
3739

40+
dynamicClient, err = dynamic.NewForConfig(cfg)
41+
utilruntime.Must(err)
42+
3843
res := m.Run()
3944
path := os.Getenv(testSummaryOutputEnvVar)
4045
if path == "" {

test/e2e/webhook_support_test.go

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

0 commit comments

Comments
 (0)