Skip to content

Commit 1452c6e

Browse files
UPSTREAM: <carry>: [OTE] add webhook tests
Migrates OLMv1 webhook operator tests from using external YAML files to defining resources in Go structs. This change removes file dependencies, improving test reliability and simplifying test setup. The migration is a refactoring of code from openshift/origin#30059. The new code uses better naming conventions and adapts the tests to work with a controller-runtime client, enhancing test consistency and maintainability. The migration covers all core test scenarios: - Validating, mutating, and conversion webhooks. - Certificate and secret rotation tolerance. Assisted-by: Gemini
1 parent 778bd57 commit 1452c6e

File tree

5 files changed

+575
-54
lines changed

5 files changed

+575
-54
lines changed

openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,55 @@
4848
"source": "openshift:payload:olmv1",
4949
"lifecycle": "blocking",
5050
"environmentSelector": {}
51+
},
52+
{
53+
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working validating webhook",
54+
"labels": {},
55+
"resources": {
56+
"isolation": {}
57+
},
58+
"source": "openshift:payload:olmv1",
59+
"lifecycle": "blocking",
60+
"environmentSelector": {}
61+
},
62+
{
63+
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working mutating webhook",
64+
"labels": {},
65+
"resources": {
66+
"isolation": {}
67+
},
68+
"source": "openshift:payload:olmv1",
69+
"lifecycle": "blocking",
70+
"environmentSelector": {}
71+
},
72+
{
73+
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working conversion webhook",
74+
"labels": {},
75+
"resources": {
76+
"isolation": {}
77+
},
78+
"source": "openshift:payload:olmv1",
79+
"lifecycle": "blocking",
80+
"environmentSelector": {}
81+
},
82+
{
83+
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should be tolerant to openshift-service-ca certificate rotation",
84+
"labels": {},
85+
"resources": {
86+
"isolation": {}
87+
},
88+
"source": "openshift:payload:olmv1",
89+
"lifecycle": "blocking",
90+
"environmentSelector": {}
91+
},
92+
{
93+
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should be tolerant to tls secret deletion",
94+
"labels": {},
95+
"resources": {
96+
"isolation": {}
97+
},
98+
"source": "openshift:payload:olmv1",
99+
"lifecycle": "blocking",
100+
"environmentSelector": {}
51101
}
52102
]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package helpers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
//nolint:staticcheck // ST1001: dot-imports for readability
9+
. "github.com/onsi/gomega"
10+
11+
"k8s.io/apimachinery/pkg/api/meta"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
15+
olmv1 "github.com/operator-framework/operator-controller/api/v1"
16+
17+
"github/operator-framework-operator-controller/openshift/tests-extension/pkg/env"
18+
)
19+
20+
// NewClusterCatalog returns a new ClusterCatalog object.
21+
// It sets the image reference as source.
22+
func NewClusterCatalog(name, imageRef string) *olmv1.ClusterCatalog {
23+
return &olmv1.ClusterCatalog{
24+
ObjectMeta: metav1.ObjectMeta{
25+
Name: name,
26+
},
27+
Spec: olmv1.ClusterCatalogSpec{
28+
Source: olmv1.CatalogSource{
29+
Type: olmv1.SourceTypeImage,
30+
Image: &olmv1.ImageSource{
31+
Ref: imageRef,
32+
},
33+
},
34+
},
35+
}
36+
}
37+
38+
// ExpectCatalogToBeServing checks that the catalog with the given name is installed
39+
func ExpectCatalogToBeServing(ctx context.Context, name string) {
40+
k8sClient := env.Get().K8sClient
41+
Eventually(func(g Gomega) {
42+
var catalog olmv1.ClusterCatalog
43+
err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, &catalog)
44+
g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get catalog %q", name))
45+
46+
conditions := catalog.Status.Conditions
47+
g.Expect(conditions).NotTo(BeEmpty(), fmt.Sprintf("catalog %q has empty status.conditions", name))
48+
49+
g.Expect(meta.IsStatusConditionPresentAndEqual(conditions, olmv1.TypeServing, metav1.ConditionTrue)).
50+
To(BeTrue(), fmt.Sprintf("catalog %q is not serving", name))
51+
}).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed())
52+
}

openshift/tests-extension/pkg/helpers/cluster_extension.go

Lines changed: 144 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,195 @@ package helpers
22

33
import (
44
"context"
5+
"fmt"
6+
"time"
57

8+
//nolint:staticcheck // ST1001: dot-imports for readability
9+
. "github.com/onsi/ginkgo/v2"
610
//nolint:staticcheck // ST1001: dot-imports for readability
711
. "github.com/onsi/gomega"
812

913
corev1 "k8s.io/api/core/v1"
1014
rbacv1 "k8s.io/api/rbac/v1"
15+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
16+
"k8s.io/apimachinery/pkg/api/errors"
17+
"k8s.io/apimachinery/pkg/api/meta"
1118
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1219
"k8s.io/apimachinery/pkg/util/rand"
20+
"sigs.k8s.io/controller-runtime/pkg/client"
1321

14-
ocv1 "github.com/operator-framework/operator-controller/api/v1"
22+
olmv1 "github.com/operator-framework/operator-controller/api/v1"
1523

1624
"github/operator-framework-operator-controller/openshift/tests-extension/pkg/env"
1725
)
1826

19-
const openshiftOperatorsNs = "openshift-operators"
20-
2127
// CreateClusterExtension creates a ServiceAccount, ClusterRoleBinding, and ClusterExtension using typed APIs.
2228
// It returns the unique suffix and a cleanup function.
23-
func CreateClusterExtension(packageName, version string) (string, func()) {
29+
func CreateClusterExtension(packageName, version, namespace string) (string, func()) {
2430
ctx := context.TODO()
2531
k8sClient := env.Get().K8sClient
26-
unique := rand.String(8)
32+
unique := rand.String(4)
2733

2834
saName := "install-test-sa-" + unique
2935
crbName := "install-test-crb-" + unique
3036
ceName := "install-test-ce-" + unique
3137

3238
// 1. Create ServiceAccount
33-
sa := &corev1.ServiceAccount{
34-
ObjectMeta: metav1.ObjectMeta{
35-
Name: saName,
36-
Namespace: openshiftOperatorsNs,
37-
},
38-
}
39-
Expect(k8sClient.Create(ctx, sa)).To(Succeed(), "failed to create ServiceAccount")
39+
sa := NewServiceAccount(saName, namespace)
40+
Expect(k8sClient.Create(ctx, sa)).To(Succeed(),
41+
"failed to create ServiceAccount")
42+
By("ensuring ServiceAccount is available before proceeding")
43+
ExpectServiceAccountExists(ctx, saName, namespace)
4044

4145
// 2. Create ClusterRoleBinding
42-
crb := &rbacv1.ClusterRoleBinding{
46+
crb := NewClusterRoleBinding(crbName, "cluster-admin", saName, namespace)
47+
Expect(k8sClient.Create(ctx, crb)).To(Succeed(), "failed to create ClusterRoleBinding")
48+
By("ensuring ClusterRoleBinding is available before proceeding")
49+
ExpectClusterRoleBindingExists(ctx, crbName)
50+
51+
// 3. Create ClusterExtension
52+
ce := NewClusterExtensionObject(packageName, version, ceName, saName, namespace)
53+
Expect(k8sClient.Create(ctx, ce)).To(Succeed(), "failed to create ClusterExtension")
54+
55+
// Cleanup closure
56+
return ceName, func() {
57+
_ = k8sClient.Delete(ctx, ce)
58+
_ = k8sClient.Delete(ctx, crb)
59+
_ = k8sClient.Delete(ctx, sa)
60+
}
61+
}
62+
63+
// NewServiceAccount creates a new ServiceAccount.
64+
func NewServiceAccount(name, namespace string) *corev1.ServiceAccount {
65+
return &corev1.ServiceAccount{
4366
ObjectMeta: metav1.ObjectMeta{
44-
Name: crbName,
67+
Name: name,
68+
Namespace: namespace,
4569
},
70+
}
71+
}
72+
73+
// NewClusterRoleBinding creates a new ClusterRoleBinding object that binds a ClusterRole to a ServiceAccount.
74+
func NewClusterRoleBinding(name, roleName, saName, namespace string) *rbacv1.ClusterRoleBinding {
75+
return &rbacv1.ClusterRoleBinding{
76+
ObjectMeta: metav1.ObjectMeta{Name: name},
4677
RoleRef: rbacv1.RoleRef{
4778
APIGroup: "rbac.authorization.k8s.io",
4879
Kind: "ClusterRole",
49-
Name: "cluster-admin",
80+
Name: roleName,
5081
},
5182
Subjects: []rbacv1.Subject{{
5283
Kind: "ServiceAccount",
5384
Name: saName,
54-
Namespace: openshiftOperatorsNs,
85+
Namespace: namespace,
5586
}},
5687
}
57-
Expect(k8sClient.Create(ctx, crb)).To(Succeed(), "failed to create ClusterRoleBinding")
88+
}
5889

59-
// 3. Create ClusterExtension
60-
ce := &ocv1.ClusterExtension{
61-
ObjectMeta: metav1.ObjectMeta{
62-
Name: ceName,
63-
},
64-
Spec: ocv1.ClusterExtensionSpec{
65-
Namespace: openshiftOperatorsNs,
66-
ServiceAccount: ocv1.ServiceAccountReference{
90+
// NewClusterExtensionObject creates a new ClusterExtension object with the specified package, version, name, and ServiceAccount.
91+
func NewClusterExtensionObject(pkg, version, ceName, saName, namespace string) *olmv1.ClusterExtension {
92+
return &olmv1.ClusterExtension{
93+
ObjectMeta: metav1.ObjectMeta{Name: ceName},
94+
Spec: olmv1.ClusterExtensionSpec{
95+
Namespace: namespace,
96+
ServiceAccount: olmv1.ServiceAccountReference{
6797
Name: saName,
6898
},
69-
Source: ocv1.SourceConfig{
70-
SourceType: ocv1.SourceTypeCatalog,
71-
Catalog: &ocv1.CatalogFilter{
72-
PackageName: packageName,
99+
Source: olmv1.SourceConfig{
100+
SourceType: olmv1.SourceTypeCatalog,
101+
Catalog: &olmv1.CatalogFilter{
102+
PackageName: pkg,
73103
Version: version,
74104
Selector: &metav1.LabelSelector{},
75-
UpgradeConstraintPolicy: ocv1.UpgradeConstraintPolicyCatalogProvided,
105+
UpgradeConstraintPolicy: olmv1.UpgradeConstraintPolicyCatalogProvided,
76106
},
77107
},
78108
},
79109
}
80-
Expect(k8sClient.Create(ctx, ce)).To(Succeed(), "failed to create ClusterExtension")
110+
}
81111

82-
// Cleanup closure
83-
return ceName, func() {
84-
_ = k8sClient.Delete(ctx, ce)
85-
_ = k8sClient.Delete(ctx, crb)
86-
_ = k8sClient.Delete(ctx, sa)
112+
// ExpectClusterExtensionToBeInstalled checks that the ClusterExtension has both Progressing=True and Installed=True.
113+
func ExpectClusterExtensionToBeInstalled(ctx context.Context, name string) {
114+
k8sClient := env.Get().K8sClient
115+
Eventually(func(g Gomega) {
116+
var ext olmv1.ClusterExtension
117+
err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, &ext)
118+
g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get ClusterExtension %q", name))
119+
120+
conditions := ext.Status.Conditions
121+
g.Expect(conditions).NotTo(BeEmpty(), fmt.Sprintf("ClusterExtension %q has empty status.conditions", name))
122+
123+
progressing := meta.FindStatusCondition(conditions, string(olmv1.TypeProgressing))
124+
g.Expect(progressing).ToNot(BeNil(), "Progressing condition not found")
125+
g.Expect(progressing.Status).To(Equal(metav1.ConditionTrue), "Progressing should be True")
126+
127+
installed := meta.FindStatusCondition(conditions, string(olmv1.TypeInstalled))
128+
g.Expect(installed).ToNot(BeNil(), "Installed condition not found")
129+
g.Expect(installed.Status).To(Equal(metav1.ConditionTrue), "Installed should be True")
130+
}).WithTimeout(5 * time.Minute).WithPolling(1 * time.Second).Should(Succeed())
131+
}
132+
133+
// EnsureCleanupClusterExtension attempts to delete any ClusterExtension and a specified CRD
134+
// that might be left over from previous test runs. This helps prevent conflicts in serial tests.
135+
func EnsureCleanupClusterExtension(ctx context.Context, packageName, crdName string) {
136+
k8sClient := env.Get().K8sClient
137+
138+
// 1. Clean up any ClusterExtensions related to this test/package
139+
ceList := &olmv1.ClusterExtensionList{}
140+
// List all ClusterExtensions, then filter in code by packageName
141+
if err := k8sClient.List(ctx, ceList); err == nil {
142+
for _, ce := range ceList.Items {
143+
if ce.Spec.Source.Catalog.PackageName == packageName {
144+
By(fmt.Sprintf("deleting ClusterExtension %s (package: %s)", ce.Name, packageName))
145+
propagationPolicy := metav1.DeletePropagationForeground
146+
deleteOpts := &client.DeleteOptions{PropagationPolicy: &propagationPolicy}
147+
if err := k8sClient.Delete(ctx, &ce, deleteOpts); err != nil && !errors.IsNotFound(err) {
148+
fmt.Fprintf(GinkgoWriter, "Warning: Failed to delete remaning ClusterExtension %s: %v\n", ce.Name, err)
149+
}
150+
Eventually(func() bool {
151+
err := k8sClient.Get(ctx, client.ObjectKey{Name: ce.Name}, &olmv1.ClusterExtension{})
152+
return errors.IsNotFound(err)
153+
}).WithTimeout(1*time.Minute).WithPolling(2*time.Second).Should(BeTrue(), "Cleanup ClusterExtension %s failed to delete", ce.Name)
154+
}
155+
}
156+
} else if !errors.IsNotFound(err) {
157+
fmt.Fprintf(GinkgoWriter, "Warning: Failed to list ClusterExtensions during cleanup: %v\n", err)
158+
}
159+
160+
// 2. Clean up specific operator-created CRD if it exists
161+
if crdName != "" {
162+
crd := &apiextensionsv1.CustomResourceDefinition{}
163+
if err := k8sClient.Get(ctx, client.ObjectKey{Name: crdName}, crd); err == nil {
164+
By(fmt.Sprintf("deleting CRD %s", crdName))
165+
if err := k8sClient.Delete(ctx, crd); err != nil && !errors.IsNotFound(err) {
166+
fmt.Fprintf(GinkgoWriter, "Warning: Failed to delete lingering CRD %s: %v\n", crdName, err)
167+
}
168+
Eventually(func() bool {
169+
err := k8sClient.Get(ctx, client.ObjectKey{Name: crdName}, &apiextensionsv1.CustomResourceDefinition{})
170+
return errors.IsNotFound(err)
171+
}).WithTimeout(1*time.Minute).WithPolling(2*time.Second).Should(BeTrue(), "Lingering CRD %s failed to delete", crdName)
172+
} else if !errors.IsNotFound(err) {
173+
fmt.Fprintf(GinkgoWriter, "Warning: Failed to get CRD %s during cleanup: %v\n", crdName, err)
174+
}
87175
}
88176
}
177+
178+
// ExpectServiceAccountExists waits for a ServiceAccount to be available and visible to the client.
179+
func ExpectServiceAccountExists(ctx context.Context, name, namespace string) {
180+
k8sClient := env.Get().K8sClient
181+
sa := &corev1.ServiceAccount{}
182+
Eventually(func(g Gomega) {
183+
err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, sa)
184+
g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get ServiceAccount %q/%q: %v", namespace, name, err))
185+
}).WithTimeout(10*time.Second).WithPolling(1*time.Second).Should(Succeed(), "ServiceAccount %q/%q did not become visible within timeout", namespace, name)
186+
}
187+
188+
// ExpectClusterRoleBindingExists waits for a ClusterRoleBinding to be available and visible to the client.
189+
func ExpectClusterRoleBindingExists(ctx context.Context, name string) {
190+
k8sClient := env.Get().K8sClient
191+
crb := &rbacv1.ClusterRoleBinding{}
192+
Eventually(func(g Gomega) {
193+
err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, crb)
194+
g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get ClusterRoleBinding %q: %v", name, err))
195+
}).WithTimeout(10*time.Second).WithPolling(1*time.Second).Should(Succeed(), "ClusterRoleBinding %q did not become visible within timeout", name)
196+
}

0 commit comments

Comments
 (0)