From ca08dbadaa4e3c9a632b5a8e44102857f1f91f3e Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Thu, 12 Oct 2023 16:04:35 +0100 Subject: [PATCH] PoC: refactor variable sources Signed-off-by: Mikalai Radchuk --- cmd/manager/main.go | 3 +- cmd/resolutioncli/main.go | 15 +- cmd/resolutioncli/variable_source.go | 98 +++- .../controllers/operator_controller_test.go | 3 +- internal/controllers/variable_source.go | 42 -- .../variablesources/bundle_deployment.go | 56 --- .../variablesources/bundle_deployment_test.go | 146 ------ .../bundles_and_dependencies.go | 118 ----- .../bundles_and_dependencies_test.go | 417 ------------------ .../resolution/variablesources/composite.go | 62 --- .../variablesources/composite_test.go | 172 -------- .../variablesources/crd_constraints.go | 76 ---- .../variablesources/crd_constraints_test.go | 324 -------------- .../variablesources/installed_package.go | 85 ---- .../variablesources/installed_package_test.go | 143 ------ internal/resolution/variablesources/olm.go | 317 +++++++++++++ .../resolution/variablesources/operator.go | 55 --- .../variablesources/operator_test.go | 156 ------- .../variablesources/required_package.go | 108 ----- .../variablesources/required_package_test.go | 137 ------ .../variablesources/variablesources_test.go | 13 - 21 files changed, 406 insertions(+), 2140 deletions(-) delete mode 100644 internal/controllers/variable_source.go delete mode 100644 internal/resolution/variablesources/bundle_deployment.go delete mode 100644 internal/resolution/variablesources/bundle_deployment_test.go delete mode 100644 internal/resolution/variablesources/bundles_and_dependencies.go delete mode 100644 internal/resolution/variablesources/bundles_and_dependencies_test.go delete mode 100644 internal/resolution/variablesources/composite.go delete mode 100644 internal/resolution/variablesources/composite_test.go delete mode 100644 internal/resolution/variablesources/crd_constraints.go delete mode 100644 internal/resolution/variablesources/crd_constraints_test.go delete mode 100644 internal/resolution/variablesources/installed_package.go delete mode 100644 internal/resolution/variablesources/installed_package_test.go create mode 100644 internal/resolution/variablesources/olm.go delete mode 100644 internal/resolution/variablesources/operator.go delete mode 100644 internal/resolution/variablesources/operator_test.go delete mode 100644 internal/resolution/variablesources/required_package.go delete mode 100644 internal/resolution/variablesources/required_package_test.go delete mode 100644 internal/resolution/variablesources/variablesources_test.go diff --git a/cmd/manager/main.go b/cmd/manager/main.go index bf646b316..8c4a916aa 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -40,6 +40,7 @@ import ( "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" + "github.com/operator-framework/operator-controller/internal/resolution/variablesources" "github.com/operator-framework/operator-controller/pkg/features" ) @@ -113,7 +114,7 @@ func main() { Client: cl, Scheme: mgr.GetScheme(), Resolver: solver.NewDeppySolver( - controllers.NewVariableSource(cl, catalogClient), + variablesources.NewOLMVariableSource(cl, catalogClient), ), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Operator") diff --git a/cmd/resolutioncli/main.go b/cmd/resolutioncli/main.go index 9350fbb9b..c3f7a9a86 100644 --- a/cmd/resolutioncli/main.go +++ b/cmd/resolutioncli/main.go @@ -35,9 +35,7 @@ import ( operatorsv1alpha1 "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" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" ) const pocMessage = `This command is a proof of concept for off-cluster resolution and is not intended for production use! @@ -71,12 +69,12 @@ func main() { ctx := context.Background() var packageName string - var packageVersion string + var packageVersionRange string var packageChannel string var indexRef string var inputDir string flag.StringVar(&packageName, flagNamePackageName, "", "Name of the package to resolve") - flag.StringVar(&packageVersion, flagNamePackageVersion, "", "Version of the package") + flag.StringVar(&packageVersionRange, flagNamePackageVersion, "", "Version or version range of the package") flag.StringVar(&packageChannel, flagNamePackageChannel, "", "Channel of the package") // TODO: Consider adding support of multiple refs flag.StringVar(&indexRef, flagNameIndexRef, "", "Index reference (FBC image or dir)") @@ -89,7 +87,7 @@ func main() { os.Exit(1) } - err := run(ctx, packageName, packageVersion, packageChannel, indexRef, inputDir) + err := run(ctx, packageName, packageChannel, packageVersionRange, indexRef, inputDir) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -108,7 +106,7 @@ func validateFlags(packageName, indexRef string) error { return nil } -func run(ctx context.Context, packageName, packageVersion, packageChannel, indexRef, inputDir string) error { +func run(ctx context.Context, packageName, packageChannel, packageVersionRange, indexRef, inputDir string) error { clientBuilder := fake.NewClientBuilder().WithScheme(scheme) if inputDir != "" { @@ -124,10 +122,7 @@ func run(ctx context.Context, packageName, packageVersion, packageChannel, index catalogClient := newIndexRefClient(indexRef) resolver := solver.NewDeppySolver( - append( - variablesources.NestedVariableSource{newPackageVariableSource(catalogClient, packageName, packageVersion, packageChannel)}, - controllers.NewVariableSource(cl, catalogClient)..., - ), + NewOfflineOLMVariableSource(cl, catalogClient, packageName, packageChannel, packageVersionRange), ) bundleImage, err := resolve(ctx, resolver, packageName) diff --git a/cmd/resolutioncli/variable_source.go b/cmd/resolutioncli/variable_source.go index cad8db182..0f934c13f 100644 --- a/cmd/resolutioncli/variable_source.go +++ b/cmd/resolutioncli/variable_source.go @@ -17,28 +17,90 @@ limitations under the License. package main import ( + "context" + + "github.com/operator-framework/deppy/pkg/deppy" "github.com/operator-framework/deppy/pkg/deppy/input" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" + operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" "github.com/operator-framework/operator-controller/internal/resolution/variablesources" ) -func newPackageVariableSource(catalogClient *indexRefClient, packageName, packageVersion, packageChannel string) func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - pkgSource, err := variablesources.NewRequiredPackageVariableSource( - catalogClient, - packageName, - variablesources.InVersionRange(packageVersion), - variablesources.InChannel(packageChannel), - ) - if err != nil { - return nil, err - } - - sliceSource := variablesources.SliceVariableSource{pkgSource} - if inputVariableSource != nil { - sliceSource = append(sliceSource, inputVariableSource) - } - - return sliceSource, nil +var _ input.VariableSource = &OfflineOLMVariableSource{} + +type OfflineOLMVariableSource struct { + client client.Client + catalogClient *indexRefClient + + packageName string + packageChannel string + packageVersionRange string +} + +func NewOfflineOLMVariableSource(cl client.Client, catalogClient *indexRefClient, packageName, packageChannel, packageVersionRange string) *OfflineOLMVariableSource { + return &OfflineOLMVariableSource{ + client: cl, + catalogClient: catalogClient, + + packageName: packageName, + packageChannel: packageChannel, + packageVersionRange: packageVersionRange, + } +} + +func (o *OfflineOLMVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { + operatorList := operatorsv1alpha1.OperatorList{} + if err := o.client.List(ctx, &operatorList); err != nil { + return nil, err + } + + bundleDeployments := rukpakv1alpha1.BundleDeploymentList{} + if err := o.client.List(ctx, &bundleDeployments); err != nil { + return nil, err + } + + allBundles, err := o.catalogClient.Bundles(ctx) + if err != nil { + return nil, err + } + + requiredPackages := []*olmvariables.RequiredPackageVariable{} + requiredPackage, err := variablesources.RequiredPackageVariable(allBundles, o.packageName, o.packageChannel, o.packageVersionRange) + if err != nil { + return nil, err + } + requiredPackages = append(requiredPackages, requiredPackage) + + installedPackages, err := variablesources.InstalledPackageVariables(allBundles, bundleDeployments.Items) + if err != nil { + return nil, err + } + + bundles, err := variablesources.BundleVariables(allBundles, requiredPackages, installedPackages) + if err != nil { + return nil, err + } + + bundleUniqueness, err := variablesources.BundleUniquenessVariables(bundles) + if err != nil { + return nil, err + } + + 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/operator_controller_test.go b/internal/controllers/operator_controller_test.go index 1f75c0518..407bf44af 100644 --- a/internal/controllers/operator_controller_test.go +++ b/internal/controllers/operator_controller_test.go @@ -24,6 +24,7 @@ import ( "github.com/operator-framework/operator-controller/internal/catalogmetadata" "github.com/operator-framework/operator-controller/internal/conditionsets" "github.com/operator-framework/operator-controller/internal/controllers" + "github.com/operator-framework/operator-controller/internal/resolution/variablesources" testutil "github.com/operator-framework/operator-controller/test/util" ) @@ -39,7 +40,7 @@ var _ = Describe("Operator Controller Test", func() { reconciler = &controllers.OperatorReconciler{ Client: cl, Scheme: sch, - Resolver: solver.NewDeppySolver(controllers.NewVariableSource(cl, &fakeCatalogClient)), + Resolver: solver.NewDeppySolver(variablesources.NewOLMVariableSource(cl, &fakeCatalogClient)), } }) When("the operator does not exist", func() { diff --git a/internal/controllers/variable_source.go b/internal/controllers/variable_source.go deleted file mode 100644 index 88608cdfb..000000000 --- a/internal/controllers/variable_source.go +++ /dev/null @@ -1,42 +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 ( - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -func NewVariableSource(cl client.Client, catalogClient variablesources.BundleProvider) variablesources.NestedVariableSource { - return variablesources.NestedVariableSource{ - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewOperatorVariableSource(cl, catalogClient, inputVariableSource), nil - }, - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewBundleDeploymentVariableSource(cl, catalogClient, inputVariableSource), nil - }, - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewBundlesAndDepsVariableSource(catalogClient, inputVariableSource), nil - }, - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewCRDUniquenessConstraintsVariableSource(inputVariableSource), nil - }, - } -} diff --git a/internal/resolution/variablesources/bundle_deployment.go b/internal/resolution/variablesources/bundle_deployment.go deleted file mode 100644 index 25c287087..000000000 --- a/internal/resolution/variablesources/bundle_deployment.go +++ /dev/null @@ -1,56 +0,0 @@ -package variablesources - -import ( - "context" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ input.VariableSource = &BundleDeploymentVariableSource{} - -type BundleDeploymentVariableSource struct { - client client.Client - catalogClient BundleProvider - inputVariableSource input.VariableSource -} - -func NewBundleDeploymentVariableSource(cl client.Client, catalogClient BundleProvider, inputVariableSource input.VariableSource) *BundleDeploymentVariableSource { - return &BundleDeploymentVariableSource{ - client: cl, - catalogClient: catalogClient, - inputVariableSource: inputVariableSource, - } -} - -func (o *BundleDeploymentVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - variableSources := SliceVariableSource{} - if o.inputVariableSource != nil { - variableSources = append(variableSources, o.inputVariableSource) - } - - bundleDeployments := rukpakv1alpha1.BundleDeploymentList{} - if err := o.client.List(ctx, &bundleDeployments); err != nil { - return nil, err - } - - processed := map[string]struct{}{} - for _, bundleDeployment := range bundleDeployments.Items { - sourceImage := bundleDeployment.Spec.Template.Spec.Source.Image - if sourceImage != nil && sourceImage.Ref != "" { - if _, ok := processed[sourceImage.Ref]; ok { - continue - } - processed[sourceImage.Ref] = struct{}{} - ips, err := NewInstalledPackageVariableSource(o.catalogClient, bundleDeployment.Spec.Template.Spec.Source.Image.Ref) - if err != nil { - return nil, err - } - variableSources = append(variableSources, ips) - } - } - - return variableSources.GetVariables(ctx) -} diff --git a/internal/resolution/variablesources/bundle_deployment_test.go b/internal/resolution/variablesources/bundle_deployment_test.go deleted file mode 100644 index 0e640210e..000000000 --- a/internal/resolution/variablesources/bundle_deployment_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package variablesources_test - -import ( - "context" - "encoding/json" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "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" - testutil "github.com/operator-framework/operator-controller/test/util" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - "github.com/operator-framework/deppy/pkg/deppy" - rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" -) - -func BundleDeploymentFakeClient(objects ...client.Object) client.Client { - scheme := runtime.NewScheme() - utilruntime.Must(rukpakv1alpha1.AddToScheme(scheme)) - return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() -} - -func bundleDeployment(name, image string) *rukpakv1alpha1.BundleDeployment { - return &rukpakv1alpha1.BundleDeployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: rukpakv1alpha1.BundleDeploymentSpec{ - ProvisionerClassName: "core-rukpak-io-plain", - Template: &rukpakv1alpha1.BundleTemplate{ - Spec: rukpakv1alpha1.BundleSpec{ - ProvisionerClassName: "core-rukpak-io-plain", - Source: rukpakv1alpha1.BundleSource{ - Image: &rukpakv1alpha1.ImageSource{ - Ref: image, - }, - }, - }, - }, - }, - } -} - -var _ = Describe("BundleDeploymentVariableSource", func() { - var fakeCatalogClient testutil.FakeCatalogClient - var betaChannel catalogmetadata.Channel - var stableChannel catalogmetadata.Channel - var testBundleList []*catalogmetadata.Bundle - - BeforeEach(func() { - betaChannel = catalogmetadata.Channel{Channel: declcfg.Channel{ - Name: "beta", - Entries: []declcfg.ChannelEntry{ - { - Name: "operatorhub/prometheus/0.37.0", - Replaces: "operatorhub/prometheus/0.32.0", - }, - { - Name: "operatorhub/prometheus/0.47.0", - Replaces: "operatorhub/prometheus/0.37.0", - }, - }, - }} - - stableChannel = catalogmetadata.Channel{Channel: declcfg.Channel{ - Name: "beta", - Entries: []declcfg.ChannelEntry{ - { - Name: "operatorhub/packageA/2.0.0", - }, - }, - }} - - testBundleList = []*catalogmetadata.Bundle{ - {Bundle: declcfg.Bundle{ - Name: "operatorhub/prometheus/0.37.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName":"prometheus","version":"0.37.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"monitoring.coreos.com","kind":"Alertmanager","version":"v1"}, {"group":"monitoring.coreos.com","kind":"Prometheus","version":"v1"}]`)}, - }, - }, InChannels: []*catalogmetadata.Channel{&betaChannel}}, - {Bundle: declcfg.Bundle{ - Name: "operatorhub/prometheus/0.47.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName":"prometheus","version":"0.47.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"monitoring.coreos.com","kind":"Alertmanager","version":"v1"}, {"group":"monitoring.coreos.com","kind":"Prometheus","version":"v1alpha1"}]`)}, - }, - }, InChannels: []*catalogmetadata.Channel{&betaChannel}}, - {Bundle: declcfg.Bundle{ - Name: "operatorhub/packageA/2.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"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }, - }, InChannels: []*catalogmetadata.Channel{&stableChannel}}, - } - - fakeCatalogClient = testutil.NewFakeCatalogClient(testBundleList) - }) - - It("should produce RequiredPackage variables", func() { - cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35")) - - bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &fakeCatalogClient, &MockRequiredPackageSource{}) - variables, err := bdVariableSource.GetVariables(context.Background()) - Expect(err).ToNot(HaveOccurred()) - - installedPackageVariable := filterVariables[*olmvariables.InstalledPackageVariable](variables) - Expect(installedPackageVariable).To(HaveLen(1)) - Expect(installedPackageVariable).To(WithTransform(func(bvars []*olmvariables.InstalledPackageVariable) map[deppy.Identifier]int { - out := map[deppy.Identifier]int{} - for _, variable := range bvars { - out[variable.Identifier()] = len(variable.Bundles()) - } - return out - }, Equal(map[deppy.Identifier]int{ - // Underlying `InstalledPackageVariableSource` returns current installed package - // as a possible upgrade edge - deppy.IdentifierFromString("installed package prometheus"): 2, - }))) - }) - It("should return an error if the bundleDeployment image doesn't match any operator resource", func() { - cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "quay.io/operatorhubio/prometheus@sha256:nonexistent")) - - bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &fakeCatalogClient, &MockRequiredPackageSource{}) - _, err := bdVariableSource.GetVariables(context.Background()) - Expect(err.Error()).To(Equal("bundleImage \"quay.io/operatorhubio/prometheus@sha256:nonexistent\" not found")) - }) -}) diff --git a/internal/resolution/variablesources/bundles_and_dependencies.go b/internal/resolution/variablesources/bundles_and_dependencies.go deleted file mode 100644 index 88d2a91c2..000000000 --- a/internal/resolution/variablesources/bundles_and_dependencies.go +++ /dev/null @@ -1,118 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - "sort" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "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" -) - -var _ input.VariableSource = &BundlesAndDepsVariableSource{} - -type BundlesAndDepsVariableSource struct { - catalogClient BundleProvider - variableSources []input.VariableSource -} - -func NewBundlesAndDepsVariableSource(catalogClient BundleProvider, inputVariableSources ...input.VariableSource) *BundlesAndDepsVariableSource { - return &BundlesAndDepsVariableSource{ - catalogClient: catalogClient, - variableSources: inputVariableSources, - } -} - -func (b *BundlesAndDepsVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - var variables []deppy.Variable - - // extract required package variables - for _, variableSource := range b.variableSources { - inputVariables, err := variableSource.GetVariables(ctx) - if err != nil { - return nil, err - } - variables = append(variables, inputVariables...) - } - - // create bundle queue for dependency resolution - var bundleQueue []*catalogmetadata.Bundle - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.RequiredPackageVariable: - bundleQueue = append(bundleQueue, v.Bundles()...) - case *olmvariables.InstalledPackageVariable: - bundleQueue = append(bundleQueue, v.Bundles()...) - } - } - - allBundles, err := b.catalogClient.Bundles(ctx) - if err != nil { - return nil, err - } - - // build bundle and dependency variables - visited := map[deppy.Identifier]struct{}{} - 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 _, ok := visited[id]; ok { - continue - } - visited[id] = struct{}{} - - // get bundle dependencies - dependencies, err := b.filterBundleDependencies(allBundles, head) - if err != nil { - return nil, fmt.Errorf("could not determine dependencies for bundle with id '%s': %w", id, err) - } - - // add bundle dependencies to queue for processing - bundleQueue = append(bundleQueue, dependencies...) - - // create variable - variables = append(variables, olmvariables.NewBundleVariable(head, dependencies)) - } - - return variables, nil -} - -func (b *BundlesAndDepsVariableSource) filterBundleDependencies(allBundles []*catalogmetadata.Bundle, bundle *catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) { - var dependencies []*catalogmetadata.Bundle - added := map[deppy.Identifier]struct{}{} - - // gather required package dependencies - // todo(perdasilva): disambiguate between not found and actual errors - 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("could not find package dependencies for bundle '%s'", bundle.Name) - } - for i := 0; i < len(packageDependencyBundles); i++ { - bundle := packageDependencyBundles[i] - id := olmvariables.BundleVariableID(bundle) - if _, ok := added[id]; !ok { - dependencies = append(dependencies, bundle) - added[id] = struct{}{} - } - } - } - - // 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/bundles_and_dependencies_test.go b/internal/resolution/variablesources/bundles_and_dependencies_test.go deleted file mode 100644 index 1c71a8ada..000000000 --- a/internal/resolution/variablesources/bundles_and_dependencies_test.go +++ /dev/null @@ -1,417 +0,0 @@ -package variablesources_test - -import ( - "context" - "encoding/json" - "errors" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "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" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" - testutil "github.com/operator-framework/operator-controller/test/util" -) - -var _ = Describe("BundlesAndDepsVariableSource", func() { - var ( - bdvs *variablesources.BundlesAndDepsVariableSource - testBundleList []*catalogmetadata.Bundle - fakeCatalogClient testutil.FakeCatalogClient - ) - - BeforeEach(func() { - channel := catalogmetadata.Channel{Channel: declcfg.Channel{Name: "stable"}} - testBundleList = []*catalogmetadata.Bundle{ - // required package bundles - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-1", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-2", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`{"group":"foo.io","kind":"Foo","version":"v1"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // dependencies - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-4", - Package: "some-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-package", "version": "1.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-5", - Package: "some-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-package", "version": "1.5.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-6", - Package: "some-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-package", "version": "2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-7", - Package: "some-other-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-other-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-8", - Package: "some-other-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-other-package", "version": "1.5.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`{"group":"foo.io","kind":"Foo","version":"v1"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`{"group":"bar.io","kind":"Bar","version":"v1"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "another-package", "versionRange": "< 2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // dependencies of dependencies - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-9", Package: "another-package", Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "another-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-10", - Package: "bar-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "bar-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"bar.io","kind":"Bar","version":"v1"}]`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-11", - Package: "bar-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "bar-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"bar.io","kind":"Bar","version":"v1"}]`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // test-package-2 required package - no dependencies - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-15", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "1.5.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-16", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "2.0.1"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-17", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "3.16.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // completely unrelated - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-12", - Package: "unrelated-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "unrelated-package", "version": "2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-13", - Package: "unrelated-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "unrelated-package-2", "version": "2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-14", - Package: "unrelated-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "unrelated-package-2", "version": "3.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - } - fakeCatalogClient = testutil.NewFakeCatalogClient(testBundleList) - bdvs = variablesources.NewBundlesAndDepsVariableSource( - &fakeCatalogClient, - &MockRequiredPackageSource{ - ResultSet: []deppy.Variable{ - // must match data in fakeCatalogClient - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-2", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`{"group":"foo.io","kind":"Foo","version":"v1"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-1", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - }), - }, - }, - &MockRequiredPackageSource{ - ResultSet: []deppy.Variable{ - // must match data in fakeCatalogClient - olmvariables.NewRequiredPackageVariable("test-package-2", []*catalogmetadata.Bundle{ - // test-package-2 required package - no dependencies - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-15", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "1.5.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-16", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "2.0.1"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-17", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "3.16.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - }), - }, - }, - ) - }) - - It("should return bundle variables with correct dependencies", func() { - variables, err := bdvs.GetVariables(context.TODO()) - Expect(err).NotTo(HaveOccurred()) - - var bundleVariables []*olmvariables.BundleVariable - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.BundleVariable: - bundleVariables = append(bundleVariables, v) - } - } - // Note: When accounting for Required GVKs (currently not in use), we would expect additional - // dependencies (bundles 7, 8, 9, 10, 11) to appear here due to their GVKs being required by - // some of the packages. - Expect(bundleVariables).To(WithTransform(CollectBundleVariableIDs, Equal([]string{ - "fake-catalog-test-package-bundle-2", - "fake-catalog-test-package-bundle-1", - "fake-catalog-test-package-2-bundle-15", - "fake-catalog-test-package-2-bundle-16", - "fake-catalog-test-package-2-bundle-17", - "fake-catalog-some-package-bundle-5", - "fake-catalog-some-package-bundle-4", - }))) - - // check dependencies for one of the bundles - bundle2 := VariableWithName("bundle-2")(bundleVariables) - // Note: As above, bundle-2 has GVK requirements satisfied by bundles 7, 8, and 9, but they - // will not appear in this list as we are not currently taking Required GVKs into account - Expect(bundle2.Dependencies()).To(HaveLen(2)) - Expect(bundle2.Dependencies()[0].Name).To(Equal("bundle-5")) - Expect(bundle2.Dependencies()[1].Name).To(Equal("bundle-4")) - }) - - It("should return error if dependencies not found", func() { - emptyCatalogClient := testutil.NewFakeCatalogClient(make([]*catalogmetadata.Bundle, 0)) - - bdvs = variablesources.NewBundlesAndDepsVariableSource( - &emptyCatalogClient, - &MockRequiredPackageSource{ - ResultSet: []deppy.Variable{ - // must match data in fakeCatalogClient - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-2", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`{"group":"foo.io","kind":"Foo","version":"v1"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{{Channel: declcfg.Channel{Name: "stable"}}}, - }, - { - CatalogName: "fake-catalog", - Bundle: declcfg.Bundle{ - Name: "bundle-1", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }, - }, - InChannels: []*catalogmetadata.Channel{{Channel: declcfg.Channel{Name: "stable"}}}, - }, - }), - }, - }, - ) - _, err := bdvs.GetVariables(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("could not determine dependencies for bundle with id 'fake-catalog-test-package-bundle-2': could not find package dependencies for bundle 'bundle-2'")) - }) - - It("should return error if an inner variable source returns an error", func() { - bdvs = variablesources.NewBundlesAndDepsVariableSource( - &fakeCatalogClient, - &MockRequiredPackageSource{Error: errors.New("fake error")}, - ) - _, err := bdvs.GetVariables(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("fake error")) - }) -}) - -type MockRequiredPackageSource struct { - ResultSet []deppy.Variable - Error error -} - -func (m *MockRequiredPackageSource) GetVariables(_ context.Context) ([]deppy.Variable, error) { - return m.ResultSet, m.Error -} - -func VariableWithName(name string) func(vars []*olmvariables.BundleVariable) *olmvariables.BundleVariable { - return func(vars []*olmvariables.BundleVariable) *olmvariables.BundleVariable { - for i := 0; i < len(vars); i++ { - if vars[i].Bundle().Name == name { - return vars[i] - } - } - return nil - } -} - -func CollectBundleVariableIDs(vars []*olmvariables.BundleVariable) []string { - ids := make([]string, 0, len(vars)) - for _, v := range vars { - ids = append(ids, v.Identifier().String()) - } - return ids -} diff --git a/internal/resolution/variablesources/composite.go b/internal/resolution/variablesources/composite.go deleted file mode 100644 index d0e3a20b9..000000000 --- a/internal/resolution/variablesources/composite.go +++ /dev/null @@ -1,62 +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 variablesources - -import ( - "context" - "errors" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" -) - -var _ input.VariableSource = &SliceVariableSource{} -var _ input.VariableSource = &NestedVariableSource{} - -type NestedVariableSource []func(inputVariableSource input.VariableSource) (input.VariableSource, error) - -func (s NestedVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - if len(s) == 0 { - return nil, errors.New("empty nested variable sources") - } - - var variableSource input.VariableSource - var err error - for _, constructor := range s { - variableSource, err = constructor(variableSource) - if err != nil { - return nil, err - } - } - - return variableSource.GetVariables(ctx) -} - -type SliceVariableSource []input.VariableSource - -func (s SliceVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - var variables []deppy.Variable - for _, variableSource := range s { - inputVariables, err := variableSource.GetVariables(ctx) - if err != nil { - return nil, err - } - variables = append(variables, inputVariables...) - } - - return variables, nil -} diff --git a/internal/resolution/variablesources/composite_test.go b/internal/resolution/variablesources/composite_test.go deleted file mode 100644 index bfbf859e8..000000000 --- a/internal/resolution/variablesources/composite_test.go +++ /dev/null @@ -1,172 +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 variablesources_test - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -func TestNestedVariableSource(t *testing.T) { - for _, tt := range []struct { - name string - varSources []*mockVariableSource - - wantVariables []deppy.Variable - wantErr string - }{ - { - name: "multiple nested sources", - varSources: []*mockVariableSource{ - {fakeVariables: []deppy.Variable{mockVariable("fake-var-1"), mockVariable("fake-var-2")}}, - {fakeVariables: []deppy.Variable{mockVariable("fake-var-3")}}, - }, - wantVariables: []deppy.Variable{mockVariable("fake-var-1"), mockVariable("fake-var-2"), mockVariable("fake-var-3")}, - }, - { - name: "error when no nested sources provided", - wantErr: "empty nested variable sources", - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - - nestedSource := variablesources.NestedVariableSource{} - for i := range tt.varSources { - i := i // Same reason as https://go.dev/doc/faq#closures_and_goroutines - nestedSource = append(nestedSource, func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - if i == 0 { - assert.Nil(t, inputVariableSource) - } else { - assert.Equal(t, tt.varSources[i-1], inputVariableSource) - - tt.varSources[i].inputVariableSource = inputVariableSource - } - - return tt.varSources[i], nil - }) - } - - variables, err := nestedSource.GetVariables(ctx) - if tt.wantErr != "" { - assert.EqualError(t, err, tt.wantErr) - } else { - assert.NoError(t, err) - } - assert.Equal(t, tt.wantVariables, variables) - }) - } - - t.Run("error from a nested constructor", func(t *testing.T) { - ctx := context.Background() - - nestedSource := variablesources.NestedVariableSource{ - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return nil, errors.New("fake error from a constructor") - }, - } - - variables, err := nestedSource.GetVariables(ctx) - assert.EqualError(t, err, "fake error from a constructor") - assert.Nil(t, variables) - }) -} - -func TestSliceVariableSource(t *testing.T) { - for _, tt := range []struct { - name string - varSources []input.VariableSource - - wantVariables []deppy.Variable - wantErr string - }{ - { - name: "multiple sources in the slice", - varSources: []input.VariableSource{ - &mockVariableSource{fakeVariables: []deppy.Variable{mockVariable("fake-var-1"), mockVariable("fake-var-2")}}, - &mockVariableSource{fakeVariables: []deppy.Variable{mockVariable("fake-var-3")}}, - }, - wantVariables: []deppy.Variable{mockVariable("fake-var-1"), mockVariable("fake-var-2"), mockVariable("fake-var-3")}, - }, - { - name: "error from GetVariables", - varSources: []input.VariableSource{ - &mockVariableSource{fakeVariables: []deppy.Variable{mockVariable("fake-var-1"), mockVariable("fake-var-2")}}, - &mockVariableSource{fakeError: errors.New("fake error from GetVariables")}, - }, - wantErr: "fake error from GetVariables", - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - - sliceSource := variablesources.SliceVariableSource(tt.varSources) - variables, err := sliceSource.GetVariables(ctx) - if tt.wantErr != "" { - assert.EqualError(t, err, tt.wantErr) - } else { - assert.NoError(t, err) - } - assert.Equal(t, tt.wantVariables, variables) - }) - } -} - -var _ input.VariableSource = &mockVariableSource{} - -type mockVariableSource struct { - inputVariableSource input.VariableSource - fakeVariables []deppy.Variable - fakeError error -} - -func (m *mockVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - if m.fakeError != nil { - return nil, m.fakeError - } - - if m.inputVariableSource == nil { - return m.fakeVariables, nil - } - - nestedVars, err := m.inputVariableSource.GetVariables(ctx) - if err != nil { - return nil, err - } - - return append(nestedVars, m.fakeVariables...), nil -} - -var _ deppy.Variable = mockVariable("") - -type mockVariable string - -func (m mockVariable) Identifier() deppy.Identifier { - return deppy.IdentifierFromString(string(m)) -} - -func (m mockVariable) Constraints() []deppy.Constraint { - return nil -} diff --git a/internal/resolution/variablesources/crd_constraints.go b/internal/resolution/variablesources/crd_constraints.go deleted file mode 100644 index 5cd896028..000000000 --- a/internal/resolution/variablesources/crd_constraints.go +++ /dev/null @@ -1,76 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ input.VariableSource = &CRDUniquenessConstraintsVariableSource{} - -// CRDUniquenessConstraintsVariableSource produces variables that constraint the solution to -// 1. at most 1 bundle per package -// 2. at most 1 bundle per gvk (provided by the bundle) -// these variables guarantee that no two operators provide the same gvk and no two version of -// the same operator are running at the same time. -// This variable source does not itself reach out to catalog metadata. It produces its variables -// by searching for BundleVariables that are produced by its 'inputVariableSource' and working out -// which bundles correspond to which package and which gvks are provided by which bundle -type CRDUniquenessConstraintsVariableSource struct { - inputVariableSource input.VariableSource -} - -// NewCRDUniquenessConstraintsVariableSource creates a new instance of the CRDUniquenessConstraintsVariableSource. -// its purpose if to provide variables with constraints that restrict the solutions to bundle sets where -// no two bundles come from the same package and not two bundles provide the same gvk -func NewCRDUniquenessConstraintsVariableSource(inputVariableSource input.VariableSource) *CRDUniquenessConstraintsVariableSource { - return &CRDUniquenessConstraintsVariableSource{ - inputVariableSource: inputVariableSource, - } -} - -func (g *CRDUniquenessConstraintsVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - variables, err := g.inputVariableSource.GetVariables(ctx) - if err != nil { - return nil, err - } - - // todo(perdasilva): better handle cases where a provided gvk is not found - // not all packages will necessarily export a CRD - - pkgToBundleMap := map[string]map[deppy.Identifier]struct{}{} - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.BundleVariable: - bundles := []*catalogmetadata.Bundle{v.Bundle()} - bundles = append(bundles, v.Dependencies()...) - for _, bundle := range bundles { - id := olmvariables.BundleVariableID(bundle) - // get bundleID package and update map - packageName := bundle.Package - - if _, ok := pkgToBundleMap[packageName]; !ok { - pkgToBundleMap[packageName] = map[deppy.Identifier]struct{}{} - } - pkgToBundleMap[packageName][id] = struct{}{} - } - } - } - - // create global constraint variables - for packageName, bundleIDMap := range pkgToBundleMap { - var bundleIDs []deppy.Identifier - for bundleID := range bundleIDMap { - bundleIDs = append(bundleIDs, bundleID) - } - varID := deppy.IdentifierFromString(fmt.Sprintf("%s package uniqueness", packageName)) - variables = append(variables, olmvariables.NewBundleUniquenessVariable(varID, bundleIDs...)) - } - - return variables, nil -} diff --git a/internal/resolution/variablesources/crd_constraints_test.go b/internal/resolution/variablesources/crd_constraints_test.go deleted file mode 100644 index a34f39d63..000000000 --- a/internal/resolution/variablesources/crd_constraints_test.go +++ /dev/null @@ -1,324 +0,0 @@ -package variablesources_test - -import ( - "context" - "encoding/json" - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "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" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -var channel = catalogmetadata.Channel{Channel: declcfg.Channel{Name: "stable"}} -var bundleSet = map[string]*catalogmetadata.Bundle{ - // required package bundles - "bundle-1": {Bundle: declcfg.Bundle{ - Name: "bundle-1", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"bit.io","kind":"Bit","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-2": {Bundle: declcfg.Bundle{ - Name: "bundle-2", - Package: "test-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`{"group":"foo.io","kind":"Foo","version":"v1"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`{"group":"bit.io","kind":"Bit","version":"v1"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // dependencies - "bundle-3": {Bundle: declcfg.Bundle{ - Name: "bundle-3", - Package: "some-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"fiz.io","kind":"Fiz","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-4": {Bundle: declcfg.Bundle{ - Name: "bundle-4", - Package: "some-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-package", "version": "1.5.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"fiz.io","kind":"Fiz","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-5": {Bundle: declcfg.Bundle{ - Name: "bundle-5", - Package: "some-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"fiz.io","kind":"Fiz","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-6": {Bundle: declcfg.Bundle{ - Name: "bundle-6", - Package: "some-other-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-other-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-7": {Bundle: declcfg.Bundle{ - Name: "bundle-7", - Package: "some-other-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "some-other-package", "version": "1.5.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`{"group":"foo.io","kind":"Foo","version":"v1"}`)}, - {Type: property.TypeGVKRequired, Value: json.RawMessage(`{"group":"bar.io","kind":"Bar","version":"v1"}`)}, - {Type: property.TypePackageRequired, Value: json.RawMessage(`{"packageName": "another-package", "versionRange": "< 2.0.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // dependencies of dependencies - "bundle-8": {Bundle: declcfg.Bundle{ - Name: "bundle-8", - Package: "another-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "another-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-9": {Bundle: declcfg.Bundle{ - Name: "bundle-9", - Package: "bar-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "bar-package", "version": "1.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"bar.io","kind":"Bar","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-10": {Bundle: declcfg.Bundle{ - Name: "bundle-10", - Package: "bar-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "bar-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"bar.io","kind":"Bar","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // test-package-2 required package - no dependencies - "bundle-14": {Bundle: declcfg.Bundle{ - Name: "bundle-14", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "1.5.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"buz.io","kind":"Buz","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-15": {Bundle: declcfg.Bundle{ - Name: "bundle-15", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "2.0.1"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"buz.io","kind":"Buz","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-16": {Bundle: declcfg.Bundle{ - Name: "bundle-16", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "3.16.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"buz.io","kind":"Buz","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - - // completely unrelated - "bundle-11": {Bundle: declcfg.Bundle{ - Name: "bundle-11", - Package: "unrelated-package", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "unrelated-package", "version": "2.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"buz.io","kind":"Buz","version":"v1alpha1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-12": {Bundle: declcfg.Bundle{ - Name: "bundle-12", - Package: "unrelated-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "unrelated-package-2", "version": "2.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"buz.io","kind":"Buz","version":"v1alpha1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - "bundle-13": {Bundle: declcfg.Bundle{ - Name: "bundle-13", - Package: "unrelated-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "unrelated-package-2", "version": "3.0.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"buz.io","kind":"Buz","version":"v1alpha1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, -} - -var _ = Describe("CRDUniquenessConstraintsVariableSource", func() { - var ( - inputVariableSource *MockInputVariableSource - crdConstraintVariableSource *variablesources.CRDUniquenessConstraintsVariableSource - ctx context.Context - ) - - BeforeEach(func() { - inputVariableSource = &MockInputVariableSource{} - crdConstraintVariableSource = variablesources.NewCRDUniquenessConstraintsVariableSource(inputVariableSource) - ctx = context.Background() - }) - - It("should get variables from the input variable source and create global constraint variables", func() { - inputVariableSource.ResultSet = []deppy.Variable{ - olmvariables.NewRequiredPackageVariable("test-package", []*catalogmetadata.Bundle{ - bundleSet["bundle-2"], - bundleSet["bundle-1"], - }), - olmvariables.NewRequiredPackageVariable("test-package-2", []*catalogmetadata.Bundle{ - bundleSet["bundle-14"], - bundleSet["bundle-15"], - bundleSet["bundle-16"], - }), - olmvariables.NewBundleVariable( - bundleSet["bundle-2"], - []*catalogmetadata.Bundle{ - bundleSet["bundle-3"], - bundleSet["bundle-4"], - bundleSet["bundle-5"], - bundleSet["bundle-6"], - bundleSet["bundle-7"], - }, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-1"], - []*catalogmetadata.Bundle{ - bundleSet["bundle-6"], - bundleSet["bundle-7"], - bundleSet["bundle-8"], - }, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-3"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-4"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-5"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-6"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-7"], - []*catalogmetadata.Bundle{ - bundleSet["bundle-8"], - bundleSet["bundle-9"], - bundleSet["bundle-10"], - }, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-8"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-9"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-10"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-14"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-15"], - []*catalogmetadata.Bundle{}, - ), - olmvariables.NewBundleVariable( - bundleSet["bundle-16"], - []*catalogmetadata.Bundle{}, - ), - } - variables, err := crdConstraintVariableSource.GetVariables(ctx) - Expect(err).ToNot(HaveOccurred()) - // Note: When accounting for GVK Uniqueness (which we are currently not doing), we - // would expect to have 26 variables from the 5 unique GVKs (Bar, Bit, Buz, Fiz, Foo). - Expect(variables).To(HaveLen(21)) - var crdConstraintVariables []*olmvariables.BundleUniquenessVariable - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.BundleUniquenessVariable: - crdConstraintVariables = append(crdConstraintVariables, v) - } - } - // Note: As above, the 5 GVKs would appear here as GVK uniqueness constraints - // if GVK Uniqueness were being accounted for. - Expect(crdConstraintVariables).To(WithTransform(CollectGlobalConstraintVariableIDs, ConsistOf([]string{ - "another-package package uniqueness", - "bar-package package uniqueness", - "test-package-2 package uniqueness", - "test-package package uniqueness", - "some-package package uniqueness", - "some-other-package package uniqueness", - }))) - }) - - It("should return an error if input variable source returns an error", func() { - inputVariableSource = &MockInputVariableSource{Err: fmt.Errorf("error getting variables")} - crdConstraintVariableSource = variablesources.NewCRDUniquenessConstraintsVariableSource(inputVariableSource) - _, err := crdConstraintVariableSource.GetVariables(ctx) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("error getting variables")) - }) -}) - -type MockInputVariableSource struct { - ResultSet []deppy.Variable - Err error -} - -func (m *MockInputVariableSource) GetVariables(_ context.Context) ([]deppy.Variable, error) { - if m.Err != nil { - return nil, m.Err - } - return m.ResultSet, nil -} - -func CollectGlobalConstraintVariableIDs(vars []*olmvariables.BundleUniquenessVariable) []string { - ids := make([]string, 0, len(vars)) - for _, v := range vars { - ids = append(ids, v.Identifier().String()) - } - return ids -} diff --git a/internal/resolution/variablesources/installed_package.go b/internal/resolution/variablesources/installed_package.go deleted file mode 100644 index 03a7eeafa..000000000 --- a/internal/resolution/variablesources/installed_package.go +++ /dev/null @@ -1,85 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - "sort" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "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" - "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ input.VariableSource = &InstalledPackageVariableSource{} - -type InstalledPackageVariableSource struct { - catalogClient BundleProvider - successors successorsFunc - bundleImage string -} - -func (r *InstalledPackageVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - allBundles, err := r.catalogClient.Bundles(ctx) - if err != nil { - return nil, err - } - - // find corresponding bundle for the installed content - resultSet := catalogfilter.Filter(allBundles, catalogfilter.WithBundleImage(r.bundleImage)) - if len(resultSet) == 0 { - return nil, r.notFoundError() - } - - // TODO: fast follow - we should check whether we are already supporting the channel attribute in the operator spec. - // if so, we should take the value from spec of the operator CR in the owner ref of the bundle deployment. - // If that channel is set, we need to update the filter above to filter by channel as well. - sort.SliceStable(resultSet, func(i, j int) bool { - return catalogsort.ByVersion(resultSet[i], resultSet[j]) - }) - installedBundle := resultSet[0] - - upgradeEdges, err := r.successors(allBundles, installedBundle) - if err != nil { - return nil, err - } - - // you can always upgrade to yourself, i.e. not upgrade - upgradeEdges = append(upgradeEdges, installedBundle) - return []deppy.Variable{ - variables.NewInstalledPackageVariable(installedBundle.Package, upgradeEdges), - }, nil -} - -func (r *InstalledPackageVariableSource) notFoundError() error { - return fmt.Errorf("bundleImage %q not found", r.bundleImage) -} - -func NewInstalledPackageVariableSource(catalogClient BundleProvider, bundleImage string) (*InstalledPackageVariableSource, error) { - return &InstalledPackageVariableSource{ - catalogClient: catalogClient, - bundleImage: bundleImage, - successors: legacySemanticsSuccessors, - }, 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 the bundle provided - // TODO: this algorithm does not yet consider skips and skipRange - upgradeEdges := catalogfilter.Filter(allBundles, catalogfilter.Replaces(installedBundle.Name)) - sort.SliceStable(upgradeEdges, func(i, j int) bool { - return catalogsort.ByVersion(upgradeEdges[i], upgradeEdges[j]) - }) - - return upgradeEdges, nil -} diff --git a/internal/resolution/variablesources/installed_package_test.go b/internal/resolution/variablesources/installed_package_test.go deleted file mode 100644 index 287f476db..000000000 --- a/internal/resolution/variablesources/installed_package_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package variablesources_test - -import ( - "context" - "encoding/json" - "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/stretchr/testify/assert" - "github.com/stretchr/testify/require" - featuregatetesting "k8s.io/component-base/featuregate/testing" - - "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" - "github.com/operator-framework/operator-controller/pkg/features" - testutil "github.com/operator-framework/operator-controller/test/util" -) - -func TestInstalledPackageVariableSource(t *testing.T) { - channel := catalogmetadata.Channel{Channel: declcfg.Channel{ - Name: "stable", - Entries: []declcfg.ChannelEntry{ - { - Name: "test-package.v1.0.0", - }, - { - Name: "test-package.v2.0.0", - Replaces: "test-package.v1.0.0", - }, - { - Name: "test-package.v2.1.0", - Replaces: "test-package.v2.0.0", - }, - { - Name: "test-package.v2.2.0", - Replaces: "test-package.v2.1.0", - }, - { - Name: "test-package.v3.0.0", - Replaces: "test-package.v2.2.0", - }, - { - Name: "test-package.v4.0.0", - Replaces: "test-package.v3.0.0", - }, - { - Name: "test-package.v5.0.0", - Replaces: "test-package.v4.0.0", - }, - }, - }} - bundleList := []*catalogmetadata.Bundle{ - {Bundle: declcfg.Bundle{ - Name: "test-package.v1.0.0", - Package: "test-package", - Image: "registry.io/repo/test-package@v1.0.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "1.0.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - {Bundle: declcfg.Bundle{ - Name: "test-package.v3.0.0", - Package: "test-package", - Image: "registry.io/repo/test-package@v3.0.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "3.0.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - {Bundle: declcfg.Bundle{ - Name: "test-package.v2.0.0", - Package: "test-package", - Image: "registry.io/repo/test-package@v2.0.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - {Bundle: declcfg.Bundle{ - Name: "test-package.v2.1.0", - Package: "test-package", - Image: "registry.io/repo/test-package@v2.1.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.1.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - {Bundle: declcfg.Bundle{ - Name: "test-package.v2.2.0", - Package: "test-package", - Image: "registry.io/repo/test-package@v2.2.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.2.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - {Bundle: declcfg.Bundle{ - Name: "test-package.v4.0.0", - Package: "test-package", - Image: "registry.io/repo/test-package@v4.0.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "4.0.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - {Bundle: declcfg.Bundle{ - Name: "test-package.v5.0.0", - Package: "test-package", - Image: "registry.io/repo/test-package@v5.0.0", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "5-0.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - } - - const bundleImage = "registry.io/repo/test-package@v2.0.0" - fakeCatalogClient := testutil.NewFakeCatalogClient(bundleList) - - t.Run("with ForceSemverUpgradeConstraints feature gate disabled", func(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, false)() - - ipvs, err := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, bundleImage) - require.NoError(t, err) - - variables, err := ipvs.GetVariables(context.TODO()) - require.NoError(t, err) - require.Len(t, variables, 1) - packageVariable, ok := variables[0].(*olmvariables.InstalledPackageVariable) - assert.True(t, ok) - assert.Equal(t, deppy.IdentifierFromString("installed package test-package"), packageVariable.Identifier()) - - // ensure bundles are in version order (high to low) - bundles := packageVariable.Bundles() - require.Len(t, bundles, 2) - assert.Equal(t, "test-package.v2.1.0", packageVariable.Bundles()[0].Name) - assert.Equal(t, "test-package.v2.0.0", packageVariable.Bundles()[1].Name) - }) -} diff --git a/internal/resolution/variablesources/olm.go b/internal/resolution/variablesources/olm.go new file mode 100644 index 000000000..299230bcc --- /dev/null +++ b/internal/resolution/variablesources/olm.go @@ -0,0 +1,317 @@ +package variablesources + +import ( + "context" + "fmt" + "sort" + + mmsemver "github.com/Masterminds/semver/v3" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/input" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1alpha1 "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" +) + +var _ input.VariableSource = &OLMVariableSource{} + +type OLMVariableSource struct { + client client.Client + catalogClient BundleProvider +} + +func NewOLMVariableSource(cl client.Client, catalogClient BundleProvider) *OLMVariableSource { + return &OLMVariableSource{ + client: cl, + catalogClient: catalogClient, + } +} + +func (o *OLMVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { + operatorList := operatorsv1alpha1.OperatorList{} + if err := o.client.List(ctx, &operatorList); err != nil { + return nil, err + } + + bundleDeployments := rukpakv1alpha1.BundleDeploymentList{} + if err := o.client.List(ctx, &bundleDeployments); err != nil { + return nil, err + } + + allBundles, err := o.catalogClient.Bundles(ctx) + if err != nil { + return nil, err + } + + requiredPackages := []*olmvariables.RequiredPackageVariable{} + for _, operator := range operatorList.Items { + requiredPackage, err := RequiredPackageVariable(allBundles, operator.Spec.PackageName, operator.Spec.Channel, operator.Spec.Version) + if err != nil { + return nil, err + } + requiredPackages = append(requiredPackages, requiredPackage) + } + + installedPackages, err := InstalledPackageVariables(allBundles, bundleDeployments.Items) + if err != nil { + return nil, err + } + + bundles, err := BundleVariables(allBundles, requiredPackages, installedPackages) + if err != nil { + return nil, err + } + + bundleUniqueness, err := BundleUniquenessVariables(bundles) + if err != nil { + return nil, err + } + + 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 +} + +// RequiredPackageVariable returns a variable which represent +// explicit requirement for a package from an user. +// This is when an user explicitly asks "install this" via Operator API. +func RequiredPackageVariable(allBundles []*catalogmetadata.Bundle, packageName, channelName, versionRange string) (*olmvariables.RequiredPackageVariable, error) { + 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 '%s': %w", versionRange, err) + } + predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) + } + + resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...)) + if len(resultSet) == 0 { + // TODO: update this error message when/if we decide to support version ranges as opposed to fixing the version + // context: we originally wanted to support version ranges and take the highest version that satisfies the range + // during the upstream call on the 2023-04-11 we decided to pin the version instead. But, we'll keep version range + // support under the covers in case we decide to pivot back. + if versionRange != "" && channelName != "" { + return nil, fmt.Errorf("package '%s' at version '%s' in channel '%s' not found", packageName, versionRange, channelName) + } + if versionRange != "" { + return nil, fmt.Errorf("package '%s' at version '%s' not found", packageName, versionRange) + } + if channelName != "" { + return nil, fmt.Errorf("package '%s' in channel '%s' not found", packageName, channelName) + } + return nil, fmt.Errorf("package '%s' not found", packageName) + } + sort.SliceStable(resultSet, func(i, j int) bool { + return catalogsort.ByVersion(resultSet[i], resultSet[j]) + }) + + return olmvariables.NewRequiredPackageVariable(packageName, resultSet), nil +} + +// InstalledPackageVariables returns variables representing packages +// already installed in the system. +// Meaning that each BundleDeployment managed by operator-controller +// has own variable. +func InstalledPackageVariables( + allBundles []*catalogmetadata.Bundle, + bundleDeployments []rukpakv1alpha1.BundleDeployment, +) ([]*olmvariables.InstalledPackageVariable, error) { + // TODO: Switch between legacy and semver based on feature flag once semver implemented + var successors successorsFunc = legacySemanticsSuccessors + + result := make([]*olmvariables.InstalledPackageVariable, 0, len(bundleDeployments)) + processed := sets.Set[string]{} + for _, bundleDeployment := range bundleDeployments { + sourceImage := bundleDeployment.Spec.Template.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.WithBundleImage(bundleImage)) + if len(resultSet) == 0 { + return nil, fmt.Errorf("bundleImage %q not found", bundleImage) + } + + // TODO: fast follow - we should check whether we are already supporting the channel attribute in the operator spec. + // if so, we should take the value from spec of the operator CR in the owner ref of the bundle deployment. + // If that channel is set, we need to update the filter above to filter by channel as well. + 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 the bundle provided + // TODO: this algorithm does not yet consider skips and skipRange + upgradeEdges := catalogfilter.Filter(allBundles, catalogfilter.Replaces(installedBundle.Name)) + sort.SliceStable(upgradeEdges, func(i, j int) bool { + return catalogsort.ByVersion(upgradeEdges[i], upgradeEdges[j]) + }) + + return upgradeEdges, nil +} + +func BundleVariables( + 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 with id '%s': %w", id, 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 + // todo(perdasilva): disambiguate between not found and actual errors + 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("could not find package dependencies for bundle '%s'", bundle.Name) + } + 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 +} + +func BundleUniquenessVariables(bundleVariables []*olmvariables.BundleVariable) ([]*olmvariables.BundleUniquenessVariable, error) { + result := []*olmvariables.BundleUniquenessVariable{} + + // TODO: https://github.com/operator-framework/operator-controller/issues/459 + pkgToBundleMap := map[string]sets.Set[deppy.Identifier]{} + for _, bundleVariable := range bundleVariables { + bundles := []*catalogmetadata.Bundle{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 := pkgToBundleMap[packageName]; !ok { + pkgToBundleMap[packageName] = sets.Set[deppy.Identifier]{} + } + pkgToBundleMap[packageName].Insert(id) + } + } + + // create global constraint variables + for packageName, bundleIDMap := range pkgToBundleMap { + var bundleIDs []deppy.Identifier + for bundleID := range bundleIDMap { + bundleIDs = append(bundleIDs, bundleID) + } + varID := deppy.IdentifierFromString(fmt.Sprintf("%s package uniqueness", packageName)) + result = append(result, olmvariables.NewBundleUniquenessVariable(varID, bundleIDs...)) + } + + return result, nil +} diff --git a/internal/resolution/variablesources/operator.go b/internal/resolution/variablesources/operator.go deleted file mode 100644 index 19cc88443..000000000 --- a/internal/resolution/variablesources/operator.go +++ /dev/null @@ -1,55 +0,0 @@ -package variablesources - -import ( - "context" - - operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ input.VariableSource = &OperatorVariableSource{} - -type OperatorVariableSource struct { - client client.Client - catalogClient BundleProvider - inputVariableSource input.VariableSource -} - -func NewOperatorVariableSource(cl client.Client, catalogClient BundleProvider, inputVariableSource input.VariableSource) *OperatorVariableSource { - return &OperatorVariableSource{ - client: cl, - catalogClient: catalogClient, - inputVariableSource: inputVariableSource, - } -} - -func (o *OperatorVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - variableSources := SliceVariableSource{} - if o.inputVariableSource != nil { - variableSources = append(variableSources, o.inputVariableSource) - } - - operatorList := operatorsv1alpha1.OperatorList{} - if err := o.client.List(ctx, &operatorList); err != nil { - return nil, err - } - - // build required package variable sources - for _, operator := range operatorList.Items { - rps, err := NewRequiredPackageVariableSource( - o.catalogClient, - operator.Spec.PackageName, - InVersionRange(operator.Spec.Version), - InChannel(operator.Spec.Channel), - ) - if err != nil { - return nil, err - } - variableSources = append(variableSources, rps) - } - - return variableSources.GetVariables(ctx) -} diff --git a/internal/resolution/variablesources/operator_test.go b/internal/resolution/variablesources/operator_test.go deleted file mode 100644 index 426cf82c2..000000000 --- a/internal/resolution/variablesources/operator_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package variablesources_test - -import ( - "context" - "encoding/json" - "errors" - - "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/onsi/ginkgo/v2" - - . "github.com/onsi/gomega" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - testutil "github.com/operator-framework/operator-controller/test/util" - - "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 FakeClient(objects ...client.Object) client.Client { - scheme := runtime.NewScheme() - utilruntime.Must(operatorsv1alpha1.AddToScheme(scheme)) - return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() -} - -func operator(name string) *operatorsv1alpha1.Operator { - return &operatorsv1alpha1.Operator{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: operatorsv1alpha1.OperatorSpec{ - PackageName: name, - }, - } -} - -var _ = Describe("OperatorVariableSource", func() { - var betaChannel catalogmetadata.Channel - var stableChannel catalogmetadata.Channel - var testBundleList []*catalogmetadata.Bundle - - BeforeEach(func() { - betaChannel = catalogmetadata.Channel{ - Channel: declcfg.Channel{ - Name: "beta", - Entries: []declcfg.ChannelEntry{ - { - Name: "operatorhub/prometheus/0.37.0", - Replaces: "operatorhub/prometheus/0.32.0", - }, - { - Name: "operatorhub/prometheus/0.47.0", - Replaces: "operatorhub/prometheus/0.37.0", - }, - }, - }, - } - - stableChannel = catalogmetadata.Channel{ - Channel: declcfg.Channel{ - Name: "stable", - Entries: []declcfg.ChannelEntry{ - { - Name: "operatorhub/packageA/2.0.0", - }, - }, - }, - } - - testBundleList = []*catalogmetadata.Bundle{ - {Bundle: declcfg.Bundle{ - Name: "operatorhub/prometheus/0.37.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName":"prometheus","version":"0.37.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"monitoring.coreos.com","kind":"Alertmanager","version":"v1"}, {"group":"monitoring.coreos.com","kind":"Prometheus","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&betaChannel}, - }, - {Bundle: declcfg.Bundle{ - Name: "operatorhub/prometheus/0.47.0", - Package: "prometheus", - Image: "quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName":"prometheus","version":"0.47.0"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"monitoring.coreos.com","kind":"Alertmanager","version":"v1"}, {"group":"monitoring.coreos.com","kind":"Prometheus","version":"v1alpha1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&betaChannel}, - }, - {Bundle: declcfg.Bundle{ - Name: "operatorhub/packageA/2.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"}`)}, - {Type: property.TypeGVK, Value: json.RawMessage(`[{"group":"foo.io","kind":"Foo","version":"v1"}]`)}, - }}, - InChannels: []*catalogmetadata.Channel{&stableChannel}, - }, - } - - }) - - It("should produce RequiredPackage variables", func() { - cl := FakeClient(operator("prometheus"), operator("packageA")) - fakeCatalogClient := testutil.NewFakeCatalogClient(testBundleList) - opVariableSource := variablesources.NewOperatorVariableSource(cl, &fakeCatalogClient, &MockRequiredPackageSource{}) - variables, err := opVariableSource.GetVariables(context.Background()) - Expect(err).ToNot(HaveOccurred()) - - packageRequiredVariables := filterVariables[*olmvariables.RequiredPackageVariable](variables) - Expect(packageRequiredVariables).To(HaveLen(2)) - Expect(packageRequiredVariables).To(WithTransform(func(bvars []*olmvariables.RequiredPackageVariable) map[deppy.Identifier]int { - out := map[deppy.Identifier]int{} - for _, variable := range bvars { - out[variable.Identifier()] = len(variable.Bundles()) - } - return out - }, Equal(map[deppy.Identifier]int{ - deppy.IdentifierFromString("required package prometheus"): 2, - deppy.IdentifierFromString("required package packageA"): 1, - }))) - }) - - It("should return an errors when they occur", func() { - cl := FakeClient(operator("prometheus"), operator("packageA")) - fakeCatalogClient := testutil.NewFakeCatalogClientWithError(errors.New("something bad happened")) - - opVariableSource := variablesources.NewOperatorVariableSource(cl, &fakeCatalogClient, nil) - _, err := opVariableSource.GetVariables(context.Background()) - Expect(err).To(HaveOccurred()) - }) -}) - -func filterVariables[D deppy.Variable](variables []deppy.Variable) []D { - var out []D - for _, variable := range variables { - switch v := variable.(type) { - case D: - out = append(out, v) - } - } - return out -} diff --git a/internal/resolution/variablesources/required_package.go b/internal/resolution/variablesources/required_package.go deleted file mode 100644 index 568a7ebfa..000000000 --- a/internal/resolution/variablesources/required_package.go +++ /dev/null @@ -1,108 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - "sort" - - mmsemver "github.com/Masterminds/semver/v3" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - "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" -) - -var _ input.VariableSource = &RequiredPackageVariableSource{} - -type RequiredPackageVariableSourceOption func(*RequiredPackageVariableSource) error - -func InVersionRange(versionRange string) RequiredPackageVariableSourceOption { - return func(r *RequiredPackageVariableSource) error { - if versionRange != "" { - vr, err := mmsemver.NewConstraint(versionRange) - if err == nil { - r.versionRange = versionRange - r.predicates = append(r.predicates, catalogfilter.InMastermindsSemverRange(vr)) - return nil - } - - return fmt.Errorf("invalid version range '%s': %w", versionRange, err) - } - return nil - } -} - -func InChannel(channelName string) RequiredPackageVariableSourceOption { - return func(r *RequiredPackageVariableSource) error { - if channelName != "" { - r.channelName = channelName - r.predicates = append(r.predicates, catalogfilter.InChannel(channelName)) - } - return nil - } -} - -type RequiredPackageVariableSource struct { - catalogClient BundleProvider - - packageName string - versionRange string - channelName string - predicates []catalogfilter.Predicate[catalogmetadata.Bundle] -} - -func NewRequiredPackageVariableSource(catalogClient BundleProvider, packageName string, options ...RequiredPackageVariableSourceOption) (*RequiredPackageVariableSource, error) { - if packageName == "" { - return nil, fmt.Errorf("package name must not be empty") - } - r := &RequiredPackageVariableSource{ - catalogClient: catalogClient, - - packageName: packageName, - predicates: []catalogfilter.Predicate[catalogmetadata.Bundle]{catalogfilter.WithPackageName(packageName)}, - } - for _, option := range options { - if err := option(r); err != nil { - return nil, err - } - } - return r, nil -} - -func (r *RequiredPackageVariableSource) GetVariables(ctx context.Context) ([]deppy.Variable, error) { - resultSet, err := r.catalogClient.Bundles(ctx) - if err != nil { - return nil, err - } - - resultSet = catalogfilter.Filter(resultSet, catalogfilter.And(r.predicates...)) - if len(resultSet) == 0 { - return nil, r.notFoundError() - } - sort.SliceStable(resultSet, func(i, j int) bool { - return catalogsort.ByVersion(resultSet[i], resultSet[j]) - }) - return []deppy.Variable{ - olmvariables.NewRequiredPackageVariable(r.packageName, resultSet), - }, nil -} - -func (r *RequiredPackageVariableSource) notFoundError() error { - // TODO: update this error message when/if we decide to support version ranges as opposed to fixing the version - // context: we originally wanted to support version ranges and take the highest version that satisfies the range - // during the upstream call on the 2023-04-11 we decided to pin the version instead. But, we'll keep version range - // support under the covers in case we decide to pivot back. - if r.versionRange != "" && r.channelName != "" { - return fmt.Errorf("package '%s' at version '%s' in channel '%s' not found", r.packageName, r.versionRange, r.channelName) - } - if r.versionRange != "" { - return fmt.Errorf("package '%s' at version '%s' not found", r.packageName, r.versionRange) - } - if r.channelName != "" { - return fmt.Errorf("package '%s' in channel '%s' not found", r.packageName, r.channelName) - } - return fmt.Errorf("package '%s' not found", r.packageName) -} diff --git a/internal/resolution/variablesources/required_package_test.go b/internal/resolution/variablesources/required_package_test.go deleted file mode 100644 index ae87f874e..000000000 --- a/internal/resolution/variablesources/required_package_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package variablesources_test - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "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" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" - testutil "github.com/operator-framework/operator-controller/test/util" -) - -var _ = Describe("RequiredPackageVariableSource", func() { - var ( - rpvs *variablesources.RequiredPackageVariableSource - fakeCatalogClient testutil.FakeCatalogClient - packageName string - ) - - BeforeEach(func() { - var err error - packageName = "test-package" - channel := catalogmetadata.Channel{Channel: declcfg.Channel{ - Name: "stable", - }} - bundleList := []*catalogmetadata.Bundle{ - {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{&channel}, - }, - {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{&channel}, - }, - {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{&channel}, - }, - // add some bundles from a different package - {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{&channel}, - }, - {Bundle: declcfg.Bundle{ - Name: "test-package-2.v2.0.0", - Package: "test-package-2", - Properties: []property.Property{ - {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package-2", "version": "2.0.0"}`)}, - }}, - InChannels: []*catalogmetadata.Channel{&channel}, - }, - } - fakeCatalogClient = testutil.NewFakeCatalogClient(bundleList) - rpvs, err = variablesources.NewRequiredPackageVariableSource(&fakeCatalogClient, packageName) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should return the correct package variable", func() { - variables, err := rpvs.GetVariables(context.TODO()) - Expect(err).NotTo(HaveOccurred()) - Expect(variables).To(HaveLen(1)) - reqPackageVar, ok := variables[0].(*olmvariables.RequiredPackageVariable) - Expect(ok).To(BeTrue()) - Expect(reqPackageVar.Identifier()).To(Equal(deppy.IdentifierFromString(fmt.Sprintf("required package %s", packageName)))) - Expect(reqPackageVar.Bundles()).To(HaveLen(3)) - // ensure bundles are in version order (high to low) - Expect(reqPackageVar.Bundles()[0].Name).To(Equal("test-package.v3.0.0")) - Expect(reqPackageVar.Bundles()[1].Name).To(Equal("test-package.v2.0.0")) - Expect(reqPackageVar.Bundles()[2].Name).To(Equal("test-package.v1.0.0")) - }) - - It("should filter by version range", func() { - // recreate source with version range option - var err error - rpvs, err = variablesources.NewRequiredPackageVariableSource(&fakeCatalogClient, packageName, variablesources.InVersionRange(">=1.0.0 !=2.0.0 <3.0.0")) - Expect(err).NotTo(HaveOccurred()) - - variables, err := rpvs.GetVariables(context.TODO()) - Expect(err).NotTo(HaveOccurred()) - Expect(variables).To(HaveLen(1)) - reqPackageVar, ok := variables[0].(*olmvariables.RequiredPackageVariable) - Expect(ok).To(BeTrue()) - Expect(reqPackageVar.Identifier()).To(Equal(deppy.IdentifierFromString(fmt.Sprintf("required package %s", packageName)))) - - Expect(reqPackageVar.Bundles()).To(HaveLen(1)) - // test-package.v1.0.0 is the only package that matches the provided filter - Expect(reqPackageVar.Bundles()[0].Name).To(Equal("test-package.v1.0.0")) - }) - - It("should fail with bad semver range", func() { - _, err := variablesources.NewRequiredPackageVariableSource(&fakeCatalogClient, packageName, variablesources.InVersionRange("not a valid semver")) - Expect(err).To(HaveOccurred()) - }) - - It("should return an error if package not found", func() { - emptyCatalogClient := testutil.NewFakeCatalogClient([]*catalogmetadata.Bundle{}) - rpvs, err := variablesources.NewRequiredPackageVariableSource(&emptyCatalogClient, packageName) - Expect(err).NotTo(HaveOccurred()) - _, err = rpvs.GetVariables(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("package 'test-package' not found")) - }) - - It("should return an error if catalog client errors", func() { - testError := errors.New("something bad happened") - emptyCatalogClient := testutil.NewFakeCatalogClientWithError(testError) - rpvs, err := variablesources.NewRequiredPackageVariableSource(&emptyCatalogClient, packageName, variablesources.InVersionRange(">=1.0.0 !=2.0.0 <3.0.0")) - Expect(err).NotTo(HaveOccurred()) - _, err = rpvs.GetVariables(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(testError)) - }) -}) diff --git a/internal/resolution/variablesources/variablesources_test.go b/internal/resolution/variablesources/variablesources_test.go deleted file mode 100644 index 7bb8d97b8..000000000 --- a/internal/resolution/variablesources/variablesources_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package variablesources_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestVariableSources(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Variable Sources Suite") -}