diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 64038ed3c..3b16bf285 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -30,8 +30,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/server" - "github.com/operator-framework/deppy/pkg/deppy/solver" - "github.com/operator-framework/operator-controller/internal/catalogmetadata/cache" catalogclient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client" "github.com/operator-framework/operator-controller/internal/controllers" @@ -93,17 +91,10 @@ func main() { cl := mgr.GetClient() catalogClient := catalogclient.New(cl, cache.NewFilesystemCache(cachePath, &http.Client{Timeout: 10 * time.Second})) - resolver, err := solver.New() - if err != nil { - setupLog.Error(err, "unable to create a solver") - os.Exit(1) - } - if err = (&controllers.ClusterExtensionReconciler{ Client: cl, BundleProvider: catalogClient, Scheme: mgr.GetScheme(), - Resolver: resolver, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") os.Exit(1) diff --git a/go.mod b/go.mod index d00b6856c..cdd9525be 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/go-logr/logr v1.4.1 github.com/google/go-cmp v0.6.0 github.com/operator-framework/catalogd v0.12.0 - github.com/operator-framework/deppy v0.3.0 github.com/operator-framework/operator-registry v1.40.0 github.com/operator-framework/rukpak v0.19.0 github.com/spf13/pflag v1.0.5 @@ -36,7 +35,6 @@ require ( github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-air/gini v1.0.4 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.11.0 // indirect diff --git a/go.sum b/go.sum index b90d8ad02..e74f88266 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1 github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-air/gini v1.0.4 h1:lteMAxHKNOAjIqazL/klOJJmxq6YxxSuJ17MnMXny+s= -github.com/go-air/gini v1.0.4/go.mod h1:dd8RvT1xcv6N1da33okvBd8DhMh1/A4siGy6ErjTljs= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= @@ -105,8 +103,6 @@ github.com/operator-framework/api v0.23.0 h1:kHymOwcHBpBVujT49SKOCd4EVG7Odwj4wl3 github.com/operator-framework/api v0.23.0/go.mod h1:oKcFOz+Xc1UhMi2Pzcp6qsO7wjS4r+yP7EQprQBXrfM= github.com/operator-framework/catalogd v0.12.0 h1:Cww+CyowkfTFugB9ZjUDpKvumh2vPe/TjCUpMHDmVBM= github.com/operator-framework/catalogd v0.12.0/go.mod h1:4lryGtBTVOdqlKR0MaVYnlsSOc7HiagVRVo3J4uIo7E= -github.com/operator-framework/deppy v0.3.0 h1:W8wpF0ehcTAdH2WfMyqMPI5Ja0Qv8M5FMO5cXgJvEQ8= -github.com/operator-framework/deppy v0.3.0/go.mod h1:EHDxZz8fKGvuymCng3G/Ou7wuX14GaLr0cmf2u29Oog= github.com/operator-framework/operator-registry v1.40.0 h1:CaYNE4F/jzahpC7UCILItaIHmB5/oE0sS066nK+5Glw= github.com/operator-framework/operator-registry v1.40.0/go.mod h1:D2YxapkfRDgjqNTO9d3h3v0DeREbV+8utCLG52zrOy4= github.com/operator-framework/rukpak v0.19.0 h1:8cW43z4jsvARlsmj2eum5bAsZEvSxqDwfMW3dSq1zq8= diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index b71baaf2c..42913e77e 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -19,8 +19,10 @@ package controllers import ( "context" "fmt" + "sort" "strings" + mmsemver "github.com/Masterminds/semver/v3" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/equality" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -37,14 +39,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/solver" "github.com/operator-framework/operator-registry/alpha/declcfg" rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" + catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" + catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" ) // ClusterExtensionReconciler reconciles a ClusterExtension object @@ -52,7 +53,6 @@ type ClusterExtensionReconciler struct { client.Client BundleProvider BundleProvider Scheme *runtime.Scheme - Resolver *solver.Solver } //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions,verbs=get;list;watch @@ -116,33 +116,8 @@ func checkForUnexpectedFieldChange(a, b ocv1alpha1.ClusterExtension) bool { // //nolint:unparam func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.ClusterExtension) (ctrl.Result, error) { - // gather vars for resolution - vars, err := r.variables(ctx) - if err != nil { - ext.Status.InstalledBundle = nil - setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted due to failure to gather data for resolution", ext.GetGeneration()) - ext.Status.ResolvedBundle = nil - setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) - - setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted due to failure to gather data for resolution", ext.GetGeneration()) - return ctrl.Result{}, err - } - - // run resolution - selection, err := r.Resolver.Solve(vars) - if err != nil { - ext.Status.InstalledBundle = nil - setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration()) - ext.Status.ResolvedBundle = nil - setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) - - setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as resolution failed", ext.GetGeneration()) - return ctrl.Result{}, err - } - - // lookup the bundle in the solution that corresponds to the - // ClusterExtension's desired package name. - bundle, err := r.bundleFromSolution(selection, ext.Spec.PackageName) + // Lookup the bundle that corresponds to the ClusterExtension's desired package. + bundle, err := r.resolve(ctx, ext) if err != nil { ext.Status.InstalledBundle = nil setInstalledStatusConditionUnknown(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration()) @@ -202,21 +177,105 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp return ctrl.Result{}, nil } -func (r *ClusterExtensionReconciler) variables(ctx context.Context) ([]deppy.Variable, error) { +func (r *ClusterExtensionReconciler) resolve(ctx context.Context, ext *ocv1alpha1.ClusterExtension) (*catalogmetadata.Bundle, error) { allBundles, err := r.BundleProvider.Bundles(ctx) if err != nil { return nil, err } - clusterExtensionList := ocv1alpha1.ClusterExtensionList{} - if err := r.Client.List(ctx, &clusterExtensionList); err != nil { + + installedBundle, err := r.installedBundle(ctx, allBundles, ext) + if err != nil { return nil, err } - bundleDeploymentList := rukpakv1alpha2.BundleDeploymentList{} - if err := r.Client.List(ctx, &bundleDeploymentList); err != nil { + + packageName := ext.Spec.PackageName + channelName := ext.Spec.Channel + versionRange := ext.Spec.Version + + predicates := []catalogfilter.Predicate[catalogmetadata.Bundle]{ + catalogfilter.WithPackageName(packageName), + } + + if channelName != "" { + predicates = append(predicates, catalogfilter.InChannel(channelName)) + } + + if versionRange != "" { + vr, err := mmsemver.NewConstraint(versionRange) + if err != nil { + return nil, fmt.Errorf("invalid version range %q: %w", versionRange, err) + } + predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) + } + + if ext.Spec.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore && installedBundle != nil { + upgradePredicate, err := SuccessorsPredicate(installedBundle) + if err != nil { + return nil, err + } + + predicates = append(predicates, upgradePredicate) + } + + resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...)) + + var upgradeErrorPrefix string + if installedBundle != nil { + installedBundleVersion, err := installedBundle.Version() + if err != nil { + return nil, err + } + upgradeErrorPrefix = fmt.Sprintf("error upgrading from currently installed version %q: ", installedBundleVersion.String()) + } + if len(resultSet) == 0 { + if versionRange != "" && channelName != "" { + return nil, fmt.Errorf("%sno package %q matching version %q found in channel %q", upgradeErrorPrefix, packageName, versionRange, channelName) + } + if versionRange != "" { + return nil, fmt.Errorf("%sno package %q matching version %q found", upgradeErrorPrefix, packageName, versionRange) + } + if channelName != "" { + return nil, fmt.Errorf("%sno package %q found in channel %q", upgradeErrorPrefix, packageName, channelName) + } + return nil, fmt.Errorf("%sno package %q found", upgradeErrorPrefix, packageName) + } + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByVersion(resultSet[i], resultSet[j]) + }) + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByDeprecated(resultSet[i], resultSet[j]) + }) + + return resultSet[0], nil +} + +func (r *ClusterExtensionReconciler) installedBundle(ctx context.Context, allBundles []*catalogmetadata.Bundle, ext *ocv1alpha1.ClusterExtension) (*catalogmetadata.Bundle, error) { + bd := &rukpakv1alpha2.BundleDeployment{} + err := r.Client.Get(ctx, types.NamespacedName{Name: ext.GetName()}, bd) + if client.IgnoreNotFound(err) != nil { return nil, err } - return GenerateVariables(allBundles, clusterExtensionList.Items, bundleDeploymentList.Items) + if bd.Spec.Source.Image == nil || bd.Spec.Source.Image.Ref == "" { + // Bundle not yet installed + return nil, nil + } + + bundleImage := bd.Spec.Source.Image.Ref + // find corresponding bundle for the installed content + resultSet := catalogfilter.Filter(allBundles, catalogfilter.And( + catalogfilter.WithPackageName(ext.Spec.PackageName), + catalogfilter.WithBundleImage(bundleImage), + )) + if len(resultSet) == 0 { + 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) + } + + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByVersion(resultSet[i], resultSet[j]) + }) + + return resultSet[0], nil } func mapBDStatusToInstalledCondition(existingTypedBundleDeployment *rukpakv1alpha2.BundleDeployment, ext *ocv1alpha1.ClusterExtension, bundle *catalogmetadata.Bundle) { @@ -346,19 +405,6 @@ func SetDeprecationStatus(ext *ocv1alpha1.ClusterExtension, bundle *catalogmetad } } -func (r *ClusterExtensionReconciler) bundleFromSolution(selection []deppy.Variable, packageName string) (*catalogmetadata.Bundle, error) { - for _, variable := range selection { - switch v := variable.(type) { - case *olmvariables.BundleVariable: - bundlePkgName := v.Bundle().Package - if packageName == bundlePkgName { - return v.Bundle(), nil - } - } - } - return nil, fmt.Errorf("bundle for package %q not found in solution", packageName) -} - func (r *ClusterExtensionReconciler) GenerateExpectedBundleDeployment(o ocv1alpha1.ClusterExtension, bundlePath string, bundleProvisioner string) *unstructured.Unstructured { // We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver. // If you use a typed object, any default values from that struct get serialized into the JSON patch, which could diff --git a/internal/controllers/clusterextension_controller_test.go b/internal/controllers/clusterextension_controller_test.go index 5f5b7fdb4..2145f55a4 100644 --- a/internal/controllers/clusterextension_controller_test.go +++ b/internal/controllers/clusterextension_controller_test.go @@ -122,7 +122,7 @@ func TestClusterExtensionNonExistentVersion(t *testing.T) { require.NotNil(t, cond) require.Equal(t, metav1.ConditionUnknown, cond.Status) require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason) - require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message) + require.Equal(t, "installation has not been attempted as resolution failed", cond.Message) verifyInvariants(ctx, t, reconciler.Client, clusterExtension) require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) @@ -608,57 +608,6 @@ func TestClusterExtensionExpectedBundleDeployment(t *testing.T) { require.NoError(t, cl.DeleteAllOf(ctx, &rukpakv1alpha2.BundleDeployment{})) } -func TestClusterExtensionDuplicatePackage(t *testing.T) { - cl, reconciler := newClientAndReconciler(t) - ctx := context.Background() - extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} - const pkgName = "prometheus" - - t.Log("When the cluster extension specifies a duplicate package") - t.Log("By initializing cluster state") - dupClusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("orig-%s", extKey.Name)}, - Spec: ocv1alpha1.ClusterExtensionSpec{PackageName: pkgName}, - } - require.NoError(t, cl.Create(ctx, dupClusterExtension)) - - clusterExtension := &ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, - Spec: ocv1alpha1.ClusterExtensionSpec{PackageName: pkgName}, - } - require.NoError(t, cl.Create(ctx, clusterExtension)) - - t.Log("It sets resolution failure status") - t.Log("By running reconcile") - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - require.Equal(t, ctrl.Result{}, res) - require.EqualError(t, err, `duplicate identifier "required package prometheus" in input`) - - t.Log("By fetching updated cluster extension after reconcile") - require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) - - t.Log("By checking the status fields") - require.Empty(t, clusterExtension.Status.ResolvedBundle) - require.Empty(t, clusterExtension.Status.InstalledBundle) - - t.Log("By checking the expected conditions") - cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeResolved) - require.NotNil(t, cond) - require.Equal(t, metav1.ConditionFalse, cond.Status) - require.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason) - require.Equal(t, `duplicate identifier "required package prometheus" in input`, cond.Message) - - cond = apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) - require.NotNil(t, cond) - require.Equal(t, metav1.ConditionUnknown, cond.Status) - require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason) - require.Equal(t, "installation has not been attempted as resolution failed", cond.Message) - - verifyInvariants(ctx, t, reconciler.Client, clusterExtension) - require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) - require.NoError(t, cl.DeleteAllOf(ctx, &rukpakv1alpha2.BundleDeployment{})) -} - func TestClusterExtensionChannelVersionExists(t *testing.T) { cl, reconciler := newClientAndReconciler(t) ctx := context.Background() @@ -819,7 +768,7 @@ func TestClusterExtensionVersionNoChannel(t *testing.T) { require.NotNil(t, cond) require.Equal(t, metav1.ConditionUnknown, cond.Status) require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason) - require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message) + require.Equal(t, "installation has not been attempted as resolution failed", cond.Message) verifyInvariants(ctx, t, reconciler.Client, clusterExtension) require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) @@ -867,7 +816,7 @@ func TestClusterExtensionNoChannel(t *testing.T) { require.NotNil(t, cond) require.Equal(t, metav1.ConditionUnknown, cond.Status) require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason) - require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message) + require.Equal(t, "installation has not been attempted as resolution failed", cond.Message) verifyInvariants(ctx, t, reconciler.Client, clusterExtension) require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) @@ -917,7 +866,7 @@ func TestClusterExtensionNoVersion(t *testing.T) { require.NotNil(t, cond) require.Equal(t, metav1.ConditionUnknown, cond.Status) require.Equal(t, ocv1alpha1.ReasonInstallationStatusUnknown, cond.Reason) - require.Equal(t, "installation has not been attempted due to failure to gather data for resolution", cond.Message) + require.Equal(t, "installation has not been attempted as resolution failed", cond.Message) verifyInvariants(ctx, t, reconciler.Client, clusterExtension) require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{})) @@ -1173,8 +1122,7 @@ func TestClusterExtensionUpgrade(t *testing.T) { require.NotNil(t, cond) assert.Equal(t, metav1.ConditionFalse, cond.Status) assert.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason) - assert.Contains(t, cond.Message, "constraints not satisfiable") - 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) + 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) // Valid update skipping one version clusterExtension.Spec.Version = "1.2.0" @@ -1266,8 +1214,7 @@ func TestClusterExtensionUpgrade(t *testing.T) { require.NotNil(t, cond) assert.Equal(t, metav1.ConditionFalse, cond.Status) assert.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason) - assert.Contains(t, cond.Message, "constraints not satisfiable") - 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") + 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) // Valid update skipping one version clusterExtension.Spec.Version = "1.0.1" @@ -1458,8 +1405,7 @@ func TestClusterExtensionDowngrade(t *testing.T) { require.NotNil(t, cond) assert.Equal(t, metav1.ConditionFalse, cond.Status) assert.Equal(t, ocv1alpha1.ReasonResolutionFailed, cond.Reason) - assert.Contains(t, cond.Message, "constraints not satisfiable") - 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") + 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) }) } }) diff --git a/internal/controllers/successors.go b/internal/controllers/successors.go new file mode 100644 index 000000000..04283d70f --- /dev/null +++ b/internal/controllers/successors.go @@ -0,0 +1,79 @@ +package controllers + +import ( + "fmt" + + mmsemver "github.com/Masterminds/semver/v3" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" + "github.com/operator-framework/operator-controller/pkg/features" +) + +func SuccessorsPredicate(installedBundle *catalogmetadata.Bundle) (catalogfilter.Predicate[catalogmetadata.Bundle], error) { + var successors successorsPredicateFunc = legacySemanticsSuccessorsPredicate + if features.OperatorControllerFeatureGate.Enabled(features.ForceSemverUpgradeConstraints) { + successors = semverSuccessorsPredicate + } + + installedBundleVersion, err := installedBundle.Version() + if err != nil { + return nil, err + } + + installedVersionConstraint, err := mmsemver.NewConstraint(installedBundleVersion.String()) + if err != nil { + return nil, err + } + + successorsPredicate, err := successors(installedBundle) + if err != nil { + return nil, err + } + + // We need either successors or current version (no upgrade) + return catalogfilter.Or( + successorsPredicate, + catalogfilter.And( + catalogfilter.WithPackageName(installedBundle.Package), + catalogfilter.InMastermindsSemverRange(installedVersionConstraint), + ), + ), nil +} + +// successorsPredicateFunc returns a predicate to find successors +// for a bundle. Predicate must not include the current version. +type successorsPredicateFunc func(bundle *catalogmetadata.Bundle) (catalogfilter.Predicate[catalogmetadata.Bundle], error) + +// legacySemanticsSuccessorsPredicate returns a predicate to find successors +// based on legacy OLMv0 semantics which rely on Replaces, Skips and skipRange. +func legacySemanticsSuccessorsPredicate(bundle *catalogmetadata.Bundle) (catalogfilter.Predicate[catalogmetadata.Bundle], error) { + // find the bundles that replace, skip, or skipRange the bundle provided + return catalogfilter.And( + catalogfilter.WithPackageName(bundle.Package), + catalogfilter.LegacySuccessor(bundle), + ), nil +} + +// semverSuccessorsPredicate returns a predicate to find successors based on Semver. +// Successors will not include versions outside the major version of the +// installed bundle as major version is intended to indicate breaking changes. +func semverSuccessorsPredicate(bundle *catalogmetadata.Bundle) (catalogfilter.Predicate[catalogmetadata.Bundle], error) { + currentVersion, err := bundle.Version() + if err != nil { + return nil, err + } + + // Based on current version create a caret range comparison constraint + // to allow only minor and patch version as successors and exclude current version. + constraintStr := fmt.Sprintf("^%s, != %s", currentVersion.String(), currentVersion.String()) + wantedVersionRangeConstraint, err := mmsemver.NewConstraint(constraintStr) + if err != nil { + return nil, err + } + + return catalogfilter.And( + catalogfilter.WithPackageName(bundle.Package), + catalogfilter.InMastermindsSemverRange(wantedVersionRangeConstraint), + ), nil +} diff --git a/internal/resolution/variablesources/installed_package_test.go b/internal/controllers/successors_test.go similarity index 56% rename from internal/resolution/variablesources/installed_package_test.go rename to internal/controllers/successors_test.go index 3c488947b..cda5261ad 100644 --- a/internal/resolution/variablesources/installed_package_test.go +++ b/internal/controllers/successors_test.go @@ -1,7 +1,8 @@ -package variablesources_test +package controllers_test import ( "encoding/json" + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -10,20 +11,17 @@ import ( "github.com/stretchr/testify/require" featuregatetesting "k8s.io/component-base/featuregate/testing" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-registry/alpha/property" - rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" + catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" + catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" + "github.com/operator-framework/operator-controller/internal/controllers" "github.com/operator-framework/operator-controller/pkg/features" ) -func TestMakeInstalledPackageVariablesWithForceSemverUpgradeConstraintsEnabled(t *testing.T) { +func TestSuccessorsPredicateWithForceSemverUpgradeConstraintsEnabled(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, true)() const testPackageName = "test-package" @@ -173,67 +171,40 @@ func TestMakeInstalledPackageVariablesWithForceSemverUpgradeConstraintsEnabled(t } for _, tt := range []struct { - name string - upgradeConstraintPolicy ocv1alpha1.UpgradeConstraintPolicy - installedBundle *catalogmetadata.Bundle - expectedResult []*olmvariables.InstalledPackageVariable - expectedError string + name string + installedBundle *catalogmetadata.Bundle + expectedResult []*catalogmetadata.Bundle }{ { - name: "with non-zero major version", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - installedBundle: bundleSet["test-package.v2.0.0"], - expectedResult: []*olmvariables.InstalledPackageVariable{ - olmvariables.NewInstalledPackageVariable(testPackageName, []*catalogmetadata.Bundle{ - // Updates are allowed within the major version. - // Ensure bundles are in version order (high to low) - // with current version at the end - bundleSet["test-package.v2.2.0"], - bundleSet["test-package.v2.1.0"], - bundleSet["test-package.v2.0.0"], - }), + name: "with non-zero major version", + installedBundle: bundleSet["test-package.v2.0.0"], + expectedResult: []*catalogmetadata.Bundle{ + // Updates are allowed within the major version + bundleSet["test-package.v2.2.0"], + bundleSet["test-package.v2.1.0"], + bundleSet["test-package.v2.0.0"], }, }, { - name: "with zero major and zero minor version", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - installedBundle: bundleSet["test-package.v0.0.1"], - expectedResult: []*olmvariables.InstalledPackageVariable{ - olmvariables.NewInstalledPackageVariable(testPackageName, []*catalogmetadata.Bundle{ - // No updates are allowed in major version zero when minor version is also zero - bundleSet["test-package.v0.0.1"], - }), + name: "with zero major and zero minor version", + installedBundle: bundleSet["test-package.v0.0.1"], + expectedResult: []*catalogmetadata.Bundle{ + // No updates are allowed in major version zero when minor version is also zero + bundleSet["test-package.v0.0.1"], }, }, { - name: "with zero major and non-zero minor version", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - installedBundle: bundleSet["test-package.v0.1.0"], - expectedResult: []*olmvariables.InstalledPackageVariable{ - olmvariables.NewInstalledPackageVariable(testPackageName, []*catalogmetadata.Bundle{ - // Patch version updates are allowed within the minor version - // Ensure bundles are in version order (high to low) - // with current version at the end. - bundleSet["test-package.v0.1.2"], - bundleSet["test-package.v0.1.1"], - bundleSet["test-package.v0.1.0"], - }), + name: "with zero major and non-zero minor version", + installedBundle: bundleSet["test-package.v0.1.0"], + expectedResult: []*catalogmetadata.Bundle{ + // Patch version updates are allowed within the minor version + bundleSet["test-package.v0.1.2"], + bundleSet["test-package.v0.1.1"], + bundleSet["test-package.v0.1.0"], }, }, { - name: "UpgradeConstraintPolicy is set to Ignore", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, - installedBundle: bundleSet["test-package.v2.0.0"], - expectedResult: []*olmvariables.InstalledPackageVariable{}, - }, - { - name: "no BundleDeployment for an ClusterExtension", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - expectedResult: []*olmvariables.InstalledPackageVariable{}, - }, - { - name: "installed bundle not found", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, + name: "installed bundle not found", installedBundle: &catalogmetadata.Bundle{ Bundle: declcfg.Bundle{ Name: "test-package.v9.0.0", @@ -245,41 +216,28 @@ func TestMakeInstalledPackageVariablesWithForceSemverUpgradeConstraintsEnabled(t }, InChannels: []*catalogmetadata.Channel{&testPackageChannel}, }, - expectedError: `bundle with image "registry.io/repo/test-package@v9.0.0" for package "test-package" not found in available catalogs but is currently installed via BundleDeployment "test-package-bd"`, + expectedResult: []*catalogmetadata.Bundle{}, }, } { t.Run(tt.name, func(t *testing.T) { - fakeOwnerClusterExtension := fakeClusterExtension("test-extension-semver", testPackageName, tt.upgradeConstraintPolicy) - bundleDeployments := []rukpakv1alpha2.BundleDeployment{} - if tt.installedBundle != nil { - bundleDeployments = append(bundleDeployments, fakeBundleDeployment("test-package-bd", tt.installedBundle.Image, &fakeOwnerClusterExtension)) - } + successors, err := controllers.SuccessorsPredicate(tt.installedBundle) + assert.NoError(t, err) + result := catalogfilter.Filter(allBundles, successors) - installedPackages, err := variablesources.MakeInstalledPackageVariables( - allBundles, - []ocv1alpha1.ClusterExtension{fakeOwnerClusterExtension}, - bundleDeployments, - ) - if tt.expectedError == "" { - assert.NoError(t, err) - } else { - assert.ErrorContains(t, err, tt.expectedError) - } + // sort before comparison for stable order + sort.SliceStable(result, func(i, j int) bool { + return catalogsort.ByVersion(result[i], result[j]) + }) gocmpopts := []cmp.Option{ cmpopts.IgnoreUnexported(catalogmetadata.Bundle{}), - cmp.AllowUnexported( - olmvariables.InstalledPackageVariable{}, - input.SimpleVariable{}, - constraint.DependencyConstraint{}, - ), } - require.Empty(t, cmp.Diff(installedPackages, tt.expectedResult, gocmpopts...)) + require.Empty(t, cmp.Diff(result, tt.expectedResult, gocmpopts...)) }) } } -func TestMakeInstalledPackageVariablesWithForceSemverUpgradeConstraintsDisabled(t *testing.T) { +func TestSuccessorsPredicateWithForceSemverUpgradeConstraintsDisabled(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, false)() const testPackageName = "test-package" @@ -391,7 +349,7 @@ func TestMakeInstalledPackageVariablesWithForceSemverUpgradeConstraintsDisabled( InChannels: []*catalogmetadata.Channel{&testPackageChannel}, }, // We need a bundle from different package to ensure that - // we filter out bundles certain bundle image + // we filter out certain bundle image "some-other-package.v2.3.0": { Bundle: declcfg.Bundle{ Name: "some-other-package.v2.3.0", @@ -410,84 +368,45 @@ func TestMakeInstalledPackageVariablesWithForceSemverUpgradeConstraintsDisabled( } for _, tt := range []struct { - name string - upgradeConstraintPolicy ocv1alpha1.UpgradeConstraintPolicy - installedBundle *catalogmetadata.Bundle - expectedResult []*olmvariables.InstalledPackageVariable - expectedError string + name string + installedBundle *catalogmetadata.Bundle + expectedResult []*catalogmetadata.Bundle }{ { - name: "respect replaces directive from catalog", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - installedBundle: bundleSet["test-package.v2.0.0"], - expectedResult: []*olmvariables.InstalledPackageVariable{ - olmvariables.NewInstalledPackageVariable(testPackageName, []*catalogmetadata.Bundle{ - // Must only have two bundle: - // - the one which replaces the current version - // - the current version (to allow to stay on the current version) - bundleSet["test-package.v2.1.0"], - bundleSet["test-package.v2.0.0"], - }), + name: "respect replaces directive from catalog", + installedBundle: bundleSet["test-package.v2.0.0"], + expectedResult: []*catalogmetadata.Bundle{ + // Must only have two bundle: + // - the one which replaces the current version + // - the current version (to allow to stay on the current version) + bundleSet["test-package.v2.1.0"], + bundleSet["test-package.v2.0.0"], }, }, { - name: "respect skips directive from catalog", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - installedBundle: bundleSet["test-package.v2.2.1"], - expectedResult: []*olmvariables.InstalledPackageVariable{ - olmvariables.NewInstalledPackageVariable(testPackageName, []*catalogmetadata.Bundle{ - // Must only have two bundle: - // - the one which skips the current version - // - the current version (to allow to stay on the current version) - bundleSet["test-package.v2.3.0"], - bundleSet["test-package.v2.2.1"], - }), + name: "respect skips directive from catalog", + installedBundle: bundleSet["test-package.v2.2.1"], + expectedResult: []*catalogmetadata.Bundle{ + // Must only have two bundle: + // - the one which skips the current version + // - the current version (to allow to stay on the current version) + bundleSet["test-package.v2.3.0"], + bundleSet["test-package.v2.2.1"], }, }, { - name: "respect skipRange directive from catalog", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - installedBundle: bundleSet["test-package.v2.3.0"], - expectedResult: []*olmvariables.InstalledPackageVariable{ - olmvariables.NewInstalledPackageVariable(testPackageName, []*catalogmetadata.Bundle{ - // Must only have two bundle: - // - the one which is skipRanges the current version - // - the current version (to allow to stay on the current version) - bundleSet["test-package.v2.4.0"], - bundleSet["test-package.v2.3.0"], - }), - }, - }, - { - name: "UpgradeConstraintPolicy is set to Ignore", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, - installedBundle: bundleSet["test-package.v2.0.0"], - expectedResult: []*olmvariables.InstalledPackageVariable{}, - }, - { - name: "no BundleDeployment for an ClusterExtension", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - expectedResult: []*olmvariables.InstalledPackageVariable{}, - }, - { - name: "installed bundle not found", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyEnforce, - installedBundle: &catalogmetadata.Bundle{ - Bundle: declcfg.Bundle{ - Name: "test-package.v9.0.0", - Package: testPackageName, - Image: "registry.io/repo/test-package@v9.0.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "9.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&testPackageChannel}, + name: "respect skipRange directive from catalog", + installedBundle: bundleSet["test-package.v2.3.0"], + expectedResult: []*catalogmetadata.Bundle{ + // Must only have two bundle: + // - the one which is skipRanges the current version + // - the current version (to allow to stay on the current version) + bundleSet["test-package.v2.4.0"], + bundleSet["test-package.v2.3.0"], }, - expectedError: `bundle with image "registry.io/repo/test-package@v9.0.0" for package "test-package" not found in available catalogs but is currently installed via BundleDeployment "test-package-bd"`, }, { - name: "installed bundle not found, but UpgradeConstraintPolicy is set to Ignore", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, + name: "installed bundle not found", installedBundle: &catalogmetadata.Bundle{ Bundle: declcfg.Bundle{ Name: "test-package.v9.0.0", @@ -499,36 +418,23 @@ func TestMakeInstalledPackageVariablesWithForceSemverUpgradeConstraintsDisabled( }, InChannels: []*catalogmetadata.Channel{&testPackageChannel}, }, - expectedResult: []*olmvariables.InstalledPackageVariable{}, + expectedResult: []*catalogmetadata.Bundle{}, }, } { t.Run(tt.name, func(t *testing.T) { - fakeOwnerClusterExtension := fakeClusterExtension("test-extension-legacy", testPackageName, tt.upgradeConstraintPolicy) - bundleDeployments := []rukpakv1alpha2.BundleDeployment{} - if tt.installedBundle != nil { - bundleDeployments = append(bundleDeployments, fakeBundleDeployment("test-package-bd", tt.installedBundle.Image, &fakeOwnerClusterExtension)) - } + successors, err := controllers.SuccessorsPredicate(tt.installedBundle) + assert.NoError(t, err) + result := catalogfilter.Filter(allBundles, successors) - installedPackages, err := variablesources.MakeInstalledPackageVariables( - allBundles, - []ocv1alpha1.ClusterExtension{fakeOwnerClusterExtension}, - bundleDeployments, - ) - if tt.expectedError == "" { - assert.NoError(t, err) - } else { - assert.ErrorContains(t, err, tt.expectedError) - } + // sort before comparison for stable order + sort.SliceStable(result, func(i, j int) bool { + return catalogsort.ByVersion(result[i], result[j]) + }) gocmpopts := []cmp.Option{ cmpopts.IgnoreUnexported(catalogmetadata.Bundle{}), - cmp.AllowUnexported( - olmvariables.InstalledPackageVariable{}, - input.SimpleVariable{}, - constraint.DependencyConstraint{}, - ), } - require.Empty(t, cmp.Diff(installedPackages, tt.expectedResult, gocmpopts...)) + require.Empty(t, cmp.Diff(result, tt.expectedResult, gocmpopts...)) }) } } diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go index cceaeaa5d..b704d0ca2 100644 --- a/internal/controllers/suite_test.go +++ b/internal/controllers/suite_test.go @@ -28,8 +28,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "github.com/operator-framework/deppy/pkg/deppy/solver" - "github.com/operator-framework/operator-controller/internal/controllers" "github.com/operator-framework/operator-controller/pkg/scheme" testutil "github.com/operator-framework/operator-controller/test/util" @@ -43,16 +41,12 @@ func newClient(t *testing.T) client.Client { } func newClientAndReconciler(t *testing.T) (client.Client, *controllers.ClusterExtensionReconciler) { - resolver, err := solver.New() - require.NoError(t, err) - cl := newClient(t) fakeCatalogClient := testutil.NewFakeCatalogClient(testBundleList) reconciler := &controllers.ClusterExtensionReconciler{ Client: cl, BundleProvider: &fakeCatalogClient, Scheme: scheme.Scheme, - Resolver: resolver, } return cl, reconciler } diff --git a/internal/controllers/variables.go b/internal/controllers/variables.go deleted file mode 100644 index d03f8bcae..000000000 --- a/internal/controllers/variables.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2023. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "github.com/operator-framework/deppy/pkg/deppy" - rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -func GenerateVariables(allBundles []*catalogmetadata.Bundle, clusterExtensions []ocv1alpha1.ClusterExtension, bundleDeployments []rukpakv1alpha2.BundleDeployment) ([]deppy.Variable, error) { - requiredPackages, err := variablesources.MakeRequiredPackageVariables(allBundles, clusterExtensions) - if err != nil { - return nil, err - } - - installedPackages, err := variablesources.MakeInstalledPackageVariables(allBundles, clusterExtensions, bundleDeployments) - if err != nil { - return nil, err - } - - bundles, err := variablesources.MakeBundleVariables(allBundles, requiredPackages, installedPackages) - if err != nil { - return nil, err - } - - bundleUniqueness := variablesources.MakeBundleUniquenessVariables(bundles) - - result := []deppy.Variable{} - for _, v := range requiredPackages { - result = append(result, v) - } - for _, v := range installedPackages { - result = append(result, v) - } - for _, v := range bundles { - result = append(result, v) - } - for _, v := range bundleUniqueness { - result = append(result, v) - } - return result, nil -} diff --git a/internal/controllers/variables_test.go b/internal/controllers/variables_test.go deleted file mode 100644 index 3db23daa8..000000000 --- a/internal/controllers/variables_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package controllers_test - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/utils/ptr" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - "github.com/operator-framework/operator-controller/internal/controllers" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -func TestVariableSource(t *testing.T) { - stableChannel := catalogmetadata.Channel{Channel: declcfg.Channel{ - Name: "stable", - Entries: []declcfg.ChannelEntry{ - { - Name: "packageA.v2.0.0", - }, - }, - }} - bundleSet := map[string]*catalogmetadata.Bundle{ - "packageA.v2.0.0": { - Bundle: declcfg.Bundle{ - Name: "packageA.v2.0.0", - Package: "packageA", - Image: "foo.io/packageA/packageA:v2.0.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName":"packageA","version":"2.0.0"}`)}, - }, - }, - CatalogName: "fake-catalog", - InChannels: []*catalogmetadata.Channel{&stableChannel}, - }, - } - allBundles := make([]*catalogmetadata.Bundle, 0, len(bundleSet)) - for _, bundle := range bundleSet { - allBundles = append(allBundles, bundle) - } - - pkgName := "packageA" - clusterExtensionName := fmt.Sprintf("clusterextension-test-%s", rand.String(8)) - clusterExtension := ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: clusterExtensionName}, - Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Channel: "stable", - Version: "2.0.0", - }, - } - - bd := rukpakv1alpha2.BundleDeployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterExtensionName, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: ocv1alpha1.GroupVersion.String(), - Kind: "ClusterExtension", - Name: clusterExtension.Name, - UID: clusterExtension.UID, - Controller: ptr.To(true), - BlockOwnerDeletion: ptr.To(true), - }, - }, - }, - Spec: rukpakv1alpha2.BundleDeploymentSpec{ - ProvisionerClassName: "core-rukpak-io-registry", - Source: rukpakv1alpha2.BundleSource{ - Type: rukpakv1alpha2.SourceTypeImage, - Image: &rukpakv1alpha2.ImageSource{ - Ref: "foo.io/packageA/packageA:v2.0.0", - }, - }, - }, - } - - vars, err := controllers.GenerateVariables(allBundles, []ocv1alpha1.ClusterExtension{clusterExtension}, []rukpakv1alpha2.BundleDeployment{bd}) - require.NoError(t, err) - - expectedVars := []deppy.Variable{ - olmvariables.NewRequiredPackageVariable("packageA", []*catalogmetadata.Bundle{ - bundleSet["packageA.v2.0.0"], - }), - olmvariables.NewInstalledPackageVariable("packageA", []*catalogmetadata.Bundle{ - bundleSet["packageA.v2.0.0"], - }), - olmvariables.NewBundleVariable(bundleSet["packageA.v2.0.0"], nil), - olmvariables.NewBundleUniquenessVariable( - "packageA package uniqueness", - deppy.Identifier("fake-catalog-packageA-packageA.v2.0.0"), - ), - } - gocmpopts := []cmp.Option{ - cmpopts.IgnoreUnexported(catalogmetadata.Bundle{}), - cmp.AllowUnexported( - olmvariables.RequiredPackageVariable{}, - olmvariables.InstalledPackageVariable{}, - olmvariables.BundleVariable{}, - olmvariables.BundleUniquenessVariable{}, - input.SimpleVariable{}, - constraint.DependencyConstraint{}, - constraint.AtMostConstraint{}, - ), - } - require.Empty(t, cmp.Diff(vars, expectedVars, gocmpopts...)) -} diff --git a/internal/resolution/variables/bundle.go b/internal/resolution/variables/bundle.go deleted file mode 100644 index 51723fb12..000000000 --- a/internal/resolution/variables/bundle.go +++ /dev/null @@ -1,67 +0,0 @@ -package variables - -import ( - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" -) - -var _ deppy.Variable = &BundleVariable{} - -type BundleVariable struct { - *input.SimpleVariable - bundle *catalogmetadata.Bundle - dependencies []*catalogmetadata.Bundle -} - -func (b *BundleVariable) Bundle() *catalogmetadata.Bundle { - return b.bundle -} - -func (b *BundleVariable) Dependencies() []*catalogmetadata.Bundle { - return b.dependencies -} - -func NewBundleVariable(bundle *catalogmetadata.Bundle, dependencies []*catalogmetadata.Bundle) *BundleVariable { - dependencyIDs := make([]deppy.Identifier, 0, len(dependencies)) - for _, dependency := range dependencies { - dependencyIDs = append(dependencyIDs, BundleVariableID(dependency)) - } - var constraints []deppy.Constraint - if len(dependencyIDs) > 0 { - constraints = append(constraints, constraint.Dependency(dependencyIDs...)) - } - return &BundleVariable{ - SimpleVariable: input.NewSimpleVariable(BundleVariableID(bundle), constraints...), - bundle: bundle, - dependencies: dependencies, - } -} - -var _ deppy.Variable = &BundleUniquenessVariable{} - -type BundleUniquenessVariable struct { - *input.SimpleVariable -} - -// NewBundleUniquenessVariable creates a new variable that instructs the resolver to choose at most a single bundle -// from the input 'atMostID'. Examples: -// 1. restrict the solution to at most a single bundle per package -// 2. restrict the solution to at most a single bundler per provided gvk -// this guarantees that no two extensions provide the same gvk and no two version of the same extension are running at the same time -func NewBundleUniquenessVariable(id deppy.Identifier, atMostIDs ...deppy.Identifier) *BundleUniquenessVariable { - return &BundleUniquenessVariable{ - SimpleVariable: input.NewSimpleVariable(id, constraint.AtMost(1, atMostIDs...)), - } -} - -// BundleVariableID returns an ID for a given bundle. -func BundleVariableID(bundle *catalogmetadata.Bundle) deppy.Identifier { - return deppy.Identifier( - fmt.Sprintf("%s-%s-%s", bundle.CatalogName, bundle.Package, bundle.Name), - ) -} diff --git a/internal/resolution/variables/bundle_test.go b/internal/resolution/variables/bundle_test.go deleted file mode 100644 index 26d831f54..000000000 --- a/internal/resolution/variables/bundle_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package variables_test - -import ( - "encoding/json" - "testing" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -func TestBundleVariable(t *testing.T) { - bundle := &catalogmetadata.Bundle{ - InChannels: []*catalogmetadata.Channel{ - {Channel: declcfg.Channel{Name: "stable"}}, - }, - Bundle: declcfg.Bundle{Name: "bundle-1", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - }}, - } - dependencies := []*catalogmetadata.Bundle{ - { - InChannels: []*catalogmetadata.Channel{ - {Channel: declcfg.Channel{Name: "stable"}}, - }, - Bundle: declcfg.Bundle{Name: "bundle-2", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - }}, - }, - { - InChannels: []*catalogmetadata.Channel{ - {Channel: declcfg.Channel{Name: "stable"}}, - }, - Bundle: declcfg.Bundle{Name: "bundle-3", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "3.0.0"}`)}, - }}, - }, - } - bv := olmvariables.NewBundleVariable(bundle, dependencies) - - if bv.Bundle() != bundle { - t.Errorf("bundle '%v' does not match expected '%v'", bv.Bundle(), bundle) - } - - for i, d := range bv.Dependencies() { - if d != dependencies[i] { - t.Errorf("dependency[%v] '%v' does not match expected '%v'", i, d, dependencies[i]) - } - } -} - -func TestBundleUniquenessVariable(t *testing.T) { - id := deppy.IdentifierFromString("test-id") - atMostIDs := []deppy.Identifier{ - deppy.IdentifierFromString("test-at-most-id-1"), - deppy.IdentifierFromString("test-at-most-id-2"), - } - globalConstraintVariable := olmvariables.NewBundleUniquenessVariable(id, atMostIDs...) - - if globalConstraintVariable.Identifier() != id { - t.Errorf("identifier '%v' does not match expected '%v'", globalConstraintVariable.Identifier(), id) - } - - constraints := []deppy.Constraint{constraint.AtMost(1, atMostIDs...)} - for i, c := range globalConstraintVariable.Constraints() { - if c.String("test") != constraints[i].String("test") { - t.Errorf("constraint[%v] '%v' does not match expected '%v'", i, c, constraints[i]) - } - } -} diff --git a/internal/resolution/variables/installed_package.go b/internal/resolution/variables/installed_package.go deleted file mode 100644 index f5ccd12cd..000000000 --- a/internal/resolution/variables/installed_package.go +++ /dev/null @@ -1,34 +0,0 @@ -package variables - -import ( - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" -) - -var _ deppy.Variable = &InstalledPackageVariable{} - -type InstalledPackageVariable struct { - *input.SimpleVariable - bundles []*catalogmetadata.Bundle -} - -func (r *InstalledPackageVariable) Bundles() []*catalogmetadata.Bundle { - return r.bundles -} - -func NewInstalledPackageVariable(packageName string, bundles []*catalogmetadata.Bundle) *InstalledPackageVariable { - id := deppy.IdentifierFromString(fmt.Sprintf("installed package %s", packageName)) - variableIDs := make([]deppy.Identifier, 0, len(bundles)) - for _, bundle := range bundles { - variableIDs = append(variableIDs, BundleVariableID(bundle)) - } - return &InstalledPackageVariable{ - SimpleVariable: input.NewSimpleVariable(id, constraint.Mandatory(), constraint.Dependency(variableIDs...)), - bundles: bundles, - } -} diff --git a/internal/resolution/variables/installed_package_test.go b/internal/resolution/variables/installed_package_test.go deleted file mode 100644 index 850783a6c..000000000 --- a/internal/resolution/variables/installed_package_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package variables_test - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -func TestInstalledPackageVariable(t *testing.T) { - packageName := "test-package" - - bundles := []*catalogmetadata.Bundle{ - {Bundle: declcfg.Bundle{Name: "bundle-1", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - }}}, - {Bundle: declcfg.Bundle{Name: "bundle-2", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - }}}, - {Bundle: declcfg.Bundle{Name: "bundle-3", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "3.0.0"}`)}, - }}}, - } - ipv := olmvariables.NewInstalledPackageVariable(packageName, bundles) - - id := deppy.IdentifierFromString(fmt.Sprintf("installed package %s", packageName)) - if ipv.Identifier() != id { - t.Errorf("package name '%v' does not match expected '%v'", ipv.Identifier(), id) - } - - for i, e := range ipv.Bundles() { - if e != bundles[i] { - t.Errorf("bundle[%v] '%v' does not match expected '%v'", i, e, bundles[i]) - } - } -} diff --git a/internal/resolution/variables/required_package.go b/internal/resolution/variables/required_package.go deleted file mode 100644 index dd1add43b..000000000 --- a/internal/resolution/variables/required_package.go +++ /dev/null @@ -1,34 +0,0 @@ -package variables - -import ( - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" -) - -var _ deppy.Variable = &RequiredPackageVariable{} - -type RequiredPackageVariable struct { - *input.SimpleVariable - bundles []*catalogmetadata.Bundle -} - -func (r *RequiredPackageVariable) Bundles() []*catalogmetadata.Bundle { - return r.bundles -} - -func NewRequiredPackageVariable(packageName string, bundles []*catalogmetadata.Bundle) *RequiredPackageVariable { - id := deppy.IdentifierFromString(fmt.Sprintf("required package %s", packageName)) - variableIDs := make([]deppy.Identifier, 0, len(bundles)) - for _, bundle := range bundles { - variableIDs = append(variableIDs, BundleVariableID(bundle)) - } - return &RequiredPackageVariable{ - SimpleVariable: input.NewSimpleVariable(id, constraint.Mandatory(), constraint.Dependency(variableIDs...)), - bundles: bundles, - } -} diff --git a/internal/resolution/variables/required_package_test.go b/internal/resolution/variables/required_package_test.go deleted file mode 100644 index d8ece1473..000000000 --- a/internal/resolution/variables/required_package_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package variables_test - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -func TestRequiredPackageVariable(t *testing.T) { - packageName := "test-package" - bundles := []*catalogmetadata.Bundle{ - {Bundle: declcfg.Bundle{Name: "bundle-1", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - }}}, - {Bundle: declcfg.Bundle{Name: "bundle-2", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - }}}, - {Bundle: declcfg.Bundle{Name: "bundle-3", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "3.0.0"}`)}, - }}}, - } - rpv := olmvariables.NewRequiredPackageVariable(packageName, bundles) - - id := deppy.IdentifierFromString(fmt.Sprintf("required package %s", packageName)) - if rpv.Identifier() != id { - t.Errorf("package name '%v' does not match expected '%v'", rpv.Identifier(), id) - } - - for i, e := range rpv.Bundles() { - if e != bundles[i] { - t.Errorf("bundle entity[%v] '%v' does not match expected '%v'", i, e, bundles[i]) - } - } - - // TODO: add this test once https://github.com/operator-framework/deppy/pull/85 gets merged - // then we'll be able to inspect constraint types - // "should contain both mandatory and dependency constraints" -} diff --git a/internal/resolution/variablesources/bundle.go b/internal/resolution/variablesources/bundle.go deleted file mode 100644 index 6e7f83dc2..000000000 --- a/internal/resolution/variablesources/bundle.go +++ /dev/null @@ -1,92 +0,0 @@ -package variablesources - -import ( - "fmt" - "sort" - - "k8s.io/apimachinery/pkg/util/sets" - - "github.com/operator-framework/deppy/pkg/deppy" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" - catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -func MakeBundleVariables( - allBundles []*catalogmetadata.Bundle, - requiredPackages []*olmvariables.RequiredPackageVariable, - installedPackages []*olmvariables.InstalledPackageVariable, -) ([]*olmvariables.BundleVariable, error) { - var bundleQueue []*catalogmetadata.Bundle - for _, variable := range requiredPackages { - bundleQueue = append(bundleQueue, variable.Bundles()...) - } - for _, variable := range installedPackages { - bundleQueue = append(bundleQueue, variable.Bundles()...) - } - - // build bundle and dependency variables - var result []*olmvariables.BundleVariable - visited := sets.Set[deppy.Identifier]{} - for len(bundleQueue) > 0 { - // pop head of queue - var head *catalogmetadata.Bundle - head, bundleQueue = bundleQueue[0], bundleQueue[1:] - - id := olmvariables.BundleVariableID(head) - - // ignore bundles that have already been processed - if visited.Has(id) { - continue - } - visited.Insert(id) - - // get bundle dependencies - dependencies, err := filterBundleDependencies(allBundles, head) - if err != nil { - return nil, fmt.Errorf("could not determine dependencies for bundle %q from package %q in catalog %q: %s", head.Name, head.Package, head.CatalogName, err) - } - - // add bundle dependencies to queue for processing - bundleQueue = append(bundleQueue, dependencies...) - - // create variable - result = append(result, olmvariables.NewBundleVariable(head, dependencies)) - } - - return result, nil -} - -func filterBundleDependencies(allBundles []*catalogmetadata.Bundle, bundle *catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) { - var dependencies []*catalogmetadata.Bundle - added := sets.Set[deppy.Identifier]{} - - // gather required package dependencies - requiredPackages, _ := bundle.RequiredPackages() - for _, requiredPackage := range requiredPackages { - packageDependencyBundles := catalogfilter.Filter(allBundles, catalogfilter.And( - catalogfilter.WithPackageName(requiredPackage.PackageName), - catalogfilter.InBlangSemverRange(requiredPackage.SemverRange), - )) - if len(packageDependencyBundles) == 0 { - return nil, fmt.Errorf("no bundles found matching required package %q in range %q", requiredPackage.PackageName, requiredPackage.VersionRange) - } - for i := 0; i < len(packageDependencyBundles); i++ { - bundle := packageDependencyBundles[i] - id := olmvariables.BundleVariableID(bundle) - if !added.Has(id) { - dependencies = append(dependencies, bundle) - added.Insert(id) - } - } - } - - // sort bundles in version order - sort.SliceStable(dependencies, func(i, j int) bool { - return catalogsort.ByVersion(dependencies[i], dependencies[j]) - }) - - return dependencies, nil -} diff --git a/internal/resolution/variablesources/bundle_test.go b/internal/resolution/variablesources/bundle_test.go deleted file mode 100644 index 9cad4ae9f..000000000 --- a/internal/resolution/variablesources/bundle_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package variablesources_test - -import ( - "encoding/json" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -func TestMakeBundleVariables_ValidDepedencies(t *testing.T) { - const fakeCatalogName = "fake-catalog" - fakeChannel := catalogmetadata.Channel{Channel: declcfg.Channel{Name: "stable"}} - bundleSet := map[string]*catalogmetadata.Bundle{ - // Test package which we will be using as input into - // the testable function - "test-package.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v1.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "first-level-dependency", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&fakeChannel}, - }, - - // First level dependency of test-package. Will be explicitly - // provided into the testable function as part of variable. - // This package must have at least one dependency with a version - // range so we can test that result has correct ordering: - // the testable function must give priority to newer versions. - "first-level-dependency.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "first-level-dependency.v1.0.0", - Package: "first-level-dependency", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "first-level-dependency", "version": "1.0.0"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "second-level-dependency", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&fakeChannel}, - }, - - // Second level dependency that matches requirements of the first level dependency. - "second-level-dependency.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "second-level-dependency.v1.0.0", - Package: "second-level-dependency", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "second-level-dependency", "version": "1.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&fakeChannel}, - }, - - // Second level dependency that matches requirements of the first level dependency. - "second-level-dependency.v1.0.1": { - Bundle: declcfg.Bundle{ - Name: "second-level-dependency.v1.0.1", - Package: "second-level-dependency", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "second-level-dependency", "version": "1.0.1"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&fakeChannel}, - }, - - // Second level dependency that does not match requirements of the first level dependency. - "second-level-dependency.v2.0.0": { - Bundle: declcfg.Bundle{ - Name: "second-level-dependency.v2.0.0", - Package: "second-level-dependency", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "second-level-dependency", "version": "2.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&fakeChannel}, - }, - - // Package that is in a our fake catalog, but is not involved - // in this dependency chain. We need this to make sure that - // the testable function filters out unrelated bundles. - "uninvolved-package.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "uninvolved-package.v1.0.0", - Package: "uninvolved-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "uninvolved-package", "version": "1.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&fakeChannel}, - }, - } - - allBundles := make([]*catalogmetadata.Bundle, 0, len(bundleSet)) - for _, bundle := range bundleSet { - allBundles = append(allBundles, bundle) - } - requiredPackages := []*olmvariables.RequiredPackageVariable{ - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - bundleSet["first-level-dependency.v1.0.0"], - }), - } - installedPackages := []*olmvariables.InstalledPackageVariable{ - olmvariables.NewInstalledPackageVariable("test-package", []*catalogmetadata.Bundle{ - bundleSet["first-level-dependency.v1.0.0"], - }), - } - - bundles, err := variablesources.MakeBundleVariables(allBundles, requiredPackages, installedPackages) - require.NoError(t, err) - - // Each dependency must have a variable. - // Dependencies from the same package must be sorted by version - // with higher versions first. - expectedVariables := []*olmvariables.BundleVariable{ - olmvariables.NewBundleVariable( - bundleSet["first-level-dependency.v1.0.0"], - []*catalogmetadata.Bundle{ - bundleSet["second-level-dependency.v1.0.1"], - bundleSet["second-level-dependency.v1.0.0"], - }, - ), - olmvariables.NewBundleVariable( - bundleSet["second-level-dependency.v1.0.1"], - nil, - ), - olmvariables.NewBundleVariable( - bundleSet["second-level-dependency.v1.0.0"], - nil, - ), - } - gocmpopts := []cmp.Option{ - cmpopts.IgnoreUnexported(catalogmetadata.Bundle{}), - cmp.AllowUnexported( - olmvariables.BundleVariable{}, - input.SimpleVariable{}, - constraint.DependencyConstraint{}, - ), - } - require.Empty(t, cmp.Diff(bundles, expectedVariables, gocmpopts...)) -} - -func TestMakeBundleVariables_NonExistentDepedencies(t *testing.T) { - const fakeCatalogName = "fake-catalog" - fakeChannel := catalogmetadata.Channel{Channel: declcfg.Channel{Name: "stable"}} - bundleSet := map[string]*catalogmetadata.Bundle{ - "test-package.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v1.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "first-level-dependency", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&fakeChannel}, - }, - } - - allBundles := make([]*catalogmetadata.Bundle, 0, len(bundleSet)) - for _, bundle := range bundleSet { - allBundles = append(allBundles, bundle) - } - requiredPackages := []*olmvariables.RequiredPackageVariable{ - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - bundleSet["test-package.v1.0.0"], - }), - } - installedPackages := []*olmvariables.InstalledPackageVariable{} - - bundles, err := variablesources.MakeBundleVariables(allBundles, requiredPackages, installedPackages) - assert.ErrorContains(t, err, `could not determine dependencies for bundle "test-package.v1.0.0" from package "test-package" in catalog "fake-catalog"`) - assert.Nil(t, bundles) -} diff --git a/internal/resolution/variablesources/bundle_uniqueness.go b/internal/resolution/variablesources/bundle_uniqueness.go deleted file mode 100644 index cb4546e2c..000000000 --- a/internal/resolution/variablesources/bundle_uniqueness.go +++ /dev/null @@ -1,51 +0,0 @@ -package variablesources - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/util/sets" - - "github.com/operator-framework/deppy/pkg/deppy" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -// MakeBundleUniquenessVariables produces variables that constrain -// the solution to at most 1 bundle per package. -// These variables guarantee that no two versions of -// the same package are running at the same time. -func MakeBundleUniquenessVariables(bundleVariables []*olmvariables.BundleVariable) []*olmvariables.BundleUniquenessVariable { - result := []*olmvariables.BundleUniquenessVariable{} - - bundleIDs := sets.Set[deppy.Identifier]{} - packageOrder := []string{} - bundleOrder := map[string][]deppy.Identifier{} - for _, bundleVariable := range bundleVariables { - bundles := make([]*catalogmetadata.Bundle, 0, 1+len(bundleVariable.Dependencies())) - bundles = append(bundles, bundleVariable.Bundle()) - bundles = append(bundles, bundleVariable.Dependencies()...) - for _, bundle := range bundles { - id := olmvariables.BundleVariableID(bundle) - // get bundleID package and update map - packageName := bundle.Package - - if _, ok := bundleOrder[packageName]; !ok { - packageOrder = append(packageOrder, packageName) - } - - if !bundleIDs.Has(id) { - bundleIDs.Insert(id) - bundleOrder[packageName] = append(bundleOrder[packageName], id) - } - } - } - - // create global constraint variables - for _, packageName := range packageOrder { - varID := deppy.IdentifierFromString(fmt.Sprintf("%s package uniqueness", packageName)) - result = append(result, olmvariables.NewBundleUniquenessVariable(varID, bundleOrder[packageName]...)) - } - - return result -} diff --git a/internal/resolution/variablesources/bundle_uniqueness_test.go b/internal/resolution/variablesources/bundle_uniqueness_test.go deleted file mode 100644 index 6404bbcd3..000000000 --- a/internal/resolution/variablesources/bundle_uniqueness_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package variablesources_test - -import ( - "encoding/json" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/require" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -func TestMakeBundleUniquenessVariables(t *testing.T) { - const fakeCatalogName = "fake-catalog" - channel := catalogmetadata.Channel{Channel: declcfg.Channel{Name: "stable"}} - bundleSet := map[string]*catalogmetadata.Bundle{ - "test-package.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v1.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "test-package.v1.0.1": { - Bundle: declcfg.Bundle{ - Name: "test-package.v1.0.1", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.1"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - "some-package.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "some-package.v1.0.0", - Package: "some-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-package", "version": "1.0.0"}`)}, - }, - }, - CatalogName: fakeCatalogName, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - } - - // Input into the testable function must include more than one bundle - // from the same package to ensure that the function - // enforces uniqueness correctly. - // We also need at least one bundle variable to have a dependency - // on another package. This will help to make sure that the function - // also creates uniqueness variables for dependencies. - bundleVariables := []*olmvariables.BundleVariable{ - olmvariables.NewBundleVariable( - bundleSet["test-package.v1.0.0"], - []*catalogmetadata.Bundle{ - bundleSet["some-package.v1.0.0"], - }, - ), - olmvariables.NewBundleVariable( - bundleSet["test-package.v1.0.1"], - []*catalogmetadata.Bundle{ - bundleSet["some-package.v1.0.0"], - }, - ), - } - - variables := variablesources.MakeBundleUniquenessVariables(bundleVariables) - - // Each package in the input must have own uniqueness variable. - // Each variable expected to have one constraint enforcing at most one - // of the involved bundles to be allowed in the solution - expectedVariables := []*olmvariables.BundleUniquenessVariable{ - { - SimpleVariable: input.NewSimpleVariable( - "test-package package uniqueness", - constraint.AtMost( - 1, - deppy.Identifier("fake-catalog-test-package-test-package.v1.0.0"), - deppy.Identifier("fake-catalog-test-package-test-package.v1.0.1"), - ), - ), - }, - { - SimpleVariable: input.NewSimpleVariable( - "some-package package uniqueness", - constraint.AtMost( - 1, - deppy.Identifier("fake-catalog-some-package-some-package.v1.0.0"), - ), - ), - }, - } - require.Empty(t, cmp.Diff(variables, expectedVariables, cmp.AllowUnexported(input.SimpleVariable{}, constraint.AtMostConstraint{}))) -} diff --git a/internal/resolution/variablesources/fake_object_utils_test.go b/internal/resolution/variablesources/fake_object_utils_test.go deleted file mode 100644 index 765cdc97f..000000000 --- a/internal/resolution/variablesources/fake_object_utils_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package variablesources_test - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/uuid" - "k8s.io/utils/ptr" - - rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" -) - -func fakeClusterExtension(name, packageName string, upgradeConstraintPolicy ocv1alpha1.UpgradeConstraintPolicy) ocv1alpha1.ClusterExtension { - return ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - // We manually set a fake UID here because the code we test - // uses UID to determine ClusterExtension CR which - // owns `BundleDeployment` - UID: uuid.NewUUID(), - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: packageName, - UpgradeConstraintPolicy: upgradeConstraintPolicy, - }, - } -} - -func fakeBundleDeployment(name, bundleImage string, owner *ocv1alpha1.ClusterExtension) rukpakv1alpha2.BundleDeployment { - bd := rukpakv1alpha2.BundleDeployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: rukpakv1alpha2.BundleDeploymentSpec{ - ProvisionerClassName: "core-rukpak-io-plain", - Source: rukpakv1alpha2.BundleSource{ - Image: &rukpakv1alpha2.ImageSource{ - Ref: bundleImage, - }, - }, - }, - } - - if owner != nil { - bd.SetOwnerReferences([]metav1.OwnerReference{ - { - APIVersion: ocv1alpha1.GroupVersion.String(), - Kind: "ClusterExtension", - Name: owner.Name, - UID: owner.UID, - Controller: ptr.To(true), - BlockOwnerDeletion: ptr.To(true), - }, - }) - } - - return bd -} diff --git a/internal/resolution/variablesources/installed_package.go b/internal/resolution/variablesources/installed_package.go deleted file mode 100644 index 8ba3e6a61..000000000 --- a/internal/resolution/variablesources/installed_package.go +++ /dev/null @@ -1,148 +0,0 @@ -package variablesources - -import ( - "fmt" - "sort" - - mmsemver "github.com/Masterminds/semver/v3" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" - - rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" - catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/pkg/features" -) - -// MakeInstalledPackageVariables returns variables representing packages -// already installed in the system. -// Meaning that each BundleDeployment managed by operator-controller -// has own variable. -func MakeInstalledPackageVariables( - allBundles []*catalogmetadata.Bundle, - clusterExtensions []ocv1alpha1.ClusterExtension, - bundleDeployments []rukpakv1alpha2.BundleDeployment, -) ([]*olmvariables.InstalledPackageVariable, error) { - var successors successorsFunc = legacySemanticsSuccessors - if features.OperatorControllerFeatureGate.Enabled(features.ForceSemverUpgradeConstraints) { - successors = semverSuccessors - } - - ownerIDToBundleDeployment := mapOwnerIDToBundleDeployment(bundleDeployments) - - result := make([]*olmvariables.InstalledPackageVariable, 0, len(clusterExtensions)) - processed := sets.Set[string]{} - for _, clusterExtension := range clusterExtensions { - if clusterExtension.Spec.UpgradeConstraintPolicy == ocv1alpha1.UpgradeConstraintPolicyIgnore { - continue - } - - bundleDeployment, ok := ownerIDToBundleDeployment[clusterExtension.UID] - if !ok { - // This can happen when an ClusterExtension is requested, - // but not yet installed (e.g. no BundleDeployment created for it) - continue - } - - sourceImage := bundleDeployment.Spec.Source.Image - if sourceImage == nil || sourceImage.Ref == "" { - continue - } - - if processed.Has(sourceImage.Ref) { - continue - } - processed.Insert(sourceImage.Ref) - - bundleImage := sourceImage.Ref - - // find corresponding bundle for the installed content - resultSet := catalogfilter.Filter(allBundles, catalogfilter.And( - catalogfilter.WithPackageName(clusterExtension.Spec.PackageName), - catalogfilter.WithBundleImage(bundleImage), - )) - if len(resultSet) == 0 { - return nil, fmt.Errorf("bundle with image %q for package %q not found in available catalogs but is currently installed via BundleDeployment %q", bundleImage, clusterExtension.Spec.PackageName, bundleDeployment.Name) - } - - sort.SliceStable(resultSet, func(i, j int) bool { - return catalogsort.ByVersion(resultSet[i], resultSet[j]) - }) - installedBundle := resultSet[0] - - upgradeEdges, err := successors(allBundles, installedBundle) - if err != nil { - return nil, err - } - - // you can always upgrade to yourself, i.e. not upgrade - upgradeEdges = append(upgradeEdges, installedBundle) - result = append(result, olmvariables.NewInstalledPackageVariable(installedBundle.Package, upgradeEdges)) - } - - return result, nil -} - -// successorsFunc must return successors of a currently installed bundle -// from a list of all bundles provided to the function. -// Must not return installed bundle as a successor -type successorsFunc func(allBundles []*catalogmetadata.Bundle, installedBundle *catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) - -// legacySemanticsSuccessors returns successors based on legacy OLMv0 semantics -// which rely on Replaces, Skips and skipRange. -func legacySemanticsSuccessors(allBundles []*catalogmetadata.Bundle, installedBundle *catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) { - // find the bundles that replace, skip, or skipRange the bundle provided - upgradeEdges := catalogfilter.Filter(allBundles, catalogfilter.And( - catalogfilter.WithPackageName(installedBundle.Package), - catalogfilter.LegacySuccessor(installedBundle), - )) - sort.SliceStable(upgradeEdges, func(i, j int) bool { - return catalogsort.ByVersion(upgradeEdges[i], upgradeEdges[j]) - }) - - return upgradeEdges, nil -} - -// semverSuccessors returns successors based on Semver. -// Successors will not include versions outside the major version of the -// installed bundle as major version is intended to indicate breaking changes. -func semverSuccessors(allBundles []*catalogmetadata.Bundle, installedBundle *catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) { - currentVersion, err := installedBundle.Version() - if err != nil { - return nil, err - } - - // Based on current version create a caret range comparison constraint - // to allow only minor and patch version as successors and exclude current version. - constraintStr := fmt.Sprintf("^%s, != %s", currentVersion.String(), currentVersion.String()) - wantedVersionRangeConstraint, err := mmsemver.NewConstraint(constraintStr) - if err != nil { - return nil, err - } - - upgradeEdges := catalogfilter.Filter(allBundles, catalogfilter.And( - catalogfilter.WithPackageName(installedBundle.Package), - catalogfilter.InMastermindsSemverRange(wantedVersionRangeConstraint), - )) - sort.SliceStable(upgradeEdges, func(i, j int) bool { - return catalogsort.ByVersion(upgradeEdges[i], upgradeEdges[j]) - }) - - return upgradeEdges, nil -} - -func mapOwnerIDToBundleDeployment(bundleDeployments []rukpakv1alpha2.BundleDeployment) map[types.UID]*rukpakv1alpha2.BundleDeployment { - result := map[types.UID]*rukpakv1alpha2.BundleDeployment{} - - for idx := range bundleDeployments { - for _, ref := range bundleDeployments[idx].OwnerReferences { - result[ref.UID] = &bundleDeployments[idx] - } - } - - return result -} diff --git a/internal/resolution/variablesources/required_package.go b/internal/resolution/variablesources/required_package.go deleted file mode 100644 index 2f23a6062..000000000 --- a/internal/resolution/variablesources/required_package.go +++ /dev/null @@ -1,67 +0,0 @@ -package variablesources - -import ( - "fmt" - "sort" - - mmsemver "github.com/Masterminds/semver/v3" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" - catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -// MakeRequiredPackageVariables returns a variable which represent -// explicit requirement for a package from an user. -// This is when a user explicitly asks "install this" via ClusterExtension API. -func MakeRequiredPackageVariables(allBundles []*catalogmetadata.Bundle, clusterExtensions []ocv1alpha1.ClusterExtension) ([]*olmvariables.RequiredPackageVariable, error) { - result := make([]*olmvariables.RequiredPackageVariable, 0, len(clusterExtensions)) - - for _, clusterExtension := range clusterExtensions { - packageName := clusterExtension.Spec.PackageName - channelName := clusterExtension.Spec.Channel - versionRange := clusterExtension.Spec.Version - - predicates := []catalogfilter.Predicate[catalogmetadata.Bundle]{ - catalogfilter.WithPackageName(packageName), - } - - if channelName != "" { - predicates = append(predicates, catalogfilter.InChannel(channelName)) - } - - if versionRange != "" { - vr, err := mmsemver.NewConstraint(versionRange) - if err != nil { - return nil, fmt.Errorf("invalid version range %q: %w", versionRange, err) - } - predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) - } - - resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...)) - if len(resultSet) == 0 { - if versionRange != "" && channelName != "" { - return nil, fmt.Errorf("no package %q matching version %q found in channel %q", packageName, versionRange, channelName) - } - if versionRange != "" { - return nil, fmt.Errorf("no package %q matching version %q found", packageName, versionRange) - } - if channelName != "" { - return nil, fmt.Errorf("no package %q found in channel %q", packageName, channelName) - } - return nil, fmt.Errorf("no package %q found", packageName) - } - sort.SliceStable(resultSet, func(i, j int) bool { - return catalogsort.ByVersion(resultSet[i], resultSet[j]) - }) - sort.SliceStable(resultSet, func(i, j int) bool { - return catalogsort.ByDeprecated(resultSet[i], resultSet[j]) - }) - - result = append(result, olmvariables.NewRequiredPackageVariable(packageName, resultSet)) - } - - return result, nil -} diff --git a/internal/resolution/variablesources/required_package_test.go b/internal/resolution/variablesources/required_package_test.go deleted file mode 100644 index 164f9a411..000000000 --- a/internal/resolution/variablesources/required_package_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package variablesources_test - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -func TestMakeRequiredPackageVariables(t *testing.T) { - stableChannel := catalogmetadata.Channel{Channel: declcfg.Channel{ - Name: "stable", - }} - betaChannel := catalogmetadata.Channel{Channel: declcfg.Channel{ - Name: "beta", - }} - bundleSet := map[string]*catalogmetadata.Bundle{ - // Bundles which belong to test-package we will be using - // to assert wether the testable function filters out the data - // correctly. - "test-package.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v1.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&stableChannel}, - }, - "test-package.v3.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v3.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "3.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&stableChannel, &betaChannel}, - }, - "test-package.v2.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v2.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&stableChannel}, - }, - "test-package.v4.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v4.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "4.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&stableChannel}, - Deprecations: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaBundle, - Name: "test-package.v4.0.0", - }, - Message: "test-package.v4.0.0 has been deprecated", - }, - }, - }, - "test-package.v4.1.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v4.1.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "4.1.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&stableChannel}, - Deprecations: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaBundle, - Name: "test-package.v4.1.0", - }, - Message: "test-package.v4.1.0 has been deprecated", - }, - }, - }, - "test-package.v5.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package.v5.0.0", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "5.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&stableChannel}, - Deprecations: []declcfg.DeprecationEntry{ - { - Reference: declcfg.PackageScopedReference{ - Schema: declcfg.SchemaBundle, - Name: "test-package.v5.0.0", - }, - Message: "test-package.v5.0.0 has been deprecated", - }, - }, - }, - - // We need at least one bundle from different package - // to make sure that we are filtering it out. - "test-package-2.v1.0.0": { - Bundle: declcfg.Bundle{ - Name: "test-package-2.v1.0.0", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "1.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&stableChannel}, - }, - } - allBundles := make([]*catalogmetadata.Bundle, 0, len(bundleSet)) - for _, bundle := range bundleSet { - allBundles = append(allBundles, bundle) - } - - fakeClusterExtension := func(packageName, channelName, versionRange string) ocv1alpha1.ClusterExtension { - return ocv1alpha1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("op-%s-%s-%s", packageName, channelName, versionRange), - }, - Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: packageName, - Version: versionRange, - Channel: channelName, - }, - } - } - - for _, tt := range []struct { - name string - clusterExtensions []ocv1alpha1.ClusterExtension - expectedResult []*olmvariables.RequiredPackageVariable - expectedError string - }{ - { - name: "package name only", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("test-package", "", ""), - }, - expectedResult: []*olmvariables.RequiredPackageVariable{ - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - bundleSet["test-package.v3.0.0"], - bundleSet["test-package.v2.0.0"], - bundleSet["test-package.v1.0.0"], - bundleSet["test-package.v5.0.0"], - bundleSet["test-package.v4.1.0"], - bundleSet["test-package.v4.0.0"], - }), - }, - }, - { - name: "package name and channel", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("test-package", "beta", ""), - }, - expectedResult: []*olmvariables.RequiredPackageVariable{ - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - bundleSet["test-package.v3.0.0"], - }), - }, - }, - { - name: "package name and version range", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("test-package", "", ">=1.0.0 !=2.0.0 <3.0.0"), - }, - expectedResult: []*olmvariables.RequiredPackageVariable{ - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - bundleSet["test-package.v1.0.0"], - }), - }, - }, - { - name: "package name and invalid version range", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("test-package", "", "not a valid semver"), - }, - expectedError: `invalid version range "not a valid semver"`, - }, - { - name: "not found: package name only", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("non-existent-test-package", "", ""), - }, - expectedError: `no package "non-existent-test-package" found`, - }, - { - name: "not found: package name and channel", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("non-existent-test-package", "stable", ""), - }, - expectedError: `no package "non-existent-test-package" found in channel "stable"`, - }, - { - name: "not found: package name and version range", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("non-existent-test-package", "", "1.0.0"), - }, - expectedError: `no package "non-existent-test-package" matching version "1.0.0" found`, - }, - { - name: "not found: package name with channel and version range", - clusterExtensions: []ocv1alpha1.ClusterExtension{ - fakeClusterExtension("non-existent-test-package", "stable", "1.0.0"), - }, - expectedError: `no package "non-existent-test-package" matching version "1.0.0" found in channel "stable"`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - vars, err := variablesources.MakeRequiredPackageVariables(allBundles, tt.clusterExtensions) - if tt.expectedError == "" { - assert.NoError(t, err) - } else { - assert.ErrorContains(t, err, tt.expectedError) - } - - gocmpopts := []cmp.Option{ - cmpopts.IgnoreUnexported(catalogmetadata.Bundle{}), - cmp.AllowUnexported( - olmvariables.RequiredPackageVariable{}, - input.SimpleVariable{}, - constraint.DependencyConstraint{}, - ), - } - require.Empty(t, cmp.Diff(vars, tt.expectedResult, gocmpopts...)) - }) - } -} diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index 1ac60b869..cdc95ce59 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -276,8 +276,7 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { return } assert.Equal(ct, ocv1alpha1.ReasonResolutionFailed, cond.Reason) - assert.Contains(ct, cond.Message, "constraints not satisfiable") - assert.Contains(ct, cond.Message, "installed package prometheus requires at least one of test-catalog-prometheus-prometheus-operator.1.0.1, test-catalog-prometheus-prometheus-operator.1.0.0") + assert.Equal(ct, "error upgrading from currently installed version \"1.0.0\": no package \"prometheus\" matching version \"1.2.0\" found", cond.Message) assert.Empty(ct, clusterExtension.Status.ResolvedBundle) }, pollDuration, pollInterval) }