diff --git a/api/v1/common_types.go b/api/v1/common_types.go index 53215dbde..bd29b271f 100644 --- a/api/v1/common_types.go +++ b/api/v1/common_types.go @@ -21,7 +21,9 @@ const ( TypeProgressing = "Progressing" // Installed reasons - ReasonAbsent = "Absent" + ReasonAbsent = "Absent" + ReasonInstalling = "Installing" + ReasonUpgrading = "Upgrading" // Progressing reasons ReasonRollingOut = "RollingOut" diff --git a/internal/operator-controller/conditionsets/conditionsets.go b/internal/operator-controller/conditionsets/conditionsets.go index 97073a02d..812de85e5 100644 --- a/internal/operator-controller/conditionsets/conditionsets.go +++ b/internal/operator-controller/conditionsets/conditionsets.go @@ -43,6 +43,8 @@ var ConditionReasons = []string{ ocv1.ReasonInvalidConfiguration, ocv1.ReasonRetrying, ocv1.ReasonAbsent, + ocv1.ReasonInstalling, + ocv1.ReasonUpgrading, ocv1.ReasonRollingOut, ocv1.ReasonProgressDeadlineExceeded, } diff --git a/internal/operator-controller/controllers/common_controller.go b/internal/operator-controller/controllers/common_controller.go index cb6834c6b..1067a7728 100644 --- a/internal/operator-controller/controllers/common_controller.go +++ b/internal/operator-controller/controllers/common_controller.go @@ -57,38 +57,56 @@ func setInstalledStatusFromRevisionStates(ext *ocv1.ClusterExtension, revisionSt // Nothing is installed if revisionStates.Installed == nil { setInstallStatus(ext, nil) - reason := determineFailureReason(revisionStates.RollingOut) + reason := determineInstalledReason(revisionStates.RollingOut) setInstalledStatusConditionFalse(ext, reason, "No bundle installed") return } - // Something is installed + + // Something is installed - check if upgrade is in progress installStatus := &ocv1.ClusterExtensionInstallStatus{ Bundle: revisionStates.Installed.BundleMetadata, } setInstallStatus(ext, installStatus) + + if len(revisionStates.RollingOut) > 0 { + latestRevision := revisionStates.RollingOut[len(revisionStates.RollingOut)-1] + progressingCond := apimeta.FindStatusCondition(latestRevision.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + + if progressingCond != nil && progressingCond.Reason == string(ocv1.ReasonRollingOut) { + setInstalledStatusConditionUpgrading(ext, fmt.Sprintf("Upgrading from %s", revisionStates.Installed.Image)) + return + } + } + setInstalledStatusConditionSuccess(ext, fmt.Sprintf("Installed bundle %s successfully", revisionStates.Installed.Image)) } -// determineFailureReason determines the appropriate reason for the Installed condition +// determineInstalledReason determines the appropriate reason for the Installed condition // when no bundle is installed (Installed: False). // // Returns Failed when: // - No rolling revisions exist (nothing to install) // - The latest rolling revision has Reason: Retrying (indicates an error occurred) // +// Returns Installing when: +// - The latest rolling revision explicitly has Reason: RollingOut (healthy installation in progress) +// // Returns Absent when: -// - Rolling revisions exist with the latest having Reason: RollingOut (healthy phased rollout in progress) +// - Rolling revisions exist but have no conditions set (rollout just started) // // Rationale: // - Failed: Semantically indicates an error prevented installation -// - Absent: Semantically indicates "not there yet" (neutral state, e.g., during healthy rollout) +// - Installing: Semantically indicates a first-time installation is actively in progress +// - Absent: Neutral state when rollout exists but hasn't progressed enough to determine health // - Retrying reason indicates an error (config validation, apply failure, etc.) -// - RollingOut reason indicates healthy progress (not an error) +// - RollingOut reason indicates confirmed healthy progress // - Only the LATEST revision matters - old errors superseded by newer healthy revisions should not cause Failed // +// Note: This function is only called when Installed == nil (first-time installation scenario). // Note: rollingRevisions are sorted in ascending order by Spec.Revision (oldest to newest), -// so the latest revision is the LAST element in the array. -func determineFailureReason(rollingRevisions []*RevisionMetadata) string { +// +// so the latest revision is the LAST element in the array. +func determineInstalledReason(rollingRevisions []*RevisionMetadata) string { if len(rollingRevisions) == 0 { return ocv1.ReasonFailed } @@ -97,14 +115,21 @@ func determineFailureReason(rollingRevisions []*RevisionMetadata) string { // Latest revision is the last element in the array (sorted ascending by Spec.Revision) latestRevision := rollingRevisions[len(rollingRevisions)-1] progressingCond := apimeta.FindStatusCondition(latestRevision.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) - if progressingCond != nil && progressingCond.Reason == string(ocv1.ClusterExtensionRevisionReasonRetrying) { - // Retrying indicates an error occurred (config, apply, validation, etc.) - // Use Failed for semantic correctness: installation failed due to error - return ocv1.ReasonFailed + if progressingCond != nil { + if progressingCond.Reason == string(ocv1.ClusterExtensionRevisionReasonRetrying) { + // Retrying indicates an error occurred (config, apply, validation, etc.) + // Use Failed for semantic correctness: installation failed due to error + return ocv1.ReasonFailed + } + if progressingCond.Reason == string(ocv1.ReasonRollingOut) { + // RollingOut indicates healthy progress is confirmed + // Use Installing to communicate that a first-time installation is actively in progress + return ocv1.ReasonInstalling + } } - // No error detected in latest revision - it's progressing healthily (RollingOut) or no conditions set - // Use Absent for neutral "not installed yet" state + // No progressing condition or unknown reason - rollout just started or hasn't progressed + // Use Absent as neutral state return ocv1.ReasonAbsent } @@ -119,6 +144,17 @@ func setInstalledStatusConditionSuccess(ext *ocv1.ClusterExtension, message stri }) } +// setInstalledStatusConditionUpgrading sets the installed status condition to upgrading. +func setInstalledStatusConditionUpgrading(ext *ocv1.ClusterExtension, message string) { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonUpgrading, + Message: message, + ObservedGeneration: ext.GetGeneration(), + }) +} + // setInstalledStatusConditionFailed sets the installed status condition to failed. func setInstalledStatusConditionFalse(ext *ocv1.ClusterExtension, reason string, message string) { SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ diff --git a/internal/operator-controller/controllers/common_controller_test.go b/internal/operator-controller/controllers/common_controller_test.go index 3cc757543..d3925dc06 100644 --- a/internal/operator-controller/controllers/common_controller_test.go +++ b/internal/operator-controller/controllers/common_controller_test.go @@ -253,6 +253,259 @@ func TestSetStatusConditionWrapper(t *testing.T) { } } +func TestSetInstalledStatusFromRevisionStates_Upgrade(t *testing.T) { + tests := []struct { + name string + revisionStates *RevisionStates + expectedInstalledCond metav1.Condition + setupProgressingCond *metav1.Condition + expectedProgressingCond *metav1.Condition + }{ + { + // When operator v1 is running and v2 upgrade starts rolling out + // Then status shows Upgrading to inform users the new version is being deployed + name: "installed bundle with healthy upgrade in progress - uses Upgrading", + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "rev-1", + Image: "quay.io/example/bundle:v1.0.0", + BundleMetadata: ocv1.BundleMetadata{ + Name: "example.v1.0.0", + Version: "1.0.0", + }, + }, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-2", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonRollingOut, + Message: "Upgrading to v2.0.0", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonUpgrading, + Message: "Upgrading from quay.io/example/bundle:v1.0.0", + }, + }, + { + // When operator is installed and no upgrades are happening + // Then status shows Succeeded because everything is stable + name: "installed bundle with no rolling revisions - uses Succeeded", + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "rev-1", + Image: "quay.io/example/bundle:v1.0.0", + BundleMetadata: ocv1.BundleMetadata{ + Name: "example.v1.0.0", + Version: "1.0.0", + }, + }, + RollingOut: nil, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + }, + }, + { + // When operator v1 is running and v2 upgrade just started but health not confirmed yet + // Then status stays Succeeded because current version still works fine + name: "installed bundle with rolling revision not yet healthy - uses Succeeded", + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "rev-1", + Image: "quay.io/example/bundle:v1.0.0", + BundleMetadata: ocv1.BundleMetadata{ + Name: "example.v1.0.0", + Version: "1.0.0", + }, + }, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-2", + Conditions: []metav1.Condition{}, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + }, + }, + { + // When operator v1 is running but v2 upgrade fails with config errors + // Then status stays Succeeded because v1 keeps working even though v2 upgrade failed + name: "installed bundle with upgrade error (Retrying) - uses Succeeded", + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "rev-1", + Image: "quay.io/example/bundle:v1.0.0", + BundleMetadata: ocv1.BundleMetadata{ + Name: "example.v1.0.0", + Version: "1.0.0", + }, + }, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-2", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonRetrying, + Message: "Upgrade failed: invalid configuration", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + }, + }, + { + // When operator v1 is running but v2 upgrade has unknown state + // Then status stays Succeeded because v1 is still healthy regardless + name: "installed bundle with upgrade having unknown reason - uses Succeeded", + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "rev-1", + Image: "quay.io/example/bundle:v1.0.0", + BundleMetadata: ocv1.BundleMetadata{ + Name: "example.v1.0.0", + Version: "1.0.0", + }, + }, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-2", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: "SomeUnknownReason", + Message: "Unknown state", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + }, + }, + { + // When operator v1 is running but multiple upgrade attempts keep failing + // Then users see both conditions showing the complete picture: + // Installed: True, Reason: Succeeded - current v1 is working fine + // Progressing: True, Reason: Retrying - v2 upgrade is failing + name: "installed bundle with multiple rolling revisions including errors - uses Succeeded", + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "rev-1", + Image: "quay.io/example/bundle:v1.0.0", + BundleMetadata: ocv1.BundleMetadata{ + Name: "example.v1.0.0", + Version: "1.0.0", + }, + }, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-2", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonRetrying, + Message: "First upgrade attempt failed", + }, + }, + }, + { + RevisionName: "rev-3", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonRetrying, + Message: "Second upgrade attempt also failed", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + }, + setupProgressingCond: &metav1.Condition{ + Type: ocv1.TypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonRetrying, + Message: "Second upgrade attempt also failed", + }, + expectedProgressingCond: &metav1.Condition{ + Type: ocv1.TypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonRetrying, + Message: "Second upgrade attempt also failed", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ext", + Generation: 1, + }, + } + + // Setup Progressing condition if specified to simulate controller behavior + if tt.setupProgressingCond != nil { + SetStatusCondition(&ext.Status.Conditions, *tt.setupProgressingCond) + } + + setInstalledStatusFromRevisionStates(ext, tt.revisionStates) + + // Verify Installed condition + installedCond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, installedCond) + require.Equal(t, tt.expectedInstalledCond.Status, installedCond.Status) + require.Equal(t, tt.expectedInstalledCond.Reason, installedCond.Reason) + if tt.expectedInstalledCond.Message != "" { + require.Equal(t, tt.expectedInstalledCond.Message, installedCond.Message) + } + + // Verify Progressing condition if expected + // This verifies that setInstalledStatusFromRevisionStates does not modify Progressing + if tt.expectedProgressingCond != nil { + progressingCond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progressingCond) + require.Equal(t, tt.expectedProgressingCond.Status, progressingCond.Status) + require.Equal(t, tt.expectedProgressingCond.Reason, progressingCond.Reason) + require.Equal(t, tt.expectedProgressingCond.Message, progressingCond.Message) + } + }) + } +} + func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T) { tests := []struct { name string @@ -260,6 +513,8 @@ func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T expectedInstalledCond metav1.Condition }{ { + // When no operator is installed and no installation attempts exist + // Then status shows Failed because there is nothing to install name: "no revisions at all - uses Failed", revisionStates: &RevisionStates{ Installed: nil, @@ -272,6 +527,8 @@ func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T }, }, { + // When installing operator for first time but it fails with validation errors + // Then status shows Failed to indicate the installation did not succeed name: "rolling revision with error (Retrying) - uses Failed", revisionStates: &RevisionStates{ Installed: nil, @@ -296,6 +553,8 @@ func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T }, }, { + // When installing operator for first time with multiple attempts and latest one fails + // Then status shows Failed because the most recent attempt has errors name: "multiple rolling revisions with one Retrying - uses Failed", revisionStates: &RevisionStates{ Installed: nil, @@ -331,7 +590,9 @@ func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T }, }, { - name: "rolling revision with RollingOut reason - uses Absent", + // When installing operator for first time and rollout is actively progressing + // Then status shows Installing to inform users installation is in progress + name: "rolling revision with RollingOut reason - uses Installing", revisionStates: &RevisionStates{ Installed: nil, RollingOut: []*RevisionMetadata{ @@ -351,11 +612,13 @@ func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T expectedInstalledCond: metav1.Condition{ Type: ocv1.TypeInstalled, Status: metav1.ConditionFalse, - Reason: ocv1.ReasonAbsent, + Reason: ocv1.ReasonInstalling, }, }, { - name: "old revision with Retrying superseded by latest healthy - uses Absent", + // When first install attempt failed but user fixed config and second attempt is progressing + // Then status shows Installing because latest attempt is healthy + name: "old revision with Retrying superseded by latest healthy - uses Installing", revisionStates: &RevisionStates{ Installed: nil, RollingOut: []*RevisionMetadata{ @@ -383,6 +646,51 @@ func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T }, }, }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonInstalling, + }, + }, + { + // When installing operator but rollout just started with no status updates yet + // Then status shows Absent because we cannot determine health yet + name: "rolling revision with no conditions set - uses Absent", + revisionStates: &RevisionStates{ + Installed: nil, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-1", + Conditions: []metav1.Condition{}, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + }, + }, + { + // When installing operator but status has unexpected reason we do not recognize + // Then status shows Absent as neutral state for unknown conditions + name: "rolling revision with unknown reason - uses Absent", + revisionStates: &RevisionStates{ + Installed: nil, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-1", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: "SomeOtherReason", + Message: "Revision is in an unknown state", + }, + }, + }, + }, + }, expectedInstalledCond: metav1.Condition{ Type: ocv1.TypeInstalled, Status: metav1.ConditionFalse, diff --git a/test/e2e/features/install.feature b/test/e2e/features/install.feature index c16af88ef..a1103b622 100644 --- a/test/e2e/features/install.feature +++ b/test/e2e/features/install.feature @@ -359,6 +359,30 @@ Feature: Install ClusterExtension invalid ClusterExtension configuration: invalid configuration: unknown field "watchNamespace" """ + Scenario: Report Installing status during initial installation + When 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: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension eventually reports Installed as False with Reason Installing or has progressed + And ClusterExtension is rolled out + And ClusterExtension is available + And bundle "test-operator.1.2.0" is installed in version "1.2.0" + @BoxcutterRuntime @ProgressDeadline Scenario: Report ClusterExtension as not progressing if the rollout does not complete within given timeout diff --git a/test/e2e/features/update.feature b/test/e2e/features/update.feature index dee45e32a..5fa8dc941 100644 --- a/test/e2e/features/update.feature +++ b/test/e2e/features/update.feature @@ -31,7 +31,8 @@ Feature: Update ClusterExtension And ClusterExtension is rolled out And ClusterExtension is available When ClusterExtension is updated to version "1.0.1" - Then ClusterExtension is rolled out + Then ClusterExtension eventually reports Installed as True with Reason Upgrading or has progressed + And ClusterExtension is rolled out And ClusterExtension is available And bundle "test-operator.1.0.1" is installed in version "1.0.1" diff --git a/test/e2e/steps/steps.go b/test/e2e/steps/steps.go index d6ba67ccf..024fd05bc 100644 --- a/test/e2e/steps/steps.go +++ b/test/e2e/steps/steps.go @@ -69,6 +69,7 @@ func RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) and Message:$`, ClusterExtensionReportsCondition) sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) and Message includes:$`, ClusterExtensionReportsConditionWithMessageFragment) sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+)$`, ClusterExtensionReportsConditionWithoutMsg) + sc.Step(`^(?i)ClusterExtension eventually reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) or has progressed$`, ClusterExtensionEventuallyReportsConditionOrProgressed) sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+)$`, ClusterExtensionReportsConditionWithoutReason) sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+)$`, ClusterExtensionRevisionReportsConditionWithoutMsg) sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) transition between (\d+) and (\d+) minutes since its creation$`, ClusterExtensionReportsConditionTransitionTime) @@ -423,6 +424,93 @@ func ClusterExtensionReportsConditionWithoutReason(ctx context.Context, conditio return waitForExtensionCondition(ctx, conditionType, conditionStatus, nil, nil) } +// ClusterExtensionEventuallyReportsConditionOrProgressed checks if a ClusterExtension ever reports +// a specific condition with a specific reason, OR has already progressed past that transient state. +// This is useful for testing transient states like "Installing" or "Upgrading" that may complete +// very quickly before the test can observe them. +// +// For Installing (Installed=False, Reason=Installing): accepts Installed=False with Reason=Installing OR Installed=True with Reason=Succeeded. +// For Upgrading (Installed=True, Reason=Upgrading): accepts Installed=True with Reason=Upgrading OR Installed=True with Reason=Succeeded. +// For any other condition reason (or when the condition does not match one of the transient patterns +// above), this helper behaves like ClusterExtensionReportsConditionWithoutMsg and requires an exact match on +// type, status, and reason via waitForExtensionCondition. +func ClusterExtensionEventuallyReportsConditionOrProgressed(ctx context.Context, conditionType, conditionStatus, conditionReason string) error { + sc := scenarioCtx(ctx) + + // Map of transient reasons to their progressed states with expected status + type progressedState struct { + reason string + status string + } + + progressedStates := map[string][]progressedState{ + "Installing": {{reason: "Succeeded", status: "True"}}, // Installing (False) -> Succeeded (True) + "Upgrading": {{reason: "Succeeded", status: "True"}}, // Upgrading (True) -> Succeeded (True) + } + + acceptedProgressed, isTransient := progressedStates[conditionReason] + if !isTransient { + // Not a transient state, use normal exact match + return waitForExtensionCondition(ctx, conditionType, conditionStatus, &conditionReason, nil) + } + + // For transient states, we accept either the target reason OR any progressed reason + require.Eventually(godog.T(ctx), func() bool { + v, err := k8sClient("get", "clusterextension", sc.clusterExtensionName, "-o", fmt.Sprintf("jsonpath={.status.conditions[?(@.type==\"%s\")]}", conditionType)) + if err != nil { + logger.V(1).Info("Failed to get clusterextension", "name", sc.clusterExtensionName, "error", err) + return false + } + + if v == "" { + logger.V(1).Info("No condition found yet", "conditionType", conditionType) + return false + } + + var condition metav1.Condition + if err := json.Unmarshal([]byte(v), &condition); err != nil { + logger.V(1).Info("Failed to unmarshal condition", "error", err, "value", v) + return false + } + + logger.V(1).Info("Checking condition", + "conditionType", conditionType, + "status", condition.Status, + "reason", condition.Reason, + "expectedStatus", conditionStatus, + "expectedReason", conditionReason) + + // Check if we have the target reason with expected status (transient state) + if condition.Status == metav1.ConditionStatus(conditionStatus) && condition.Reason == conditionReason { + logger.V(1).Info("Found transient state", + "conditionType", conditionType, + "status", condition.Status, + "reason", condition.Reason) + return true + } + + // Check if we've progressed past the transient state + for _, progressed := range acceptedProgressed { + if condition.Status == metav1.ConditionStatus(progressed.status) && condition.Reason == progressed.reason { + logger.V(1).Info("State has progressed past transient state", + "conditionType", conditionType, + "expectedTransientStatus", conditionStatus, + "expectedTransientReason", conditionReason, + "actualStatus", condition.Status, + "actualReason", condition.Reason) + return true + } + } + + logger.V(1).Info("Condition doesn't match expected or progressed states", + "actualStatus", condition.Status, + "actualReason", condition.Reason) + return false + }, timeout, tick) + + return nil +} + func ClusterExtensionReportsConditionTransitionTime(ctx context.Context, conditionType string, minMinutes, maxMinutes int) error { sc := scenarioCtx(ctx) t := godog.T(ctx)