feat: namespace metadata sync on creation #1378 (#1379)

* feat: namespace metadata sync on creation #1378

Signed-off-by: Siarhei Rasiukevich <s_rasiukevich@wargaming.net>

* fix(tenant): internal error is not returned in cordon webhook

Signed-off-by: Siarhei Rasiukevich <s_rasiukevich@wargaming.net>

* fix(utils): lint on pkg/utils/namespace_selector.go

Signed-off-by: Siarhei Rasiukevich <s_rasiukevich@wargaming.net>

---------

Signed-off-by: Siarhei Rasiukevich <s_rasiukevich@wargaming.net>
Co-authored-by: Siarhei Rasiukevich <s_rasiukevich@wargaming.net>
This commit is contained in:
Siarhei Rasiukevich
2025-05-09 06:39:12 +02:00
committed by GitHub
co-authored by Siarhei Rasiukevich
parent dea9b1604f
commit f85b61860e
9 changed files with 544 additions and 189 deletions
+48 -43
View File
@@ -52,50 +52,8 @@ func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, t
return conflictErr
}
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
res, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
annotations := buildNamespaceAnnotationsForTenant(tnt)
labels := buildNamespaceLabelsForTenant(tnt)
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
for _, md := range opts.AdditionalMetadataList {
ok, err := utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
if err != nil {
return err
}
if !ok {
continue
}
maps.Copy(labels, md.Labels)
maps.Copy(annotations, md.Annotations)
}
}
labels["kubernetes.io/metadata.name"] = namespace
labels[capsuleLabel] = tnt.GetName()
if tnt.Spec.Cordoned {
ns.Labels[utils.CordonedLabel] = "true"
} else {
delete(ns.Labels, utils.CordonedLabel)
}
if ns.Annotations == nil {
ns.SetAnnotations(annotations)
} else {
maps.Copy(ns.Annotations, annotations)
}
if ns.Labels == nil {
ns.SetLabels(labels)
} else {
maps.Copy(ns.Labels, labels)
}
return nil
return SyncNamespaceMetadata(tnt, ns)
})
return conflictErr
@@ -206,3 +164,50 @@ func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.
return
})
}
// SyncNamespaceMetadata sync namespace metadata according to tenant spec.
func SyncNamespaceMetadata(tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) error {
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
annotations := buildNamespaceAnnotationsForTenant(tnt)
labels := buildNamespaceLabelsForTenant(tnt)
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
for _, md := range opts.AdditionalMetadataList {
ok, err := utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
if err != nil {
return err
}
if !ok {
continue
}
maps.Copy(labels, md.Labels)
maps.Copy(annotations, md.Annotations)
}
}
labels["kubernetes.io/metadata.name"] = ns.GetName()
labels[capsuleLabel] = tnt.GetName()
if tnt.Spec.Cordoned {
ns.Labels[utils.CordonedLabel] = "true"
} else {
delete(ns.Labels, utils.CordonedLabel)
}
if ns.Annotations == nil {
ns.SetAnnotations(annotations)
} else {
maps.Copy(ns.Annotations, annotations)
}
if ns.Labels == nil {
ns.SetLabels(labels)
} else {
maps.Copy(ns.Labels, labels)
}
return nil
}
+95
View File
@@ -0,0 +1,95 @@
//go:build e2e
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
)
var _ = Describe("creating a Namespace for a Tenant with additional metadata", func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "cap",
Kind: "dummy",
Name: "tenant-metadata",
UID: "tenant-metadata",
},
},
},
Spec: capsulev1beta2.TenantSpec{
Owners: capsulev1beta2.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
AdditionalMetadata: &api.AdditionalMetadataSpec{
Labels: map[string]string{
"k8s.io/custom-label": "foo",
"clastix.io/custom-label": "bar",
},
Annotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
"clastix.io/custom-annotation": "buzz",
},
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should contain Namespace metadata after tenant update", func() {
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("checking labels", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newlabel", "foobazbar"))
tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels["newlabel"] = "foobazbar"
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
ok, _ = Equal(ns.Labels["newlabel"]).Match("foobazbar")
return
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
By("checking annotations", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newannotation", "foobazbar"))
tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations["newannotation"] = "foobazbar"
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
ok, _ = Equal(ns.Annotations["newannotation"]).Match("foobazbar")
return
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
})
})
+94
View File
@@ -0,0 +1,94 @@
//go:build e2e
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
)
var _ = Describe("creating a Namespace for a Tenant with additional metadata", func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "cap",
Kind: "dummy",
Name: "tenant-metadata",
UID: "tenant-metadata",
},
},
},
Spec: capsulev1beta2.TenantSpec{
Owners: capsulev1beta2.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
NodeSelector: map[string]string{
"node": "foobar",
},
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
AdditionalMetadata: &api.AdditionalMetadataSpec{
Labels: map[string]string{
"k8s.io/custom-label": "foo",
"clastix.io/custom-label": "bar",
},
Annotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
"clastix.io/custom-annotation": "buzz",
},
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should contain additional Namespace metadata", func() {
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("checking additional labels", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels {
Expect(ns.Labels).To(HaveKeyWithValue(k, v))
}
return
})
By("checking additional annotations", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations {
Expect(ns.Annotations).To(HaveKeyWithValue(k, v))
}
return
})
By("checking namespace node-selector annotation", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
Expect(ns.Annotations).Should(HaveKeyWithValue("scheduler.alpha.kubernetes.io/node-selector", "node=foobar"))
return
})
})
})
+1 -1
View File
@@ -229,7 +229,7 @@ func main() {
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler(), tenant.MetaHandler()),
route.Cordoning(tenant.CordoningHandler(cfg)),
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
route.NamespacePatch(utils.InCapsuleGroups(cfg, namespacemutation.CordoningLabelHandler(cfg), namespacemutation.OwnerReferenceHandler(cfg))),
route.NamespacePatch(utils.InCapsuleGroups(cfg, namespacemutation.CordoningLabelHandler(cfg), namespacemutation.OwnerReferenceHandler(cfg), namespacemutation.MetadataHandler(cfg))),
route.CustomResources(tenant.ResourceCounterHandler(manager.GetClient())),
route.Defaults(defaults.Handler(cfg, kubeVersion)),
)
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package utils
import (
+8 -2
View File
@@ -6,10 +6,11 @@ package mutation
import (
"context"
"encoding/json"
"net/http"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"net/http"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -48,13 +49,16 @@ func (h *cordoningLabelHandler) OnUpdate(c client.Client, decoder admission.Deco
if err := decoder.Decode(req, ns); err != nil {
return utils.ErroredResponse(err)
}
response := h.syncNamespaceCordonLabel(ctx, c, req, ns)
return response
}
}
func (h *cordoningLabelHandler) syncNamespaceCordonLabel(ctx context.Context, c client.Client, req admission.Request, ns *corev1.Namespace) *admission.Response {
tnt := &capsulev1beta2.Tenant{}
ln, err := capsuleutils.GetTypeLabel(tnt)
if err != nil {
response := admission.Errored(http.StatusInternalServerError, err)
@@ -64,7 +68,9 @@ func (h *cordoningLabelHandler) syncNamespaceCordonLabel(ctx context.Context, c
if label, ok := ns.Labels[ln]; ok {
if err = c.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
admission.Errored(http.StatusInternalServerError, err)
response := admission.Errored(http.StatusInternalServerError, err)
return &response
}
}
@@ -0,0 +1,81 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package mutation
import (
"context"
"encoding/json"
"net/http"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsuletenant "github.com/projectcapsule/capsule/controllers/tenant"
"github.com/projectcapsule/capsule/pkg/configuration"
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
)
type metadataHandler struct {
cfg configuration.Configuration
}
func MetadataHandler(cfg configuration.Configuration) capsulewebhook.Handler {
return &metadataHandler{
cfg: cfg,
}
}
func (h *metadataHandler) OnCreate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
tenant, errResponse := getNamespaceTenant(ctx, client, ns, req, h.cfg, recorder)
if errResponse != nil {
return errResponse
}
if tenant == nil {
response := admission.Denied("Unable to assign namespace to tenant.")
return &response
}
// sync namespace metadata
if err := capsuletenant.SyncNamespaceMetadata(tenant, ns); err != nil {
response := admission.Errored(http.StatusInternalServerError, err)
return &response
}
marshaled, err := json.Marshal(ns)
if err != nil {
response := admission.Errored(http.StatusInternalServerError, err)
return &response
}
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
return &response
}
}
func (h *metadataHandler) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
}
}
func (h *metadataHandler) OnUpdate(_ client.Client, _ admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
return func(_ context.Context, _ admission.Request) *admission.Response {
return nil
}
}
@@ -6,16 +6,12 @@ package mutation
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -143,114 +139,19 @@ func (h *ownerReferenceHandler) setOwnerRef(ctx context.Context, req admission.R
return &response
}
// If we already had TenantName label on NS -> assign to it
if label, ok := ns.Labels[ln]; ok {
// retrieving the selected Tenant
tnt := &capsulev1beta2.Tenant{}
if err = client.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
tnt, errResponse := getNamespaceTenant(ctx, client, ns, req, h.cfg, recorder)
if errResponse != nil {
return errResponse
}
return &response
}
// Tenant owner must adhere to user that asked for NS creation
if !utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
return &response
}
// Check if namespace needs Tenant name prefix
if errResponse := h.validateNamespacePrefix(ns, tnt); errResponse != nil {
return errResponse
}
// Patching the response
response := h.patchResponseForOwnerRef(tnt, ns, recorder)
if tnt == nil {
response := admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
return &response
}
// If we forceTenantPrefix -> find Tenant from NS name
var tenants sortedTenants
// Find tenants belonging to user (it can be regular user or ServiceAccount)
if strings.HasPrefix(req.UserInfo.Username, "system:serviceaccount:") {
var tntList *capsulev1beta2.TenantList
if tntList, err = h.listTenantsForOwnerKind(ctx, "ServiceAccount", req.UserInfo.Username, client); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
} else {
var tntList *capsulev1beta2.TenantList
if tntList, err = h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, client); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
}
// Find tenants belonging to user groups
{
for _, group := range req.UserInfo.Groups {
tntList, err := h.listTenantsForOwnerKind(ctx, "Group", group, client)
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
for _, tnt := range tntList.Items {
tenants = append(tenants, tnt)
}
}
}
sort.Sort(sort.Reverse(tenants))
if len(tenants) == 0 {
response := admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators")
return &response
}
if len(tenants) == 1 {
// Check if namespace needs Tenant name prefix
if errResponse := h.validateNamespacePrefix(ns, &tenants[0]); errResponse != nil {
return errResponse
}
response := h.patchResponseForOwnerRef(&tenants[0], ns, recorder)
return &response
}
if h.cfg.ForceTenantPrefix() {
for _, tnt := range tenants {
if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
response := h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder)
return &response
}
}
response := admission.Denied("The Namespace prefix used doesn't match any available Tenant")
return &response
}
response := admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
response := h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder)
return &response
}
@@ -280,40 +181,3 @@ func (h *ownerReferenceHandler) patchResponseForOwnerRef(tenant *capsulev1beta2.
return admission.PatchResponseFromRaw(o, c)
}
func (h *ownerReferenceHandler) listTenantsForOwnerKind(ctx context.Context, ownerKind string, ownerName string, clt client.Client) (*capsulev1beta2.TenantList, error) {
tntList := &capsulev1beta2.TenantList{}
fields := client.MatchingFields{
".spec.owner.ownerkind": fmt.Sprintf("%s:%s", ownerKind, ownerName),
}
err := clt.List(ctx, tntList, fields)
return tntList, err
}
func (h *ownerReferenceHandler) validateNamespacePrefix(ns *corev1.Namespace, tenant *capsulev1beta2.Tenant) *admission.Response {
// Check if ForceTenantPrefix is true
if tenant.Spec.ForceTenantPrefix != nil && *tenant.Spec.ForceTenantPrefix {
if !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tenant.GetName())) {
response := admission.Denied(fmt.Sprintf("The Namespace name must start with '%s-' when ForceTenantPrefix is enabled in the Tenant.", tenant.GetName()))
return &response
}
}
return nil
}
type sortedTenants []capsulev1beta2.Tenant
func (s sortedTenants) Len() int {
return len(s)
}
func (s sortedTenants) Less(i, j int) bool {
return len(s[i].GetName()) < len(s[j].GetName())
}
func (s sortedTenants) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
+207
View File
@@ -0,0 +1,207 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package mutation
import (
"context"
"fmt"
"net/http"
"sort"
"strings"
v1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/configuration"
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
)
type sortedTenants []capsulev1beta2.Tenant
func (s sortedTenants) Len() int {
return len(s)
}
func (s sortedTenants) Less(i, j int) bool {
return len(s[i].GetName()) < len(s[j].GetName())
}
func (s sortedTenants) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// getNamespaceTenant returns namespace owner tenant.
func getNamespaceTenant(
ctx context.Context,
client client.Client,
ns *corev1.Namespace,
req admission.Request,
cfg configuration.Configuration,
recorder record.EventRecorder,
) (*capsulev1beta2.Tenant, *admission.Response) {
tenant, errResponse := getTenantByLabels(ctx, client, ns, req, recorder)
if errResponse != nil {
return nil, errResponse
}
if tenant == nil {
tenant, errResponse = getTenantByUserInfo(ctx, ns, req.UserInfo, client, cfg)
if errResponse != nil {
return nil, errResponse
}
}
return tenant, nil
}
// getTenantByLabels returns tenant from labels.
func getTenantByLabels(
ctx context.Context,
client client.Client,
ns *corev1.Namespace,
req admission.Request,
recorder record.EventRecorder,
) (*capsulev1beta2.Tenant, *admission.Response) {
ln, err := capsuleutils.GetTypeLabel(&capsulev1beta2.Tenant{})
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return nil, &response
}
// Get tenant from namespace labels.
if label, ok := ns.Labels[ln]; ok {
// retrieving the selected Tenant
tnt := &capsulev1beta2.Tenant{}
if err = client.Get(ctx, types.NamespacedName{Name: label}, tnt); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return nil, &response
}
// Tenant owner must adhere to user that asked for NS creation
if !utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
return nil, &response
}
return tnt, nil
}
// Nothing found in the labels.
return nil, nil
}
// getTenantByUserInfo returns tenant list associated with admission request userinfo.
func getTenantByUserInfo(
ctx context.Context,
ns *corev1.Namespace,
userInfo v1.UserInfo,
clt client.Client,
cfg configuration.Configuration,
) (*capsulev1beta2.Tenant, *admission.Response) {
var tenants sortedTenants
// User tenants.
userTntList := &capsulev1beta2.TenantList{}
fields := client.MatchingFields{
".spec.owner.ownerkind": fmt.Sprintf("User:%s", userInfo.Username),
}
err := clt.List(ctx, userTntList, fields)
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return nil, &response
}
tenants = userTntList.Items
// ServiceAccount tenants.
if strings.HasPrefix(userInfo.Username, "system:serviceaccount:") {
saTntList := &capsulev1beta2.TenantList{}
fields = client.MatchingFields{
".spec.owner.ownerkind": fmt.Sprintf("ServiceAccount:%s", userInfo.Username),
}
err = clt.List(ctx, saTntList, fields)
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return nil, &response
}
tenants = append(tenants, saTntList.Items...)
}
// Group tenants.
groupTntList := &capsulev1beta2.TenantList{}
for _, group := range userInfo.Groups {
fields = client.MatchingFields{
".spec.owner.ownerkind": fmt.Sprintf("Group:%s", group),
}
err = clt.List(ctx, groupTntList, fields)
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return nil, &response
}
tenants = append(tenants, groupTntList.Items...)
}
sort.Sort(sort.Reverse(tenants))
if len(tenants) == 0 {
response := admission.Denied("You do not have any Tenant assigned: please, reach out to the system administrators")
return nil, &response
}
if len(tenants) == 1 {
// Check if namespace needs Tenant name prefix
if !validateNamespacePrefix(ns, &tenants[0]) {
response := admission.Denied(fmt.Sprintf("The Namespace name must start with '%s-' when ForceTenantPrefix is enabled in the Tenant.", tenants[0].GetName()))
return nil, &response
}
return &tenants[0], nil
}
if cfg.ForceTenantPrefix() {
for _, tnt := range tenants {
if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
return &tnt, nil
}
}
response := admission.Denied("The Namespace prefix used doesn't match any available Tenant")
return nil, &response
}
return nil, nil
}
func validateNamespacePrefix(ns *corev1.Namespace, tenant *capsulev1beta2.Tenant) bool {
// Check if ForceTenantPrefix is true
if tenant.Spec.ForceTenantPrefix != nil && *tenant.Spec.ForceTenantPrefix {
if !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tenant.GetName())) {
return false
}
}
return true
}