Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions test/e2e/features/update.feature
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ Feature: Update ClusterExtension
And bundle "${PACKAGE:test}.1.0.4" is installed in version "1.0.4"

@BoxcutterRuntime
@DeploymentConfig
Scenario: Detect collision when a second ClusterExtension installs the same package after an upgrade
Given ClusterExtension is applied
"""
Expand All @@ -251,7 +252,6 @@ Feature: Update ClusterExtension
Then ClusterExtension is rolled out
And ClusterExtension is available
And bundle "${PACKAGE:test}.1.0.1" is installed in version "1.0.1"
And the current ClusterExtension is tracked for cleanup
When ClusterExtension is applied
"""
apiVersion: olm.operatorframework.io/v1
Expand All @@ -271,10 +271,44 @@ Feature: Update ClusterExtension
"olm.operatorframework.io/metadata.name": ${CATALOG:test}
version: 1.0.1
"""
Then ClusterExtension reports Progressing as True with Reason Retrying and Message includes:
Then ClusterExtension "${NAME}-dup" reports Progressing as True with Reason Retrying and Message includes:
"""
revision object collisions
"""
And ClusterExtension "${NAME}" reports Installed as True
# Force a second revision on the dup via env var change — collision must persist
When ClusterExtension is updated
"""
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: ${NAME}-dup
spec:
namespace: ${TEST_NAMESPACE}
serviceAccount:
name: olm-sa
config:
configType: Inline
inline:
deploymentConfig:
env:
- name: MY_VAR
value: "my-value"
source:
sourceType: Catalog
catalog:
packageName: ${PACKAGE:test}
selector:
matchLabels:
"olm.operatorframework.io/metadata.name": ${CATALOG:test}
version: 1.0.1
"""
Then ClusterExtension "${NAME}-dup" owns 2 ClusterObjectSets
And ClusterExtension "${NAME}-dup" reports Progressing as True with Reason Retrying and Message includes:
"""
revision object collisions
"""
And ClusterExtension "${NAME}" reports Installed as True

@BoxcutterRuntime
Scenario: Each update creates a new revision and resources not present in the new revision are removed from the cluster
Expand Down
3 changes: 0 additions & 3 deletions test/e2e/steps/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,6 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context
}

forDeletion := sc.addedResources
if sc.clusterExtensionName != "" {
forDeletion = append(forDeletion, resource{name: sc.clusterExtensionName, kind: "clusterextension"})
}
Comment on lines -271 to -273

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT removing cluster extension? Will this cause issues for other tests?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added later in steps.go?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, ClusterExtensions are still cleaned up. ResourceIsApplied now adds every applied CE to sc.addedResources (line 476 in steps.go), which ScenarioCleanup iterates over for deletion (line 270). This removed block was redundant — it just added sc.clusterExtensionName a second time, resulting in a duplicate delete (harmless due to --ignore-not-found=true, but unnecessary).

if sc.clusterObjectSetName != "" && featureGates[features.BoxcutterRuntime] {
forDeletion = append(forDeletion, resource{name: sc.clusterObjectSetName, kind: "clusterobjectset"})
}
Expand Down
75 changes: 46 additions & 29 deletions test/e2e/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ func RegisterSteps(sc *godog.ScenarioContext) {

sc.Step(`^(?i)min value for (ClusterExtension|ClusterObjectSet) ((?:\.[a-zA-Z]+)+) is set to (\d+)$`, SetCRDFieldMinValue)

sc.Step(`^(?i)the current ClusterExtension is tracked for cleanup$`, TrackCurrentClusterExtensionForCleanup)
sc.Step(`^(?i)ClusterExtension "([^"]+)" owns (\d+) ClusterObjectSets?$`, ClusterExtensionOwnsClusterObjectSets)
sc.Step(`^(?i)ClusterExtension "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+)$`, NamedClusterExtensionReportsCondition)
sc.Step(`^(?i)ClusterExtension "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) and Message includes:$`, NamedClusterExtensionReportsConditionWithMessageFragment)

// TLS profile enforcement steps — deployment configuration
sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS minimum version "([^"]+)"$`, ConfigureDeploymentWithCustomTLSVersion)
Expand Down Expand Up @@ -382,17 +384,39 @@ func ResourceApplyFails(ctx context.Context, errMsg string, yamlTemplate *godog.
return nil
}

// TrackCurrentClusterExtensionForCleanup saves the current ClusterExtension name in the cleanup list
// so it gets deleted at the end of the scenario. Call this before applying a second ClusterExtension
// in the same scenario, because ResourceIsApplied overwrites the tracked name.
func TrackCurrentClusterExtensionForCleanup(ctx context.Context) error {
// ClusterExtensionOwnsClusterObjectSets waits for the named ClusterExtension to own exactly the
// expected number of ClusterObjectSets. Polls with timeout.
func ClusterExtensionOwnsClusterObjectSets(ctx context.Context, extName string, expectedCount int) error {
sc := scenarioCtx(ctx)
if sc.clusterExtensionName != "" {
sc.addedResources = append(sc.addedResources, resource{name: sc.clusterExtensionName, kind: "clusterextension"})
}
extName = substituteScenarioVars(extName, sc)
waitFor(ctx, func() bool {
out, err := k8sClient("get", "clusterobjectsets",
"-l", fmt.Sprintf("olm.operatorframework.io/owner-name=%s", extName),
"-o", "jsonpath={.items[*].metadata.name}")
if err != nil {
return false
}
names := strings.Fields(strings.TrimSpace(out))
return len(names) == expectedCount
})
return nil
}

// NamedClusterExtensionReportsCondition waits for a specific ClusterExtension (by name) to have a condition
// matching type and status. Polls with timeout.
func NamedClusterExtensionReportsCondition(ctx context.Context, extName, conditionType, conditionStatus string) error {
sc := scenarioCtx(ctx)
extName = substituteScenarioVars(extName, sc)
return waitForCondition(ctx, "clusterextension", extName, conditionType, conditionStatus, nil, nil)
}

// NamedClusterExtensionReportsConditionWithMessageFragment waits for a specific ClusterExtension (by name)
// to have a condition matching type, status, reason, with a message containing the specified fragment.
func NamedClusterExtensionReportsConditionWithMessageFragment(ctx context.Context, extName, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error {
extName = substituteScenarioVars(extName, scenarioCtx(ctx))
return waitForCondition(ctx, "clusterextension", extName, conditionType, conditionStatus, &conditionReason, messageFragmentComparison(ctx, msgFragment))
}

// ClusterExtensionVersionUpdate patches the ClusterExtension's catalog version to the specified value.
func ClusterExtensionVersionUpdate(ctx context.Context, version string) error {
sc := scenarioCtx(ctx)
Expand Down Expand Up @@ -449,7 +473,7 @@ func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error
return fmt.Errorf("failed to apply resource %v; err: %w; stderr: %s", out, err, stderrOutput(err))
}
if res.GetKind() == "ClusterExtension" {
sc.clusterExtensionName = res.GetName()
sc.addedResources = append(sc.addedResources, resource{name: res.GetName(), kind: "clusterextension"})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you still update sc.clusterExtensionName here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, removing this is intentional. sc.clusterExtensionName is initialized once in CreateScenarioContext as ce-<SCENARIO_ID>, and ${NAME} resolves to it. By not overwriting it on each apply, ${NAME} stays constant throughout the scenario. This lets multi-CE scenarios use ${NAME} for the original CE and ${NAME}-dup for the duplicate without the implicit pointer drifting to the last-applied CE.

The overwrite was always a no-op for existing tests since they all apply CEs with name: ${NAME}, which resolves to the same value sc.clusterExtensionName already holds. Unnamed steps like ClusterExtension is available continue to work because they read sc.clusterExtensionName, which is still set — just never changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a follow up I'll make the same changes to the COS handling

} else if res.GetKind() == "ClusterObjectSet" {
sc.clusterObjectSetName = res.GetName()
} else {
Expand Down Expand Up @@ -606,6 +630,16 @@ func messageComparison(ctx context.Context, msg *godog.DocString) msgMatchFn {
return msgCmp
}

func messageFragmentComparison(ctx context.Context, msgFragment *godog.DocString) msgMatchFn {
if msgFragment == nil {
return alwaysMatch
}
expectedFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx))
return func(actualMsg string) bool {
return strings.Contains(strings.Join(strings.Fields(actualMsg), " "), expectedFragment)
}
}

func waitForCondition(ctx context.Context, resourceType, resourceName, conditionType, conditionStatus string, conditionReason *string, msgCmp msgMatchFn) error {
require.Eventually(godog.T(ctx), func() bool {
v, err := k8sClient("get", resourceType, resourceName, "-o", fmt.Sprintf("jsonpath={.status.conditions[?(@.type==\"%s\")]}", conditionType))
Expand Down Expand Up @@ -646,15 +680,7 @@ func ClusterExtensionReportsCondition(ctx context.Context, conditionType, condit
// ClusterExtensionReportsConditionWithMessageFragment waits for the ClusterExtension to have a condition matching
// type, status, and reason, with a message containing the specified fragment. Polls with timeout.
func ClusterExtensionReportsConditionWithMessageFragment(ctx context.Context, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error {
msgCmp := alwaysMatch
if msgFragment != nil {
expectedMsgFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx))
msgCmp = func(actualMsg string) bool {
normalizedActual := strings.Join(strings.Fields(actualMsg), " ")
return strings.Contains(normalizedActual, expectedMsgFragment)
}
}
return waitForExtensionCondition(ctx, conditionType, conditionStatus, &conditionReason, msgCmp)
return waitForExtensionCondition(ctx, conditionType, conditionStatus, &conditionReason, messageFragmentComparison(ctx, msgFragment))
}

// ClusterExtensionReportsConditionWithoutMsg waits for the ClusterExtension to have a condition matching type,
Expand Down Expand Up @@ -743,15 +769,7 @@ func ClusterObjectSetReportsConditionWithMsg(ctx context.Context, revisionName,
// ClusterObjectSetReportsConditionWithMessageFragment waits for the named ClusterObjectSet to have a condition
// matching type, status, reason, with a message containing the specified fragment. Polls with timeout.
func ClusterObjectSetReportsConditionWithMessageFragment(ctx context.Context, revisionName, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error {
msgCmp := alwaysMatch
if msgFragment != nil {
expectedMsgFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx))
msgCmp = func(actualMsg string) bool {
normalizedActual := strings.Join(strings.Fields(actualMsg), " ")
return strings.Contains(normalizedActual, expectedMsgFragment)
}
}
return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, conditionStatus, &conditionReason, msgCmp)
return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, conditionStatus, &conditionReason, messageFragmentComparison(ctx, msgFragment))
}

// TriggerClusterObjectSetReconciliation annotates the named ClusterObjectSet
Expand Down Expand Up @@ -1870,10 +1888,9 @@ func templateContent(content string, values map[string]string) string {
if v, found := values[k]; found {
return v
}
return ""
return "${" + k + "}"
}

// Replace template variables
return os.Expand(content, m)
}

Expand Down
Loading