Skip to content

Commit 6a9f311

Browse files
committed
Namespace and PVC probes
Adds readiness probing via CEL for namespaces and PVCs, to prevent subsequent phases from installing until their readiness checks have passed. Also adds e2e coverage via direct CER creation. Signed-off-by: Daniel Franz <dfranz@redhat.com>
1 parent 55473d8 commit 6a9f311

6 files changed

Lines changed: 274 additions & 20 deletions

File tree

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,10 @@ type Sourcerer interface {
321321
}
322322

323323
func (c *ClusterExtensionRevisionReconciler) SetupWithManager(mgr ctrl.Manager) error {
324+
// Initialize probes once at setup time
325+
if err := initializeProbes(); err != nil {
326+
return err
327+
}
324328
skipProgressDeadlineExceededPredicate := predicate.Funcs{
325329
UpdateFunc: func(e event.UpdateEvent) bool {
326330
rev, ok := e.ObjectNew.(*ocv1.ClusterExtensionRevision)
@@ -465,7 +469,7 @@ func (c *ClusterExtensionRevisionReconciler) toBoxcutterRevision(ctx context.Con
465469
opts := []boxcutter.RevisionReconcileOption{
466470
boxcutter.WithPreviousOwners(previousObjs),
467471
boxcutter.WithProbe(boxcutter.ProgressProbeType, probing.And{
468-
deploymentProbe, statefulSetProbe, crdProbe, issuerProbe, certProbe,
472+
&namespaceActiveProbe, deploymentProbe, statefulSetProbe, crdProbe, issuerProbe, certProbe, &pvcBoundProbe,
469473
}),
470474
}
471475

@@ -511,6 +515,28 @@ func EffectiveCollisionProtection(cp ...ocv1.CollisionProtection) ocv1.Collision
511515
return ecp
512516
}
513517

518+
// initializeProbes is used to initialize CEL probes at startup time, so we don't recreate them on every reconcile
519+
func initializeProbes() error {
520+
nsCEL, err := probing.NewCELProbe(namespaceActiveCEL, `namespace phase must be "Active"`)
521+
if err != nil {
522+
return fmt.Errorf("constructing namespace CEL probe: %w", err)
523+
}
524+
pvcCEL, err := probing.NewCELProbe(pvcBoundCEL, `persistentvolumeclaim phase must be "Bound"`)
525+
if err != nil {
526+
return fmt.Errorf("constructing PVC CEL probe: %w", err)
527+
}
528+
namespaceActiveProbe = probing.GroupKindSelector{
529+
GroupKind: schema.GroupKind{Group: corev1.GroupName, Kind: "Namespace"},
530+
Prober: nsCEL,
531+
}
532+
pvcBoundProbe = probing.GroupKindSelector{
533+
GroupKind: schema.GroupKind{Group: corev1.GroupName, Kind: "PersistentVolumeClaim"},
534+
Prober: pvcCEL,
535+
}
536+
537+
return nil
538+
}
539+
514540
var (
515541
deploymentProbe = &probing.GroupKindSelector{
516542
GroupKind: schema.GroupKind{Group: appsv1.GroupName, Kind: "Deployment"},
@@ -542,6 +568,14 @@ var (
542568
},
543569
}
544570

571+
// namespaceActiveCEL is a CEL rule which asserts that the namespace is in "Active" phase
572+
namespaceActiveCEL = `self.status.phase == "Active"`
573+
namespaceActiveProbe probing.GroupKindSelector
574+
575+
// pvcBoundCEL is a CEL rule which asserts that the PVC is in "Bound" phase
576+
pvcBoundCEL = `self.status.phase == "Bound"`
577+
pvcBoundProbe probing.GroupKindSelector
578+
545579
// deplStaefulSetProbe probes Deployment, StatefulSet objects.
546580
deplStatefulSetProbe = &probing.ObservedGenerationProbe{
547581
Prober: probing.And{

test/e2e/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ Leverage existing steps for common operations:
199199
Use these variables in YAML templates:
200200

201201
- `${NAME}`: Scenario-specific ClusterExtension name (e.g., `ce-123`)
202+
- `${CER_NAME}`: Scenario-specific ClusterExtensionRevision name (e.g., `cer-123`; for applying CERs directly)
202203
- `${TEST_NAMESPACE}`: Scenario-specific namespace (e.g., `ns-123`)
203204
- `${CATALOG_IMG}`: Catalog image reference (defaults to in-cluster registry, overridable via `CATALOG_IMG` env var)
204205

test/e2e/features/revision.feature

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
Feature: Install ClusterExtensionRevision
2+
3+
As an OLM user I would like to install a cluster extension revision.
4+
5+
Background:
6+
Given OLM is available
7+
And ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE}
8+
9+
@WIP
10+
@BoxcutterRuntime
11+
Scenario: Install simple revision
12+
When ClusterExtensionRevision is applied
13+
"""
14+
apiVersion: olm.operatorframework.io/v1
15+
kind: ClusterExtensionRevision
16+
metadata:
17+
annotations:
18+
olm.operatorframework.io/service-account-name: olm-sa
19+
olm.operatorframework.io/service-account-namespace: ${TEST_NAMESPACE}
20+
name: ${CER_NAME}
21+
spec:
22+
lifecycleState: Active
23+
collisionProtection: Prevent
24+
phases:
25+
- name: policies
26+
objects:
27+
- object:
28+
apiVersion: networking.k8s.io/v1
29+
kind: NetworkPolicy
30+
metadata:
31+
name: test-operator-network-policy
32+
namespace: ${TEST_NAMESPACE}
33+
spec:
34+
podSelector: {}
35+
policyTypes:
36+
- Ingress
37+
- name: deploy
38+
objects:
39+
- object:
40+
apiVersion: v1
41+
data:
42+
httpd.sh: |
43+
#!/bin/sh
44+
echo "Version 1.2.0"
45+
echo true > /var/www/started
46+
echo true > /var/www/ready
47+
echo true > /var/www/live
48+
exec httpd -f -h /var/www -p 80
49+
kind: ConfigMap
50+
metadata:
51+
name: httpd-script
52+
namespace: ${TEST_NAMESPACE}
53+
- object:
54+
apiVersion: v1
55+
data:
56+
name: test-configmap
57+
version: v1.2.0
58+
kind: ConfigMap
59+
metadata:
60+
annotations:
61+
shouldNotTemplate: |
62+
The namespace is {{ $labels.namespace }}. The templated $labels.namespace is NOT expected to be processed by OLM's rendering engine for registry+v1 bundles.
63+
name: test-configmap
64+
namespace: ${TEST_NAMESPACE}
65+
revision: 1
66+
"""
67+
68+
And ClusterExtensionRevision "${CER_NAME}" reports Progressing as True with Reason Succeeded
69+
And ClusterExtensionRevision "${CER_NAME}" reports Available as True with Reason ProbesSucceeded
70+
And resource "networkpolicy/test-operator-network-policy" is installed
71+
And resource "configmap/test-configmap" is installed
72+
73+
@WIP
74+
@BoxcutterRuntime
75+
Scenario: Probe failure for PersistentVolumeClaim halts phase progression
76+
When ClusterExtensionRevision is applied
77+
"""
78+
apiVersion: olm.operatorframework.io/v1
79+
kind: ClusterExtensionRevision
80+
metadata:
81+
annotations:
82+
olm.operatorframework.io/service-account-name: olm-sa
83+
olm.operatorframework.io/service-account-namespace: ${TEST_NAMESPACE}
84+
name: ${CER_NAME}
85+
spec:
86+
lifecycleState: Active
87+
collisionProtection: Prevent
88+
phases:
89+
- name: pvc
90+
objects:
91+
- object:
92+
apiVersion: v1
93+
kind: PersistentVolumeClaim
94+
metadata:
95+
name: test-pvc
96+
namespace: ${TEST_NAMESPACE}
97+
spec:
98+
accessModes:
99+
- ReadWriteOnce
100+
storageClassName: ""
101+
volumeName: test-pv
102+
resources:
103+
requests:
104+
storage: 1Mi
105+
- name: configmap
106+
objects:
107+
- object:
108+
apiVersion: v1
109+
kind: ConfigMap
110+
metadata:
111+
annotations:
112+
shouldNotTemplate: |
113+
The namespace is {{ $labels.namespace }}. The templated $labels.namespace is NOT expected to be processed by OLM's rendering engine for registry+v1 bundles.
114+
name: test-configmap
115+
namespace: ${TEST_NAMESPACE}
116+
data:
117+
name: test-configmap
118+
version: v1.2.0
119+
revision: 1
120+
"""
121+
122+
And resource "persistentvolumeclaim/test-pvc" is installed
123+
And ClusterExtensionRevision "${CER_NAME}" reports Available as False with Reason ProbeFailure
124+
And resource "configmap/test-configmap" is not installed
125+
126+
@WIP
127+
@BoxcutterRuntime
128+
Scenario: Phases progress when PersistentVolumeClaim becomes "Bound"
129+
When ClusterExtensionRevision is applied
130+
"""
131+
apiVersion: olm.operatorframework.io/v1
132+
kind: ClusterExtensionRevision
133+
metadata:
134+
annotations:
135+
olm.operatorframework.io/service-account-name: olm-sa
136+
olm.operatorframework.io/service-account-namespace: ${TEST_NAMESPACE}
137+
name: ${CER_NAME}
138+
spec:
139+
lifecycleState: Active
140+
collisionProtection: Prevent
141+
phases:
142+
- name: pvc
143+
objects:
144+
- object:
145+
apiVersion: v1
146+
kind: PersistentVolumeClaim
147+
metadata:
148+
name: test-pvc
149+
namespace: ${TEST_NAMESPACE}
150+
spec:
151+
accessModes:
152+
- ReadWriteOnce
153+
storageClassName: ""
154+
volumeName: test-pv
155+
resources:
156+
requests:
157+
storage: 1Mi
158+
- object:
159+
apiVersion: v1
160+
kind: PersistentVolume
161+
metadata:
162+
name: test-pv
163+
spec:
164+
accessModes:
165+
- ReadWriteOnce
166+
capacity:
167+
storage: 1Mi
168+
claimRef:
169+
apiVersion: v1
170+
kind: PersistentVolumeClaim
171+
name: test-pvc
172+
namespace: ${TEST_NAMESPACE}
173+
persistentVolumeReclaimPolicy: Delete
174+
storageClassName: ""
175+
volumeMode: Filesystem
176+
local:
177+
path: /tmp/persistent-volume
178+
nodeAffinity:
179+
required:
180+
nodeSelectorTerms:
181+
- matchExpressions:
182+
- key: kubernetes.io/hostname
183+
operator: In
184+
values:
185+
- operator-controller-e2e-control-plane
186+
- name: configmap
187+
objects:
188+
- object:
189+
apiVersion: v1
190+
kind: ConfigMap
191+
metadata:
192+
annotations:
193+
shouldNotTemplate: |
194+
The namespace is {{ $labels.namespace }}. The templated $labels.namespace is NOT expected to be processed by OLM's rendering engine for registry+v1 bundles.
195+
name: test-configmap
196+
namespace: ${TEST_NAMESPACE}
197+
data:
198+
name: test-configmap
199+
version: v1.2.0
200+
revision: 1
201+
"""
202+
203+
And ClusterExtensionRevision "${CER_NAME}" reports Progressing as True with Reason Succeeded
204+
And ClusterExtensionRevision "${CER_NAME}" reports Available as True with Reason ProbesSucceeded
205+
And resource "persistentvolume/test-pv" is installed
206+
And resource "persistentvolumeclaim/test-pvc" is installed
207+
And resource "configmap/test-configmap" is installed

test/e2e/steps/hooks.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ type resource struct {
2727
}
2828

2929
type scenarioContext struct {
30-
id string
31-
namespace string
32-
clusterExtensionName string
33-
removedResources []unstructured.Unstructured
34-
backGroundCmds []*exec.Cmd
35-
metricsResponse map[string]string
36-
37-
extensionObjects []client.Object
30+
id string
31+
namespace string
32+
clusterExtensionName string
33+
clusterExtensionRevisionName string
34+
removedResources []unstructured.Unstructured
35+
backGroundCmds []*exec.Cmd
36+
metricsResponse map[string]string
37+
38+
extensionObjects []client.Object
39+
revisionSingleton client.Object
3840
}
3941

4042
// GatherClusterExtensionObjects collects all resources related to the ClusterExtension container in
@@ -142,9 +144,10 @@ func CheckFeatureTags(ctx context.Context, sc *godog.Scenario) (context.Context,
142144

143145
func CreateScenarioContext(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
144146
scCtx := &scenarioContext{
145-
id: sc.Id,
146-
namespace: fmt.Sprintf("ns-%s", sc.Id),
147-
clusterExtensionName: fmt.Sprintf("ce-%s", sc.Id),
147+
id: sc.Id,
148+
namespace: fmt.Sprintf("ns-%s", sc.Id),
149+
clusterExtensionName: fmt.Sprintf("ce-%s", sc.Id),
150+
clusterExtensionRevisionName: fmt.Sprintf("cer-%s", sc.Id),
148151
}
149152
return context.WithValue(ctx, scenarioContextKey, scCtx), nil
150153
}
@@ -176,13 +179,16 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context
176179
if sc.clusterExtensionName != "" {
177180
forDeletion = append(forDeletion, resource{name: sc.clusterExtensionName, kind: "clusterextension"})
178181
}
182+
if sc.clusterExtensionRevisionName != "" {
183+
forDeletion = append(forDeletion, resource{name: sc.clusterExtensionRevisionName, kind: "clusterextensionrevision"})
184+
}
179185
forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"})
180-
go func() {
181-
for _, r := range forDeletion {
182-
if _, err := k8sClient("delete", r.kind, r.name, "--ignore-not-found=true"); err != nil {
183-
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderrOutput(err))
186+
for _, r := range forDeletion {
187+
go func(res resource) {
188+
if _, err := k8sClient("delete", res.kind, res.name, "--ignore-not-found=true"); err != nil {
189+
logger.Info("Error deleting resource", "name", res.name, "namespace", sc.namespace, "stderr", stderrOutput(err))
184190
}
185-
}
186-
}()
191+
}(r)
192+
}
187193
return ctx, nil
188194
}

test/e2e/steps/steps.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func RegisterSteps(sc *godog.ScenarioContext) {
7272
sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+)$`, ClusterExtensionReportsConditionWithoutReason)
7373
sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+)$`, ClusterExtensionRevisionReportsConditionWithoutMsg)
7474
sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) transition between (\d+) and (\d+) minutes since its creation$`, ClusterExtensionReportsConditionTransitionTime)
75+
sc.Step(`^(?i)ClusterExtensionRevision is applied(?:\s+.*)?$`, ResourceIsApplied)
7576
sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" is archived$`, ClusterExtensionRevisionIsArchived)
7677
sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" contains annotation "([^"]+)" with value$`, ClusterExtensionRevisionHasAnnotationWithValue)
7778
sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" has label "([^"]+)" with value "([^"]+)"$`, ClusterExtensionRevisionHasLabelWithValue)
@@ -80,7 +81,7 @@ func RegisterSteps(sc *godog.ScenarioContext) {
8081
sc.Step(`^(?i)resource "([^"]+)" is installed$`, ResourceAvailable)
8182
sc.Step(`^(?i)resource "([^"]+)" is available$`, ResourceAvailable)
8283
sc.Step(`^(?i)resource "([^"]+)" is removed$`, ResourceRemoved)
83-
sc.Step(`^(?i)resource "([^"]+)" is eventually not found$`, ResourceEventuallyNotFound)
84+
sc.Step(`^(?i)resource "([^"]+)" is (eventually not found|not installed)$`, ResourceEventuallyNotFound)
8485
sc.Step(`^(?i)resource "([^"]+)" exists$`, ResourceAvailable)
8586
sc.Step(`^(?i)resource is applied$`, ResourceIsApplied)
8687
sc.Step(`^(?i)resource "deployment/test-operator" reports as (not ready|ready)$`, MarkTestOperatorNotReady)
@@ -186,6 +187,7 @@ func substituteScenarioVars(content string, sc *scenarioContext) string {
186187
vars := map[string]string{
187188
"TEST_NAMESPACE": sc.namespace,
188189
"NAME": sc.clusterExtensionName,
190+
"CER_NAME": sc.clusterExtensionRevisionName,
189191
"CATALOG_IMG": "docker-registry.operator-controller-e2e.svc.cluster.local:5000/e2e/test-catalog:v1",
190192
}
191193
if v, found := os.LookupEnv("CATALOG_IMG"); found {
@@ -246,10 +248,12 @@ func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error
246248
}
247249
out, err := k8scliWithInput(yamlContent, "apply", "-f", "-")
248250
if err != nil {
249-
return fmt.Errorf("failed to apply resource %v %w", out, err)
251+
return fmt.Errorf("failed to apply resource %v %w", out, stderrOutput(err))
250252
}
251253
if res.GetKind() == "ClusterExtension" {
252254
sc.clusterExtensionName = res.GetName()
255+
} else if res.GetKind() == "ClusterExtensionRevision" {
256+
sc.clusterExtensionRevisionName = res.GetName()
253257
}
254258
return nil
255259
}

test/e2e/steps/testdata/rbac-template.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ rules:
2828
- serviceaccounts
2929
- events
3030
- namespaces
31+
- persistentvolumes
32+
- persistentvolumeclaims
3133
verbs: [update, create, list, watch, get, delete, patch]
3234
- apiGroups: ["apps"]
3335
resources:

0 commit comments

Comments
 (0)