@@ -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"
@@ -139,13 +140,23 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req
139140 return res , reconcileErr
140141}
141142
142- // ensureAllConditionsWithReason checks that all defined condition types exist in the given ClusterExtension,
143- // and assigns a specified reason and custom message to any missing condition.
144- func ensureAllConditionsWithReason (ext * ocv1.ClusterExtension , reason v1alpha1.ConditionReason , message string ) {
143+ // ensureFailureConditionsWithReason keeps every non-deprecation condition present.
144+ // If one is missing, we add it with the given reason and message so users see why
145+ // reconcile failed. Deprecation conditions are handled later by SetDeprecationStatus.
146+ func ensureFailureConditionsWithReason (ext * ocv1.ClusterExtension , reason v1alpha1.ConditionReason , message string ) {
145147 for _ , condType := range conditionsets .ConditionTypes {
148+ if isDeprecationCondition (condType ) {
149+ continue
150+ }
146151 cond := apimeta .FindStatusCondition (ext .Status .Conditions , condType )
152+ // Guard so we only fill empty slots.
153+ // GIVEN richer helpers keep existing conditions up to date.
154+ // WHEN we skip this guard, each run would overwrite their real status with the fallback.
155+ // THEN users would lose the true progressing/deprecation messages.
147156 if cond == nil {
148- // Create a new condition with a valid reason and add it
157+ // GIVEN no condition exists yet.
158+ // WHEN we add the fallback, users see the failure instead of a blank spot.
159+ // THEN richer helpers overwrite the fallback with the real progressing/bundle/package/channel message.
149160 SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
150161 Type : condType ,
151162 Status : metav1 .ConditionFalse ,
@@ -157,6 +168,17 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C
157168 }
158169}
159170
171+ // isDeprecationCondition reports whether the given type is one of the deprecation
172+ // conditions we manage separately.
173+ func isDeprecationCondition (condType string ) bool {
174+ switch condType {
175+ case ocv1 .TypeDeprecated , ocv1 .TypePackageDeprecated , ocv1 .TypeChannelDeprecated , ocv1 .TypeBundleDeprecated :
176+ return true
177+ default :
178+ return false
179+ }
180+ }
181+
160182// Compare resources - ignoring status & metadata.finalizers
161183func checkForUnexpectedClusterExtensionFieldChange (a , b ocv1.ClusterExtension ) bool {
162184 a .Status , b .Status = ocv1.ClusterExtensionStatus {}, ocv1.ClusterExtensionStatus {}
@@ -229,37 +251,41 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl
229251 return ctrl.Result {}, err
230252 }
231253
254+ // Hold deprecation updates until the end. That way:
255+ // * if nothing installs, BundleDeprecated stays Unknown/Absent
256+ // * if a bundle installs, we report its real deprecation status
257+ // * install errors never leak into the deprecation conditions
258+ var resolvedDeprecation * declcfg.Deprecation
259+ defer func (resolvedDeprecationPtr * * declcfg.Deprecation ) {
260+ installedBundleName := ""
261+ if revisionStates != nil && revisionStates .Installed != nil {
262+ installedBundleName = revisionStates .Installed .Name
263+ }
264+ SetDeprecationStatus (ext , installedBundleName , * resolvedDeprecationPtr )
265+ }(& resolvedDeprecation )
266+
232267 var resolvedRevisionMetadata * RevisionMetadata
233268 if len (revisionStates .RollingOut ) == 0 {
234269 l .Info ("resolving bundle" )
235270 var bm * ocv1.BundleMetadata
236271 if revisionStates .Installed != nil {
237272 bm = & revisionStates .Installed .BundleMetadata
238273 }
239- resolvedBundle , resolvedBundleVersion , resolvedDeprecation , err := r .Resolver .Resolve (ctx , ext , bm )
274+ var resolvedBundle * declcfg.Bundle
275+ var resolvedBundleVersion * bsemver.Version
276+ resolvedBundle , resolvedBundleVersion , resolvedDeprecation , err = r .Resolver .Resolve (ctx , ext , bm )
277+ // Keep any deprecation data the resolver returned. The deferred update will use it
278+ // even if installation later fails or never begins.
240279 if err != nil {
241280 // Note: We don't distinguish between resolution-specific errors and generic errors
242281 setStatusProgressing (ext , err )
243282 setInstalledStatusFromRevisionStates (ext , revisionStates )
244- ensureAllConditionsWithReason (ext , ocv1 .ReasonFailed , err .Error ())
283+ // Ensure non-deprecation conditions capture the failure immediately. The deferred
284+ // SetDeprecationStatus call is responsible for updating the deprecation conditions
285+ // based on any catalog data returned by the resolver.
286+ ensureFailureConditionsWithReason (ext , ocv1 .ReasonFailed , err .Error ())
245287 return ctrl.Result {}, err
246288 }
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 )
263289 resolvedRevisionMetadata = & RevisionMetadata {
264290 Package : resolvedBundle .Package ,
265291 Image : resolvedBundle .Image ,
@@ -326,83 +352,141 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl
326352 return ctrl.Result {}, nil
327353}
328354
329- // SetDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension
330- // based on the provided bundle
331- func SetDeprecationStatus (ext * ocv1.ClusterExtension , bundleName string , deprecation * declcfg.Deprecation ) {
332- deprecations := map [string ][]declcfg.DeprecationEntry {}
333- channelSet := sets .New [string ]()
355+ // DeprecationInfo captures the deprecation data needed to update condition status.
356+ type DeprecationInfo struct {
357+ PackageEntries []declcfg.DeprecationEntry
358+ ChannelEntries []declcfg.DeprecationEntry
359+ BundleEntries []declcfg.DeprecationEntry
360+ BundleStatus metav1.ConditionStatus
361+ }
362+
363+ // SetDeprecationStatus updates the ClusterExtension deprecation conditions using the
364+ // catalog data from resolve plus the name of the bundle that actually landed. Examples:
365+ // - no bundle installed -> bundle status stays Unknown/Absent
366+ // - installed bundle marked deprecated -> bundle status True/Deprecated
367+ // - installed bundle not deprecated -> bundle status False/Deprecated
368+ //
369+ // This keeps the deprecation conditions focused on catalog information:
370+ // - PackageDeprecated: true if the catalog marks the package deprecated
371+ // - ChannelDeprecated: true if any requested channel is marked deprecated
372+ // - BundleDeprecated: reflects the installed bundle (Unknown/Absent when nothing installed)
373+ // - Deprecated (rollup): true if any of the above signals a deprecation
374+ //
375+ // Install or validation errors never appear here because they belong on the
376+ // Progressing/Installed conditions instead. Callers should invoke this after reconcile
377+ // finishes (for example via a defer) so catalog data replaces any transient error messages.
378+ func SetDeprecationStatus (ext * ocv1.ClusterExtension , installedBundleName string , deprecation * declcfg.Deprecation ) {
379+ info := buildDeprecationInfo (ext , installedBundleName , deprecation )
380+
381+ packageMessages := collectDeprecationMessages (info .PackageEntries )
382+ channelMessages := collectDeprecationMessages (info .ChannelEntries )
383+ bundleMessages := collectDeprecationMessages (info .BundleEntries )
384+
385+ messages := slices .Concat (packageMessages , channelMessages , bundleMessages )
386+
387+ status := metav1 .ConditionFalse
388+ if len (messages ) > 0 {
389+ status = metav1 .ConditionTrue
390+ }
391+
392+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
393+ Type : ocv1 .TypeDeprecated ,
394+ Status : status ,
395+ Reason : ocv1 .ReasonDeprecated ,
396+ Message : strings .Join (messages , "\n " ),
397+ ObservedGeneration : ext .GetGeneration (),
398+ })
399+
400+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
401+ Type : ocv1 .TypePackageDeprecated ,
402+ Status : conditionStatus (len (packageMessages ) > 0 ),
403+ Reason : ocv1 .ReasonDeprecated ,
404+ Message : strings .Join (packageMessages , "\n " ),
405+ ObservedGeneration : ext .GetGeneration (),
406+ })
407+
408+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
409+ Type : ocv1 .TypeChannelDeprecated ,
410+ Status : conditionStatus (len (channelMessages ) > 0 ),
411+ Reason : ocv1 .ReasonDeprecated ,
412+ Message : strings .Join (channelMessages , "\n " ),
413+ ObservedGeneration : ext .GetGeneration (),
414+ })
415+
416+ bundleReason := ocv1 .ReasonDeprecated
417+ bundleMessage := strings .Join (bundleMessages , "\n " )
418+ if info .BundleStatus == metav1 .ConditionUnknown {
419+ bundleReason = ocv1 .ReasonAbsent
420+ bundleMessage = ""
421+ }
422+
423+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
424+ Type : ocv1 .TypeBundleDeprecated ,
425+ Status : info .BundleStatus ,
426+ Reason : bundleReason ,
427+ Message : bundleMessage ,
428+ ObservedGeneration : ext .GetGeneration (),
429+ })
430+ }
431+
432+ // buildDeprecationInfo filters the catalog deprecation data down to the package, channel,
433+ // and bundle entries that matter for this ClusterExtension. An empty bundle name means
434+ // nothing is installed yet, so we leave bundle status Unknown/Absent.
435+ func buildDeprecationInfo (ext * ocv1.ClusterExtension , installedBundleName string , deprecation * declcfg.Deprecation ) DeprecationInfo {
436+ info := DeprecationInfo {BundleStatus : metav1 .ConditionUnknown }
437+ var channelSet sets.Set [string ]
334438 if ext .Spec .Source .Catalog != nil {
335- for _ , channel := range ext .Spec .Source .Catalog .Channels {
336- channelSet . Insert ( channel )
337- }
439+ channelSet = sets . New ( ext .Spec .Source .Catalog .Channels ... )
440+ } else {
441+ channelSet = sets . New [ string ]()
338442 }
443+
339444 if deprecation != nil {
340445 for _ , entry := range deprecation .Entries {
341446 switch entry .Reference .Schema {
342447 case declcfg .SchemaPackage :
343- deprecations [ ocv1 . TypePackageDeprecated ] = []declcfg. DeprecationEntry { entry }
448+ info . PackageEntries = append ( info . PackageEntries , entry )
344449 case declcfg .SchemaChannel :
345450 if channelSet .Has (entry .Reference .Name ) {
346- deprecations [ ocv1 . TypeChannelDeprecated ] = append (deprecations [ ocv1 . TypeChannelDeprecated ] , entry )
451+ info . ChannelEntries = append (info . ChannelEntries , entry )
347452 }
348453 case declcfg .SchemaBundle :
349- if bundleName != entry .Reference .Name {
350- continue
454+ if installedBundleName != "" && entry .Reference .Name == installedBundleName {
455+ info . BundleEntries = append ( info . BundleEntries , entry )
351456 }
352- deprecations [ocv1 .TypeBundleDeprecated ] = []declcfg.DeprecationEntry {entry }
353457 }
354458 }
355459 }
356460
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- }
461+ // installedBundleName is empty when nothing is installed. In that case we want
462+ // to report the bundle deprecation condition as Unknown/Absent.
463+ if installedBundleName != "" {
464+ if len (info .BundleEntries ) > 0 {
465+ info .BundleStatus = metav1 .ConditionTrue
466+ } else {
467+ info .BundleStatus = metav1 .ConditionFalse
368468 }
369469 }
370470
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 , ";" )
375- }
376- SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
377- Type : ocv1 .TypeDeprecated ,
378- Reason : reason ,
379- Status : status ,
380- Message : message ,
381- ObservedGeneration : ext .Generation ,
382- })
471+ return info
472+ }
383473
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- }
474+ // collectDeprecationMessages collects the non-empty deprecation messages from the provided entries.
475+ func collectDeprecationMessages (entries []declcfg.DeprecationEntry ) []string {
476+ messages := make ([]string , 0 , len (entries ))
477+ for _ , entry := range entries {
478+ if entry .Message != "" {
479+ messages = append (messages , entry .Message )
397480 }
398- SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
399- Type : conditionType ,
400- Reason : reason ,
401- Status : status ,
402- Message : message ,
403- ObservedGeneration : ext .Generation ,
404- })
405481 }
482+ return messages
483+ }
484+
485+ func conditionStatus (ok bool ) metav1.ConditionStatus {
486+ if ok {
487+ return metav1 .ConditionTrue
488+ }
489+ return metav1 .ConditionFalse
406490}
407491
408492type ControllerBuilderOption func (builder * ctrl.Builder )
0 commit comments