Skip to content

Commit 5786e67

Browse files
perdasilvaPer Goncalves da Silva
andauthored
🌱 Add basic webhook support e2e (#2108)
* Add webhook-operator bundle and catalog entry Signed-off-by: Per Goncalves da Silva <[email protected]> * Add basic webhook support e2e Signed-off-by: Per Goncalves da Silva <[email protected]> --------- Signed-off-by: Per Goncalves da Silva <[email protected]> Co-authored-by: Per Goncalves da Silva <[email protected]>
1 parent 5f5142d commit 5786e67

File tree

6 files changed

+595
-2
lines changed

6 files changed

+595
-2
lines changed

test/experimental-e2e/experimental_e2e_test.go

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,45 @@
11
package experimental_e2e
22

33
import (
4+
"context"
5+
"fmt"
46
"os"
57
"testing"
8+
"time"
69

10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
appsv1 "k8s.io/api/apps/v1"
13+
corev1 "k8s.io/api/core/v1"
14+
rbacv1 "k8s.io/api/rbac/v1"
715
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
16+
apimeta "k8s.io/apimachinery/pkg/api/meta"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
19+
"k8s.io/apimachinery/pkg/runtime/schema"
20+
"k8s.io/apimachinery/pkg/types"
821
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
22+
"k8s.io/client-go/dynamic"
923
"k8s.io/client-go/rest"
24+
"k8s.io/utils/ptr"
1025
ctrl "sigs.k8s.io/controller-runtime"
1126
"sigs.k8s.io/controller-runtime/pkg/client"
1227

28+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
1329
"github.com/operator-framework/operator-controller/internal/operator-controller/scheme"
1430
"github.com/operator-framework/operator-controller/test/utils"
1531
)
1632

1733
const (
1834
artifactName = "operator-controller-experimental-e2e"
35+
pollDuration = time.Minute
36+
pollInterval = time.Second
1937
)
2038

2139
var (
22-
cfg *rest.Config
23-
c client.Client
40+
cfg *rest.Config
41+
c client.Client
42+
dynamicClient dynamic.Interface
2443
)
2544

2645
func TestMain(m *testing.M) {
@@ -31,10 +50,203 @@ func TestMain(m *testing.M) {
3150
c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
3251
utilruntime.Must(err)
3352

53+
dynamicClient, err = dynamic.NewForConfig(cfg)
54+
utilruntime.Must(err)
55+
3456
os.Exit(m.Run())
3557
}
3658

3759
func TestNoop(t *testing.T) {
3860
t.Log("Running experimental-e2e tests")
3961
defer utils.CollectTestArtifacts(t, artifactName, c, cfg)
4062
}
63+
64+
func TestWebhookSupport(t *testing.T) {
65+
t.Log("Test support for bundles with webhooks")
66+
defer utils.CollectTestArtifacts(t, artifactName, c, cfg)
67+
68+
t.Log("By creating install namespace, and necessary rbac resources")
69+
namespace := corev1.Namespace{
70+
ObjectMeta: metav1.ObjectMeta{
71+
Name: "webhook-operator",
72+
},
73+
}
74+
require.NoError(t, c.Create(t.Context(), &namespace))
75+
t.Cleanup(func() {
76+
require.NoError(t, c.Delete(context.Background(), &namespace))
77+
})
78+
79+
serviceAccount := corev1.ServiceAccount{
80+
ObjectMeta: metav1.ObjectMeta{
81+
Name: "webhook-operator-installer",
82+
Namespace: namespace.GetName(),
83+
},
84+
}
85+
require.NoError(t, c.Create(t.Context(), &serviceAccount))
86+
t.Cleanup(func() {
87+
require.NoError(t, c.Delete(context.Background(), &serviceAccount))
88+
})
89+
90+
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
91+
ObjectMeta: metav1.ObjectMeta{
92+
Name: "webhook-operator-installer",
93+
},
94+
Subjects: []rbacv1.Subject{
95+
{
96+
Kind: "ServiceAccount",
97+
APIGroup: corev1.GroupName,
98+
Name: serviceAccount.GetName(),
99+
Namespace: serviceAccount.GetNamespace(),
100+
},
101+
},
102+
RoleRef: rbacv1.RoleRef{
103+
APIGroup: rbacv1.GroupName,
104+
Kind: "ClusterRole",
105+
Name: "cluster-admin",
106+
},
107+
}
108+
require.NoError(t, c.Create(t.Context(), clusterRoleBinding))
109+
t.Cleanup(func() {
110+
require.NoError(t, c.Delete(context.Background(), clusterRoleBinding))
111+
})
112+
113+
t.Log("By creating the webhook-operator ClusterCatalog")
114+
extensionCatalog := &ocv1.ClusterCatalog{
115+
ObjectMeta: metav1.ObjectMeta{
116+
Name: "webhook-operator-catalog",
117+
},
118+
Spec: ocv1.ClusterCatalogSpec{
119+
Source: ocv1.CatalogSource{
120+
Type: ocv1.SourceTypeImage,
121+
Image: &ocv1.ImageSource{
122+
Ref: fmt.Sprintf("%s/e2e/test-catalog:v1", os.Getenv("LOCAL_REGISTRY_HOST")),
123+
PollIntervalMinutes: ptr.To(1),
124+
},
125+
},
126+
},
127+
}
128+
require.NoError(t, c.Create(t.Context(), extensionCatalog))
129+
t.Cleanup(func() {
130+
require.NoError(t, c.Delete(context.Background(), extensionCatalog))
131+
})
132+
133+
t.Log("By waiting for the catalog to serve its metadata")
134+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
135+
assert.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog))
136+
cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing)
137+
assert.NotNil(t, cond)
138+
assert.Equal(t, metav1.ConditionTrue, cond.Status)
139+
assert.Equal(t, ocv1.ReasonAvailable, cond.Reason)
140+
}, pollDuration, pollInterval)
141+
142+
t.Log("By installing the webhook-operator ClusterExtension")
143+
clusterExtension := &ocv1.ClusterExtension{
144+
ObjectMeta: metav1.ObjectMeta{
145+
Name: "webhook-operator-extension",
146+
},
147+
Spec: ocv1.ClusterExtensionSpec{
148+
Source: ocv1.SourceConfig{
149+
SourceType: "Catalog",
150+
Catalog: &ocv1.CatalogFilter{
151+
PackageName: "webhook-operator",
152+
Selector: &metav1.LabelSelector{
153+
MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name},
154+
},
155+
},
156+
},
157+
Namespace: namespace.GetName(),
158+
ServiceAccount: ocv1.ServiceAccountReference{
159+
Name: serviceAccount.GetName(),
160+
},
161+
},
162+
}
163+
require.NoError(t, c.Create(t.Context(), clusterExtension))
164+
t.Cleanup(func() {
165+
require.NoError(t, c.Delete(context.Background(), clusterExtension))
166+
})
167+
168+
t.Log("By waiting for webhook-operator extension to be installed successfully")
169+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
170+
assert.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
171+
cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
172+
if assert.NotNil(ct, cond) {
173+
assert.Equal(ct, metav1.ConditionTrue, cond.Status)
174+
assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
175+
assert.Contains(ct, cond.Message, "Installed bundle")
176+
assert.NotEmpty(ct, clusterExtension.Status.Install.Bundle)
177+
}
178+
}, pollDuration, pollInterval)
179+
180+
t.Log("By waiting for webhook-operator deployment to be available")
181+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
182+
deployment := &appsv1.Deployment{}
183+
assert.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "webhook-operator-webhook"}, deployment))
184+
available := false
185+
for _, cond := range deployment.Status.Conditions {
186+
if cond.Type == appsv1.DeploymentAvailable {
187+
available = cond.Status == corev1.ConditionTrue
188+
}
189+
}
190+
assert.True(ct, available)
191+
}, pollDuration, pollInterval)
192+
193+
v1Gvr := schema.GroupVersionResource{
194+
Group: "webhook.operators.coreos.io",
195+
Version: "v1",
196+
Resource: "webhooktests",
197+
}
198+
v1Client := dynamicClient.Resource(v1Gvr).Namespace(namespace.GetName())
199+
200+
t.Log("By checking an invalid CR is rejected by the validating webhook")
201+
obj := getWebhookOperatorResource("invalid-test-cr", namespace.GetName(), false)
202+
_, err := v1Client.Create(t.Context(), obj, metav1.CreateOptions{})
203+
require.Error(t, err)
204+
require.Contains(t, err.Error(), "Invalid value: false: Spec.Valid must be true")
205+
206+
t.Log("By checking a valid CR is mutated by the mutating webhook")
207+
obj = getWebhookOperatorResource("valid-test-cr", namespace.GetName(), true)
208+
_, err = v1Client.Create(t.Context(), obj, metav1.CreateOptions{})
209+
require.NoError(t, err)
210+
t.Cleanup(func() {
211+
require.NoError(t, dynamicClient.Resource(v1Gvr).Namespace(namespace.GetName()).Delete(context.Background(), obj.GetName(), metav1.DeleteOptions{}))
212+
})
213+
res, err := v1Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{})
214+
require.NoError(t, err)
215+
require.Equal(t, map[string]interface{}{
216+
"valid": true,
217+
"mutate": true,
218+
}, res.Object["spec"])
219+
220+
t.Log("By checking a valid CR is converted to v2 by the conversion webhook")
221+
v2Gvr := schema.GroupVersionResource{
222+
Group: "webhook.operators.coreos.io",
223+
Version: "v2",
224+
Resource: "webhooktests",
225+
}
226+
v2Client := dynamicClient.Resource(v2Gvr).Namespace(namespace.GetName())
227+
228+
res, err = v2Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{})
229+
require.NoError(t, err)
230+
require.Equal(t, map[string]interface{}{
231+
"conversion": map[string]interface{}{
232+
"valid": true,
233+
"mutate": true,
234+
},
235+
}, res.Object["spec"])
236+
}
237+
238+
func getWebhookOperatorResource(name string, namespace string, valid bool) *unstructured.Unstructured {
239+
return &unstructured.Unstructured{
240+
Object: map[string]interface{}{
241+
"apiVersion": "webhook.operators.coreos.io/v1",
242+
"kind": "webhooktests",
243+
"metadata": map[string]interface{}{
244+
"name": name,
245+
"namespace": namespace,
246+
},
247+
"spec": map[string]interface{}{
248+
"valid": valid,
249+
},
250+
},
251+
}
252+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
kind: ClusterRole
3+
metadata:
4+
creationTimestamp: null
5+
name: webhook-operator-metrics-reader
6+
rules:
7+
- nonResourceURLs:
8+
- /metrics
9+
verbs:
10+
- get

0 commit comments

Comments
 (0)