Skip to content

Commit 9193d36

Browse files
committed
feat(api): Add forceTenantPrefix option to Tenant spec
Signed-off-by: samir-tahir <samirtahir91@gmail.com>
1 parent f82c2f4 commit 9193d36

File tree

7 files changed

+209
-2
lines changed

7 files changed

+209
-2
lines changed

api/v1beta2/tenant_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ type TenantSpec struct {
5656
// When enabled, the deletion request will be declined.
5757
//+kubebuilder:default:=false
5858
PreventDeletion bool `json:"preventDeletion,omitempty"`
59+
// Use this if you want to disable/enable the Tenant name prefix to specific Tenants, overriding global forceTenantPrefix in CapsuleConfiguration.
60+
// When set to 'true', it enforces Namespaces created for this Tenant to be named with the Tenant name prefix,
61+
// separated by a dash (i.e. for Tenant 'foo', namespace names must be prefixed with 'foo-'),
62+
// this is useful to avoid Namespace name collision.
63+
// When set to 'false', it allows Namespaces created for this Tenant to be named anything.
64+
// Overrides CapsuleConfiguration global forceTenantPrefix for the Tenant only.
65+
// If unset, Tenant uses CapsuleConfiguration's forceTenantPrefix
66+
// Optional
67+
ForceTenantPrefix *bool `json:"forceTenantPrefix,omitempty"`
5968
}
6069

6170
// +kubebuilder:object:root=true

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/capsule/crds/capsule.clastix.io_tenants.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,17 @@ spec:
11491149
description: Toggling the Tenant resources cordoning, when enable
11501150
resources cannot be deleted.
11511151
type: boolean
1152+
forceTenantPrefix:
1153+
description: |-
1154+
Use this if you want to disable/enable the Tenant name prefix to specific Tenants, overriding global forceTenantPrefix in CapsuleConfiguration.
1155+
When set to 'true', it enforces Namespaces created for this Tenant to be named with the Tenant name prefix,
1156+
separated by a dash (i.e. for Tenant 'foo', namespace names must be prefixed with 'foo-'),
1157+
this is useful to avoid Namespace name collision.
1158+
When set to 'false', it allows Namespaces created for this Tenant to be named anything.
1159+
Overrides CapsuleConfiguration global forceTenantPrefix for the Tenant only.
1160+
If unset, Tenant uses CapsuleConfiguration's forceTenantPrefix
1161+
Optional
1162+
type: boolean
11521163
imagePullPolicies:
11531164
description: Specify the allowed values for the imagePullPolicies
11541165
option in Pod resources. Capsule assures that all Pod resources
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//go:build e2e
2+
3+
// Copyright 2020-2023 Project Capsule Authors.
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package e2e
7+
8+
import (
9+
"context"
10+
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
15+
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
16+
)
17+
18+
var _ = Describe("creating a Namespace with Tenant name prefix enforcement at Tenant scope", func() {
19+
t1 := &capsulev1beta2.Tenant{
20+
ObjectMeta: metav1.ObjectMeta{
21+
Name: "awesome",
22+
},
23+
Spec: capsulev1beta2.TenantSpec{
24+
ForceTenantPrefix: &[]bool{true}[0],
25+
Owners: capsulev1beta2.OwnerListSpec{
26+
{
27+
Name: "john",
28+
Kind: "User",
29+
},
30+
},
31+
},
32+
}
33+
t2 := &capsulev1beta2.Tenant{
34+
ObjectMeta: metav1.ObjectMeta{
35+
Name: "awesome-tenant",
36+
},
37+
Spec: capsulev1beta2.TenantSpec{
38+
ForceTenantPrefix: &[]bool{false}[0],
39+
Owners: capsulev1beta2.OwnerListSpec{
40+
{
41+
Name: "john",
42+
Kind: "User",
43+
},
44+
},
45+
},
46+
}
47+
48+
JustBeforeEach(func() {
49+
EventuallyCreation(func() error {
50+
t1.ResourceVersion = ""
51+
return k8sClient.Create(context.TODO(), t1)
52+
}).Should(Succeed())
53+
EventuallyCreation(func() error {
54+
t2.ResourceVersion = ""
55+
return k8sClient.Create(context.TODO(), t2)
56+
}).Should(Succeed())
57+
})
58+
JustAfterEach(func() {
59+
Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed())
60+
Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed())
61+
62+
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
63+
configuration.Spec.ForceTenantPrefix = false
64+
})
65+
})
66+
67+
It("should fail when not using prefix, with tenant label for a tenant with ForceTenantPrefix true and global ForceTenantPrefix false", func() {
68+
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
69+
configuration.Spec.ForceTenantPrefix = false
70+
})
71+
labels := map[string]string{
72+
"capsule.clastix.io/tenant": t1.GetName(),
73+
}
74+
ns := NewNamespace("awesome", labels)
75+
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
76+
})
77+
78+
It("should fail using prefix without capsule.clastix.io/tenant label, where the user owns more than one Tenant, for a tenant with ForceTenantPrefix true and global ForceTenantPrefix false", func() {
79+
ns := NewNamespace("awesome-namespace")
80+
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
81+
})
82+
83+
It("should fail using prefix without capsule.clastix.io/tenant label, where the user owns more than one Tenant, for a tenant with ForceTenantPrefix false and global ForceTenantPrefix true", func() {
84+
ns := NewNamespace("awesome-namespace")
85+
NamespaceCreation(ns, t2.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
86+
})
87+
88+
It("should succeed and be assigned with prefix and label, for a tenant with ForceTenantPrefix true and global ForceTenantPrefix false", func() {
89+
labels := map[string]string{
90+
"capsule.clastix.io/tenant": t1.GetName(),
91+
}
92+
ns := NewNamespace("awesome-tenant", labels)
93+
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
94+
95+
TenantNamespaceList(t1, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
96+
})
97+
98+
It("should fail when not using prefix, with tenant label for a tenant with ForceTenantPrefix true and global ForceTenantPrefix true", func() {
99+
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
100+
configuration.Spec.ForceTenantPrefix = true
101+
})
102+
labels := map[string]string{
103+
"capsule.clastix.io/tenant": t1.GetName(),
104+
}
105+
ns := NewNamespace("awesome", labels)
106+
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
107+
})
108+
109+
It("should succeed and be assigned with prefix and label, for a tenant with ForceTenantPrefix true and global ForceTenantPrefix true", func() {
110+
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
111+
configuration.Spec.ForceTenantPrefix = true
112+
})
113+
labels := map[string]string{
114+
"capsule.clastix.io/tenant": t1.GetName(),
115+
}
116+
ns := NewNamespace("awesome-tenant", labels)
117+
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
118+
119+
TenantNamespaceList(t1, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
120+
})
121+
122+
It("should fail using prefix without capsule.clastix.io/tenant label, for a tenant with ForceTenantPrefix true and global ForceTenantPrefix true", func() {
123+
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
124+
configuration.Spec.ForceTenantPrefix = true
125+
})
126+
ns := NewNamespace("awesome-namespace")
127+
NamespaceCreation(ns, t1.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed())
128+
})
129+
130+
It("should succeed when not using prefix, with tenant label for a tenant with ForceTenantPrefix false and global ForceTenantPrefix false", func() {
131+
labels := map[string]string{
132+
"capsule.clastix.io/tenant": t2.GetName(),
133+
}
134+
ns := NewNamespace("awesome", labels)
135+
NamespaceCreation(ns, t2.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
136+
})
137+
138+
It("should succeed when not using prefix, with tenant label for a tenant with ForceTenantPrefix false and global ForceTenantPrefix true", func() {
139+
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
140+
configuration.Spec.ForceTenantPrefix = true
141+
})
142+
labels := map[string]string{
143+
"capsule.clastix.io/tenant": t2.GetName(),
144+
}
145+
ns := NewNamespace("awesome", labels)
146+
NamespaceCreation(ns, t2.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
147+
})
148+
})

e2e/utils_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,20 @@ func ServiceCreation(svc *corev1.Service, owner capsulev1beta2.OwnerSpec, timeou
5353
}, timeout, defaultPollInterval)
5454
}
5555

56-
func NewNamespace(name string) *corev1.Namespace {
56+
func NewNamespace(name string, labels ...map[string]string) *corev1.Namespace {
5757
if len(name) == 0 {
5858
name = rand.String(10)
5959
}
6060

61+
var namespaceLabels map[string]string
62+
if len(labels) > 0 {
63+
namespaceLabels = labels[0]
64+
}
65+
6166
return &corev1.Namespace{
6267
ObjectMeta: metav1.ObjectMeta{
63-
Name: name,
68+
Name: name,
69+
Labels: namespaceLabels,
6470
},
6571
}
6672
}

pkg/webhook/namespace/prefix.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func (r *prefixHandler) OnCreate(clt client.Client, decoder admission.Decoder, r
5959
return utils.ErroredResponse(err)
6060
}
6161

62+
// Check for Tenant-level ForceTenantPrefix override
63+
if tnt.Spec.ForceTenantPrefix != nil && !*tnt.Spec.ForceTenantPrefix {
64+
return nil
65+
}
66+
6267
if e := fmt.Sprintf("%s-%s", tnt.GetName(), ns.GetName()); !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
6368
recorder.Eventf(tnt, corev1.EventTypeWarning, "InvalidTenantPrefix", "Namespace %s does not match the expected prefix for the current Tenant", ns.GetName())
6469

pkg/webhook/ownerreference/patching.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client
144144
return &response
145145
}
146146
// If we already had TenantName label on NS -> assign to it
147+
147148
if label, ok := ns.ObjectMeta.Labels[ln]; ok {
148149
// retrieving the selected Tenant
149150
tnt := &capsulev1beta2.Tenant{}
@@ -160,6 +161,10 @@ func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client
160161

161162
return &response
162163
}
164+
// Check if namespace needs Tenant name prefix
165+
if errResponse := h.validateNamespacePrefix(ns, tnt); errResponse != nil {
166+
return errResponse
167+
}
163168
// Patching the response
164169
response := h.patchResponseForOwnerRef(tnt, ns, recorder)
165170

@@ -221,6 +226,11 @@ func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client
221226
}
222227

223228
if len(tenants) == 1 {
229+
// Check if namespace needs Tenant name prefix
230+
if errResponse := h.validateNamespacePrefix(ns, &tenants[0]); errResponse != nil {
231+
return errResponse
232+
}
233+
224234
response := h.patchResponseForOwnerRef(&tenants[0], ns, recorder)
225235

226236
return &response
@@ -281,6 +291,19 @@ func (h *handler) listTenantsForOwnerKind(ctx context.Context, ownerKind string,
281291
return tntList, err
282292
}
283293

294+
func (h *handler) validateNamespacePrefix(ns *corev1.Namespace, tenant *capsulev1beta2.Tenant) *admission.Response {
295+
// Check if ForceTenantPrefix is true
296+
if tenant.Spec.ForceTenantPrefix != nil && *tenant.Spec.ForceTenantPrefix {
297+
if !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tenant.GetName())) {
298+
response := admission.Denied(fmt.Sprintf("The Namespace name must start with '%s-' when ForceTenantPrefix is enabled in the Tenant.", tenant.GetName()))
299+
300+
return &response
301+
}
302+
}
303+
304+
return nil
305+
}
306+
284307
type sortedTenants []capsulev1beta2.Tenant
285308

286309
func (s sortedTenants) Len() int {

0 commit comments

Comments
 (0)