From e3849c7ffaeb41b8d86fb4ab67dab6b72639fa72 Mon Sep 17 00:00:00 2001 From: Camila Macedo <7708031+camilamacedo86@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:03:34 +0000 Subject: [PATCH 1/2] Fix deprecation conditions leaking install errors - stop copying install/validation errors into deprecation conditions - base bundle deprecation on the installed bundle (Unknown when none) - extend unit tests to cover resolver failures with catalog deprecations Assisted-by: Cursor --- .../clusterextension_admission_test.go | 28 ++- .../clusterextension_controller.go | 233 ++++++++++++------ .../clusterextension_controller_test.go | 207 +++++++++++++++- .../controllers/common_controller_test.go | 2 +- test/e2e/cluster_extension_install_test.go | 24 +- 5 files changed, 389 insertions(+), 105 deletions(-) diff --git a/internal/operator-controller/controllers/clusterextension_admission_test.go b/internal/operator-controller/controllers/clusterextension_admission_test.go index 38c6c60d41..8b572dbe8c 100644 --- a/internal/operator-controller/controllers/clusterextension_admission_test.go +++ b/internal/operator-controller/controllers/clusterextension_admission_test.go @@ -13,7 +13,7 @@ import ( ) func TestClusterExtensionSourceConfig(t *testing.T) { - sourceTypeEmptyError := "Invalid value: null" + sourceTypeEmptyErrors := []string{"Invalid value: \"null\"", "Invalid value: null"} sourceTypeMismatchError := "spec.source.sourceType: Unsupported value" sourceConfigInvalidError := "spec.source: Invalid value" // unionField represents the required Catalog or (future) Bundle field required by SourceConfig @@ -21,12 +21,12 @@ func TestClusterExtensionSourceConfig(t *testing.T) { name string sourceType string unionField string - errMsg string + errMsgs []string }{ - {"sourceType is null", "", "Catalog", sourceTypeEmptyError}, - {"sourceType is invalid", "Invalid", "Catalog", sourceTypeMismatchError}, - {"catalog field does not exist", "Catalog", "", sourceConfigInvalidError}, - {"sourceConfig has required fields", "Catalog", "Catalog", ""}, + {"sourceType is null", "", "Catalog", sourceTypeEmptyErrors}, + {"sourceType is invalid", "Invalid", "Catalog", []string{sourceTypeMismatchError}}, + {"catalog field does not exist", "Catalog", "", []string{sourceConfigInvalidError}}, + {"sourceConfig has required fields", "Catalog", "Catalog", nil}, } t.Parallel() @@ -62,12 +62,20 @@ func TestClusterExtensionSourceConfig(t *testing.T) { })) } - if tc.errMsg == "" { + if len(tc.errMsgs) == 0 { require.NoError(t, err, "unexpected error for sourceType %q: %w", tc.sourceType, err) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) + return + } + + require.Error(t, err) + matched := false + for _, msg := range tc.errMsgs { + if strings.Contains(err.Error(), msg) { + matched = true + break + } } + require.True(t, matched, "expected one of %v in error %q", tc.errMsgs, err) }) } } diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index 7bcedde656..2e527874d4 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -25,6 +25,7 @@ import ( "slices" "strings" + bsemver "github.com/blang/semver/v4" "github.com/go-logr/logr" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" @@ -139,13 +140,16 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req return res, reconcileErr } -// ensureAllConditionsWithReason checks that all defined condition types exist in the given ClusterExtension, -// and assigns a specified reason and custom message to any missing condition. -func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.ConditionReason, message string) { +// ensureFailureConditionsWithReason keeps every non-deprecation condition present. +// If one is missing, we add it with the given reason and message so users see why +// reconcile failed. Deprecation conditions are handled later by SetDeprecationStatus. +func ensureFailureConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.ConditionReason, message string) { for _, condType := range conditionsets.ConditionTypes { + if isDeprecationCondition(condType) { + continue + } cond := apimeta.FindStatusCondition(ext.Status.Conditions, condType) if cond == nil { - // Create a new condition with a valid reason and add it SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: condType, Status: metav1.ConditionFalse, @@ -157,6 +161,17 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C } } +// isDeprecationCondition reports whether the given type is one of the deprecation +// conditions we manage separately. +func isDeprecationCondition(condType string) bool { + switch condType { + case ocv1.TypeDeprecated, ocv1.TypePackageDeprecated, ocv1.TypeChannelDeprecated, ocv1.TypeBundleDeprecated: + return true + default: + return false + } +} + // Compare resources - ignoring status & metadata.finalizers func checkForUnexpectedClusterExtensionFieldChange(a, b ocv1.ClusterExtension) bool { a.Status, b.Status = ocv1.ClusterExtensionStatus{}, ocv1.ClusterExtensionStatus{} @@ -229,6 +244,19 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl return ctrl.Result{}, err } + // Hold deprecation updates until the end. That way: + // * if nothing installs, BundleDeprecated stays Unknown/Absent + // * if a bundle installs, we report its real deprecation status + // * install errors never leak into the deprecation conditions + var resolvedDeprecation *declcfg.Deprecation + defer func() { + installedBundleName := "" + if revisionStates != nil && revisionStates.Installed != nil { + installedBundleName = revisionStates.Installed.Name + } + SetDeprecationStatus(ext, installedBundleName, resolvedDeprecation) + }() + var resolvedRevisionMetadata *RevisionMetadata if len(revisionStates.RollingOut) == 0 { l.Info("resolving bundle") @@ -236,30 +264,21 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl if revisionStates.Installed != nil { bm = &revisionStates.Installed.BundleMetadata } - resolvedBundle, resolvedBundleVersion, resolvedDeprecation, err := r.Resolver.Resolve(ctx, ext, bm) + var resolvedBundle *declcfg.Bundle + var resolvedBundleVersion *bsemver.Version + resolvedBundle, resolvedBundleVersion, resolvedDeprecation, err = r.Resolver.Resolve(ctx, ext, bm) + // Keep any deprecation data the resolver returned. The deferred update will use it + // even if installation later fails or never begins. if err != nil { // Note: We don't distinguish between resolution-specific errors and generic errors setStatusProgressing(ext, err) setInstalledStatusFromRevisionStates(ext, revisionStates) - ensureAllConditionsWithReason(ext, ocv1.ReasonFailed, err.Error()) + // Ensure non-deprecation conditions capture the failure immediately. The deferred + // SetDeprecationStatus call is responsible for updating the deprecation conditions + // based on any catalog data returned by the resolver. + ensureFailureConditionsWithReason(ext, ocv1.ReasonFailed, err.Error()) return ctrl.Result{}, err } - - // set deprecation status after _successful_ resolution - // TODO: - // 1. It seems like deprecation status should reflect the currently installed bundle, not the resolved - // bundle. So perhaps we should set package and channel deprecations directly after resolution, but - // defer setting the bundle deprecation until we successfully install the bundle. - // 2. If resolution fails because it can't find a bundle, that doesn't mean we wouldn't be able to find - // a deprecation for the ClusterExtension's spec.packageName. Perhaps we should check for a non-nil - // resolvedDeprecation even if resolution returns an error. If present, we can still update some of - // our deprecation status. - // - Open question though: what if different catalogs have different opinions of what's deprecated. - // If we can't resolve a bundle, how do we know which catalog to trust for deprecation information? - // Perhaps if the package shows up in multiple catalogs and deprecations don't match, we can set - // the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from - // all catalogs? - SetDeprecationStatus(ext, resolvedBundle.Name, resolvedDeprecation) resolvedRevisionMetadata = &RevisionMetadata{ Package: resolvedBundle.Package, Image: resolvedBundle.Image, @@ -326,83 +345,141 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl return ctrl.Result{}, nil } -// SetDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension -// based on the provided bundle -func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, deprecation *declcfg.Deprecation) { - deprecations := map[string][]declcfg.DeprecationEntry{} - channelSet := sets.New[string]() +// DeprecationInfo captures the deprecation data needed to update condition status. +type DeprecationInfo struct { + PackageEntries []declcfg.DeprecationEntry + ChannelEntries []declcfg.DeprecationEntry + BundleEntries []declcfg.DeprecationEntry + BundleStatus metav1.ConditionStatus +} + +// SetDeprecationStatus updates the ClusterExtension deprecation conditions using the +// catalog data from resolve plus the name of the bundle that actually landed. Examples: +// - no bundle installed -> bundle status stays Unknown/Absent +// - installed bundle marked deprecated -> bundle status True/Deprecated +// - installed bundle not deprecated -> bundle status False/Deprecated +// +// This keeps the deprecation conditions focused on catalog information: +// - PackageDeprecated: true if the catalog marks the package deprecated +// - ChannelDeprecated: true if any requested channel is marked deprecated +// - BundleDeprecated: reflects the installed bundle (Unknown/Absent when nothing installed) +// - Deprecated (rollup): true if any of the above signals a deprecation +// +// Install or validation errors never appear here because they belong on the +// Progressing/Installed conditions instead. Callers should invoke this after reconcile +// finishes (for example via a defer) so catalog data replaces any transient error messages. +func SetDeprecationStatus(ext *ocv1.ClusterExtension, installedBundleName string, deprecation *declcfg.Deprecation) { + info := buildDeprecationInfo(ext, installedBundleName, deprecation) + + packageMessages := collectDeprecationMessages(info.PackageEntries) + channelMessages := collectDeprecationMessages(info.ChannelEntries) + bundleMessages := collectDeprecationMessages(info.BundleEntries) + + messages := slices.Concat(packageMessages, channelMessages) + messages = slices.Concat(messages, bundleMessages) + + status := metav1.ConditionFalse + if len(messages) > 0 { + status = metav1.ConditionTrue + } + + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeDeprecated, + Status: status, + Reason: ocv1.ReasonDeprecated, + Message: strings.Join(messages, "\n"), + ObservedGeneration: ext.GetGeneration(), + }) + + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypePackageDeprecated, + Status: conditionStatus(len(packageMessages) > 0), + Reason: ocv1.ReasonDeprecated, + Message: strings.Join(packageMessages, "\n"), + ObservedGeneration: ext.GetGeneration(), + }) + + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeChannelDeprecated, + Status: conditionStatus(len(channelMessages) > 0), + Reason: ocv1.ReasonDeprecated, + Message: strings.Join(channelMessages, "\n"), + ObservedGeneration: ext.GetGeneration(), + }) + + bundleReason := ocv1.ReasonDeprecated + bundleMessage := strings.Join(bundleMessages, "\n") + if info.BundleStatus == metav1.ConditionUnknown { + bundleReason = ocv1.ReasonAbsent + bundleMessage = "" + } + + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeBundleDeprecated, + Status: info.BundleStatus, + Reason: bundleReason, + Message: bundleMessage, + ObservedGeneration: ext.GetGeneration(), + }) +} + +// buildDeprecationInfo filters the catalog deprecation data down to the package, channel, +// and bundle entries that matter for this ClusterExtension. An empty bundle name means +// nothing is installed yet, so we leave bundle status Unknown/Absent. +func buildDeprecationInfo(ext *ocv1.ClusterExtension, installedBundleName string, deprecation *declcfg.Deprecation) DeprecationInfo { + info := DeprecationInfo{BundleStatus: metav1.ConditionUnknown} + var channelSet sets.Set[string] if ext.Spec.Source.Catalog != nil { - for _, channel := range ext.Spec.Source.Catalog.Channels { - channelSet.Insert(channel) - } + channelSet = sets.New(ext.Spec.Source.Catalog.Channels...) + } else { + channelSet = sets.New[string]() } + if deprecation != nil { for _, entry := range deprecation.Entries { switch entry.Reference.Schema { case declcfg.SchemaPackage: - deprecations[ocv1.TypePackageDeprecated] = []declcfg.DeprecationEntry{entry} + info.PackageEntries = append(info.PackageEntries, entry) case declcfg.SchemaChannel: if channelSet.Has(entry.Reference.Name) { - deprecations[ocv1.TypeChannelDeprecated] = append(deprecations[ocv1.TypeChannelDeprecated], entry) + info.ChannelEntries = append(info.ChannelEntries, entry) } case declcfg.SchemaBundle: - if bundleName != entry.Reference.Name { - continue + if installedBundleName != "" && entry.Reference.Name == installedBundleName { + info.BundleEntries = append(info.BundleEntries, entry) } - deprecations[ocv1.TypeBundleDeprecated] = []declcfg.DeprecationEntry{entry} } } } - // first get ordered deprecation messages that we'll join in the Deprecated condition message - var deprecationMessages []string - for _, conditionType := range []string{ - ocv1.TypePackageDeprecated, - ocv1.TypeChannelDeprecated, - ocv1.TypeBundleDeprecated, - } { - if entries, ok := deprecations[conditionType]; ok { - for _, entry := range entries { - deprecationMessages = append(deprecationMessages, entry.Message) - } + // installedBundleName is empty when nothing is installed. In that case we want + // to report the bundle deprecation condition as Unknown/Absent. + if installedBundleName != "" { + if len(info.BundleEntries) > 0 { + info.BundleStatus = metav1.ConditionTrue + } else { + info.BundleStatus = metav1.ConditionFalse } } - // next, set the Deprecated condition - status, reason, message := metav1.ConditionFalse, ocv1.ReasonDeprecated, "" - if len(deprecationMessages) > 0 { - status, reason, message = metav1.ConditionTrue, ocv1.ReasonDeprecated, strings.Join(deprecationMessages, ";") - } - SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: ocv1.TypeDeprecated, - Reason: reason, - Status: status, - Message: message, - ObservedGeneration: ext.Generation, - }) + return info +} - // finally, set the individual deprecation conditions for package, channel, and bundle - for _, conditionType := range []string{ - ocv1.TypePackageDeprecated, - ocv1.TypeChannelDeprecated, - ocv1.TypeBundleDeprecated, - } { - entries, ok := deprecations[conditionType] - status, reason, message := metav1.ConditionFalse, ocv1.ReasonDeprecated, "" - if ok { - status, reason = metav1.ConditionTrue, ocv1.ReasonDeprecated - for _, entry := range entries { - message = fmt.Sprintf("%s\n%s", message, entry.Message) - } +func collectDeprecationMessages(entries []declcfg.DeprecationEntry) []string { + messages := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.Message != "" { + messages = append(messages, entry.Message) } - SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: conditionType, - Reason: reason, - Status: status, - Message: message, - ObservedGeneration: ext.Generation, - }) } + return messages +} + +func conditionStatus(ok bool) metav1.ConditionStatus { + if ok { + return metav1.ConditionTrue + } + return metav1.ConditionFalse } type ControllerBuilderOption func(builder *ctrl.Builder) diff --git a/internal/operator-controller/controllers/clusterextension_controller_test.go b/internal/operator-controller/controllers/clusterextension_controller_test.go index 437f62dcec..a84f7329d4 100644 --- a/internal/operator-controller/controllers/clusterextension_controller_test.go +++ b/internal/operator-controller/controllers/clusterextension_controller_test.go @@ -31,6 +31,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" "github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets" "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" + "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/operator-controller/finalizers" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" @@ -172,6 +173,58 @@ func TestClusterExtensionResolutionFails(t *testing.T) { require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) } +func TestClusterExtensionResolutionFailsWithDeprecationData(t *testing.T) { + ctx := context.Background() + cl, reconciler := newClientAndReconciler(t) + deprecationMessage := "package marked deprecated in catalog" + pkgName := fmt.Sprintf("deprecated-%s", rand.String(6)) + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { + return nil, nil, &declcfg.Deprecation{ + Entries: []declcfg.DeprecationEntry{{ + Reference: declcfg.PackageScopedReference{Schema: declcfg.SchemaPackage}, + Message: deprecationMessage, + }}, + }, fmt.Errorf("no package %q found", pkgName) + }) + + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{PackageName: pkgName}, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{Name: "default"}, + }, + } + require.NoError(t, cl.Create(ctx, clusterExtension)) + + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + require.EqualError(t, err, fmt.Sprintf("no package %q found", pkgName)) + + require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) + + pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, pkgCond) + require.Equal(t, metav1.ConditionTrue, pkgCond.Status) + require.Equal(t, deprecationMessage, pkgCond.Message) + + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionTrue, deprecatedCond.Status) + + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status, "no bundle installed yet, so keep it Unknown/Absent") + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + + verifyInvariants(ctx, t, reconciler.Client, clusterExtension) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) +} + func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { type testCase struct { name string @@ -264,6 +317,24 @@ func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { require.Equal(t, expectReason, progressingCond.Reason) require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version)) + t.Log("By checking deprecation conditions remain neutral and bundle is Unknown when not installed") + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) + require.Equal(t, ocv1.ReasonDeprecated, deprecatedCond.Reason) + pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, pkgCond) + require.Equal(t, metav1.ConditionFalse, pkgCond.Status) + require.Equal(t, ocv1.ReasonDeprecated, pkgCond.Reason) + chanCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) + require.NotNil(t, chanCond) + require.Equal(t, metav1.ConditionFalse, chanCond.Status) + require.Equal(t, ocv1.ReasonDeprecated, chanCond.Reason) + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status) + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) }) } @@ -343,6 +414,118 @@ func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T) require.Equal(t, ocv1.ReasonRetrying, progressingCond.Reason) require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version)) + t.Log("By checking deprecation conditions remain neutral and bundle is Unknown when not installed") + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) + require.Equal(t, ocv1.ReasonDeprecated, deprecatedCond.Reason) + pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, pkgCond) + require.Equal(t, metav1.ConditionFalse, pkgCond.Status) + require.Equal(t, ocv1.ReasonDeprecated, pkgCond.Reason) + chanCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) + require.NotNil(t, chanCond) + require.Equal(t, metav1.ConditionFalse, chanCond.Status) + require.Equal(t, ocv1.ReasonDeprecated, chanCond.Reason) + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status) + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) +} + +func TestClusterExtensionBoxcutterApplierFailsDoesNotLeakDeprecationErrors(t *testing.T) { + require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=true", features.BoxcutterRuntime))) + t.Cleanup(func() { + require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=false", features.BoxcutterRuntime))) + }) + + cl, reconciler := newClientAndReconciler(t) + // Boxcutter keeps a rolling revision when apply fails. We mirror that state so the test uses + // the same inputs the runtime would see. + reconciler.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + RollingOut: []*controllers.RevisionMetadata{{}}, + }, + } + reconciler.ImagePuller = &imageutil.MockPuller{ImageFS: fstest.MapFS{}} + + ctx := context.Background() + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + + t.Log("When the Boxcutter Feature Flag is enabled and apply fails") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "prometheus", + Version: "1.0.0", + Channels: []string{"beta"}, + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "default", + }, + }, + } + require.NoError(t, cl.Create(ctx, clusterExtension)) + + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { + v := bsemver.MustParse("1.0.0") + return &declcfg.Bundle{ + Name: "prometheus.v1.0.0", + Package: "prometheus", + Image: "quay.io/operatorhubio/prometheus@fake1.0.0", + }, &v, nil, nil + }) + reconciler.Applier = &MockApplier{err: errors.New("boxcutter apply failure")} + + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + require.Error(t, err) + + require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) + + installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, installedCond) + require.Equal(t, metav1.ConditionFalse, installedCond.Status) + require.Equal(t, ocv1.ReasonAbsent, installedCond.Reason) + require.Contains(t, installedCond.Message, "No bundle installed") + + progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progressingCond) + require.Equal(t, metav1.ConditionTrue, progressingCond.Status) + require.Equal(t, ocv1.ReasonRetrying, progressingCond.Reason) + require.Contains(t, progressingCond.Message, "boxcutter apply failure") + + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) + require.Equal(t, ocv1.ReasonDeprecated, deprecatedCond.Reason) + require.Empty(t, deprecatedCond.Message) + + packageCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, packageCond) + require.Equal(t, metav1.ConditionFalse, packageCond.Status, "catalog said nothing about the package, so stay False") + require.Equal(t, ocv1.ReasonDeprecated, packageCond.Reason) + require.Empty(t, packageCond.Message) + + channelCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) + require.NotNil(t, channelCond) + require.Equal(t, metav1.ConditionFalse, channelCond.Status, "channel also has no deprecation info") + require.Equal(t, ocv1.ReasonDeprecated, channelCond.Reason) + require.Empty(t, channelCond.Message) + + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status, "apply failed before install, so bundle status stays Unknown/Absent") + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + require.Empty(t, bundleCond.Message) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) } @@ -887,8 +1070,8 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, ObservedGeneration: 1, }, }, @@ -945,8 +1128,8 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, ObservedGeneration: 1, }, }, @@ -1014,8 +1197,8 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, ObservedGeneration: 1, }, }, @@ -1085,8 +1268,8 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, ObservedGeneration: 1, }, }, @@ -1321,8 +1504,8 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, ObservedGeneration: 1, }, }, @@ -1399,8 +1582,8 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, ObservedGeneration: 1, }, }, diff --git a/internal/operator-controller/controllers/common_controller_test.go b/internal/operator-controller/controllers/common_controller_test.go index 4d0a0536d1..93fad962e8 100644 --- a/internal/operator-controller/controllers/common_controller_test.go +++ b/internal/operator-controller/controllers/common_controller_test.go @@ -146,7 +146,7 @@ func TestClusterExtensionDeprecationMessageTruncation(t *testing.T) { deprecationMessages = append(deprecationMessages, fmt.Sprintf("API version 'v1beta1' of resource 'customresources%d.example.com' is deprecated, use 'v1' instead", i)) } - longDeprecationMsg := strings.Join(deprecationMessages, "; ") + longDeprecationMsg := strings.Join(deprecationMessages, "\n") setInstalledStatusConditionUnknown(ext, longDeprecationMsg) cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index ab0bf48b1c..6ac6087b9f 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "slices" "testing" "time" @@ -656,6 +657,12 @@ func TestClusterExtensionRecoversFromNoNamespaceWhenFailureFixed(t *testing.T) { require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) require.Contains(ct, cond.Message, "Installed bundle") require.NotEmpty(ct, clusterExtension.Status.Install) + + bundleDeprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(ct, bundleDeprecatedCond) + require.Equal(ct, metav1.ConditionFalse, bundleDeprecatedCond.Status) + require.Equal(ct, ocv1.ReasonDeprecated, bundleDeprecatedCond.Reason) + require.Empty(ct, bundleDeprecatedCond.Message) }, pollDuration, pollInterval) t.Log("By eventually reporting Progressing == True with Reason Success") @@ -755,11 +762,20 @@ func TestClusterExtensionRecoversFromExistingDeploymentWhenFailureFixed(t *testi cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) require.NotNil(ct, cond) require.Equal(ct, metav1.ConditionFalse, cond.Status) - // TODO: We probably _should_ be testing the reason here, but helm and boxcutter applier have different reasons. - // Maybe we change helm to use "Absent" rather than "Failed" since the Progressing condition already captures - // the failure? - //require.Equal(ct, ocv1.ReasonFailed, cond.Reason) + // Helm uses Failed, Boxcutter uses Absent; both are fine here. + require.True(ct, slices.Contains([]string{ocv1.ReasonFailed, ocv1.ReasonAbsent}, cond.Reason)) require.Contains(ct, cond.Message, "No bundle installed") + + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(ct, deprecatedCond) + require.Equal(ct, metav1.ConditionFalse, deprecatedCond.Status) + require.Empty(ct, deprecatedCond.Message) + + bundleDeprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(ct, bundleDeprecatedCond) + require.Equal(ct, metav1.ConditionUnknown, bundleDeprecatedCond.Status) + require.Equal(ct, ocv1.ReasonAbsent, bundleDeprecatedCond.Reason) + require.Empty(ct, bundleDeprecatedCond.Message) }, pollDuration, pollInterval) t.Log("By deleting the new Deployment") From a0dc46902ed8a16a309520d2da81a7e42d972aa2 Mon Sep 17 00:00:00 2001 From: Camila Macedo <7708031+camilamacedo86@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:09:39 +0000 Subject: [PATCH 2/2] Persist deprecation conditions - leave BundleDeprecated at Unknown/Absent when nothing installs - flip package/channel/bundle to True/Deprecated when the catalog says so - keep the conditions at False/NotDeprecated (empty message) when no warnings exist Assisted-by: Cursor --- api/v1/common_types.go | 3 +- .../conditionsets/conditionsets.go | 1 + .../clusterextension_controller.go | 62 ++++++++++++++----- .../clusterextension_controller_test.go | 42 ++++++------- 4 files changed, 72 insertions(+), 36 deletions(-) diff --git a/api/v1/common_types.go b/api/v1/common_types.go index 6ab5336ac2..2758bcdee9 100644 --- a/api/v1/common_types.go +++ b/api/v1/common_types.go @@ -29,7 +29,8 @@ const ( ReasonBlocked = "Blocked" // Deprecation reasons - ReasonDeprecated = "Deprecated" + ReasonDeprecated = "Deprecated" + ReasonNotDeprecated = "NotDeprecated" // Common reasons ReasonSucceeded = "Succeeded" diff --git a/internal/operator-controller/conditionsets/conditionsets.go b/internal/operator-controller/conditionsets/conditionsets.go index 0d63e1abb2..e25d63b413 100644 --- a/internal/operator-controller/conditionsets/conditionsets.go +++ b/internal/operator-controller/conditionsets/conditionsets.go @@ -36,6 +36,7 @@ var ConditionTypes = []string{ var ConditionReasons = []string{ ocv1.ReasonSucceeded, ocv1.ReasonDeprecated, + ocv1.ReasonNotDeprecated, ocv1.ReasonFailed, ocv1.ReasonBlocked, ocv1.ReasonRetrying, diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index 2e527874d4..76616d57de 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -355,9 +355,15 @@ type DeprecationInfo struct { // SetDeprecationStatus updates the ClusterExtension deprecation conditions using the // catalog data from resolve plus the name of the bundle that actually landed. Examples: -// - no bundle installed -> bundle status stays Unknown/Absent -// - installed bundle marked deprecated -> bundle status True/Deprecated -// - installed bundle not deprecated -> bundle status False/Deprecated +// - No bundle installed -> BundleDeprecated stays Unknown/Absent (ReasonAbsent) because we +// cannot judge a bundle that never landed. +// - Catalog marks the package or one of the requested channels deprecated -> the matching +// conditions flip to Status=True and Reason=Deprecated, with the catalog's message. +// - Catalog marks the installed bundle deprecated -> BundleDeprecated becomes Status=True +// and Reason=Deprecated, again echoing the catalog message. +// - Catalog says nothing about a particular level (package/channel/bundle) -> that condition +// stays Status=False with Reason=NotDeprecated and an empty message so users can rely on +// the field existing even when everything is healthy. // // This keeps the deprecation conditions focused on catalog information: // - PackageDeprecated: true if the catalog marks the package deprecated @@ -379,39 +385,67 @@ func SetDeprecationStatus(ext *ocv1.ClusterExtension, installedBundleName string messages = slices.Concat(messages, bundleMessages) status := metav1.ConditionFalse + reason := ocv1.ReasonNotDeprecated + message := "" if len(messages) > 0 { status = metav1.ConditionTrue + reason = ocv1.ReasonDeprecated + message = strings.Join(messages, "\n") } SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: ocv1.TypeDeprecated, Status: status, - Reason: ocv1.ReasonDeprecated, - Message: strings.Join(messages, "\n"), + Reason: reason, + Message: message, ObservedGeneration: ext.GetGeneration(), }) + packageStatus := conditionStatus(len(packageMessages) > 0) + packageReason := ocv1.ReasonNotDeprecated + packageMessage := "" + if packageStatus == metav1.ConditionTrue { + packageReason = ocv1.ReasonDeprecated + packageMessage = strings.Join(packageMessages, "\n") + } + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: ocv1.TypePackageDeprecated, - Status: conditionStatus(len(packageMessages) > 0), - Reason: ocv1.ReasonDeprecated, - Message: strings.Join(packageMessages, "\n"), + Status: packageStatus, + Reason: packageReason, + Message: packageMessage, ObservedGeneration: ext.GetGeneration(), }) + channelStatus := conditionStatus(len(channelMessages) > 0) + channelReason := ocv1.ReasonNotDeprecated + channelMessage := "" + if channelStatus == metav1.ConditionTrue { + channelReason = ocv1.ReasonDeprecated + channelMessage = strings.Join(channelMessages, "\n") + } + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: ocv1.TypeChannelDeprecated, - Status: conditionStatus(len(channelMessages) > 0), - Reason: ocv1.ReasonDeprecated, - Message: strings.Join(channelMessages, "\n"), + Status: channelStatus, + Reason: channelReason, + Message: channelMessage, ObservedGeneration: ext.GetGeneration(), }) - bundleReason := ocv1.ReasonDeprecated - bundleMessage := strings.Join(bundleMessages, "\n") - if info.BundleStatus == metav1.ConditionUnknown { + var bundleReason string + var bundleMessage string + + switch info.BundleStatus { + case metav1.ConditionTrue: + bundleReason = ocv1.ReasonDeprecated + bundleMessage = strings.Join(bundleMessages, "\n") + case metav1.ConditionUnknown: bundleReason = ocv1.ReasonAbsent bundleMessage = "" + default: + bundleReason = ocv1.ReasonNotDeprecated + bundleMessage = "" } SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ diff --git a/internal/operator-controller/controllers/clusterextension_controller_test.go b/internal/operator-controller/controllers/clusterextension_controller_test.go index a84f7329d4..1d4a03cf92 100644 --- a/internal/operator-controller/controllers/clusterextension_controller_test.go +++ b/internal/operator-controller/controllers/clusterextension_controller_test.go @@ -321,15 +321,15 @@ func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) require.NotNil(t, deprecatedCond) require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) - require.Equal(t, ocv1.ReasonDeprecated, deprecatedCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, deprecatedCond.Reason) pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) require.NotNil(t, pkgCond) require.Equal(t, metav1.ConditionFalse, pkgCond.Status) - require.Equal(t, ocv1.ReasonDeprecated, pkgCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, pkgCond.Reason) chanCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) require.NotNil(t, chanCond) require.Equal(t, metav1.ConditionFalse, chanCond.Status) - require.Equal(t, ocv1.ReasonDeprecated, chanCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, chanCond.Reason) bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) require.NotNil(t, bundleCond) require.Equal(t, metav1.ConditionUnknown, bundleCond.Status) @@ -418,15 +418,15 @@ func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T) deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) require.NotNil(t, deprecatedCond) require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) - require.Equal(t, ocv1.ReasonDeprecated, deprecatedCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, deprecatedCond.Reason) pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) require.NotNil(t, pkgCond) require.Equal(t, metav1.ConditionFalse, pkgCond.Status) - require.Equal(t, ocv1.ReasonDeprecated, pkgCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, pkgCond.Reason) chanCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) require.NotNil(t, chanCond) require.Equal(t, metav1.ConditionFalse, chanCond.Status) - require.Equal(t, ocv1.ReasonDeprecated, chanCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, chanCond.Reason) bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) require.NotNil(t, bundleCond) require.Equal(t, metav1.ConditionUnknown, bundleCond.Status) @@ -505,19 +505,19 @@ func TestClusterExtensionBoxcutterApplierFailsDoesNotLeakDeprecationErrors(t *te deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) require.NotNil(t, deprecatedCond) require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) - require.Equal(t, ocv1.ReasonDeprecated, deprecatedCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, deprecatedCond.Reason) require.Empty(t, deprecatedCond.Message) packageCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) require.NotNil(t, packageCond) require.Equal(t, metav1.ConditionFalse, packageCond.Status, "catalog said nothing about the package, so stay False") - require.Equal(t, ocv1.ReasonDeprecated, packageCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, packageCond.Reason) require.Empty(t, packageCond.Message) channelCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) require.NotNil(t, channelCond) require.Equal(t, metav1.ConditionFalse, channelCond.Status, "channel also has no deprecation info") - require.Equal(t, ocv1.ReasonDeprecated, channelCond.Reason) + require.Equal(t, ocv1.ReasonNotDeprecated, channelCond.Reason) require.Empty(t, channelCond.Message) bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) @@ -1052,19 +1052,19 @@ func TestSetDeprecationStatus(t *testing.T) { Conditions: []metav1.Condition{ { Type: ocv1.TypeDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, @@ -1110,19 +1110,19 @@ func TestSetDeprecationStatus(t *testing.T) { Conditions: []metav1.Condition{ { Type: ocv1.TypeDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, @@ -1179,19 +1179,19 @@ func TestSetDeprecationStatus(t *testing.T) { Conditions: []metav1.Condition{ { Type: ocv1.TypeDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, @@ -1256,7 +1256,7 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, @@ -1413,7 +1413,7 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, }, @@ -1570,7 +1570,7 @@ func TestSetDeprecationStatus(t *testing.T) { }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, ObservedGeneration: 1, },