@@ -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,16 @@ 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 )
147152 if cond == nil {
148- // Create a new condition with a valid reason and add it
149153 SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
150154 Type : condType ,
151155 Status : metav1 .ConditionFalse ,
@@ -157,6 +161,17 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C
157161 }
158162}
159163
164+ // isDeprecationCondition reports whether the given type is one of the deprecation
165+ // conditions we manage separately.
166+ func isDeprecationCondition (condType string ) bool {
167+ switch condType {
168+ case ocv1 .TypeDeprecated , ocv1 .TypePackageDeprecated , ocv1 .TypeChannelDeprecated , ocv1 .TypeBundleDeprecated :
169+ return true
170+ default :
171+ return false
172+ }
173+ }
174+
160175// Compare resources - ignoring status & metadata.finalizers
161176func checkForUnexpectedClusterExtensionFieldChange (a , b ocv1.ClusterExtension ) bool {
162177 a .Status , b .Status = ocv1.ClusterExtensionStatus {}, ocv1.ClusterExtensionStatus {}
@@ -229,37 +244,41 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl
229244 return ctrl.Result {}, err
230245 }
231246
247+ // Hold deprecation updates until the end. That way:
248+ // * if nothing installs, BundleDeprecated stays Unknown/Absent
249+ // * if a bundle installs, we report its real deprecation status
250+ // * install errors never leak into the deprecation conditions
251+ var resolvedDeprecation * declcfg.Deprecation
252+ defer func (resolvedDeprecationCopy * declcfg.Deprecation ) {
253+ installedBundleName := ""
254+ if revisionStates != nil && revisionStates .Installed != nil {
255+ installedBundleName = revisionStates .Installed .Name
256+ }
257+ SetDeprecationStatus (ext , installedBundleName , resolvedDeprecationCopy )
258+ }(resolvedDeprecation )
259+
232260 var resolvedRevisionMetadata * RevisionMetadata
233261 if len (revisionStates .RollingOut ) == 0 {
234262 l .Info ("resolving bundle" )
235263 var bm * ocv1.BundleMetadata
236264 if revisionStates .Installed != nil {
237265 bm = & revisionStates .Installed .BundleMetadata
238266 }
239- resolvedBundle , resolvedBundleVersion , resolvedDeprecation , err := r .Resolver .Resolve (ctx , ext , bm )
267+ var resolvedBundle * declcfg.Bundle
268+ var resolvedBundleVersion * bsemver.Version
269+ resolvedBundle , resolvedBundleVersion , resolvedDeprecation , err = r .Resolver .Resolve (ctx , ext , bm )
270+ // Keep any deprecation data the resolver returned. The deferred update will use it
271+ // even if installation later fails or never begins.
240272 if err != nil {
241273 // Note: We don't distinguish between resolution-specific errors and generic errors
242274 setStatusProgressing (ext , err )
243275 setInstalledStatusFromRevisionStates (ext , revisionStates )
244- ensureAllConditionsWithReason (ext , ocv1 .ReasonFailed , err .Error ())
276+ // Ensure non-deprecation conditions capture the failure immediately. The deferred
277+ // SetDeprecationStatus call is responsible for updating the deprecation conditions
278+ // based on any catalog data returned by the resolver.
279+ ensureFailureConditionsWithReason (ext , ocv1 .ReasonFailed , err .Error ())
245280 return ctrl.Result {}, err
246281 }
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 )
263282 resolvedRevisionMetadata = & RevisionMetadata {
264283 Package : resolvedBundle .Package ,
265284 Image : resolvedBundle .Image ,
@@ -326,83 +345,141 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl
326345 return ctrl.Result {}, nil
327346}
328347
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 ]()
348+ // DeprecationInfo captures the deprecation data needed to update condition status.
349+ type DeprecationInfo struct {
350+ PackageEntries []declcfg.DeprecationEntry
351+ ChannelEntries []declcfg.DeprecationEntry
352+ BundleEntries []declcfg.DeprecationEntry
353+ BundleStatus metav1.ConditionStatus
354+ }
355+
356+ // SetDeprecationStatus updates the ClusterExtension deprecation conditions using the
357+ // catalog data from resolve plus the name of the bundle that actually landed. Examples:
358+ // - no bundle installed -> bundle status stays Unknown/Absent
359+ // - installed bundle marked deprecated -> bundle status True/Deprecated
360+ // - installed bundle not deprecated -> bundle status False/Deprecated
361+ //
362+ // This keeps the deprecation conditions focused on catalog information:
363+ // - PackageDeprecated: true if the catalog marks the package deprecated
364+ // - ChannelDeprecated: true if any requested channel is marked deprecated
365+ // - BundleDeprecated: reflects the installed bundle (Unknown/Absent when nothing installed)
366+ // - Deprecated (rollup): true if any of the above signals a deprecation
367+ //
368+ // Install or validation errors never appear here because they belong on the
369+ // Progressing/Installed conditions instead. Callers should invoke this after reconcile
370+ // finishes (for example via a defer) so catalog data replaces any transient error messages.
371+ func SetDeprecationStatus (ext * ocv1.ClusterExtension , installedBundleName string , deprecation * declcfg.Deprecation ) {
372+ info := buildDeprecationInfo (ext , installedBundleName , deprecation )
373+
374+ packageMessages := collectDeprecationMessages (info .PackageEntries )
375+ channelMessages := collectDeprecationMessages (info .ChannelEntries )
376+ bundleMessages := collectDeprecationMessages (info .BundleEntries )
377+
378+ messages := slices .Concat (packageMessages , channelMessages , bundleMessages )
379+
380+ status := metav1 .ConditionFalse
381+ if len (messages ) > 0 {
382+ status = metav1 .ConditionTrue
383+ }
384+
385+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
386+ Type : ocv1 .TypeDeprecated ,
387+ Status : status ,
388+ Reason : ocv1 .ReasonDeprecated ,
389+ Message : strings .Join (messages , "\n " ),
390+ ObservedGeneration : ext .GetGeneration (),
391+ })
392+
393+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
394+ Type : ocv1 .TypePackageDeprecated ,
395+ Status : conditionStatus (len (packageMessages ) > 0 ),
396+ Reason : ocv1 .ReasonDeprecated ,
397+ Message : strings .Join (packageMessages , "\n " ),
398+ ObservedGeneration : ext .GetGeneration (),
399+ })
400+
401+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
402+ Type : ocv1 .TypeChannelDeprecated ,
403+ Status : conditionStatus (len (channelMessages ) > 0 ),
404+ Reason : ocv1 .ReasonDeprecated ,
405+ Message : strings .Join (channelMessages , "\n " ),
406+ ObservedGeneration : ext .GetGeneration (),
407+ })
408+
409+ bundleReason := ocv1 .ReasonDeprecated
410+ bundleMessage := strings .Join (bundleMessages , "\n " )
411+ if info .BundleStatus == metav1 .ConditionUnknown {
412+ bundleReason = ocv1 .ReasonAbsent
413+ bundleMessage = ""
414+ }
415+
416+ SetStatusCondition (& ext .Status .Conditions , metav1.Condition {
417+ Type : ocv1 .TypeBundleDeprecated ,
418+ Status : info .BundleStatus ,
419+ Reason : bundleReason ,
420+ Message : bundleMessage ,
421+ ObservedGeneration : ext .GetGeneration (),
422+ })
423+ }
424+
425+ // buildDeprecationInfo filters the catalog deprecation data down to the package, channel,
426+ // and bundle entries that matter for this ClusterExtension. An empty bundle name means
427+ // nothing is installed yet, so we leave bundle status Unknown/Absent.
428+ func buildDeprecationInfo (ext * ocv1.ClusterExtension , installedBundleName string , deprecation * declcfg.Deprecation ) DeprecationInfo {
429+ info := DeprecationInfo {BundleStatus : metav1 .ConditionUnknown }
430+ var channelSet sets.Set [string ]
334431 if ext .Spec .Source .Catalog != nil {
335- for _ , channel := range ext .Spec .Source .Catalog .Channels {
336- channelSet . Insert ( channel )
337- }
432+ channelSet = sets . New ( ext .Spec .Source .Catalog .Channels ... )
433+ } else {
434+ channelSet = sets . New [ string ]()
338435 }
436+
339437 if deprecation != nil {
340438 for _ , entry := range deprecation .Entries {
341439 switch entry .Reference .Schema {
342440 case declcfg .SchemaPackage :
343- deprecations [ ocv1 . TypePackageDeprecated ] = []declcfg. DeprecationEntry { entry }
441+ info . PackageEntries = append ( info . PackageEntries , entry )
344442 case declcfg .SchemaChannel :
345443 if channelSet .Has (entry .Reference .Name ) {
346- deprecations [ ocv1 . TypeChannelDeprecated ] = append (deprecations [ ocv1 . TypeChannelDeprecated ] , entry )
444+ info . ChannelEntries = append (info . ChannelEntries , entry )
347445 }
348446 case declcfg .SchemaBundle :
349- if bundleName != entry .Reference .Name {
350- continue
447+ if installedBundleName != "" && entry .Reference .Name == installedBundleName {
448+ info . BundleEntries = append ( info . BundleEntries , entry )
351449 }
352- deprecations [ocv1 .TypeBundleDeprecated ] = []declcfg.DeprecationEntry {entry }
353450 }
354451 }
355452 }
356453
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- }
454+ // installedBundleName is empty when nothing is installed. In that case we want
455+ // to report the bundle deprecation condition as Unknown/Absent.
456+ if installedBundleName != "" {
457+ if len (info .BundleEntries ) > 0 {
458+ info .BundleStatus = metav1 .ConditionTrue
459+ } else {
460+ info .BundleStatus = metav1 .ConditionFalse
368461 }
369462 }
370463
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- })
464+ return info
465+ }
383466
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- }
467+ // collectDeprecationMessages collects the non-empty deprecation messages from the provided entries.
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