diff --git a/api/v1alpha1/tenant_types.go b/api/v1alpha1/tenant_types.go index d32b3bb5..d85a328e 100644 --- a/api/v1alpha1/tenant_types.go +++ b/api/v1alpha1/tenant_types.go @@ -25,9 +25,18 @@ import ( // +kubebuilder:validation:Minimum=1 type NamespaceQuota uint +type NamespaceMetadata struct { + // +nullable + AdditionalLabels map[string]string `json:"additionalLabels"` + // +nullable + AdditionalAnnotations map[string]string `json:"additionalAnnotations"` +} + // TenantSpec defines the desired state of Tenant type TenantSpec struct { Owner string `json:"owner"` + // +kubebuilder:validation:Optional + NamespacesMetadata NamespaceMetadata `json:"namespacesMetadata"` // +kubebuilder:validation:Required StorageClasses StorageClassList `json:"storageClasses"` IngressClasses IngressClassList `json:"ingressClasses"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5f65bfb9..f731f07d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -64,6 +64,35 @@ 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 *NamespaceMetadata) DeepCopyInto(out *NamespaceMetadata) { + *out = *in + if in.AdditionalLabels != nil { + in, out := &in.AdditionalLabels, &out.AdditionalLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.AdditionalAnnotations != nil { + in, out := &in.AdditionalAnnotations, &out.AdditionalAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceMetadata. +func (in *NamespaceMetadata) DeepCopy() *NamespaceMetadata { + if in == nil { + return nil + } + out := new(NamespaceMetadata) + 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) { { @@ -145,6 +174,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 + in.NamespacesMetadata.DeepCopyInto(&out.NamespacesMetadata) if in.StorageClasses != nil { in, out := &in.StorageClasses, &out.StorageClasses *out = make(StorageClassList, len(*in)) diff --git a/config/crd/bases/capsule.clastix.io_tenants.yaml b/config/crd/bases/capsule.clastix.io_tenants.yaml index 7a3b9b63..50dbae77 100644 --- a/config/crd/bases/capsule.clastix.io_tenants.yaml +++ b/config/crd/bases/capsule.clastix.io_tenants.yaml @@ -139,6 +139,22 @@ spec: namespaceQuota: minimum: 1 type: integer + namespacesMetadata: + properties: + additionalAnnotations: + additionalProperties: + type: string + nullable: true + type: object + additionalLabels: + additionalProperties: + type: string + nullable: true + type: object + required: + - additionalAnnotations + - additionalLabels + type: object networkPolicies: items: description: NetworkPolicySpec provides the specification of a NetworkPolicy diff --git a/controllers/tenant_controller.go b/controllers/tenant_controller.go index ad66f815..f487b87d 100644 --- a/controllers/tenant_controller.go +++ b/controllers/tenant_controller.go @@ -393,29 +393,46 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error return nil } -func (r *TenantReconciler) syncNamespace(namespace string, ingressClasses []string, storageClasses []string, tenantLabel string, wg *sync.WaitGroup, channel chan error) { +func (r *TenantReconciler) syncNamespace(namespace string, ingressClasses []string, storageClasses []string, nsMetadata capsulev1alpha1.NamespaceMetadata, tenantLabel string, wg *sync.WaitGroup, channel chan error) { defer wg.Done() - t := &corev1.Namespace{} - if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, t); err != nil { + ns := &corev1.Namespace{} + if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace}, ns); err != nil { channel <- err } channel <- retry.RetryOnConflict(retry.DefaultBackoff, func() error { - if t.Annotations == nil { - t.Annotations = make(map[string]string) + a := ns.GetAnnotations() + if a == nil { + a = make(map[string]string) } - t.Annotations[capsulev1alpha1.AvailableIngressClassesAnnotation] = strings.Join(ingressClasses, ",") - t.Annotations[capsulev1alpha1.AvailableStorageClassesAnnotation] = strings.Join(storageClasses, ",") - if t.Labels == nil { - t.Labels = make(map[string]string) + a[capsulev1alpha1.AvailableIngressClassesAnnotation] = strings.Join(ingressClasses, ",") + a[capsulev1alpha1.AvailableStorageClassesAnnotation] = strings.Join(storageClasses, ",") + if aa := nsMetadata.AdditionalAnnotations; aa != nil { + for k, v := range aa { + a[k] = v + } + } + + l := ns.GetLabels() + if l == nil { + l = make(map[string]string) } capsuleLabel, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{}) if err != nil { return err } - t.Labels[capsuleLabel] = tenantLabel - return r.Client.Update(context.TODO(), t, &client.UpdateOptions{}) + l[capsuleLabel] = tenantLabel + if al := nsMetadata.AdditionalLabels; al != nil { + for k, v := range al { + l[k] = v + } + } + + ns.SetLabels(l) + ns.SetAnnotations(a) + + return r.Client.Update(context.TODO(), ns, &client.UpdateOptions{}) }) } @@ -427,7 +444,7 @@ func (r *TenantReconciler) syncNamespaces(tenant *capsulev1alpha1.Tenant) (err e wg.Add(tenant.Status.Namespaces.Len()) for _, ns := range tenant.Status.Namespaces { - go r.syncNamespace(ns, tenant.Spec.IngressClasses, tenant.Spec.StorageClasses, tenant.GetName(), wg, ch) + go r.syncNamespace(ns, tenant.Spec.IngressClasses, tenant.Spec.StorageClasses, tenant.Spec.NamespacesMetadata, tenant.GetName(), wg, ch) } wg.Wait() diff --git a/e2e/custom_capsule_group_test.go b/e2e/custom_capsule_group_test.go index dd8f96e2..efa887de 100644 --- a/e2e/custom_capsule_group_test.go +++ b/e2e/custom_capsule_group_test.go @@ -35,13 +35,14 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro Name: "tenant-assigned-custom-group", }, Spec: v1alpha1.TenantSpec{ - Owner: "alice", - StorageClasses: []string{}, - IngressClasses: []string{}, - LimitRanges: []corev1.LimitRangeSpec{}, - NamespaceQuota: 10, - NodeSelector: map[string]string{}, - ResourceQuota: []corev1.ResourceQuotaSpec{}, + Owner: "alice", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, }, } JustBeforeEach(func() { diff --git a/e2e/ingress_class_test.go b/e2e/ingress_class_test.go index 7249f175..ff605c61 100644 --- a/e2e/ingress_class_test.go +++ b/e2e/ingress_class_test.go @@ -41,8 +41,9 @@ var _ = Describe("when Tenant handles Ingress classes", func() { Name: "ingress-class", }, Spec: v1alpha1.TenantSpec{ - Owner: "ingress", - StorageClasses: []string{}, + Owner: "ingress", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, IngressClasses: []string{ "nginx", "haproxy", diff --git a/e2e/namespace_metadata_test.go b/e2e/namespace_metadata_test.go new file mode 100644 index 00000000..75107c79 --- /dev/null +++ b/e2e/namespace_metadata_test.go @@ -0,0 +1,81 @@ +//+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" + "k8s.io/apimachinery/pkg/types" + + "github.com/clastix/capsule/api/v1alpha1" +) + +var _ = Describe("creating a Namespace for a Tenant with additional metadata", func() { + tnt := &v1alpha1.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-metadata", + }, + Spec: v1alpha1.TenantSpec{ + Owner: "gatsby", + StorageClasses: []string{}, + IngressClasses: []string{}, + NamespacesMetadata: v1alpha1.NamespaceMetadata{ + AdditionalLabels: map[string]string{ + "k8s.io/custom-label": "foo", + "clastix.io/custom-label": "bar", + }, + AdditionalAnnotations: map[string]string{ + "k8s.io/custom-annotation": "bizz", + "clastix.io/custom-annotation": "buzz", + }, + }, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, + }, + } + JustBeforeEach(func() { + Expect(k8sClient.Create(context.TODO(), tnt)).Should(Succeed()) + }) + JustAfterEach(func() { + Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + }) + It("should contains additional Namespace metadata", func() { + ns := NewNamespace("namespace-metadata") + NamespaceCreationShouldSucceed(ns, tnt, defaultTimeoutInterval) + NamespaceShouldBeManagedByTenant(ns, tnt, defaultTimeoutInterval) + + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + By("checking additional labels", func() { + for _, l := range tnt.Spec.NamespacesMetadata.AdditionalLabels { + Expect(ns.Labels).Should(ContainElement(l)) + } + }) + By("checking additional annotations", func() { + for _, a := range tnt.Spec.NamespacesMetadata.AdditionalAnnotations { + Expect(ns.Annotations).Should(ContainElement(a)) + } + }) + }) +}) diff --git a/e2e/new_namespace_test.go b/e2e/new_namespace_test.go index 18b90877..6760cc07 100644 --- a/e2e/new_namespace_test.go +++ b/e2e/new_namespace_test.go @@ -38,6 +38,7 @@ var _ = Describe("creating a Namespace as Tenant owner", func() { Owner: "alice", StorageClasses: []string{}, IngressClasses: []string{}, + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, LimitRanges: []corev1.LimitRangeSpec{}, NamespaceQuota: 10, NodeSelector: map[string]string{}, diff --git a/e2e/overquota_namespace_test.go b/e2e/overquota_namespace_test.go index d29ac01e..8e57b483 100644 --- a/e2e/overquota_namespace_test.go +++ b/e2e/overquota_namespace_test.go @@ -35,13 +35,14 @@ var _ = Describe("creating a Namespace over-quota", func() { Name: "overquota-tenant", }, Spec: v1alpha1.TenantSpec{ - Owner: "bob", - StorageClasses: []string{}, - IngressClasses: []string{}, - LimitRanges: []corev1.LimitRangeSpec{}, - NamespaceQuota: 3, - NodeSelector: map[string]string{}, - ResourceQuota: []corev1.ResourceQuotaSpec{}, + Owner: "bob", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 3, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, }, } JustBeforeEach(func() { diff --git a/e2e/owner_webhooks_test.go b/e2e/owner_webhooks_test.go index ef177cd6..b786f33f 100644 --- a/e2e/owner_webhooks_test.go +++ b/e2e/owner_webhooks_test.go @@ -39,7 +39,8 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { Name: "tenant-owner", }, Spec: v1alpha1.TenantSpec{ - Owner: "ruby", + Owner: "ruby", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, StorageClasses: []string{ "cephfs", "glusterfs", diff --git a/e2e/protected_namespace_regex_test.go b/e2e/protected_namespace_regex_test.go index dce57813..639f64dd 100644 --- a/e2e/protected_namespace_regex_test.go +++ b/e2e/protected_namespace_regex_test.go @@ -2,10 +2,13 @@ /* 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. @@ -32,13 +35,14 @@ var _ = Describe("creating a Namespace with --protected-namespace-regex enabled" Name: "tenantprotectednamespace", }, Spec: v1alpha1.TenantSpec{ - Owner: "alice", - StorageClasses: []string{}, - IngressClasses: []string{}, - LimitRanges: []corev1.LimitRangeSpec{}, - NamespaceQuota: 10, - NodeSelector: map[string]string{}, - ResourceQuota: []corev1.ResourceQuotaSpec{}, + Owner: "alice", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, }, } JustBeforeEach(func() { diff --git a/e2e/resource_quota_exceeded_test.go b/e2e/resource_quota_exceeded_test.go index bad90573..1f1ff201 100644 --- a/e2e/resource_quota_exceeded_test.go +++ b/e2e/resource_quota_exceeded_test.go @@ -42,9 +42,10 @@ var _ = Describe("exceeding Tenant resource quota", func() { Name: "tenant-resources-changes", }, Spec: v1alpha1.TenantSpec{ - Owner: "bobby", - StorageClasses: []string{}, - IngressClasses: []string{}, + Owner: "bobby", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, LimitRanges: []corev1.LimitRangeSpec{ { Limits: []corev1.LimitRangeItem{ diff --git a/e2e/selecting_non_owned_tenant_test.go b/e2e/selecting_non_owned_tenant_test.go index e5f9a8df..618f5445 100644 --- a/e2e/selecting_non_owned_tenant_test.go +++ b/e2e/selecting_non_owned_tenant_test.go @@ -35,13 +35,14 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", func() Name: "tenant-non-owned", }, Spec: v1alpha1.TenantSpec{ - Owner: "undefined", - StorageClasses: []string{}, - IngressClasses: []string{}, - LimitRanges: []corev1.LimitRangeSpec{}, - NamespaceQuota: 10, - NodeSelector: map[string]string{}, - ResourceQuota: []corev1.ResourceQuotaSpec{}, + Owner: "undefined", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, }, } JustBeforeEach(func() { diff --git a/e2e/selecting_tenant_test.go b/e2e/selecting_tenant_test.go index 293cfd70..b4b3afc8 100644 --- a/e2e/selecting_tenant_test.go +++ b/e2e/selecting_tenant_test.go @@ -35,13 +35,14 @@ var _ = Describe("creating a Namespace with Tenant selector", func() { Name: "tenant-one", }, Spec: v1alpha1.TenantSpec{ - Owner: "john", - StorageClasses: []string{}, - IngressClasses: []string{}, - LimitRanges: []corev1.LimitRangeSpec{}, - NamespaceQuota: 10, - NodeSelector: map[string]string{}, - ResourceQuota: []corev1.ResourceQuotaSpec{}, + Owner: "john", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, }, } t2 := &v1alpha1.Tenant{ @@ -49,13 +50,14 @@ var _ = Describe("creating a Namespace with Tenant selector", func() { Name: "tenant-two", }, Spec: v1alpha1.TenantSpec{ - Owner: "john", - StorageClasses: []string{}, - IngressClasses: []string{}, - LimitRanges: []corev1.LimitRangeSpec{}, - NamespaceQuota: 10, - NodeSelector: map[string]string{}, - ResourceQuota: []corev1.ResourceQuotaSpec{}, + Owner: "john", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, + LimitRanges: []corev1.LimitRangeSpec{}, + NamespaceQuota: 10, + NodeSelector: map[string]string{}, + ResourceQuota: []corev1.ResourceQuotaSpec{}, }, } JustBeforeEach(func() { diff --git a/e2e/storage_class_test.go b/e2e/storage_class_test.go index eff5cc4f..4050171f 100644 --- a/e2e/storage_class_test.go +++ b/e2e/storage_class_test.go @@ -38,7 +38,8 @@ var _ = Describe("when Tenant handles Storage classes", func() { Name: "storage-class", }, Spec: v1alpha1.TenantSpec{ - Owner: "storage", + Owner: "storage", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, StorageClasses: []string{ "cephfs", "glusterfs", diff --git a/e2e/tenant_resources_changes_test.go b/e2e/tenant_resources_changes_test.go index 893f2d56..e0c602f3 100644 --- a/e2e/tenant_resources_changes_test.go +++ b/e2e/tenant_resources_changes_test.go @@ -40,9 +40,10 @@ var _ = Describe("changing Tenant managed Kubernetes resources", func() { Name: "tenant-resources-changes", }, Spec: v1alpha1.TenantSpec{ - Owner: "laura", - StorageClasses: []string{}, - IngressClasses: []string{}, + Owner: "laura", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, LimitRanges: []corev1.LimitRangeSpec{ { Limits: []corev1.LimitRangeItem{ diff --git a/e2e/tenant_resources_test.go b/e2e/tenant_resources_test.go index 7be230ec..8120db23 100644 --- a/e2e/tenant_resources_test.go +++ b/e2e/tenant_resources_test.go @@ -41,9 +41,10 @@ var _ = Describe("creating namespaces within a Tenant with resources", func() { Name: "tenant-resources", }, Spec: v1alpha1.TenantSpec{ - Owner: "john", - StorageClasses: []string{}, - IngressClasses: []string{}, + Owner: "john", + NamespacesMetadata: v1alpha1.NamespaceMetadata{}, + StorageClasses: []string{}, + IngressClasses: []string{}, LimitRanges: []corev1.LimitRangeSpec{ { Limits: []corev1.LimitRangeItem{