From 8e241d0400ed8d9b9afdd103c7f6ece1914be75e Mon Sep 17 00:00:00 2001 From: Maksim Fedotov Date: Thu, 27 Aug 2020 20:20:04 +0300 Subject: [PATCH] add use-groups-as-tenant-owner flag, create e2e tests, update README modify crd to support owner struct. add tenant name validation webhook. rewrite owner_reference hook logic. update and add new e2e tests apply review notes fix typo in timeout interval apply review notes move GetNamespaceTenant logic to utils from webhook rebase on master. update tests. implement new tenant selection logic for namespace --- api/v1alpha1/tenant_types.go | 18 ++- api/v1alpha1/zz_generated.deepcopy.go | 16 ++ .../crd/bases/capsule.clastix.io_tenants.yaml | 22 ++- config/samples/capsule_v1alpha1_tenant.yaml | 4 +- config/webhook/manifests.yaml | 17 +++ controllers/tenant_controller.go | 4 +- e2e/custom_capsule_group_test.go | 7 +- e2e/force_tenant_prefix_test.go | 96 ++++++++++++ e2e/ingress_class_test.go | 7 +- e2e/missing_tenant_test.go | 5 +- e2e/namespace_metadata_test.go | 7 +- e2e/new_namespace_test.go | 7 +- e2e/overquota_namespace_test.go | 7 +- e2e/owner_webhooks_test.go | 7 +- e2e/protected_namespace_regex_test.go | 7 +- e2e/resource_quota_exceeded_test.go | 7 +- e2e/selecting_non_owned_tenant_test.go | 9 +- e2e/selecting_tenant_fail_test.go | 144 ++++++++++++++++++ ...go => selecting_tenant_with_label_test.go} | 16 +- e2e/service_metadata_test.go | 7 +- e2e/storage_class_test.go | 7 +- e2e/suite_test.go | 14 +- e2e/tenant_name_webhook_test.go | 55 +++++++ e2e/tenant_owner_group_test.go | 65 ++++++++ e2e/tenant_resources_changes_test.go | 7 +- e2e/tenant_resources_test.go | 31 ++-- e2e/utils_test.go | 11 ++ main.go | 7 +- pkg/indexer/tenant/owner.go | 5 +- pkg/utils/owner.go | 23 +++ pkg/webhook/owner_reference/patching.go | 95 ++++++++++-- pkg/webhook/tenant_name/validating.go | 85 +++++++++++ use_cases.md | 13 +- 33 files changed, 750 insertions(+), 82 deletions(-) create mode 100644 e2e/force_tenant_prefix_test.go create mode 100644 e2e/selecting_tenant_fail_test.go rename e2e/{selecting_tenant_test.go => selecting_tenant_with_label_test.go} (89%) create mode 100644 e2e/tenant_name_webhook_test.go create mode 100644 e2e/tenant_owner_group_test.go create mode 100644 pkg/utils/owner.go create mode 100644 pkg/webhook/tenant_name/validating.go diff --git a/api/v1alpha1/tenant_types.go b/api/v1alpha1/tenant_types.go index 27d1ad4f..9d2b2260 100644 --- a/api/v1alpha1/tenant_types.go +++ b/api/v1alpha1/tenant_types.go @@ -34,7 +34,7 @@ type AdditionalMetadata struct { // TenantSpec defines the desired state of Tenant type TenantSpec struct { - Owner string `json:"owner"` + Owner OwnerSpec `json:"owner"` // +kubebuilder:validation:Optional NamespacesMetadata AdditionalMetadata `json:"namespacesMetadata"` // +kubebuilder:validation:Optional @@ -51,6 +51,19 @@ type TenantSpec struct { ResourceQuota []corev1.ResourceQuotaSpec `json:"resourceQuotas"` } +// OwnerSpec defines tenant owner name and kind +type OwnerSpec struct { + Name string `json:"name"` + Kind Kind `json:"kind"` +} + +// +kubebuilder:validation:Enum=User;Group +type Kind string + +func (k Kind) String() string { + return string(k) +} + // TenantStatus defines the observed state of Tenant type TenantStatus struct { Size uint `json:"size"` @@ -64,7 +77,8 @@ type TenantStatus struct { // +kubebuilder:resource:scope=Cluster,shortName=tnt // +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceQuota",description="The max amount of Namespaces can be created" // +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use" -// +kubebuilder:printcolumn:name="Owner",type="string",JSONPath=".spec.owner",description="The assigned Tenant owner" +// +kubebuilder:printcolumn:name="Owner name",type="string",JSONPath=".spec.owner.name",description="The assigned Tenant owner" +// +kubebuilder:printcolumn:name="Owner kind",type="string",JSONPath=".spec.owner.kind",description="The assigned Tenant owner kind" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age" // Tenant is the Schema for the tenants API diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 10055386..c4edd809 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -93,6 +93,21 @@ func (in NamespaceList) DeepCopy() NamespaceList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerSpec. +func (in *OwnerSpec) DeepCopy() *OwnerSpec { + if in == nil { + return nil + } + out := new(OwnerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in StorageClassList) DeepCopyInto(out *StorageClassList) { { @@ -174,6 +189,7 @@ func (in *TenantList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TenantSpec) DeepCopyInto(out *TenantSpec) { *out = *in + out.Owner = in.Owner in.NamespacesMetadata.DeepCopyInto(&out.NamespacesMetadata) in.ServicesMetadata.DeepCopyInto(&out.ServicesMetadata) if in.StorageClasses != nil { diff --git a/config/crd/bases/capsule.clastix.io_tenants.yaml b/config/crd/bases/capsule.clastix.io_tenants.yaml index be0d5125..b21350b7 100644 --- a/config/crd/bases/capsule.clastix.io_tenants.yaml +++ b/config/crd/bases/capsule.clastix.io_tenants.yaml @@ -17,9 +17,13 @@ spec: description: The total amount of Namespaces in use name: Namespace count type: integer - - JSONPath: .spec.owner + - JSONPath: .spec.owner.name description: The assigned Tenant owner - name: Owner + name: Owner name + type: string + - JSONPath: .spec.owner.kind + description: The assigned Tenant owner kind + name: Owner kind type: string - JSONPath: .metadata.creationTimestamp description: Age @@ -617,7 +621,19 @@ spec: type: string type: object owner: - type: string + description: OwnerSpec defines tenant owner name and kind + properties: + kind: + enum: + - User + - Group + type: string + name: + type: string + required: + - kind + - name + type: object resourceQuotas: items: description: ResourceQuotaSpec defines the desired hard limits to diff --git a/config/samples/capsule_v1alpha1_tenant.yaml b/config/samples/capsule_v1alpha1_tenant.yaml index 69fce40e..f81bcc2c 100644 --- a/config/samples/capsule_v1alpha1_tenant.yaml +++ b/config/samples/capsule_v1alpha1_tenant.yaml @@ -66,7 +66,9 @@ spec: - Egress nodeSelector: kubernetes.io/os: linux - owner: alice + owner: + name: alice + kind: User resourceQuotas: - hard: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 93099f3d..555cc028 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -125,6 +125,23 @@ webhooks: - CREATE resources: - persistentvolumeclaims +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validating-v1-tenant-name + failurePolicy: Fail + name: tenant.name.capsule.clastix.io + rules: + - apiGroups: + - capsule.clastix.io + apiVersions: + - v1alpha1 + operations: + - CREATE + resources: + - tenants - clientConfig: caBundle: Cg== service: diff --git a/controllers/tenant_controller.go b/controllers/tenant_controller.go index d0445318..9a764f47 100644 --- a/controllers/tenant_controller.go +++ b/controllers/tenant_controller.go @@ -519,8 +519,8 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro l := map[string]string{tl: tenant.Name} s := []rbacv1.Subject{ { - Kind: "User", - Name: tenant.Spec.Owner, + Kind: tenant.Spec.Owner.Kind.String(), + Name: tenant.Spec.Owner.Name, }, } diff --git a/e2e/custom_capsule_group_test.go b/e2e/custom_capsule_group_test.go index 0fc21e9a..875240a0 100644 --- a/e2e/custom_capsule_group_test.go +++ b/e2e/custom_capsule_group_test.go @@ -32,10 +32,13 @@ import ( var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-group", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-assigned-custom-group", + Name: "tenantassignedcustomgroup", }, Spec: v1alpha1.TenantSpec{ - Owner: "alice", + Owner: v1alpha1.OwnerSpec{ + Name: "alice", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, diff --git a/e2e/force_tenant_prefix_test.go b/e2e/force_tenant_prefix_test.go new file mode 100644 index 00000000..c0cdfa4d --- /dev/null +++ b/e2e/force_tenant_prefix_test.go @@ -0,0 +1,96 @@ +//+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 --force-tenant-name flag", func() { + t1 := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "first", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + t2 := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "second", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + JustBeforeEach(func() { + t1.ResourceVersion = "" + t2.ResourceVersion = "" + Expect(k8sClient.Create(context.TODO(), t1)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), t2)).Should(Succeed()) + }) + JustAfterEach(func() { + Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed()) + Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed()) + }) + It("should fail", func() { + args := append(defaulManagerPodArgs, []string{"--force-tenant-prefix"}...) + ModifyCapsuleManagerPodArgs(args) + ns := NewNamespace("test") + NamespaceCreationShouldNotSucceed(ns, t1, podRecreationTimeoutInterval) + }) + It("should be assigned to the second Tenant", func() { + ns := NewNamespace("second-test") + ns2 := NewNamespace("second-test2") + NamespaceCreationShouldSucceed(ns, t2, podRecreationTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, t2, podRecreationTimeoutInterval) + NamespaceCreationShouldNotSucceed(ns2, t1, podRecreationTimeoutInterval) + args := defaulManagerPodArgs + ModifyCapsuleManagerPodArgs(args) + }) +}) diff --git a/e2e/ingress_class_test.go b/e2e/ingress_class_test.go index c1baa59e..07a0c0c3 100644 --- a/e2e/ingress_class_test.go +++ b/e2e/ingress_class_test.go @@ -38,10 +38,13 @@ import ( var _ = Describe("when Tenant handles Ingress classes", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-class", + Name: "ingressclass", }, Spec: v1alpha1.TenantSpec{ - Owner: "ingress", + Owner: v1alpha1.OwnerSpec{ + Name: "ingress", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, diff --git a/e2e/missing_tenant_test.go b/e2e/missing_tenant_test.go index de38be9b..836e387e 100644 --- a/e2e/missing_tenant_test.go +++ b/e2e/missing_tenant_test.go @@ -32,7 +32,10 @@ var _ = Describe("Namespace creation with no Tenant assigned", func() { It("should fail", func() { tnt := &v1alpha1.Tenant{ Spec: v1alpha1.TenantSpec{ - Owner: "missing", + Owner: v1alpha1.OwnerSpec{ + Name: "missing", + Kind: "User", + }, }, } ns := NewNamespace("no-namespace") diff --git a/e2e/namespace_metadata_test.go b/e2e/namespace_metadata_test.go index 400fa734..acb4de30 100644 --- a/e2e/namespace_metadata_test.go +++ b/e2e/namespace_metadata_test.go @@ -33,10 +33,13 @@ import ( var _ = Describe("creating a Namespace for a Tenant with additional metadata", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-metadata", + Name: "tenantmetadata", }, Spec: v1alpha1.TenantSpec{ - Owner: "gatsby", + Owner: v1alpha1.OwnerSpec{ + Name: "gatsby", + Kind: "User", + }, StorageClasses: []string{}, IngressClasses: []string{}, NamespacesMetadata: v1alpha1.AdditionalMetadata{ diff --git a/e2e/new_namespace_test.go b/e2e/new_namespace_test.go index cf61b851..ba248253 100644 --- a/e2e/new_namespace_test.go +++ b/e2e/new_namespace_test.go @@ -32,10 +32,13 @@ import ( var _ = Describe("creating a Namespace as Tenant owner", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-assigned", + Name: "tenantassigned", }, Spec: v1alpha1.TenantSpec{ - Owner: "alice", + Owner: v1alpha1.OwnerSpec{ + Name: "alice", + Kind: "User", + }, StorageClasses: []string{}, IngressClasses: []string{}, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, diff --git a/e2e/overquota_namespace_test.go b/e2e/overquota_namespace_test.go index 48bf51bf..19e498db 100644 --- a/e2e/overquota_namespace_test.go +++ b/e2e/overquota_namespace_test.go @@ -32,10 +32,13 @@ import ( var _ = Describe("creating a Namespace over-quota", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "overquota-tenant", + Name: "overquotatenant", }, Spec: v1alpha1.TenantSpec{ - Owner: "bob", + Owner: v1alpha1.OwnerSpec{ + Name: "bob", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, diff --git a/e2e/owner_webhooks_test.go b/e2e/owner_webhooks_test.go index 2eb75898..ce24c136 100644 --- a/e2e/owner_webhooks_test.go +++ b/e2e/owner_webhooks_test.go @@ -36,10 +36,13 @@ import ( var _ = Describe("when Tenant owner interacts with the webhooks", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-owner", + Name: "tenantowner", }, Spec: v1alpha1.TenantSpec{ - Owner: "ruby", + Owner: v1alpha1.OwnerSpec{ + Name: "ruby", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{ diff --git a/e2e/protected_namespace_regex_test.go b/e2e/protected_namespace_regex_test.go index 22b8da8e..cad56c7e 100644 --- a/e2e/protected_namespace_regex_test.go +++ b/e2e/protected_namespace_regex_test.go @@ -35,7 +35,10 @@ var _ = Describe("creating a Namespace with --protected-namespace-regex enabled" Name: "tenantprotectednamespace", }, Spec: v1alpha1.TenantSpec{ - Owner: "alice", + Owner: v1alpha1.OwnerSpec{ + Name: "alice", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, @@ -61,8 +64,8 @@ var _ = Describe("creating a Namespace with --protected-namespace-regex enabled" NamespaceShouldBeManagedByTenant(ns, tnt, podRecreationTimeoutInterval) }) It("should fail", func() { - ModifyCapsuleManagerPodArgs(defaulManagerPodArgs) ns := NewNamespace("test-system") NamespaceCreationShouldNotSucceed(ns, tnt, podRecreationTimeoutInterval) + ModifyCapsuleManagerPodArgs(defaulManagerPodArgs) }) }) diff --git a/e2e/resource_quota_exceeded_test.go b/e2e/resource_quota_exceeded_test.go index 76ce21d5..1a83e665 100644 --- a/e2e/resource_quota_exceeded_test.go +++ b/e2e/resource_quota_exceeded_test.go @@ -39,10 +39,13 @@ import ( var _ = Describe("exceeding Tenant resource quota", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-resources-changes", + Name: "tenantresourceschanges", }, Spec: v1alpha1.TenantSpec{ - Owner: "bobby", + Owner: v1alpha1.OwnerSpec{ + Name: "bobby", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, diff --git a/e2e/selecting_non_owned_tenant_test.go b/e2e/selecting_non_owned_tenant_test.go index 1ea560ef..a815508a 100644 --- a/e2e/selecting_non_owned_tenant_test.go +++ b/e2e/selecting_non_owned_tenant_test.go @@ -32,10 +32,13 @@ import ( var _ = Describe("creating a Namespace trying to select a third Tenant", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-non-owned", + Name: "tenantnonowned", }, Spec: v1alpha1.TenantSpec{ - Owner: "undefined", + Owner: v1alpha1.OwnerSpec{ + Name: "undefined", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, @@ -65,7 +68,7 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", func() }) }) - cs := ownerClient(&v1alpha1.Tenant{Spec: v1alpha1.TenantSpec{Owner: "dale"}}) + cs := ownerClient(&v1alpha1.Tenant{Spec: v1alpha1.TenantSpec{Owner: v1alpha1.OwnerSpec{Name: "dale", Kind: "User"}}}) _, err := cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) Expect(err).ShouldNot(Succeed()) }) diff --git a/e2e/selecting_tenant_fail_test.go b/e2e/selecting_tenant_fail_test.go new file mode 100644 index 00000000..dc14d3d1 --- /dev/null +++ b/e2e/selecting_tenant_fail_test.go @@ -0,0 +1,144 @@ +//+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 without a Tenant selector when user owns multiple Tenants", func() { + t1 := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenantone", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + t2 := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenanttwo", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + t3 := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenantthree", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "Group", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + t4 := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenantfour", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "Group", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + It("should fail", func() { + ns := NewNamespace("fail-ns") + By("user owns 2 tenants", func() { + Expect(k8sClient.Create(context.TODO(), t1)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), t2)).Should(Succeed()) + NamespaceCreationShouldNotSucceed(ns, t1, defaultTimeoutInterval) + NamespaceCreationShouldNotSucceed(ns, t2, defaultTimeoutInterval) + Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed()) + Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed()) + }) + By("group owns 2 tenants", func() { + Expect(k8sClient.Create(context.TODO(), t3)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), t4)).Should(Succeed()) + NamespaceCreationShouldNotSucceed(ns, t3, defaultTimeoutInterval) + NamespaceCreationShouldNotSucceed(ns, t4, defaultTimeoutInterval) + Expect(k8sClient.Delete(context.TODO(), t3)).Should(Succeed()) + Expect(k8sClient.Delete(context.TODO(), t4)).Should(Succeed()) + }) + By("user and group owns 4 tenants", func() { + t1.ResourceVersion, t2.ResourceVersion, t3.ResourceVersion, t4.ResourceVersion = "", "", "", "" + Expect(k8sClient.Create(context.TODO(), t1)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), t2)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), t3)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), t4)).Should(Succeed()) + NamespaceCreationShouldNotSucceed(ns, t1, defaultTimeoutInterval) + NamespaceCreationShouldNotSucceed(ns, t2, defaultTimeoutInterval) + NamespaceCreationShouldNotSucceed(ns, t3, defaultTimeoutInterval) + NamespaceCreationShouldNotSucceed(ns, t4, defaultTimeoutInterval) + Expect(k8sClient.Delete(context.TODO(), t1)).Should(Succeed()) + Expect(k8sClient.Delete(context.TODO(), t2)).Should(Succeed()) + Expect(k8sClient.Delete(context.TODO(), t3)).Should(Succeed()) + Expect(k8sClient.Delete(context.TODO(), t4)).Should(Succeed()) + }) + }) + +}) diff --git a/e2e/selecting_tenant_test.go b/e2e/selecting_tenant_with_label_test.go similarity index 89% rename from e2e/selecting_tenant_test.go rename to e2e/selecting_tenant_with_label_test.go index cf9951f1..3f703fc7 100644 --- a/e2e/selecting_tenant_test.go +++ b/e2e/selecting_tenant_with_label_test.go @@ -29,13 +29,16 @@ import ( "github.com/clastix/capsule/api/v1alpha1" ) -var _ = Describe("creating a Namespace with Tenant selector", func() { +var _ = Describe("creating a Namespace with Tenant selector when user owns multiple tenants", func() { t1 := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-one", + Name: "tenantone", }, Spec: v1alpha1.TenantSpec{ - Owner: "john", + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, @@ -48,10 +51,13 @@ var _ = Describe("creating a Namespace with Tenant selector", func() { } t2 := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-two", + Name: "tenanttwo", }, Spec: v1alpha1.TenantSpec{ - Owner: "john", + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, diff --git a/e2e/service_metadata_test.go b/e2e/service_metadata_test.go index 745d5d34..2c6ec144 100644 --- a/e2e/service_metadata_test.go +++ b/e2e/service_metadata_test.go @@ -34,10 +34,13 @@ import ( var _ = Describe("creating a Service/Endpoint/EndpointSlice for a Tenant with additional metadata", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "service-metadata", + Name: "servicemetadata", }, Spec: v1alpha1.TenantSpec{ - Owner: "gatsby", + Owner: v1alpha1.OwnerSpec{ + Name: "gatsby", + Kind: "User", + }, StorageClasses: []string{}, IngressClasses: []string{}, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, diff --git a/e2e/storage_class_test.go b/e2e/storage_class_test.go index 614c7a1d..e945ccb9 100644 --- a/e2e/storage_class_test.go +++ b/e2e/storage_class_test.go @@ -35,10 +35,13 @@ import ( var _ = Describe("when Tenant handles Storage classes", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "storage-class", + Name: "storageclass", }, Spec: v1alpha1.TenantSpec{ - Owner: "storage", + Owner: v1alpha1.OwnerSpec{ + Name: "storage", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{ diff --git a/e2e/suite_test.go b/e2e/suite_test.go index 218fbef3..46dc4017 100644 --- a/e2e/suite_test.go +++ b/e2e/suite_test.go @@ -44,10 +44,11 @@ import ( // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var ( - cfg *rest.Config - k8sClient client.Client - testEnv *envtest.Environment - defaulManagerPodArgs []string + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + defaulManagerPodArgs []string + tenantRoleBindingNames = []string{"namespace:admin", "namespace:deleter"} ) const ( @@ -95,7 +96,6 @@ var _ = BeforeSuite(func(done Done) { } } Expect(defaulManagerPodArgs).ToNot(BeEmpty()) - close(done) }, 60) @@ -107,8 +107,8 @@ var _ = AfterSuite(func() { func ownerClient(tenant *capsulev1alpha.Tenant) (cs kubernetes.Interface) { c, err := config.GetConfig() Expect(err).ToNot(HaveOccurred()) - c.Impersonate.Groups = []string{capsulev1alpha.GroupVersion.Group} - c.Impersonate.UserName = tenant.Spec.Owner + c.Impersonate.Groups = []string{capsulev1alpha.GroupVersion.Group, tenant.Spec.Owner.Name} + c.Impersonate.UserName = tenant.Spec.Owner.Name cs, err = kubernetes.NewForConfig(c) Expect(err).ToNot(HaveOccurred()) return diff --git a/e2e/tenant_name_webhook_test.go b/e2e/tenant_name_webhook_test.go new file mode 100644 index 00000000..00ba8bc0 --- /dev/null +++ b/e2e/tenant_name_webhook_test.go @@ -0,0 +1,55 @@ +//+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 Tenant with wrong name", func() { + tnt := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wrong-name", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + It("should fail", func() { + Expect(k8sClient.Create(context.TODO(), tnt)).ShouldNot(Succeed()) + }) +}) diff --git a/e2e/tenant_owner_group_test.go b/e2e/tenant_owner_group_test.go new file mode 100644 index 00000000..9a480e68 --- /dev/null +++ b/e2e/tenant_owner_group_test.go @@ -0,0 +1,65 @@ +//+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 group Tenant owner", func() { + tnt := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenantgroupowner", + }, + Spec: v1alpha1.TenantSpec{ + Owner: v1alpha1.OwnerSpec{ + Name: "alice", + Kind: "Group", + }, + NamespacesMetadata: v1alpha1.AdditionalMetadata{}, + ServicesMetadata: v1alpha1.AdditionalMetadata{}, + 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() { + ns := NewNamespace("gto-namespace") + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) + GroupShouldBeUsedInTenantRoleBinding(ns, tnt, defaultTimeoutInterval) + }) +}) diff --git a/e2e/tenant_resources_changes_test.go b/e2e/tenant_resources_changes_test.go index 6032f349..56707706 100644 --- a/e2e/tenant_resources_changes_test.go +++ b/e2e/tenant_resources_changes_test.go @@ -37,10 +37,13 @@ import ( var _ = Describe("changing Tenant managed Kubernetes resources", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-resources-changes", + Name: "tenantresourceschanges", }, Spec: v1alpha1.TenantSpec{ - Owner: "laura", + Owner: v1alpha1.OwnerSpec{ + Name: "laura", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, diff --git a/e2e/tenant_resources_test.go b/e2e/tenant_resources_test.go index 78fc1999..e0a7ffa5 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" - v1 "k8s.io/api/networking/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -38,10 +38,13 @@ import ( var _ = Describe("creating namespaces within a Tenant with resources", func() { tnt := &v1alpha1.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "tenant-resources", + Name: "tenantresources", }, Spec: v1alpha1.TenantSpec{ - Owner: "john", + Owner: v1alpha1.OwnerSpec{ + Name: "john", + Kind: "User", + }, NamespacesMetadata: v1alpha1.AdditionalMetadata{}, ServicesMetadata: v1alpha1.AdditionalMetadata{}, StorageClasses: []string{}, @@ -91,11 +94,11 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() { }, }, }, - NetworkPolicies: []v1.NetworkPolicySpec{ + NetworkPolicies: []networkingv1.NetworkPolicySpec{ { - Ingress: []v1.NetworkPolicyIngressRule{ + Ingress: []networkingv1.NetworkPolicyIngressRule{ { - From: []v1.NetworkPolicyPeer{ + From: []networkingv1.NetworkPolicyPeer{ { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -107,18 +110,18 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() { PodSelector: &metav1.LabelSelector{}, }, { - IPBlock: &v1.IPBlock{ + IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/12", }, }, }, }, }, - Egress: []v1.NetworkPolicyEgressRule{ + Egress: []networkingv1.NetworkPolicyEgressRule{ { - To: []v1.NetworkPolicyPeer{ + To: []networkingv1.NetworkPolicyPeer{ { - IPBlock: &v1.IPBlock{ + IPBlock: &networkingv1.IPBlock{ CIDR: "0.0.0.0/0", Except: []string{ "192.168.0.0/12", @@ -129,9 +132,9 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() { }, }, PodSelector: metav1.LabelSelector{}, - PolicyTypes: []v1.PolicyType{ - v1.PolicyTypeIngress, - v1.PolicyTypeEgress, + PolicyTypes: []networkingv1.PolicyType{ + networkingv1.PolicyTypeIngress, + networkingv1.PolicyTypeEgress, }, }, }, @@ -195,7 +198,7 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() { By("checking Network Policy resources", func() { for i, s := range tnt.Spec.NetworkPolicies { n := fmt.Sprintf("capsule-%s-%d", tnt.GetName(), i) - np := &v1.NetworkPolicy{} + np := &networkingv1.NetworkPolicy{} Eventually(func() error { return k8sClient.Get(context.TODO(), types.NamespacedName{Name: n, Namespace: name}, np) }, 10*time.Second, time.Second).Should(Succeed()) diff --git a/e2e/utils_test.go b/e2e/utils_test.go index 46fcc7ca..37313c3a 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -115,3 +115,14 @@ func ModifyCapsuleManagerPodArgs(args []string) { // had to add sleep in order to manager be started time.Sleep(defaultTimeoutInterval) } + +func GroupShouldBeUsedInTenantRoleBinding(ns *corev1.Namespace, t *v1alpha1.Tenant, timeout time.Duration) { + for _, roleBindingName := range tenantRoleBindingNames { + tenantRoleBindig := &rbacv1.RoleBinding{} + Eventually(func() string { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: roleBindingName, Namespace: ns.GetName()}, tenantRoleBindig)).Should(Succeed()) + return tenantRoleBindig.Subjects[0].Kind + }, timeout, defaultPollInterval).Should(BeIdenticalTo("Group")) + + } +} diff --git a/main.go b/main.go index 8e8871c7..8664132d 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ import ( "github.com/clastix/capsule/pkg/webhook/owner_reference" "github.com/clastix/capsule/pkg/webhook/pvc" "github.com/clastix/capsule/pkg/webhook/service_labels" + "github.com/clastix/capsule/pkg/webhook/tenant_name" "github.com/clastix/capsule/pkg/webhook/tenant_prefix" "github.com/clastix/capsule/pkg/webhook/utils" "github.com/clastix/capsule/version" @@ -91,6 +92,7 @@ func main() { "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() @@ -129,6 +131,8 @@ func main() { _ = mgr.AddReadyzCheck("ping", healthz.Ping) _ = mgr.AddHealthzCheck("ping", healthz.Ping) + setupLog.Info("starting with following options:", "metricsAddr", metricsAddr, "enableLeaderElection", enableLeaderElection, "forceTenantPrefix", forceTenantPrefix) + if err = (&controllers.TenantReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Tenant"), @@ -144,11 +148,12 @@ func main() { make([]webhook.Webhook, 0), ingress.Webhook(utils.InCapsuleGroup(capsuleGroup, ingress.Handler())), pvc.Webhook(utils.InCapsuleGroup(capsuleGroup, pvc.Handler())), - owner_reference.Webhook(utils.InCapsuleGroup(capsuleGroup, owner_reference.Handler())), + owner_reference.Webhook(utils.InCapsuleGroup(capsuleGroup, owner_reference.Handler(forceTenantPrefix))), namespace_quota.Webhook(utils.InCapsuleGroup(capsuleGroup, namespace_quota.Handler())), network_policies.Webhook(utils.InCapsuleGroup(capsuleGroup, network_policies.Handler())), service_labels.Webhook(utils.InCapsuleGroup(capsuleGroup, service_labels.Handler())), tenant_prefix.Webhook(utils.InCapsuleGroup(capsuleGroup, tenant_prefix.Handler(forceTenantPrefix, protectedNamespaceRegexp))), + tenant_name.Webhook(tenant_name.Handler()), ) if err = webhook.Register(mgr, wl...); err != nil { setupLog.Error(err, "unable to setup webhooks") diff --git a/pkg/indexer/tenant/owner.go b/pkg/indexer/tenant/owner.go index 50d329c5..7f0e3d61 100644 --- a/pkg/indexer/tenant/owner.go +++ b/pkg/indexer/tenant/owner.go @@ -21,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/clastix/capsule/api/v1alpha1" + "github.com/clastix/capsule/pkg/utils" ) type OwnerReference struct { @@ -31,12 +32,12 @@ func (o OwnerReference) Object() runtime.Object { } func (o OwnerReference) Field() string { - return ".spec.owner" + return ".spec.owner.ownerkind" } func (o OwnerReference) Func() client.IndexerFunc { return func(object runtime.Object) []string { tenant := object.(*v1alpha1.Tenant) - return []string{tenant.Spec.Owner} + return []string{utils.GetOwnerWithKind(tenant)} } } diff --git a/pkg/utils/owner.go b/pkg/utils/owner.go new file mode 100644 index 00000000..cf3e1e0c --- /dev/null +++ b/pkg/utils/owner.go @@ -0,0 +1,23 @@ +/* +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 utils + +import "github.com/clastix/capsule/api/v1alpha1" + +func GetOwnerWithKind(tenant *v1alpha1.Tenant) string { + return tenant.Spec.Owner.Kind.String() + ":" + tenant.Spec.Owner.Name +} diff --git a/pkg/webhook/owner_reference/patching.go b/pkg/webhook/owner_reference/patching.go index 20b0b3f1..2ebc228e 100644 --- a/pkg/webhook/owner_reference/patching.go +++ b/pkg/webhook/owner_reference/patching.go @@ -19,18 +19,21 @@ package owner_reference import ( "context" "encoding/json" + "fmt" "net/http" + "strings" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "github.com/clastix/capsule/api/v1alpha1" capsulev1alpha1 "github.com/clastix/capsule/api/v1alpha1" capsulewebhook "github.com/clastix/capsule/pkg/webhook" + authenticationv1 "k8s.io/api/authentication/v1" ) // +kubebuilder:webhook:path=/mutate-v1-namespace-owner-reference,mutating=true,failurePolicy=fail,groups="",resources=namespaces,verbs=create,versions=v1,name=owner.namespace.capsule.clastix.io @@ -56,10 +59,13 @@ func (w *webhook) GetPath() string { } type handler struct { + forceTenantPrefix bool } -func Handler() capsulewebhook.Handler { - return &handler{} +func Handler(forceTenantPrefix bool) capsulewebhook.Handler { + return &handler{ + forceTenantPrefix: forceTenantPrefix, + } } func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsulewebhook.Func { @@ -68,12 +74,12 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul if err := decoder.Decode(req, ns); err != nil { return admission.Errored(http.StatusBadRequest, err) } - + ln, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{}) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + // If we already had TenantName label on NS -> assign to it if len(ns.ObjectMeta.Labels) > 0 { - ln, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{}) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) - } l, ok := ns.ObjectMeta.Labels[ln] // assigning namespace to Tenant in case of label if ok { @@ -83,7 +89,7 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul return admission.Errored(http.StatusBadRequest, err) } // Tenant owner must adhere to user that asked for NS creation - if t.Spec.Owner != req.UserInfo.Username { + if !h.isTenantOwner(t.Spec.Owner, req.UserInfo) { return admission.Denied("Cannot assign the desired namespace to a non-owned Tenant") } // Patching the response @@ -91,16 +97,52 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul } } + // If we forceTenantPrefix -> find Tenant from NS name + if h.forceTenantPrefix { + t := &v1alpha1.Tenant{} + tenantName := strings.Split(ns.GetName(), "-")[0] + if err := clt.Get(ctx, types.NamespacedName{Name: tenantName}, t); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + return h.patchResponseForOwnerRef(t, ns) + } - tl := &capsulev1alpha1.TenantList{} - if err := clt.List(ctx, tl, client.MatchingFieldsSelector{ - Selector: fields.OneTermEqualSelector(".spec.owner", req.UserInfo.Username), - }); err != nil { + tenants := []*capsulev1alpha1.Tenant{} + + // Find tenants belonging to user + tlu, err := h.listTenantsForOwnerKind(ctx, "User", req.UserInfo.Username, clt) + if err != nil { return admission.Errored(http.StatusBadRequest, err) } + // No groups single tenant short-circuit + if len(req.UserInfo.Groups) == 0 && len(tlu.Items) == 1 { + return h.patchResponseForOwnerRef(&tlu.Items[0], ns) + } + + switch userTenants := len(tlu.Items); { + case userTenants > 1: + return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace") + case userTenants == 1: + tenants = append(tenants, &tlu.Items[0]) + } + + for _, group := range req.UserInfo.Groups { + tl, err := h.listTenantsForOwnerKind(ctx, "Group", group, clt) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + for _, item := range tl.Items { + tenants = append(tenants, &item) + } + // more than one tenant found, returning error + if len(tenants) > 1 { + return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace") + } + } - if len(tl.Items) > 0 { - return h.patchResponseForOwnerRef(&tl.Items[0], ns) + // Single tenant found for group + if len(tenants) == 1 { + return h.patchResponseForOwnerRef(tenants[0], ns) } return admission.Denied("You do not have any Tenant assigned: please, reach out the system administrators") @@ -131,3 +173,26 @@ func (h *handler) patchResponseForOwnerRef(tenant *capsulev1alpha1.Tenant, ns *c c, _ := json.Marshal(ns) return admission.PatchResponseFromRaw(o, c) } + +func (h *handler) listTenantsForOwnerKind(ctx context.Context, ownerKind string, ownerName string, clt client.Client) (*v1alpha1.TenantList, error) { + tl := &v1alpha1.TenantList{} + f := client.MatchingFields{ + ".spec.owner.ownerkind": fmt.Sprintf("%s:%s", ownerKind, ownerName), + } + err := clt.List(ctx, tl, f) + return tl, err +} + +func (h *handler) isTenantOwner(os v1alpha1.OwnerSpec, userInfo authenticationv1.UserInfo) bool { + if os.Kind == "User" && userInfo.Username == os.Name { + return true + } + if os.Kind == "Group" { + for _, group := range userInfo.Groups { + if group == os.Name { + return true + } + } + } + return false +} diff --git a/pkg/webhook/tenant_name/validating.go b/pkg/webhook/tenant_name/validating.go new file mode 100644 index 00000000..c65f7dd3 --- /dev/null +++ b/pkg/webhook/tenant_name/validating.go @@ -0,0 +1,85 @@ +/* +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 tenant_name + +import ( + "context" + "net/http" + "regexp" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/clastix/capsule/api/v1alpha1" + capsulewebhook "github.com/clastix/capsule/pkg/webhook" +) + +// +kubebuilder:webhook:path=/validating-v1-tenant-name,mutating=false,failurePolicy=fail,groups="capsule.clastix.io",resources=tenants,verbs=create,versions=v1alpha1,name=tenant.name.capsule.clastix.io + +type webhook struct { + handler capsulewebhook.Handler +} + +func Webhook(handler capsulewebhook.Handler) capsulewebhook.Webhook { + return &webhook{handler: handler} +} + +func (w webhook) GetName() string { + return "TenantName" +} + +func (w webhook) GetPath() string { + return "/validating-v1-tenant-name" +} + +func (w webhook) GetHandler() capsulewebhook.Handler { + return w.handler +} + +type handler struct { +} + +func Handler() capsulewebhook.Handler { + return &handler{} +} + +func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + tnt := &v1alpha1.Tenant{} + if err := decoder.Decode(req, tnt); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + matched, _ := regexp.MatchString(`^[a-z0-9]([a-z0-9]*[a-z0-9])?$`, tnt.GetName()) + if !matched { + return admission.Denied("Tenant name has forbidden characters") + } + return admission.Allowed("") + } +} + +func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + return admission.Allowed("") + } +} + +func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) admission.Response { + return admission.Allowed("") + } +} diff --git a/use_cases.md b/use_cases.md index 68b98afe..485d6f1d 100644 --- a/use_cases.md +++ b/use_cases.md @@ -58,7 +58,9 @@ metadata: annotations: name: oil spec: - owner: alice + owner: + name: alice + kind: User nodeSelector: vpc: oil ingressClasses: @@ -127,13 +129,16 @@ spec: - 192.168.0.0/16 ``` +> N.B.: Tenant name can only consist of alphanumeric characters. No other symbols are allowed. + + Bill checks the new tenant is created and operational: ``` bill@caas# kubectl get tenants -NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER AGE -oil 3 0 alice 3m -foo 10 9 bar 30d +NAME NAMESPACE QUOTA NAMESPACE COUNT OWNER NAME OWNER KIND AGE +oil 3 0 alice User 3m +foo 10 9 bar User 30d ``` > Note that namespaces are not yet assigned to the new tenant.