@@ -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,143 @@ 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. Callers should invoke this after reconcile
367+ // finishes (for example via a defer) so catalog data replaces any transient error messages.
331368func SetDeprecationStatus (ext * ocv1.ClusterExtension , bundleName string , deprecation * declcfg.Deprecation ) {
332- deprecations := map [string ][]declcfg.DeprecationEntry {}
333- channelSet := sets .New [string ]()
369+ info := buildDeprecationInfo (ext , bundleName , deprecation )
370+ setDeprecatedConditions (ext , info )
371+ }
372+
373+ func buildDeprecationInfo (ext * ocv1.ClusterExtension , bundleName string , deprecation * declcfg.Deprecation ) DeprecationInfo {
374+ info := DeprecationInfo {BundleStatus : metav1 .ConditionUnknown }
375+ var channelSet sets.Set [string ]
334376 if ext .Spec .Source .Catalog != nil {
335- for _ , channel := range ext .Spec .Source .Catalog .Channels {
336- channelSet . Insert ( channel )
337- }
377+ channelSet = sets . New ( ext .Spec .Source .Catalog .Channels ... )
378+ } else {
379+ channelSet = sets . New [ string ]()
338380 }
381+
339382 if deprecation != nil {
340383 for _ , entry := range deprecation .Entries {
341384 switch entry .Reference .Schema {
342385 case declcfg .SchemaPackage :
343- deprecations [ ocv1 . TypePackageDeprecated ] = []declcfg. DeprecationEntry { entry }
386+ info . PackageEntries = append ( info . PackageEntries , entry )
344387 case declcfg .SchemaChannel :
345388 if channelSet .Has (entry .Reference .Name ) {
346- deprecations [ ocv1 . TypeChannelDeprecated ] = append (deprecations [ ocv1 . TypeChannelDeprecated ] , entry )
389+ info . ChannelEntries = append (info . ChannelEntries , entry )
347390 }
348391 case declcfg .SchemaBundle :
349- if bundleName != entry .Reference .Name {
392+ if bundleName == "" || entry .Reference .Name != bundleName {
350393 continue
351394 }
352- deprecations [ ocv1 . TypeBundleDeprecated ] = []declcfg. DeprecationEntry { entry }
395+ info . BundleEntries = append ( info . BundleEntries , entry )
353396 }
354397 }
355398 }
356399
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- }
400+ if bundleName == "" {
401+ info .BundleStatus = metav1 .ConditionUnknown
402+ } else if len (info .BundleEntries ) > 0 {
403+ info .BundleStatus = metav1 .ConditionTrue
404+ } else {
405+ info .BundleStatus = metav1 .ConditionFalse
406+ }
407+
408+ return info
409+ }
410+
411+ func setDeprecatedConditions (ext * ocv1.ClusterExtension , info DeprecationInfo ) {
412+ packageMessages := collectDeprecationMessages (info .PackageEntries )
413+ channelMessages := collectDeprecationMessages (info .ChannelEntries )
414+ bundleMessages := collectDeprecationMessages (info .BundleEntries )
415+
416+ messages := make ([]string , 0 , len (packageMessages )+ len (channelMessages )+ len (bundleMessages ))
417+ messages = append (messages , packageMessages ... )
418+ messages = append (messages , channelMessages ... )
419+ if info .BundleStatus == metav1 .ConditionTrue {
420+ messages = append (messages , bundleMessages ... )
369421 }
370422
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 , ";" )
423+ status := metav1 .ConditionFalse
424+ if len (messages ) > 0 {
425+ status = metav1 .ConditionTrue
375426 }
427+
376428 SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
377429 Type : ocv1 .TypeDeprecated ,
378- Reason : reason ,
379430 Status : status ,
380- Message : message ,
381- ObservedGeneration : ext .Generation ,
431+ Reason : ocv1 .ReasonDeprecated ,
432+ Message : strings .Join (messages , "\n " ),
433+ ObservedGeneration : ext .GetGeneration (),
382434 })
383435
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- }
436+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
437+ Type : ocv1 .TypePackageDeprecated ,
438+ Status : conditionStatus (len (packageMessages ) > 0 ),
439+ Reason : ocv1 .ReasonDeprecated ,
440+ Message : strings .Join (packageMessages , "\n " ),
441+ ObservedGeneration : ext .GetGeneration (),
442+ })
443+
444+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
445+ Type : ocv1 .TypeChannelDeprecated ,
446+ Status : conditionStatus (len (channelMessages ) > 0 ),
447+ Reason : ocv1 .ReasonDeprecated ,
448+ Message : strings .Join (channelMessages , "\n " ),
449+ ObservedGeneration : ext .GetGeneration (),
450+ })
451+
452+ bundleReason := ocv1 .ReasonDeprecated
453+ bundleMessage := strings .Join (bundleMessages , "\n " )
454+ if info .BundleStatus == metav1 .ConditionUnknown {
455+ bundleReason = ocv1 .ReasonAbsent
456+ bundleMessage = ""
457+ }
458+
459+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
460+ Type : ocv1 .TypeBundleDeprecated ,
461+ Status : info .BundleStatus ,
462+ Reason : bundleReason ,
463+ Message : bundleMessage ,
464+ ObservedGeneration : ext .GetGeneration (),
465+ })
466+ }
467+
468+ func collectDeprecationMessages (entries []declcfg.DeprecationEntry ) []string {
469+ messages := make ([]string , 0 , len (entries ))
470+ for _ , entry := range entries {
471+ if entry .Message != "" {
472+ messages = append (messages , entry .Message )
397473 }
398- SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
399- Type : conditionType ,
400- Reason : reason ,
401- Status : status ,
402- Message : message ,
403- ObservedGeneration : ext .Generation ,
404- })
405474 }
475+ return messages
476+ }
477+
478+ func conditionStatus (ok bool ) metav1.ConditionStatus {
479+ if ok {
480+ return metav1 .ConditionTrue
481+ }
482+ return metav1 .ConditionFalse
406483}
407484
408485type ControllerBuilderOption func (builder * ctrl.Builder )
0 commit comments