Skip to content

Commit d9f36f5

Browse files
author
Mikalai Radchuk
committed
Replace solver with simple catalog filtering
Signed-off-by: Mikalai Radchuk <[email protected]>
1 parent 143a98e commit d9f36f5

File tree

7 files changed

+603
-126
lines changed

7 files changed

+603
-126
lines changed

cmd/manager/main.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import (
3535
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
3636

3737
catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1"
38-
"github.com/operator-framework/deppy/pkg/deppy/solver"
3938
rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2"
4039

4140
ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
@@ -111,17 +110,10 @@ func main() {
111110
cl := mgr.GetClient()
112111
catalogClient := catalogclient.New(cl, cache.NewFilesystemCache(cachePath, &http.Client{Timeout: 10 * time.Second}))
113112

114-
resolver, err := solver.New()
115-
if err != nil {
116-
setupLog.Error(err, "unable to create a solver")
117-
os.Exit(1)
118-
}
119-
120113
if err = (&controllers.ClusterExtensionReconciler{
121114
Client: cl,
122115
BundleProvider: catalogClient,
123116
Scheme: mgr.GetScheme(),
124-
Resolver: resolver,
125117
}).SetupWithManager(mgr); err != nil {
126118
setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension")
127119
os.Exit(1)

internal/controllers/clusterextension_controller.go

Lines changed: 125 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package controllers
1919
import (
2020
"context"
2121
"fmt"
22+
"sort"
2223
"strings"
2324

25+
mmsemver "github.com/Masterminds/semver/v3"
2426
"github.com/go-logr/logr"
2527
"k8s.io/apimachinery/pkg/api/equality"
2628
apimeta "k8s.io/apimachinery/pkg/api/meta"
@@ -37,22 +39,21 @@ import (
3739
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3840

3941
catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1"
40-
"github.com/operator-framework/deppy/pkg/deppy"
41-
"github.com/operator-framework/deppy/pkg/deppy/solver"
4242
"github.com/operator-framework/operator-registry/alpha/declcfg"
4343
rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2"
4444

4545
ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
4646
"github.com/operator-framework/operator-controller/internal/catalogmetadata"
47-
olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables"
47+
catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter"
48+
catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort"
49+
"github.com/operator-framework/operator-controller/pkg/features"
4850
)
4951

5052
// ClusterExtensionReconciler reconciles a ClusterExtension object
5153
type ClusterExtensionReconciler struct {
5254
client.Client
5355
BundleProvider BundleProvider
5456
Scheme *runtime.Scheme
55-
Resolver *solver.Solver
5657
}
5758

5859
//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions,verbs=get;list;watch
@@ -116,33 +117,8 @@ func checkForUnexpectedFieldChange(a, b ocv1alpha1.ClusterExtension) bool {
116117
//
117118
//nolint:unparam
118119
func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.ClusterExtension) (ctrl.Result, error) {
119-
// gather vars for resolution
120-
vars, err := r.variables(ctx)
121-
if err != nil {
122-
ext.Status.InstalledBundle = nil
123-
setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted due to failure to gather data for resolution", ext.GetGeneration())
124-
ext.Status.ResolvedBundle = nil
125-
setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration())
126-
127-
setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted due to failure to gather data for resolution", ext.GetGeneration())
128-
return ctrl.Result{}, err
129-
}
130-
131-
// run resolution
132-
selection, err := r.Resolver.Solve(vars)
133-
if err != nil {
134-
ext.Status.InstalledBundle = nil
135-
setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration())
136-
ext.Status.ResolvedBundle = nil
137-
setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration())
138-
139-
setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as resolution failed", ext.GetGeneration())
140-
return ctrl.Result{}, err
141-
}
142-
143-
// lookup the bundle in the solution that corresponds to the
144-
// ClusterExtension's desired package name.
145-
bundle, err := r.bundleFromSolution(selection, ext.Spec.PackageName)
120+
// Lookup the bundle that corresponds to the ClusterExtension's desired package.
121+
bundle, err := r.resolve(ctx, ext)
146122
if err != nil {
147123
ext.Status.InstalledBundle = nil
148124
setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration())
@@ -202,21 +178,133 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp
202178
return ctrl.Result{}, nil
203179
}
204180

205-
func (r *ClusterExtensionReconciler) variables(ctx context.Context) ([]deppy.Variable, error) {
181+
func (r *ClusterExtensionReconciler) resolve(ctx context.Context, ext *ocv1alpha1.ClusterExtension) (*catalogmetadata.Bundle, error) {
206182
allBundles, err := r.BundleProvider.Bundles(ctx)
207183
if err != nil {
208184
return nil, err
209185
}
210-
clusterExtensionList := ocv1alpha1.ClusterExtensionList{}
211-
if err := r.Client.List(ctx, &clusterExtensionList); err != nil {
186+
187+
installedBundle, err := r.installedBundle(ctx, allBundles, ext)
188+
if err != nil {
189+
return nil, err
190+
}
191+
192+
packageName := ext.Spec.PackageName
193+
channelName := ext.Spec.Channel
194+
versionRange := ext.Spec.Version
195+
196+
predicates := []catalogfilter.Predicate[catalogmetadata.Bundle]{
197+
catalogfilter.WithPackageName(packageName),
198+
}
199+
200+
if channelName != "" {
201+
predicates = append(predicates, catalogfilter.InChannel(channelName))
202+
}
203+
204+
if versionRange != "" {
205+
vr, err := mmsemver.NewConstraint(versionRange)
206+
if err != nil {
207+
return nil, fmt.Errorf("invalid version range %q: %w", versionRange, err)
208+
}
209+
predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr))
210+
}
211+
212+
if ext.Spec.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore && installedBundle != nil {
213+
upgradePredicate, err := upgradePredicate(installedBundle)
214+
if err != nil {
215+
return nil, err
216+
}
217+
218+
predicates = append(predicates, upgradePredicate)
219+
}
220+
221+
resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...))
222+
223+
var upgradeErrorPrefix string
224+
if installedBundle != nil {
225+
installedBundleVersion, err := installedBundle.Version()
226+
if err != nil {
227+
return nil, err
228+
}
229+
upgradeErrorPrefix = fmt.Sprintf("error upgrading from currently installed version %q: ", installedBundleVersion.String())
230+
}
231+
if len(resultSet) == 0 {
232+
if versionRange != "" && channelName != "" {
233+
return nil, fmt.Errorf("%sno package %q matching version %q found in channel %q", upgradeErrorPrefix, packageName, versionRange, channelName)
234+
}
235+
if versionRange != "" {
236+
return nil, fmt.Errorf("%sno package %q matching version %q found", upgradeErrorPrefix, packageName, versionRange)
237+
}
238+
if channelName != "" {
239+
return nil, fmt.Errorf("%sno package %q found in channel %q", upgradeErrorPrefix, packageName, channelName)
240+
}
241+
return nil, fmt.Errorf("%sno package %q found", upgradeErrorPrefix, packageName)
242+
}
243+
sort.SliceStable(resultSet, func(i, j int) bool {
244+
return catalogsort.ByVersion(resultSet[i], resultSet[j])
245+
})
246+
sort.SliceStable(resultSet, func(i, j int) bool {
247+
return catalogsort.ByDeprecated(resultSet[i], resultSet[j])
248+
})
249+
250+
return resultSet[0], nil
251+
}
252+
253+
func (r *ClusterExtensionReconciler) installedBundle(ctx context.Context, allBundles []*catalogmetadata.Bundle, ext *ocv1alpha1.ClusterExtension) (*catalogmetadata.Bundle, error) {
254+
bd := &rukpakv1alpha2.BundleDeployment{}
255+
err := r.Client.Get(ctx, types.NamespacedName{Name: ext.GetName()}, bd)
256+
if client.IgnoreNotFound(err) != nil {
257+
return nil, err
258+
}
259+
260+
if bd.Spec.Source.Image == nil || bd.Spec.Source.Image.Ref == "" {
261+
// Bundle not yet installed
262+
return nil, nil
263+
}
264+
265+
bundleImage := bd.Spec.Source.Image.Ref
266+
// find corresponding bundle for the installed content
267+
resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(
268+
catalogfilter.WithPackageName(ext.Spec.PackageName),
269+
catalogfilter.WithBundleImage(bundleImage),
270+
))
271+
if len(resultSet) == 0 {
272+
return nil, fmt.Errorf("bundle with image %q for package %q not found in available catalogs but is currently installed via BundleDeployment %q", bundleImage, ext.Spec.PackageName, bd.Name)
273+
}
274+
275+
sort.SliceStable(resultSet, func(i, j int) bool {
276+
return catalogsort.ByVersion(resultSet[i], resultSet[j])
277+
})
278+
279+
return resultSet[0], nil
280+
}
281+
282+
func upgradePredicate(installedBundle *catalogmetadata.Bundle) (catalogfilter.Predicate[catalogmetadata.Bundle], error) {
283+
var successors SuccessorsPredicateFunc = LegacySemanticsSuccessorsPredicate
284+
if features.OperatorControllerFeatureGate.Enabled(features.ForceSemverUpgradeConstraints) {
285+
successors = SemverSuccessorsPredicate
286+
}
287+
288+
installedBundleVersion, err := installedBundle.Version()
289+
if err != nil {
290+
return nil, err
291+
}
292+
293+
installedVersionConstraint, err := mmsemver.NewConstraint(installedBundleVersion.String())
294+
if err != nil {
212295
return nil, err
213296
}
214-
bundleDeploymentList := rukpakv1alpha2.BundleDeploymentList{}
215-
if err := r.Client.List(ctx, &bundleDeploymentList); err != nil {
297+
298+
successorsPredicate, err := successors(installedBundle)
299+
if err != nil {
216300
return nil, err
217301
}
218302

219-
return GenerateVariables(allBundles, clusterExtensionList.Items, bundleDeploymentList.Items)
303+
// We need either successors or current version (no upgrade)
304+
return catalogfilter.Or(
305+
successorsPredicate,
306+
catalogfilter.InMastermindsSemverRange(installedVersionConstraint),
307+
), nil
220308
}
221309

222310
func mapBDStatusToInstalledCondition(existingTypedBundleDeployment *rukpakv1alpha2.BundleDeployment, ext *ocv1alpha1.ClusterExtension, bundle *catalogmetadata.Bundle) {
@@ -346,19 +434,6 @@ func SetDeprecationStatus(ext *ocv1alpha1.ClusterExtension, bundle *catalogmetad
346434
}
347435
}
348436

349-
func (r *ClusterExtensionReconciler) bundleFromSolution(selection []deppy.Variable, packageName string) (*catalogmetadata.Bundle, error) {
350-
for _, variable := range selection {
351-
switch v := variable.(type) {
352-
case *olmvariables.BundleVariable:
353-
bundlePkgName := v.Bundle().Package
354-
if packageName == bundlePkgName {
355-
return v.Bundle(), nil
356-
}
357-
}
358-
}
359-
return nil, fmt.Errorf("bundle for package %q not found in solution", packageName)
360-
}
361-
362437
func (r *ClusterExtensionReconciler) GenerateExpectedBundleDeployment(o ocv1alpha1.ClusterExtension, bundlePath string, bundleProvisioner string) *unstructured.Unstructured {
363438
// We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver.
364439
// If you use a typed object, any default values from that struct get serialized into the JSON patch, which could

internal/controllers/clusterextension_controller_test.go

Lines changed: 7 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func TestClusterExtensionNonExistentVersion(t *testing.T) {
122122
require.NotNil(t, cond)
123123
require.Equal(t, metav1.ConditionUnknown, cond.Status)
124124
require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason)
125-
require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message)
125+
require.Equal(t, "installation has not been attempted as resolution failed", cond.Message)
126126

127127
verifyInvariants(ctx, t, reconciler.Client, clusterExtension)
128128
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
@@ -608,57 +608,6 @@ func TestClusterExtensionExpectedBundleDeployment(t *testing.T) {
608608
require.NoError(t, cl.DeleteAllOf(ctx, &rukpakv1alpha2.BundleDeployment{}))
609609
}
610610

611-
func TestClusterExtensionDuplicatePackage(t *testing.T) {
612-
cl, reconciler := newClientAndReconciler(t)
613-
ctx := context.Background()
614-
extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))}
615-
const pkgName = "prometheus"
616-
617-
t.Log("When the cluster extension specifies a duplicate package")
618-
t.Log("By initializing cluster state")
619-
dupClusterExtension := &ocv1alpha1.ClusterExtension{
620-
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("orig-%s", extKey.Name)},
621-
Spec: ocv1alpha1.ClusterExtensionSpec{PackageName: pkgName},
622-
}
623-
require.NoError(t, cl.Create(ctx, dupClusterExtension))
624-
625-
clusterExtension := &ocv1alpha1.ClusterExtension{
626-
ObjectMeta: metav1.ObjectMeta{Name: extKey.Name},
627-
Spec: ocv1alpha1.ClusterExtensionSpec{PackageName: pkgName},
628-
}
629-
require.NoError(t, cl.Create(ctx, clusterExtension))
630-
631-
t.Log("It sets resolution failure status")
632-
t.Log("By running reconcile")
633-
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
634-
require.Equal(t, ctrl.Result{}, res)
635-
require.EqualError(t, err, `duplicate identifier "required package prometheus" in input`)
636-
637-
t.Log("By fetching updated cluster extension after reconcile")
638-
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
639-
640-
t.Log("By checking the status fields")
641-
require.Empty(t, clusterExtension.Status.ResolvedBundle)
642-
require.Empty(t, clusterExtension.Status.InstalledBundle)
643-
644-
t.Log("By checking the expected conditions")
645-
cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeResolved)
646-
require.NotNil(t, cond)
647-
require.Equal(t, metav1.ConditionFalse, cond.Status)
648-
require.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason)
649-
require.Equal(t, `duplicate identifier "required package prometheus" in input`, cond.Message)
650-
651-
cond = apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled)
652-
require.NotNil(t, cond)
653-
require.Equal(t, metav1.ConditionUnknown, cond.Status)
654-
require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason)
655-
require.Equal(t, "installation has not been attempted as resolution failed", cond.Message)
656-
657-
verifyInvariants(ctx, t, reconciler.Client, clusterExtension)
658-
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
659-
require.NoError(t, cl.DeleteAllOf(ctx, &rukpakv1alpha2.BundleDeployment{}))
660-
}
661-
662611
func TestClusterExtensionChannelVersionExists(t *testing.T) {
663612
cl, reconciler := newClientAndReconciler(t)
664613
ctx := context.Background()
@@ -819,7 +768,7 @@ func TestClusterExtensionVersionNoChannel(t *testing.T) {
819768
require.NotNil(t, cond)
820769
require.Equal(t, metav1.ConditionUnknown, cond.Status)
821770
require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason)
822-
require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message)
771+
require.Equal(t, "installation has not been attempted as resolution failed", cond.Message)
823772

824773
verifyInvariants(ctx, t, reconciler.Client, clusterExtension)
825774
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
@@ -867,7 +816,7 @@ func TestClusterExtensionNoChannel(t *testing.T) {
867816
require.NotNil(t, cond)
868817
require.Equal(t, metav1.ConditionUnknown, cond.Status)
869818
require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason)
870-
require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message)
819+
require.Equal(t, "installation has not been attempted as resolution failed", cond.Message)
871820

872821
verifyInvariants(ctx, t, reconciler.Client, clusterExtension)
873822
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
@@ -917,7 +866,7 @@ func TestClusterExtensionNoVersion(t *testing.T) {
917866
require.NotNil(t, cond)
918867
require.Equal(t, metav1.ConditionUnknown, cond.Status)
919868
require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason)
920-
require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message)
869+
require.Equal(t, "installation has not been attempted as resolution failed", cond.Message)
921870

922871
verifyInvariants(ctx, t, reconciler.Client, clusterExtension)
923872
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
@@ -1173,8 +1122,7 @@ func TestClusterExtensionUpgrade(t *testing.T) {
11731122
require.NotNil(t, cond)
11741123
assert.Equal(t, metav1.ConditionFalse, cond.Status)
11751124
assert.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason)
1176-
assert.Contains(t, cond.Message, "constraints not satisfiable")
1177-
assert.Regexp(t, "installed package prometheus requires at least one of fake-catalog-prometheus-operatorhub/prometheus/beta/1.2.0, fake-catalog-prometheus-operatorhub/prometheus/beta/1.0.1, fake-catalog-prometheus-operatorhub/prometheus/beta/1.0.0$", cond.Message)
1125+
assert.Equal(t, "error upgrading from currently installed version \"1.0.0\": no package \"prometheus\" matching version \"2.0.0\" found in channel \"beta\"", cond.Message)
11781126

11791127
// Valid update skipping one version
11801128
clusterExtension.Spec.Version = "1.2.0"
@@ -1266,8 +1214,7 @@ func TestClusterExtensionUpgrade(t *testing.T) {
12661214
require.NotNil(t, cond)
12671215
assert.Equal(t, metav1.ConditionFalse, cond.Status)
12681216
assert.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason)
1269-
assert.Contains(t, cond.Message, "constraints not satisfiable")
1270-
assert.Contains(t, cond.Message, "installed package prometheus requires at least one of fake-catalog-prometheus-operatorhub/prometheus/beta/1.0.1, fake-catalog-prometheus-operatorhub/prometheus/beta/1.0.0\n")
1217+
assert.Equal(t, "error upgrading from currently installed version \"1.0.0\": no package \"prometheus\" matching version \"1.2.0\" found in channel \"beta\"", cond.Message)
12711218

12721219
// Valid update skipping one version
12731220
clusterExtension.Spec.Version = "1.0.1"
@@ -1458,8 +1405,7 @@ func TestClusterExtensionDowngrade(t *testing.T) {
14581405
require.NotNil(t, cond)
14591406
assert.Equal(t, metav1.ConditionFalse, cond.Status)
14601407
assert.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason)
1461-
assert.Contains(t, cond.Message, "constraints not satisfiable")
1462-
assert.Contains(t, cond.Message, "installed package prometheus requires at least one of fake-catalog-prometheus-operatorhub/prometheus/beta/1.2.0, fake-catalog-prometheus-operatorhub/prometheus/beta/1.0.1\n")
1408+
assert.Equal(t, "error upgrading from currently installed version \"1.0.1\": no package \"prometheus\" matching version \"1.0.0\" found in channel \"beta\"", cond.Message)
14631409
})
14641410
}
14651411
})

0 commit comments

Comments
 (0)