Skip to content

Commit 0c6ce57

Browse files
committed
Add CSV Namespace Plug-In
Signed-off-by: perdasilva <[email protected]>
1 parent c3671cb commit 0c6ce57

File tree

4 files changed

+796
-0
lines changed

4 files changed

+796
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package olm
2+
3+
import (
4+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm/plugins"
5+
)
6+
7+
func init() {
8+
operatorPlugInFactoryFuncs = []plugins.OperatorPlugInFactoryFunc{
9+
// labels unlabeled non-payload openshift-* csv namespaces with
10+
// security.openshift.io/scc.podSecurityLabelSync: true
11+
plugins.NewCsvNamespaceLabelerPluginFunc,
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
package plugins
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/openshift/cluster-policy-controller/pkg/psalabelsyncer/nsexemptions"
10+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
11+
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
12+
listerv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1"
13+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubestate"
14+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
15+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer"
16+
"github.com/sirupsen/logrus"
17+
v1 "k8s.io/api/core/v1"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/labels"
20+
"k8s.io/apimachinery/pkg/runtime"
21+
"k8s.io/apimachinery/pkg/watch"
22+
listerv1 "k8s.io/client-go/listers/core/v1"
23+
"k8s.io/client-go/tools/cache"
24+
"k8s.io/client-go/util/workqueue"
25+
)
26+
27+
const NamespaceLabelSyncerLabelKey = "security.openshift.io/scc.podSecurityLabelSync"
28+
const openshiftPrefix = "openshift-"
29+
30+
const noCopiedCsvSelector = "!" + v1alpha1.CopiedLabelKey
31+
32+
// csvNamespaceLabelerPlugin is responsible for labeling non-payload openshift-* namespaces
33+
// with the label "security.openshift.io/scc.podSecurityLabelSync=true" so that the PSA Label Syncer
34+
// see https://github.com/openshift/cluster-policy-controller/blob/master/pkg/psalabelsyncer/podsecurity_label_sync_controller.go
35+
// can help ensure that the operator payloads in the namespace continue to work even if they don't yet respect the
36+
// upstream Pod Security Admission controller, which will become active in k8s 1.25.
37+
// see https://kubernetes.io/docs/concepts/security/pod-security-admission/
38+
// If a CSV is created or modified, this controller will look at the csv's namespace. If it is a non-payload namespace,
39+
// if the namespace name is prefixed with 'openshift-', and if the namespace does not contain the label (whatever
40+
// value it may be set to), it will add the "security.openshift.io/scc.podSecurityLabelSync=true" to the namespace.
41+
type csvNamespaceLabelerPlugin struct {
42+
namespaceLister listerv1.NamespaceLister
43+
nonCopiedCsvListerMap map[string]listerv1alpha1.ClusterServiceVersionLister
44+
kubeClient operatorclient.ClientInterface
45+
externalClient versioned.Interface
46+
logger *logrus.Logger
47+
}
48+
49+
func NewCsvNamespaceLabelerPluginFunc(ctx context.Context, config OperatorConfig, hostOperator HostOperator) (OperatorPlugin, error) {
50+
51+
if hostOperator == nil {
52+
return nil, fmt.Errorf("cannot initialize plugin: operator undefined")
53+
}
54+
55+
plugin := &csvNamespaceLabelerPlugin{
56+
kubeClient: config.OperatorClient(),
57+
externalClient: config.ExternalClient(),
58+
logger: config.Logger(),
59+
namespaceLister: nil,
60+
nonCopiedCsvListerMap: map[string]listerv1alpha1.ClusterServiceVersionLister{},
61+
}
62+
63+
plugin.log("setting up csv namespace plug-in for namespaces: %s", config.WatchedNamespaces())
64+
65+
namespaceInformer := newNamespaceInformer(config.OperatorClient(), config.ResyncPeriod()())
66+
67+
plugin.log("registering namespace informer")
68+
69+
plugin.namespaceLister = listerv1.NewNamespaceLister(namespaceInformer.GetIndexer())
70+
71+
namespaceQueue := workqueue.NewNamedRateLimitingQueue(
72+
workqueue.DefaultControllerRateLimiter(),
73+
"csv-ns-labeler-plugin-ns-queue",
74+
)
75+
namespaceQueueInformer, err := queueinformer.NewQueueInformer(
76+
ctx,
77+
queueinformer.WithInformer(namespaceInformer),
78+
queueinformer.WithLogger(config.Logger()),
79+
queueinformer.WithQueue(namespaceQueue),
80+
queueinformer.WithIndexer(namespaceInformer.GetIndexer()),
81+
queueinformer.WithSyncer(plugin),
82+
)
83+
if err != nil {
84+
return nil, err
85+
}
86+
if err := hostOperator.RegisterQueueInformer(namespaceQueueInformer); err != nil {
87+
return nil, err
88+
}
89+
90+
for _, namespace := range config.WatchedNamespaces() {
91+
plugin.log("setting up namespace: %s", namespace)
92+
// ignore namespaces that are *NOT* prefixed with openshift- but accept metav1.NamespaceAll
93+
if !(hasOpenshiftPrefix(namespace)) && namespace != metav1.NamespaceAll {
94+
continue
95+
}
96+
97+
nonCopiedCsvInformer := newNonCopiedCsvInformerForNamespace(namespace, config.ExternalClient(), config.ResyncPeriod()())
98+
99+
nonCopiedCsvQueue := workqueue.NewNamedRateLimitingQueue(
100+
workqueue.DefaultControllerRateLimiter(),
101+
fmt.Sprintf("%s/csv-ns-labeler-plugin-csv-queue", namespace),
102+
)
103+
nonCopiedCsvQueueInformer, err := queueinformer.NewQueueInformer(
104+
ctx,
105+
queueinformer.WithInformer(nonCopiedCsvInformer),
106+
queueinformer.WithLogger(config.Logger()),
107+
queueinformer.WithQueue(nonCopiedCsvQueue),
108+
queueinformer.WithIndexer(nonCopiedCsvInformer.GetIndexer()),
109+
queueinformer.WithSyncer(plugin),
110+
)
111+
if err != nil {
112+
return nil, err
113+
}
114+
if err := hostOperator.RegisterQueueInformer(nonCopiedCsvQueueInformer); err != nil {
115+
return nil, err
116+
}
117+
plugin.nonCopiedCsvListerMap[namespace] = listerv1alpha1.NewClusterServiceVersionLister(nonCopiedCsvInformer.GetIndexer())
118+
plugin.log("registered csv queue informer for: %s", namespace)
119+
}
120+
plugin.log("finished setting up csv namespace labeler plugin")
121+
122+
return plugin, nil
123+
}
124+
125+
func (p *csvNamespaceLabelerPlugin) Shutdown() error {
126+
return nil
127+
}
128+
129+
func (p *csvNamespaceLabelerPlugin) Sync(ctx context.Context, event kubestate.ResourceEvent) error {
130+
// only act on csv added and updated events
131+
if event.Type() != kubestate.ResourceAdded && event.Type() != kubestate.ResourceUpdated {
132+
return nil
133+
}
134+
135+
var namespace *v1.Namespace
136+
var err error
137+
138+
// get namespace from the event resource
139+
switch eventResource := event.Resource().(type) {
140+
141+
// handle csv events
142+
case *v1alpha1.ClusterServiceVersion:
143+
// ignore copied csvs and namespaces that should be ignored
144+
if eventResource.IsCopied() || ignoreNamespace(eventResource.GetNamespace()) {
145+
return nil
146+
}
147+
148+
namespace, err = p.getNamespace(eventResource.GetNamespace())
149+
if err != nil {
150+
return fmt.Errorf("error getting csv namespace (%s) for label sync'er labeling", eventResource.GetNamespace())
151+
}
152+
153+
// handle namespace events
154+
case *v1.Namespace:
155+
// ignore namespaces that should be ignored and ones that are already labeled
156+
if ignoreNamespace(eventResource.GetName()) || hasLabelSyncerLabel(eventResource) {
157+
return nil
158+
}
159+
160+
// get csv count for namespace
161+
csvCount, err := p.countClusterServiceVersions(eventResource.GetName())
162+
if err != nil {
163+
return fmt.Errorf("error counting csvs in namespace=%s: %s", eventResource.GetName(), err)
164+
}
165+
166+
// ignore namespaces with no csvs
167+
if csvCount <= 0 {
168+
return nil
169+
}
170+
171+
namespace = eventResource
172+
default:
173+
return fmt.Errorf("event resource is neither a ClusterServiceVersion or a Namespace")
174+
}
175+
176+
// add label sync'er label if it does not exist
177+
if !(hasLabelSyncerLabel(namespace)) {
178+
if err := applyLabelSyncerLabel(ctx, p.kubeClient, namespace); err != nil {
179+
return fmt.Errorf("error updating csv namespace (%s) with label sync'er label", namespace.GetNamespace())
180+
}
181+
p.log("applied %s=true label to namespace %s", NamespaceLabelSyncerLabelKey, namespace.GetNamespace())
182+
}
183+
184+
return nil
185+
}
186+
187+
func (p *csvNamespaceLabelerPlugin) getNamespace(namespace string) (*v1.Namespace, error) {
188+
ns, err := p.namespaceLister.Get(namespace)
189+
if err != nil {
190+
return nil, err
191+
}
192+
return ns, nil
193+
}
194+
195+
func (p *csvNamespaceLabelerPlugin) countClusterServiceVersions(namespace string) (int, error) {
196+
lister, ok := p.nonCopiedCsvListerMap[namespace]
197+
if !ok {
198+
lister, ok = p.nonCopiedCsvListerMap[metav1.NamespaceAll]
199+
if !ok {
200+
return 0, fmt.Errorf("no csv indexer found for namespace: %s", namespace)
201+
}
202+
}
203+
labelSelector, err := labels.Parse(noCopiedCsvSelector)
204+
if err != nil {
205+
return 0, err
206+
}
207+
208+
csvList, err := lister.ClusterServiceVersions(namespace).List(labelSelector)
209+
if err != nil {
210+
return 0, err
211+
}
212+
return len(csvList), nil
213+
}
214+
215+
func (p *csvNamespaceLabelerPlugin) log(format string, args ...interface{}) {
216+
if p.logger != nil {
217+
p.logger.Infof("[CSV NS Plug-in] "+format, args...)
218+
}
219+
}
220+
221+
// newNamespaceInformer creates a namespace informer that filters namespaces the plug-in is not interested in:
222+
// payload namespaces (except openshift-operators) and non openshift- prefixed namespaces
223+
// the informer also prunes the namespace objects to only keep basic type and object metadata and annotations
224+
func newNamespaceInformer(k8sClient operatorclient.ClientInterface, resyncPeriod time.Duration) cache.SharedIndexInformer {
225+
// create a namespace informer
226+
pruneNamespace := func(namespace *v1.Namespace) {
227+
namespace = &v1.Namespace{
228+
TypeMeta: namespace.TypeMeta,
229+
ObjectMeta: metav1.ObjectMeta{
230+
Name: namespace.GetName(),
231+
Namespace: namespace.GetNamespace(),
232+
Annotations: namespace.GetAnnotations(),
233+
},
234+
}
235+
}
236+
237+
return cache.NewSharedIndexInformer(
238+
&cache.ListWatch{
239+
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
240+
list, err := k8sClient.KubernetesInterface().CoreV1().Namespaces().List(context.Background(), options)
241+
if err != nil {
242+
return list, err
243+
}
244+
245+
// filter and prune namespaces
246+
var filteredList []v1.Namespace
247+
for i := range list.Items {
248+
ns := list.Items[i]
249+
if !(ignoreNamespace(ns.GetName())) {
250+
pruneNamespace(&ns)
251+
filteredList = append(filteredList, ns)
252+
}
253+
}
254+
return &v1.NamespaceList{
255+
Items: filteredList,
256+
}, nil
257+
},
258+
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
259+
nsWatch, err := k8sClient.KubernetesInterface().CoreV1().Namespaces().Watch(context.Background(), options)
260+
if err != nil {
261+
return nsWatch, err
262+
}
263+
return watch.Filter(nsWatch, func(e watch.Event) (watch.Event, bool) {
264+
if ns, ok := e.Object.(*v1.Namespace); ok {
265+
if !(ignoreNamespace(ns.GetName())) {
266+
pruneNamespace(ns)
267+
return e, true
268+
}
269+
}
270+
return e, false
271+
}), nil
272+
},
273+
},
274+
&v1.Namespace{},
275+
resyncPeriod,
276+
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
277+
)
278+
}
279+
280+
// newNonCopiedCsvInformerForNamespace creates a csv-based informer that filters out copied csvs and csv events coming
281+
// from namespaces the plug-in is not interested in: payload namespaces (except openshift-operators) and
282+
// non openshift- prefixed namespaces
283+
// the informer also prunes the csvs to only keep basic type and object metadata and annotations
284+
func newNonCopiedCsvInformerForNamespace(namespace string, externalClient versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
285+
// create a new csv informer and prune status to reduce memory footprint
286+
pruneCSV := func(csv *v1alpha1.ClusterServiceVersion) {
287+
csv = &v1alpha1.ClusterServiceVersion{
288+
TypeMeta: csv.TypeMeta,
289+
ObjectMeta: metav1.ObjectMeta{
290+
Name: csv.GetName(),
291+
Namespace: csv.GetNamespace(),
292+
Annotations: csv.GetAnnotations(),
293+
},
294+
}
295+
}
296+
297+
return cache.NewSharedIndexInformer(
298+
&cache.ListWatch{
299+
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
300+
options.LabelSelector = noCopiedCsvSelector
301+
list, err := externalClient.OperatorsV1alpha1().ClusterServiceVersions(namespace).List(context.Background(), options)
302+
if err != nil {
303+
return list, err
304+
}
305+
306+
// filter and prune csvs
307+
var filteredList []v1alpha1.ClusterServiceVersion
308+
for i := range list.Items {
309+
csv := list.Items[i]
310+
if !(ignoreNamespace(csv.GetNamespace())) {
311+
pruneCSV(&csv)
312+
filteredList = append(filteredList, csv)
313+
}
314+
}
315+
return &v1alpha1.ClusterServiceVersionList{
316+
Items: filteredList,
317+
}, nil
318+
},
319+
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
320+
options.LabelSelector = noCopiedCsvSelector
321+
csvWatch, err := externalClient.OperatorsV1alpha1().ClusterServiceVersions(namespace).Watch(context.Background(), options)
322+
if err != nil {
323+
return csvWatch, err
324+
}
325+
return watch.Filter(csvWatch, func(e watch.Event) (watch.Event, bool) {
326+
if csv, ok := e.Object.(*v1alpha1.ClusterServiceVersion); ok {
327+
if !(ignoreNamespace(csv.GetNamespace())) && !csv.IsCopied() {
328+
pruneCSV(csv)
329+
return e, true
330+
}
331+
}
332+
return e, false
333+
}), nil
334+
},
335+
},
336+
&v1alpha1.ClusterServiceVersion{},
337+
resyncPeriod,
338+
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
339+
)
340+
}
341+
342+
func hasOpenshiftPrefix(namespaceName string) bool {
343+
return strings.HasPrefix(namespaceName, openshiftPrefix)
344+
}
345+
346+
func ignoreNamespace(namespace string) bool {
347+
// ignore non-openshift-* and payload openshift-* namespaces
348+
return !hasOpenshiftPrefix(namespace) || nsexemptions.IsNamespacePSALabelSyncExemptedInVendoredOCPVersion(namespace)
349+
}
350+
351+
func applyLabelSyncerLabel(ctx context.Context, kubeClient operatorclient.ClientInterface, namespace *v1.Namespace) error {
352+
if _, ok := namespace.GetLabels()[NamespaceLabelSyncerLabelKey]; !ok {
353+
nsCopy := namespace.DeepCopy()
354+
if nsCopy.GetLabels() == nil {
355+
nsCopy.SetLabels(map[string]string{})
356+
}
357+
nsCopy.GetLabels()[NamespaceLabelSyncerLabelKey] = "true"
358+
if _, err := kubeClient.KubernetesInterface().CoreV1().Namespaces().Update(ctx, nsCopy, metav1.UpdateOptions{}); err != nil {
359+
return err
360+
}
361+
}
362+
return nil
363+
}
364+
365+
func hasLabelSyncerLabel(namespace *v1.Namespace) bool {
366+
_, ok := namespace.GetLabels()[NamespaceLabelSyncerLabelKey]
367+
return ok
368+
}

0 commit comments

Comments
 (0)