Skip to content

Commit 091f594

Browse files
authored
Merge pull request #166 from appuio/agent-unmanaged
Allow unmanaged (no RBAC, LimitRange, Quotas, ...) organization namespaces (lbl: `appuio.io/unmanaged-namespace`)
1 parent f9113e6 commit 091f594

13 files changed

Lines changed: 100 additions & 33 deletions

config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import (
1515
type Config struct {
1616
// OrganizationLabel is the label used to mark namespaces to belong to an organization
1717
OrganizationLabel string
18+
// UnmanagedOrganizationNamespaceLabel is the label used to mark namespaces that should not be
19+
// managed by this controller or its webhooks and this should not be restricted by resource quotas
20+
// and limit ranges, even if they belong to an organization.
21+
// The controller doesn't look at the value of this label, it only looks for the label and skips any namespaces that have both the organization label and this label.
22+
UnmanagedOrganizationNamespaceLabel string
1823

1924
// UserDefaultOrganizationAnnotation is the annotation the default organization setting for a user is stored in.
2025
UserDefaultOrganizationAnnotation string
@@ -106,6 +111,9 @@ func (c Config) Validate() error {
106111
if c.OrganizationLabel == "" {
107112
errs = append(errs, errors.New("OrganizationLabel must not be empty"))
108113
}
114+
if c.UnmanagedOrganizationNamespaceLabel == "" {
115+
errs = append(errs, errors.New("UnmanagedOrganizationNamespaceLabel must not be empty"))
116+
}
109117

110118
return multierr.Combine(errs...)
111119
}

config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
# The label used to mark namespaces to belong to an organization
33
OrganizationLabel: appuio.io/organization
4+
# The label used to mark namespaces that are not managed by the controller or its webhooks, but still belong to an organization.
5+
UnmanagedOrganizationNamespaceLabel: appuio.io/unmanaged-namespace
46
# UserDefaultOrganizationAnnotation is the annotation the default organization setting for a user is stored in.
57
UserDefaultOrganizationAnnotation: appuio.io/default-organization
68

controllers/legacy_resource_quota_controller.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"go.uber.org/multierr"
1010
corev1 "k8s.io/api/core/v1"
1111
"k8s.io/apimachinery/pkg/api/resource"
12+
"k8s.io/apimachinery/pkg/labels"
1213
"k8s.io/apimachinery/pkg/runtime"
1314
"k8s.io/client-go/tools/record"
1415
ctrl "sigs.k8s.io/controller-runtime"
@@ -24,7 +25,7 @@ type LegacyResourceQuotaReconciler struct {
2425
Scheme *runtime.Scheme
2526
Recorder record.EventRecorder
2627

27-
OrganizationLabel string
28+
NamespaceSelector labels.Selector
2829

2930
ResourceQuotaAnnotationBase string
3031
DefaultResourceQuotas map[string]corev1.ResourceQuotaSpec
@@ -46,8 +47,8 @@ func (r *LegacyResourceQuotaReconciler) Reconcile(ctx context.Context, req ctrl.
4647
return ctrl.Result{}, nil
4748
}
4849

49-
if _, ok := ns.Labels[r.OrganizationLabel]; !ok {
50-
l.Info("Namespace does not have organization label, skipping reconciliation")
50+
if !r.NamespaceSelector.Matches(labels.Set(ns.Labels)) {
51+
l.Info("Namespace does not match namespace selector, skipping reconciliation")
5152
return ctrl.Result{}, nil
5253
}
5354

@@ -127,7 +128,7 @@ func (r *LegacyResourceQuotaReconciler) Reconcile(ctx context.Context, req ctrl.
127128

128129
// SetupWithManager sets up the controller with the Manager.
129130
func (r *LegacyResourceQuotaReconciler) SetupWithManager(mgr ctrl.Manager) error {
130-
orgPredicate, err := labelExistsPredicate(r.OrganizationLabel)
131+
orgPredicate, err := labelSelectorPredicate(r.NamespaceSelector)
131132
if err != nil {
132133
return fmt.Errorf("failed to create organization label predicate: %w", err)
133134
}

controllers/legacy_resource_quota_controller_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/stretchr/testify/require"
1010
corev1 "k8s.io/api/core/v1"
1111
"k8s.io/apimachinery/pkg/api/resource"
12+
"k8s.io/apimachinery/pkg/labels"
1213
"k8s.io/apimachinery/pkg/types"
1314
"k8s.io/utils/ptr"
1415
ctrl "sigs.k8s.io/controller-runtime"
@@ -28,7 +29,7 @@ func Test_LegacyResourceQuotaReconciler_Reconcile(t *testing.T) {
2829
Scheme: scheme,
2930
Recorder: recorder,
3031

31-
OrganizationLabel: "organization",
32+
NamespaceSelector: labels.SelectorFromValidatedSet(labels.Set{"organization": "testorg"}),
3233

3334
ResourceQuotaAnnotationBase: "resourcequota.example.com",
3435
DefaultResourceQuotas: map[string]corev1.ResourceQuotaSpec{

controllers/org_rbac_controller.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ package controllers
22

33
import (
44
"context"
5+
"fmt"
56
"strconv"
67

78
"go.uber.org/multierr"
89
corev1 "k8s.io/api/core/v1"
910
rbacv1 "k8s.io/api/rbac/v1"
1011
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/labels"
1113
"k8s.io/client-go/tools/record"
1214

1315
"k8s.io/apimachinery/pkg/runtime"
1416
ctrl "sigs.k8s.io/controller-runtime"
17+
"sigs.k8s.io/controller-runtime/pkg/builder"
1518
"sigs.k8s.io/controller-runtime/pkg/client"
1619
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1720
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -23,6 +26,8 @@ type OrganizationRBACReconciler struct {
2326
Recorder record.EventRecorder
2427
Scheme *runtime.Scheme
2528

29+
NamespaceSelector labels.Selector
30+
2631
// OrganizationLabel is the label that marks to what organization (if any) the namespace belongs to
2732
OrganizationLabel string
2833
// DefaultClusterRoles is a map where the keys are the name of default rolebindings to create and the values are the names of the clusterroles they bind to
@@ -60,6 +65,11 @@ func (r *OrganizationRBACReconciler) Reconcile(ctx context.Context, req ctrl.Req
6065
return ctrl.Result{}, nil
6166
}
6267

68+
if !r.NamespaceSelector.Matches(labels.Set(ns.Labels)) {
69+
l.Info("Namespace does not match namespace selector, skipping reconciliation")
70+
return ctrl.Result{}, nil
71+
}
72+
6373
org := r.getOrganization(ns)
6474
if org == "" {
6575
return ctrl.Result{}, nil
@@ -143,8 +153,12 @@ func rolebindingIsUninitialized(rolebinding *rbacv1.RoleBinding) bool {
143153

144154
// SetupWithManager sets up the controller with the Manager.
145155
func (r *OrganizationRBACReconciler) SetupWithManager(mgr ctrl.Manager) error {
156+
p, err := labelSelectorPredicate(r.NamespaceSelector)
157+
if err != nil {
158+
return fmt.Errorf("failed to create predicate: %w", err)
159+
}
146160
return ctrl.NewControllerManagedBy(mgr).
147-
For(&corev1.Namespace{}).
161+
For(&corev1.Namespace{}, builder.WithPredicates(p)).
148162
Owns(&rbacv1.RoleBinding{}).
149163
Complete(r)
150164
}

controllers/org_rbac_controller_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
corev1 "k8s.io/api/core/v1"
1212
rbacv1 "k8s.io/api/rbac/v1"
1313
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/labels"
1415
"k8s.io/apimachinery/pkg/runtime"
1516
"k8s.io/apimachinery/pkg/types"
1617
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -407,11 +408,15 @@ func prepareOranizationRBACTest(t *testing.T, cfg testOrganizationRBACfg) *Organ
407408
cfg.recorder = &record.FakeRecorder{}
408409
}
409410

411+
sel, err := labels.Parse(cfg.organizationLabel)
412+
require.NoError(t, err)
413+
410414
return &OrganizationRBACReconciler{
411415
Client: client,
412416
Recorder: cfg.recorder,
413417
Scheme: scheme,
414418
OrganizationLabel: cfg.organizationLabel,
419+
NamespaceSelector: sel,
415420
DefaultClusterRoles: cfg.clusterRoles,
416421
}
417422
}

controllers/zoneusageprofileapply_controller.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88

99
"go.uber.org/multierr"
1010
corev1 "k8s.io/api/core/v1"
11-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1211
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12+
"k8s.io/apimachinery/pkg/labels"
1313
"k8s.io/apimachinery/pkg/runtime"
1414
"k8s.io/apimachinery/pkg/runtime/schema"
1515
"k8s.io/client-go/tools/record"
@@ -46,7 +46,7 @@ type ZoneUsageProfileApplyReconciler struct {
4646
// watchTracker keeps track of which resources are already being watched.
4747
watchTracker sync.Map
4848

49-
OrganizationLabel string
49+
NamespaceSelector labels.Selector
5050
Transformers []transformers.Transformer
5151

5252
// SelectedProfile applies only selected profile, if set. Dynamic selection is in future tickets.
@@ -79,7 +79,7 @@ func (r *ZoneUsageProfileApplyReconciler) Reconcile(ctx context.Context, req ctr
7979
}
8080

8181
var orgNsl corev1.NamespaceList
82-
if err := r.Client.List(ctx, &orgNsl, client.HasLabels{r.OrganizationLabel}); err != nil {
82+
if err := r.Client.List(ctx, &orgNsl, client.MatchingLabelsSelector{Selector: r.NamespaceSelector}); err != nil {
8383
l.Error(err, "unable to list Namespaces")
8484
return ctrl.Result{}, err
8585
}
@@ -180,7 +180,7 @@ func (r *ZoneUsageProfileApplyReconciler) ensureWatch(ctx context.Context, gvk s
180180

181181
// SetupWithManager sets up the controller with the Manager.
182182
func (r *ZoneUsageProfileApplyReconciler) SetupWithManager(mgr ctrl.Manager) error {
183-
orgPredicate, err := labelExistsPredicate(r.OrganizationLabel)
183+
orgPredicate, err := labelSelectorPredicate(r.NamespaceSelector)
184184
if err != nil {
185185
return fmt.Errorf("unable to create LabelSelectorPredicate: %w", err)
186186
}
@@ -201,13 +201,11 @@ func (r *ZoneUsageProfileApplyReconciler) SetupWithManager(mgr ctrl.Manager) err
201201
return nil
202202
}
203203

204-
// labelExistsPredicate returns a predicate that matches objects with the given label.
205-
func labelExistsPredicate(label string) (predicate.Predicate, error) {
206-
return predicate.LabelSelectorPredicate(metav1.LabelSelector{
207-
MatchExpressions: []metav1.LabelSelectorRequirement{{
208-
Key: label,
209-
Operator: metav1.LabelSelectorOpExists,
210-
}}})
204+
// labelSelectorPredicate returns a predicate that matches objects with the given label.
205+
func labelSelectorPredicate(sel labels.Selector) (predicate.Predicate, error) {
206+
return predicate.NewPredicateFuncs(func(o client.Object) bool {
207+
return sel.Matches(labels.Set(o.GetLabels()))
208+
}), nil
211209
}
212210

213211
// mapToAllUsageProfiles returns a MapFunc that enqueues reconcile requests for all ZoneUsageProfiles on every event.

controllers/zoneusageprofileapply_controller_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"k8s.io/apimachinery/pkg/api/resource"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
"k8s.io/apimachinery/pkg/labels"
1718
"k8s.io/apimachinery/pkg/runtime"
1819
"k8s.io/apimachinery/pkg/types"
1920
"sigs.k8s.io/controller-runtime/pkg/event"
@@ -53,13 +54,16 @@ func Test_ZoneUsageProfileApplyReconciler_Reconcile(t *testing.T) {
5354
org2NS := newNamespace("org2", map[string]string{orgLbl: "bar"}, nil)
5455
require.NoError(t, c.Create(context.Background(), org2NS))
5556

57+
nsSel, err := labels.Parse(orgLbl)
58+
require.NoError(t, err)
59+
5660
subject := &ZoneUsageProfileApplyReconciler{
5761
Client: c,
5862
Scheme: scheme,
5963
Recorder: recorder,
6064
Cache: mgr.GetCache(),
6165

62-
OrganizationLabel: orgLbl,
66+
NamespaceSelector: nsSel,
6367

6468
Transformers: []transformers.Transformer{
6569
addTestAnnotationTransformer{},
@@ -121,9 +125,11 @@ func requireEventually(t *testing.T, f func(collect *assert.CollectT), msgAndArg
121125
require.EventuallyWithT(t, f, 10*time.Second, time.Second/10, msgAndArgs...)
122126
}
123127

124-
func Test_labelExistsPredicate(t *testing.T) {
128+
func Test_labelSelectorPredicate(t *testing.T) {
125129
lbl := "test.com/organization"
126-
subject, err := labelExistsPredicate(lbl)
130+
sel, err := labels.Parse(lbl)
131+
require.NoError(t, err)
132+
subject, err := labelSelectorPredicate(sel)
127133
require.NoError(t, err)
128134

129135
assert.True(t, subject.Generic(event.GenericEvent{

main.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
userv1 "github.com/openshift/api/user/v1"
1414
"go.uber.org/multierr"
1515
authenticationv1 "k8s.io/api/authentication/v1"
16+
"k8s.io/apimachinery/pkg/labels"
1617
"k8s.io/apimachinery/pkg/runtime"
18+
"k8s.io/apimachinery/pkg/selection"
1719
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
1820
"k8s.io/client-go/kubernetes"
1921
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -184,8 +186,14 @@ func main() {
184186
os.Exit(1)
185187
}
186188

187-
registerRatioController(mgr, conf, conf.OrganizationLabel)
188-
registerOrganizationRBACController(mgr, conf.OrganizationLabel, conf.DefaultOrganizationClusterRoles)
189+
nsSel, err := newNamespaceLabelSelector(conf.OrganizationLabel, conf.UnmanagedOrganizationNamespaceLabel)
190+
if err != nil {
191+
setupLog.Error(err, "unable to create namespace label selector")
192+
os.Exit(1)
193+
}
194+
195+
registerRatioController(mgr, conf, nsSel)
196+
registerOrganizationRBACController(mgr, conf.OrganizationLabel, nsSel, conf.DefaultOrganizationClusterRoles)
189197
registerZoneK8sVersionController(mgr, controlAPICluster, upstreamZoneIdentifier)
190198

191199
if !disableUserAttributeSync {
@@ -232,7 +240,7 @@ func main() {
232240
Recorder: mgr.GetEventRecorderFor("usage-profile-apply-controller"),
233241
Cache: mgr.GetCache(),
234242

235-
OrganizationLabel: conf.OrganizationLabel,
243+
NamespaceSelector: nsSel,
236244
Transformers: []transformers.Transformer{
237245
transformers.NewResourceQuotaTransformer("resourcequota.appuio.io"),
238246
},
@@ -249,7 +257,7 @@ func main() {
249257
Scheme: mgr.GetScheme(),
250258
Recorder: mgr.GetEventRecorderFor("legacy-resource-quota-controller"),
251259

252-
OrganizationLabel: conf.OrganizationLabel,
260+
NamespaceSelector: nsSel,
253261
ResourceQuotaAnnotationBase: conf.LegacyResourceQuotaAnnotationBase,
254262
DefaultResourceQuotas: conf.LegacyDefaultResourceQuotas,
255263
LimitRangeName: conf.LegacyLimitRangeName,
@@ -280,6 +288,7 @@ func main() {
280288
SkipValidateQuota: disableUsageProfiles && !legacyNamespaceQuotaEnabled,
281289

282290
OrganizationLabel: conf.OrganizationLabel,
291+
NamespaceSelector: nsSel,
283292
UserDefaultOrganizationAnnotation: conf.UserDefaultOrganizationAnnotation,
284293

285294
SelectedProfile: selectedUsageProfile,
@@ -412,21 +421,22 @@ func registerNodeSelectorValidationWebhooks(mgr ctrl.Manager, conf Config) {
412421
})
413422
}
414423

415-
func registerOrganizationRBACController(mgr ctrl.Manager, orgLabel string, defaultClusterRoles map[string]string) {
424+
func registerOrganizationRBACController(mgr ctrl.Manager, orgLabel string, nsSel labels.Selector, defaultClusterRoles map[string]string) {
416425
if err := (&controllers.OrganizationRBACReconciler{
417426
Client: mgr.GetClient(),
418427
Recorder: mgr.GetEventRecorderFor("organization-rbac-controller"),
419428
Scheme: mgr.GetScheme(),
420429

421430
OrganizationLabel: orgLabel,
431+
NamespaceSelector: nsSel,
422432
DefaultClusterRoles: defaultClusterRoles,
423433
}).SetupWithManager(mgr); err != nil {
424-
setupLog.Error(err, "unable to create controller", "controller", "ratio")
434+
setupLog.Error(err, "unable to create controller", "controller", "organization-rbac")
425435
os.Exit(1)
426436
}
427437
}
428438

429-
func registerRatioController(mgr ctrl.Manager, conf Config, orgLabel string) {
439+
func registerRatioController(mgr ctrl.Manager, conf Config, nsSel labels.Selector) {
430440
mgr.GetWebhookServer().Register("/validate-request-ratio", &webhook.Admission{
431441
Handler: &webhooks.RatioValidator{
432442
DefaultNodeSelector: conf.DefaultNodeSelector,
@@ -450,7 +460,7 @@ func registerRatioController(mgr ctrl.Manager, conf Config, orgLabel string) {
450460
RatioLimits: conf.MemoryPerCoreLimits,
451461
Ratio: &ratio.Fetcher{
452462
Client: mgr.GetClient(),
453-
OrganizationLabel: orgLabel,
463+
NamespaceSelector: nsSel,
454464
},
455465
RatioWarnThreshold: conf.MemoryPerCoreWarnThreshold,
456466
}).SetupWithManager(mgr); err != nil {
@@ -479,3 +489,15 @@ func registerZoneK8sVersionController(mgr ctrl.Manager, controlAPICluster cluste
479489
os.Exit(1)
480490
}
481491
}
492+
493+
func newNamespaceLabelSelector(orgLabel, unmanagedOrgNsLabel string) (labels.Selector, error) {
494+
orgReq, err := labels.NewRequirement(orgLabel, selection.Exists, nil)
495+
if err != nil {
496+
return nil, err
497+
}
498+
unmanagedReq, err := labels.NewRequirement(unmanagedOrgNsLabel, selection.DoesNotExist, nil)
499+
if err != nil {
500+
return nil, err
501+
}
502+
return labels.NewSelector().Add(*orgReq, *unmanagedReq), nil
503+
}

ratio/fetcher.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var ErrorDisabled error = errors.New("request ratio validation disabled")
2020
type Fetcher struct {
2121
Client client.Client
2222

23-
OrganizationLabel string
23+
NamespaceSelector labels.Selector
2424
}
2525

2626
// FetchRatios collects the CPU to memory request ratio for the given namespace grouped by `.spec.nodeSelector`.
@@ -41,8 +41,8 @@ func (f Fetcher) FetchRatios(ctx context.Context, name string) (map[string]*Rati
4141
}
4242
}
4343

44-
if f.OrganizationLabel != "" {
45-
if _, isOrgNs := ns.Labels[f.OrganizationLabel]; !isOrgNs {
44+
if f.NamespaceSelector != nil {
45+
if !f.NamespaceSelector.Matches(labels.Set(ns.Labels)) {
4646
return nil, ErrorDisabled
4747
}
4848
}

0 commit comments

Comments
 (0)