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..30d0139db 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,19 @@ 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 { + // Using the fake Kubernetes client and creating objects + // in it from manifests & CLI flags is fine for PoC. + // But when/if we decide to proceed with CLI/offline resolution + // we will need to come up with a better way to create inputs + // for resolver when working with CLI. + // + // We will need to think about multiple types of inputs: + // - How to read required package (what we want to install/update) + // - How to read bundles from multiple catalogs + // - How to take into account cluster information. Some package + // will have constraints like "need Kubernetes version to be >= X" + // or "need >= 3 worker nodes"). clientBuilder := fake.NewClientBuilder().WithScheme(scheme) if inputDir != "" { @@ -124,10 +134,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..3d045fff5 100644 --- a/cmd/resolutioncli/variable_source.go +++ b/cmd/resolutioncli/variable_source.go @@ -17,28 +17,87 @@ limitations under the License. package main import ( - "github.com/operator-framework/deppy/pkg/deppy/input" + "context" + "github.com/operator-framework/deppy/pkg/deppy" + 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 +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 05d070367..6741dfeed 100644 --- a/internal/controllers/operator_controller_test.go +++ b/internal/controllers/operator_controller_test.go @@ -28,6 +28,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" "github.com/operator-framework/operator-controller/pkg/features" testutil "github.com/operator-framework/operator-controller/test/util" ) @@ -44,7 +45,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() { @@ -1059,7 +1060,7 @@ func TestOperatorUpgrade(t *testing.T) { reconciler := &controllers.OperatorReconciler{ Client: cl, Scheme: sch, - Resolver: solver.NewDeppySolver(controllers.NewVariableSource(cl, &fakeCatalogClient)), + Resolver: solver.NewDeppySolver(variablesources.NewOLMVariableSource(cl, &fakeCatalogClient)), } t.Run("semver upgrade constraints", func(t *testing.T) { 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/bundles_and_dependencies.go b/internal/resolution/variablesources/bundle.go similarity index 53% rename from internal/resolution/variablesources/bundles_and_dependencies.go rename to internal/resolution/variablesources/bundle.go index 88eaafc94..4cf86d8b4 100644 --- a/internal/resolution/variablesources/bundles_and_dependencies.go +++ b/internal/resolution/variablesources/bundle.go @@ -1,63 +1,34 @@ package variablesources import ( - "context" "fmt" "sort" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" "k8s.io/apimachinery/pkg/util/sets" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/operator-controller/internal/catalogmetadata" catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" ) -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 +func BundleVariables( + allBundles []*catalogmetadata.Bundle, + requiredPackages []*olmvariables.RequiredPackageVariable, + installedPackages []*olmvariables.InstalledPackageVariable, +) ([]*olmvariables.BundleVariable, error) { 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()...) - } + for _, variable := range requiredPackages { + bundleQueue = append(bundleQueue, variable.Bundles()...) } - - allBundles, err := b.catalogClient.Bundles(ctx) - if err != nil { - return nil, err + 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 @@ -73,7 +44,7 @@ func (b *BundlesAndDepsVariableSource) GetVariables(ctx context.Context) ([]depp visited.Insert(id) // get bundle dependencies - dependencies, err := b.filterBundleDependencies(allBundles, head) + dependencies, err := filterBundleDependencies(allBundles, head) if err != nil { return nil, fmt.Errorf("could not determine dependencies for bundle with id '%s': %w", id, err) } @@ -82,21 +53,23 @@ func (b *BundlesAndDepsVariableSource) GetVariables(ctx context.Context) ([]depp bundleQueue = append(bundleQueue, dependencies...) // create variable - variables = append(variables, olmvariables.NewBundleVariable(head, dependencies)) + result = append(result, olmvariables.NewBundleVariable(head, dependencies)) } - return variables, nil + return result, nil } -func (b *BundlesAndDepsVariableSource) filterBundleDependencies(allBundles []*catalogmetadata.Bundle, bundle *catalogmetadata.Bundle) ([]*catalogmetadata.Bundle, error) { +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))) + 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) } diff --git a/internal/resolution/variablesources/bundle_deployment.go b/internal/resolution/variablesources/bundle_deployment.go deleted file mode 100644 index 95fe55d59..000000000 --- a/internal/resolution/variablesources/bundle_deployment.go +++ /dev/null @@ -1,57 +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" - "k8s.io/apimachinery/pkg/util/sets" - "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 := sets.Set[string]{} - for _, bundleDeployment := range bundleDeployments.Items { - sourceImage := bundleDeployment.Spec.Template.Spec.Source.Image - if sourceImage != nil && sourceImage.Ref != "" { - if processed.Has(sourceImage.Ref) { - continue - } - processed.Insert(sourceImage.Ref) - 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/bundle_test.go b/internal/resolution/variablesources/bundle_test.go new file mode 100644 index 000000000..f334ef3c1 --- /dev/null +++ b/internal/resolution/variablesources/bundle_test.go @@ -0,0 +1,338 @@ +package variablesources_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" + "github.com/operator-framework/operator-controller/internal/resolution/variablesources" +) + +func TestBundleVariables(t *testing.T) { + channel := catalogmetadata.Channel{Channel: declcfg.Channel{Name: "stable"}} + allBundles := []*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}, + }, + } + + t.Run("valid dependencies", func(t *testing.T) { + requiredPackages := []*olmvariables.RequiredPackageVariable{ + 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}, + }, + }), + } + installedPackages := []*olmvariables.InstalledPackageVariable{ + olmvariables.NewInstalledPackageVariable("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}, + }, + }), + } + + bundles, err := variablesources.BundleVariables(allBundles, requiredPackages, installedPackages) + require.NoError(t, err) + + // Note: When accounting for Required GVKs (currently not implemented), we would expect additional + // dependencies to appear here due to their GVKs being required by some of the packages. + expectedIDs := []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", + } + actualIDs := collectVariableIDs(bundles) + assert.EqualValues(t, expectedIDs, actualIDs) + + // check dependencies for one of the bundles + bundle2 := findVariableWithName(bundles, "bundle-2") + // 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 + dependencies := bundle2.Dependencies() + require.Len(t, dependencies, 2) + assert.Equal(t, "bundle-5", dependencies[0].Name) + assert.Equal(t, "bundle-4", dependencies[1].Name) + }) + + t.Run("non existent dependencies", func(t *testing.T) { + allBundles := []*catalogmetadata.Bundle{} + requiredPackages := []*olmvariables.RequiredPackageVariable{ + 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"}}}, + }, + }), + } + installedPackages := []*olmvariables.InstalledPackageVariable{} + + bundles, err := variablesources.BundleVariables(allBundles, requiredPackages, installedPackages) + assert.ErrorContains(t, err, "could not determine dependencies for bundle") + assert.Nil(t, bundles) + }) +} diff --git a/internal/resolution/variablesources/bundle_uniqueness.go b/internal/resolution/variablesources/bundle_uniqueness.go new file mode 100644 index 000000000..be6d25985 --- /dev/null +++ b/internal/resolution/variablesources/bundle_uniqueness.go @@ -0,0 +1,46 @@ +package variablesources + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/operator-framework/deppy/pkg/deppy" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" +) + +func BundleUniquenessVariables(bundleVariables []*olmvariables.BundleVariable) ([]*olmvariables.BundleUniquenessVariable, error) { + result := []*olmvariables.BundleUniquenessVariable{} + + bundleIDs := sets.Set[deppy.Identifier]{} + packageOrder := []string{} + bundleOrder := map[string][]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 := bundleOrder[packageName]; !ok { + packageOrder = append(packageOrder, packageName) + } + + if !bundleIDs.Has(id) { + bundleIDs.Insert(id) + bundleOrder[packageName] = append(bundleOrder[packageName], id) + } + } + } + + // create global constraint variables + for _, packageName := range packageOrder { + varID := deppy.IdentifierFromString(fmt.Sprintf("%s package uniqueness", packageName)) + result = append(result, olmvariables.NewBundleUniquenessVariable(varID, bundleOrder[packageName]...)) + } + + return result, nil +} diff --git a/internal/resolution/variablesources/bundle_uniqueness_test.go b/internal/resolution/variablesources/bundle_uniqueness_test.go new file mode 100644 index 000000000..395c12df5 --- /dev/null +++ b/internal/resolution/variablesources/bundle_uniqueness_test.go @@ -0,0 +1,265 @@ +package variablesources_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" + "github.com/operator-framework/operator-controller/internal/resolution/variablesources" +) + +func TestBundleUniquenessVariables(t *testing.T) { + channel := catalogmetadata.Channel{Channel: declcfg.Channel{Name: "stable"}} + 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}, + }, + } + + t.Run("convert bundle variables into create global uniqueness constraint variables", func(t *testing.T) { + bundleVariables := []*olmvariables.BundleVariable{ + 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 := variablesources.BundleUniquenessVariables(bundleVariables) + require.NoError(t, err) + + expectedIDs := []string{ + "test-package package uniqueness", + "some-package package uniqueness", + "some-other-package package uniqueness", + "another-package package uniqueness", + "bar-package package uniqueness", + "test-package-2 package uniqueness", + } + actualIDs := collectVariableIDs(variables) + assert.EqualValues(t, expectedIDs, actualIDs) + }) +} 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 39084b36c..000000000 --- a/internal/resolution/variablesources/crd_constraints.go +++ /dev/null @@ -1,79 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "k8s.io/apimachinery/pkg/util/sets" - - "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 - - bundleIDs := sets.Set[deppy.Identifier]{} - packageOrder := []string{} - bundleOrder := map[string][]deppy.Identifier{} - 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 := bundleOrder[packageName]; !ok { - packageOrder = append(packageOrder, packageName) - } - - if !bundleIDs.Has(id) { - bundleIDs.Insert(id) - bundleOrder[packageName] = append(bundleOrder[packageName], id) - } - } - } - } - - // create global constraint variables - for _, packageName := range packageOrder { - varID := deppy.IdentifierFromString(fmt.Sprintf("%s package uniqueness", packageName)) - variables = append(variables, olmvariables.NewBundleUniquenessVariable(varID, bundleOrder[packageName]...)) - } - - 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 b0e054512..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, Equal([]string{ - "test-package package uniqueness", - "some-package package uniqueness", - "some-other-package package uniqueness", - "another-package package uniqueness", - "bar-package package uniqueness", - "test-package-2 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 index 071afaa73..7858671a7 100644 --- a/internal/resolution/variablesources/installed_package.go +++ b/internal/resolution/variablesources/installed_package.go @@ -1,76 +1,74 @@ 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" + "k8s.io/apimachinery/pkg/util/sets" + + rukpakv1alpha1 "github.com/operator-framework/rukpak/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" - "github.com/operator-framework/operator-controller/internal/resolution/variables" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" "github.com/operator-framework/operator-controller/pkg/features" ) -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) { - successors := legacySemanticsSuccessors +// 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) { + var successors successorsFunc = legacySemanticsSuccessors if features.OperatorControllerFeatureGate.Enabled(features.ForceSemverUpgradeConstraints) { successors = semverSuccessors } - return &InstalledPackageVariableSource{ - catalogClient: catalogClient, - bundleImage: bundleImage, - successors: successors, - }, nil + 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 diff --git a/internal/resolution/variablesources/installed_package_test.go b/internal/resolution/variablesources/installed_package_test.go index 23b785126..f8d7ab77d 100644 --- a/internal/resolution/variablesources/installed_package_test.go +++ b/internal/resolution/variablesources/installed_package_test.go @@ -1,25 +1,26 @@ package variablesources_test import ( - "context" "encoding/json" + "fmt" "testing" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" featuregatetesting "k8s.io/component-base/featuregate/testing" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/catalogmetadata" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" "github.com/operator-framework/operator-controller/internal/resolution/variablesources" "github.com/operator-framework/operator-controller/pkg/features" - testutil "github.com/operator-framework/operator-controller/test/util" ) -func TestInstalledPackageVariableSource(t *testing.T) { +func TestInstalledPackageVariables(t *testing.T) { someOtherPackageChannel := catalogmetadata.Channel{Channel: declcfg.Channel{ Name: "stable", Package: "some-other-package", @@ -82,7 +83,7 @@ func TestInstalledPackageVariableSource(t *testing.T) { }, }, }} - bundleList := []*catalogmetadata.Bundle{ + allBundles := []*catalogmetadata.Bundle{ {Bundle: declcfg.Bundle{ Name: "test-package.v0.0.1", Package: "test-package", @@ -202,21 +203,41 @@ func TestInstalledPackageVariableSource(t *testing.T) { }, } - fakeCatalogClient := testutil.NewFakeCatalogClient(bundleList) + fakeBundleDeployments := func(bundleImages ...string) []rukpakv1alpha1.BundleDeployment { + bundleDeployments := []rukpakv1alpha1.BundleDeployment{} + for idx, bundleImage := range bundleImages { + bd := rukpakv1alpha1.BundleDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("bd-%d", idx), + }, + Spec: rukpakv1alpha1.BundleDeploymentSpec{ + Template: &rukpakv1alpha1.BundleTemplate{ + Spec: rukpakv1alpha1.BundleSpec{ + Source: rukpakv1alpha1.BundleSource{ + Image: &rukpakv1alpha1.ImageSource{ + Ref: bundleImage, + }, + }, + }, + }, + }, + } + bundleDeployments = append(bundleDeployments, bd) + } + + return bundleDeployments + } t.Run("with ForceSemverUpgradeConstraints feature gate enabled", func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, true)() t.Run("with non-zero major version", func(t *testing.T) { const bundleImage = "registry.io/repo/test-package@v2.0.0" - ipvs, err := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, bundleImage) + installedPackages, err := variablesources.InstalledPackageVariables(allBundles, fakeBundleDeployments(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) + require.Len(t, installedPackages, 1) + packageVariable := installedPackages[0] assert.Equal(t, deppy.IdentifierFromString("installed package test-package"), packageVariable.Identifier()) // ensure bundles are in version order (high to low) @@ -230,14 +251,11 @@ func TestInstalledPackageVariableSource(t *testing.T) { t.Run("with zero major version", func(t *testing.T) { t.Run("with zero minor version", func(t *testing.T) { const bundleImage = "registry.io/repo/test-package@v0.0.1" - ipvs, err := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, bundleImage) + installedPackages, err := variablesources.InstalledPackageVariables(allBundles, fakeBundleDeployments(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) + require.Len(t, installedPackages, 1) + packageVariable := installedPackages[0] assert.Equal(t, deppy.IdentifierFromString("installed package test-package"), packageVariable.Identifier()) // No upgrades are allowed in major version zero when minor version is also zero @@ -248,14 +266,11 @@ func TestInstalledPackageVariableSource(t *testing.T) { t.Run("with non-zero minor version", func(t *testing.T) { const bundleImage = "registry.io/repo/test-package@v0.1.0" - ipvs, err := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, bundleImage) + installedPackages, err := variablesources.InstalledPackageVariables(allBundles, fakeBundleDeployments(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) + require.Len(t, installedPackages, 1) + packageVariable := installedPackages[0] assert.Equal(t, deppy.IdentifierFromString("installed package test-package"), packageVariable.Identifier()) // Patch version upgrades are allowed, but not minor upgrades @@ -271,14 +286,11 @@ func TestInstalledPackageVariableSource(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.ForceSemverUpgradeConstraints, false)() const bundleImage = "registry.io/repo/test-package@v2.0.0" - ipvs, err := variablesources.NewInstalledPackageVariableSource(&fakeCatalogClient, bundleImage) + installedPackages, err := variablesources.InstalledPackageVariables(allBundles, fakeBundleDeployments(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) + require.Len(t, installedPackages, 1) + packageVariable := installedPackages[0] assert.Equal(t, deppy.IdentifierFromString("installed package test-package"), packageVariable.Identifier()) // ensure bundles are in version order (high to low) @@ -287,4 +299,11 @@ func TestInstalledPackageVariableSource(t *testing.T) { assert.Equal(t, "test-package.v2.1.0", packageVariable.Bundles()[0].Name) assert.Equal(t, "test-package.v2.0.0", packageVariable.Bundles()[1].Name) }) + + t.Run("installed bundle not found", func(t *testing.T) { + const bundleImage = "registry.io/repo/test-package@v9.0.0" + installedPackages, err := variablesources.InstalledPackageVariables(allBundles, fakeBundleDeployments(bundleImage)) + assert.Nil(t, installedPackages) + assert.ErrorContains(t, err, `bundleImage "registry.io/repo/test-package@v9.0.0" not found`) + }) } diff --git a/internal/resolution/variablesources/olm_variable_source.go b/internal/resolution/variablesources/olm_variable_source.go new file mode 100644 index 000000000..fe9a84844 --- /dev/null +++ b/internal/resolution/variablesources/olm_variable_source.go @@ -0,0 +1,81 @@ +package variablesources + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/deppy/pkg/deppy" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + + operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" +) + +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 + } + + bundleDeploymentList := rukpakv1alpha1.BundleDeploymentList{} + if err := o.client.List(ctx, &bundleDeploymentList); 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, bundleDeploymentList.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 +} diff --git a/internal/resolution/variablesources/olm_variable_source_test.go b/internal/resolution/variablesources/olm_variable_source_test.go new file mode 100644 index 000000000..807925dd4 --- /dev/null +++ b/internal/resolution/variablesources/olm_variable_source_test.go @@ -0,0 +1,114 @@ +package variablesources_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/rand" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + + operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/catalogmetadata" + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" + "github.com/operator-framework/operator-controller/internal/resolution/variablesources" + testutil "github.com/operator-framework/operator-controller/test/util" +) + +func TestOLMVariableSource(t *testing.T) { + sch := runtime.NewScheme() + utilruntime.Must(operatorsv1alpha1.AddToScheme(sch)) + utilruntime.Must(rukpakv1alpha1.AddToScheme(sch)) + + stableChannel := catalogmetadata.Channel{Channel: declcfg.Channel{ + Name: "stable", + Entries: []declcfg.ChannelEntry{ + { + Name: "packageA.v2.0.0", + }, + }, + }} + testBundleList := []*catalogmetadata.Bundle{ + { + Bundle: declcfg.Bundle{ + Name: "packageA.v2.0.0", + Package: "packageA", + Image: "foo.io/packageA/packageA:v2.0.0", + Properties: []property.Property{ + {Type: property.TypePackage, Value: json.RawMessage(`{"packageName":"packageA","version":"2.0.0"}`)}, + }, + }, + CatalogName: "fake-catalog", + InChannels: []*catalogmetadata.Channel{&stableChannel}, + }, + } + + pkgName := "packageA" + opName := fmt.Sprintf("operator-test-%s", rand.String(8)) + operator := &operatorsv1alpha1.Operator{ + ObjectMeta: metav1.ObjectMeta{Name: opName}, + Spec: operatorsv1alpha1.OperatorSpec{ + PackageName: pkgName, + Channel: "stable", + Version: "2.0.0", + }, + } + + bd := &rukpakv1alpha1.BundleDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: opName, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: operatorsv1alpha1.GroupVersion.String(), + Kind: "Operator", + Name: operator.Name, + UID: operator.UID, + Controller: pointer.Bool(true), + BlockOwnerDeletion: pointer.Bool(true), + }, + }, + }, + Spec: rukpakv1alpha1.BundleDeploymentSpec{ + ProvisionerClassName: "core-rukpak-io-plain", + Template: &rukpakv1alpha1.BundleTemplate{ + Spec: rukpakv1alpha1.BundleSpec{ + ProvisionerClassName: "core-rukpak-io-registry", + Source: rukpakv1alpha1.BundleSource{ + Type: rukpakv1alpha1.SourceTypeImage, + Image: &rukpakv1alpha1.ImageSource{ + Ref: "foo.io/packageA/packageA:v2.0.0", + }, + }, + }, + }, + }, + } + fakeClient := fake.NewClientBuilder().WithScheme(sch).WithObjects(operator, bd).Build() + fakeCatalogClient := testutil.NewFakeCatalogClient(testBundleList) + + vs := variablesources.NewOLMVariableSource(fakeClient, &fakeCatalogClient) + + vars, err := vs.GetVariables(context.Background()) + require.NoError(t, err) + + require.Len(t, vars, 4) + assert.Equal(t, "required package packageA", vars[0].Identifier().String()) + assert.IsType(t, &olmvariables.RequiredPackageVariable{}, vars[0]) + assert.Equal(t, "installed package packageA", vars[1].Identifier().String()) + assert.IsType(t, &olmvariables.InstalledPackageVariable{}, vars[1]) + assert.Equal(t, "fake-catalog-packageA-packageA.v2.0.0", vars[2].Identifier().String()) + assert.IsType(t, &olmvariables.BundleVariable{}, vars[2]) + assert.Equal(t, "packageA package uniqueness", vars[3].Identifier().String()) + assert.IsType(t, &olmvariables.BundleUniquenessVariable{}, vars[3]) +} 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 index e45df4e17..2b9a2e751 100644 --- a/internal/resolution/variablesources/required_package.go +++ b/internal/resolution/variablesources/required_package.go @@ -1,13 +1,10 @@ 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" @@ -15,90 +12,42 @@ import ( 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 +// 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), } -} - -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") + if channelName != "" { + predicates = append(predicates, catalogfilter.InChannel(channelName)) } - 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 + if versionRange != "" { + vr, err := mmsemver.NewConstraint(versionRange) + if err != nil { + return nil, fmt.Errorf("invalid version range '%s': %w", versionRange, 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 + predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) } - resultSet = catalogfilter.Filter(resultSet, catalogfilter.And(r.predicates...)) + resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...)) if len(resultSet) == 0 { - return nil, r.notFoundError() + if versionRange != "" && channelName != "" { + return nil, fmt.Errorf("no package '%s' matching version '%s' found in channel '%s'", packageName, versionRange, channelName) + } + if versionRange != "" { + return nil, fmt.Errorf("no package '%s' matching version '%s' found", packageName, versionRange) + } + if channelName != "" { + return nil, fmt.Errorf("no package '%s' found in channel '%s'", packageName, channelName) + } + return nil, fmt.Errorf("no package '%s' found", packageName) } 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 { - if r.versionRange != "" && r.channelName != "" { - return fmt.Errorf("no package '%s' matching version '%s' found in channel '%s'", r.packageName, r.versionRange, r.channelName) - } - if r.versionRange != "" { - return fmt.Errorf("no package '%s' matching version '%s' found", r.packageName, r.versionRange) - } - if r.channelName != "" { - return fmt.Errorf("no package '%s' found in channel '%s'", r.packageName, r.channelName) - } - return fmt.Errorf("no package '%s' found", r.packageName) + return olmvariables.NewRequiredPackageVariable(packageName, resultSet), nil } diff --git a/internal/resolution/variablesources/required_package_test.go b/internal/resolution/variablesources/required_package_test.go index a6f0bd32a..12d2f0c35 100644 --- a/internal/resolution/variablesources/required_package_test.go +++ b/internal/resolution/variablesources/required_package_test.go @@ -1,137 +1,130 @@ package variablesources_test import ( - "context" "encoding/json" - "errors" - "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - . "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()) +func TestRequiredPackageVariable(t *testing.T) { + stableChannel := catalogmetadata.Channel{Channel: declcfg.Channel{ + Name: "stable", + }} + betaChannel := catalogmetadata.Channel{Channel: declcfg.Channel{ + Name: "beta", + }} + allBundles := []*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{&stableChannel}, + }, + {Bundle: declcfg.Bundle{ + Name: "test-package.v3.0.0", + Package: "test-package", + Properties: []property.Property{ + {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "3.0.0"}`)}, + }}, + InChannels: []*catalogmetadata.Channel{&stableChannel, &betaChannel}, + }, + {Bundle: declcfg.Bundle{ + Name: "test-package.v2.0.0", + Package: "test-package", + Properties: []property.Property{ + {Type: property.TypePackage, Value: json.RawMessage(`{"packageName": "test-package", "version": "2.0.0"}`)}, + }}, + InChannels: []*catalogmetadata.Channel{&stableChannel}, + }, + // 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{&stableChannel}, + }, + {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{&stableChannel}, + }, + } + + t.Run("package name only", func(t *testing.T) { + reqPackageVar, err := variablesources.RequiredPackageVariable(allBundles, "test-package", "", "") + require.NoError(t, err) + + assert.Equal(t, deppy.IdentifierFromString("required package test-package"), reqPackageVar.Identifier()) + + bundles := reqPackageVar.Bundles() + require.Len(t, bundles, 3) + // ensure bundles are in version order (high to low) + assert.Equal(t, "test-package.v3.0.0", bundles[0].Name) + assert.Equal(t, "test-package.v2.0.0", bundles[1].Name) + assert.Equal(t, "test-package.v1.0.0", bundles[2].Name) }) - 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)) + t.Run("package name and channel", func(t *testing.T) { + reqPackageVar, err := variablesources.RequiredPackageVariable(allBundles, "test-package", "beta", "") + require.NoError(t, err) + + assert.Equal(t, deppy.IdentifierFromString("required package test-package"), reqPackageVar.Identifier()) + + bundles := reqPackageVar.Bundles() + require.Len(t, bundles, 1) // 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")) + assert.Equal(t, "test-package.v3.0.0", bundles[0].Name) }) - 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()) + t.Run("package name and version range", func(t *testing.T) { + reqPackageVar, err := variablesources.RequiredPackageVariable(allBundles, "test-package", "", ">=1.0.0 !=2.0.0 <3.0.0") + require.NoError(t, err) - 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)))) + assert.Equal(t, deppy.IdentifierFromString("required package test-package"), reqPackageVar.Identifier()) - Expect(reqPackageVar.Bundles()).To(HaveLen(1)) + bundles := reqPackageVar.Bundles() + require.Len(t, bundles, 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")) + assert.Equal(t, "test-package.v1.0.0", bundles[0].Name) }) - It("should fail with bad semver range", func() { - _, err := variablesources.NewRequiredPackageVariableSource(&fakeCatalogClient, packageName, variablesources.InVersionRange("not a valid semver")) - Expect(err).To(HaveOccurred()) + t.Run("package name and invalid version range", func(t *testing.T) { + reqPackageVar, err := variablesources.RequiredPackageVariable(allBundles, "test-package", "", "not a valid semver") + assert.Nil(t, reqPackageVar) + assert.Error(t, err) }) - 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("no package 'test-package' found")) - }) + t.Run("package not found", func(t *testing.T) { + reqPackageVar, err := variablesources.RequiredPackageVariable([]*catalogmetadata.Bundle{}, "test-package", "", "") + assert.Nil(t, reqPackageVar) + assert.ErrorContains(t, err, "no package 'test-package' found") + + reqPackageVar, err = variablesources.RequiredPackageVariable([]*catalogmetadata.Bundle{}, "test-package", "stable", "") + assert.Nil(t, reqPackageVar) + assert.ErrorContains(t, err, "no package 'test-package' found in channel 'stable'") + + reqPackageVar, err = variablesources.RequiredPackageVariable([]*catalogmetadata.Bundle{}, "test-package", "", "1.0.0") + assert.Nil(t, reqPackageVar) + assert.ErrorContains(t, err, "no package 'test-package' matching version '1.0.0' 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)) + reqPackageVar, err = variablesources.RequiredPackageVariable([]*catalogmetadata.Bundle{}, "test-package", "stable", "1.0.0") + assert.Nil(t, reqPackageVar) + assert.ErrorContains(t, err, "no package 'test-package' matching version '1.0.0' found in channel 'stable'") }) -}) +} diff --git a/internal/resolution/variablesources/variable_utils_test.go b/internal/resolution/variablesources/variable_utils_test.go new file mode 100644 index 000000000..1381e855d --- /dev/null +++ b/internal/resolution/variablesources/variable_utils_test.go @@ -0,0 +1,24 @@ +package variablesources_test + +import ( + "github.com/operator-framework/deppy/pkg/deppy" + + olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" +) + +func findVariableWithName(vars []*olmvariables.BundleVariable, name string) *olmvariables.BundleVariable { + for i := 0; i < len(vars); i++ { + if vars[i].Bundle().Name == name { + return vars[i] + } + } + return nil +} + +func collectVariableIDs[T deppy.Variable](vars []T) []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/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") -}