From d027887c7f9ada5fc00f7a817afdc7004d0cde6d Mon Sep 17 00:00:00 2001 From: Maksim Fedotov Date: Mon, 16 May 2022 20:13:18 +0300 Subject: [PATCH 1/3] feat: protected tenant annotation --- api/v1beta1/tenant_annotations.go | 1 + e2e/tenant_protected_webhook_test.go | 40 +++++++++++++++++++++ main.go | 2 +- pkg/webhook/tenant/protected.go | 52 ++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 e2e/tenant_protected_webhook_test.go create mode 100644 pkg/webhook/tenant/protected.go diff --git a/api/v1beta1/tenant_annotations.go b/api/v1beta1/tenant_annotations.go index 31e5a5fa..9c7f3ad7 100644 --- a/api/v1beta1/tenant_annotations.go +++ b/api/v1beta1/tenant_annotations.go @@ -19,6 +19,7 @@ const ( ForbiddenNamespaceLabelsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-labels-regexp" ForbiddenNamespaceAnnotationsAnnotation = "capsule.clastix.io/forbidden-namespace-annotations" ForbiddenNamespaceAnnotationsRegexpAnnotation = "capsule.clastix.io/forbidden-namespace-annotations-regexp" + ProtectedTenantAnnotation = "capsule.clastix.io/protected" ) func UsedQuotaFor(resource fmt.Stringer) string { diff --git a/e2e/tenant_protected_webhook_test.go b/e2e/tenant_protected_webhook_test.go new file mode 100644 index 00000000..3ad33247 --- /dev/null +++ b/e2e/tenant_protected_webhook_test.go @@ -0,0 +1,40 @@ +//go:build e2e + +// Copyright 2020-2021 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + capsulev1beta1 "github.com/clastix/capsule/api/v1beta1" +) + +var _ = Describe("Deleting a tenant with protected annotation", func() { + tnt := &capsulev1beta1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "protected_tenant", + Annotations: map[string]string{ + capsulev1beta1.ProtectedTenantAnnotation: "", + }, + }, + Spec: capsulev1beta1.TenantSpec{ + Owners: capsulev1beta1.OwnerListSpec{ + { + Name: "john", + Kind: "User", + }, + }, + }, + } + + It("should fail", func() { + Expect(k8sClient.Create(context.TODO(), tnt)).Should(Succeed()) + Expect(k8sClient.Delete(context.TODO(), tnt)).ShouldNot(Succeed()) + }) +}) diff --git a/main.go b/main.go index 03ec791d..0c73f54e 100644 --- a/main.go +++ b/main.go @@ -217,7 +217,7 @@ func main() { route.PVC(pvc.Handler()), route.Service(service.Handler()), route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())), - route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler()), + route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler()), route.OwnerReference(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))), route.Cordoning(tenant.CordoningHandler(cfg), tenant.ResourceCounterHandler()), route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))), diff --git a/pkg/webhook/tenant/protected.go b/pkg/webhook/tenant/protected.go new file mode 100644 index 00000000..a39dd7e7 --- /dev/null +++ b/pkg/webhook/tenant/protected.go @@ -0,0 +1,52 @@ +// Copyright 2020-2022 Clastix Labs +// SPDX-License-Identifier: Apache-2.0 + +package tenant + +import ( + "context" + "fmt" + + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + capsulev1beta1 "github.com/clastix/capsule/api/v1beta1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" + "github.com/clastix/capsule/pkg/webhook/utils" +) + +type protectedHandler struct{} + +func ProtectedHandler() capsulewebhook.Handler { + return &protectedHandler{} +} + +func (h *protectedHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} + +func (h *protectedHandler) OnDelete(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + tenant := &capsulev1beta1.Tenant{} + if err := decoder.Decode(req, tenant); err != nil { + return utils.ErroredResponse(err) + } + + if _, protected := tenant.Annotations[capsulev1beta1.ProtectedTenantAnnotation]; protected { + response := admission.Denied(fmt.Sprintf("tenant is protected and cannot be deleted, remove %s annotation before proceeding", capsulev1beta1.ProtectedTenantAnnotation)) + + return &response + } + + return nil + } +} + +func (h *protectedHandler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func { + return func(context.Context, admission.Request) *admission.Response { + return nil + } +} From 2a1f4899f1b3e013a666cca5190198b3c533a927 Mon Sep 17 00:00:00 2001 From: Maksim Fedotov Date: Mon, 16 May 2022 23:46:15 +0300 Subject: [PATCH 2/3] docs: documenting protected tenants annotation --- docs/content/general/tutorial.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/content/general/tutorial.md b/docs/content/general/tutorial.md index d82e77dd..d1a0d5f5 100644 --- a/docs/content/general/tutorial.md +++ b/docs/content/general/tutorial.md @@ -1661,6 +1661,26 @@ EOF >* v1.20.6 >* v1.21.0 +## Protecting tenants from deletion + +Sometimes it is important to protect business critical tenants from accidental deletion. +This can be achieved by adding `capsule.clastix.io/protected` annotation on the tenant: + +```yaml +kubectl apply -f - << EOF +apiVersion: capsule.clastix.io/v1beta1 +kind: Tenant +metadata: + name: oil + annotations: + capsule.clastix.io/protected: "" +spec: + owners: + - name: alice + kind: User +EOF +``` + --- This ends our tutorial on how to implement complex multi-tenancy and policy-driven scenarios with Capsule. As we improve it, more use cases about multi-tenancy, policy admission control, and cluster governance will be covered in the future. From 8fe0640d6e245d2fc484886910d6ce7bc5d6f300 Mon Sep 17 00:00:00 2001 From: Maksim Fedotov Date: Tue, 17 May 2022 14:54:52 +0300 Subject: [PATCH 3/3] fix: protectedHandler OnDelete get tenant using client --- e2e/tenant_protected_webhook_test.go | 2 +- pkg/webhook/tenant/protected.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/e2e/tenant_protected_webhook_test.go b/e2e/tenant_protected_webhook_test.go index 3ad33247..e80039b8 100644 --- a/e2e/tenant_protected_webhook_test.go +++ b/e2e/tenant_protected_webhook_test.go @@ -18,7 +18,7 @@ import ( var _ = Describe("Deleting a tenant with protected annotation", func() { tnt := &capsulev1beta1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "protected_tenant", + Name: "protected-tenant", Annotations: map[string]string{ capsulev1beta1.ProtectedTenantAnnotation: "", }, diff --git a/pkg/webhook/tenant/protected.go b/pkg/webhook/tenant/protected.go index a39dd7e7..e5381bfa 100644 --- a/pkg/webhook/tenant/protected.go +++ b/pkg/webhook/tenant/protected.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "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" @@ -28,10 +29,11 @@ func (h *protectedHandler) OnCreate(client.Client, *admission.Decoder, record.Ev } } -func (h *protectedHandler) OnDelete(_ client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { +func (h *protectedHandler) OnDelete(clt client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { return func(ctx context.Context, req admission.Request) *admission.Response { tenant := &capsulev1beta1.Tenant{} - if err := decoder.Decode(req, tenant); err != nil { + + if err := clt.Get(ctx, types.NamespacedName{Name: req.AdmissionRequest.Name}, tenant); err != nil { return utils.ErroredResponse(err) }