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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ require (
k8s.io/component-base v0.37.1
k8s.io/klog/v2 v2.140.0
k8s.io/kube-aggregator v0.37.1
k8s.io/kubectl v0.37.1
k8s.io/utils v0.0.0-20260626114624-be93311217bd
pkg.package-operator.run/boxcutter v0.15.0
sigs.k8s.io/controller-runtime v0.25.1
Expand Down Expand Up @@ -251,7 +252,6 @@ require (
k8s.io/code-generator v0.37.1 // indirect
k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
k8s.io/kubectl v0.37.1 // indirect
k8s.io/streaming v0.37.1 // indirect
oras.land/oras-go/v2 v2.6.2 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 // indirect
Expand Down
26 changes: 22 additions & 4 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,35 @@ sc.Step(`^ClusterExtension is applied$`, ResourceIsApplied)

**Step Implementation Pattern:**

Status assertions must read metadata and status from the same API response and
require the asserted condition's `observedGeneration` to equal `metadata.generation`.
Use `waitForCondition` for condition checks. Bundle metadata uses the `Installed`
condition, and COS phase digests and object counts use the `Ready` condition to
verify that the status reflects the current spec. Deployments instead expose
`status.observedGeneration` at the resource level.

After changing a spec, use a status assertion directly when checking its result;
a separate generation-only wait is redundant. Use
`ClusterExtension has been reconciled the latest generation` before assertions
on other cluster resources when you only need to confirm the spec was processed.
That step accepts any current-generation `Progressing` status or reason, whereas
`ClusterExtension is rolled out` requires `Progressing=True/Succeeded`.

```go
func BundleInstalled(ctx context.Context, name, version string) error {
sc := scenarioCtx(ctx)
waitFor(ctx, func() bool {
v, err := kubectl("get", "clusterextension", sc.clusterExtensionName, "-o", "jsonpath={.status.install.bundle}")
v, err := k8sClient(ctx, "get", "clusterextension", sc.clusterExtensionName, "-o", "json")
if err != nil {
return false
}
var bundle map[string]interface{}
json.Unmarshal([]byte(v), &bundle)
return bundle["name"] == name && bundle["version"] == version
var ext ocv1.ClusterExtension
if err := json.Unmarshal([]byte(v), &ext); err != nil {
return false
}
installed := currentCondition(ext.Generation, ext.Status.Conditions, ocv1.TypeInstalled)
return conditionMatches(installed, metav1.ConditionTrue, nil, nil) &&
ext.Status.Install.Bundle.Name == name && ext.Status.Install.Bundle.Version == version
})
return nil
}
Expand Down
1 change: 0 additions & 1 deletion test/e2e/features/recover.feature
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ Feature: Recover cluster extension from errors that might occur during its lifet
matchLabels:
"olm.operatorframework.io/metadata.name": ${CATALOG:test}
"""
And ClusterExtension latest generation has been reconciled
And ClusterExtension reports Progressing as True with Reason Succeeded
Then ClusterExtension is available
And ClusterExtension reports Installed as True
1 change: 0 additions & 1 deletion test/e2e/features/update.feature
Original file line number Diff line number Diff line change
Expand Up @@ -360,5 +360,4 @@ Feature: Update ClusterExtension
"olm.operatorframework.io/metadata.name": ${CATALOG:test}
"""
Then ClusterExtension apply does not emit warning
And ClusterExtension has been reconciled the latest generation
And ClusterExtension is available
51 changes: 51 additions & 0 deletions test/e2e/steps/conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package steps

import (
"context"
"encoding/json"
"fmt"

apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// resourceConditions reads metadata and conditions from the same API response so
// status assertions cannot accept a condition from an earlier spec generation.
type resourceConditions struct {
Metadata metav1.ObjectMeta `json:"metadata"`
Status struct {
Conditions []metav1.Condition `json:"conditions"`
} `json:"status"`
}

func readResourceConditions(ctx context.Context, resourceType, resourceName string) (*resourceConditions, error) {
out, err := k8sClient(ctx, "get", resourceType, resourceName, "-o", "json")
if err != nil {
return nil, fmt.Errorf("reading %s %q: %w", resourceType, resourceName, err)
}
var resource resourceConditions
if err := json.Unmarshal([]byte(out), &resource); err != nil {
return nil, fmt.Errorf("unmarshaling %s %q: %w", resourceType, resourceName, err)
}
return &resource, nil
}

func generationObserved(generation, observedGeneration int64) bool {
return generation > 0 && observedGeneration == generation
}

// currentCondition returns only the requested condition, and only if it reflects
// the current generation. An unrelated up-to-date condition is not sufficient.
func currentCondition(generation int64, conditions []metav1.Condition, conditionType string) *metav1.Condition {
condition := apimeta.FindStatusCondition(conditions, conditionType)
if condition == nil || !generationObserved(generation, condition.ObservedGeneration) {
return nil
}
return condition
}

func conditionMatches(condition *metav1.Condition, conditionStatus metav1.ConditionStatus, reason *string, msgCmp msgMatchFn) bool {
return condition != nil && condition.Status == conditionStatus &&
(reason == nil || condition.Reason == *reason) &&
(msgCmp == nil || msgCmp(condition.Message))
}
3 changes: 2 additions & 1 deletion test/e2e/steps/demo_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/cucumber/godog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func RegisterDemoSteps(sc *godog.ScenarioContext) {
Expand Down Expand Up @@ -64,7 +65,7 @@ func CatalogReportsConditionWithoutReason(ctx context.Context, catalogUserName,
}
catalogName = catalogUserName
}
err := waitForCondition(ctx, "clustercatalog", catalogName, conditionType, conditionStatus, nil, nil)
err := waitForCondition(ctx, "clustercatalog", catalogName, conditionType, metav1.ConditionStatus(conditionStatus), nil, nil)
if err == nil {
if rec := RecorderFromContext(ctx); rec != nil {
out, _ := k8sClient(ctx, "get", "clustercatalog", catalogName,
Expand Down
Loading
Loading