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.