Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
jmprusi committed Jun 27, 2023
1 parent 22afecf commit 04efb43
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 13 deletions.
32 changes: 30 additions & 2 deletions internal/controllers/operator_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,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)
dep := r.generateExpectedBundleDeployment(*op, bundleEntity)
if err := r.ensureBundleDeployment(ctx, dep); err != nil {
// originally Reason: operatorsv1alpha1.ReasonInstallationFailed
op.Status.InstalledBundleResource = ""
Expand Down Expand Up @@ -245,17 +245,45 @@ 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) *unstructured.Unstructured {
func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundleEntity *entity.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
}

// TODO(jmprusi): create a proper const for operators.operatorframework.io/operator-ref
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
Expand Down
7 changes: 7 additions & 0 deletions internal/resolution/entitysources/catalogdsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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,
Expand All @@ -114,6 +117,10 @@ func getEntities(ctx context.Context, client client.Client) (input.EntityList, e
return entities, nil
}

type replacesProperty struct {
Replaces string `json:"replaces"`
}

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 {
Expand Down
11 changes: 8 additions & 3 deletions internal/resolution/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (

"github.com/operator-framework/deppy/pkg/deppy/input"
"github.com/operator-framework/deppy/pkg/deppy/solver"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/operator-framework/operator-controller/api/v1alpha1"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/olm"
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type OperatorResolver struct {
Expand All @@ -32,7 +32,12 @@ func (o *OperatorResolver) Resolve(ctx context.Context) (*solver.Solution, error
return &solver.Solution{}, nil
}

olmVariableSource := olm.NewOLMVariableSource(operatorList.Items...)
bundleDeploymentList := rukpakv1alpha1.BundleDeploymentList{}
if err := o.client.List(ctx, &bundleDeploymentList); err != nil {
return nil, err
}

olmVariableSource := olm.NewOLMVariableSource(operatorList.Items, bundleDeploymentList.Items)
deppySolver := solver.NewDeppySolver(o.entitySource, olmVariableSource)

solution, err := deppySolver.Solve(ctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/operator-framework/deppy/pkg/deppy/input"

olmentity "github.com/operator-framework/operator-controller/internal/resolution/variable_sources/entity"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/installed_package"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/required_package"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/util/predicates"
entitysort "github.com/operator-framework/operator-controller/internal/resolution/variable_sources/util/sort"
Expand Down Expand Up @@ -76,6 +77,8 @@ func (b *BundlesAndDepsVariableSource) GetVariables(ctx context.Context, entityS
switch v := variable.(type) {
case *required_package.RequiredPackageVariable:
bundleEntityQueue = append(bundleEntityQueue, v.BundleEntities()...)
case *installed_package.InstalledPackageVariable:
bundleEntityQueue = append(bundleEntityQueue, v.BundleEntities()...)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package installed_package

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/variable_sources/entity"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/util/predicates"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/util/sort"
)

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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package installed_package_test

import (
"fmt"
"testing"

. "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/variable_sources/entity"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/installed_package"
)

func TestInstalledPackage(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "InstalledPackageVariableSource Suite")
}

var _ = Describe("installedPackageVariable", func() {
var (
rpv *installed_package.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}`,
"olm.replaces": `{"packageName": "test-package", "version": "0.0.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}`,
"olm.replaces": `{"packageName": "test-package", "version": "1.0.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}`,
"olm.replaces": `{"packageName": "test-package", "version": "2.0.0"}`,
})),
}
rpv = installed_package.NewInstalledPackageVariable(packageName, bundleEntities)
})

It("should return the correct package name", func() {
Expect(rpv.Identifier()).To(Equal(deppy.IdentifierFromString(fmt.Sprintf("installed package %s", packageName))))
})

It("should return the correct bundle entities", func() {
Expect(rpv.BundleEntities()).To(Equal(bundleEntities))
})
})
37 changes: 34 additions & 3 deletions internal/resolution/variable_sources/olm/olm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ import (
operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/bundles_and_dependencies"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/crd_constraints"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/installed_package"
"github.com/operator-framework/operator-controller/internal/resolution/variable_sources/required_package"
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
)

var _ input.VariableSource = &OLMVariableSource{}

type OLMVariableSource struct {
operators []operatorsv1alpha1.Operator
operators []operatorsv1alpha1.Operator
bundleDeployments []rukpakv1alpha1.BundleDeployment
}

func NewOLMVariableSource(operators ...operatorsv1alpha1.Operator) *OLMVariableSource {
func NewOLMVariableSource(operators []operatorsv1alpha1.Operator, bundleDeployments []rukpakv1alpha1.BundleDeployment) *OLMVariableSource {
return &OLMVariableSource{
operators: operators,
operators: operators,
bundleDeployments: bundleDeployments,
}
}

Expand All @@ -36,11 +40,38 @@ func (o *OLMVariableSource) GetVariables(ctx context.Context, entitySource input
inputVariableSources = append(inputVariableSources, rps)
}

// Scan for all the bundleDeployments and look for the one with the proper annotation
// with that we will have the actual version and packagename of the installed package.
//
// That information can be used to check if the new desired version is compatible with the installed one.
//
for _, bundleDeployment := range o.bundleDeployments {
if _, ok := bundleDeployment.Annotations["operators.operatorframework.io/package"]; !ok {
continue
}
ips, err := o.installedPackage(&bundleDeployment)
if err != nil {
return nil, err
}
inputVariableSources = append(inputVariableSources, ips)
}

// build variable source pipeline
variableSource := crd_constraints.NewCRDUniquenessConstraintsVariableSource(bundles_and_dependencies.NewBundlesAndDepsVariableSource(inputVariableSources...))

return variableSource.GetVariables(ctx, entitySource)
}

func (o *OLMVariableSource) installedPackage(bundleDeployment *rukpakv1alpha1.BundleDeployment) (*installed_package.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 installed_package.NewInstalledPackageVariableSource(pkg, version, channel)
}

func (o *OLMVariableSource) requiredPackageFromOperator(operator *operatorsv1alpha1.Operator) (*required_package.RequiredPackageVariableSource, error) {
var opts []required_package.RequiredPackageOption
if operator.Spec.Version != "" {
Expand Down
Loading

0 comments on commit 04efb43

Please sign in to comment.