|  | 
|  | 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