mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
* 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:
co-authored by
Siarhei Rasiukevich
parent
dea9b1604f
commit
f85b61860e
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user