Skip to content

Commit 2391092

Browse files
authored
Ambiguous Resolution (#1165)
Changes the behavior of catalog resolution by returning an error when multiple packages are available from multiple catalogs. Signed-off-by: dtfranz <[email protected]>
1 parent e664658 commit 2391092

File tree

3 files changed

+38
-66
lines changed

3 files changed

+38
-66
lines changed

internal/resolve/catalog.go

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterEx
5151
}
5252

5353
var (
54-
resolvedBundle *declcfg.Bundle
55-
resolvedDeprecation *declcfg.Deprecation
54+
resolvedBundles []*declcfg.Bundle
55+
matchedCatalogs []string
56+
priorDeprecation *declcfg.Deprecation
5657
)
5758

5859
listOptions := []client.ListOption{
@@ -109,30 +110,39 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterEx
109110
})
110111

111112
thisBundle := packageFBC.Bundles[0]
112-
if resolvedBundle != nil {
113-
// Cases where we stick with `resolvedBundle`:
114-
// 1. If `thisBundle` is deprecated and `resolvedBundle` is not
115-
// 2. If `thisBundle` and `resolvedBundle` have the same deprecation status AND `resolvedBundle` is a higher version
116-
if isDeprecated(thisBundle, thisDeprecation) && !isDeprecated(*resolvedBundle, resolvedDeprecation) {
117-
return nil
118-
}
119-
if compare.ByVersion(*resolvedBundle, thisBundle) < 0 {
113+
114+
if len(resolvedBundles) != 0 {
115+
// We've already found one or more package candidates
116+
currentIsDeprecated := isDeprecated(thisBundle, thisDeprecation)
117+
priorIsDeprecated := isDeprecated(*resolvedBundles[0], priorDeprecation) // Slice index doesn't matter; the whole slice is either deprecated or not
118+
if currentIsDeprecated && !priorIsDeprecated {
119+
// Skip this deprecated package and retain the non-deprecated package(s)
120120
return nil
121+
} else if !currentIsDeprecated && priorIsDeprecated {
122+
// Our package candidates so far were deprecated and this one is not; clear the lists
123+
resolvedBundles = []*declcfg.Bundle{}
124+
matchedCatalogs = []string{}
121125
}
122126
}
123-
resolvedBundle = &thisBundle
124-
resolvedDeprecation = thisDeprecation
127+
// The current bundle shares deprecation status with prior bundles or
128+
// there are no prior bundles. Add it to the list.
129+
resolvedBundles = append(resolvedBundles, &thisBundle)
130+
matchedCatalogs = append(matchedCatalogs, cat.GetName())
131+
priorDeprecation = thisDeprecation
125132
return nil
126133
}, listOptions...); err != nil {
127134
return nil, nil, nil, fmt.Errorf("error walking catalogs: %w", err)
128135
}
129136

130-
if resolvedBundle == nil {
137+
if len(resolvedBundles) != 1 {
131138
errPrefix := ""
132139
if installedBundle != nil {
133140
errPrefix = fmt.Sprintf("error upgrading from currently installed version %q: ", installedBundle.Version)
134141
}
135142
switch {
143+
case len(resolvedBundles) > 1:
144+
slices.Sort(matchedCatalogs) // sort for consistent error message
145+
return nil, nil, nil, fmt.Errorf("%smatching packages found in multiple catalogs: %v", errPrefix, matchedCatalogs)
136146
case versionRange != "" && channelName != "":
137147
return nil, nil, nil, fmt.Errorf("%sno package %q matching version %q in channel %q found", errPrefix, packageName, versionRange, channelName)
138148
case versionRange != "":
@@ -143,7 +153,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterEx
143153
return nil, nil, nil, fmt.Errorf("%sno package %q found", errPrefix, packageName)
144154
}
145155
}
146-
156+
resolvedBundle := resolvedBundles[0]
147157
resolvedBundleVersion, err := bundleutil.GetVersion(*resolvedBundle)
148158
if err != nil {
149159
return nil, nil, nil, fmt.Errorf("error getting resolved bundle version for bundle %q: %w", resolvedBundle.Name, err)
@@ -157,7 +167,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterEx
157167
}
158168
}
159169

160-
return resolvedBundle, resolvedBundleVersion, resolvedDeprecation, nil
170+
return resolvedBundle, resolvedBundleVersion, priorDeprecation, nil
161171
}
162172

163173
func isDeprecated(bundle declcfg.Bundle, deprecation *declcfg.Deprecation) bool {

internal/resolve/catalog_test.go

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1313
"k8s.io/apimachinery/pkg/labels"
1414
"k8s.io/apimachinery/pkg/util/rand"
15-
"k8s.io/apimachinery/pkg/util/sets"
1615
featuregatetesting "k8s.io/component-base/featuregate/testing"
1716
"k8s.io/utils/ptr"
1817
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -262,60 +261,25 @@ func TestPackageVariationsBetweenCatalogs(t *testing.T) {
262261
}
263262
r := CatalogResolver{WalkCatalogsFunc: w.WalkCatalogs}
264263

265-
t.Run("always prefer non-deprecated when versions match", func(t *testing.T) {
266-
for i := 0; i < 100; i++ {
267-
// When the same version exists in both catalogs, we prefer the non-deprecated one.
268-
ce := buildFooClusterExtension(pkgName, "", ">=1.0.0 <=1.0.1", ocv1alpha1.UpgradeConstraintPolicyEnforce)
269-
gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil)
270-
require.NoError(t, err)
271-
assert.Equal(t, genBundle(pkgName, "1.0.1").Name, gotBundle.Name)
272-
assert.Equal(t, bsemver.MustParse("1.0.1"), *gotVersion)
273-
assert.Nil(t, gotDeprecation)
274-
}
275-
})
276-
277264
t.Run("when catalog b has a newer version that matches the range", func(t *testing.T) {
278-
// When one version exists in one catalog but not the other, we prefer the one that exists.
279265
ce := buildFooClusterExtension(pkgName, "", ">=1.0.0 <=1.0.3", ocv1alpha1.UpgradeConstraintPolicyEnforce)
280266
gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil)
281-
require.NoError(t, err)
282-
assert.Equal(t, genBundle(pkgName, "1.0.3").Name, gotBundle.Name)
283-
assert.Equal(t, genImgRef("catalog-b", gotBundle.Name), gotBundle.Image)
284-
assert.Equal(t, bsemver.MustParse("1.0.3"), *gotVersion)
285-
assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation)
267+
require.Error(t, err)
268+
assert.ErrorContains(t, err, "found in multiple catalogs: [b c]")
269+
assert.Nil(t, gotBundle)
270+
assert.Nil(t, gotVersion)
271+
assert.Nil(t, gotDeprecation)
286272
})
287273

288274
t.Run("when catalog c has a newer version that matches the range", func(t *testing.T) {
289275
ce := buildFooClusterExtension(pkgName, "", ">=0.1.0 <1.0.0", ocv1alpha1.UpgradeConstraintPolicyEnforce)
290276
gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil)
291-
require.NoError(t, err)
292-
assert.Equal(t, genBundle(pkgName, "0.1.1").Name, gotBundle.Name)
293-
assert.Equal(t, genImgRef("catalog-c", gotBundle.Name), gotBundle.Image)
294-
assert.Equal(t, bsemver.MustParse("0.1.1"), *gotVersion)
277+
require.Error(t, err)
278+
assert.ErrorContains(t, err, "found in multiple catalogs: [b c]")
279+
assert.Nil(t, gotBundle)
280+
assert.Nil(t, gotVersion)
295281
assert.Nil(t, gotDeprecation)
296282
})
297-
298-
t.Run("when there is ambiguity between catalogs", func(t *testing.T) {
299-
// When there is no way to disambiguate between two versions, the choice is undefined.
300-
foundImages := sets.New[string]()
301-
foundDeprecations := sets.New[*declcfg.Deprecation]()
302-
for i := 0; i < 100; i++ {
303-
ce := buildFooClusterExtension(pkgName, "", "0.1.0", ocv1alpha1.UpgradeConstraintPolicyEnforce)
304-
gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil)
305-
require.NoError(t, err)
306-
assert.Equal(t, genBundle(pkgName, "0.1.0").Name, gotBundle.Name)
307-
assert.Equal(t, bsemver.MustParse("0.1.0"), *gotVersion)
308-
foundImages.Insert(gotBundle.Image)
309-
foundDeprecations.Insert(gotDeprecation)
310-
}
311-
assert.ElementsMatch(t, []string{
312-
genImgRef("catalog-b", bundleName(pkgName, "0.1.0")),
313-
genImgRef("catalog-c", bundleName(pkgName, "0.1.0")),
314-
}, foundImages.UnsortedList())
315-
316-
assert.Contains(t, foundDeprecations, (*declcfg.Deprecation)(nil))
317-
assert.Contains(t, foundDeprecations, ptr.To(packageDeprecation(pkgName)))
318-
})
319283
}
320284

321285
func TestUpgradeFoundLegacy(t *testing.T) {

test/e2e/cluster_extension_install_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,10 @@ func TestClusterExtensionInstallRegistryMultipleBundles(t *testing.T) {
306306
if !assert.NotNil(ct, cond) {
307307
return
308308
}
309-
// TODO(tmshort/dtfranz): This should fail due to multiple bundles
310-
assert.Equal(ct, metav1.ConditionTrue, cond.Status)
311-
//assert.Equal(ct, metav1.ConditionFalse, cond.Status)
312-
//assert.Equal(ct, ocv1alpha1.ReasonResolutionFailed, cond.Reason)
313-
//assert.Contains(ct, cond.Message, "TODO: matching bundles found in multiple catalogs")
314-
//assert.Nil(ct, clusterExtension.Status.ResolvedBundle)
309+
assert.Equal(ct, metav1.ConditionFalse, cond.Status)
310+
assert.Equal(ct, ocv1alpha1.ReasonResolutionFailed, cond.Reason)
311+
assert.Contains(ct, cond.Message, "matching packages found in multiple catalogs")
312+
assert.Nil(ct, clusterExtension.Status.ResolvedBundle)
315313
}, pollDuration, pollInterval)
316314
}
317315

0 commit comments

Comments
 (0)