Skip to content

Commit 70d7339

Browse files
Promote Webhook FeatureGates to GA
This PR promote for GA WebhookProviderOpenshiftServiceCA and WebhookProviderCertManager. For upstream WebhookProviderCertManager is used by default when WebhookProviderOpenshiftServiceCA is disabled by default.
1 parent 292c0db commit 70d7339

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
@@ -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/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)