Skip to content

Commit dba08eb

Browse files
exdxperdasilva
andauthored
resolver: Add support for excluding global catalogs from resolution (#2788)
The resolver is currently configured to always consider resolving from global catalogs. In some cases, this may not be desirable, and the user would like to explicitly ignore global catalogs for the purposes of resolution. This change enables per-namespace exclusion from global catalog sources via an annotation on existing registry source provider. A queueinformer for OperatorGroups is added since they are now an input to resolution via the global catalog exclusion annotation. Namespace resolution will be triggered on changes to an OperatorGroup, in case the value provided on that annotation by the user changes. Updated the error message returned by the cache in case a source has an error to be more clear. Signed-off-by: Daniel Sover <[email protected]> Co-authored-by: perdasilva <[email protected]> Co-authored-by: perdasilva <[email protected]>
1 parent 97b64e1 commit dba08eb

File tree

6 files changed

+251
-8
lines changed

6 files changed

+251
-8
lines changed
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package catalog
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
v1listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1"
8+
9+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache"
10+
"github.com/sirupsen/logrus"
11+
"k8s.io/apimachinery/pkg/labels"
12+
)
13+
14+
type OperatorGroupToggleSourceProvider struct {
15+
sp cache.SourceProvider
16+
logger *logrus.Logger
17+
ogLister v1listers.OperatorGroupLister
18+
}
19+
20+
func NewOperatorGroupToggleSourceProvider(sp cache.SourceProvider, logger *logrus.Logger,
21+
ogLister v1listers.OperatorGroupLister) *OperatorGroupToggleSourceProvider {
22+
return &OperatorGroupToggleSourceProvider{
23+
sp: sp,
24+
logger: logger,
25+
ogLister: ogLister,
26+
}
27+
}
28+
29+
const exclusionAnnotation string = "olm.operatorframework.io/exclude-global-namespace-resolution"
30+
31+
func (e *OperatorGroupToggleSourceProvider) Sources(namespaces ...string) map[cache.SourceKey]cache.Source {
32+
// Check if annotation is set first
33+
resolutionNamespaces, err := e.CheckForExclusion(namespaces...)
34+
if err != nil {
35+
e.logger.Errorf("error checking namespaces %#v for global resolution exlusion: %s", namespaces, err)
36+
// Fail early with a dummy Source that returns an error
37+
// TODO: Update the Sources interface to return an error
38+
m := make(map[cache.SourceKey]cache.Source)
39+
key := cache.SourceKey{Name: "operatorgroup-unavailable", Namespace: namespaces[0]}
40+
source := errorSource{err}
41+
m[key] = source
42+
return m
43+
}
44+
return e.sp.Sources(resolutionNamespaces...)
45+
}
46+
47+
type errorSource struct {
48+
error
49+
}
50+
51+
func (e errorSource) Snapshot(ctx context.Context) (*cache.Snapshot, error) {
52+
return nil, e.error
53+
}
54+
55+
func (e *OperatorGroupToggleSourceProvider) CheckForExclusion(namespaces ...string) ([]string, error) {
56+
var defaultResult = namespaces
57+
// The first namespace provided is always the current namespace being synced
58+
var ownNamespace = namespaces[0]
59+
var toggledResult = []string{ownNamespace}
60+
61+
// Check the OG on the NS provided for the exclusion annotation
62+
ogs, err := e.ogLister.OperatorGroups(ownNamespace).List(labels.Everything())
63+
if err != nil {
64+
return nil, fmt.Errorf("listing operatorgroups in namespace %s: %s", ownNamespace, err)
65+
}
66+
67+
if len(ogs) != 1 {
68+
// Problem with the operatorgroup configuration in the namespace, or the operatorgroup has not yet been persisted
69+
// Note: a resync will be triggered if/when the operatorgroup becomes available
70+
return nil, fmt.Errorf("found %d operatorgroups in namespace %s: expected 1", len(ogs), ownNamespace)
71+
}
72+
73+
var og = ogs[0]
74+
if val, ok := og.Annotations[exclusionAnnotation]; ok && val == "true" {
75+
// Exclusion specified
76+
// Ignore the globalNamespace for the purposes of resolution in this namespace
77+
e.logger.Printf("excluding global catalogs from resolution in namespace %s", ownNamespace)
78+
return toggledResult, nil
79+
}
80+
81+
return defaultResult, nil
82+
}

Diff for: pkg/controller/operators/catalog/operator.go

+33-5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"k8s.io/client-go/util/workqueue"
4343

4444
"github.com/operator-framework/api/pkg/operators/reference"
45+
operatorsv1 "github.com/operator-framework/api/pkg/operators/v1"
4546
"github.com/operator-framework/api/pkg/operators/v1alpha1"
4647
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
4748
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions"
@@ -118,7 +119,7 @@ type Operator struct {
118119
bundleUnpackTimeout time.Duration
119120
clientFactory clients.Factory
120121
muInstallPlan sync.Mutex
121-
resolverSourceProvider *resolver.RegistrySourceProvider
122+
sourceInvalidator *resolver.RegistrySourceProvider
122123
}
123124

124125
type CatalogSourceSyncFunc func(logger *logrus.Entry, in *v1alpha1.CatalogSource) (out *v1alpha1.CatalogSource, continueSync bool, syncError error)
@@ -191,9 +192,10 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
191192
clientFactory: clients.NewFactory(config),
192193
}
193194
op.sources = grpc.NewSourceStore(logger, 10*time.Second, 10*time.Minute, op.syncSourceState)
194-
op.resolverSourceProvider = resolver.SourceProviderFromRegistryClientProvider(op.sources, logger)
195+
op.sourceInvalidator = resolver.SourceProviderFromRegistryClientProvider(op.sources, logger)
196+
resolverSourceProvider := NewOperatorGroupToggleSourceProvider(op.sourceInvalidator, logger, op.lister.OperatorsV1().OperatorGroupLister())
195197
op.reconciler = reconciler.NewRegistryReconcilerFactory(lister, opClient, configmapRegistryImage, op.now, ssaClient)
196-
res := resolver.NewOperatorStepResolver(lister, crClient, operatorNamespace, op.resolverSourceProvider, logger)
198+
res := resolver.NewOperatorStepResolver(lister, crClient, operatorNamespace, resolverSourceProvider, logger)
197199
op.resolver = resolver.NewInstrumentedResolver(res, metrics.RegisterDependencyResolutionSuccess, metrics.RegisterDependencyResolutionFailure)
198200

199201
// Wire OLM CR sharedIndexInformers
@@ -259,7 +261,19 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
259261

260262
operatorGroupInformer := crInformerFactory.Operators().V1().OperatorGroups()
261263
op.lister.OperatorsV1().RegisterOperatorGroupLister(metav1.NamespaceAll, operatorGroupInformer.Lister())
262-
if err := op.RegisterInformer(operatorGroupInformer.Informer()); err != nil {
264+
ogQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ogs")
265+
op.ogQueueSet.Set(metav1.NamespaceAll, ogQueue)
266+
operatorGroupQueueInformer, err := queueinformer.NewQueueInformer(
267+
ctx,
268+
queueinformer.WithLogger(op.logger),
269+
queueinformer.WithQueue(ogQueue),
270+
queueinformer.WithInformer(operatorGroupInformer.Informer()),
271+
queueinformer.WithSyncer(queueinformer.LegacySyncHandler(op.syncOperatorGroups).ToSyncer()),
272+
)
273+
if err != nil {
274+
return nil, err
275+
}
276+
if err := op.RegisterQueueInformer(operatorGroupQueueInformer); err != nil {
263277
return nil, err
264278
}
265279

@@ -475,7 +489,7 @@ func (o *Operator) syncSourceState(state grpc.SourceState) {
475489

476490
switch state.State {
477491
case connectivity.Ready:
478-
o.resolverSourceProvider.Invalidate(resolvercache.SourceKey(state.Key))
492+
o.sourceInvalidator.Invalidate(resolvercache.SourceKey(state.Key))
479493
if o.namespace == state.Key.Namespace {
480494
namespaces, err := index.CatalogSubscriberNamespaces(o.catalogSubscriberIndexer,
481495
state.Key.Name, state.Key.Namespace)
@@ -1085,6 +1099,20 @@ func (o *Operator) syncSubscriptions(obj interface{}) error {
10851099
return nil
10861100
}
10871101

1102+
// syncOperatorGroups requeues the namespace resolution queue on changes to an operatorgroup
1103+
// This is because the operatorgroup is now an input to resolution via the global catalog exclusion annotation
1104+
func (o *Operator) syncOperatorGroups(obj interface{}) error {
1105+
og, ok := obj.(*operatorsv1.OperatorGroup)
1106+
if !ok {
1107+
o.logger.Debugf("wrong type: %#v", obj)
1108+
return fmt.Errorf("casting OperatorGroup failed")
1109+
}
1110+
1111+
o.nsResolveQueue.Add(og.GetNamespace())
1112+
1113+
return nil
1114+
}
1115+
10881116
func (o *Operator) nothingToUpdate(logger *logrus.Entry, sub *v1alpha1.Subscription) bool {
10891117
if sub.Status.InstallPlanRef != nil && sub.Status.State == v1alpha1.SubscriptionStateUpgradePending {
10901118
logger.Debugf("skipping update: installplan already created")

Diff for: pkg/controller/registry/resolver/cache/cache.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func (c *NamespacedOperatorCache) Error() error {
139139
err := snapshot.err
140140
snapshot.m.RUnlock()
141141
if err != nil {
142-
errs = append(errs, fmt.Errorf("error using catalog %s (in namespace %s): %w", key.Name, key.Namespace, err))
142+
errs = append(errs, fmt.Errorf("failed to populate resolver cache from source %v: %w", key.String(), err))
143143
}
144144
}
145145
return errors.NewAggregate(errs)

Diff for: pkg/controller/registry/resolver/cache/cache_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,5 +238,5 @@ func TestNamespaceOperatorCacheError(t *testing.T) {
238238
key: ErrorSource{Error: errors.New("testing")},
239239
})
240240

241-
require.EqualError(t, c.Namespaced("dummynamespace").Error(), "error using catalog dummyname (in namespace dummynamespace): testing")
241+
require.EqualError(t, c.Namespaced("dummynamespace").Error(), "failed to populate resolver cache from source dummyname/dummynamespace: testing")
242242
}

Diff for: pkg/controller/registry/resolver/step_resolver_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1182,7 +1182,7 @@ func TestResolver(t *testing.T) {
11821182
steps: [][]*v1alpha1.Step{},
11831183
subs: []*v1alpha1.Subscription{},
11841184
errAssert: func(t *testing.T, err error) {
1185-
assert.Contains(t, err.Error(), "error using catalog @existing (in namespace catsrc-namespace): csv")
1185+
assert.Contains(t, err.Error(), "failed to populate resolver cache from source @existing/catsrc-namespace: csv catsrc-namespace/a.v1")
11861186
assert.Contains(t, err.Error(), "in phase Failed instead of Replacing")
11871187
},
11881188
},

Diff for: test/e2e/catalog_exclusion_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
operatorsv1 "github.com/operator-framework/api/pkg/operators/v1"
10+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
11+
"github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx"
12+
"github.com/operator-framework/operator-lifecycle-manager/test/e2e/util"
13+
. "github.com/operator-framework/operator-lifecycle-manager/test/e2e/util/gomega"
14+
"google.golang.org/grpc/connectivity"
15+
corev1 "k8s.io/api/core/v1"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
k8scontrollerclient "sigs.k8s.io/controller-runtime/pkg/client"
18+
)
19+
20+
const magicCatalogDir = "magiccatalog"
21+
22+
var _ = Describe("Global Catalog Exclusion", func() {
23+
var (
24+
testNamespace corev1.Namespace
25+
determinedE2eClient *util.DeterminedE2EClient
26+
operatorGroup operatorsv1.OperatorGroup
27+
localCatalog *MagicCatalog
28+
)
29+
30+
BeforeEach(func() {
31+
determinedE2eClient = util.NewDeterminedClient(ctx.Ctx().E2EClient())
32+
33+
By("creating a namespace with an own namespace operator group without annotations")
34+
e2eTestNamespace := genName("global-catalog-exclusion-e2e-")
35+
operatorGroup = operatorsv1.OperatorGroup{
36+
ObjectMeta: metav1.ObjectMeta{
37+
Namespace: e2eTestNamespace,
38+
Name: genName("og-"),
39+
Annotations: nil,
40+
},
41+
Spec: operatorsv1.OperatorGroupSpec{
42+
TargetNamespaces: []string{e2eTestNamespace},
43+
},
44+
}
45+
testNamespace = SetupGeneratedTestNamespaceWithOperatorGroup(e2eTestNamespace, operatorGroup)
46+
47+
By("creating a broken catalog in the global namespace")
48+
globalCatalog := &v1alpha1.CatalogSource{
49+
ObjectMeta: metav1.ObjectMeta{
50+
Name: genName("bad-global-catalog-"),
51+
Namespace: operatorNamespace,
52+
},
53+
Spec: v1alpha1.CatalogSourceSpec{
54+
DisplayName: "Broken Global Catalog Source",
55+
SourceType: v1alpha1.SourceTypeGrpc,
56+
Address: "1.1.1.1:1337", // points to non-existing service
57+
},
58+
}
59+
_ = determinedE2eClient.Create(context.Background(), globalCatalog)
60+
61+
By("creating a healthy catalog in the test namespace")
62+
localCatalogName := genName("good-catsrc-")
63+
var err error = nil
64+
65+
fbcPath := filepath.Join(testdataDir, magicCatalogDir, "fbc_initial.yaml")
66+
localCatalog, err = NewMagicCatalogFromFile(determinedE2eClient, testNamespace.GetName(), localCatalogName, fbcPath)
67+
Expect(err).To(Succeed())
68+
69+
// deploy catalog blocks until the catalog has reached a ready state or fails
70+
Expect(localCatalog.DeployCatalog(context.Background())).To(Succeed())
71+
72+
By("checking that the global catalog is broken")
73+
// Adding this check here to speed up the test
74+
// the global catalog can fail while we wait for the local catalog to get to a ready state
75+
EventuallyResource(globalCatalog).Should(HaveGrpcConnectionWithLastConnectionState(connectivity.TransientFailure))
76+
})
77+
78+
AfterEach(func() {
79+
TeardownNamespace(testNamespace.GetName())
80+
})
81+
82+
When("a subscription referring to the local catalog is created", func() {
83+
var subscription *v1alpha1.Subscription
84+
85+
BeforeEach(func() {
86+
subscription = &v1alpha1.Subscription{
87+
ObjectMeta: metav1.ObjectMeta{
88+
Namespace: testNamespace.GetName(),
89+
Name: genName("local-subscription-"),
90+
},
91+
Spec: &v1alpha1.SubscriptionSpec{
92+
CatalogSource: localCatalog.GetName(),
93+
CatalogSourceNamespace: localCatalog.GetNamespace(),
94+
Package: "packageA",
95+
Channel: "stable",
96+
InstallPlanApproval: v1alpha1.ApprovalAutomatic,
97+
},
98+
}
99+
100+
By("creating a subscription")
101+
_ = determinedE2eClient.Create(context.Background(), subscription)
102+
})
103+
104+
When("the operator group is annotated with olm.operatorframework.io/exclude-global-namespace-resolution=true", func() {
105+
106+
It("the broken subscription should resolve and have state AtLatest", func() {
107+
By("checking that the subscription is not resolving and has a condition with type ResolutionFailed")
108+
EventuallyResource(subscription).Should(ContainSubscriptionConditionOfType(v1alpha1.SubscriptionResolutionFailed))
109+
110+
By("annotating the operator group with olm.operatorframework.io/exclude-global-namespace-resolution=true")
111+
Eventually(func() error {
112+
annotatedOperatorGroup := operatorGroup.DeepCopy()
113+
if err := determinedE2eClient.Get(context.Background(), k8scontrollerclient.ObjectKeyFromObject(annotatedOperatorGroup), annotatedOperatorGroup); err != nil {
114+
return err
115+
}
116+
117+
if annotatedOperatorGroup.Annotations == nil {
118+
annotatedOperatorGroup.Annotations = map[string]string{}
119+
}
120+
121+
annotatedOperatorGroup.Annotations["olm.operatorframework.io/exclude-global-namespace-resolution"] = "true"
122+
if err := determinedE2eClient.Update(context.Background(), annotatedOperatorGroup); err != nil {
123+
return err
124+
}
125+
return nil
126+
}).Should(Succeed())
127+
128+
By("checking that the subscription resolves and has state AtLatest")
129+
EventuallyResource(subscription).Should(HaveSubscriptionState(v1alpha1.SubscriptionStateAtLatest))
130+
})
131+
})
132+
})
133+
})

0 commit comments

Comments
 (0)