Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,16 +436,14 @@ func run() error {
})

resolver := &resolve.CatalogResolver{
WalkCatalogsFunc: resolve.CatalogWalker(
func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) {
var catalogs ocv1.ClusterCatalogList
if err := cl.List(ctx, &catalogs, option...); err != nil {
return nil, err
}
return catalogs.Items, nil
},
catalogClient.GetPackage,
),
ListCatalogsFunc: func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) {
var catalogs ocv1.ClusterCatalogList
if err := cl.List(ctx, &catalogs, option...); err != nil {
return nil, err
}
return catalogs.Items, nil
},
GetPackageFunc: catalogClient.GetPackage,
Validations: []resolve.ValidationFunc{
resolve.NoDependencyValidation,
},
Expand Down
31 changes: 17 additions & 14 deletions docs/concepts/controlling-catalog-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ This selects catalogs that do not have the `example.com/support` label.

### Setting Catalog Priority

When multiple catalogs provide the same package, you can set priorities to resolve ambiguities. Higher priority catalogs are preferred.
When multiple available catalogs selected by a `ClusterExtension` provide the same package, you can set priorities to choose which catalog supplies it. Higher priority catalogs are considered first.

#### Defining Catalog Priority

Expand All @@ -152,23 +152,26 @@ spec:
ref: quay.io/example/high-priority-content-management:latest
```

Catalogs have a default priority of `0`. The priority can be any 32-bit integer. Catalogs with higher priority values are preferred during bundle resolution.
Catalogs have a default priority of `0`. The priority can be any 32-bit integer. Catalogs with higher priority values are considered first when selecting a catalog for a package.

#### How Priority Resolves Ambiguity

When multiple bundles match your criteria:
Catalogs are grouped by priority, from highest to lowest. For each group:

1. **Bundles from catalogs with higher priority are selected.**
2. **If multiple bundles are from catalogs with the same highest priority, and there is still ambiguity, an error is generated.**
3. **Deprecated bundles are deprioritized.** If non-deprecated bundles are available, deprecated ones are ignored.
1. If no catalog in the group contains the requested package, consider the next lower-priority group.
2. If exactly one catalog contains the package, select that catalog. Only its bundles are filtered by the requested version, channels, and upgrade constraints. If none qualifies, resolution fails rather than switching to a lower-priority catalog.
3. If multiple catalogs in the group contain the package, resolution fails due to ambiguity, even if only one has a bundle matching the requested criteria.

Within the selected catalog, non-deprecated bundles are preferred over deprecated bundles, then the highest eligible version and release is chosen. Deprecation in another catalog does not affect catalog selection.

### Handling Ambiguity Errors

If the system cannot resolve to a single bundle due to ambiguity, it will generate an error. You can resolve this by:
If multiple catalogs with the same priority contain the package, resolution fails. You can resolve this by:

- **Selecting only one catalog by name or label.**
- **Assigning different catalog priorities.**

- **Refining your catalog selection criteria.**
- **Adjusting catalog priorities.**
- **Ensuring that only one bundle matches your package name and version requirements.**
Changing the requested version or channel does not resolve package-level catalog ambiguity. If the selected catalog has no matching bundle, adjust the version, channel, or upgrade constraints, or explicitly select another catalog.

## End to End Example

Expand All @@ -195,7 +198,7 @@ If the system cannot resolve to a single bundle due to ambiguity, it will genera
metadata:
name: catalog-b
labels:
example.com/support: "false"
example.com/support: "true"
spec:
priority: 500
source:
Expand Down Expand Up @@ -241,13 +244,13 @@ If the system cannot resolve to a single bundle due to ambiguity, it will genera
kubectl get clusterextension install-my-operator -o yaml
```

The status should indicate that the bundle was resolved from `catalog-a` due to the higher priority and matching label.
Both catalogs match the selector. If both contain `my-operator`, the higher-priority `catalog-a` is selected. Check `.status.install.bundle.name` and `.status.install.bundle.version` against the bundle content published in `catalog-a`. The `ClusterExtension` status does not directly report the name of the catalog used for resolution.

## Important Notes

- **Default Behavior**: If you do not specify any catalog selection criteria, the system may select any available catalog that provides the requested package, and the choice is undefined.
- **Default Behavior**: Without a selector, every available catalog is considered. The package is resolved from the highest-priority catalog that contains it; if multiple catalogs at that priority contain it, resolution fails due to ambiguity.
- **Logical AND of Selectors**: When using both `matchLabels` and `matchExpressions`, catalogs must satisfy all criteria.
- **Deprecation Status**: Non-deprecated bundles are preferred over deprecated ones during resolution.
- **Deprecation Status**: Within the selected catalog, non-deprecated bundles are preferred over deprecated ones. Deprecation conditions reflect that catalog's package metadata even if no bundle matches the requested criteria.
- **Error Messages**: The system will update the `.status.conditions` of the `ClusterExtension` with meaningful messages if resolution fails due to ambiguity or no catalogs being selected.

## References
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func TestClusterExtensionResolutionFails(t *testing.T) {
// TestClusterExtensionResolutionFailsWithDeprecationData verifies that deprecation warnings are shown even when resolution fails.
//
// Scenario:
// - Resolution fails (package not found or version not available)
// - Bundle resolution fails after a catalog package was selected
// - Resolver returns deprecation data along with the error
// - Catalog has marked the package as deprecated
// - PackageDeprecated and Deprecated conditions show True with the deprecation message
Expand All @@ -195,7 +195,7 @@ func TestClusterExtensionResolutionFailsWithDeprecationData(t *testing.T) {
Reference: declcfg.PackageScopedReference{Schema: declcfg.SchemaPackage},
Message: deprecationMessage,
}},
}, fmt.Errorf("no package %q found", pkgName)
}, fmt.Errorf("no bundles found for package %q", pkgName)
})
})

Expand All @@ -214,7 +214,7 @@ func TestClusterExtensionResolutionFailsWithDeprecationData(t *testing.T) {

res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
require.Equal(t, ctrl.Result{}, res)
require.EqualError(t, err, fmt.Sprintf("no package %q found", pkgName))
require.EqualError(t, err, fmt.Sprintf("no bundles found for package %q", pkgName))

require.NoError(t, cl.Get(ctx, extKey, clusterExtension))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,10 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc {
installedBundleName = state.revisionStates.Installed.Name
}

// Set deprecation status based on resolution results:
// - If resolution succeeds: hasCatalogData=true, deprecation shows catalog data (nil=not deprecated)
// - If resolution fails but returns deprecation: hasCatalogData=true, show package/channel deprecation warnings
// - If resolution fails with nil deprecation: hasCatalogData=false, all conditions go Unknown
//
// Note: We DO check for deprecation data even when resolution fails (hasCatalogData = err == nil || resolvedDeprecation != nil).
// This allows us to show package/channel deprecation warnings even when we can't resolve a specific bundle.
//
// TODO: Open question - what if different catalogs have different opinions of what's deprecated?
// If we can't resolve a bundle, how do we know which catalog to trust for deprecation information?
// Perhaps if the package shows up in multiple catalogs and deprecations don't match, we can set
// the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from
// all catalogs? This needs a follow-up discussion and PR.
// Once the catalog resolver selects one package, it returns that catalog's deprecation
// data even when no bundle matches. An empty, non-nil deprecation means the selected
// catalog has no deprecation entries; nil on failure means no catalog was selected.
// Keep the success check for other resolver implementations that may return nil.
hasCatalogData := err == nil || resolvedDeprecation != nil
state.resolvedDeprecation = resolvedDeprecation
state.hasCatalogData = hasCatalogData
Expand Down
Loading
Loading