Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to catalogd's CatalogMetadata APIs #343

Merged
merged 1 commit into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 3 additions & 55 deletions cmd/resolutioncli/entity_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@ package main

import (
"context"
"encoding/json"
"fmt"

"github.com/operator-framework/deppy/pkg/deppy"
"github.com/operator-framework/deppy/pkg/deppy/input"
"github.com/operator-framework/operator-registry/alpha/action"
"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/model"
"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/entitysources"
)

type indexRefEntitySource struct {
Expand Down Expand Up @@ -101,12 +96,8 @@ func (es *indexRefEntitySource) entities(ctx context.Context) (input.EntityList,
return nil, err
}

model, err := declcfg.ConvertToModel(*cfg)
if err != nil {
return nil, err
}

entities, err := modelToEntities(model)
// TODO: update empty catalog name string to be catalog name once we support multiple catalogs in CLI
entities, err := entitysources.MetadataToEntities("", cfg.Channels, cfg.Bundles)
if err != nil {
return nil, err
}
Expand All @@ -116,46 +107,3 @@ func (es *indexRefEntitySource) entities(ctx context.Context) (input.EntityList,

return es.entitiesCache, nil
}

// TODO: Reduce code duplication: share a function with catalogdEntitySource (see getEntities)
// We don't want to maintain two functions performing conversion into input.EntityList.
// For this we need some common format. So we need a package which will be able
// to convert from declfcg structs into CRD structs directly or via model.Model.
// One option would be to make this piece of code from catalogd reusable and exportable:
// https://github.com/operator-framework/catalogd/blob/9fe45a628de2e74d9cd73c3650fa2582aaac5213/pkg/controllers/core/catalog_controller.go#L200-L360
func modelToEntities(model model.Model) (input.EntityList, error) {
entities := input.EntityList{}

for _, pkg := range model {
for _, ch := range pkg.Channels {
for _, bundle := range ch.Bundles {
props := map[string]string{}

for _, prop := range bundle.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)
}
}

imgValue, err := json.Marshal(bundle.Image)
if err != nil {
return nil, err
}
props[olmentity.PropertyBundlePath] = string(imgValue)

channelValue, _ := json.Marshal(property.Channel{ChannelName: ch.Name, Priority: 0})
props[property.TypeChannel] = string(channelValue)
entity := input.Entity{
ID: deppy.IdentifierFromString(fmt.Sprintf("%s%s%s", bundle.Name, bundle.Package.Name, ch.Name)),
Properties: props,
}
entities = append(entities, entity)
}
}
}

return entities, nil
}
2 changes: 1 addition & 1 deletion config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ rules:
- apiGroups:
- catalogd.operatorframework.io
resources:
- bundlemetadata
- catalogmetadata
verbs:
- list
- watch
Expand Down
2 changes: 1 addition & 1 deletion internal/controllers/operator_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ type OperatorReconciler struct {

//+kubebuilder:rbac:groups=core.rukpak.io,resources=bundledeployments,verbs=get;list;watch;create;update;patch

//+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=catalogs,verbs=list;watch
//+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=catalogmetadata,verbs=list;watch

func (r *OperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx).WithName("operator-controller")
Expand Down
148 changes: 99 additions & 49 deletions internal/resolution/entitysources/catalogdsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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/declcfg"
"github.com/operator-framework/operator-registry/alpha/property"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -71,70 +72,119 @@
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 {
func getEntities(ctx context.Context, cl client.Client) (input.EntityList, error) {
allEntitiesList := input.EntityList{}

var catalogList catalogd.CatalogList
if err := cl.List(ctx, &catalogList); 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)
}
for _, catalog := range catalogList.Items {
channels, bundles, err := fetchCatalogMetadata(ctx, cl, catalog.Name)
Comment on lines +82 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to fetch and loop through all the catalogs here? If we want to ensure we fetch all CatalogMetadata for all Catalog resources I think we can simplify this to listing all CatalogMetadata resources and then processing each one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea is that we process one catalog at a time and let garbage collection do its thing while we move onto the next catalog.

But yes, we definitevely can fetch all the data from all catalogs and loop trough it, but the data won't be garbage collectable until after we process whole slice. So we need to find a balance between 1) how much we want to hold in memory and 2) how many network calls we make.

Notice: I haven't tested/measured memory consumption with multipe catalogs. So it is based on gut feeling and maybe it is not very beneficial from memory consumption point of view.

But another benefit of doing one catalog at a time is that it avoids doing more mapping between catalogs, channels and bundles.

if err != nil {
return nil, err

Check warning on line 85 in internal/resolution/entitysources/catalogdsource.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entitysources/catalogdsource.go#L85

Added line #L85 was not covered by tests
}

imgValue, err := json.Marshal(bundle.Spec.Image)
catalogEntitiesList, err := MetadataToEntities(catalog.Name, channels, bundles)
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)

allEntitiesList = append(allEntitiesList, catalogEntitiesList...)
}

return allEntitiesList, nil
}

func MetadataToEntities(catalogName string, channels []declcfg.Channel, bundles []declcfg.Bundle) (input.EntityList, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I'm reviewing this function I have some concerns about it's performance. How frequently are we retrieving and transforming this data?

If this is an issue, we can address it in a follow up.

Copy link
Member Author

@m1kola m1kola Aug 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please explain your concerns?

I'm concerned about CatalogdEntitySource since it seems to call getEntities on each Filter call. I decided not to touch it since we will be removing entity sources in #347.

I think as part of #347 we should think about some cache which will be valid for the duration of one resolution.

In the CLI we cache entites into the enttiy source property:

func (es *indexRefEntitySource) entities(ctx context.Context) (input.EntityList, error) {
if es.entitiesCache == nil {
cfg, err := es.renderer.Run(ctx)
if err != nil {
return nil, err
}
// TODO: update empty catalog name string to be catalog name once we support multiple catalogs in CLI
entities, err := entitysources.MetadataToEntities("", cfg.Channels, cfg.Bundles)
if err != nil {
return nil, err
}
es.entitiesCache = entities
}
return es.entitiesCache, nil
}

In operator controller we can not do the same since entity source is resused between reconciliations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a chat with Alex on this. The consern is not about MetadataToEntities where we just convert data from declcfg to entities, but about fetching the data from API server on each reconcile & multiple times during one resolution.

We came to an agreement that this is something which needs to be addressed, but not as part as this PR since this is problem with an entity source in general, not with the way we consume catalog medatada. Moreover entity sources will be removed soon as part of #347.

@awgreene please correct me if I captured it incorrectly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetching the data from API server on each reconcile & multiple times during one resolution.

Is that true? I thought we had informer caches setup for the catalogd data such that cl.Get and cl.List would only ever hit the cache (and never the real apiserver)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you might be right! I totally forgot about informers.

entityList := input.EntityList{}

bundlesMap := map[string]*declcfg.Bundle{}
for i := range bundles {
bundleKey := fmt.Sprintf("%s-%s", bundles[i].Package, bundles[i].Name)
bundlesMap[bundleKey] = &bundles[i]
}

for _, ch := range channels {
for _, chEntry := range ch.Entries {
bundleKey := fmt.Sprintf("%s-%s", ch.Package, chEntry.Name)
bundle, ok := bundlesMap[bundleKey]
if !ok {
return nil, fmt.Errorf("bundle %q not found in catalog %q (package %q, channel %q)", chEntry.Name, catalogName, ch.Package, ch.Name)

Check warning on line 113 in internal/resolution/entitysources/catalogdsource.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entitysources/catalogdsource.go#L113

Added line #L113 was not covered by tests
}

props := map[string]string{}

for _, prop := range bundle.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.Image)
if err != nil {
return nil, err

Check warning on line 131 in internal/resolution/entitysources/catalogdsource.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entitysources/catalogdsource.go#L131

Added line #L131 was not covered by tests
}
props[entities.PropertyBundlePath] = string(imgValue)

channelValue, _ := json.Marshal(property.Channel{ChannelName: ch.Name, Priority: 0})
props[property.TypeChannel] = string(channelValue)
replacesValue, _ := json.Marshal(entities.ChannelEntry{
Name: bundle.Name,
Replaces: chEntry.Replaces,
})
props[entities.PropertyBundleChannelEntry] = string(replacesValue)

catalogScopedEntryName := fmt.Sprintf("%s-%s", catalogName, bundle.Name)
entity := input.Entity{
ID: deppy.IdentifierFromString(fmt.Sprintf("%s%s%s", catalogScopedEntryName, bundle.Package, ch.Name)),
Properties: props,
}
everettraven marked this conversation as resolved.
Show resolved Hide resolved
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
func fetchCatalogMetadata(ctx context.Context, cl client.Client, catalogName string) ([]declcfg.Channel, []declcfg.Bundle, error) {
channels, err := fetchCatalogMetadataByScheme[declcfg.Channel](ctx, cl, declcfg.SchemaChannel, catalogName)
if err != nil {
return nil, nil, err

Check warning on line 158 in internal/resolution/entitysources/catalogdsource.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entitysources/catalogdsource.go#L158

Added line #L158 was not covered by tests
}
bundles, err := fetchCatalogMetadataByScheme[declcfg.Bundle](ctx, cl, declcfg.SchemaBundle, catalogName)
if err != nil {
return nil, nil, err

Check warning on line 162 in internal/resolution/entitysources/catalogdsource.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entitysources/catalogdsource.go#L162

Added line #L162 was not covered by tests
everettraven marked this conversation as resolved.
Show resolved Hide resolved
}
bundleMetadatas := catalogd.BundleMetadataList{}
if err := client.List(ctx, &bundleMetadatas); err != nil {
return catalogd.BundleMetadataList{}, nil, err

return channels, bundles, nil
}

type declcfgSchema interface {
declcfg.Package | declcfg.Bundle | declcfg.Channel
}

// TODO: Cleanup once https://github.com/golang/go/issues/45380 implemented
// We should be able to get rid of the schema arg and switch based on the type passed to this generic
func fetchCatalogMetadataByScheme[T declcfgSchema](ctx context.Context, cl client.Client, schema, catalogName string) ([]T, error) {
cmList := catalogd.CatalogMetadataList{}
if err := cl.List(ctx, &cmList, client.MatchingLabels{"schema": schema, "catalog": catalogName}); err != nil {
return nil, err

Check warning on line 177 in internal/resolution/entitysources/catalogdsource.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entitysources/catalogdsource.go#L177

Added line #L177 was not covered by tests
}
packages := map[string]catalogd.Package{}
for _, pkg := range packageMetdatas.Items {
packages[pkg.Name] = pkg

contents := []T{}
for _, cm := range cmList.Items {
var content T
if err := json.Unmarshal(cm.Spec.Content, &content); err != nil {
return nil, err

Check warning on line 184 in internal/resolution/entitysources/catalogdsource.go

View check run for this annotation

Codecov / codecov/patch

internal/resolution/entitysources/catalogdsource.go#L184

Added line #L184 was not covered by tests
}
contents = append(contents, content)
}
return bundleMetadatas, packages, nil

return contents, nil
}
7 changes: 6 additions & 1 deletion test/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ var _ = AfterSuite(func() {
}).Should(Succeed())

// speed up delete without waiting for gc
Expect(c.DeleteAllOf(ctx, &catalogd.BundleMetadata{})).To(Succeed())
Expect(c.DeleteAllOf(ctx, &catalogd.Package{})).To(Succeed())
Expect(c.DeleteAllOf(ctx, &catalogd.BundleMetadata{})).To(Succeed())
Expect(c.DeleteAllOf(ctx, &catalogd.CatalogMetadata{})).To(Succeed())

Eventually(func(g Gomega) {
// ensure resource cleanup
Expand All @@ -107,6 +108,10 @@ var _ = AfterSuite(func() {
bmd := &catalogd.BundleMetadataList{}
g.Expect(c.List(ctx, bmd)).To(Succeed())
g.Expect(bmd.Items).To(BeEmpty())

cmd := &catalogd.CatalogMetadataList{}
g.Expect(c.List(ctx, cmd)).To(Succeed())
g.Expect(bmd.Items).To(BeEmpty())
}).Should(Succeed())
})

Expand Down
57 changes: 28 additions & 29 deletions test/operator-framework-e2e/operator_framework_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
Expand All @@ -27,6 +26,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1"
"github.com/operator-framework/operator-registry/alpha/declcfg"
"github.com/operator-framework/operator-registry/alpha/property"
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"

operatorv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
Expand Down Expand Up @@ -833,8 +834,7 @@ func createCatalogCheckResources(operatorCatalog *catalogd.Catalog, catalogDInfo
// checking if the bundle metadatas are created
By("Eventually checking if bundle metadata is created")
Eventually(func(g Gomega) {
err = validateBundleMetadataCreation(operatorCatalog, catalogDInfo.operatorName, bundleVersions)
g.Expect(err).ToNot(HaveOccurred())
validateCatalogMetadataCreation(g, operatorCatalog, catalogDInfo.operatorName, bundleVersions)
}).Should(Succeed())
return operatorCatalog, nil
}
Expand Down Expand Up @@ -886,39 +886,38 @@ func validatePackageCreation(operatorCatalog *catalogd.Catalog, pkgName string)
return nil
}

// Checks if the bundle metadatas are created from the catalog and returns error if not.
// Checks if the CatalogMetadata was created from the catalog and returns error if not.
// The expected pkgNames and their versions are taken as input. This is then compared against the collected bundle versions.
// The collected bundle versions are formed by reading the version from "olm.package" property type whose catalog name
// matches the catalog name and pkgName matches the pkgName under consideration.
func validateBundleMetadataCreation(operatorCatalog *catalogd.Catalog, pkgName string, versions []string) error {
type Package struct {
PackageName string `json:"packageName"`
Version string `json:"version"`
}
var pkgValue Package
collectedBundleVersions := make([]string, 0)
bmList := &catalogd.BundleMetadataList{}
if err := c.List(ctx, bmList); err != nil {
return fmt.Errorf("Error retrieving the bundle metadata after %v catalog instance creation: %v", operatorCatalog.Name, err)
}

for _, bm := range bmList.Items {
if bm.Spec.Catalog.Name == operatorCatalog.Name {
for _, prop := range bm.Spec.Properties {
if prop.Type == "olm.package" {
err := json.Unmarshal(prop.Value, &pkgValue)
if err == nil && pkgValue.PackageName == pkgName {
collectedBundleVersions = append(collectedBundleVersions, pkgValue.Version)
}
}
func validateCatalogMetadataCreation(g Gomega, operatorCatalog *catalogd.Catalog, pkgName string, versions []string) {
cmList := catalogd.CatalogMetadataList{}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests were passing without change to this function, but since we switched to using CatalogMetadata instead of BundleMetadata in the controller, I think it makes sense to update this pice too.

Also I switched to using g.Expect here since it makes a bit easier to track down a specific failure vs this being logged in the calling case.

err := c.List(ctx, &cmList, client.MatchingLabels{
"schema": declcfg.SchemaBundle,
"catalog": operatorCatalog.Name,
"package": pkgName,
})
g.Expect(err).ToNot(HaveOccurred())

collectedBundleVersions := []string{}
for _, cm := range cmList.Items {
bundle := declcfg.Bundle{}

err := json.Unmarshal(cm.Spec.Content, &bundle)
g.Expect(err).ToNot(HaveOccurred())

for _, prop := range bundle.Properties {
if prop.Type == "olm.package" {
var pkgValue property.Package
err := json.Unmarshal(prop.Value, &pkgValue)
g.Expect(err).ToNot(HaveOccurred())

collectedBundleVersions = append(collectedBundleVersions, pkgValue.Version)
}
}
}
if !reflect.DeepEqual(collectedBundleVersions, versions) {
return fmt.Errorf("Package %v for the catalog %v is not created", pkgName, operatorCatalog.Name)
}

return nil
g.Expect(collectedBundleVersions).To(ConsistOf(versions))
}

// Checks for a successful resolution and bundle path for the operator and returns error if not.
Expand Down
Loading