Skip to content

Commit b4e2c87

Browse files
committed
Cover group tracking in experimental E2E tests
Check generated groups, shared SSA ownership during rollout, pruning isolation, and cascade cleanup. Update direct COS fixtures and query revision associations by spec.group. Signed-off-by: Francesco Giudici <fgiudici@redhat.com>
1 parent d08a520 commit b4e2c87

8 files changed

Lines changed: 258 additions & 16 deletions

File tree

‎test/e2e/features/group.feature‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
@BoxcutterRuntime
2+
Feature: Track ClusterObjectSet revisions by group
3+
4+
Background:
5+
Given OLM is available
6+
And an image registry is available
7+
And namespace "${TEST_NAMESPACE}" is available
8+
9+
Scenario: Revision pruning is isolated by group
10+
Given a catalog "test" with packages:
11+
| package | version | channel | replaces | contents |
12+
| test | 1.0.0 | beta | | CRD, Deployment, ConfigMap |
13+
| test | 1.0.1 | beta | 1.0.0 | CRD, Deployment, ConfigMap |
14+
| test | 1.0.2 | beta | 1.0.1 | CRD, Deployment, ConfigMap |
15+
| test | 1.0.3 | beta | 1.0.2 | CRD, Deployment, ConfigMap |
16+
| test | 1.0.4 | beta | 1.0.3 | CRD, Deployment, ConfigMap |
17+
| test | 1.0.5 | beta | 1.0.4 | CRD, Deployment, ConfigMap |
18+
| test | 1.0.6 | beta | 1.0.5 | CRD, Deployment, ConfigMap |
19+
And ClusterObjectSet is applied
20+
"""
21+
apiVersion: olm.operatorframework.io/v1
22+
kind: ClusterObjectSet
23+
metadata:
24+
name: ${COS_NAME}
25+
labels:
26+
olm.operatorframework.io/owner-name: ${NAME}
27+
spec:
28+
group: ${NAME}-other
29+
revision: 1
30+
lifecycleState: Archived
31+
collisionProtection: Prevent
32+
"""
33+
And ClusterExtension is applied
34+
"""
35+
apiVersion: olm.operatorframework.io/v1
36+
kind: ClusterExtension
37+
metadata:
38+
name: ${NAME}
39+
spec:
40+
namespace: ${TEST_NAMESPACE}
41+
source:
42+
sourceType: Catalog
43+
catalog:
44+
packageName: ${PACKAGE:test}
45+
version: 1.0.0
46+
selector:
47+
matchLabels:
48+
olm.operatorframework.io/metadata.name: ${CATALOG:test}
49+
"""
50+
And ClusterExtension is rolled out
51+
When ClusterExtension version is updated to "1.0.1"
52+
Then ClusterExtension is rolled out
53+
When ClusterExtension version is updated to "1.0.2"
54+
Then ClusterExtension is rolled out
55+
When ClusterExtension version is updated to "1.0.3"
56+
Then ClusterExtension is rolled out
57+
When ClusterExtension version is updated to "1.0.4"
58+
Then ClusterExtension is rolled out
59+
When ClusterExtension version is updated to "1.0.5"
60+
Then ClusterExtension is rolled out
61+
When ClusterExtension version is updated to "1.0.6"
62+
Then ClusterExtension is rolled out
63+
And ClusterExtension is available
64+
And ClusterExtension reports "${NAME}-7" as active revision
65+
And ClusterObjectSet "${NAME}-7" has group "${NAME}"
66+
And resource "clusterobjectset/${NAME}-1" is eventually not found
67+
# The existing retention policy keeps five prior revisions plus the current one.
68+
And ClusterExtension "${NAME}" owns 6 ClusterObjectSets
69+
And resource "clusterobjectset/${COS_NAME}" exists
70+
And ClusterObjectSet "${COS_NAME}" has group "${NAME}-other"

‎test/e2e/features/install.feature‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ Feature: Install ClusterExtension
465465
And ClusterExtension is available
466466
And ClusterObjectSet "${NAME}-1" has label "olm.operatorframework.io/owner-kind" with value "ClusterExtension"
467467
And ClusterObjectSet "${NAME}-1" has label "olm.operatorframework.io/owner-name" with value "${NAME}"
468+
And ClusterObjectSet "${NAME}-1" has group "${NAME}"
469+
And ClusterExtension "${NAME}" owns 1 ClusterObjectSet
468470

469471
@BoxcutterRuntime
470472
Scenario: ClusterObjectSet objects are externalized to immutable Secrets

‎test/e2e/features/revision.feature‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Feature: Install ClusterObjectSet
1515
metadata:
1616
name: ${COS_NAME}
1717
spec:
18+
group: ${NAME}
1819
lifecycleState: Active
1920
collisionProtection: Prevent
2021
progressionProbes:
@@ -75,6 +76,7 @@ Feature: Install ClusterObjectSet
7576
metadata:
7677
name: ${COS_NAME}
7778
spec:
79+
group: ${NAME}
7880
lifecycleState: Active
7981
collisionProtection: Prevent
8082
progressionProbes:
@@ -161,6 +163,7 @@ Feature: Install ClusterObjectSet
161163
metadata:
162164
name: ${COS_NAME}
163165
spec:
166+
group: ${NAME}
164167
lifecycleState: Active
165168
collisionProtection: Prevent
166169
progressionProbes:
@@ -219,6 +222,7 @@ Feature: Install ClusterObjectSet
219222
metadata:
220223
name: ${COS_NAME}
221224
spec:
225+
group: ${NAME}
222226
lifecycleState: Active
223227
collisionProtection: Prevent
224228
progressionProbes:
@@ -403,6 +407,7 @@ Feature: Install ClusterObjectSet
403407
metadata:
404408
name: ${COS_NAME}
405409
spec:
410+
group: ${NAME}
406411
lifecycleState: Active
407412
collisionProtection: Prevent
408413
phases:
@@ -454,6 +459,7 @@ Feature: Install ClusterObjectSet
454459
metadata:
455460
name: ${COS_NAME}
456461
spec:
462+
group: ${NAME}
457463
lifecycleState: Active
458464
collisionProtection: Prevent
459465
phases:
@@ -502,6 +508,7 @@ Feature: Install ClusterObjectSet
502508
metadata:
503509
name: ${COS_NAME}
504510
spec:
511+
group: ${NAME}
505512
lifecycleState: Active
506513
collisionProtection: Prevent
507514
phases:
@@ -585,6 +592,7 @@ Feature: Install ClusterObjectSet
585592
metadata:
586593
name: ${COS_NAME}
587594
spec:
595+
group: ${NAME}
588596
lifecycleState: Active
589597
collisionProtection: Prevent
590598
progressDeadlineMinutes: 1
@@ -651,6 +659,7 @@ Feature: Install ClusterObjectSet
651659
metadata:
652660
name: ${COS_NAME}
653661
spec:
662+
group: ${NAME}
654663
lifecycleState: Active
655664
collisionProtection: Prevent
656665
progressDeadlineMinutes: 1

‎test/e2e/features/uninstall.feature‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,13 @@ Feature: Uninstall ClusterExtension
3434
When ClusterExtension is removed
3535
Then the ClusterExtension's constituent resources are removed
3636

37+
@BoxcutterRuntime
38+
Scenario: Removing ClusterExtension cascades to grouped revisions and their content Secrets
39+
Given ClusterObjectSet "${NAME}-1" has group "${NAME}"
40+
And ClusterExtension "${NAME}" owns 1 ClusterObjectSet
41+
And ClusterObjectSet "${NAME}-1" referred secrets are owned by the object set
42+
And ClusterObjectSet "${NAME}-1" referred secrets are remembered
43+
When ClusterExtension is removed
44+
Then resource "clusterobjectset/${NAME}-1" is eventually not found
45+
And the ClusterExtension's constituent resources are removed
46+
And the remembered revision secrets are removed

‎test/e2e/features/update.feature‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ Feature: Update ClusterExtension
310310
"""
311311
And ClusterExtension is rolled out
312312
And ClusterExtension is available
313+
And ClusterObjectSet "${NAME}-1" has group "${NAME}"
314+
And ClusterObjectSet "${NAME}-1" phase objects use SSA manager "cos-group/${NAME}"
313315
When ClusterExtension version is updated to "1.2.0"
314316
Then bundle "${PACKAGE:test}.1.2.0" is installed in version "1.2.0"
315317
And ClusterExtension is rolled out
@@ -318,6 +320,8 @@ Feature: Update ClusterExtension
318320
And ClusterObjectSet "${NAME}-2" reports Ready as True with Reason ProbesSucceeded
319321
And ClusterObjectSet "${NAME}-1" is archived
320322
And ClusterObjectSet "${NAME}-1" phase objects are not found or not owned by the revision
323+
And ClusterObjectSet "${NAME}-2" has group "${NAME}"
324+
And ClusterObjectSet "${NAME}-2" phase objects use SSA manager "cos-group/${NAME}"
321325

322326
@BoxcutterRuntime
323327
Scenario: Report all active revisions on ClusterExtension

‎test/e2e/steps/group_steps.go‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package steps
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/apimachinery/pkg/runtime"
12+
13+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
14+
)
15+
16+
// A local cache index does not make spec.group selectable through kubectl.
17+
func listClusterObjectSetsInGroup(ctx context.Context, group string) ([]ocv1.ClusterObjectSet, error) {
18+
out, err := k8sClient(ctx, "get", "clusterobjectsets", "-o", "json")
19+
if err != nil {
20+
return nil, err
21+
}
22+
var list ocv1.ClusterObjectSetList
23+
if err := json.Unmarshal([]byte(out), &list); err != nil {
24+
return nil, err
25+
}
26+
var revisions []ocv1.ClusterObjectSet
27+
for _, rev := range list.Items {
28+
if rev.Spec.Group == group {
29+
revisions = append(revisions, rev)
30+
}
31+
}
32+
return revisions, nil
33+
}
34+
35+
func ClusterObjectSetHasGroup(ctx context.Context, revisionName, group string) error {
36+
sc := scenarioCtx(ctx)
37+
revisionName = substituteScenarioVars(revisionName, sc)
38+
group = substituteScenarioVars(group, sc)
39+
waitFor(ctx, func() bool {
40+
obj, err := getResource("clusterobjectset", revisionName, "")
41+
if err != nil {
42+
return false
43+
}
44+
var cos ocv1.ClusterObjectSet
45+
return runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &cos) == nil && cos.Spec.Group == group
46+
})
47+
return nil
48+
}
49+
50+
// ClusterObjectSetObjectsUseSSAManager checks actual managed fields on every
51+
// inline or externally stored phase object, including their revision owner.
52+
func ClusterObjectSetObjectsUseSSAManager(ctx context.Context, revisionName, manager string) error {
53+
sc := scenarioCtx(ctx)
54+
revisionName = substituteScenarioVars(revisionName, sc)
55+
manager = substituteScenarioVars(manager, sc)
56+
waitFor(ctx, func() bool {
57+
obj, err := getResource("clusterobjectset", revisionName, "")
58+
if err != nil {
59+
return false
60+
}
61+
var cos ocv1.ClusterObjectSet
62+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &cos); err != nil {
63+
return false
64+
}
65+
count := 0
66+
for _, phase := range cos.Spec.Phases {
67+
for _, phaseObj := range phase.Objects {
68+
desired := &phaseObj.Object
69+
if phaseObj.Ref.Name != "" {
70+
desired, err = resolveObjectRef(phaseObj.Ref)
71+
if err != nil {
72+
return false
73+
}
74+
}
75+
// kubectl's JSON/YAML printers omit managedFields by default.
76+
args := []string{"get", desired.GetKind(), desired.GetName(), "-o", "json", "--show-managed-fields"}
77+
if desired.GetNamespace() != "" {
78+
args = append(args, "-n", desired.GetNamespace())
79+
}
80+
out, err := k8sClient(ctx, args...)
81+
if err != nil {
82+
return false
83+
}
84+
actual := &unstructured.Unstructured{}
85+
if err := json.Unmarshal([]byte(out), &actual.Object); err != nil {
86+
return false
87+
}
88+
owner := metav1.GetControllerOf(actual)
89+
if owner == nil || owner.Kind != ocv1.ClusterObjectSetKind || owner.UID != cos.UID {
90+
return false
91+
}
92+
found := false
93+
for _, fields := range actual.GetManagedFields() {
94+
if fields.Operation != metav1.ManagedFieldsOperationApply {
95+
continue
96+
}
97+
if strings.HasPrefix(fields.Manager, "cos-group/") && fields.Manager != manager {
98+
return false
99+
}
100+
if fields.Manager == manager {
101+
found = true
102+
}
103+
}
104+
if !found {
105+
return false
106+
}
107+
count++
108+
}
109+
}
110+
return count > 0
111+
})
112+
return nil
113+
}
114+
115+
func RememberClusterObjectSetSecrets(ctx context.Context, revisionName string) error {
116+
sc := scenarioCtx(ctx)
117+
revisionName = substituteScenarioVars(revisionName, sc)
118+
names, err := collectReferredSecretNames(ctx, revisionName)
119+
if err != nil {
120+
return err
121+
}
122+
for _, name := range names {
123+
sc.revisionSecrets = append(sc.revisionSecrets, resource{kind: "secret", name: name, namespace: olmNamespace})
124+
}
125+
return nil
126+
}
127+
128+
func RememberedRevisionSecretsRemoved(ctx context.Context) error {
129+
sc := scenarioCtx(ctx)
130+
if len(sc.revisionSecrets) == 0 {
131+
return fmt.Errorf("no revision secrets were remembered")
132+
}
133+
for _, secret := range sc.revisionSecrets {
134+
waitFor(ctx, func() bool {
135+
out, err := k8sClient(ctx, "get", secret.kind, secret.name, "-n", secret.namespace, "--ignore-not-found", "-o", "name")
136+
return err == nil && strings.TrimSpace(out) == ""
137+
})
138+
}
139+
return nil
140+
}

‎test/e2e/steps/hooks.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type scenarioContext struct {
5656
leaderPods map[string]string // component name -> leader pod name
5757
deploymentRestores []deploymentRestore
5858
extensionObjects []client.Object
59+
revisionSecrets []resource
5960
proxy *recordingProxy
6061
catalogAddr string
6162
catalogCleanup func()

‎test/e2e/steps/steps.go‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ func RegisterSteps(sc *godog.ScenarioContext) {
134134
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" is archived$`, ClusterObjectSetIsArchived)
135135
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" contains annotation "([^"]+)" with value$`, ClusterObjectSetHasAnnotationWithValue)
136136
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" has label "([^"]+)" with value "([^"]+)"$`, ClusterObjectSetHasLabelWithValue)
137+
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" has group "([^"]+)"$`, ClusterObjectSetHasGroup)
138+
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" phase objects use SSA manager "([^"]+)"$`, ClusterObjectSetObjectsUseSSAManager)
139+
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" referred secrets are remembered$`, RememberClusterObjectSetSecrets)
140+
sc.Step(`^(?i)the remembered revision secrets are removed$`, RememberedRevisionSecretsRemoved)
137141
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" phase objects are not found or not owned by the revision$`, ClusterObjectSetObjectsNotFoundOrNotOwned)
138142
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" phase objects are managed in Kubernetes secrets$`, ClusterObjectSetPhaseObjectsManagedInSecrets)
139143
sc.Step(`^(?i)ClusterObjectSet "([^"]+)" referred secrets exist in "([^"]+)" namespace$`, ClusterObjectSetReferredSecretsExist)
@@ -548,14 +552,24 @@ func ClusterExtensionOwnsClusterObjectSets(ctx context.Context, extName string,
548552
sc := scenarioCtx(ctx)
549553
extName = substituteScenarioVars(extName, sc)
550554
waitFor(ctx, func() bool {
551-
out, err := k8sClient(ctx, "get", "clusterobjectsets",
552-
"-l", fmt.Sprintf("olm.operatorframework.io/owner-name=%s", extName),
553-
"-o", "jsonpath={.items[*].metadata.name}")
555+
revisions, err := listClusterObjectSetsInGroup(ctx, extName)
554556
if err != nil {
555557
return false
556558
}
557-
names := strings.Fields(strings.TrimSpace(out))
558-
return len(names) == expectedCount
559+
if len(revisions) != expectedCount {
560+
return false
561+
}
562+
ext, err := getResource("clusterextension", extName, "")
563+
if err != nil {
564+
return false
565+
}
566+
for _, rev := range revisions {
567+
owner := metav1.GetControllerOf(&rev)
568+
if owner == nil || owner.Kind != ocv1.ClusterExtensionKind || owner.Name != extName || owner.UID != ext.GetUID() {
569+
return false
570+
}
571+
}
572+
return true
559573
})
560574
return nil
561575
}
@@ -2296,21 +2310,13 @@ func resolveObjectRef(ref ocv1.ObjectSourceRef) (*unstructured.Unstructured, err
22962310

22972311
// latestActiveRevisionForExtension returns the latest active revision for the extension called extName
22982312
func latestActiveRevisionForExtension(extName string) (*ocv1.ClusterObjectSet, error) {
2299-
out, err := k8sClient(context.Background(), "get", "clusterobjectsets", "-l", fmt.Sprintf("olm.operatorframework.io/owner-name=%s", extName), "-o", "json")
2313+
revisions, err := listClusterObjectSetsInGroup(context.Background(), extName)
23002314
if err != nil {
23012315
return nil, fmt.Errorf("error listing revisions for extension '%s': %w", extName, err)
23022316
}
2303-
if strings.TrimSpace(out) == "" {
2304-
return nil, fmt.Errorf("no revisions found for extension '%s'", extName)
2305-
}
2306-
var revisionList ocv1.ClusterObjectSetList
2307-
if err := json.Unmarshal([]byte(out), &revisionList); err != nil {
2308-
return nil, fmt.Errorf("error unmarshalling revisions for extension '%s': %w", extName, err)
2309-
}
2310-
23112317
var latest *ocv1.ClusterObjectSet
2312-
for i := range revisionList.Items {
2313-
rev := &revisionList.Items[i]
2318+
for i := range revisions {
2319+
rev := &revisions[i]
23142320
if rev.Spec.LifecycleState != ocv1.ClusterObjectSetLifecycleStateActive {
23152321
continue
23162322
}

0 commit comments

Comments
 (0)