diff --git a/internal/controllers/operator_controller.go b/internal/controllers/operator_controller.go index a842b8d20..fbec4268c 100644 --- a/internal/controllers/operator_controller.go +++ b/internal/controllers/operator_controller.go @@ -174,7 +174,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, bundleImage, bundleProvisioner, bundleEntity) if err := r.ensureBundleDeployment(ctx, dep); err != nil { // originally Reason: operatorsv1alpha1.ReasonInstallationFailed op.Status.InstalledBundleResource = "" @@ -260,18 +260,44 @@ func (r *OperatorReconciler) getBundleEntityFromSolution(solution *solver.Soluti 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, bundlePath string, bundleProvisioner string, bundleEntity *entities.BundleEntity) *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 // unstructured ensures that the patch contains only what is specified. Using unstructured like this is basically // identical to "kubectl apply -f" + // TODO(jmprusi): return err in func? + bundlePath, err := bundleEntity.BundlePath() + if err != nil { + return nil + } + + channelName, err := bundleEntity.ChannelName() + if err != nil { + return nil + } + + packageName, err := bundleEntity.PackageName() + if err != nil { + return nil + } + + packageVersion, err := bundleEntity.Version() + if err != nil { + return nil + } + bd := &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": rukpakv1alpha1.GroupVersion.String(), "kind": rukpakv1alpha1.BundleDeploymentKind, "metadata": map[string]interface{}{ "name": o.GetName(), + "annotations": map[string]interface{}{ + "operators.operatorframework.io/package": packageName, + "operators.operatorframework.io/channel": channelName, + "operators.operatorframework.io/version": packageVersion, + }, }, "spec": map[string]interface{}{ // TODO: Don't assume plain provisioner diff --git a/internal/resolution/entitysources/catalogdsource.go b/internal/resolution/entitysources/catalogdsource.go index a9a0037b1..ce303658d 100644 --- a/internal/resolution/entitysources/catalogdsource.go +++ b/internal/resolution/entitysources/catalogdsource.go @@ -71,6 +71,10 @@ func (es *CatalogdEntitySource) Iterate(ctx context.Context, fn input.IteratorFu return nil } +type replacesProperty struct { + Replaces string `json:"replaces"` +} + func getEntities(ctx context.Context, client client.Client) (input.EntityList, error) { entityList := input.EntityList{} bundleMetadatas, packageMetdatas, err := fetchMetadata(ctx, client) @@ -106,6 +110,9 @@ func getEntities(ctx context.Context, client client.Client) (input.EntityList, e if catalogScopedEntryName == bundle.Name { channelValue, _ := json.Marshal(property.Channel{ChannelName: ch.Name, Priority: 0}) props[property.TypeChannel] = string(channelValue) + // TODO(jmprusi): Add the proper PropertyType for this + replacesValue, _ := json.Marshal(replacesProperty{Replaces: b.Replaces}) + props["olm.replaces"] = string(replacesValue) entity := input.Entity{ ID: deppy.IdentifierFromString(fmt.Sprintf("%s%s%s", bundle.Name, bundle.Spec.Package, ch.Name)), Properties: props, diff --git a/internal/resolution/variable_sources/installedpackage/installed_package.go b/internal/resolution/variable_sources/installedpackage/installed_package.go new file mode 100644 index 000000000..3b1bd859b --- /dev/null +++ b/internal/resolution/variable_sources/installedpackage/installed_package.go @@ -0,0 +1,138 @@ +package installedpackage + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/blang/semver/v4" + "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" + "github.com/operator-framework/operator-controller/internal/resolution/util/predicates" + "github.com/operator-framework/operator-controller/internal/resolution/util/sort" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" +) + +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)) + var entityIDs []deppy.Identifier + for _, bundle := range bundleEntities { + entityIDs = append(entityIDs, bundle.ID) + } + return &InstalledPackageVariable{ + SimpleVariable: input.NewSimpleVariable(id, constraint.Mandatory(), constraint.Dependency(entityIDs...)), + bundleEntities: bundleEntities, + } +} + +var _ input.VariableSource = &InstalledPackageVariableSource{} + +type InstalledPackageVariableSource struct { + packageName string + version semver.Version + channelName string +} + +func NewInstalledPackageVariableSource(packageName, version, channel string) (*InstalledPackageVariableSource, error) { + if packageName == "" { + return nil, fmt.Errorf("package name must not be empty") + } + + semverVersion, err := semver.Parse(version) + if err != nil { + return nil, err + } + + //TODO(jmprusi): check version and channel + return &InstalledPackageVariableSource{ + packageName: packageName, + version: semverVersion, + channelName: channel, + }, nil +} + +// TODO(jmprusi): move this somewhere else? +type replacesProperty struct { + Replaces string `json:"replaces"` +} + +// TODO(jmprusi): move this somewhere else? +type packageProperty struct { + Package string `json:"packageName"` + Version string `json:"version"` +} + +func (r *InstalledPackageVariableSource) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { + validRange, err := semver.ParseRange(">=" + r.version.String()) + if err != nil { + return nil, err + } + resultSet, err := entitySource.Filter(ctx, input.And(predicates.WithPackageName(r.packageName), predicates.InChannel(r.channelName), predicates.InSemverRange(validRange))) + 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++ { + + replacesJSON := resultSet[i].Properties["olm.replaces"] + packageJSON := resultSet[i].Properties["olm.package"] + + if replacesJSON == "" || packageJSON == "" { + continue + } + + // unmarshal replaces and packages + var replaces replacesProperty + var packages packageProperty + if err := json.Unmarshal([]byte(replacesJSON), &replaces); err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(packageJSON), &packages); err != nil { + return nil, err + } + + version, err := semver.Parse(packages.Version) + if err != nil { + return nil, err + } + + expectedReplace := fmt.Sprintf("%s.v%s", r.packageName, r.version.String()) + if r.version.Equals(version) || replaces.Replaces == expectedReplace { + bundleEntities = append(bundleEntities, olmentity.NewBundleEntity(&resultSet[i])) + } + + } + return []deppy.Variable{ + NewInstalledPackageVariable(r.packageName, bundleEntities), + }, nil +} + +func (r *InstalledPackageVariableSource) notFoundError() error { + return fmt.Errorf("package '%s' not installed", r.packageName) +} + +func NewInstalledPackage(bundleDeployment *rukpakv1alpha1.BundleDeployment) (*InstalledPackageVariableSource, error) { + + // TODO(jmprusi): proper if ... validation + version := bundleDeployment.Annotations["operators.operatorframework.io/version"] + channel := bundleDeployment.Annotations["operators.operatorframework.io/channel"] + pkg := bundleDeployment.Annotations["operators.operatorframework.io/package"] + + return NewInstalledPackageVariableSource(pkg, version, channel) +} diff --git a/internal/resolution/variablesources/operator.go b/internal/resolution/variablesources/operator.go index e0d438ac9..9c650449c 100644 --- a/internal/resolution/variablesources/operator.go +++ b/internal/resolution/variablesources/operator.go @@ -5,9 +5,10 @@ import ( "github.com/operator-framework/deppy/pkg/deppy" "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/variable_sources/installedpackage" + rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ input.VariableSource = &OperatorVariableSource{} @@ -48,5 +49,23 @@ func (o *OperatorVariableSource) GetVariables(ctx context.Context, entitySource variableSources = append(variableSources, rps) } - return variableSources.GetVariables(ctx, entitySource) + bundleDeployments := rukpakv1alpha1.BundleDeploymentList{} + if err := o.client.List(ctx, &bundleDeployments); err != nil { + return nil, err + } + + for _, bundleDeployment := range bundleDeployments.Items { + if _, ok := bundleDeployment.Annotations["operators.operatorframework.io/package"]; !ok { + continue + } + ips, err := installedpackage.NewInstalledPackage(&bundleDeployment) + if err != nil { + return nil, err + } + variableSources = append(variableSources, ips) + } + + // build variable source pipeline + variableSource := NewCRDUniquenessConstraintsVariableSource(NewBundlesAndDepsVariableSource(variableSources...)) + return variableSource.GetVariables(ctx, entitySource) }