diff --git a/config/samples/operators_v1alpha1_operator.yaml b/config/samples/operators_v1alpha1_operator.yaml index 46312e957..249b3c683 100644 --- a/config/samples/operators_v1alpha1_operator.yaml +++ b/config/samples/operators_v1alpha1_operator.yaml @@ -9,4 +9,5 @@ metadata: app.kubernetes.io/created-by: operator-controller name: operator-sample spec: - packageName: argocd-operator + packageName: zookeeper-operator + version: 0.16.2 diff --git a/internal/controllers/operator_controller.go b/internal/controllers/operator_controller.go index a842b8d20..1112e33fb 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,7 +260,7 @@ 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 diff --git a/internal/controllers/variable_source.go b/internal/controllers/variable_source.go index a3ff9dce7..21fa0b219 100644 --- a/internal/controllers/variable_source.go +++ b/internal/controllers/variable_source.go @@ -29,6 +29,9 @@ func NewVariableSource(cl client.Client) 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 }, diff --git a/internal/resolution/entities/bundle_entity.go b/internal/resolution/entities/bundle_entity.go index 020b68d06..3712c4143 100644 --- a/internal/resolution/entities/bundle_entity.go +++ b/internal/resolution/entities/bundle_entity.go @@ -25,13 +25,6 @@ const ( // ---- -type ChannelProperties struct { - property.Channel - Replaces string `json:"replaces,omitempty"` - Skips []string `json:"skips,omitempty"` - SkipRange string `json:"skipRange,omitempty"` -} - type propertyRequirement bool const ( @@ -46,6 +39,10 @@ type PackageRequired struct { type GVK property.GVK +type Replaces struct { + Replaces string `json:"replaces"` +} + func (g GVK) String() string { return fmt.Sprintf(`group:"%s" version:"%s" kind:"%s"`, g.Group, g.Version, g.Kind) } @@ -64,15 +61,16 @@ type BundleEntity struct { *input.Entity // these properties are lazy loaded as they are requested - bundlePackage *property.Package - providedGVKs []GVK - requiredGVKs []GVKRequired - requiredPackages []PackageRequired - channelProperties *ChannelProperties - semVersion *semver.Version - bundlePath string - mediaType string - mu sync.RWMutex + bundlePackage *property.Package + providedGVKs []GVK + requiredGVKs []GVKRequired + requiredPackages []PackageRequired + channel *property.Channel + replaces *Replaces + semVersion *semver.Version + bundlePath string + mediaType string + mu sync.RWMutex } func NewBundleEntity(entity *input.Entity) *BundleEntity { @@ -121,14 +119,35 @@ func (b *BundleEntity) ChannelName() (string, error) { if err := b.loadChannelProperties(); err != nil { return "", err } - return b.channelProperties.ChannelName, nil + return b.channel.ChannelName, nil } -func (b *BundleEntity) ChannelProperties() (*ChannelProperties, error) { +func (b *BundleEntity) Channel() (*property.Channel, error) { if err := b.loadChannelProperties(); err != nil { return nil, err } - return b.channelProperties, nil + return b.channel, nil +} + +func (b *BundleEntity) Replaces() (string, error) { + if err := b.loadReplaces(); err != nil { + return "", err + } + return b.replaces.Replaces, nil +} + +func (b *BundleEntity) loadReplaces() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.replaces == nil { + // TODO: move property name to constant + replaces, err := loadFromEntity[Replaces](b.Entity, "olm.replaces", optional) + if err != nil { + return fmt.Errorf("error determining replaces for entity '%s': %w", b.ID, err) + } + b.replaces = &replaces + } + return nil } func (b *BundleEntity) BundlePath() (string, error) { @@ -228,12 +247,12 @@ func (b *BundleEntity) loadRequiredPackages() error { func (b *BundleEntity) loadChannelProperties() error { b.mu.Lock() defer b.mu.Unlock() - if b.channelProperties == nil { - channel, err := loadFromEntity[ChannelProperties](b.Entity, property.TypeChannel, required) + 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.channelProperties = &channel + b.channel = &channel } return nil } diff --git a/internal/resolution/entities/bundle_entity_test.go b/internal/resolution/entities/bundle_entity_test.go index f17756035..a0e52651d 100644 --- a/internal/resolution/entities/bundle_entity_test.go +++ b/internal/resolution/entities/bundle_entity_test.go @@ -205,13 +205,13 @@ var _ = Describe("BundleEntity", func() { }) }) - Describe("ChannelProperties", func() { + 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.ChannelProperties() + channelProperties, err := bundleEntity.Channel() Expect(err).ToNot(HaveOccurred()) Expect(*channelProperties).To(Equal(olmentity.ChannelProperties{ Channel: property.Channel{ @@ -226,7 +226,7 @@ var _ = Describe("BundleEntity", func() { 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.ChannelProperties() + 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")) }) @@ -235,7 +235,7 @@ var _ = Describe("BundleEntity", func() { "olm.channel": "badChannelPropertiesStructure", }) bundleEntity := olmentity.NewBundleEntity(entity) - channelProperties, err := bundleEntity.ChannelProperties() + 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")) }) 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/util/predicates/predicates.go b/internal/resolution/util/predicates/predicates.go index fc124cc7a..13de41091 100644 --- a/internal/resolution/util/predicates/predicates.go +++ b/internal/resolution/util/predicates/predicates.go @@ -56,3 +56,25 @@ func ProvidesGVK(gvk *olmentity.GVK) input.Predicate { 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.Replaces() + if err != nil { + return false + } + return replaces == bundleID + } +} diff --git a/internal/resolution/util/sort/sort.go b/internal/resolution/util/sort/sort.go index 5006b7e71..d013b982f 100644 --- a/internal/resolution/util/sort/sort.go +++ b/internal/resolution/util/sort/sort.go @@ -55,8 +55,8 @@ func packageOrder(e1, e2 *entities.BundleEntity) int { } func channelOrder(e1, e2 *entities.BundleEntity) int { - channelProperties1, err1 := e1.ChannelProperties() - channelProperties2, err2 := e2.ChannelProperties() + channelProperties1, err1 := e1.Channel() + channelProperties2, err2 := e2.Channel() errComp := compareErrors(err1, err2) if errComp != 0 { return errComp diff --git a/internal/resolution/variables/installed_package.go b/internal/resolution/variables/installed_package.go new file mode 100644 index 000000000..765d50401 --- /dev/null +++ b/internal/resolution/variables/installed_package.go @@ -0,0 +1,32 @@ +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" +) + +type InstalledPackageVariable struct { + *input.SimpleVariable + bundleEntities []*olmentity.BundleEntity +} + +func (r *InstalledPackageVariable) BundleEntities() []*olmentity.BundleEntity { + return r.bundleEntities +} + +func NewInstalledPackageVariable(bundleImage string, bundleEntities []*olmentity.BundleEntity) *InstalledPackageVariable { + id := deppy.IdentifierFromString(fmt.Sprintf("installed package %s", bundleImage)) + 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, + } +} diff --git a/internal/resolution/variablesources/bundle_deployment.go b/internal/resolution/variablesources/bundle_deployment.go new file mode 100644 index 000000000..e42369c56 --- /dev/null +++ b/internal/resolution/variablesources/bundle_deployment.go @@ -0,0 +1,58 @@ +package variablesources + +import ( + "context" + "fmt" + + "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{}{} + if len(bundleDeployments.Items) > 1 { + fmt.Println("hello") + } + 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/bundles_and_dependencies.go b/internal/resolution/variablesources/bundles_and_dependencies.go index b37f9b2e4..3e46a2027 100644 --- a/internal/resolution/variablesources/bundles_and_dependencies.go +++ b/internal/resolution/variablesources/bundles_and_dependencies.go @@ -45,6 +45,8 @@ func (b *BundlesAndDepsVariableSource) GetVariables(ctx context.Context, entityS switch v := variable.(type) { case *olmvariables.RequiredPackageVariable: bundleEntityQueue = append(bundleEntityQueue, v.BundleEntities()...) + case *olmvariables.InstalledPackageVariable: + bundleEntityQueue = append(bundleEntityQueue, v.BundleEntities()...) } } diff --git a/internal/resolution/variablesources/installed_package.go b/internal/resolution/variablesources/installed_package.go new file mode 100644 index 000000000..f429b66df --- /dev/null +++ b/internal/resolution/variablesources/installed_package.go @@ -0,0 +1,83 @@ +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 + } + version, err := installedBundle.Version() + if err != nil { + return nil, err + } + bundleID := fmt.Sprintf("%s.v%s", packageName, version.String()) + resultSet, err = entitySource.Filter(ctx, predicates.Replaces(bundleID)) + 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(bundleID, 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/operator.go b/internal/resolution/variablesources/operator.go index e0d438ac9..e4a6dc288 100644 --- a/internal/resolution/variablesources/operator.go +++ b/internal/resolution/variablesources/operator.go @@ -5,9 +5,8 @@ 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" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ input.VariableSource = &OperatorVariableSource{}