Skip to content

Commit 90c3196

Browse files
authored
Implement legacy namespace quota mode (#119)
Replaces https://hub.syn.tools/appuio-cloud/references/policies/12_namespace_quota_per_zone.html if usage profiles are disabled.
1 parent a00c8e5 commit 90c3196

File tree

4 files changed

+97
-8
lines changed

4 files changed

+97
-8
lines changed

config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ type Config struct {
6363
// AllowedLabels is a list of labels that are allowed on namespaces.
6464
// Supports '*' and '?' wildcards.
6565
AllowedLabels []string
66+
67+
// LegacyNamespaceQuota is the default quota for namespaces if no ZoneUsageProfile is selected.
68+
LegacyNamespaceQuota int
6669
}
6770

6871
func ConfigFromFile(path string) (c Config, warn []string, err error) {

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ func main() {
248248

249249
SelectedProfile: selectedUsageProfile,
250250
QuotaOverrideNamespace: conf.QuotaOverrideNamespace,
251+
252+
LegacyNamespaceQuota: conf.LegacyNamespaceQuota,
251253
},
252254
})
253255

webhooks/namespace_quota_validator.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,23 @@ type NamespaceQuotaValidator struct {
4444
UserDefaultOrganizationAnnotation string
4545

4646
// SelectedProfile is the name of the ZoneUsageProfile to use for the quota
47+
// An empty string means that the legacy namespace quota is used if set.
4748
SelectedProfile string
4849

4950
// QuotaOverrideNamespace is the namespace in which the quota overrides are stored
5051
QuotaOverrideNamespace string
52+
53+
// LegacyNamespaceQuota is the namespace quota for legacy mode.
54+
// It is used if no ZoneUsageProfile is selected.
55+
LegacyNamespaceQuota int
5156
}
5257

5358
// Handle handles the admission requests
5459
func (v *NamespaceQuotaValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
5560
ctx = log.IntoContext(ctx, log.FromContext(ctx).
5661
WithName("webhook.validate-namespace-quota.appuio.io").
5762
WithValues("id", req.UID, "user", req.UserInfo.Username).
63+
WithValues("legacyMode", v.legacyMode()).
5864
WithValues("namespace", req.Namespace, "name", req.Name,
5965
"group", req.Kind.Group, "version", req.Kind.Version, "kind", req.Kind.Kind))
6066

@@ -110,16 +116,21 @@ func (v *NamespaceQuotaValidator) handle(ctx context.Context, req admission.Requ
110116
return admission.Allowed("skipped quota validation")
111117
}
112118

113-
if v.SelectedProfile == "" {
114-
return admission.Denied("No ZoneUsageProfile selected")
115-
}
119+
var nsCountLimit int
120+
if v.legacyMode() {
121+
nsCountLimit = v.LegacyNamespaceQuota
122+
} else {
123+
if v.SelectedProfile == "" {
124+
return admission.Denied("No ZoneUsageProfile selected")
125+
}
116126

117-
var profile cloudagentv1.ZoneUsageProfile
118-
if err := v.Client.Get(ctx, types.NamespacedName{Name: v.SelectedProfile}, &profile); err != nil {
119-
l.Error(err, "error while fetching zone usage profile")
120-
return admission.Errored(http.StatusInternalServerError, err)
127+
var profile cloudagentv1.ZoneUsageProfile
128+
if err := v.Client.Get(ctx, types.NamespacedName{Name: v.SelectedProfile}, &profile); err != nil {
129+
l.Error(err, "error while fetching zone usage profile")
130+
return admission.Errored(http.StatusInternalServerError, err)
131+
}
132+
nsCountLimit = profile.Spec.UpstreamSpec.NamespaceCount
121133
}
122-
nsCountLimit := profile.Spec.UpstreamSpec.NamespaceCount
123134

124135
var overrideCM corev1.ConfigMap
125136
if err := v.Client.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("override-%s", organizationName), Namespace: v.QuotaOverrideNamespace}, &overrideCM); err == nil {
@@ -152,6 +163,11 @@ func (v *NamespaceQuotaValidator) handle(ctx context.Context, req admission.Requ
152163
return admission.Allowed("allowed")
153164
}
154165

166+
// legacyMode returns true if the legacy namespace quota is set and no ZoneUsageProfile is selected.
167+
func (v *NamespaceQuotaValidator) legacyMode() bool {
168+
return v.SelectedProfile == "" && v.LegacyNamespaceQuota > 0
169+
}
170+
155171
// logAdmissionResponse logs the admission response to the logger derived from the given context and returns it unchanged.
156172
func logAdmissionResponse(ctx context.Context, res admission.Response) admission.Response {
157173
l := log.FromContext(ctx)

webhooks/namespace_quota_validator_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func TestNamespaceQuotaValidator_Handle(t *testing.T) {
3131
allowed bool
3232
skipQuotaValidation bool
3333
matchMessage string
34+
disableProfile bool
35+
legacyQuota int
3436
}{
3537
"Allow Namespace": {
3638
initObjects: []client.Object{
@@ -261,6 +263,67 @@ func TestNamespaceQuotaValidator_Handle(t *testing.T) {
261263
skipQuotaValidation: true,
262264
allowed: false,
263265
},
266+
267+
"LegacyMode: Allow Namespace": {
268+
initObjects: []client.Object{
269+
newNamespace("a", map[string]string{orgLabel: "other"}, nil), newNamespace("b", map[string]string{orgLabel: "other"}, nil),
270+
newNamespace("an", nil, nil), newNamespace("bn", nil, nil),
271+
},
272+
object: &corev1.Namespace{
273+
ObjectMeta: metav1.ObjectMeta{
274+
Name: "test",
275+
Labels: map[string]string{
276+
orgLabel: "testorg",
277+
},
278+
},
279+
},
280+
allowed: true,
281+
legacyQuota: 1,
282+
disableProfile: true,
283+
},
284+
"LegacyMode: Deny Namespace TooMany": {
285+
initObjects: []client.Object{
286+
newNamespace("a", map[string]string{orgLabel: "testorg"}, nil),
287+
},
288+
object: &corev1.Namespace{
289+
ObjectMeta: metav1.ObjectMeta{
290+
Name: "test",
291+
Labels: map[string]string{
292+
orgLabel: "testorg",
293+
},
294+
},
295+
},
296+
allowed: false,
297+
legacyQuota: 1,
298+
disableProfile: true,
299+
},
300+
"LegacyMode: Allow Namespace Override": {
301+
initObjects: []client.Object{
302+
newNamespace("a", map[string]string{orgLabel: "testorg"}, nil),
303+
newNamespace("b", map[string]string{orgLabel: "testorg"}, nil),
304+
newNamespace("c", map[string]string{orgLabel: "testorg"}, nil),
305+
&corev1.ConfigMap{
306+
ObjectMeta: metav1.ObjectMeta{
307+
Name: "override-testorg",
308+
Namespace: "test",
309+
},
310+
Data: map[string]string{
311+
"namespaceQuota": "4",
312+
},
313+
},
314+
},
315+
object: &corev1.Namespace{
316+
ObjectMeta: metav1.ObjectMeta{
317+
Name: "test",
318+
Labels: map[string]string{
319+
orgLabel: "testorg",
320+
},
321+
},
322+
},
323+
allowed: true,
324+
legacyQuota: 1,
325+
disableProfile: true,
326+
},
264327
}
265328

266329
for name, test := range tests {
@@ -278,6 +341,11 @@ func TestNamespaceQuotaValidator_Handle(t *testing.T) {
278341

279342
SelectedProfile: "test",
280343
QuotaOverrideNamespace: "test",
344+
LegacyNamespaceQuota: test.legacyQuota,
345+
}
346+
347+
if test.disableProfile {
348+
subject.SelectedProfile = ""
281349
}
282350

283351
require.NoError(t, c.Create(ctx, &cloudagentv1.ZoneUsageProfile{

0 commit comments

Comments
 (0)