Skip to content

Commit 6173613

Browse files
Add test verifies that OLMv1 does not revert user-initiated changes to deployed resources
1 parent 10c2841 commit 6173613

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
@BoxcutterRuntime
22+
Scenario: User-initiated deployment changes persist after OLM reconciliation
23+
When ClusterExtension is applied
24+
"""
25+
apiVersion: olm.operatorframework.io/v1
26+
kind: ClusterExtension
27+
metadata:
28+
name: ${NAME}
29+
spec:
30+
namespace: ${TEST_NAMESPACE}
31+
serviceAccount:
32+
name: olm-sa
33+
source:
34+
sourceType: Catalog
35+
catalog:
36+
packageName: test
37+
selector:
38+
matchLabels:
39+
"olm.operatorframework.io/metadata.name": test-catalog
40+
"""
41+
Then ClusterExtension is rolled out
42+
And ClusterExtension is available
43+
And resource "deployment/test-operator" is installed
44+
# Simulate user running "kubectl rollout restart deployment/test-operator"
45+
# This adds a restart annotation to trigger a rolling restart
46+
When user adds restart annotation to "deployment/test-operator"
47+
# Trigger OLM reconciliation by updating a ClusterExtension annotation
48+
# This simulates the controller re-reconciling the deployment
49+
And ClusterExtension annotation "test-annotation" is set to "trigger-reconcile"
50+
And ClusterExtension has been reconciled the latest generation
51+
# The restart annotation should still be present - OLMv1 should not revert user changes
52+
Then resource "deployment/test-operator" has restart annotation

test/e2e/steps/steps.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ 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)
92+
sc.Step(`^(?i)ClusterExtension annotation "([^"]+)" is set to "([^"]+)"$`, ClusterExtensionAnnotationIsSet)
9093

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

11691172
return latest, nil
11701173
}
1174+
1175+
// UserAddsRestartAnnotation simulates a user running `kubectl rollout restart deployment/<name>`.
1176+
// This adds a restart annotation to the deployment's pod template to trigger a rolling restart.
1177+
// In OLMv0, this annotation would be reverted by the controller. In OLMv1 with Server-Side Apply,
1178+
// it should persist because the user (kubectl) manages this field, not the controller.
1179+
// See: https://github.com/operator-framework/operator-lifecycle-manager/issues/3392
1180+
func UserAddsRestartAnnotation(ctx context.Context, resourceName string) error {
1181+
sc := scenarioCtx(ctx)
1182+
1183+
// Parse the resource name (format: "deployment/test-operator")
1184+
parts := strings.Split(resourceName, "/")
1185+
if len(parts) != 2 {
1186+
return fmt.Errorf("invalid resource name format: %s (expected kind/name)", resourceName)
1187+
}
1188+
kind := parts[0]
1189+
1190+
if kind != "deployment" {
1191+
return fmt.Errorf("only deployment resources are supported for restart annotation, got: %s", kind)
1192+
}
1193+
1194+
// Use kubectl rollout restart to add the restart annotation
1195+
// This is the actual command users would run, ensuring we test real-world behavior
1196+
_, err := k8sClient("rollout", "restart", resourceName, "-n", sc.namespace)
1197+
if err != nil {
1198+
return fmt.Errorf("failed to rollout restart %s: %w", resourceName, err)
1199+
}
1200+
1201+
return nil
1202+
}
1203+
1204+
// ResourceHasRestartAnnotation verifies that a deployment has a restart annotation.
1205+
// This confirms that user-initiated changes persist after OLM reconciliation.
1206+
func ResourceHasRestartAnnotation(ctx context.Context, resourceName string) error {
1207+
sc := scenarioCtx(ctx)
1208+
1209+
// Parse the resource name (format: "deployment/test-operator")
1210+
parts := strings.Split(resourceName, "/")
1211+
if len(parts) != 2 {
1212+
return fmt.Errorf("invalid resource name format: %s (expected kind/name)", resourceName)
1213+
}
1214+
kind := parts[0]
1215+
deploymentName := parts[1]
1216+
1217+
if kind != "deployment" {
1218+
return fmt.Errorf("only deployment resources are supported for restart annotation check, got: %s", kind)
1219+
}
1220+
1221+
// Check for the restart annotation added by kubectl rollout restart
1222+
restartAnnotationKey := "kubectl.kubernetes.io/restartedAt"
1223+
1224+
waitFor(ctx, func() bool {
1225+
// Get the restart annotation from the deployment's pod template
1226+
out, err := k8sClient("get", "deployment", deploymentName, "-n", sc.namespace,
1227+
"-o", fmt.Sprintf("jsonpath={.spec.template.metadata.annotations['%s']}", restartAnnotationKey))
1228+
if err != nil {
1229+
return false
1230+
}
1231+
1232+
// If the annotation exists and has a value, it persisted
1233+
return out != ""
1234+
})
1235+
1236+
return nil
1237+
}
1238+
1239+
// ClusterExtensionAnnotationIsSet sets an annotation on the ClusterExtension to trigger reconciliation.
1240+
// This simulates changes that would cause the controller to re-apply managed resources.
1241+
func ClusterExtensionAnnotationIsSet(ctx context.Context, annotationKey, annotationValue string) error {
1242+
sc := scenarioCtx(ctx)
1243+
1244+
// Build the patch to add the annotation
1245+
patch := map[string]interface{}{
1246+
"metadata": map[string]interface{}{
1247+
"annotations": map[string]interface{}{
1248+
annotationKey: annotationValue,
1249+
},
1250+
},
1251+
}
1252+
1253+
patchBytes, err := json.Marshal(patch)
1254+
if err != nil {
1255+
return fmt.Errorf("failed to marshal annotation patch: %w", err)
1256+
}
1257+
1258+
// Apply the patch to add the annotation
1259+
_, err = k8sClient("patch", "clusterextension", sc.clusterExtensionName, "--type", "merge", "-p", string(patchBytes))
1260+
if err != nil {
1261+
return fmt.Errorf("failed to patch ClusterExtension with annotation: %w", err)
1262+
}
1263+
1264+
return nil
1265+
}

0 commit comments

Comments
 (0)