@@ -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,140 @@ 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 {
350- continue
392+ if bundleName != "" && entry .Reference .Name == bundleName {
393+ info . BundleEntries = append ( info . BundleEntries , entry )
351394 }
352- deprecations [ocv1 .TypeBundleDeprecated ] = []declcfg.DeprecationEntry {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- }
399+ if bundleName != "" {
400+ if len (info .BundleEntries ) > 0 {
401+ info .BundleStatus = metav1 .ConditionTrue
402+ } else {
403+ info .BundleStatus = metav1 .ConditionFalse
368404 }
369405 }
370406
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 , ";" )
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 := slices .Concat (packageMessages , channelMessages )
416+ if info .BundleStatus == metav1 .ConditionTrue {
417+ messages = slices .Concat (messages , bundleMessages )
418+ }
419+
420+ status := metav1 .ConditionFalse
421+ if len (messages ) > 0 {
422+ status = metav1 .ConditionTrue
375423 }
424+
376425 SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
377426 Type : ocv1 .TypeDeprecated ,
378- Reason : reason ,
379427 Status : status ,
380- Message : message ,
381- ObservedGeneration : ext .Generation ,
428+ Reason : ocv1 .ReasonDeprecated ,
429+ Message : strings .Join (messages , "\n " ),
430+ ObservedGeneration : ext .GetGeneration (),
382431 })
383432
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- }
433+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
434+ Type : ocv1 .TypePackageDeprecated ,
435+ Status : conditionStatus (len (packageMessages ) > 0 ),
436+ Reason : ocv1 .ReasonDeprecated ,
437+ Message : strings .Join (packageMessages , "\n " ),
438+ ObservedGeneration : ext .GetGeneration (),
439+ })
440+
441+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
442+ Type : ocv1 .TypeChannelDeprecated ,
443+ Status : conditionStatus (len (channelMessages ) > 0 ),
444+ Reason : ocv1 .ReasonDeprecated ,
445+ Message : strings .Join (channelMessages , "\n " ),
446+ ObservedGeneration : ext .GetGeneration (),
447+ })
448+
449+ bundleReason := ocv1 .ReasonDeprecated
450+ bundleMessage := strings .Join (bundleMessages , "\n " )
451+ if info .BundleStatus == metav1 .ConditionUnknown {
452+ bundleReason = ocv1 .ReasonAbsent
453+ bundleMessage = ""
454+ }
455+
456+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
457+ Type : ocv1 .TypeBundleDeprecated ,
458+ Status : info .BundleStatus ,
459+ Reason : bundleReason ,
460+ Message : bundleMessage ,
461+ ObservedGeneration : ext .GetGeneration (),
462+ })
463+ }
464+
465+ func collectDeprecationMessages (entries []declcfg.DeprecationEntry ) []string {
466+ messages := make ([]string , 0 , len (entries ))
467+ for _ , entry := range entries {
468+ if entry .Message != "" {
469+ messages = append (messages , entry .Message )
397470 }
398- SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
399- Type : conditionType ,
400- Reason : reason ,
401- Status : status ,
402- Message : message ,
403- ObservedGeneration : ext .Generation ,
404- })
405471 }
472+ return messages
473+ }
474+
475+ func conditionStatus (ok bool ) metav1.ConditionStatus {
476+ if ok {
477+ return metav1 .ConditionTrue
478+ }
479+ return metav1 .ConditionFalse
406480}
407481
408482type ControllerBuilderOption func (builder * ctrl.Builder )
0 commit comments