From a99153cbe70432d7e21ad5a2610ba737b1ba27d3 Mon Sep 17 00:00:00 2001 From: Maxim Fedotov Date: Wed, 2 Sep 2020 13:43:02 +0300 Subject: [PATCH] Add protected-namespace-regex (#73) --- README.md | 2 + e2e/custom_capsule_group_test.go | 10 ++--- e2e/ingress_class_test.go | 12 ++--- e2e/new_namespace_test.go | 4 +- e2e/overquota_namespace_test.go | 4 +- e2e/owner_webhooks_test.go | 28 ++++++------ e2e/protected_namespace_regex_test.go | 63 +++++++++++++++++++++++++++ e2e/resource_quota_exceeded_test.go | 12 ++--- e2e/selecting_tenant_test.go | 4 +- e2e/storage_class_test.go | 8 ++-- e2e/tenant_resources_changes_test.go | 4 +- e2e/tenant_resources_test.go | 6 +-- e2e/utils_test.go | 29 ++++++------ main.go | 13 +++++- pkg/webhook/tenant_prefix/patching.go | 22 +++++++--- 15 files changed, 155 insertions(+), 66 deletions(-) create mode 100644 e2e/protected_namespace_regex_test.go diff --git a/README.md b/README.md index 2a983e01..f260009c 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ Log verbosity of the Capsule controller can be increased by passing the `--zap-l During startup Capsule controller will create additional ClusterRoles `capsule-namespace:deleter`, `capsule-namespace:provisioner` and ClusterRoleBinding `capsule-namespace:provisioner`. These resources are used in order to allow Capsule users to manage their namespaces in tenants. +You can disallow users to create namespaces matching a particular regexp by passing `--protected-namespace-regex` option with a value of regular expression. + ## Admission Controllers Capsule implements Kubernetes multi-tenancy capabilities using a minimum set of standard [Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) enabled on the Kubernetes APIs server: `--enable-admission-plugins=PodNodeSelector,LimitRanger,ResourceQuota,MutatingAdmissionWebhook,ValidatingAdmissionWebhook`. In addition to these default controllers, Capsule implements its own set of Admission Controllers through the [Dynamic Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/), providing callbacks to add further validation or resource patching. diff --git a/e2e/custom_capsule_group_test.go b/e2e/custom_capsule_group_test.go index d09900e9..dd8f96e2 100644 --- a/e2e/custom_capsule_group_test.go +++ b/e2e/custom_capsule_group_test.go @@ -54,16 +54,16 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro It("should fail", func() { args := append(defaulManagerPodArgs, []string{"--capsule-user-group=test"}...) ModifyCapsuleManagerPodArgs(args) - CapsuleClusterGroupParamShouldBeUpdated("test") + CapsuleClusterGroupParamShouldBeUpdated("test", podRecreationTimeoutInterval) ns := NewNamespace("cg-namespace-fail") - NamespaceCreationShouldNotSucceed(ns, tnt) + NamespaceCreationShouldNotSucceed(ns, tnt, podRecreationTimeoutInterval) }) It("should succeed and be available in Tenant namespaces list", func() { ModifyCapsuleManagerPodArgs(defaulManagerPodArgs) - CapsuleClusterGroupParamShouldBeUpdated("capsule.clastix.io") + CapsuleClusterGroupParamShouldBeUpdated("capsule.clastix.io", podRecreationTimeoutInterval) ns := NewNamespace("cg-namespace") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, podRecreationTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, podRecreationTimeoutInterval) }) }) diff --git a/e2e/ingress_class_test.go b/e2e/ingress_class_test.go index 9984a3cd..7249f175 100644 --- a/e2e/ingress_class_test.go +++ b/e2e/ingress_class_test.go @@ -65,8 +65,8 @@ var _ = Describe("when Tenant handles Ingress classes", func() { ns := NewNamespace("ingress-class-disallowed") cs := ownerClient(tnt) - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) By("non-specifying the class", func() { Eventually(func() (err error) { @@ -128,8 +128,8 @@ var _ = Describe("when Tenant handles Ingress classes", func() { ns := NewNamespace("ingress-class-allowed-annotation") cs := ownerClient(tnt) - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) for _, c := range tnt.Spec.IngressClasses { Eventually(func() (err error) { @@ -168,8 +168,8 @@ var _ = Describe("when Tenant handles Ingress classes", func() { Skip("Running test ont Kubernetes " + v.String() + ", doesn't provide .spec.ingressClassName") } - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) for _, c := range tnt.Spec.IngressClasses { Eventually(func() (err error) { diff --git a/e2e/new_namespace_test.go b/e2e/new_namespace_test.go index c631fe8d..18b90877 100644 --- a/e2e/new_namespace_test.go +++ b/e2e/new_namespace_test.go @@ -52,7 +52,7 @@ var _ = Describe("creating a Namespace as Tenant owner", func() { }) It("should be available in Tenant namespaces list", func() { ns := NewNamespace("new-namespace") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) }) }) diff --git a/e2e/overquota_namespace_test.go b/e2e/overquota_namespace_test.go index 576db2e5..d29ac01e 100644 --- a/e2e/overquota_namespace_test.go +++ b/e2e/overquota_namespace_test.go @@ -54,8 +54,8 @@ var _ = Describe("creating a Namespace over-quota", func() { By("creating three Namespaces", func() { for _, name := range []string{"bob-dev", "bob-staging", "bob-production"} { ns := NewNamespace(name) - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) } }) diff --git a/e2e/owner_webhooks_test.go b/e2e/owner_webhooks_test.go index 7f515de4..ef177cd6 100644 --- a/e2e/owner_webhooks_test.go +++ b/e2e/owner_webhooks_test.go @@ -103,8 +103,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { It("should disallow deletions", func() { By("blocking Capsule Limit ranges", func() { ns := NewNamespace("limit-range-disallow") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) lr := &corev1.LimitRange{} Eventually(func() error { @@ -117,8 +117,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { }) By("blocking Capsule Network Policy", func() { ns := NewNamespace("network-policy-disallow") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) np := &networkingv1.NetworkPolicy{} Eventually(func() error { @@ -131,8 +131,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { }) By("blocking blocking Capsule Resource Quota", func() { ns := NewNamespace("resource-quota-disallow") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) rq := &corev1.ResourceQuota{} Eventually(func() error { @@ -147,8 +147,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { It("should allow listing", func() { By("Limit Range resources", func() { ns := NewNamespace("limit-range-list") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) Eventually(func() (err error) { cs := ownerClient(tnt) @@ -158,8 +158,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { }) By("Network Policy resources", func() { ns := NewNamespace("network-policy-list") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) Eventually(func() (err error) { cs := ownerClient(tnt) @@ -169,8 +169,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { }) By("Resource Quota resources", func() { ns := NewNamespace("resource-quota-list") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) Eventually(func() (err error) { cs := ownerClient(tnt) @@ -181,8 +181,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { }) It("should allow all actions to Tenant owner Network Policy resources", func() { ns := NewNamespace("network-policy-allow") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) cs := ownerClient(tnt) np := &networkingv1.NetworkPolicy{ diff --git a/e2e/protected_namespace_regex_test.go b/e2e/protected_namespace_regex_test.go new file mode 100644 index 00000000..dce57813 --- /dev/null +++ b/e2e/protected_namespace_regex_test.go @@ -0,0 +1,63 @@ +//+build e2e + +/* +Copyright 2020 Clastix Labs. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/clastix/capsule/api/v1alpha1" +) + +var _ = Describe("creating a Namespace with --protected-namespace-regex enabled", func() { + tnt := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenantprotectednamespace", + }, + Spec: v1alpha1.TenantSpec{ + Owner: "alice", + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + JustBeforeEach(func() { + tnt.ResourceVersion = "" + Expect(k8sClient.Create(context.TODO(), tnt)).Should(Succeed()) + }) + JustAfterEach(func() { + Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + }) + It("should succeed and be available in Tenant namespaces list", func() { + args := append(defaulManagerPodArgs, []string{"--protected-namespace-regex=^.*[-.]system$"}...) + ModifyCapsuleManagerPodArgs(args) + ns := NewNamespace("test-ok") + NamespaceCreationShouldSucceed(ns, tnt, podRecreationTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, podRecreationTimeoutInterval) + }) + It("should fail", func() { + ModifyCapsuleManagerPodArgs(defaulManagerPodArgs) + ns := NewNamespace("test-system") + NamespaceCreationShouldNotSucceed(ns, tnt, podRecreationTimeoutInterval) + }) +}) diff --git a/e2e/resource_quota_exceeded_test.go b/e2e/resource_quota_exceeded_test.go index 3adbe693..bad90573 100644 --- a/e2e/resource_quota_exceeded_test.go +++ b/e2e/resource_quota_exceeded_test.go @@ -91,8 +91,8 @@ var _ = Describe("exceeding Tenant resource quota", func() { }, }, NetworkPolicies: []networkingv1.NetworkPolicySpec{}, - NamespaceQuota: 2, - NodeSelector: map[string]string{}, + NamespaceQuota: 2, + NodeSelector: map[string]string{}, ResourceQuota: []corev1.ResourceQuotaSpec{ { Hard: map[corev1.ResourceName]resource.Quantity{ @@ -125,8 +125,8 @@ var _ = Describe("exceeding Tenant resource quota", func() { By("creating the Namespaces", func() { for _, i := range nsl { ns := NewNamespace(i) - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) } }) }) @@ -157,7 +157,7 @@ var _ = Describe("exceeding Tenant resource quota", func() { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "my-pause", + Name: "my-pause", Image: "gcr.io/google_containers/pause-amd64:3.0", }, }, @@ -204,7 +204,7 @@ var _ = Describe("exceeding Tenant resource quota", func() { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { - Name: "my-exceeded", + Name: "my-exceeded", Image: "gcr.io/google_containers/pause-amd64:3.0", }, }, diff --git a/e2e/selecting_tenant_test.go b/e2e/selecting_tenant_test.go index 80e4f610..293cfd70 100644 --- a/e2e/selecting_tenant_test.go +++ b/e2e/selecting_tenant_test.go @@ -75,7 +75,7 @@ var _ = Describe("creating a Namespace with Tenant selector", func() { l: t2.Name, } }) - NamespaceCreationShouldSucceed(ns, t2) - NamespaceShouldBeManagedByTenant(ns, t2) + NamespaceCreationShouldSucceed(ns, t2, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, t2, defaultTimeoutInterval) }) }) diff --git a/e2e/storage_class_test.go b/e2e/storage_class_test.go index 138d9d42..eff5cc4f 100644 --- a/e2e/storage_class_test.go +++ b/e2e/storage_class_test.go @@ -60,8 +60,8 @@ var _ = Describe("when Tenant handles Storage classes", func() { }) It("should block non allowed Storage Class", func() { ns := NewNamespace("storage-class-disallowed") - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) By("non-specifying the class", func() { Eventually(func() (err error) { @@ -108,8 +108,8 @@ var _ = Describe("when Tenant handles Storage classes", func() { ns := NewNamespace("storage-class-allowed") cs := ownerClient(tnt) - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) for _, c := range tnt.Spec.StorageClasses { Eventually(func() (err error) { diff --git a/e2e/tenant_resources_changes_test.go b/e2e/tenant_resources_changes_test.go index 341753a2..893f2d56 100644 --- a/e2e/tenant_resources_changes_test.go +++ b/e2e/tenant_resources_changes_test.go @@ -168,8 +168,8 @@ var _ = Describe("changing Tenant managed Kubernetes resources", func() { By("creating the Namespaces", func() { for _, i := range nsl { ns := NewNamespace(i) - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) } }) }) diff --git a/e2e/tenant_resources_test.go b/e2e/tenant_resources_test.go index 29b5dfeb..7be230ec 100644 --- a/e2e/tenant_resources_test.go +++ b/e2e/tenant_resources_test.go @@ -27,7 +27,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - "k8s.io/api/networking/v1" + v1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -170,8 +170,8 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() { By("creating the Namespaces", func() { for _, i := range nsl { ns := NewNamespace(i) - NamespaceCreationShouldSucceed(ns, tnt) - NamespaceShouldBeManagedByTenant(ns, tnt) + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) } }) }) diff --git a/e2e/utils_test.go b/e2e/utils_test.go index 75646ad4..46fcc7ca 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -34,8 +34,9 @@ import ( ) const ( - defaultTimeoutInterval = 25 * time.Second - defaultPollInterval = time.Second + defaultTimeoutInterval = 15 * time.Second + podRecreationTimeoutInterval = 90 * time.Second + defaultPollInterval = time.Second ) func NewNamespace(name string) *corev1.Namespace { @@ -46,36 +47,36 @@ func NewNamespace(name string) *corev1.Namespace { } } -func NamespaceCreationShouldSucceed(ns *corev1.Namespace, t *v1alpha1.Tenant) { +func NamespaceCreationShouldSucceed(ns *corev1.Namespace, t *v1alpha1.Tenant, timeout time.Duration) { cs := ownerClient(t) Eventually(func() (err error) { _, err = cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) return - }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) + }, timeout, defaultPollInterval).Should(Succeed()) } -func NamespaceCreationShouldNotSucceed(ns *corev1.Namespace, t *v1alpha1.Tenant) { +func NamespaceCreationShouldNotSucceed(ns *corev1.Namespace, t *v1alpha1.Tenant, timeout time.Duration) { cs := ownerClient(t) Eventually(func() (err error) { _, err = cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) return - }, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed()) + }, timeout, defaultPollInterval).ShouldNot(Succeed()) } -func NamespaceShouldBeManagedByTenant(ns *corev1.Namespace, t *v1alpha1.Tenant) { +func NamespaceShouldBeManagedByTenant(ns *corev1.Namespace, t *v1alpha1.Tenant, timeout time.Duration) { Eventually(func() v1alpha1.NamespaceList { Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: t.GetName()}, t)).Should(Succeed()) return t.Status.Namespaces - }, defaultTimeoutInterval, defaultPollInterval).Should(ContainElement(ns.GetName())) + }, timeout, defaultPollInterval).Should(ContainElement(ns.GetName())) } -func CapsuleClusterGroupParamShouldBeUpdated(capsuleClusterGroup string) { +func CapsuleClusterGroupParamShouldBeUpdated(capsuleClusterGroup string, timeout time.Duration) { capsuleCRB := &rbacv1.ClusterRoleBinding{} Eventually(func() string { Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "capsule-namespace:provisioner"}, capsuleCRB)).Should(Succeed()) return capsuleCRB.Subjects[0].Name - }, defaultTimeoutInterval, defaultPollInterval).Should(BeIdenticalTo(capsuleClusterGroup)) + }, timeout, defaultPollInterval).Should(BeIdenticalTo(capsuleClusterGroup)) } @@ -100,15 +101,17 @@ func ModifyCapsuleManagerPodArgs(args []string) { } } return containerArgs - }, defaultTimeoutInterval, defaultPollInterval).Should(HaveLen(len(args))) + }, podRecreationTimeoutInterval, defaultPollInterval).Should(HaveLen(len(args))) pl := &corev1.PodList{} Eventually(func() []corev1.Pod { Expect(k8sClient.List(context.TODO(), pl, client.MatchingLabels{"control-plane": "controller-manager"})).Should(Succeed()) return pl.Items - }, defaultTimeoutInterval, defaultPollInterval).Should(HaveLen(2)) + }, podRecreationTimeoutInterval, defaultPollInterval).Should(HaveLen(2)) Eventually(func() []corev1.Pod { Expect(k8sClient.List(context.TODO(), pl, client.MatchingLabels{"control-plane": "controller-manager"})).Should(Succeed()) return pl.Items - }, defaultTimeoutInterval, defaultPollInterval).Should(HaveLen(1)) + }, podRecreationTimeoutInterval, defaultPollInterval).Should(HaveLen(1)) + // had to add sleep in order to manager be started + time.Sleep(defaultTimeoutInterval) } diff --git a/main.go b/main.go index cc391634..3b76b3c6 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "flag" "fmt" "os" + "regexp" goRuntime "runtime" corev1 "k8s.io/api/core/v1" @@ -73,6 +74,8 @@ func main() { var forceTenantPrefix bool var v bool var capsuleGroup string + var protectedNamespaceRegexpString string + var protectedNamespaceRegexp *regexp.Regexp flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&capsuleGroup, "capsule-user-group", capsulev1alpha1.GroupVersion.Group, "Name of the group for capsule users") @@ -83,6 +86,7 @@ func main() { flag.BoolVar(&forceTenantPrefix, "force-tenant-prefix", false, "Enforces the Tenant owner, "+ "during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash. "+ "This is useful to avoid Namespace name collision in a public CaaS environment.") + flag.StringVar(&protectedNamespaceRegexpString, "protected-namespace-regex", "", "Disallow creation of namespaces, whose name matches this regexp") opts := zap.Options{} opts.BindFlags(flag.CommandLine) flag.Parse() @@ -106,6 +110,13 @@ func main() { setupLog.Error(err, "unable to start manager") os.Exit(1) } + if len(protectedNamespaceRegexpString) > 0 { + protectedNamespaceRegexp, err = regexp.Compile(protectedNamespaceRegexpString) + if err != nil { + setupLog.Error(err, "unable to compile protected-namespace-regex", "protected-namespace-regex", protectedNamespaceRegexp) + os.Exit(1) + } + } _ = mgr.AddReadyzCheck("ping", healthz.Ping) _ = mgr.AddHealthzCheck("ping", healthz.Ping) @@ -122,7 +133,7 @@ func main() { //webhooks wl := make([]webhook.Webhook, 0) - wl = append(wl, &ingress.ExtensionIngress{}, &ingress.NetworkIngress{}, pvc.Webhook{}, &owner_reference.Webhook{}, &namespace_quota.Webhook{}, network_policies.Webhook{}, tenant_prefix.Webhook{ForceTenantPrefix: forceTenantPrefix}) + wl = append(wl, &ingress.ExtensionIngress{}, &ingress.NetworkIngress{}, pvc.Webhook{}, &owner_reference.Webhook{}, &namespace_quota.Webhook{}, network_policies.Webhook{}, tenant_prefix.Webhook{ForceTenantPrefix: forceTenantPrefix, ProtectedNamespacesRegex: protectedNamespaceRegexp}) err = webhook.Register(mgr, capsuleGroup, wl...) if err != nil { setupLog.Error(err, "unable to setup webhooks") diff --git a/pkg/webhook/tenant_prefix/patching.go b/pkg/webhook/tenant_prefix/patching.go index 30b44fd0..2c3b5944 100644 --- a/pkg/webhook/tenant_prefix/patching.go +++ b/pkg/webhook/tenant_prefix/patching.go @@ -19,6 +19,7 @@ package tenant_prefix import ( "context" "net/http" + "regexp" "strings" corev1 "k8s.io/api/core/v1" @@ -33,11 +34,12 @@ import ( // +kubebuilder:webhook:path=/validating-v1-namespace-tenant-prefix,mutating=false,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=prefix.namespace.capsule.clastix.io type Webhook struct { - ForceTenantPrefix bool + ForceTenantPrefix bool + ProtectedNamespacesRegex *regexp.Regexp } func (o Webhook) GetHandler() webhook.Handler { - return &handler{forceTenantPrefix: o.ForceTenantPrefix} + return &handler{forceTenantPrefix: o.ForceTenantPrefix, protectedNamespacesRegex: o.ProtectedNamespacesRegex} } func (o Webhook) GetName() string { @@ -49,18 +51,26 @@ func (o Webhook) GetPath() string { } type handler struct { - forceTenantPrefix bool + forceTenantPrefix bool + protectedNamespacesRegex *regexp.Regexp } func (r *handler) OnCreate(ctx context.Context, req admission.Request, clt client.Client, decoder *admission.Decoder) admission.Response { - if !r.forceTenantPrefix { - return admission.Allowed("") - } ns := &corev1.Namespace{} if err := decoder.Decode(req, ns); err != nil { return admission.Errored(http.StatusBadRequest, err) } + if r.protectedNamespacesRegex != nil { + if matched := r.protectedNamespacesRegex.MatchString(ns.GetName()); matched { + return admission.Denied("Creating namespaces with name matching " + r.protectedNamespacesRegex.String() + " regexp is not allowed; please, reach out the system administrators") + } + } + + if !r.forceTenantPrefix { + return admission.Allowed("") + } + t := &v1alpha1.Tenant{} for _, or := range ns.ObjectMeta.OwnerReferences { // retrieving the selected Tenant