Skip to content

Commit d87dfaf

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 d87dfaf

File tree

6 files changed

+269
-18
lines changed

6 files changed

+269
-18
lines changed

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

test/e2e/steps/hooks.go

Lines changed: 21 additions & 15 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 {
186+
for _, r := range forDeletion {
187+
go func() {
182188
if _, err := k8sClient("delete", r.kind, r.name, "--ignore-not-found=true"); err != nil {
183189
logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderrOutput(err))
184190
}
185-
}
186-
}()
191+
}()
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)