Skip to content

Commit f626edd

Browse files
Add test verifies that OLMv1 does not revert user-initiated changes to deployed resources
Generate-by: Cursor/Claude
1 parent 10c2841 commit f626edd

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
Feature: Rollout Restart User Changes
2+
3+
# This test verifies that OLMv1 does not revert user-initiated changes to deployed resources,
4+
# specifically testing the scenario where a user runs `kubectl rollout restart deployment`.
5+
#
6+
# Background:
7+
# - In OLMv0, running `kubectl rollout restart deployment` would add a restart annotation
8+
# to the deployment, but OLM would revert this change because it actively reconciles
9+
# the deployment based on CSV contents.
10+
# - In OLMv1, we use Server-Side Apply which should only manage the fields that the controller
11+
# explicitly owns, allowing user-initiated changes to other fields to persist.
12+
#
13+
# This test ensures that OLMv1 handles this correctly and does not exhibit the OLMv0 behavior.
14+
# See: https://github.com/operator-framework/operator-lifecycle-manager/issues/3392
15+
16+
Background:
17+
Given OLM is available
18+
And ClusterCatalog "test" serves bundles
19+
And ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE}
20+
21+
# KNOWN BUG: Boxcutter currently reverts user-initiated deployment changes
22+
#
23+
# ROOT CAUSE:
24+
# When Boxcutter (pkg.package-operator.run/boxcutter v0.10.0) reconciles a
25+
# ClusterExtensionRevision, it applies the manifest stored in the revision spec.
26+
# This manifest snapshot was taken when the bundle was unpacked and does NOT
27+
# include user-added annotations like the restart timestamp from `kubectl rollout restart`.
28+
#
29+
# Because Boxcutter applies this full manifest (including the annotations field),
30+
# it takes ownership of the entire field via Server-Side Apply, overwriting any
31+
# annotations added by other field managers (like kubectl).
32+
#
33+
# EXPECTED BEHAVIOR (not yet implemented):
34+
# The controller should only manage the fields it explicitly cares about, allowing
35+
# user-managed fields (like restart annotations) to persist across reconciliation.
36+
#
37+
# This is the same issue that existed in OLMv0 (see upstream issue below) and needs
38+
# to be fixed in the OLMv1 Boxcutter implementation.
39+
#
40+
# UPSTREAM ISSUE: https://github.com/operator-framework/operator-lifecycle-manager/issues/3392
41+
#
42+
# TODO: Fix the Boxcutter implementation to properly use Server-Side Apply field
43+
# ownership so user changes persist, then remove @skip tag
44+
@skip
45+
@BoxcutterRuntime
46+
Scenario: User-initiated deployment changes persist after OLM reconciliation
47+
When ClusterExtension is applied
48+
"""
49+
apiVersion: olm.operatorframework.io/v1
50+
kind: ClusterExtension
51+
metadata:
52+
name: ${NAME}
53+
spec:
54+
namespace: ${TEST_NAMESPACE}
55+
serviceAccount:
56+
name: olm-sa
57+
source:
58+
sourceType: Catalog
59+
catalog:
60+
packageName: test
61+
selector:
62+
matchLabels:
63+
"olm.operatorframework.io/metadata.name": test-catalog
64+
"""
65+
Then ClusterExtension is rolled out
66+
And ClusterExtension is available
67+
And resource "deployment/test-operator" is installed
68+
# Simulate user running "kubectl rollout restart deployment/test-operator"
69+
# This adds a restart annotation to trigger a rolling restart
70+
# In OLMv0, the controller would revert this annotation
71+
# In OLMv1 with SSA, the annotation should persist because kubectl owns this field
72+
When user adds restart annotation to "deployment/test-operator"
73+
# The restart annotation should still be present - OLMv1 should not revert user changes
74+
# The controller reconciles periodically, so if it was going to revert the change, it would happen
75+
Then resource "deployment/test-operator" has restart annotation

test/e2e/steps/steps.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func RegisterSteps(sc *godog.ScenarioContext) {
8787
sc.Step(`^(?i)resource apply fails with error msg containing "([^"]+)"$`, ResourceApplyFails)
8888
sc.Step(`^(?i)resource "([^"]+)" is eventually restored$`, ResourceRestored)
8989
sc.Step(`^(?i)resource "([^"]+)" matches$`, ResourceMatches)
90+
sc.Step(`^(?i)user adds restart annotation to "([^"]+)"$`, UserAddsRestartAnnotation)
91+
sc.Step(`^(?i)resource "([^"]+)" has restart annotation$`, ResourceHasRestartAnnotation)
9092

9193
sc.Step(`^(?i)ServiceAccount "([^"]*)" with needed permissions is available in test namespace$`, ServiceAccountWithNeededPermissionsIsAvailableInNamespace)
9294
sc.Step(`^(?i)ServiceAccount "([^"]*)" with needed permissions is available in \${TEST_NAMESPACE}$`, ServiceAccountWithNeededPermissionsIsAvailableInNamespace)
@@ -1168,3 +1170,64 @@ func latestActiveRevisionForExtension(extName string) (*ocv1.ClusterExtensionRev
11681170

11691171
return latest, nil
11701172
}
1173+
1174+
// UserAddsRestartAnnotation simulates a user running `kubectl rollout restart deployment/<name>`.
1175+
// This adds a restart annotation to the deployment's pod template to trigger a rolling restart.
1176+
// In OLMv0, this annotation would be reverted by the controller. In OLMv1 with Server-Side Apply,
1177+
// it should persist because the user (kubectl) manages this field, not the controller.
1178+
// See: https://github.com/operator-framework/operator-lifecycle-manager/issues/3392
1179+
func UserAddsRestartAnnotation(ctx context.Context, resourceName string) error {
1180+
sc := scenarioCtx(ctx)
1181+
resourceName = substituteScenarioVars(resourceName, sc)
1182+
1183+
kind, _, ok := strings.Cut(resourceName, "/")
1184+
if !ok {
1185+
return fmt.Errorf("invalid resource name format: %s (expected kind/name)", resourceName)
1186+
}
1187+
1188+
if kind != "deployment" {
1189+
return fmt.Errorf("only deployment resources are supported for restart annotation, got: %s", kind)
1190+
}
1191+
1192+
// Use kubectl rollout restart to add the restart annotation
1193+
// This is the actual command users would run, ensuring we test real-world behavior
1194+
_, err := k8sClient("rollout", "restart", resourceName, "-n", sc.namespace)
1195+
if err != nil {
1196+
return fmt.Errorf("failed to rollout restart %s: %w", resourceName, err)
1197+
}
1198+
1199+
return nil
1200+
}
1201+
1202+
// ResourceHasRestartAnnotation verifies that a deployment has a restart annotation.
1203+
// This confirms that user-initiated changes persist after OLM reconciliation.
1204+
func ResourceHasRestartAnnotation(ctx context.Context, resourceName string) error {
1205+
sc := scenarioCtx(ctx)
1206+
resourceName = substituteScenarioVars(resourceName, sc)
1207+
1208+
kind, deploymentName, ok := strings.Cut(resourceName, "/")
1209+
if !ok {
1210+
return fmt.Errorf("invalid resource name format: %s (expected kind/name)", resourceName)
1211+
}
1212+
1213+
if kind != "deployment" {
1214+
return fmt.Errorf("only deployment resources are supported for restart annotation check, got: %s", kind)
1215+
}
1216+
1217+
// Check for the restart annotation added by kubectl rollout restart
1218+
restartAnnotationKey := "kubectl.kubernetes.io/restartedAt"
1219+
1220+
waitFor(ctx, func() bool {
1221+
// Get the restart annotation from the deployment's pod template
1222+
out, err := k8sClient("get", "deployment", deploymentName, "-n", sc.namespace,
1223+
"-o", fmt.Sprintf("jsonpath={.spec.template.metadata.annotations['%s']}", restartAnnotationKey))
1224+
if err != nil {
1225+
return false
1226+
}
1227+
1228+
// If the annotation exists and has a value, it persisted
1229+
return out != ""
1230+
})
1231+
1232+
return nil
1233+
}

0 commit comments

Comments
 (0)