@@ -25,6 +25,7 @@ import (
2525 "slices"
2626 "strings"
2727
28+ bsemver "github.com/blang/semver/v4"
2829 "github.com/go-logr/logr"
2930 "helm.sh/helm/v3/pkg/release"
3031 "helm.sh/helm/v3/pkg/storage/driver"
@@ -229,37 +230,53 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl
229230 return ctrl.Result {}, err
230231 }
231232
233+ // Defer deprecation updates until the end of reconcile. We only want to talk about
234+ // catalog deprecation data after we know which bundle is actually installed.
235+ // This always runs (success or failure) so install/validation errors never leak into
236+ // the deprecation conditions.
237+ //
238+ // What the deferred update reports:
239+ // * Package deprecation: true if the package is marked deprecated in the catalog
240+ // * Channel deprecation: true if any requested channel is deprecated in the catalog
241+ // * Bundle deprecation: reflects the INSTALLED bundle
242+ // - no bundle installed -> status Unknown, reason Absent
243+ // - installed bundle deprecated -> status True, reason Deprecated
244+ // - installed bundle not deprecated -> status False, reason Deprecated
245+ //
246+ // If a bundle resolves but fails to install, package/channel results still show the
247+ // catalog data, and the bundle condition stays Unknown because nothing is installed.
248+ var resolvedDeprecation * declcfg.Deprecation
249+ defer func () {
250+ bundleName := ""
251+ if revisionStates != nil && revisionStates .Installed != nil {
252+ bundleName = revisionStates .Installed .Name
253+ }
254+ SetDeprecationStatus (ext , bundleName , resolvedDeprecation )
255+ }()
256+
232257 var resolvedRevisionMetadata * RevisionMetadata
233258 if len (revisionStates .RollingOut ) == 0 {
234259 l .Info ("resolving bundle" )
235260 var bm * ocv1.BundleMetadata
236261 if revisionStates .Installed != nil {
237262 bm = & revisionStates .Installed .BundleMetadata
238263 }
239- resolvedBundle , resolvedBundleVersion , resolvedDeprecation , err := r .Resolver .Resolve (ctx , ext , bm )
264+ var resolvedBundle * declcfg.Bundle
265+ var resolvedBundleVersion * bsemver.Version
266+ resolvedBundle , resolvedBundleVersion , resolvedDeprecation , err = r .Resolver .Resolve (ctx , ext , bm )
267+ // Keep any deprecation data the resolver returned. The deferred update will use it
268+ // even if installation later fails or never begins.
240269 if err != nil {
241270 // Note: We don't distinguish between resolution-specific errors and generic errors
242271 setStatusProgressing (ext , err )
243272 setInstalledStatusFromRevisionStates (ext , revisionStates )
273+ // ensureAllConditionsWithReason writes the failure message to every condition.
274+ // The deferred SetDeprecationStatus call overwrites the deprecation conditions right
275+ // away so they only reflect catalog data. This also supports the case where resolution
276+ // returned deprecation info even though it could not find an installable bundle.
244277 ensureAllConditionsWithReason (ext , ocv1 .ReasonFailed , err .Error ())
245278 return ctrl.Result {}, err
246279 }
247-
248- // set deprecation status after _successful_ resolution
249- // TODO:
250- // 1. It seems like deprecation status should reflect the currently installed bundle, not the resolved
251- // bundle. So perhaps we should set package and channel deprecations directly after resolution, but
252- // defer setting the bundle deprecation until we successfully install the bundle.
253- // 2. If resolution fails because it can't find a bundle, that doesn't mean we wouldn't be able to find
254- // a deprecation for the ClusterExtension's spec.packageName. Perhaps we should check for a non-nil
255- // resolvedDeprecation even if resolution returns an error. If present, we can still update some of
256- // our deprecation status.
257- // - Open question though: what if different catalogs have different opinions of what's deprecated.
258- // If we can't resolve a bundle, how do we know which catalog to trust for deprecation information?
259- // Perhaps if the package shows up in multiple catalogs and deprecations don't match, we can set
260- // the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from
261- // all catalogs?
262- SetDeprecationStatus (ext , resolvedBundle .Name , resolvedDeprecation )
263280 resolvedRevisionMetadata = & RevisionMetadata {
264281 Package : resolvedBundle .Package ,
265282 Image : resolvedBundle .Image ,
@@ -326,83 +343,142 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl
326343 return ctrl.Result {}, nil
327344}
328345
329- // SetDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension
330- // based on the provided bundle
346+ // DeprecationInfo captures the deprecation data needed to update condition status.
347+ type DeprecationInfo struct {
348+ PackageEntries []declcfg.DeprecationEntry
349+ ChannelEntries []declcfg.DeprecationEntry
350+ BundleEntries []declcfg.DeprecationEntry
351+ BundleStatus metav1.ConditionStatus
352+ }
353+
354+ // SetDeprecationStatus updates the ClusterExtension deprecation conditions based on the
355+ // provided deprecation information and the currently installed bundle name. When bundleName
356+ // is empty, no bundle is considered installed and the bundle deprecation condition is
357+ // reported as Unknown with reason Absent.
358+ //
359+ // This keeps the deprecation conditions focused on catalog information:
360+ // - PackageDeprecated: true if the catalog marks the package deprecated
361+ // - ChannelDeprecated: true if any requested channel is marked deprecated
362+ // - BundleDeprecated: reflects the installed bundle (Unknown/Absent when nothing installed)
363+ // - Deprecated (rollup): true if any of the above signals a deprecation
364+ //
365+ // Install or validation errors never appear here because they belong on the
366+ // Progressing/Installed conditions instead.
331367func SetDeprecationStatus (ext * ocv1.ClusterExtension , bundleName string , deprecation * declcfg.Deprecation ) {
332- deprecations := map [string ][]declcfg.DeprecationEntry {}
333- channelSet := sets .New [string ]()
368+ info := buildDeprecationInfo (ext , bundleName , deprecation )
369+ setDeprecatedConditions (ext , info )
370+ }
371+
372+ func buildDeprecationInfo (ext * ocv1.ClusterExtension , bundleName string , deprecation * declcfg.Deprecation ) DeprecationInfo {
373+ info := DeprecationInfo {BundleStatus : metav1 .ConditionUnknown }
374+ var channelSet sets.Set [string ]
334375 if ext .Spec .Source .Catalog != nil {
335- for _ , channel := range ext .Spec .Source .Catalog .Channels {
336- channelSet . Insert ( channel )
337- }
376+ channelSet = sets . New ( ext .Spec .Source .Catalog .Channels ... )
377+ } else {
378+ channelSet = sets . New [ string ]()
338379 }
380+
339381 if deprecation != nil {
340382 for _ , entry := range deprecation .Entries {
341383 switch entry .Reference .Schema {
342384 case declcfg .SchemaPackage :
343- deprecations [ ocv1 . TypePackageDeprecated ] = []declcfg. DeprecationEntry { entry }
385+ info . PackageEntries = append ( info . PackageEntries , entry )
344386 case declcfg .SchemaChannel :
345387 if channelSet .Has (entry .Reference .Name ) {
346- deprecations [ ocv1 . TypeChannelDeprecated ] = append (deprecations [ ocv1 . TypeChannelDeprecated ] , entry )
388+ info . ChannelEntries = append (info . ChannelEntries , entry )
347389 }
348390 case declcfg .SchemaBundle :
349- if bundleName != entry .Reference .Name {
391+ if bundleName == "" || entry .Reference .Name != bundleName {
350392 continue
351393 }
352- deprecations [ ocv1 . TypeBundleDeprecated ] = []declcfg. DeprecationEntry { entry }
394+ info . BundleEntries = append ( info . BundleEntries , entry )
353395 }
354396 }
355397 }
356398
357- // first get ordered deprecation messages that we'll join in the Deprecated condition message
358- var deprecationMessages []string
359- for _ , conditionType := range []string {
360- ocv1 .TypePackageDeprecated ,
361- ocv1 .TypeChannelDeprecated ,
362- ocv1 .TypeBundleDeprecated ,
363- } {
364- if entries , ok := deprecations [conditionType ]; ok {
365- for _ , entry := range entries {
366- deprecationMessages = append (deprecationMessages , entry .Message )
367- }
368- }
399+ if bundleName == "" {
400+ info .BundleStatus = metav1 .ConditionUnknown
401+ } else if len (info .BundleEntries ) > 0 {
402+ info .BundleStatus = metav1 .ConditionTrue
403+ } else {
404+ info .BundleStatus = metav1 .ConditionFalse
405+ }
406+
407+ return info
408+ }
409+
410+ func setDeprecatedConditions (ext * ocv1.ClusterExtension , info DeprecationInfo ) {
411+ packageMessages := collectDeprecationMessages (info .PackageEntries )
412+ channelMessages := collectDeprecationMessages (info .ChannelEntries )
413+ bundleMessages := collectDeprecationMessages (info .BundleEntries )
414+
415+ messages := make ([]string , 0 , len (packageMessages )+ len (channelMessages )+ len (bundleMessages ))
416+ messages = append (messages , packageMessages ... )
417+ messages = append (messages , channelMessages ... )
418+ if info .BundleStatus == metav1 .ConditionTrue {
419+ messages = append (messages , bundleMessages ... )
369420 }
370421
371- // next, set the Deprecated condition
372- status , reason , message := metav1 .ConditionFalse , ocv1 .ReasonDeprecated , ""
373- if len (deprecationMessages ) > 0 {
374- status , reason , message = metav1 .ConditionTrue , ocv1 .ReasonDeprecated , strings .Join (deprecationMessages , ";" )
422+ status := metav1 .ConditionFalse
423+ if len (messages ) > 0 {
424+ status = metav1 .ConditionTrue
375425 }
426+
376427 SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
377428 Type : ocv1 .TypeDeprecated ,
378- Reason : reason ,
379429 Status : status ,
380- Message : message ,
381- ObservedGeneration : ext .Generation ,
430+ Reason : ocv1 .ReasonDeprecated ,
431+ Message : strings .Join (messages , "; " ),
432+ ObservedGeneration : ext .GetGeneration (),
382433 })
383434
384- // finally, set the individual deprecation conditions for package, channel, and bundle
385- for _ , conditionType := range []string {
386- ocv1 .TypePackageDeprecated ,
387- ocv1 .TypeChannelDeprecated ,
388- ocv1 .TypeBundleDeprecated ,
389- } {
390- entries , ok := deprecations [conditionType ]
391- status , reason , message := metav1 .ConditionFalse , ocv1 .ReasonDeprecated , ""
392- if ok {
393- status , reason = metav1 .ConditionTrue , ocv1 .ReasonDeprecated
394- for _ , entry := range entries {
395- message = fmt .Sprintf ("%s\n %s" , message , entry .Message )
396- }
435+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
436+ Type : ocv1 .TypePackageDeprecated ,
437+ Status : conditionStatus (len (packageMessages ) > 0 ),
438+ Reason : ocv1 .ReasonDeprecated ,
439+ Message : strings .Join (packageMessages , "\n " ),
440+ ObservedGeneration : ext .GetGeneration (),
441+ })
442+
443+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
444+ Type : ocv1 .TypeChannelDeprecated ,
445+ Status : conditionStatus (len (channelMessages ) > 0 ),
446+ Reason : ocv1 .ReasonDeprecated ,
447+ Message : strings .Join (channelMessages , "\n " ),
448+ ObservedGeneration : ext .GetGeneration (),
449+ })
450+
451+ bundleReason := ocv1 .ReasonDeprecated
452+ bundleMessage := strings .Join (bundleMessages , "\n " )
453+ if info .BundleStatus == metav1 .ConditionUnknown {
454+ bundleReason = ocv1 .ReasonAbsent
455+ bundleMessage = ""
456+ }
457+
458+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
459+ Type : ocv1 .TypeBundleDeprecated ,
460+ Status : info .BundleStatus ,
461+ Reason : bundleReason ,
462+ Message : bundleMessage ,
463+ ObservedGeneration : ext .GetGeneration (),
464+ })
465+ }
466+
467+ func collectDeprecationMessages (entries []declcfg.DeprecationEntry ) []string {
468+ messages := make ([]string , 0 , len (entries ))
469+ for _ , entry := range entries {
470+ if entry .Message != "" {
471+ messages = append (messages , entry .Message )
397472 }
398- SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
399- Type : conditionType ,
400- Reason : reason ,
401- Status : status ,
402- Message : message ,
403- ObservedGeneration : ext .Generation ,
404- })
405473 }
474+ return messages
475+ }
476+
477+ func conditionStatus (ok bool ) metav1.ConditionStatus {
478+ if ok {
479+ return metav1 .ConditionTrue
480+ }
481+ return metav1 .ConditionFalse
406482}
407483
408484type ControllerBuilderOption func (builder * ctrl.Builder )
0 commit comments