Skip to content
Open
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
90 changes: 90 additions & 0 deletions test/e2e/features/update.feature
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,93 @@ Feature: Update ClusterExtension
Then ClusterExtension reports "${NAME}-1, ${NAME}-2" as active revisions
And ClusterObjectSet "${NAME}-2" reports Progressing as True with Reason RollingOut
And ClusterObjectSet "${NAME}-2" reports Available as False with Reason ProbeFailure

@BoxcutterRuntime
@DeploymentConfig
Scenario: Cannot install a ClusterExtension that refers to an already installed bundle
Given ClusterExtension is applied
"""
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: ${NAME}
spec:
namespace: ${TEST_NAMESPACE}
serviceAccount:
name: olm-sa
source:
sourceType: Catalog
catalog:
packageName: ${PACKAGE:test}
version: 1.0.0
selector:
matchLabels:
"olm.operatorframework.io/metadata.name": ${CATALOG:test}
"""
And ClusterExtension is rolled out
And ClusterExtension is available
And ClusterExtension "${NAME}" owns 1 ClusterObjectSet
And the current ClusterExtension is tracked for cleanup
When ClusterExtension is applied
"""
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: ${NAME}-dup
spec:
namespace: ${TEST_NAMESPACE}
serviceAccount:
name: olm-sa
source:
sourceType: Catalog
catalog:
packageName: ${PACKAGE:test}
version: 1.0.0
selector:
matchLabels:
"olm.operatorframework.io/metadata.name": ${CATALOG:test}
"""
Then ClusterExtension "${NAME}" reports Progressing as True with Reason Retrying and Message includes:
"""
revision object collisions
"""
And ClusterExtension "${PREV_NAME}" reports Installed as True
# Update the conflicting ClusterExtension with a deployment config env var. At the time
# of writing, there is no other way to get the operator controller to generate another
# ClusterObjectSet by simply targeting another bundle version in the ClusterExtension.
# Since nothing has been installed yet for this CE, the resolver returns the currently
# installed bundle. Injecting an environment variable via DeploymentConfig changes the
# desired state, which causes the controller to create a second ClusterObjectSet
# (revision 2) after the first one collides.
When ClusterExtension is updated
"""
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: ${NAME}
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}
version: 1.0.0
selector:
matchLabels:
"olm.operatorframework.io/metadata.name": ${CATALOG:test}
"""
Then ClusterExtension "${NAME}" owns 2 ClusterObjectSets
And ClusterExtension "${NAME}" reports Progressing as True with Reason Retrying and Message includes:
"""
revision object collisions
"""
And ClusterExtension "${PREV_NAME}" reports Installed as True
31 changes: 16 additions & 15 deletions test/e2e/steps/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,22 @@ type deploymentRestore struct {
}

type scenarioContext struct {
id string
featureName string
scenarioName string
namespace string
clusterExtensionName string
clusterObjectSetName string
catalogs map[string]string // user-chosen name -> ClusterCatalog resource name
catalogPackageNames map[string]string // original package name -> parameterized name
addedResources []resource
removedResources []unstructured.Unstructured
metricsResponse map[string]string
leaderPods map[string]string // component name -> leader pod name
deploymentRestores []deploymentRestore
extensionObjects []client.Object
proxy *recordingProxy
id string
featureName string
scenarioName string
namespace string
clusterExtensionName string
previousClusterExtensionName string
clusterObjectSetName string
catalogs map[string]string // user-chosen name -> ClusterCatalog resource name
catalogPackageNames map[string]string // original package name -> parameterized name
addedResources []resource
removedResources []unstructured.Unstructured
metricsResponse map[string]string
leaderPods map[string]string // component name -> leader pod name
deploymentRestores []deploymentRestore
extensionObjects []client.Object
proxy *recordingProxy
}

// GatherClusterExtensionObjects collects all resources related to the ClusterExtension container in
Expand Down
81 changes: 59 additions & 22 deletions test/e2e/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,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 @@ -352,6 +355,9 @@ func substituteScenarioVars(content string, sc *scenarioContext) string {
"SCENARIO_ID": sc.id,
"OLM_NAMESPACE": olmNamespace,
}
if sc.previousClusterExtensionName != "" {
vars["PREV_NAME"] = sc.previousClusterExtensionName
}
for orig, param := range sc.catalogPackageNames {
vars[fmt.Sprintf("PACKAGE:%s", orig)] = param
}
Expand Down Expand Up @@ -387,12 +393,50 @@ func ResourceApplyFails(ctx context.Context, errMsg string, yamlTemplate *godog.
// in the same scenario, because ResourceIsApplied overwrites the tracked name.
func TrackCurrentClusterExtensionForCleanup(ctx context.Context) error {
sc := scenarioCtx(ctx)
if sc.clusterExtensionName != "" {
sc.addedResources = append(sc.addedResources, resource{name: sc.clusterExtensionName, kind: "clusterextension"})
if sc.clusterExtensionName == "" {
return nil
}
if sc.previousClusterExtensionName != "" {
return fmt.Errorf("previousClusterExtensionName already set to %q; tracking more than one previous ClusterExtension is not supported", sc.previousClusterExtensionName)
}
sc.previousClusterExtensionName = sc.clusterExtensionName
sc.addedResources = append(sc.addedResources, resource{name: sc.clusterExtensionName, kind: "clusterextension"})
return nil
}

// 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)
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 @@ -606,6 +650,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 +700,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 +789,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 +1908,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