Skip to content

🌱 Add basic webhook support e2e #2108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
216 changes: 214 additions & 2 deletions test/experimental-e2e/experimental_e2e_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
package experimental_e2e

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

"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"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

ocv1 "github.com/operator-framework/operator-controller/api/v1"
"github.com/operator-framework/operator-controller/internal/operator-controller/scheme"
"github.com/operator-framework/operator-controller/test/utils"
)

const (
artifactName = "operator-controller-experimental-e2e"
pollDuration = time.Minute
pollInterval = time.Second
)

var (
cfg *rest.Config
c client.Client
cfg *rest.Config
c client.Client
dynamicClient dynamic.Interface
)

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

dynamicClient, err = dynamic.NewForConfig(cfg)
utilruntime.Must(err)

os.Exit(m.Run())
}

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)

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

t.Log("By checking a valid CR is mutated by the mutating webhook")
obj = getWebhookOperatorResource("valid-test-cr", namespace.GetName(), true)
_, err = v1Client.Create(t.Context(), obj, metav1.CreateOptions{})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, dynamicClient.Resource(v1Gvr).Namespace(namespace.GetName()).Delete(context.Background(), obj.GetName(), metav1.DeleteOptions{}))
})
res, err := v1Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{})
require.NoError(t, err)
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())

res, err = v2Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{})
require.NoError(t, err)
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,
},
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: webhook-operator-metrics-reader
rules:
- nonResourceURLs:
- /metrics
verbs:
- get
Loading
Loading