From 31082f42fa9fbe61a4c9155683d0956e2425b774 Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Fri, 11 Aug 2023 09:49:35 -0400 Subject: [PATCH] wip Signed-off-by: Joe Lanford --- api/v1alpha1/operator_types.go | 29 +- cmd/manager/main.go | 27 +- ...rators.operatorframework.io_operators.yaml | 17 +- config/rbac/role.yaml | 14 + go.mod | 1 + go.sum | 2 + internal/controllers/operator_controller.go | 71 ++-- internal/controllers/validators/validators.go | 2 +- internal/controllers/variable_source.go | 42 -- internal/resolution/entities/bundle_entity.go | 285 -------------- .../resolution/entities/bundle_entity_test.go | 358 ------------------ .../entitysources/catalogdsource.go | 140 ------- .../resolution/util/predicates/predicates.go | 80 ---- .../util/predicates/predicates_test.go | 126 ------ internal/resolution/util/sort/sort.go | 80 ---- internal/resolution/util/sort/sort_test.go | 152 -------- internal/resolution/v2/store/catalog.go | 90 +++++ internal/resolution/v2/store/store.go | 177 +++++++++ internal/resolution/v2/variables/bundle.go | 29 ++ .../resolution/v2/variablesources/operator.go | 234 ++++++++++++ internal/resolution/variables/bundle.go | 58 --- internal/resolution/variables/bundle_test.go | 70 ---- .../resolution/variables/installed_package.go | 34 -- .../variables/installed_package_test.go | 50 --- .../resolution/variables/required_package.go | 34 -- .../variables/required_package_test.go | 55 --- .../resolution/variables/variables_test.go | 13 - .../variablesources/bundle_deployment.go | 54 --- .../variablesources/bundle_deployment_test.go | 109 ------ .../bundles_and_dependencies.go | 137 ------- .../bundles_and_dependencies_test.go | 248 ------------ .../resolution/variablesources/composite.go | 62 --- .../variablesources/composite_test.go | 175 --------- .../variablesources/crd_constraints.go | 101 ----- .../variablesources/crd_constraints_test.go | 283 -------------- .../variablesources/installed_package.go | 84 ---- .../variablesources/installed_package_test.go | 76 ---- .../resolution/variablesources/operator.go | 52 --- .../variablesources/operator_test.go | 129 ------- .../variablesources/required_package.go | 103 ----- .../variablesources/required_package_test.go | 119 ------ .../variablesources/variablesources_test.go | 13 - 42 files changed, 658 insertions(+), 3357 deletions(-) delete mode 100644 internal/controllers/variable_source.go delete mode 100644 internal/resolution/entities/bundle_entity.go delete mode 100644 internal/resolution/entities/bundle_entity_test.go delete mode 100644 internal/resolution/entitysources/catalogdsource.go delete mode 100644 internal/resolution/util/predicates/predicates.go delete mode 100644 internal/resolution/util/predicates/predicates_test.go delete mode 100644 internal/resolution/util/sort/sort.go delete mode 100644 internal/resolution/util/sort/sort_test.go create mode 100644 internal/resolution/v2/store/catalog.go create mode 100644 internal/resolution/v2/store/store.go create mode 100644 internal/resolution/v2/variables/bundle.go create mode 100644 internal/resolution/v2/variablesources/operator.go delete mode 100644 internal/resolution/variables/bundle.go delete mode 100644 internal/resolution/variables/bundle_test.go delete mode 100644 internal/resolution/variables/installed_package.go delete mode 100644 internal/resolution/variables/installed_package_test.go delete mode 100644 internal/resolution/variables/required_package.go delete mode 100644 internal/resolution/variables/required_package_test.go delete mode 100644 internal/resolution/variables/variables_test.go delete mode 100644 internal/resolution/variablesources/bundle_deployment.go delete mode 100644 internal/resolution/variablesources/bundle_deployment_test.go delete mode 100644 internal/resolution/variablesources/bundles_and_dependencies.go delete mode 100644 internal/resolution/variablesources/bundles_and_dependencies_test.go delete mode 100644 internal/resolution/variablesources/composite.go delete mode 100644 internal/resolution/variablesources/composite_test.go delete mode 100644 internal/resolution/variablesources/crd_constraints.go delete mode 100644 internal/resolution/variablesources/crd_constraints_test.go delete mode 100644 internal/resolution/variablesources/installed_package.go delete mode 100644 internal/resolution/variablesources/installed_package_test.go delete mode 100644 internal/resolution/variablesources/operator.go delete mode 100644 internal/resolution/variablesources/operator_test.go delete mode 100644 internal/resolution/variablesources/required_package.go delete mode 100644 internal/resolution/variablesources/required_package_test.go delete mode 100644 internal/resolution/variablesources/variablesources_test.go diff --git a/api/v1alpha1/operator_types.go b/api/v1alpha1/operator_types.go index ae3eb3985..79ff56b00 100644 --- a/api/v1alpha1/operator_types.go +++ b/api/v1alpha1/operator_types.go @@ -29,8 +29,10 @@ type OperatorSpec struct { PackageName string `json:"packageName"` //+kubebuilder:validation:MaxLength:=64 - //+kubebuilder:validation:Pattern=^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ + // TODO: add this pattern back in with masterminds range syntax support + // => +kubebuilder:validation:Pattern=^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ //+kubebuilder:Optional + // Version is an optional semver constraint on the package version. If not specified, the latest version available of the package will be installed. // If specified, the specific version of the package will be installed so long as it is available in any of the content sources available. // Examples: 1.2.3, 1.0.0-alpha, 1.0.0-rc.1 @@ -40,8 +42,27 @@ type OperatorSpec struct { //+kubebuilder:validation:MaxLength:=48 //+kubebuilder:validation:Pattern:=^[a-z0-9]+([\.-][a-z0-9]+)*$ + // Channel constraint definition Channel string `json:"channel,omitempty"` + + //+kubebuilder:validation:enum:=Enforce;Ignore + //+kubebuilder:default:=Enforce + //+kubebuilder:validation:Optional + + // UpgradeEdgeConstraintPolicy defines the policy for how to handle upgrades. If set to Enforce, the operator will only upgrade + // if the new version satisfies the edge constraint set by the extension author. If set to Ignore, the operator can be upgraded + // or downgraded, regardless of edge constraint. + UpgradeEdgeConstraintPolicy string `json:"upgradeEdgeConstraintPolicy,omitempty"` + + //+kubebuilder:validation:enum:=Enforce;Ignore + //+kubebuilder:default:=Enforce + //+kubebuilder:validation:Optional + + // ClusterConstraintPolicy defines the policy for how to handle cluster constraints defined by the bundle. If set to Enforce, + // resolved bundles must satisfy all cluster constraints. If set to Ignore, resolved bundles can be installed regardless of + // their cluster constraints. + ClusterConstraintPolicy string `json:"clusterConstraintPolicy,omitempty"` } const ( @@ -57,6 +78,12 @@ const ( ReasonResolutionFailed = "ResolutionFailed" ReasonResolutionUnknown = "ResolutionUnknown" ReasonSuccess = "Success" + + UpgradeEdgeConstraintPolicyEnforce = "Enforce" + UpgradeEdgeConstraintPolicyIgnore = "Ignore" + + ClusterConstraintPolicyEnforce = "Enforce" + ClusterConstraintPolicyIgnore = "Ignore" ) func init() { diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 769442ad3..70071eb4f 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -20,23 +20,24 @@ import ( "flag" "os" + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + "github.com/operator-framework/deppy/pkg/deppy/solver" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" "github.com/spf13/pflag" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/discovery" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" - "github.com/operator-framework/deppy/pkg/deppy/solver" - rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" - operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/controllers" - "github.com/operator-framework/operator-controller/internal/resolution/entitysources" + "github.com/operator-framework/operator-controller/internal/resolution/v2/store" + "github.com/operator-framework/operator-controller/internal/resolution/v2/variablesources" "github.com/operator-framework/operator-controller/pkg/features" ) @@ -99,12 +100,24 @@ func main() { os.Exit(1) } + discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) + if err != nil { + setupLog.Error(err, "unable to create discovery client") + os.Exit(1) + } + if err = (&controllers.OperatorReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Resolver: solver.NewDeppySolver( - entitysources.NewCatalogdEntitySource(mgr.GetClient()), - controllers.NewVariableSource(mgr.GetClient()), + nil, + variablesources.Operator{ + Client: mgr.GetClient(), + Store: store.CatalogMetadataStore{ + Client: mgr.GetClient(), + ServerVersionInterface: discoveryClient, + }, + }, ), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Operator") diff --git a/config/crd/bases/operators.operatorframework.io_operators.yaml b/config/crd/bases/operators.operatorframework.io_operators.yaml index 95203347b..2fbf9ad23 100644 --- a/config/crd/bases/operators.operatorframework.io_operators.yaml +++ b/config/crd/bases/operators.operatorframework.io_operators.yaml @@ -39,10 +39,26 @@ spec: maxLength: 48 pattern: ^[a-z0-9]+([\.-][a-z0-9]+)*$ type: string + clusterConstraintPolicy: + default: Enforce + description: ClusterConstraintPolicy defines the policy for how to + handle cluster constraints defined by the bundle. If set to Enforce, + resolved bundles must satisfy all cluster constraints. If set to + Ignore, resolved bundles can be installed regardless of their cluster + constraints. + type: string packageName: maxLength: 48 pattern: ^[a-z0-9]+(-[a-z0-9]+)*$ type: string + upgradeEdgeConstraintPolicy: + default: Enforce + description: UpgradeEdgeConstraintPolicy defines the policy for how + to handle upgrades. If set to Enforce, the operator will only upgrade + if the new version satisfies the edge constraint set by the extension + author. If set to Ignore, the operator can be upgraded or downgraded, + regardless of edge constraint. + type: string version: description: "Version is an optional semver constraint on the package version. If not specified, the latest version available of the package @@ -51,7 +67,6 @@ spec: sources available. Examples: 1.2.3, 1.0.0-alpha, 1.0.0-rc.1 \n For more information on semver, please see https://semver.org/" maxLength: 64 - pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ type: string required: - packageName diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 49d3c2458..71f606b11 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,13 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - nodes + verbs: + - list + - watch - apiGroups: - catalogd.operatorframework.io resources: @@ -11,6 +18,13 @@ rules: verbs: - list - watch +- apiGroups: + - catalogd.operatorframework.io + resources: + - catalogmetadata + verbs: + - list + - watch - apiGroups: - catalogd.operatorframework.io resources: diff --git a/go.mod b/go.mod index 1dba611ac..960cabf74 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/operator-framework/operator-controller go 1.20 require ( + github.com/Masterminds/semver/v3 v3.2.0 github.com/blang/semver/v4 v4.0.0 github.com/go-logr/logr v1.2.4 github.com/onsi/ginkgo/v2 v2.11.0 diff --git a/go.sum b/go.sum index 4c1fc6d2d..9d723db9c 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= diff --git a/internal/controllers/operator_controller.go b/internal/controllers/operator_controller.go index a842b8d20..1767c5892 100644 --- a/internal/controllers/operator_controller.go +++ b/internal/controllers/operator_controller.go @@ -18,10 +18,13 @@ package controllers import ( "context" + "encoding/json" + "errors" "fmt" "github.com/go-logr/logr" catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + "github.com/operator-framework/deppy/pkg/deppy" "github.com/operator-framework/deppy/pkg/deppy/solver" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" "k8s.io/apimachinery/pkg/api/equality" @@ -41,8 +44,8 @@ import ( operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/controllers/validators" - "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" + "github.com/operator-framework/operator-controller/internal/resolution/v2/store" + "github.com/operator-framework/operator-controller/internal/resolution/v2/variables" ) // OperatorReconciler reconciles a Operator object @@ -60,8 +63,11 @@ type OperatorReconciler struct { //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=bundlemetadata,verbs=list;watch //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=packages,verbs=list;watch +//+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=catalogmetadata,verbs=list;watch //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=catalogs,verbs=list;watch +//+kubebuilder:rbac:groups="",resources=nodes,verbs=list;watch + func (r *OperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx).WithName("operator-controller") l.V(1).Info("starting") @@ -136,10 +142,9 @@ func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha return ctrl.Result{}, err } - // lookup the bundle entity in the solution that corresponds to the - // Operator's desired package name. - bundleEntity, err := r.getBundleEntityFromSolution(solution, op.Spec.PackageName) - if err != nil { + unsat := deppy.NotSatisfiable{} + if errors.As(solution.Error(), &unsat); len(unsat) > 0 { + err := unsat op.Status.InstalledBundleResource = "" setInstalledStatusConditionUnknown(&op.Status.Conditions, "installation has not been attempted as resolution failed", op.GetGeneration()) op.Status.ResolvedBundleResource = "" @@ -147,26 +152,40 @@ func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha return ctrl.Result{}, err } - // Get the bundle image reference for the bundle - bundleImage, err := bundleEntity.BundlePath() + // lookup the bundle entity in the solution that corresponds to the + // Operator's desired package name. + bundleEntity, err := r.getBundleFromSolution(solution, op.Spec.PackageName) if err != nil { op.Status.InstalledBundleResource = "" setInstalledStatusConditionUnknown(&op.Status.Conditions, "installation has not been attempted as resolution failed", op.GetGeneration()) - op.Status.ResolvedBundleResource = "" setResolvedStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration()) return ctrl.Result{}, err } + // Get the bundle image reference for the bundle + bundleImage := bundleEntity.Image + // Now we can set the Resolved Condition, and the resolvedBundleSource field to the bundleImage value. op.Status.ResolvedBundleResource = bundleImage setResolvedStatusConditionSuccess(&op.Status.Conditions, fmt.Sprintf("resolved to %q", bundleImage), op.GetGeneration()) - mediaType, err := bundleEntity.MediaType() - if err != nil { - setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration()) - return ctrl.Result{}, err + mediaType := store.MediaTypeRegistryV1 + for _, p := range bundleEntity.Properties { + if p.Type == store.PropertyBundleMediaType { + var v string + if err := json.Unmarshal(p.Value, &v); err == nil { + op.Status.InstalledBundleResource = "" + setInstalledStatusConditionUnknown(&op.Status.Conditions, "installation has not been attempted as resolution failed", op.GetGeneration()) + op.Status.ResolvedBundleResource = "" + err = fmt.Errorf("failed to parse bundle mediatype %q: %w", string(p.Value), err) + setResolvedStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration()) + return ctrl.Result{}, err + } + mediaType = v + } } + bundleProvisioner, err := mapBundleMediaTypeToBundleProvisioner(mediaType) if err != nil { setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration()) @@ -174,7 +193,7 @@ func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha } // Ensure a BundleDeployment exists with its bundle source from the bundle // image we just looked up in the solution. - dep := r.generateExpectedBundleDeployment(*op, bundleImage, bundleProvisioner) + dep := r.generateExpectedBundleDeployment(*op, bundleEntity, bundleProvisioner) if err := r.ensureBundleDeployment(ctx, dep); err != nil { // originally Reason: operatorsv1alpha1.ReasonInstallationFailed op.Status.InstalledBundleResource = "" @@ -244,23 +263,20 @@ func mapBDStatusToInstalledCondition(existingTypedBundleDeployment *rukpakv1alph } } -func (r *OperatorReconciler) getBundleEntityFromSolution(solution *solver.Solution, packageName string) (*entities.BundleEntity, error) { +func (r *OperatorReconciler) getBundleFromSolution(solution *solver.Solution, packageName string) (*store.Bundle, error) { for _, variable := range solution.SelectedVariables() { switch v := variable.(type) { - case *olmvariables.BundleVariable: - entityPkgName, err := v.BundleEntity().PackageName() - if err != nil { - return nil, err - } + case *variables.Bundle: + entityPkgName := v.Bundle.Package if packageName == entityPkgName { - return v.BundleEntity(), nil + return v.Bundle, nil } } } return nil, fmt.Errorf("entity for package %q not found in solution", packageName) } -func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundlePath string, bundleProvisioner string) *unstructured.Unstructured { +func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundle *store.Bundle, bundleProvisioner string) *unstructured.Unstructured { // We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver. // If you use a typed object, any default values from that struct get serialized into the JSON patch, which could // cause unrelated fields to be patched back to the default value even though that isn't the intention. Using an @@ -272,6 +288,11 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha "kind": rukpakv1alpha1.BundleDeploymentKind, "metadata": map[string]interface{}{ "name": o.GetName(), + "annotations": map[string]string{ + "operators.operatorframework.io/package": bundle.Package, + "operators.operatorframework.io/name": bundle.Name, + "operators.operatorframework.io/version": bundle.Version.String(), + }, }, "spec": map[string]interface{}{ // TODO: Don't assume plain provisioner @@ -283,7 +304,7 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha // TODO: Don't assume image type "type": string(rukpakv1alpha1.SourceTypeImage), "image": map[string]interface{}{ - "ref": bundlePath, + "ref": bundle.Image, }, }, }, @@ -358,13 +379,13 @@ func (r *OperatorReconciler) existingBundleDeploymentUnstructured(ctx context.Co // rukpak bundle provisioner class name that is capable of unpacking the bundle type func mapBundleMediaTypeToBundleProvisioner(mediaType string) (string, error) { switch mediaType { - case entities.MediaTypePlain: + case store.MediaTypePlainV0: return "core-rukpak-io-plain", nil // To ensure compatibility with bundles created with OLMv0 where the // olm.bundle.mediatype property doesn't exist, we assume that if the // property is empty (i.e doesn't exist) that the bundle is one created // with OLMv0 and therefore should use the registry provisioner - case entities.MediaTypeRegistry, "": + case store.MediaTypeRegistryV1, "": return "core-rukpak-io-registry", nil default: return "", fmt.Errorf("unknown bundle mediatype: %s", mediaType) diff --git a/internal/controllers/validators/validators.go b/internal/controllers/validators/validators.go index 93f777fe4..d469269d0 100644 --- a/internal/controllers/validators/validators.go +++ b/internal/controllers/validators/validators.go @@ -27,7 +27,7 @@ func validateSemver(operator *operatorsv1alpha1.Operator) error { // ValidateOperatorSpec validates the operator spec, e.g. ensuring that .spec.version, if provided, is a valid SemVer func ValidateOperatorSpec(operator *operatorsv1alpha1.Operator) error { validators := []operatorCRValidatorFunc{ - validateSemver, + //∂validateSemver, } // TODO: currently we only have a single validator, but more will likely be added in the future diff --git a/internal/controllers/variable_source.go b/internal/controllers/variable_source.go deleted file mode 100644 index 21fa0b219..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) variablesources.NestedVariableSource { - return variablesources.NestedVariableSource{ - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewOperatorVariableSource(cl, inputVariableSource), nil - }, - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewBundleDeploymentVariableSource(cl, inputVariableSource), nil - }, - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewBundlesAndDepsVariableSource(inputVariableSource), nil - }, - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return variablesources.NewCRDUniquenessConstraintsVariableSource(inputVariableSource), nil - }, - } -} diff --git a/internal/resolution/entities/bundle_entity.go b/internal/resolution/entities/bundle_entity.go deleted file mode 100644 index ebf49a2a2..000000000 --- a/internal/resolution/entities/bundle_entity.go +++ /dev/null @@ -1,285 +0,0 @@ -package entities - -import ( - "encoding/json" - "fmt" - "sync" - - "github.com/blang/semver/v4" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" -) - -const PropertyBundlePath = "olm.bundle.path" -const PropertyBundleChannelEntry = "olm.bundle.channelEntry" -const PropertyBundleMediaType = "olm.bundle.mediatype" - -type MediaType string - -const ( - MediaTypePlain = "plain+v0" - MediaTypeRegistry = "registry+v1" -) - -// ---- - -type propertyRequirement bool - -const ( - required propertyRequirement = true - optional propertyRequirement = false -) - -type PackageRequired struct { - property.PackageRequired - SemverRange *semver.Range `json:"-"` -} - -type GVK property.GVK - -type ChannelEntry struct { - Name string `json:"name"` - Replaces string `json:"replaces"` - // Skips and skipRange will probably go here as well -} - -func (g GVK) String() string { - return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind) -} - -type GVKRequired property.GVKRequired - -func (g GVKRequired) String() string { - return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind) -} - -func (g GVKRequired) AsGVK() GVK { - return GVK(g) -} - -type BundleEntity struct { - *input.Entity - - // these properties are lazy loaded as they are requested - bundlePackage *property.Package - providedGVKs []GVK - requiredGVKs []GVKRequired - requiredPackages []PackageRequired - channel *property.Channel - channelEntry *ChannelEntry - semVersion *semver.Version - bundlePath string - mediaType string - mu sync.RWMutex -} - -func NewBundleEntity(entity *input.Entity) *BundleEntity { - return &BundleEntity{ - Entity: entity, - mu: sync.RWMutex{}, - } -} - -func (b *BundleEntity) PackageName() (string, error) { - if err := b.loadPackage(); err != nil { - return "", err - } - return b.bundlePackage.PackageName, nil -} - -func (b *BundleEntity) Version() (*semver.Version, error) { - if err := b.loadPackage(); err != nil { - return nil, err - } - return b.semVersion, nil -} - -func (b *BundleEntity) ProvidedGVKs() ([]GVK, error) { - if err := b.loadProvidedGVKs(); err != nil { - return nil, err - } - return b.providedGVKs, nil -} - -func (b *BundleEntity) RequiredGVKs() ([]GVKRequired, error) { - if err := b.loadRequiredGVKs(); err != nil { - return nil, err - } - return b.requiredGVKs, nil -} - -func (b *BundleEntity) RequiredPackages() ([]PackageRequired, error) { - if err := b.loadRequiredPackages(); err != nil { - return nil, err - } - return b.requiredPackages, nil -} - -func (b *BundleEntity) ChannelName() (string, error) { - if err := b.loadChannelProperties(); err != nil { - return "", err - } - return b.channel.ChannelName, nil -} - -func (b *BundleEntity) Channel() (*property.Channel, error) { - if err := b.loadChannelProperties(); err != nil { - return nil, err - } - return b.channel, nil -} - -func (b *BundleEntity) BundleChannelEntry() (*ChannelEntry, error) { - if err := b.loadBundleChannelEntry(); err != nil { - return nil, err - } - return b.channelEntry, nil -} - -func (b *BundleEntity) loadBundleChannelEntry() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.channelEntry == nil { - channelEntry, err := loadFromEntity[*ChannelEntry](b.Entity, PropertyBundleChannelEntry, optional) - if err != nil || channelEntry == nil { - return fmt.Errorf("error determining replaces for entity '%s': %w", b.ID, err) - } - b.channelEntry = channelEntry - } - return nil -} - -func (b *BundleEntity) BundlePath() (string, error) { - if err := b.loadBundlePath(); err != nil { - return "", err - } - return b.bundlePath, nil -} - -func (b *BundleEntity) MediaType() (string, error) { - if err := b.loadMediaType(); err != nil { - return "", err - } - - return b.mediaType, nil -} - -func (b *BundleEntity) loadMediaType() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.mediaType == "" { - mediaType, err := loadFromEntity[string](b.Entity, PropertyBundleMediaType, optional) - if err != nil { - return fmt.Errorf("error determining bundle mediatype for entity '%s': %w", b.ID, err) - } - b.mediaType = mediaType - } - return nil -} - -func (b *BundleEntity) loadPackage() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.bundlePackage == nil { - bundlePackage, err := loadFromEntity[property.Package](b.Entity, property.TypePackage, required) - if err != nil { - return fmt.Errorf("error determining package for entity '%s': %w", b.ID, err) - } - b.bundlePackage = &bundlePackage - if b.semVersion == nil { - semVer, err := semver.Parse(b.bundlePackage.Version) - if err != nil { - return fmt.Errorf("could not parse semver (%s) for entity '%s': %w", b.bundlePackage.Version, b.ID, err) - } - b.semVersion = &semVer - } - } - return nil -} - -func (b *BundleEntity) loadProvidedGVKs() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.providedGVKs == nil { - providedGVKs, err := loadFromEntity[[]GVK](b.Entity, property.TypeGVK, optional) - if err != nil { - return fmt.Errorf("error determining bundle provided gvks for entity '%s': %w", b.ID, err) - } - b.providedGVKs = providedGVKs - } - return nil -} - -func (b *BundleEntity) loadRequiredGVKs() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.requiredGVKs == nil { - requiredGVKs, err := loadFromEntity[[]GVKRequired](b.Entity, property.TypeGVKRequired, optional) - if err != nil { - return fmt.Errorf("error determining bundle required gvks for entity '%s': %w", b.ID, err) - } - b.requiredGVKs = requiredGVKs - } - return nil -} - -func (b *BundleEntity) loadRequiredPackages() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.requiredPackages == nil { - requiredPackages, err := loadFromEntity[[]PackageRequired](b.Entity, property.TypePackageRequired, optional) - if err != nil { - return fmt.Errorf("error determining bundle required packages for entity '%s': %w", b.ID, err) - } - for _, requiredPackage := range requiredPackages { - semverRange, err := semver.ParseRange(requiredPackage.VersionRange) - if err != nil { - return fmt.Errorf("error determining bundle required package semver range for entity '%s': '%w'", b.ID, err) - } - requiredPackage.SemverRange = &semverRange - } - b.requiredPackages = requiredPackages - } - return nil -} - -func (b *BundleEntity) loadChannelProperties() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.channel == nil { - channel, err := loadFromEntity[property.Channel](b.Entity, property.TypeChannel, required) - if err != nil { - return fmt.Errorf("error determining bundle channel properties for entity '%s': %w", b.ID, err) - } - b.channel = &channel - } - return nil -} - -func (b *BundleEntity) loadBundlePath() error { - b.mu.Lock() - defer b.mu.Unlock() - if b.bundlePath == "" { - bundlePath, err := loadFromEntity[string](b.Entity, PropertyBundlePath, required) - if err != nil { - return fmt.Errorf("error determining bundle path for entity '%s': %w", b.ID, err) - } - b.bundlePath = bundlePath - } - return nil -} - -func loadFromEntity[T interface{}](entity *input.Entity, propertyName string, required propertyRequirement) (T, error) { - deserializedProperty := *new(T) - propertyValue, ok := entity.Properties[propertyName] - if ok { - // TODO: In order to avoid invalid properties we should use a decoder that only allows the properties we expect. - // ie. decoder.DisallowUnknownFields() - if err := json.Unmarshal([]byte(propertyValue), &deserializedProperty); err != nil { - return deserializedProperty, fmt.Errorf("property '%s' ('%s') could not be parsed: %w", propertyName, propertyValue, err) - } - } else if required { - return deserializedProperty, fmt.Errorf("required property '%s' not found", propertyName) - } - return deserializedProperty, nil -} diff --git a/internal/resolution/entities/bundle_entity_test.go b/internal/resolution/entities/bundle_entity_test.go deleted file mode 100644 index 0f077e017..000000000 --- a/internal/resolution/entities/bundle_entity_test.go +++ /dev/null @@ -1,358 +0,0 @@ -package entities_test - -import ( - "fmt" - "testing" - - "github.com/blang/semver/v4" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" -) - -func TestBundleEntity(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "BundleEntity Suite") -} - -var _ = Describe("BundleEntity", func() { - Describe("PackageName", func() { - It("should return the package name if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.package": "{\"packageName\":\"prometheus\",\"version\":\"0.14.0\"}", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - packageName, err := bundleEntity.PackageName() - Expect(err).ToNot(HaveOccurred()) - Expect(packageName).To(Equal("prometheus")) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - packageName, err := bundleEntity.PackageName() - Expect(packageName).To(Equal("")) - Expect(err.Error()).To(Equal("error determining package for entity 'operatorhub/prometheus/0.14.0': required property 'olm.package' not found")) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.package": "badPackageNameStructure", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - packageName, err := bundleEntity.PackageName() - Expect(packageName).To(Equal("")) - Expect(err.Error()).To(Equal("error determining package for entity 'operatorhub/prometheus/0.14.0': property 'olm.package' ('badPackageNameStructure') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - Describe("Version", func() { - It("should return the bundle version if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.package": "{\"packageName\":\"prometheus\",\"version\":\"0.14.0\"}", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - version, err := bundleEntity.Version() - Expect(err).ToNot(HaveOccurred()) - Expect(*version).To(Equal(semver.MustParse("0.14.0"))) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - version, err := bundleEntity.Version() - Expect(version).To(BeNil()) - Expect(err.Error()).To(Equal("error determining package for entity 'operatorhub/prometheus/0.14.0': required property 'olm.package' not found")) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.package": "badPackageStructure", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - version, err := bundleEntity.Version() - Expect(version).To(BeNil()) - Expect(err.Error()).To(Equal("error determining package for entity 'operatorhub/prometheus/0.14.0': property 'olm.package' ('badPackageStructure') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - It("should return error if the version is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.package": "{\"packageName\":\"prometheus\",\"version\":\"badversion\"}", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - version, err := bundleEntity.Version() - Expect(version).To(BeNil()) - Expect(err.Error()).To(Equal("could not parse semver (badversion) for entity 'operatorhub/prometheus/0.14.0': No Major.Minor.Patch elements found")) - }) - }) - - Describe("ProvidedGVKs", func() { - It("should return the bundle provided gvks if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.gvk": "[{\"group\":\"foo.io\",\"kind\":\"Foo\",\"version\":\"v1\"},{\"group\":\"bar.io\",\"kind\":\"Bar\",\"version\":\"v1alpha1\"}]", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - providedGvks, err := bundleEntity.ProvidedGVKs() - Expect(err).ToNot(HaveOccurred()) - Expect(providedGvks).To(Equal([]olmentity.GVK{ - {Group: "foo.io", Kind: "Foo", Version: "v1"}, - {Group: "bar.io", Kind: "Bar", Version: "v1alpha1"}, - })) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - providedGvks, err := bundleEntity.ProvidedGVKs() - Expect(providedGvks).To(BeNil()) - Expect(err).ToNot(HaveOccurred()) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.gvk": "badGvkStructure", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - providedGvks, err := bundleEntity.ProvidedGVKs() - Expect(providedGvks).To(BeNil()) - Expect(err.Error()).To(Equal("error determining bundle provided gvks for entity 'operatorhub/prometheus/0.14.0': property 'olm.gvk' ('badGvkStructure') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - Describe("RequiredGVKs", func() { - It("should return the bundle required gvks if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.gvk.required": "[{\"group\":\"foo.io\",\"kind\":\"Foo\",\"version\":\"v1\"},{\"group\":\"bar.io\",\"kind\":\"Bar\",\"version\":\"v1alpha1\"}]", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - requiredGvks, err := bundleEntity.RequiredGVKs() - Expect(err).ToNot(HaveOccurred()) - Expect(requiredGvks).To(Equal([]olmentity.GVKRequired{ - {Group: "foo.io", Kind: "Foo", Version: "v1"}, - {Group: "bar.io", Kind: "Bar", Version: "v1alpha1"}, - })) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - requiredGvks, err := bundleEntity.RequiredGVKs() - Expect(requiredGvks).To(BeNil()) - Expect(err).ToNot(HaveOccurred()) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.gvk.required": "badGvkStructure", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - requiredGvks, err := bundleEntity.RequiredGVKs() - Expect(requiredGvks).To(BeNil()) - Expect(err.Error()).To(Equal("error determining bundle required gvks for entity 'operatorhub/prometheus/0.14.0': property 'olm.gvk.required' ('badGvkStructure') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - Describe("RequiredPackages", func() { - It("should return the bundle required packages if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.package.required": `[{"packageName": "packageA", "versionRange": ">1.0.0"}, {"packageName": "packageB", "versionRange": ">0.5.0 <0.8.6"}]`, - }) - bundleEntity := olmentity.NewBundleEntity(entity) - requiredPackages, err := bundleEntity.RequiredPackages() - Expect(err).ToNot(HaveOccurred()) - Expect(requiredPackages).To(Equal([]olmentity.PackageRequired{ - {PackageRequired: property.PackageRequired{PackageName: "packageA", VersionRange: ">1.0.0"}}, - {PackageRequired: property.PackageRequired{PackageName: "packageB", VersionRange: ">0.5.0 <0.8.6"}}, - })) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - requiredPackages, err := bundleEntity.RequiredPackages() - Expect(requiredPackages).To(BeNil()) - Expect(err).ToNot(HaveOccurred()) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.package.required": "badRequiredPackageStructure", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - requiredPackages, err := bundleEntity.RequiredPackages() - Expect(requiredPackages).To(BeNil()) - Expect(err.Error()).To(Equal("error determining bundle required packages for entity 'operatorhub/prometheus/0.14.0': property 'olm.package.required' ('badRequiredPackageStructure') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - Describe("ChannelName", func() { - It("should return the bundle channel name if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.channel": "{\"channelName\":\"beta\",\"priority\":0}", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - channelName, err := bundleEntity.ChannelName() - Expect(err).ToNot(HaveOccurred()) - Expect(channelName).To(Equal("beta")) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - channelName, err := bundleEntity.ChannelName() - Expect(channelName).To(BeEmpty()) - Expect(err.Error()).To(Equal("error determining bundle channel properties for entity 'operatorhub/prometheus/0.14.0': required property 'olm.channel' not found")) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.channel": "badChannelPropertiesStructure", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - channelName, err := bundleEntity.ChannelName() - Expect(channelName).To(BeEmpty()) - Expect(err.Error()).To(Equal("error determining bundle channel properties for entity 'operatorhub/prometheus/0.14.0': property 'olm.channel' ('badChannelPropertiesStructure') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - Describe("Channel", func() { - It("should return the bundle channel properties if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.channel": `{"channelName":"beta","priority":0, "replaces": "bundle.v1.0.0", "skips": ["bundle.v0.9.0", "bundle.v0.9.6"], "skipRange": ">=0.9.0 <=0.9.6"}`, - }) - bundleEntity := olmentity.NewBundleEntity(entity) - channelProperties, err := bundleEntity.Channel() - Expect(err).ToNot(HaveOccurred()) - Expect(*channelProperties).To(Equal(property.Channel{ - ChannelName: "beta", - Priority: 0, - }, - )) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - channelProperties, err := bundleEntity.Channel() - Expect(channelProperties).To(BeNil()) - Expect(err.Error()).To(Equal("error determining bundle channel properties for entity 'operatorhub/prometheus/0.14.0': required property 'olm.channel' not found")) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.channel": "badChannelPropertiesStructure", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - channelProperties, err := bundleEntity.Channel() - Expect(channelProperties).To(BeNil()) - Expect(err.Error()).To(Equal("error determining bundle channel properties for entity 'operatorhub/prometheus/0.14.0': property 'olm.channel' ('badChannelPropertiesStructure') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - Describe("BundlePath", func() { - It("should return the bundle channel properties if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.bundle.path": `"bundle.io/path/to/bundle"`, - }) - bundleEntity := olmentity.NewBundleEntity(entity) - bundlePath, err := bundleEntity.BundlePath() - Expect(err).ToNot(HaveOccurred()) - Expect(bundlePath).To(Equal("bundle.io/path/to/bundle")) - }) - It("should return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - bundlePath, err := bundleEntity.BundlePath() - Expect(bundlePath).To(BeEmpty()) - Expect(err.Error()).To(Equal("error determining bundle path for entity 'operatorhub/prometheus/0.14.0': required property 'olm.bundle.path' not found")) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - "olm.bundle.path": "badBundlePath", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - bundlePath, err := bundleEntity.BundlePath() - Expect(bundlePath).To(BeEmpty()) - Expect(err.Error()).To(Equal("error determining bundle path for entity 'operatorhub/prometheus/0.14.0': property 'olm.bundle.path' ('badBundlePath') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - Describe("ChannelEntry", func() { - It("should return the channel entry property if present", func() { - entity := input.NewEntity("test", map[string]string{ - "olm.bundle.channelEntry": `{"name":"test.v0.3.0","replaces": "test.v0.2.0"}`, - }) - bundleEntity := olmentity.NewBundleEntity(entity) - channelEntry, err := bundleEntity.BundleChannelEntry() - Expect(err).ToNot(HaveOccurred()) - Expect(channelEntry).To(Equal(&olmentity.ChannelEntry{Name: "test.v0.3.0", Replaces: "test.v0.2.0"})) - }) - It("should not return an error if the property is not found", func() { - entity := input.NewEntity("test", map[string]string{ - "olm.thingy": `{"whatever":"this"}`, - }) - bundleEntity := olmentity.NewBundleEntity(entity) - channelEntry, err := bundleEntity.BundleChannelEntry() - Expect(channelEntry).To(BeNil()) - Expect(err).To(HaveOccurred()) - }) - }) - - Describe("MediaType", func() { - It("should return the bundle mediatype property if present", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - olmentity.PropertyBundleMediaType: fmt.Sprintf(`"%s"`, olmentity.MediaTypePlain), - }) - bundleEntity := olmentity.NewBundleEntity(entity) - mediaType, err := bundleEntity.MediaType() - Expect(err).ToNot(HaveOccurred()) - Expect(mediaType).To(Equal(olmentity.MediaTypePlain)) - }) - It("should not return an error if the property is not found", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) - bundleEntity := olmentity.NewBundleEntity(entity) - mediaType, err := bundleEntity.MediaType() - Expect(mediaType).To(BeEmpty()) - Expect(err).ToNot(HaveOccurred()) - }) - It("should return error if the property is malformed", func() { - entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ - olmentity.PropertyBundleMediaType: "badtype", - }) - bundleEntity := olmentity.NewBundleEntity(entity) - mediaType, err := bundleEntity.MediaType() - Expect(mediaType).To(BeEmpty()) - Expect(err.Error()).To(Equal("error determining bundle mediatype for entity 'operatorhub/prometheus/0.14.0': property 'olm.bundle.mediatype' ('badtype') could not be parsed: invalid character 'b' looking for beginning of value")) - }) - }) - - // Increase test coverage - Describe("GVKRequired properties", func() { - It("should return the GVKRequired properties", func() { - gvk := olmentity.GVKRequired{ - Group: "foo.io", - Kind: "Foo", - Version: "v1", - } - Expect(gvk.AsGVK().Version).To(Equal("v1")) - Expect(gvk.AsGVK().Group).To(Equal("foo.io")) - Expect(gvk.AsGVK().Kind).To(Equal("Foo")) - }) - It("should return the GVKRequired properties as a string", func() { - gvk := olmentity.GVKRequired{ - Group: "foo.io", - Kind: "Foo", - Version: "v1", - } - Expect(gvk.String()).To(Equal(`group:"foo.io" version:"v1" kind:"Foo"`)) - }) - }) - Describe("GVK properties", func() { - It("should return the gvk properties", func() { - gvk := olmentity.GVK{ - Group: "foo.io", - Kind: "Foo", - Version: "v1", - } - Expect(gvk.Version).To(Equal("v1")) - Expect(gvk.Group).To(Equal("foo.io")) - Expect(gvk.Kind).To(Equal("Foo")) - }) - It("should return the gvk properties as a string", func() { - gvk := olmentity.GVK{ - Group: "foo.io", - Kind: "Foo", - Version: "v1", - } - Expect(gvk.String()).To(Equal(`group:"foo.io" version:"v1" kind:"Foo"`)) - }) - }) -}) diff --git a/internal/resolution/entitysources/catalogdsource.go b/internal/resolution/entitysources/catalogdsource.go deleted file mode 100644 index 6c9460e68..000000000 --- a/internal/resolution/entitysources/catalogdsource.go +++ /dev/null @@ -1,140 +0,0 @@ -package entitysources - -import ( - "context" - "encoding/json" - "fmt" - - catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/operator-framework/operator-controller/internal/resolution/entities" -) - -// CatalogdEntitySource is a source for(/collection of) deppy defined input.Entity, built from content -// made accessible on-cluster by https://github.com/operator-framework/catalogd. -// It is an implementation of deppy defined input.EntitySource -type CatalogdEntitySource struct { - client client.Client -} - -func NewCatalogdEntitySource(client client.Client) *CatalogdEntitySource { - return &CatalogdEntitySource{client: client} -} - -func (es *CatalogdEntitySource) Get(_ context.Context, _ deppy.Identifier) (*input.Entity, error) { - panic("not implemented") -} - -func (es *CatalogdEntitySource) Filter(ctx context.Context, filter input.Predicate) (input.EntityList, error) { - resultSet := input.EntityList{} - entities, err := getEntities(ctx, es.client) - if err != nil { - return nil, err - } - for i := range entities { - if filter(&entities[i]) { - resultSet = append(resultSet, entities[i]) - } - } - return resultSet, nil -} - -func (es *CatalogdEntitySource) GroupBy(ctx context.Context, fn input.GroupByFunction) (input.EntityListMap, error) { - entities, err := getEntities(ctx, es.client) - if err != nil { - return nil, err - } - resultSet := input.EntityListMap{} - for i := range entities { - keys := fn(&entities[i]) - for _, key := range keys { - resultSet[key] = append(resultSet[key], entities[i]) - } - } - return resultSet, nil -} - -func (es *CatalogdEntitySource) Iterate(ctx context.Context, fn input.IteratorFunction) error { - entities, err := getEntities(ctx, es.client) - if err != nil { - return err - } - for i := range entities { - if err := fn(&entities[i]); err != nil { - return err - } - } - return nil -} - -func getEntities(ctx context.Context, client client.Client) (input.EntityList, error) { - entityList := input.EntityList{} - bundleMetadatas, packageMetdatas, err := fetchMetadata(ctx, client) - if err != nil { - return nil, err - } - for _, bundle := range bundleMetadatas.Items { - props := map[string]string{} - - // TODO: We should make sure all properties are forwarded - // through and avoid a lossy translation from FBC --> entity - for _, prop := range bundle.Spec.Properties { - switch prop.Type { - case property.TypePackage: - // this is already a json marshalled object, so it doesn't need to be marshalled - // like the other ones - props[property.TypePackage] = string(prop.Value) - case entities.PropertyBundleMediaType: - props[entities.PropertyBundleMediaType] = string(prop.Value) - } - } - - imgValue, err := json.Marshal(bundle.Spec.Image) - if err != nil { - return nil, err - } - props[entities.PropertyBundlePath] = string(imgValue) - catalogScopedPkgName := fmt.Sprintf("%s-%s", bundle.Spec.Catalog.Name, bundle.Spec.Package) - bundlePkg := packageMetdatas[catalogScopedPkgName] - for _, ch := range bundlePkg.Spec.Channels { - for _, b := range ch.Entries { - catalogScopedEntryName := fmt.Sprintf("%s-%s", bundle.Spec.Catalog.Name, b.Name) - if catalogScopedEntryName == bundle.Name { - channelValue, _ := json.Marshal(property.Channel{ChannelName: ch.Name, Priority: 0}) - props[property.TypeChannel] = string(channelValue) - replacesValue, _ := json.Marshal(entities.ChannelEntry{ - Name: b.Name, - Replaces: b.Replaces, - }) - props[entities.PropertyBundleChannelEntry] = string(replacesValue) - entity := input.Entity{ - ID: deppy.IdentifierFromString(fmt.Sprintf("%s%s%s", bundle.Name, bundle.Spec.Package, ch.Name)), - Properties: props, - } - entityList = append(entityList, entity) - } - } - } - } - return entityList, nil -} - -func fetchMetadata(ctx context.Context, client client.Client) (catalogd.BundleMetadataList, map[string]catalogd.Package, error) { - packageMetdatas := catalogd.PackageList{} - if err := client.List(ctx, &packageMetdatas); err != nil { - return catalogd.BundleMetadataList{}, nil, err - } - bundleMetadatas := catalogd.BundleMetadataList{} - if err := client.List(ctx, &bundleMetadatas); err != nil { - return catalogd.BundleMetadataList{}, nil, err - } - packages := map[string]catalogd.Package{} - for _, pkg := range packageMetdatas.Items { - packages[pkg.Name] = pkg - } - return bundleMetadatas, packages, nil -} diff --git a/internal/resolution/util/predicates/predicates.go b/internal/resolution/util/predicates/predicates.go deleted file mode 100644 index c339b6ac8..000000000 --- a/internal/resolution/util/predicates/predicates.go +++ /dev/null @@ -1,80 +0,0 @@ -package predicates - -import ( - "github.com/blang/semver/v4" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" -) - -func WithPackageName(packageName string) input.Predicate { - return func(entity *input.Entity) bool { - bundleEntity := olmentity.NewBundleEntity(entity) - name, err := bundleEntity.PackageName() - if err != nil { - return false - } - return name == packageName - } -} - -func InSemverRange(semverRange semver.Range) input.Predicate { - return func(entity *input.Entity) bool { - bundleEntity := olmentity.NewBundleEntity(entity) - bundleVersion, err := bundleEntity.Version() - if err != nil { - return false - } - return semverRange(*bundleVersion) - } -} - -func InChannel(channelName string) input.Predicate { - return func(entity *input.Entity) bool { - bundleEntity := olmentity.NewBundleEntity(entity) - bundleChannel, err := bundleEntity.ChannelName() - if err != nil { - return false - } - return channelName == bundleChannel - } -} - -func ProvidesGVK(gvk *olmentity.GVK) input.Predicate { - return func(entity *input.Entity) bool { - bundleEntity := olmentity.NewBundleEntity(entity) - providedGVKs, err := bundleEntity.ProvidedGVKs() - if err != nil { - return false - } - for i := 0; i < len(providedGVKs); i++ { - providedGVK := &providedGVKs[i] - if providedGVK.String() == gvk.String() { - return true - } - } - return false - } -} - -func WithBundleImage(bundleImage string) input.Predicate { - return func(entity *input.Entity) bool { - bundleEntity := olmentity.NewBundleEntity(entity) - bundlePath, err := bundleEntity.BundlePath() - if err != nil { - return false - } - return bundlePath == bundleImage - } -} - -func Replaces(bundleID string) input.Predicate { - return func(entity *input.Entity) bool { - bundleEntity := olmentity.NewBundleEntity(entity) - replaces, err := bundleEntity.BundleChannelEntry() - if err != nil { - return false - } - return replaces.Replaces == bundleID - } -} diff --git a/internal/resolution/util/predicates/predicates_test.go b/internal/resolution/util/predicates/predicates_test.go deleted file mode 100644 index 5f2b03d00..000000000 --- a/internal/resolution/util/predicates/predicates_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package predicates_test - -import ( - "testing" - - "github.com/blang/semver/v4" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - "github.com/operator-framework/operator-controller/internal/resolution/util/predicates" -) - -func TestPredicates(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Predicates Suite") -} - -var _ = Describe("Predicates", func() { - Describe("WithPackageName", func() { - It("should return true when the entity has the same package name", func() { - entity := input.NewEntity("test", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`, - }) - Expect(predicates.WithPackageName("mypackage")(entity)).To(BeTrue()) - Expect(predicates.WithPackageName("notmypackage")(entity)).To(BeFalse()) - }) - It("should return false when the entity does not have a package name", func() { - entity := input.NewEntity("test", map[string]string{}) - Expect(predicates.WithPackageName("mypackage")(entity)).To(BeFalse()) - }) - }) - - Describe("InSemverRange", func() { - It("should return true when the entity has the has version in the right range", func() { - entity := input.NewEntity("test", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`, - }) - inRange := semver.MustParseRange(">=1.0.0") - notInRange := semver.MustParseRange(">=2.0.0") - Expect(predicates.InSemverRange(inRange)(entity)).To(BeTrue()) - Expect(predicates.InSemverRange(notInRange)(entity)).To(BeFalse()) - }) - It("should return false when the entity does not have a version", func() { - entity := input.NewEntity("test", map[string]string{}) - inRange := semver.MustParseRange(">=1.0.0") - Expect(predicates.InSemverRange(inRange)(entity)).To(BeFalse()) - }) - }) - - Describe("InChannel", func() { - It("should return true when the entity comes from the specified channel", func() { - entity := input.NewEntity("test", map[string]string{ - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - Expect(predicates.InChannel("stable")(entity)).To(BeTrue()) - Expect(predicates.InChannel("unstable")(entity)).To(BeFalse()) - }) - It("should return false when the entity does not have a channel", func() { - entity := input.NewEntity("test", map[string]string{}) - Expect(predicates.InChannel("stable")(entity)).To(BeFalse()) - }) - }) - - Describe("ProvidesGVK", func() { - It("should return true when the entity provides the specified gvk", func() { - entity := input.NewEntity("test", map[string]string{ - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"},{"group":"bar.io","kind":"Bar","version":"v1"}]`, - }) - Expect(predicates.ProvidesGVK(&olmentity.GVK{ - Group: "foo.io", - Version: "v1", - Kind: "Foo", - })(entity)).To(BeTrue()) - Expect(predicates.ProvidesGVK(&olmentity.GVK{ - Group: "baz.io", - Version: "v1alpha1", - Kind: "Baz", - })(entity)).To(BeFalse()) - }) - It("should return false when the entity does not provide a gvk", func() { - entity := input.NewEntity("test", map[string]string{}) - Expect(predicates.ProvidesGVK(&olmentity.GVK{ - Group: "foo.io", - Version: "v1", - Kind: "Foo", - })(entity)).To(BeFalse()) - }) - }) - - Describe("WithBundleImage", func() { - It("should return true when the entity provides the same bundle image", func() { - entity := input.NewEntity("test", map[string]string{ - olmentity.PropertyBundlePath: `"registry.io/repo/image@sha256:1234567890"`, - }) - Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:1234567890")(entity)).To(BeTrue()) - Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:0987654321")(entity)).To(BeFalse()) - }) - It("should return false when the entity does not provide a bundle image", func() { - entity := input.NewEntity("test", map[string]string{}) - Expect(predicates.WithBundleImage("registry.io/repo/image@sha256:1234567890")(entity)).To(BeFalse()) - }) - }) - - Describe("Replaces", func() { - It("should return true when the entity provides the replaces property", func() { - entity := input.NewEntity("test", map[string]string{ - "olm.bundle.channelEntry": `{"replaces": "test.v0.2.0"}`, - }) - Expect(predicates.Replaces("test.v0.2.0")(entity)).To(BeTrue()) - Expect(predicates.Replaces("test.v0.1.0")(entity)).To(BeFalse()) - }) - It("should return false when the entity does not provide a replaces property", func() { - entity := input.NewEntity("test", map[string]string{}) - Expect(predicates.Replaces("test.v0.2.0")(entity)).To(BeFalse()) - }) - It("should return false when the replace value is not a valid json", func() { - entity := input.NewEntity("test", map[string]string{ - "olm.bundle.channelEntry": `{"replaces"}`, - }) - Expect(predicates.Replaces("test.v0.2.0")(entity)).To(BeFalse()) - }) - }) -}) diff --git a/internal/resolution/util/sort/sort.go b/internal/resolution/util/sort/sort.go deleted file mode 100644 index d013b982f..000000000 --- a/internal/resolution/util/sort/sort.go +++ /dev/null @@ -1,80 +0,0 @@ -package sort - -import ( - "strings" - - "github.com/operator-framework/deppy/pkg/deppy/input" - - "github.com/operator-framework/operator-controller/internal/resolution/entities" -) - -// ByChannelAndVersion is an entity sort function that orders the entities in -// package, channel (default channel at the head), and inverse version (higher versions on top) -// if a property does not exist for one of the entities, the one missing the property is pushed down -// if both entities are missing the same property they are ordered by id -func ByChannelAndVersion(entity1 *input.Entity, entity2 *input.Entity) bool { - e1 := entities.NewBundleEntity(entity1) - e2 := entities.NewBundleEntity(entity2) - - // first sort package lexical order - pkgOrder := packageOrder(e1, e2) - if pkgOrder != 0 { - return pkgOrder < 0 - } - - // todo(perdasilva): handle default channel in ordering once it is being exposed by the entity - channelOrder := channelOrder(e1, e2) - if channelOrder != 0 { - return channelOrder < 0 - } - - // order version from highest to lowest (favor the latest release) - versionOrder := versionOrder(e1, e2) - return versionOrder > 0 -} - -func compareErrors(err1 error, err2 error) int { - if err1 != nil && err2 == nil { - return 1 - } - - if err1 == nil && err2 != nil { - return -1 - } - return 0 -} - -func packageOrder(e1, e2 *entities.BundleEntity) int { - name1, err1 := e1.PackageName() - name2, err2 := e2.PackageName() - errComp := compareErrors(err1, err2) - if errComp != 0 { - return errComp - } - return strings.Compare(name1, name2) -} - -func channelOrder(e1, e2 *entities.BundleEntity) int { - channelProperties1, err1 := e1.Channel() - channelProperties2, err2 := e2.Channel() - errComp := compareErrors(err1, err2) - if errComp != 0 { - return errComp - } - if channelProperties1.Priority != channelProperties2.Priority { - return channelProperties1.Priority - channelProperties2.Priority - } - return strings.Compare(channelProperties1.ChannelName, channelProperties2.ChannelName) -} - -func versionOrder(e1, e2 *entities.BundleEntity) int { - ver1, err1 := e1.Version() - ver2, err2 := e2.Version() - errComp := compareErrors(err1, err2) - if errComp != 0 { - // the sign gets inverted because version is sorted - // from highest to lowest - return -1 * errComp - } - return ver1.Compare(*ver2) -} diff --git a/internal/resolution/util/sort/sort_test.go b/internal/resolution/util/sort/sort_test.go deleted file mode 100644 index 6f1677cfe..000000000 --- a/internal/resolution/util/sort/sort_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package sort_test - -import ( - "sort" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - entitysort "github.com/operator-framework/operator-controller/internal/resolution/util/sort" -) - -func TestSort(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Sort Suite") -} - -var _ = Describe("Sort", func() { - Describe("ByChannelAndVersion", func() { - It("should order entities by package name", func() { - e1 := input.NewEntity("test1", map[string]string{ - property.TypePackage: `{"packageName": "package1", "version": "1.0.0"}`, - }) - e2 := input.NewEntity("test2", map[string]string{ - property.TypePackage: `{"packageName": "package2", "version": "1.0.0"}`, - }) - e3 := input.NewEntity("test3", map[string]string{ - property.TypePackage: `{"packageName": "package3", "version": "1.0.0"}`, - }) - entities := []*input.Entity{e2, e3, e1} - - sort.Slice(entities, func(i, j int) bool { - return entitysort.ByChannelAndVersion(entities[i], entities[j]) - }) - - Expect(entities[0]).To(Equal(e1)) - Expect(entities[1]).To(Equal(e2)) - Expect(entities[2]).To(Equal(e3)) - }) - - It("should order entities by channel name", func() { - e1 := input.NewEntity("test1", map[string]string{ - property.TypePackage: `{"packageName": "package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stableA","priority":0}`, - }) - e2 := input.NewEntity("test2", map[string]string{ - property.TypePackage: `{"packageName": "package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stableB","priority":0}`, - }) - e3 := input.NewEntity("test3", map[string]string{ - property.TypePackage: `{"packageName": "package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stableC","priority":0}`, - }) - entities := []*input.Entity{e2, e3, e1} - - sort.Slice(entities, func(i, j int) bool { - return entitysort.ByChannelAndVersion(entities[i], entities[j]) - }) - - Expect(entities[0]).To(Equal(e1)) - Expect(entities[1]).To(Equal(e2)) - Expect(entities[2]).To(Equal(e3)) - }) - - It("should order entities by version number (highest first)", func() { - e1 := input.NewEntity("test1", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - e2 := input.NewEntity("test2", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - e3 := input.NewEntity("test3", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - entities := []*input.Entity{e2, e3, e1} - - sort.Slice(entities, func(i, j int) bool { - return entitysort.ByChannelAndVersion(entities[i], entities[j]) - }) - - Expect(entities[0]).To(Equal(e3)) - Expect(entities[1]).To(Equal(e2)) - Expect(entities[2]).To(Equal(e1)) - }) - - It("should order entities by version number (highest first) and channel priority (lower value -> higher priority)", func() { - e1 := input.NewEntity("test1", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"beta","priority":1}`, - }) - e2 := input.NewEntity("test2", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - e3 := input.NewEntity("test3", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"beta","priority":1}`, - }) - e4 := input.NewEntity("test4", map[string]string{ - property.TypePackage: `{"packageName": "mypackage", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"beta","priority":1}`, - }) - entities := []*input.Entity{e2, e3, e1, e4} - - sort.Slice(entities, func(i, j int) bool { - return entitysort.ByChannelAndVersion(entities[i], entities[j]) - }) - - Expect(entities[0]).To(Equal(e2)) - Expect(entities[1]).To(Equal(e4)) - Expect(entities[2]).To(Equal(e3)) - Expect(entities[3]).To(Equal(e1)) - }) - - It("should order entities missing a property after those that have it", func() { - e1 := input.NewEntity("test1", map[string]string{ - property.TypePackage: `{"packageName": "mypackageA", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - e2 := input.NewEntity("test2", map[string]string{ - property.TypePackage: `{"packageName": "mypackageA"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - e3 := input.NewEntity("test3", map[string]string{ - property.TypePackage: `{"packageName": "mypackageA", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - e4 := input.NewEntity("test4", map[string]string{ - property.TypePackage: `{"packageName": "mypackageB", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }) - e5 := input.NewEntity("test5", map[string]string{}) - entities := []*input.Entity{e2, e3, e1, e4, e5} - - sort.Slice(entities, func(i, j int) bool { - return entitysort.ByChannelAndVersion(entities[i], entities[j]) - }) - - Expect(entities[0]).To(Equal(e3)) - Expect(entities[1]).To(Equal(e1)) - Expect(entities[2]).To(Equal(e4)) - Expect(entities[3]).To(Equal(e2)) // no version - Expect(entities[4]).To(Equal(e5)) // no package - or anything - }) - }) - -}) diff --git a/internal/resolution/v2/store/catalog.go b/internal/resolution/v2/store/catalog.go new file mode 100644 index 000000000..910d1e8ab --- /dev/null +++ b/internal/resolution/v2/store/catalog.go @@ -0,0 +1,90 @@ +package store + +import ( + "encoding/json" + "fmt" + + masterminds "github.com/Masterminds/semver/v3" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + "k8s.io/apimachinery/pkg/util/sets" +) + +const ( + PropertyBundleMediaType = "olm.bundle.mediatype" + MediaTypeRegistryV1 = "registry+v1" + MediaTypePlainV0 = "plain+v0" +) + +type Catalog struct { + Name string + Packages map[string]*Package +} + +type Package struct { + CatalogName string + declcfg.Package + Channels map[string]*Channel + Bundles map[string]*Bundle +} + +type Channel struct { + CatalogName string + declcfg.Channel + Bundles []*Bundle +} + +func NewChannel(catalogName string, ch declcfg.Channel, packageBundles map[string]*Bundle) (*Channel, error) { + memberNames := sets.New[string]() + for _, e := range ch.Entries { + memberNames.Insert(e.Name) + } + + members := make([]*Bundle, 0, len(memberNames)) + for _, b := range packageBundles { + if memberNames.Has(b.Name) { + members = append(members, b) + } + } + return &Channel{ + CatalogName: catalogName, + Channel: ch, + Bundles: members, + }, nil +} + +type Bundle struct { + CatalogName string + declcfg.Bundle + Version masterminds.Version +} + +func NewBundle(catalogName string, b declcfg.Bundle) (*Bundle, error) { + vers, err := bundleVersion(b) + if err != nil { + return nil, err + } + return &Bundle{ + CatalogName: catalogName, + Bundle: b, + Version: *vers, + }, nil +} + +func bundleVersion(b declcfg.Bundle) (*masterminds.Version, error) { + for _, p := range b.Properties { + if p.Type != property.TypePackage { + continue + } + var pkg property.Package + if err := json.Unmarshal(p.Value, &pkg); err != nil { + return nil, fmt.Errorf("invalid package property: %w", err) + } + v, err := masterminds.NewVersion(pkg.Version) + if err != nil { + return nil, fmt.Errorf("invalid bundle version: %w", err) + } + return v, nil + } + return nil, fmt.Errorf("could not get bundle version: no olm.package property found") +} diff --git a/internal/resolution/v2/store/store.go b/internal/resolution/v2/store/store.go new file mode 100644 index 000000000..286a2c63e --- /dev/null +++ b/internal/resolution/v2/store/store.go @@ -0,0 +1,177 @@ +package store + +import ( + "context" + "encoding/json" + + masterminds "github.com/Masterminds/semver/v3" + catalogdv1alpha1 "github.com/operator-framework/catalogd/api/core/v1alpha1" + "github.com/operator-framework/operator-registry/alpha/declcfg" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Store interface { + GetCatalogs(context.Context) ([]Catalog, error) + GetInstalledBundles(context.Context) (map[string]*InstalledBundle, error) + GetCluster(context.Context) (*Cluster, error) +} + +type InstalledBundle struct { + Package string + Name string + Version *masterminds.Version +} + +func (c CatalogMetadataStore) GetInstalledBundles(ctx context.Context) (map[string]*InstalledBundle, error) { + var bdList rukpakv1alpha1.BundleDeploymentList + if err := c.List(ctx, &bdList); err != nil { + return nil, err + } + + bundles := make(map[string]*InstalledBundle, len(bdList.Items)) + for _, bd := range bdList.Items { + pkg := bd.Annotations["operators.operatorframework.io/package"] + name := bd.Annotations["operators.operatorframework.io/name"] + version := bd.Annotations["operators.operatorframework.io/version"] + if pkg == "" || name == "" || version == "" { + continue + } + vers, err := masterminds.StrictNewVersion(version) + if err != nil { + return nil, err + } + bundles[pkg] = &InstalledBundle{ + Package: pkg, + Name: name, + Version: vers, + } + } + return bundles, nil +} + +type Cluster struct { + Nodes []corev1.Node + VersionInfo *version.Info +} + +func (c CatalogMetadataStore) GetCluster(ctx context.Context) (*Cluster, error) { + var nodeList corev1.NodeList + if err := c.List(ctx, &nodeList); err != nil { + return nil, err + } + + serverVersion, err := c.ServerVersion() + if err != nil { + return nil, err + } + + return &Cluster{ + Nodes: nodeList.Items, + VersionInfo: serverVersion, + }, nil +} + +type CatalogMetadataStore struct { + client.Client + discovery.ServerVersionInterface +} + +func (c CatalogMetadataStore) GetCatalogs(ctx context.Context) ([]Catalog, error) { + var catalogList catalogdv1alpha1.CatalogList + if err := c.List(ctx, &catalogList); err != nil { + return nil, err + } + + catalogs := make([]Catalog, 0, len(catalogList.Items)) + for _, catalog := range catalogList.Items { + packages, err := c.getPackages(ctx, catalog.Name) + if err != nil { + return nil, err + } + for _, pkg := range packages { + bundles, err := c.getBundles(ctx, catalog.Name, pkg.Name) + if err != nil { + return nil, err + } + pkg.Bundles = bundles + + channels, err := c.getChannels(ctx, catalog.Name, pkg.Name, bundles) + if err != nil { + return nil, err + } + pkg.Channels = channels + } + catalogs = append(catalogs, Catalog{ + Name: catalog.Name, + Packages: packages, + }) + } + return catalogs, nil +} + +func (c CatalogMetadataStore) getPackages(ctx context.Context, catalogName string) (map[string]*Package, error) { + var cmList catalogdv1alpha1.CatalogMetadataList + if err := c.List(ctx, &cmList, client.MatchingLabels{"catalog": catalogName, "schema": declcfg.SchemaPackage}); err != nil { + return nil, err + } + + packages := map[string]*Package{} + for _, cm := range cmList.Items { + var p declcfg.Package + if err := json.Unmarshal(cm.Spec.Content, &p); err != nil { + return nil, err + } + packages[p.Name] = &Package{ + CatalogName: catalogName, + Package: p, + Channels: map[string]*Channel{}, + } + } + return packages, nil +} + +func (c CatalogMetadataStore) getChannels(ctx context.Context, catalogName string, packageName string, bundles map[string]*Bundle) (map[string]*Channel, error) { + var cmList catalogdv1alpha1.CatalogMetadataList + if err := c.List(ctx, &cmList, client.MatchingLabels{"catalog": catalogName, "schema": declcfg.SchemaChannel, "package": packageName}); err != nil { + return nil, err + } + + channels := map[string]*Channel{} + for _, cm := range cmList.Items { + var ch declcfg.Channel + if err := json.Unmarshal(cm.Spec.Content, &ch); err != nil { + return nil, err + } + cch, err := NewChannel(catalogName, ch, bundles) + if err != nil { + return nil, err + } + channels[ch.Name] = cch + } + return channels, nil +} + +func (c CatalogMetadataStore) getBundles(ctx context.Context, catalogName string, packageName string) (map[string]*Bundle, error) { + var cmList catalogdv1alpha1.CatalogMetadataList + if err := c.List(ctx, &cmList, client.MatchingLabels{"catalog": catalogName, "schema": declcfg.SchemaBundle, "package": packageName}); err != nil { + return nil, err + } + + bundles := map[string]*Bundle{} + for _, cm := range cmList.Items { + var b declcfg.Bundle + if err := json.Unmarshal(cm.Spec.Content, &b); err != nil { + return nil, err + } + cb, err := NewBundle(catalogName, b) + if err != nil { + return nil, err + } + bundles[b.Name] = cb + } + return bundles, nil +} diff --git a/internal/resolution/v2/variables/bundle.go b/internal/resolution/v2/variables/bundle.go new file mode 100644 index 000000000..a389e95c5 --- /dev/null +++ b/internal/resolution/v2/variables/bundle.go @@ -0,0 +1,29 @@ +package variables + +import ( + "github.com/operator-framework/deppy/pkg/deppy" + + "github.com/operator-framework/operator-controller/internal/resolution/v2/store" +) + +var _ deppy.Variable = &Bundle{} + +type Bundle struct { + ID deppy.Identifier + *store.Bundle +} + +func (b *Bundle) Identifier() deppy.Identifier { + return b.ID +} + +func (b *Bundle) Constraints() []deppy.Constraint { + return nil +} + +func NewBundle(id deppy.Identifier, bundle *store.Bundle) *Bundle { + return &Bundle{ + ID: id, + Bundle: bundle, + } +} diff --git a/internal/resolution/v2/variablesources/operator.go b/internal/resolution/v2/variablesources/operator.go new file mode 100644 index 000000000..015785b53 --- /dev/null +++ b/internal/resolution/v2/variablesources/operator.go @@ -0,0 +1,234 @@ +package variablesources + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + + masterminds "github.com/Masterminds/semver/v3" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/constraint" + "github.com/operator-framework/deppy/pkg/deppy/input" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/resolution/v2/store" + "github.com/operator-framework/operator-controller/internal/resolution/v2/variables" +) + +var _ input.VariableSource = &Operator{} + +type Operator struct { + client.Client + store.Store +} + +func (c Operator) GetVariables(ctx context.Context, _ input.EntitySource) ([]deppy.Variable, error) { + catalogs, err := c.GetCatalogs(ctx) + if err != nil { + return nil, err + } + + installedBundles, err := c.GetInstalledBundles(ctx) + if err != nil { + return nil, err + } + + cluster, err := c.GetCluster(ctx) + if err != nil { + return nil, err + } + + var opList operatorsv1alpha1.OperatorList + if err := c.List(ctx, &opList); err != nil { + return nil, err + } + vars := make([]deppy.Variable, 0, len(opList.Items)) + + for _, op := range opList.Items { + opVarID := deppy.IdentifierFromString("Operator/" + op.Name) + pkgs := make([]*store.Package, 0) + for _, c := range catalogs { + catPkg := c.Packages[op.Spec.PackageName] + if c.Packages[op.Spec.PackageName] != nil { + pkgs = append(pkgs, catPkg) + } + } + if len(pkgs) == 0 { + vars = append(vars, input.NewSimpleVariable(opVarID, + constraint.Mandatory(), + constraint.NewUserFriendlyConstraint(constraint.Prohibited(), + func(constraint deppy.Constraint, subject deppy.Identifier) string { + return fmt.Sprintf("Package %q not found.", op.Spec.PackageName) + }, + )), + ) + continue + } + + allBundles := make([]*store.Bundle, 0) + for _, pkg := range pkgs { + for _, bundle := range pkg.Bundles { + allBundles = append(allBundles, bundle) + } + } + + notFoundMessage := fmt.Sprintf("no bundles found from package %q", op.Spec.PackageName) + if len(allBundles) == 0 { + vars = append(vars, input.NewSimpleVariable(opVarID, + constraint.Mandatory(), + constraint.NewUserFriendlyConstraint(constraint.Prohibited(), + func(constraint deppy.Constraint, subject deppy.Identifier) string { + return notFoundMessage + }, + )), + ) + continue + } + + sort.Slice(allBundles, func(i, j int) bool { + return allBundles[i].Version.GreaterThan(&allBundles[j].Version) + }) + + if op.Spec.Channel != "" { + notFoundMessage = fmt.Sprintf("%s, channel %q", notFoundMessage, op.Spec.Channel) + chBundles := allBundles[:0] + for _, pkg := range pkgs { + for _, ch := range pkg.Channels { + if ch.Name == op.Spec.Channel { + chBundles = append(chBundles, ch.Bundles...) + } + } + } + if len(chBundles) == 0 { + vars = append(vars, input.NewSimpleVariable(opVarID, + constraint.Mandatory(), + constraint.NewUserFriendlyConstraint(constraint.Prohibited(), + func(constraint deppy.Constraint, subject deppy.Identifier) string { + return notFoundMessage + }, + )), + ) + continue + } + allBundles = chBundles + } + + if op.Spec.Version != "" { + notFoundMessage = fmt.Sprintf("%s, in version range %q", notFoundMessage, op.Spec.Version) + + verBundles := allBundles[:0] + verConstraint, err := masterminds.NewConstraint(op.Spec.Version) + if err != nil { + return nil, err + } + for _, bundle := range allBundles { + if verConstraint.Check(&bundle.Version) { + verBundles = append(verBundles, bundle) + } + } + if len(verBundles) == 0 { + vars = append(vars, input.NewSimpleVariable(opVarID, + constraint.Mandatory(), + constraint.NewUserFriendlyConstraint(constraint.Prohibited(), + func(constraint deppy.Constraint, subject deppy.Identifier) string { + return notFoundMessage + }, + ), + )) + continue + } + allBundles = verBundles + } + + if op.Spec.UpgradeEdgeConstraintPolicy == operatorsv1alpha1.UpgradeEdgeConstraintPolicyEnforce { + installedBundle := installedBundles[op.Spec.PackageName] + if installedBundle != nil { + successorRange := fmt.Sprintf("^%s", installedBundle.Version) + notFoundMessage = fmt.Sprintf("%s, in version range %q as a successor to currently installed version %q", notFoundMessage, successorRange, installedBundle.Version) + + verBundles := allBundles[:0] + verConstraint, err := masterminds.NewConstraint(successorRange) + if err != nil { + return nil, err + } + for _, bundle := range allBundles { + if verConstraint.Check(&bundle.Version) { + verBundles = append(verBundles, bundle) + } + } + if len(verBundles) == 0 { + vars = append(vars, input.NewSimpleVariable(opVarID, + constraint.Mandatory(), + constraint.NewUserFriendlyConstraint(constraint.Prohibited(), + func(constraint deppy.Constraint, subject deppy.Identifier) string { + return notFoundMessage + }, + )), + ) + continue + } + allBundles = verBundles + } + } + + if op.Spec.ClusterConstraintPolicy == operatorsv1alpha1.ClusterConstraintPolicyEnforce { + compatibleWithClusterBundles := allBundles[:0] + for _, b := range allBundles { + k8sVersionRange := "*" + minNodeCount := 0 + for _, p := range b.Properties { + switch p.Type { + case "olm.kubernetesVersionRange": + var v string + if err := json.Unmarshal(p.Value, &v); err != nil { + return nil, err + } + k8sVersionRange = v + case "olm.minimumNodeCount": + var v int + if err := json.Unmarshal(p.Value, &v); err != nil { + return nil, err + } + minNodeCount = v + } + } + clusterVersion, err := masterminds.StrictNewVersion(strings.TrimPrefix(cluster.VersionInfo.GitVersion, "v")) + if err != nil { + return nil, err + } + requiredRangeConstraint, err := masterminds.NewConstraint(k8sVersionRange) + if err != nil { + return nil, err + } + if requiredRangeConstraint.Check(clusterVersion) && minNodeCount <= len(cluster.Nodes) { + compatibleWithClusterBundles = append(compatibleWithClusterBundles, b) + } + } + notFoundMessage = fmt.Sprintf("%s, compatible with cluster version %q with %d nodes", notFoundMessage, cluster.VersionInfo.GitVersion, len(cluster.Nodes)) + if len(compatibleWithClusterBundles) == 0 { + vars = append(vars, input.NewSimpleVariable(opVarID, + constraint.Mandatory(), + constraint.NewUserFriendlyConstraint(constraint.Prohibited(), + func(constraint deppy.Constraint, subject deppy.Identifier) string { + return notFoundMessage + }, + )), + ) + continue + } + allBundles = compatibleWithClusterBundles + } + + opDependencies := make([]deppy.Identifier, 0, len(allBundles)) + for _, b := range allBundles { + bundleID := deppy.IdentifierFromString(fmt.Sprintf("Catalog/%s/%s/%s/%s", b.CatalogName, b.Schema, b.Package, b.Name)) + vars = append(vars, variables.NewBundle(bundleID, b)) + opDependencies = append(opDependencies, bundleID) + } + vars = append(vars, input.NewSimpleVariable(opVarID, constraint.Mandatory(), constraint.Dependency(opDependencies...))) + } + return vars, nil +} diff --git a/internal/resolution/variables/bundle.go b/internal/resolution/variables/bundle.go deleted file mode 100644 index dce8bed50..000000000 --- a/internal/resolution/variables/bundle.go +++ /dev/null @@ -1,58 +0,0 @@ -package variables - -import ( - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" -) - -var _ deppy.Variable = &BundleVariable{} - -type BundleVariable struct { - *input.SimpleVariable - bundleEntity *olmentity.BundleEntity - dependencies []*olmentity.BundleEntity -} - -func (b *BundleVariable) BundleEntity() *olmentity.BundleEntity { - return b.bundleEntity -} - -func (b *BundleVariable) Dependencies() []*olmentity.BundleEntity { - return b.dependencies -} - -func NewBundleVariable(bundleEntity *olmentity.BundleEntity, dependencyBundleEntities []*olmentity.BundleEntity) *BundleVariable { - dependencyIDs := make([]deppy.Identifier, 0, len(dependencyBundleEntities)) - for _, bundle := range dependencyBundleEntities { - dependencyIDs = append(dependencyIDs, bundle.ID) - } - var constraints []deppy.Constraint - if len(dependencyIDs) > 0 { - constraints = append(constraints, constraint.Dependency(dependencyIDs...)) - } - return &BundleVariable{ - SimpleVariable: input.NewSimpleVariable(bundleEntity.ID, constraints...), - bundleEntity: bundleEntity, - dependencies: dependencyBundleEntities, - } -} - -var _ deppy.Variable = &BundleUniquenessVariable{} - -type BundleUniquenessVariable struct { - *input.SimpleVariable -} - -// NewBundleUniquenessVariable creates a new variable that instructs the resolver to choose at most a single bundle -// from the input 'atMostID'. Examples: -// 1. restrict the solution to at most a single bundle per package -// 2. restrict the solution to at most a single bundler per provided gvk -// this guarantees that no two operators provide the same gvk and no two version of the same operator are running at the same time -func NewBundleUniquenessVariable(id deppy.Identifier, atMostIDs ...deppy.Identifier) *BundleUniquenessVariable { - return &BundleUniquenessVariable{ - SimpleVariable: input.NewSimpleVariable(id, constraint.AtMost(1, atMostIDs...)), - } -} diff --git a/internal/resolution/variables/bundle_test.go b/internal/resolution/variables/bundle_test.go deleted file mode 100644 index 506625642..000000000 --- a/internal/resolution/variables/bundle_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package variables_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ = Describe("BundleVariable", func() { - var ( - bv *olmvariables.BundleVariable - bundleEntity *olmentity.BundleEntity - dependencies []*olmentity.BundleEntity - ) - - BeforeEach(func() { - bundleEntity = olmentity.NewBundleEntity(input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })) - dependencies = []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-3", map[string]string{ - property.TypePackage: `{"packageName": "test-package-3", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - } - bv = olmvariables.NewBundleVariable(bundleEntity, dependencies) - }) - - It("should return the correct bundle entity", func() { - Expect(bv.BundleEntity()).To(Equal(bundleEntity)) - }) - - It("should return the correct dependencies", func() { - Expect(bv.Dependencies()).To(Equal(dependencies)) - }) -}) - -var _ = Describe("BundleUniquenessVariable", func() { - var ( - id deppy.Identifier - atMostIDs []deppy.Identifier - globalConstraintVariable *olmvariables.BundleUniquenessVariable - ) - - BeforeEach(func() { - id = deppy.IdentifierFromString("test-id") - atMostIDs = []deppy.Identifier{ - deppy.IdentifierFromString("test-at-most-id-1"), - deppy.IdentifierFromString("test-at-most-id-2"), - } - globalConstraintVariable = olmvariables.NewBundleUniquenessVariable(id, atMostIDs...) - }) - - It("should initialize a new global constraint variable", func() { - Expect(globalConstraintVariable.Identifier()).To(Equal(id)) - Expect(globalConstraintVariable.Constraints()).To(Equal([]deppy.Constraint{constraint.AtMost(1, atMostIDs...)})) - }) -}) diff --git a/internal/resolution/variables/installed_package.go b/internal/resolution/variables/installed_package.go deleted file mode 100644 index 0d176b81a..000000000 --- a/internal/resolution/variables/installed_package.go +++ /dev/null @@ -1,34 +0,0 @@ -package variables - -import ( - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" -) - -var _ deppy.Variable = &InstalledPackageVariable{} - -type InstalledPackageVariable struct { - *input.SimpleVariable - bundleEntities []*olmentity.BundleEntity -} - -func (r *InstalledPackageVariable) BundleEntities() []*olmentity.BundleEntity { - return r.bundleEntities -} - -func NewInstalledPackageVariable(packageName string, bundleEntities []*olmentity.BundleEntity) *InstalledPackageVariable { - id := deppy.IdentifierFromString(fmt.Sprintf("installed package %s", packageName)) - entityIDs := make([]deppy.Identifier, 0, len(bundleEntities)) - for _, bundle := range bundleEntities { - entityIDs = append(entityIDs, bundle.ID) - } - return &InstalledPackageVariable{ - SimpleVariable: input.NewSimpleVariable(id, constraint.Mandatory(), constraint.Dependency(entityIDs...)), - bundleEntities: bundleEntities, - } -} diff --git a/internal/resolution/variables/installed_package_test.go b/internal/resolution/variables/installed_package_test.go deleted file mode 100644 index e725b9dbd..000000000 --- a/internal/resolution/variables/installed_package_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package variables_test - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ = Describe("InstalledPackageVariable", func() { - var ( - ipv *olmvariables.InstalledPackageVariable - packageName string - bundleEntities []*olmentity.BundleEntity - ) - - BeforeEach(func() { - packageName = "test-package" - bundleEntities = []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-3", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - } - ipv = olmvariables.NewInstalledPackageVariable(packageName, bundleEntities) - }) - - It("should return the correct package name", func() { - Expect(ipv.Identifier()).To(Equal(deppy.IdentifierFromString(fmt.Sprintf("installed package %s", packageName)))) - }) - - It("should return the correct bundle entities", func() { - Expect(ipv.BundleEntities()).To(Equal(bundleEntities)) - }) -}) diff --git a/internal/resolution/variables/required_package.go b/internal/resolution/variables/required_package.go deleted file mode 100644 index d2916f5f3..000000000 --- a/internal/resolution/variables/required_package.go +++ /dev/null @@ -1,34 +0,0 @@ -package variables - -import ( - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/constraint" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" -) - -var _ deppy.Variable = &RequiredPackageVariable{} - -type RequiredPackageVariable struct { - *input.SimpleVariable - bundleEntities []*olmentity.BundleEntity -} - -func (r *RequiredPackageVariable) BundleEntities() []*olmentity.BundleEntity { - return r.bundleEntities -} - -func NewRequiredPackageVariable(packageName string, bundleEntities []*olmentity.BundleEntity) *RequiredPackageVariable { - id := deppy.IdentifierFromString(fmt.Sprintf("required package %s", packageName)) - entityIDs := make([]deppy.Identifier, 0, len(bundleEntities)) - for _, bundle := range bundleEntities { - entityIDs = append(entityIDs, bundle.ID) - } - return &RequiredPackageVariable{ - SimpleVariable: input.NewSimpleVariable(id, constraint.Mandatory(), constraint.Dependency(entityIDs...)), - bundleEntities: bundleEntities, - } -} diff --git a/internal/resolution/variables/required_package_test.go b/internal/resolution/variables/required_package_test.go deleted file mode 100644 index 3065ba81a..000000000 --- a/internal/resolution/variables/required_package_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package variables_test - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ = Describe("RequiredPackageVariable", func() { - var ( - rpv *olmvariables.RequiredPackageVariable - packageName string - bundleEntities []*olmentity.BundleEntity - ) - - BeforeEach(func() { - packageName = "test-package" - bundleEntities = []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-3", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - } - rpv = olmvariables.NewRequiredPackageVariable(packageName, bundleEntities) - }) - - It("should return the correct package name", func() { - Expect(rpv.Identifier()).To(Equal(deppy.IdentifierFromString(fmt.Sprintf("required package %s", packageName)))) - }) - - It("should return the correct bundle entities", func() { - Expect(rpv.BundleEntities()).To(Equal(bundleEntities)) - }) - - It("should contain both mandatory and dependency constraints", func() { - // TODO: add this test once https://github.com/operator-framework/deppy/pull/85 gets merged - // then we'll be able to inspect constraint types - }) -}) diff --git a/internal/resolution/variables/variables_test.go b/internal/resolution/variables/variables_test.go deleted file mode 100644 index e1b7a553d..000000000 --- a/internal/resolution/variables/variables_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package variables_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestVariableSources(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Variables Suite") -} diff --git a/internal/resolution/variablesources/bundle_deployment.go b/internal/resolution/variablesources/bundle_deployment.go deleted file mode 100644 index 4363d6bc2..000000000 --- a/internal/resolution/variablesources/bundle_deployment.go +++ /dev/null @@ -1,54 +0,0 @@ -package variablesources - -import ( - "context" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ input.VariableSource = &BundleDeploymentVariableSource{} - -type BundleDeploymentVariableSource struct { - client client.Client - inputVariableSource input.VariableSource -} - -func NewBundleDeploymentVariableSource(cl client.Client, inputVariableSource input.VariableSource) *BundleDeploymentVariableSource { - return &BundleDeploymentVariableSource{ - client: cl, - inputVariableSource: inputVariableSource, - } -} - -func (o *BundleDeploymentVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - variableSources := SliceVariableSource{} - if o.inputVariableSource != nil { - variableSources = append(variableSources, o.inputVariableSource) - } - - bundleDeployments := rukpakv1alpha1.BundleDeploymentList{} - if err := o.client.List(ctx, &bundleDeployments); err != nil { - return nil, err - } - - processed := map[string]struct{}{} - for _, bundleDeployment := range bundleDeployments.Items { - sourceImage := bundleDeployment.Spec.Template.Spec.Source.Image - if sourceImage != nil && sourceImage.Ref != "" { - if _, ok := processed[sourceImage.Ref]; ok { - continue - } - processed[sourceImage.Ref] = struct{}{} - ips, err := NewInstalledPackageVariableSource(bundleDeployment.Spec.Template.Spec.Source.Image.Ref) - if err != nil { - return nil, err - } - variableSources = append(variableSources, ips) - } - } - - return variableSources.GetVariables(ctx, entitySource) -} diff --git a/internal/resolution/variablesources/bundle_deployment_test.go b/internal/resolution/variablesources/bundle_deployment_test.go deleted file mode 100644 index 3eb82321a..000000000 --- a/internal/resolution/variablesources/bundle_deployment_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package variablesources_test - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" - - 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" - "github.com/operator-framework/deppy/pkg/deppy/input" - 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 BundleDeploymentTestEntityCache = map[deppy.Identifier]input.Entity{ - "operatorhub/prometheus/0.37.0": *input.NewEntity("operatorhub/prometheus/0.37.0", map[string]string{ - "olm.bundle.path": `"quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, - "olm.bundle.channelEntry": "{\"name\":\"prometheus.0.37.0\"}", - "olm.channel": "{\"channelName\":\"beta\",\"priority\":0,\"replaces\":\"prometheusoperator.0.32.0\"}", - "olm.gvk": "[{\"group\":\"monitoring.coreos.com\",\"kind\":\"Alertmanager\",\"version\":\"v1\"}, {\"group\":\"monitoring.coreos.com\",\"kind\":\"Prometheus\",\"version\":\"v1\"}]", - "olm.package": "{\"packageName\":\"prometheus\",\"version\":\"0.37.0\"}", - }), - "operatorhub/prometheus/0.47.0": *input.NewEntity("operatorhub/prometheus/0.47.0", map[string]string{ - "olm.bundle.path": `"quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed"`, - "olm.bundle.channelEntry": "{\"replaces\":\"prometheus.0.37.0\", \"name\":\"prometheus.0.47.0\"}", - "olm.channel": "{\"channelName\":\"beta\",\"priority\":0,\"replaces\":\"prometheusoperator.0.37.0\"}", - "olm.gvk": "[{\"group\":\"monitoring.coreos.com\",\"kind\":\"Alertmanager\",\"version\":\"v1\"}, {\"group\":\"monitoring.coreos.com\",\"kind\":\"Prometheus\",\"version\":\"v1alpha1\"}]", - "olm.package": "{\"packageName\":\"prometheus\",\"version\":\"0.47.0\"}", - }), - "operatorhub/packageA/2.0.0": *input.NewEntity("operatorhub/packageA/2.0.0", map[string]string{ - "olm.bundle.path": `"foo.io/packageA/packageA:v2.0.0"`, - "olm.bundle.channelEntry": "{\"name\":\"packageA.2.0.0\"}", - "olm.channel": "{\"channelName\":\"stable\",\"priority\":0}", - "olm.gvk": "[{\"group\":\"foo.io\",\"kind\":\"Foo\",\"version\":\"v1\"}]", - "olm.package": "{\"packageName\":\"packageA\",\"version\":\"2.0.0\"}", - }), -} - -var _ = Describe("BundleDeploymentVariableSource", func() { - var bundleTestEntityCache input.EntitySource - - BeforeEach(func() { - bundleTestEntityCache = input.NewCacheQuerier(BundleDeploymentTestEntityCache) - }) - - It("should produce RequiredPackage variables", func() { - cl := BundleDeploymentFakeClient(bundleDeployment("prometheus", "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35")) - - bdVariableSource := variablesources.NewBundleDeploymentVariableSource(cl, &MockRequiredPackageSource{}) - variables, err := bdVariableSource.GetVariables(context.Background(), bundleTestEntityCache) - 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.BundleEntities()) - } - 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, &MockRequiredPackageSource{}) - _, err := bdVariableSource.GetVariables(context.Background(), bundleTestEntityCache) - Expect(err.Error()).To(Equal("bundleImage \"quay.io/operatorhubio/prometheus@sha256:nonexistent\" not found")) - }) -}) diff --git a/internal/resolution/variablesources/bundles_and_dependencies.go b/internal/resolution/variablesources/bundles_and_dependencies.go deleted file mode 100644 index 3e46a2027..000000000 --- a/internal/resolution/variablesources/bundles_and_dependencies.go +++ /dev/null @@ -1,137 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - "sort" - - "github.com/blang/semver/v4" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - "github.com/operator-framework/operator-controller/internal/resolution/util/predicates" - entitysort "github.com/operator-framework/operator-controller/internal/resolution/util/sort" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ input.VariableSource = &BundlesAndDepsVariableSource{} - -type BundlesAndDepsVariableSource struct { - variableSources []input.VariableSource -} - -func NewBundlesAndDepsVariableSource(inputVariableSources ...input.VariableSource) *BundlesAndDepsVariableSource { - return &BundlesAndDepsVariableSource{ - variableSources: inputVariableSources, - } -} - -func (b *BundlesAndDepsVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - var variables []deppy.Variable - - // extract required package variables - for _, variableSource := range b.variableSources { - inputVariables, err := variableSource.GetVariables(ctx, entitySource) - if err != nil { - return nil, err - } - variables = append(variables, inputVariables...) - } - - // create bundle queue for dependency resolution - var bundleEntityQueue []*olmentity.BundleEntity - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.RequiredPackageVariable: - bundleEntityQueue = append(bundleEntityQueue, v.BundleEntities()...) - case *olmvariables.InstalledPackageVariable: - bundleEntityQueue = append(bundleEntityQueue, v.BundleEntities()...) - } - } - - // build bundle and dependency variables - visited := map[deppy.Identifier]struct{}{} - for len(bundleEntityQueue) > 0 { - // pop head of queue - var head *olmentity.BundleEntity - head, bundleEntityQueue = bundleEntityQueue[0], bundleEntityQueue[1:] - - // ignore bundles that have already been processed - if _, ok := visited[head.ID]; ok { - continue - } - visited[head.ID] = struct{}{} - - // get bundle dependencies - dependencyEntityBundles, err := b.getEntityDependencies(ctx, head, entitySource) - if err != nil { - return nil, fmt.Errorf("could not determine dependencies for entity with id '%s': %w", head.ID, err) - } - - // add bundle dependencies to queue for processing - bundleEntityQueue = append(bundleEntityQueue, dependencyEntityBundles...) - - // create variable - variables = append(variables, olmvariables.NewBundleVariable(head, dependencyEntityBundles)) - } - - return variables, nil -} - -func (b *BundlesAndDepsVariableSource) getEntityDependencies(ctx context.Context, bundleEntity *olmentity.BundleEntity, entitySource input.EntitySource) ([]*olmentity.BundleEntity, error) { - var dependencies []*olmentity.BundleEntity - added := map[deppy.Identifier]struct{}{} - - // gather required package dependencies - // todo(perdasilva): disambiguate between not found and actual errors - requiredPackages, _ := bundleEntity.RequiredPackages() - for _, requiredPackage := range requiredPackages { - semverRange, err := semver.ParseRange(requiredPackage.VersionRange) - if err != nil { - return nil, err - } - packageDependencyBundles, err := entitySource.Filter(ctx, input.And(predicates.WithPackageName(requiredPackage.PackageName), predicates.InSemverRange(semverRange))) - if err != nil { - return nil, err - } - if len(packageDependencyBundles) == 0 { - return nil, fmt.Errorf("could not find package dependencies for bundle '%s'", bundleEntity.ID) - } - for i := 0; i < len(packageDependencyBundles); i++ { - entity := packageDependencyBundles[i] - if _, ok := added[entity.ID]; !ok { - dependencies = append(dependencies, olmentity.NewBundleEntity(&entity)) - added[entity.ID] = struct{}{} - } - } - } - - // gather required gvk dependencies - // todo(perdasilva): disambiguate between not found and actual errors - gvkDependencies, _ := bundleEntity.RequiredGVKs() - for i := 0; i < len(gvkDependencies); i++ { - providedGvk := gvkDependencies[i].AsGVK() - gvkDependencyBundles, err := entitySource.Filter(ctx, predicates.ProvidesGVK(&providedGvk)) - if err != nil { - return nil, err - } - if len(gvkDependencyBundles) == 0 { - return nil, fmt.Errorf("could not find gvk dependencies for bundle '%s'", bundleEntity.ID) - } - for i := 0; i < len(gvkDependencyBundles); i++ { - entity := gvkDependencyBundles[i] - if _, ok := added[entity.ID]; !ok { - dependencies = append(dependencies, olmentity.NewBundleEntity(&entity)) - added[entity.ID] = struct{}{} - } - } - } - - // sort bundles in version order - sort.SliceStable(dependencies, func(i, j int) bool { - return entitysort.ByChannelAndVersion(dependencies[i].Entity, dependencies[j].Entity) - }) - - return dependencies, nil -} diff --git a/internal/resolution/variablesources/bundles_and_dependencies_test.go b/internal/resolution/variablesources/bundles_and_dependencies_test.go deleted file mode 100644 index 472369893..000000000 --- a/internal/resolution/variablesources/bundles_and_dependencies_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package variablesources_test - -import ( - "context" - "errors" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -var _ = Describe("BundlesAndDepsVariableSource", func() { - var ( - bdvs *variablesources.BundlesAndDepsVariableSource - mockEntitySource input.EntitySource - ) - - BeforeEach(func() { - bdvs = variablesources.NewBundlesAndDepsVariableSource( - &MockRequiredPackageSource{ - ResultSet: []deppy.Variable{ - // must match data in mockEntitySource - olmvariables.NewRequiredPackageVariable("test-package", []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVKRequired: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - property.TypePackageRequired: `[{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}]`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVKRequired: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - })), - }), - }, - }, - &MockRequiredPackageSource{ - ResultSet: []deppy.Variable{ - // must match data in mockEntitySource - olmvariables.NewRequiredPackageVariable("test-package-2", []*olmentity.BundleEntity{ - // test-package-2 required package - no dependencies - olmentity.NewBundleEntity(input.NewEntity("bundle-15", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-16", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "2.0.1"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-17", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "3.16.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - }), - }, - }, - ) - mockEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{ - // required package bundles - "bundle-1": *input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVKRequired: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - }), - "bundle-2": *input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVKRequired: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - property.TypePackageRequired: `[{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}]`, - }), - - // dependencies - "bundle-4": *input.NewEntity("bundle-4", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-5": *input.NewEntity("bundle-5", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-6": *input.NewEntity("bundle-6", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-7": *input.NewEntity("bundle-7", map[string]string{ - property.TypePackage: `{"packageName": "some-other-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - }), - "bundle-8": *input.NewEntity("bundle-8", map[string]string{ - property.TypePackage: `{"packageName": "some-other-package", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - property.TypeGVKRequired: `[{"group":"bar.io","kind":"Bar","version":"v1"}]`, - property.TypePackageRequired: `[{"packageName": "another-package", "versionRange": "< 2.0.0"}]`, - }), - - // dependencies of dependencies - "bundle-9": *input.NewEntity("bundle-9", map[string]string{ - property.TypePackage: `{"packageName": "another-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - }), - "bundle-10": *input.NewEntity("bundle-10", map[string]string{ - property.TypePackage: `{"packageName": "bar-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"bar.io","kind":"Bar","version":"v1"}]`, - }), - "bundle-11": *input.NewEntity("bundle-11", map[string]string{ - property.TypePackage: `{"packageName": "bar-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"bar.io","kind":"Bar","version":"v1"}]`, - }), - - // test-package-2 required package - no dependencies - "bundle-15": *input.NewEntity("bundle-15", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-16": *input.NewEntity("bundle-16", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "2.0.1"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-17": *input.NewEntity("bundle-17", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "3.16.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - - // completely unrelated - "bundle-12": *input.NewEntity("bundle-12", map[string]string{ - property.TypePackage: `{"packageName": "unrelated-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-13": *input.NewEntity("bundle-13", map[string]string{ - property.TypePackage: `{"packageName": "unrelated-package-2", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-14": *input.NewEntity("bundle-14", map[string]string{ - property.TypePackage: `{"packageName": "unrelated-package-2", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - }) - }) - - It("should return bundle variables with correct dependencies", func() { - variables, err := bdvs.GetVariables(context.TODO(), mockEntitySource) - Expect(err).NotTo(HaveOccurred()) - - var bundleVariables []*olmvariables.BundleVariable - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.BundleVariable: - bundleVariables = append(bundleVariables, v) - } - } - Expect(bundleVariables).To(WithTransform(CollectBundleVariableIDs, Equal([]string{"bundle-2", "bundle-1", "bundle-15", "bundle-16", "bundle-17", "bundle-9", "bundle-8", "bundle-7", "bundle-5", "bundle-4", "bundle-11", "bundle-10"}))) - - // check dependencies for one of the bundles - bundle2 := VariableWithID("bundle-2")(bundleVariables) - Expect(bundle2.Dependencies()).To(WithTransform(CollectDeppyEntities, Equal([]*input.Entity{ - input.NewEntity("bundle-9", map[string]string{ - property.TypePackage: `{"packageName": "another-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - }), - input.NewEntity("bundle-8", map[string]string{ - property.TypePackage: `{"packageName": "some-other-package", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - property.TypeGVKRequired: `[{"group":"bar.io","kind":"Bar","version":"v1"}]`, - property.TypePackageRequired: `[{"packageName": "another-package", "versionRange": "< 2.0.0"}]`, - }), - input.NewEntity("bundle-7", map[string]string{ - property.TypePackage: `{"packageName": "some-other-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - }), - input.NewEntity("bundle-5", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - input.NewEntity("bundle-4", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - }))) - }) - - It("should return error if dependencies not found", func() { - mockEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{}) - _, err := bdvs.GetVariables(context.TODO(), mockEntitySource) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("could not determine dependencies for entity with id '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( - &MockRequiredPackageSource{Error: errors.New("fake error")}, - ) - mockEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{}) - _, err := bdvs.GetVariables(context.TODO(), mockEntitySource) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("fake error")) - }) -}) - -type MockRequiredPackageSource struct { - ResultSet []deppy.Variable - Error error -} - -func (m *MockRequiredPackageSource) GetVariables(_ context.Context, _ input.EntitySource) ([]deppy.Variable, error) { - return m.ResultSet, m.Error -} - -func VariableWithID(id deppy.Identifier) func(vars []*olmvariables.BundleVariable) *olmvariables.BundleVariable { - return func(vars []*olmvariables.BundleVariable) *olmvariables.BundleVariable { - for i := 0; i < len(vars); i++ { - if vars[i].Identifier() == id { - 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 -} - -func CollectDeppyEntities(vars []*olmentity.BundleEntity) []*input.Entity { - entities := make([]*input.Entity, 0, len(vars)) - for _, v := range vars { - entities = append(entities, v.Entity) - } - return entities -} diff --git a/internal/resolution/variablesources/composite.go b/internal/resolution/variablesources/composite.go deleted file mode 100644 index b66bf75ef..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, entitySource input.EntitySource) ([]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, entitySource) -} - -type SliceVariableSource []input.VariableSource - -func (s SliceVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - var variables []deppy.Variable - for _, variableSource := range s { - inputVariables, err := variableSource.GetVariables(ctx, entitySource) - 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 18a9acbd0..000000000 --- a/internal/resolution/variablesources/composite_test.go +++ /dev/null @@ -1,175 +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() - mockEntitySource := input.NewCacheQuerier(map[deppy.Identifier]input.Entity{}) - - 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, mockEntitySource) - 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() - mockEntitySource := input.NewCacheQuerier(map[deppy.Identifier]input.Entity{}) - - nestedSource := variablesources.NestedVariableSource{ - func(inputVariableSource input.VariableSource) (input.VariableSource, error) { - return nil, errors.New("fake error from a constructor") - }, - } - - variables, err := nestedSource.GetVariables(ctx, mockEntitySource) - 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() - mockEntitySource := input.NewCacheQuerier(map[deppy.Identifier]input.Entity{}) - - sliceSource := variablesources.SliceVariableSource(tt.varSources) - variables, err := sliceSource.GetVariables(ctx, mockEntitySource) - 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, entitySource input.EntitySource) ([]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, entitySource) - 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 798fca211..000000000 --- a/internal/resolution/variablesources/crd_constraints.go +++ /dev/null @@ -1,101 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - 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 its entitySource. 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, entitySource input.EntitySource) ([]deppy.Variable, error) { - variables, err := g.inputVariableSource.GetVariables(ctx, entitySource) - if err != nil { - return nil, err - } - - // todo(perdasilva): better handle cases where a provided gvk is not found - // not all packages will necessarily export a CRD - - pkgToBundleMap := map[string]map[deppy.Identifier]struct{}{} - gvkToBundleMap := map[string]map[deppy.Identifier]struct{}{} - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.BundleVariable: - bundleEntities := []*olmentity.BundleEntity{v.BundleEntity()} - bundleEntities = append(bundleEntities, v.Dependencies()...) - for _, bundleEntity := range bundleEntities { - // get bundleID package and update map - packageName, err := bundleEntity.PackageName() - if err != nil { - return nil, fmt.Errorf("error creating global constraints: %w", err) - } - - if _, ok := pkgToBundleMap[packageName]; !ok { - pkgToBundleMap[packageName] = map[deppy.Identifier]struct{}{} - } - pkgToBundleMap[packageName][bundleEntity.ID] = struct{}{} - - // get bundleID gvks and update map - exportedGVKs, err := bundleEntity.ProvidedGVKs() - if err != nil { - return nil, fmt.Errorf("error creating global constraints: %w", err) - } - for i := 0; i < len(exportedGVKs); i++ { - gvk := exportedGVKs[i].String() - if _, ok := gvkToBundleMap[gvk]; !ok { - gvkToBundleMap[gvk] = map[deppy.Identifier]struct{}{} - } - gvkToBundleMap[gvk][bundleEntity.ID] = struct{}{} - } - } - } - } - - // create global constraint variables - for packageName, bundleIDMap := range pkgToBundleMap { - var bundleIDs []deppy.Identifier - for bundleID := range bundleIDMap { - bundleIDs = append(bundleIDs, bundleID) - } - varID := deppy.IdentifierFromString(fmt.Sprintf("%s package uniqueness", packageName)) - variables = append(variables, olmvariables.NewBundleUniquenessVariable(varID, bundleIDs...)) - } - - for gvk, bundleIDMap := range gvkToBundleMap { - var bundleIDs []deppy.Identifier - for bundleID := range bundleIDMap { - bundleIDs = append(bundleIDs, bundleID) - } - varID := deppy.IdentifierFromString(fmt.Sprintf("%s gvk uniqueness", gvk)) - variables = append(variables, olmvariables.NewBundleUniquenessVariable(varID, bundleIDs...)) - } - - return variables, nil -} diff --git a/internal/resolution/variablesources/crd_constraints_test.go b/internal/resolution/variablesources/crd_constraints_test.go deleted file mode 100644 index bb456eae0..000000000 --- a/internal/resolution/variablesources/crd_constraints_test.go +++ /dev/null @@ -1,283 +0,0 @@ -package variablesources_test - -import ( - "context" - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -var bundleSet = map[deppy.Identifier]*input.Entity{ - // required package bundles - "bundle-1": input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVKRequired: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - property.TypeGVK: `[{"group":"bit.io","kind":"Bit","version":"v1"}]`, - }), - "bundle-2": input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVKRequired: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - property.TypePackageRequired: `[{"packageName": "some-package", "versionRange": ">=1.0.0 <2.0.0"}]`, - property.TypeGVK: `[{"group":"bit.io","kind":"Bit","version":"v1"}]`, - }), - - // dependencies - "bundle-3": input.NewEntity("bundle-3", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"fiz.io","kind":"Fiz","version":"v1"}]`, - }), - "bundle-4": input.NewEntity("bundle-4", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"fiz.io","kind":"Fiz","version":"v1"}]`, - }), - "bundle-5": input.NewEntity("bundle-5", map[string]string{ - property.TypePackage: `{"packageName": "some-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"fiz.io","kind":"Fiz","version":"v1"}]`, - }), - "bundle-6": input.NewEntity("bundle-6", map[string]string{ - property.TypePackage: `{"packageName": "some-other-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - }), - "bundle-7": input.NewEntity("bundle-7", map[string]string{ - property.TypePackage: `{"packageName": "some-other-package", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - property.TypeGVKRequired: `[{"group":"bar.io","kind":"Bar","version":"v1"}]`, - property.TypePackageRequired: `[{"packageName": "another-package", "versionRange": "< 2.0.0"}]`, - }), - - // dependencies of dependencies - "bundle-8": input.NewEntity("bundle-8", map[string]string{ - property.TypePackage: `{"packageName": "another-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"foo.io","kind":"Foo","version":"v1"}]`, - }), - "bundle-9": input.NewEntity("bundle-9", map[string]string{ - property.TypePackage: `{"packageName": "bar-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"bar.io","kind":"Bar","version":"v1"}]`, - }), - "bundle-10": input.NewEntity("bundle-10", map[string]string{ - property.TypePackage: `{"packageName": "bar-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"bar.io","kind":"Bar","version":"v1"}]`, - }), - - // test-package-2 required package - no dependencies - "bundle-14": input.NewEntity("bundle-14", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "1.5.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"buz.io","kind":"Buz","version":"v1"}]`, - }), - "bundle-15": input.NewEntity("bundle-15", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "2.0.1"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"buz.io","kind":"Buz","version":"v1"}]`, - }), - "bundle-16": input.NewEntity("bundle-16", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "3.16.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"buz.io","kind":"Buz","version":"v1"}]`, - }), - - // completely unrelated - "bundle-11": input.NewEntity("bundle-11", map[string]string{ - property.TypePackage: `{"packageName": "unrelated-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"buz.io","kind":"Buz","version":"v1alpha1"}]`, - }), - "bundle-12": input.NewEntity("bundle-12", map[string]string{ - property.TypePackage: `{"packageName": "unrelated-package-2", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"buz.io","kind":"Buz","version":"v1alpha1"}]`, - }), - "bundle-13": input.NewEntity("bundle-13", map[string]string{ - property.TypePackage: `{"packageName": "unrelated-package-2", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - property.TypeGVK: `[{"group":"buz.io","kind":"Buz","version":"v1alpha1"}]`, - }), -} - -var _ = Describe("CRDUniquenessConstraintsVariableSource", func() { - var ( - inputVariableSource *MockInputVariableSource - crdConstraintVariableSource *variablesources.CRDUniquenessConstraintsVariableSource - ctx context.Context - entitySource input.EntitySource - ) - - BeforeEach(func() { - inputVariableSource = &MockInputVariableSource{} - crdConstraintVariableSource = variablesources.NewCRDUniquenessConstraintsVariableSource(inputVariableSource) - ctx = context.Background() - - // the entity is not used in this variable source - entitySource = &PanicEntitySource{} - }) - - It("should get variables from the input variable source and create global constraint variables", func() { - inputVariableSource.ResultSet = []deppy.Variable{ - olmvariables.NewRequiredPackageVariable("test-package", []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(bundleSet["bundle-2"]), - olmentity.NewBundleEntity(bundleSet["bundle-1"]), - }), - olmvariables.NewRequiredPackageVariable("test-package-2", []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(bundleSet["bundle-14"]), - olmentity.NewBundleEntity(bundleSet["bundle-15"]), - olmentity.NewBundleEntity(bundleSet["bundle-16"]), - }), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-2"]), - []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(bundleSet["bundle-3"]), - olmentity.NewBundleEntity(bundleSet["bundle-4"]), - olmentity.NewBundleEntity(bundleSet["bundle-5"]), - olmentity.NewBundleEntity(bundleSet["bundle-6"]), - olmentity.NewBundleEntity(bundleSet["bundle-7"]), - }, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-1"]), - []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(bundleSet["bundle-6"]), - olmentity.NewBundleEntity(bundleSet["bundle-7"]), - olmentity.NewBundleEntity(bundleSet["bundle-8"]), - }, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-3"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-4"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-5"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-6"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-7"]), - []*olmentity.BundleEntity{ - olmentity.NewBundleEntity(bundleSet["bundle-8"]), - olmentity.NewBundleEntity(bundleSet["bundle-9"]), - olmentity.NewBundleEntity(bundleSet["bundle-10"]), - }, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-8"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-9"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-10"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-14"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-15"]), - []*olmentity.BundleEntity{}, - ), - olmvariables.NewBundleVariable( - olmentity.NewBundleEntity(bundleSet["bundle-16"]), - []*olmentity.BundleEntity{}, - ), - } - variables, err := crdConstraintVariableSource.GetVariables(ctx, entitySource) - Expect(err).ToNot(HaveOccurred()) - Expect(variables).To(HaveLen(26)) - var crdConstraintVariables []*olmvariables.BundleUniquenessVariable - for _, variable := range variables { - switch v := variable.(type) { - case *olmvariables.BundleUniquenessVariable: - crdConstraintVariables = append(crdConstraintVariables, v) - } - } - Expect(crdConstraintVariables).To(WithTransform(CollectGlobalConstraintVariableIDs, ConsistOf([]string{ - "another-package package uniqueness", - "bar-package package uniqueness", - "test-package-2 package uniqueness", - "test-package package uniqueness", - "some-package package uniqueness", - "some-other-package package uniqueness", - `group:"buz.io" version:"v1" kind:"Buz" gvk uniqueness`, - `group:"bit.io" version:"v1" kind:"Bit" gvk uniqueness`, - `group:"fiz.io" version:"v1" kind:"Fiz" gvk uniqueness`, - `group:"foo.io" version:"v1" kind:"Foo" gvk uniqueness`, - `group:"bar.io" version:"v1" kind:"Bar" gvk 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, entitySource) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("error getting variables")) - }) -}) - -var _ input.EntitySource = &PanicEntitySource{} - -type PanicEntitySource struct{} - -func (p PanicEntitySource) Get(_ context.Context, _ deppy.Identifier) (*input.Entity, error) { - return nil, fmt.Errorf("if you are seeing this it is because the global variable source is calling the entity source - this shouldn't happen") -} - -func (p PanicEntitySource) Filter(_ context.Context, _ input.Predicate) (input.EntityList, error) { - return nil, fmt.Errorf("if you are seeing this it is because the global variable source is calling the entity source - this shouldn't happen") -} - -func (p PanicEntitySource) GroupBy(_ context.Context, _ input.GroupByFunction) (input.EntityListMap, error) { - return nil, fmt.Errorf("if you are seeing this it is because the global variable source is calling the entity source - this shouldn't happen") -} - -func (p PanicEntitySource) Iterate(_ context.Context, _ input.IteratorFunction) error { - return fmt.Errorf("if you are seeing this it is because the global variable source is calling the entity source - this shouldn't happen") -} - -type MockInputVariableSource struct { - ResultSet []deppy.Variable - Err error -} - -func (m *MockInputVariableSource) GetVariables(_ context.Context, _ input.EntitySource) ([]deppy.Variable, error) { - if m.Err != nil { - return nil, m.Err - } - return m.ResultSet, nil -} - -func CollectGlobalConstraintVariableIDs(vars []*olmvariables.BundleUniquenessVariable) []string { - ids := make([]string, 0, len(vars)) - for _, v := range vars { - ids = append(ids, v.Identifier().String()) - } - return ids -} diff --git a/internal/resolution/variablesources/installed_package.go b/internal/resolution/variablesources/installed_package.go deleted file mode 100644 index a36256f1d..000000000 --- a/internal/resolution/variablesources/installed_package.go +++ /dev/null @@ -1,84 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - "github.com/operator-framework/operator-controller/internal/resolution/util/predicates" - "github.com/operator-framework/operator-controller/internal/resolution/util/sort" - "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ input.VariableSource = &InstalledPackageVariableSource{} - -type InstalledPackageVariableSource struct { - bundleImage string -} - -func (r *InstalledPackageVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - // find corresponding bundle entity for the installed content - resultSet, err := entitySource.Filter(ctx, predicates.WithBundleImage(r.bundleImage)) - if err != nil { - return nil, err - } - if len(resultSet) == 0 { - return nil, r.notFoundError() - } - - // sort by channel and version - // TODO: this is a bit of a hack and it assumes a well formed catalog. - // we currently have one entity per bundle/channel, i.e. if a bundle - // appears in multiple channels, we have multiple entities for it. - // this means that for a well formed catalog, we could get multiple entities - // back as a response to the filter above. For now, we sort by channel and version - // and take the top most element. Soon, we will add package and channel variables making - // this unnecessary. - // 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. - resultSet = resultSet.Sort(sort.ByChannelAndVersion) - installedBundle := olmentity.NewBundleEntity(&resultSet[0]) - - // now find the bundles that replace the installed bundle - // TODO: this algorithm does not yet consider skips and skipRange - // we simplify the process here by just searching for the bundle that replaces the installed bundle - packageName, err := installedBundle.PackageName() - if err != nil { - return nil, err - } - - channelEntry, err := installedBundle.BundleChannelEntry() - if err != nil { - return nil, err - } - - resultSet, err = entitySource.Filter(ctx, predicates.Replaces(channelEntry.Name)) - if err != nil { - return nil, err - } - resultSet = resultSet.Sort(sort.ByChannelAndVersion) - upgradeEdges := make([]*olmentity.BundleEntity, 0, len(resultSet)) - for i := range resultSet { - upgradeEdges = append(upgradeEdges, olmentity.NewBundleEntity(&resultSet[i])) - } - - // you can always upgrade to yourself, i.e. not upgrade - upgradeEdges = append(upgradeEdges, installedBundle) - return []deppy.Variable{ - variables.NewInstalledPackageVariable(packageName, upgradeEdges), - }, nil -} - -func (r *InstalledPackageVariableSource) notFoundError() error { - return fmt.Errorf("bundleImage %q not found", r.bundleImage) -} - -func NewInstalledPackageVariableSource(bundleImage string) (*InstalledPackageVariableSource, error) { - return &InstalledPackageVariableSource{ - bundleImage: bundleImage, - }, nil -} diff --git a/internal/resolution/variablesources/installed_package_test.go b/internal/resolution/variablesources/installed_package_test.go deleted file mode 100644 index 5581039a4..000000000 --- a/internal/resolution/variablesources/installed_package_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package variablesources_test - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -var _ = Describe("InstalledPackageVariableSource", func() { - var ( - ipvs *variablesources.InstalledPackageVariableSource - bundleImage string - mockEntitySource input.EntitySource - ) - - BeforeEach(func() { - var err error - bundleImage = "registry.io/repo/test-package@v2.0.0" - ipvs, err = variablesources.NewInstalledPackageVariableSource(bundleImage) - Expect(err).NotTo(HaveOccurred()) - - mockEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{ - "test-package.v1.0.0": *input.NewEntity("test-package.v1.0.0test-packagestable", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - olmentity.PropertyBundlePath: `"registry.io/repo/test-package@v1.0.0"`, - olmentity.PropertyBundleChannelEntry: `{"name": "test-package.v1.0.0"}`, - }), - "test-package.v3.0.0": *input.NewEntity("test-package.v3.0.0test-packagestable", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - olmentity.PropertyBundlePath: `"registry.io/repo/test-package@v3.0.0"`, - olmentity.PropertyBundleChannelEntry: `{"name": "test-package.v3.0.0","replaces": "test-package.v2.0.0"}`, - }), - "test-package.v2.0.0": *input.NewEntity("test-package.v2.0.0test-packagestable", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - olmentity.PropertyBundlePath: `"registry.io/repo/test-package@v2.0.0"`, - olmentity.PropertyBundleChannelEntry: `{"name": "test-package.v2.0.0","replaces": "test-package.v1.0.0"}`, - }), - "test-package.4.0.0": *input.NewEntity("test-package.v4.0.0test-packagestable", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "4.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - olmentity.PropertyBundlePath: `"registry.io/repo/test-package@v4.0.0"`, - olmentity.PropertyBundleChannelEntry: `{"name": "test-package.v4.0.0","replaces": "test-package.v3.0.0"}`, - }), - "test-package.5.0.0": *input.NewEntity("test-package.v5.0.0test-packagestable", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "5-00"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - olmentity.PropertyBundlePath: `"registry.io/repo/test-package@v5.0.0"`, - olmentity.PropertyBundleChannelEntry: `{"name": "test-package.v5.0.0","replaces": "test-package.v4.0.0"}`, - }), - }) - }) - - It("should return the correct package variable", func() { - variables, err := ipvs.GetVariables(context.TODO(), mockEntitySource) - Expect(err).NotTo(HaveOccurred()) - Expect(variables).To(HaveLen(1)) - reqPackageVar, ok := variables[0].(*olmvariables.InstalledPackageVariable) - Expect(ok).To(BeTrue()) - Expect(reqPackageVar.Identifier()).To(Equal(deppy.IdentifierFromString("installed package test-package"))) - - // ensure bundle entities are in version order (high to low) - Expect(reqPackageVar.BundleEntities()[0].ID).To(Equal(deppy.IdentifierFromString("test-package.v3.0.0test-packagestable"))) - Expect(reqPackageVar.BundleEntities()[1].ID).To(Equal(deppy.IdentifierFromString("test-package.v2.0.0test-packagestable"))) - }) -}) diff --git a/internal/resolution/variablesources/operator.go b/internal/resolution/variablesources/operator.go deleted file mode 100644 index a827cd273..000000000 --- a/internal/resolution/variablesources/operator.go +++ /dev/null @@ -1,52 +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 - inputVariableSource input.VariableSource -} - -func NewOperatorVariableSource(cl client.Client, inputVariableSource input.VariableSource) *OperatorVariableSource { - return &OperatorVariableSource{ - client: cl, - inputVariableSource: inputVariableSource, - } -} - -func (o *OperatorVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]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( - 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, entitySource) -} diff --git a/internal/resolution/variablesources/operator_test.go b/internal/resolution/variablesources/operator_test.go deleted file mode 100644 index 0920b4961..000000000 --- a/internal/resolution/variablesources/operator_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package variablesources_test - -import ( - "context" - "fmt" - - . "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" - - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - 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 FakeClient(objects ...client.Object) client.Client { - scheme := runtime.NewScheme() - utilruntime.Must(operatorsv1alpha1.AddToScheme(scheme)) - return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() -} - -var testEntityCache = map[deppy.Identifier]input.Entity{ - "operatorhub/prometheus/0.37.0": *input.NewEntity("operatorhub/prometheus/0.37.0", map[string]string{ - "olm.bundle.path": `"quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, - "olm.channel": "{\"channelName\":\"beta\",\"priority\":0,\"replaces\":\"prometheusoperator.0.32.0\"}", - "olm.gvk": "[{\"group\":\"monitoring.coreos.com\",\"kind\":\"Alertmanager\",\"version\":\"v1\"}, {\"group\":\"monitoring.coreos.com\",\"kind\":\"Prometheus\",\"version\":\"v1\"}]", - "olm.package": "{\"packageName\":\"prometheus\",\"version\":\"0.37.0\"}", - }), - "operatorhub/prometheus/0.47.0": *input.NewEntity("operatorhub/prometheus/0.47.0", map[string]string{ - "olm.bundle.path": `"quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed"`, - "olm.channel": "{\"channelName\":\"beta\",\"priority\":0,\"replaces\":\"prometheusoperator.0.37.0\"}", - "olm.gvk": "[{\"group\":\"monitoring.coreos.com\",\"kind\":\"Alertmanager\",\"version\":\"v1\"}, {\"group\":\"monitoring.coreos.com\",\"kind\":\"Prometheus\",\"version\":\"v1alpha1\"}]", - "olm.package": "{\"packageName\":\"prometheus\",\"version\":\"0.47.0\"}", - }), - "operatorhub/packageA/2.0.0": *input.NewEntity("operatorhub/packageA/2.0.0", map[string]string{ - "olm.bundle.path": `"foo.io/packageA/packageA:v2.0.0"`, - "olm.channel": "{\"channelName\":\"stable\",\"priority\":0}", - "olm.gvk": "[{\"group\":\"foo.io\",\"kind\":\"Foo\",\"version\":\"v1\"}]", - "olm.package": "{\"packageName\":\"packageA\",\"version\":\"2.0.0\"}", - }), -} - -func operator(name string) *operatorsv1alpha1.Operator { - return &operatorsv1alpha1.Operator{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: operatorsv1alpha1.OperatorSpec{ - PackageName: name, - }, - } -} - -var _ = Describe("OperatorVariableSource", func() { - var testEntitySource input.EntitySource - - BeforeEach(func() { - testEntitySource = input.NewCacheQuerier(testEntityCache) - }) - - It("should produce RequiredPackage variables", func() { - cl := FakeClient(operator("prometheus"), operator("packageA")) - - opVariableSource := variablesources.NewOperatorVariableSource(cl, &MockRequiredPackageSource{}) - variables, err := opVariableSource.GetVariables(context.Background(), testEntitySource) - 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.BundleEntities()) - } - 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")) - - opVariableSource := variablesources.NewOperatorVariableSource(cl, nil) - _, err := opVariableSource.GetVariables(context.Background(), FailEntitySource{}) - Expect(err).To(HaveOccurred()) - }) -}) - -var _ input.EntitySource = &FailEntitySource{} - -type FailEntitySource struct { -} - -func (f FailEntitySource) Get(_ context.Context, _ deppy.Identifier) (*input.Entity, error) { - return nil, fmt.Errorf("error executing get") -} - -func (f FailEntitySource) Filter(_ context.Context, _ input.Predicate) (input.EntityList, error) { - return nil, fmt.Errorf("error executing filter") -} - -func (f FailEntitySource) GroupBy(_ context.Context, _ input.GroupByFunction) (input.EntityListMap, error) { - return nil, fmt.Errorf("error executing group by") -} - -func (f FailEntitySource) Iterate(_ context.Context, _ input.IteratorFunction) error { - return fmt.Errorf("error executing iterate") -} - -func filterVariables[D deppy.Variable](variables []deppy.Variable) []D { - var out []D - for _, variable := range variables { - switch v := variable.(type) { - case D: - out = append(out, v) - } - } - return out -} diff --git a/internal/resolution/variablesources/required_package.go b/internal/resolution/variablesources/required_package.go deleted file mode 100644 index d93e6b6a3..000000000 --- a/internal/resolution/variablesources/required_package.go +++ /dev/null @@ -1,103 +0,0 @@ -package variablesources - -import ( - "context" - "fmt" - - "github.com/blang/semver/v4" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - "github.com/operator-framework/operator-controller/internal/resolution/util/predicates" - "github.com/operator-framework/operator-controller/internal/resolution/util/sort" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" -) - -var _ input.VariableSource = &RequiredPackageVariableSource{} - -type RequiredPackageVariableSourceOption func(*RequiredPackageVariableSource) error - -func InVersionRange(versionRange string) RequiredPackageVariableSourceOption { - return func(r *RequiredPackageVariableSource) error { - if versionRange != "" { - vr, err := semver.ParseRange(versionRange) - if err == nil { - r.versionRange = versionRange - r.predicates = append(r.predicates, predicates.InSemverRange(vr)) - return nil - } - - return fmt.Errorf("invalid version range '%s': %v", versionRange, err) - } - return nil - } -} - -func InChannel(channelName string) RequiredPackageVariableSourceOption { - return func(r *RequiredPackageVariableSource) error { - if channelName != "" { - r.channelName = channelName - r.predicates = append(r.predicates, predicates.InChannel(channelName)) - } - return nil - } -} - -type RequiredPackageVariableSource struct { - packageName string - versionRange string - channelName string - predicates []input.Predicate -} - -func NewRequiredPackageVariableSource(packageName string, options ...RequiredPackageVariableSourceOption) (*RequiredPackageVariableSource, error) { - if packageName == "" { - return nil, fmt.Errorf("package name must not be empty") - } - r := &RequiredPackageVariableSource{ - packageName: packageName, - predicates: []input.Predicate{predicates.WithPackageName(packageName)}, - } - for _, option := range options { - if err := option(r); err != nil { - return nil, err - } - } - return r, nil -} - -func (r *RequiredPackageVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { - resultSet, err := entitySource.Filter(ctx, input.And(r.predicates...)) - if err != nil { - return nil, err - } - if len(resultSet) == 0 { - return nil, r.notFoundError() - } - resultSet = resultSet.Sort(sort.ByChannelAndVersion) - var bundleEntities []*olmentity.BundleEntity - for i := 0; i < len(resultSet); i++ { - bundleEntities = append(bundleEntities, olmentity.NewBundleEntity(&resultSet[i])) - } - return []deppy.Variable{ - olmvariables.NewRequiredPackageVariable(r.packageName, bundleEntities), - }, nil -} - -func (r *RequiredPackageVariableSource) notFoundError() error { - // TODO: update this error message when/if we decide to support version ranges as opposed to fixing the version - // context: we originally wanted to support version ranges and take the highest version that satisfies the range - // during the upstream call on the 2023-04-11 we decided to pin the version instead. But, we'll keep version range - // support under the covers in case we decide to pivot back. - if r.versionRange != "" && r.channelName != "" { - return fmt.Errorf("package '%s' at version '%s' in channel '%s' not found", r.packageName, r.versionRange, r.channelName) - } - if r.versionRange != "" { - return fmt.Errorf("package '%s' at version '%s' not found", r.packageName, r.versionRange) - } - if r.channelName != "" { - return fmt.Errorf("package '%s' in channel '%s' not found", r.packageName, r.channelName) - } - return fmt.Errorf("package '%s' not found", r.packageName) -} diff --git a/internal/resolution/variablesources/required_package_test.go b/internal/resolution/variablesources/required_package_test.go deleted file mode 100644 index 19c14824b..000000000 --- a/internal/resolution/variablesources/required_package_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package variablesources_test - -import ( - "context" - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/deppy" - "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/operator-registry/alpha/property" - - olmentity "github.com/operator-framework/operator-controller/internal/resolution/entities" - olmvariables "github.com/operator-framework/operator-controller/internal/resolution/variables" - "github.com/operator-framework/operator-controller/internal/resolution/variablesources" -) - -var _ = Describe("RequiredPackageVariableSource", func() { - var ( - rpvs *variablesources.RequiredPackageVariableSource - packageName string - mockEntitySource input.EntitySource - ) - - BeforeEach(func() { - var err error - packageName = "test-package" - rpvs, err = variablesources.NewRequiredPackageVariableSource(packageName) - Expect(err).NotTo(HaveOccurred()) - mockEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{ - "bundle-1": *input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-2": *input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-3": *input.NewEntity("bundle-3", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - - // add some bundles from a different package - "bundle-4": *input.NewEntity("bundle-4", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - "bundle-5": *input.NewEntity("bundle-5", map[string]string{ - property.TypePackage: `{"packageName": "test-package-2", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - }), - }) - }) - - It("should return the correct package variable", func() { - variables, err := rpvs.GetVariables(context.TODO(), mockEntitySource) - 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)))) - - // ensure bundle entities are in version order (high to low) - Expect(reqPackageVar.BundleEntities()).To(Equal([]*olmentity.BundleEntity{ - olmentity.NewBundleEntity(input.NewEntity("bundle-2", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "3.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-3", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "2.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - olmentity.NewBundleEntity(input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`})), - })) - }) - - It("should filter by version range", func() { - // recreate source with version range option - var err error - rpvs, err = variablesources.NewRequiredPackageVariableSource(packageName, variablesources.InVersionRange(">=1.0.0 !2.0.0 <3.0.0")) - Expect(err).NotTo(HaveOccurred()) - - variables, err := rpvs.GetVariables(context.TODO(), mockEntitySource) - 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)))) - - // ensure bundle entities are in version order (high to low) - Expect(reqPackageVar.BundleEntities()).To(Equal([]*olmentity.BundleEntity{ - olmentity.NewBundleEntity(input.NewEntity("bundle-1", map[string]string{ - property.TypePackage: `{"packageName": "test-package", "version": "1.0.0"}`, - property.TypeChannel: `{"channelName":"stable","priority":0}`, - })), - })) - }) - - It("should fail with bad semver range", func() { - _, err := variablesources.NewRequiredPackageVariableSource(packageName, variablesources.InVersionRange("not a valid semver")) - Expect(err).To(HaveOccurred()) - }) - - It("should return an error if package not found", func() { - mockEntitySource := input.NewCacheQuerier(map[deppy.Identifier]input.Entity{}) - _, err := rpvs.GetVariables(context.TODO(), mockEntitySource) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("package 'test-package' not found")) - }) - - It("should return an error if entity source errors", func() { - _, err := rpvs.GetVariables(context.TODO(), FailEntitySource{}) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("error executing filter")) - }) -}) 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") -}