From 1ccc6dca4b4761348c6be26ffce938a1ee087c5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 01:31:35 +0000 Subject: [PATCH 1/3] Initial plan From ed15e2674966520de699ba800f024a3e0b541f8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 01:40:34 +0000 Subject: [PATCH 2/3] Improve error messages for upgrade constraint failures (issue #1022) - Enhanced resolutionError to track bundles before upgrade constraints - Added logic to distinguish between no bundles and no successors - Implemented best successor suggestions (y-stream and z-stream) - Added test case for issue #1022 scenario Co-authored-by: joelanford <580047+joelanford@users.noreply.github.com> --- .../operator-controller/resolve/catalog.go | 148 ++++++++++++++++-- .../resolve/catalog_test.go | 44 ++++++ 2 files changed, 177 insertions(+), 15 deletions(-) diff --git a/internal/operator-controller/resolve/catalog.go b/internal/operator-controller/resolve/catalog.go index f0d4da6fab..d64a56c686 100644 --- a/internal/operator-controller/resolve/catalog.go +++ b/internal/operator-controller/resolve/catalog.go @@ -77,6 +77,8 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio var catStats []*catStat var resolvedBundles []foundBundle + var bundlesBeforeUpgrade []foundBundle // Track bundles before upgrade constraints + var allSuccessors []foundBundle // Track all successors for suggestions var priorDeprecation *declcfg.Deprecation listOptions := []client.ListOption{ @@ -97,25 +99,59 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio cs.PackageFound = true cs.TotalBundles = len(packageFBC.Bundles) - var predicates []filterutil.Predicate[declcfg.Bundle] + // Build predicates for channel and version filtering + var preUpgradePredicates []filterutil.Predicate[declcfg.Bundle] if len(channels) > 0 { channelSet := sets.New(channels...) filteredChannels := slices.DeleteFunc(packageFBC.Channels, func(c declcfg.Channel) bool { return !channelSet.Has(c.Name) }) - predicates = append(predicates, filter.InAnyChannel(filteredChannels...)) + preUpgradePredicates = append(preUpgradePredicates, filter.InAnyChannel(filteredChannels...)) } if versionRangeConstraints != nil { - predicates = append(predicates, filter.InSemverRange(versionRangeConstraints)) + preUpgradePredicates = append(preUpgradePredicates, filter.InSemverRange(versionRangeConstraints)) + } + + // Apply pre-upgrade predicates to track bundles before upgrade constraints + bundlesBeforeUpgradeConstraints := slices.Clone(packageFBC.Bundles) + bundlesBeforeUpgradeConstraints = filterutil.InPlace(bundlesBeforeUpgradeConstraints, filterutil.And(preUpgradePredicates...)) + + // Track bundles before upgrade constraints for better error messages + if len(bundlesBeforeUpgradeConstraints) > 0 && ext.Spec.Source.Catalog.UpgradeConstraintPolicy != ocv1.UpgradeConstraintPolicySelfCertified && installedBundle != nil { + // Sort to get the best bundle + slices.SortStableFunc(bundlesBeforeUpgradeConstraints, compare.ByVersionAndRelease) + if len(bundlesBeforeUpgradeConstraints) > 0 { + bundlesBeforeUpgrade = append(bundlesBeforeUpgrade, foundBundle{&bundlesBeforeUpgradeConstraints[0], cat.GetName(), cat.Spec.Priority}) + } } + // Now apply upgrade constraints + var predicates []filterutil.Predicate[declcfg.Bundle] + predicates = append(predicates, preUpgradePredicates...) + if ext.Spec.Source.Catalog.UpgradeConstraintPolicy != ocv1.UpgradeConstraintPolicySelfCertified && installedBundle != nil { successorPredicate, err := filter.SuccessorsOf(*installedBundle, packageFBC.Channels...) if err != nil { return fmt.Errorf("error finding upgrade edges: %w", err) } predicates = append(predicates, successorPredicate) + + // Also collect all successors (without version constraints) for suggestions + allSuccessorBundles := slices.Clone(packageFBC.Bundles) + var successorOnlyPredicates []filterutil.Predicate[declcfg.Bundle] + if len(channels) > 0 { + channelSet := sets.New(channels...) + filteredChannels := slices.DeleteFunc(packageFBC.Channels, func(c declcfg.Channel) bool { + return !channelSet.Has(c.Name) + }) + successorOnlyPredicates = append(successorOnlyPredicates, filter.InAnyChannel(filteredChannels...)) + } + successorOnlyPredicates = append(successorOnlyPredicates, successorPredicate) + allSuccessorBundles = filterutil.InPlace(allSuccessorBundles, filterutil.And(successorOnlyPredicates...)) + for i := range allSuccessorBundles { + allSuccessors = append(allSuccessors, foundBundle{&allSuccessorBundles[i], cat.GetName(), cat.Spec.Priority}) + } } // Apply the predicates to get the candidate bundles @@ -182,11 +218,13 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio if len(resolvedBundles) != 1 { l.Info("resolution failed", "stats", catStats) return nil, nil, nil, resolutionError{ - PackageName: packageName, - Version: versionRange, - Channels: channels, - InstalledBundle: installedBundle, - ResolvedBundles: resolvedBundles, + PackageName: packageName, + Version: versionRange, + Channels: channels, + InstalledBundle: installedBundle, + ResolvedBundles: resolvedBundles, + BundlesBeforeUpgrade: bundlesBeforeUpgrade, + AllSuccessors: allSuccessors, } } resolvedBundle := resolvedBundles[0].bundle @@ -210,11 +248,13 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio } type resolutionError struct { - PackageName string - Version string - Channels []string - InstalledBundle *ocv1.BundleMetadata - ResolvedBundles []foundBundle + PackageName string + Version string + Channels []string + InstalledBundle *ocv1.BundleMetadata + ResolvedBundles []foundBundle + BundlesBeforeUpgrade []foundBundle // Bundles that matched before applying upgrade constraints + AllSuccessors []foundBundle // All successor bundles found (for suggestions) } func (rei resolutionError) Error() string { @@ -223,13 +263,29 @@ func (rei resolutionError) Error() string { sb.WriteString(fmt.Sprintf("error upgrading from currently installed version %q: ", rei.InstalledBundle.Version)) } - if len(rei.ResolvedBundles) > 1 { + // Check if we have bundles that matched before upgrade constraints + if len(rei.ResolvedBundles) == 0 && len(rei.BundlesBeforeUpgrade) > 0 { + // Bundles exist matching the version range, but none are successors + if rei.Version != "" { + sb.WriteString(fmt.Sprintf("desired package %q with version range %q does not match any successor of %q", rei.PackageName, rei.Version, rei.InstalledBundle.Version)) + } else { + sb.WriteString(fmt.Sprintf("no successor of %q found for package %q", rei.InstalledBundle.Version, rei.PackageName)) + } + + // Add suggestions for best successors if available + if len(rei.AllSuccessors) > 0 { + suggestions := rei.findBestSuccessors() + if suggestions != "" { + sb.WriteString(fmt.Sprintf(". %s", suggestions)) + } + } + } else if len(rei.ResolvedBundles) > 1 { sb.WriteString(fmt.Sprintf("found bundles for package %q ", rei.PackageName)) } else { sb.WriteString(fmt.Sprintf("no bundles found for package %q ", rei.PackageName)) } - if rei.Version != "" { + if rei.Version != "" && !(len(rei.ResolvedBundles) == 0 && len(rei.BundlesBeforeUpgrade) > 0) { sb.WriteString(fmt.Sprintf("matching version %q ", rei.Version)) } @@ -249,6 +305,68 @@ func (rei resolutionError) Error() string { return strings.TrimSpace(sb.String()) } +// findBestSuccessors finds the highest version successors in different streams +func (rei resolutionError) findBestSuccessors() string { + if len(rei.AllSuccessors) == 0 { + return "" + } + + // Parse installed version + installedVer, err := bsemver.Parse(rei.InstalledBundle.Version) + if err != nil { + return "" + } + + var zStreamHighest *declcfg.Bundle + var yStreamHighest *declcfg.Bundle + + for _, fb := range rei.AllSuccessors { + bundleVer, err := bundleutil.GetVersionAndRelease(*fb.bundle) + if err != nil { + continue + } + + // Z-stream: same major.minor, different patch + if bundleVer.Version.Major == installedVer.Major && bundleVer.Version.Minor == installedVer.Minor { + if zStreamHighest == nil { + zStreamHighest = fb.bundle + } else { + currentHighest, _ := bundleutil.GetVersionAndRelease(*zStreamHighest) + if bundleVer.Compare(*currentHighest) > 0 { + zStreamHighest = fb.bundle + } + } + } + + // Y-stream: same major, different minor + if bundleVer.Version.Major == installedVer.Major && bundleVer.Version.Minor != installedVer.Minor { + if yStreamHighest == nil { + yStreamHighest = fb.bundle + } else { + currentHighest, _ := bundleutil.GetVersionAndRelease(*yStreamHighest) + if bundleVer.Compare(*currentHighest) > 0 { + yStreamHighest = fb.bundle + } + } + } + } + + var suggestions []string + if yStreamHighest != nil { + yVer, _ := bundleutil.GetVersionAndRelease(*yStreamHighest) + suggestions = append(suggestions, fmt.Sprintf("%q (y-stream)", yVer.AsLegacyRegistryV1Version().String())) + } + if zStreamHighest != nil { + zVer, _ := bundleutil.GetVersionAndRelease(*zStreamHighest) + suggestions = append(suggestions, fmt.Sprintf("%q (z-stream)", zVer.AsLegacyRegistryV1Version().String())) + } + + if len(suggestions) > 0 { + return fmt.Sprintf("Highest version successors of %q are %s", rei.InstalledBundle.Version, strings.Join(suggestions, " and ")) + } + return "" +} + func isDeprecated(bundle declcfg.Bundle, deprecation *declcfg.Deprecation) bool { if deprecation == nil { return false diff --git a/internal/operator-controller/resolve/catalog_test.go b/internal/operator-controller/resolve/catalog_test.go index 2ec3192b69..0299152c36 100644 --- a/internal/operator-controller/resolve/catalog_test.go +++ b/internal/operator-controller/resolve/catalog_test.go @@ -526,6 +526,50 @@ func TestDowngradeNotFound(t *testing.T) { assert.EqualError(t, err, fmt.Sprintf(`error upgrading from currently installed version "1.0.2": no bundles found for package %q matching version ">0.1.0 <1.0.0"`, pkgName)) } +func TestIssue1022DowngradeNotSuccessor(t *testing.T) { + pkgName := randPkg() + + // Create a package similar to cockroachdb with versions 6.0.0, 6.0.1, etc. + fbc := &declcfg.DeclarativeConfig{ + Packages: []declcfg.Package{{Name: pkgName}}, + Channels: []declcfg.Channel{ + {Package: pkgName, Name: "stable", Entries: []declcfg.ChannelEntry{ + {Name: bundleName(pkgName, "6.0.0")}, + {Name: bundleName(pkgName, "6.0.1"), Replaces: bundleName(pkgName, "6.0.0")}, + {Name: bundleName(pkgName, "6.0.10"), Replaces: bundleName(pkgName, "6.0.1")}, + {Name: bundleName(pkgName, "6.3.11"), SkipRange: ">=6.0.0 <6.3.11"}, + }}, + }, + Bundles: []declcfg.Bundle{ + genBundle(pkgName, "6.0.0"), + genBundle(pkgName, "6.0.1"), + genBundle(pkgName, "6.0.10"), + genBundle(pkgName, "6.3.11"), + }, + } + + w := staticCatalogWalker{ + "catalog": func() (*declcfg.DeclarativeConfig, *ocv1.ClusterCatalogSpec, error) { + return fbc, nil, nil + }, + } + + r := CatalogResolver{WalkCatalogsFunc: w.WalkCatalogs} + ce := buildFooClusterExtension(pkgName, []string{}, "6.0.0", ocv1.UpgradeConstraintPolicyCatalogProvided) + installedBundle := &ocv1.BundleMetadata{ + Name: bundleName(pkgName, "6.0.1"), + Version: "6.0.1", + } + + // Try to downgrade to 6.0.0, which exists but is not a successor + _, _, _, err := r.Resolve(context.Background(), ce, installedBundle) + + // The new error message should be more helpful + expectedMsg := fmt.Sprintf(`error upgrading from currently installed version "6.0.1": desired package %q with version range "6.0.0" does not match any successor of "6.0.1". Highest version successors of "6.0.1" are "6.3.11" (y-stream) and "6.0.10" (z-stream)`, pkgName) + assert.EqualError(t, err, expectedMsg) +} + + func TestCatalogWalker(t *testing.T) { t.Run("error listing catalogs", func(t *testing.T) { w := CatalogWalker( From b1c47db68e700a859a93c21d3e46e9fe6617b721 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:51:44 +0000 Subject: [PATCH 3/3] Refactor resolver to naturally provide successor information - Introduced GetBestSuccessors() method to query successor bundles - Collect successor info before resolution walk (avoiding state mutation issues) - Enhanced error messages to distinguish "no successors" from "no bundles" - Added BundleRef type for cleaner successor tracking - Moved provider interface definitions to provider.go - All tests passing with improved error messages Co-authored-by: joelanford <580047+joelanford@users.noreply.github.com> --- .../operator-controller/resolve/catalog.go | 183 +++++++++++------- .../resolve/catalog_test.go | 3 +- .../operator-controller/resolve/provider.go | 43 ++++ .../operator-controller/resolve/resolver.go | 10 + 4 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 internal/operator-controller/resolve/provider.go diff --git a/internal/operator-controller/resolve/catalog.go b/internal/operator-controller/resolve/catalog.go index d64a56c686..5e830367b2 100644 --- a/internal/operator-controller/resolve/catalog.go +++ b/internal/operator-controller/resolve/catalog.go @@ -67,6 +67,13 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio } } + // Collect successor information BEFORE the main resolution walk + // This is used for better error messages if resolution fails + var bestSuccessors []BundleRef + if installedBundle != nil && ext.Spec.Source.Catalog.UpgradeConstraintPolicy != ocv1.UpgradeConstraintPolicySelfCertified { + bestSuccessors, _ = r.GetBestSuccessors(ctx, ext, installedBundle) + } + type catStat struct { CatalogName string `json:"catalogName"` PackageFound bool `json:"packageFound"` @@ -77,8 +84,6 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio var catStats []*catStat var resolvedBundles []foundBundle - var bundlesBeforeUpgrade []foundBundle // Track bundles before upgrade constraints - var allSuccessors []foundBundle // Track all successors for suggestions var priorDeprecation *declcfg.Deprecation listOptions := []client.ListOption{ @@ -99,59 +104,25 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio cs.PackageFound = true cs.TotalBundles = len(packageFBC.Bundles) - // Build predicates for channel and version filtering - var preUpgradePredicates []filterutil.Predicate[declcfg.Bundle] + var predicates []filterutil.Predicate[declcfg.Bundle] if len(channels) > 0 { channelSet := sets.New(channels...) filteredChannels := slices.DeleteFunc(packageFBC.Channels, func(c declcfg.Channel) bool { return !channelSet.Has(c.Name) }) - preUpgradePredicates = append(preUpgradePredicates, filter.InAnyChannel(filteredChannels...)) + predicates = append(predicates, filter.InAnyChannel(filteredChannels...)) } if versionRangeConstraints != nil { - preUpgradePredicates = append(preUpgradePredicates, filter.InSemverRange(versionRangeConstraints)) - } - - // Apply pre-upgrade predicates to track bundles before upgrade constraints - bundlesBeforeUpgradeConstraints := slices.Clone(packageFBC.Bundles) - bundlesBeforeUpgradeConstraints = filterutil.InPlace(bundlesBeforeUpgradeConstraints, filterutil.And(preUpgradePredicates...)) - - // Track bundles before upgrade constraints for better error messages - if len(bundlesBeforeUpgradeConstraints) > 0 && ext.Spec.Source.Catalog.UpgradeConstraintPolicy != ocv1.UpgradeConstraintPolicySelfCertified && installedBundle != nil { - // Sort to get the best bundle - slices.SortStableFunc(bundlesBeforeUpgradeConstraints, compare.ByVersionAndRelease) - if len(bundlesBeforeUpgradeConstraints) > 0 { - bundlesBeforeUpgrade = append(bundlesBeforeUpgrade, foundBundle{&bundlesBeforeUpgradeConstraints[0], cat.GetName(), cat.Spec.Priority}) - } + predicates = append(predicates, filter.InSemverRange(versionRangeConstraints)) } - // Now apply upgrade constraints - var predicates []filterutil.Predicate[declcfg.Bundle] - predicates = append(predicates, preUpgradePredicates...) - if ext.Spec.Source.Catalog.UpgradeConstraintPolicy != ocv1.UpgradeConstraintPolicySelfCertified && installedBundle != nil { successorPredicate, err := filter.SuccessorsOf(*installedBundle, packageFBC.Channels...) if err != nil { return fmt.Errorf("error finding upgrade edges: %w", err) } predicates = append(predicates, successorPredicate) - - // Also collect all successors (without version constraints) for suggestions - allSuccessorBundles := slices.Clone(packageFBC.Bundles) - var successorOnlyPredicates []filterutil.Predicate[declcfg.Bundle] - if len(channels) > 0 { - channelSet := sets.New(channels...) - filteredChannels := slices.DeleteFunc(packageFBC.Channels, func(c declcfg.Channel) bool { - return !channelSet.Has(c.Name) - }) - successorOnlyPredicates = append(successorOnlyPredicates, filter.InAnyChannel(filteredChannels...)) - } - successorOnlyPredicates = append(successorOnlyPredicates, successorPredicate) - allSuccessorBundles = filterutil.InPlace(allSuccessorBundles, filterutil.And(successorOnlyPredicates...)) - for i := range allSuccessorBundles { - allSuccessors = append(allSuccessors, foundBundle{&allSuccessorBundles[i], cat.GetName(), cat.Spec.Priority}) - } } // Apply the predicates to get the candidate bundles @@ -217,14 +188,14 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio // Check for ambiguity if len(resolvedBundles) != 1 { l.Info("resolution failed", "stats", catStats) + return nil, nil, nil, resolutionError{ - PackageName: packageName, - Version: versionRange, - Channels: channels, - InstalledBundle: installedBundle, - ResolvedBundles: resolvedBundles, - BundlesBeforeUpgrade: bundlesBeforeUpgrade, - AllSuccessors: allSuccessors, + PackageName: packageName, + Version: versionRange, + Channels: channels, + InstalledBundle: installedBundle, + ResolvedBundles: resolvedBundles, + BestSuccessors: bestSuccessors, } } resolvedBundle := resolvedBundles[0].bundle @@ -247,14 +218,70 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio return resolvedBundle, resolvedBundleVersion, priorDeprecation, nil } +// GetBestSuccessors returns the best available successor bundles ignoring version range and channel filters. +// This provides helpful information when resolution fails. +func (r *CatalogResolver) GetBestSuccessors(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) ([]BundleRef, error) { + if installedBundle == nil { + return nil, nil + } + + packageName := ext.Spec.Source.Catalog.PackageName + + // unless overridden, default to selecting all bundles + var selector = labels.Everything() + var err error + if ext.Spec.Source.Catalog != nil { + selector, err = metav1.LabelSelectorAsSelector(ext.Spec.Source.Catalog.Selector) + if err != nil { + return nil, fmt.Errorf("desired catalog selector is invalid: %w", err) + } + if selector == labels.Nothing() { + selector = labels.Everything() + } + } + + var allSuccessors []BundleRef + + listOptions := []client.ListOption{ + client.MatchingLabelsSelector{Selector: selector}, + } + + if err := r.WalkCatalogsFunc(ctx, packageName, func(ctx context.Context, cat *ocv1.ClusterCatalog, packageFBC *declcfg.DeclarativeConfig, err error) error { + if err != nil || isFBCEmpty(packageFBC) { + return nil + } + + // Only apply successor filter, no version or channel filters + successorPredicate, err := filter.SuccessorsOf(*installedBundle, packageFBC.Channels...) + if err != nil { + return nil // Skip on error + } + + successorBundles := slices.Clone(packageFBC.Bundles) + successorBundles = filterutil.InPlace(successorBundles, successorPredicate) + + for i := range successorBundles { + allSuccessors = append(allSuccessors, BundleRef{ + Bundle: &successorBundles[i], + Catalog: cat.GetName(), + Priority: cat.Spec.Priority, + }) + } + return nil + }, listOptions...); err != nil { + return nil, err + } + + return allSuccessors, nil +} + type resolutionError struct { - PackageName string - Version string - Channels []string - InstalledBundle *ocv1.BundleMetadata - ResolvedBundles []foundBundle - BundlesBeforeUpgrade []foundBundle // Bundles that matched before applying upgrade constraints - AllSuccessors []foundBundle // All successor bundles found (for suggestions) + PackageName string + Version string + Channels []string + InstalledBundle *ocv1.BundleMetadata + ResolvedBundles []foundBundle + BestSuccessors []BundleRef // Best available successors (for better error messages) } func (rei resolutionError) Error() string { @@ -263,29 +290,30 @@ func (rei resolutionError) Error() string { sb.WriteString(fmt.Sprintf("error upgrading from currently installed version %q: ", rei.InstalledBundle.Version)) } - // Check if we have bundles that matched before upgrade constraints - if len(rei.ResolvedBundles) == 0 && len(rei.BundlesBeforeUpgrade) > 0 { - // Bundles exist matching the version range, but none are successors + // Check if we have successor information for better error messages + if len(rei.ResolvedBundles) == 0 && rei.InstalledBundle != nil && len(rei.BestSuccessors) > 0 { + // We have successors available, so the version range doesn't match any successor if rei.Version != "" { sb.WriteString(fmt.Sprintf("desired package %q with version range %q does not match any successor of %q", rei.PackageName, rei.Version, rei.InstalledBundle.Version)) } else { sb.WriteString(fmt.Sprintf("no successor of %q found for package %q", rei.InstalledBundle.Version, rei.PackageName)) } - - // Add suggestions for best successors if available - if len(rei.AllSuccessors) > 0 { - suggestions := rei.findBestSuccessors() - if suggestions != "" { - sb.WriteString(fmt.Sprintf(". %s", suggestions)) - } + + // Add best successor suggestions + suggestion := findBestSuccessors(rei.BestSuccessors, rei.InstalledBundle) + if suggestion != "" { + sb.WriteString(fmt.Sprintf(". %s", suggestion)) } - } else if len(rei.ResolvedBundles) > 1 { + return strings.TrimSpace(sb.String()) + } + + if len(rei.ResolvedBundles) > 1 { sb.WriteString(fmt.Sprintf("found bundles for package %q ", rei.PackageName)) } else { sb.WriteString(fmt.Sprintf("no bundles found for package %q ", rei.PackageName)) } - if rei.Version != "" && !(len(rei.ResolvedBundles) == 0 && len(rei.BundlesBeforeUpgrade) > 0) { + if rei.Version != "" { sb.WriteString(fmt.Sprintf("matching version %q ", rei.Version)) } @@ -306,13 +334,13 @@ func (rei resolutionError) Error() string { } // findBestSuccessors finds the highest version successors in different streams -func (rei resolutionError) findBestSuccessors() string { - if len(rei.AllSuccessors) == 0 { +func findBestSuccessors(successors []BundleRef, installedBundle *ocv1.BundleMetadata) string { + if len(successors) == 0 { return "" } // Parse installed version - installedVer, err := bsemver.Parse(rei.InstalledBundle.Version) + installedVer, err := bsemver.Parse(installedBundle.Version) if err != nil { return "" } @@ -320,20 +348,25 @@ func (rei resolutionError) findBestSuccessors() string { var zStreamHighest *declcfg.Bundle var yStreamHighest *declcfg.Bundle - for _, fb := range rei.AllSuccessors { - bundleVer, err := bundleutil.GetVersionAndRelease(*fb.bundle) + for _, bundleRef := range successors { + bundleVer, err := bundleutil.GetVersionAndRelease(*bundleRef.Bundle) if err != nil { continue } + // Skip the currently installed version itself + if bundleVer.Version.EQ(installedVer) { + continue + } + // Z-stream: same major.minor, different patch if bundleVer.Version.Major == installedVer.Major && bundleVer.Version.Minor == installedVer.Minor { if zStreamHighest == nil { - zStreamHighest = fb.bundle + zStreamHighest = bundleRef.Bundle } else { currentHighest, _ := bundleutil.GetVersionAndRelease(*zStreamHighest) if bundleVer.Compare(*currentHighest) > 0 { - zStreamHighest = fb.bundle + zStreamHighest = bundleRef.Bundle } } } @@ -341,11 +374,11 @@ func (rei resolutionError) findBestSuccessors() string { // Y-stream: same major, different minor if bundleVer.Version.Major == installedVer.Major && bundleVer.Version.Minor != installedVer.Minor { if yStreamHighest == nil { - yStreamHighest = fb.bundle + yStreamHighest = bundleRef.Bundle } else { currentHighest, _ := bundleutil.GetVersionAndRelease(*yStreamHighest) if bundleVer.Compare(*currentHighest) > 0 { - yStreamHighest = fb.bundle + yStreamHighest = bundleRef.Bundle } } } @@ -362,7 +395,7 @@ func (rei resolutionError) findBestSuccessors() string { } if len(suggestions) > 0 { - return fmt.Sprintf("Highest version successors of %q are %s", rei.InstalledBundle.Version, strings.Join(suggestions, " and ")) + return fmt.Sprintf("Highest version successors of %q are %s", installedBundle.Version, strings.Join(suggestions, " and ")) } return "" } diff --git a/internal/operator-controller/resolve/catalog_test.go b/internal/operator-controller/resolve/catalog_test.go index 0299152c36..bd1f1c70c0 100644 --- a/internal/operator-controller/resolve/catalog_test.go +++ b/internal/operator-controller/resolve/catalog_test.go @@ -471,7 +471,8 @@ func TestUpgradeNotFoundLegacy(t *testing.T) { } // 0.1.0 only upgrades to 1.0.x with its legacy upgrade edges, so this fails. _, _, _, err := r.Resolve(context.Background(), ce, installedBundle) - assert.EqualError(t, err, fmt.Sprintf(`error upgrading from currently installed version "0.1.0": no bundles found for package %q matching version "<1.0.0 >=2.0.0"`, pkgName)) + // The new error message indicates that the version range doesn't match any successor + assert.EqualError(t, err, fmt.Sprintf(`error upgrading from currently installed version "0.1.0": desired package %q with version range "<1.0.0 >=2.0.0" does not match any successor of "0.1.0"`, pkgName)) } func TestDowngradeFound(t *testing.T) { diff --git a/internal/operator-controller/resolve/provider.go b/internal/operator-controller/resolve/provider.go new file mode 100644 index 0000000000..fcb6d860a9 --- /dev/null +++ b/internal/operator-controller/resolve/provider.go @@ -0,0 +1,43 @@ +package resolve + +import ( + "context" + + "github.com/operator-framework/operator-registry/alpha/declcfg" +) + +// PackageProvider defines an API for providing bundle and deprecation info +// from a catalog for a specific package without any filtering applied. +type PackageProvider interface { + // GetPackage returns the raw package data (bundles, channels, deprecations) + // for the specified package from catalogs matching the selector. + GetPackage(ctx context.Context, packageName string, selector CatalogSelector) (*PackageData, error) +} + +// CatalogSelector defines criteria for selecting catalogs +type CatalogSelector struct { + // LabelSelector filters catalogs by labels + LabelSelector string +} + +// PackageData contains unfiltered package information from one or more catalogs +type PackageData struct { + // CatalogPackages maps catalog name to its package data + CatalogPackages map[string]*CatalogPackage +} + +// CatalogPackage represents package data from a single catalog +type CatalogPackage struct { + Name string + Priority int32 + Bundles []declcfg.Bundle + Channels []declcfg.Channel + Deprecations []declcfg.Deprecation +} + +// BundleRef references a bundle with its source catalog +type BundleRef struct { + Bundle *declcfg.Bundle + Catalog string + Priority int32 +} diff --git a/internal/operator-controller/resolve/resolver.go b/internal/operator-controller/resolve/resolver.go index 1fbde0fdea..0eaa1cb96a 100644 --- a/internal/operator-controller/resolve/resolver.go +++ b/internal/operator-controller/resolve/resolver.go @@ -9,10 +9,20 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" ) +// Resolver defines the interface for resolving bundles based on ClusterExtension specs type Resolver interface { + // Resolve returns a Bundle from a catalog that needs to get installed on the cluster. Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) } +// SuccessorQuerier is an optional interface that resolvers can implement to provide +// information about available successors. This is useful for generating helpful error messages. +type SuccessorQuerier interface { + // GetBestSuccessors returns the best available successor bundles for the currently + // installed bundle, ignoring version range and channel filters. + GetBestSuccessors(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) ([]BundleRef, error) +} + type Func func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) func (f Func) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) {