diff --git a/e2e/ingress_class_extensions_test.go b/e2e/ingress_class_extensions_test.go index e17b205e..71b912be 100644 --- a/e2e/ingress_class_extensions_test.go +++ b/e2e/ingress_class_extensions_test.go @@ -35,13 +35,18 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1", }, }, IngressOptions: capsulev1beta2.IngressOptions{ - AllowedClasses: &api.SelectorAllowedListSpec{ - AllowedListSpec: api.AllowedListSpec{ - Exact: []string{ - "nginx", - "haproxy", + AllowedClasses: &api.DefaultAllowedListSpec{ + Default: "tenant-default", + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + AllowedListSpec: api.AllowedListSpec{ + Exact: []string{"nginx", "haproxy"}, + Regex: "^oil-.*$", + }, + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "customers", + }, }, - Regex: "^oil-.*$", }, }, }, diff --git a/e2e/ingress_class_networking_test.go b/e2e/ingress_class_networking_test.go index 0ecdd67b..99e19118 100644 --- a/e2e/ingress_class_networking_test.go +++ b/e2e/ingress_class_networking_test.go @@ -9,52 +9,151 @@ import ( "context" "errors" "fmt" + "strconv" + "strings" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" capsulev1beta2 "github.com/clastix/capsule/api/v1beta2" "github.com/clastix/capsule/pkg/api" ) var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1", func() { - tnt := &capsulev1beta2.Tenant{ + tntNoDefault := &capsulev1beta2.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "ingress-class-networking-v1", + Name: "ic-selector-networking-v1", }, Spec: capsulev1beta2.TenantSpec{ Owners: []capsulev1beta2.OwnerSpec{ { - Name: "ingress", + Name: "ingress-selector", Kind: "User", }, }, IngressOptions: capsulev1beta2.IngressOptions{ - AllowedClasses: &api.SelectorAllowedListSpec{ - AllowedListSpec: api.AllowedListSpec{ - Exact: []string{ - "nginx", - "haproxy", + AllowedClasses: &api.DefaultAllowedListSpec{ + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + AllowedListSpec: api.AllowedListSpec{ + Exact: []string{"nginx", "haproxy"}, + Regex: "^oil-.*$", + }, + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "customers", + }, }, - Regex: "^oil-.*$", }, }, }, }, } + tntWithDefault := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ic-default-networking-v1", + }, + Spec: capsulev1beta2.TenantSpec{ + Owners: []capsulev1beta2.OwnerSpec{ + { + Name: "ingress-default", + Kind: "User", + }, + }, + IngressOptions: capsulev1beta2.IngressOptions{ + AllowedClasses: &api.DefaultAllowedListSpec{ + Default: "tenant-default", + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": "tenant-default", + }, + }, + }, + }, + }, + }, + } + + tenantDefault := networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-default", + Labels: map[string]string{ + "name": "tenant-default", + "env": "e2e", + }, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: "k8s.io/ingress-nginx", + }, + } + + globalDefault := networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "global-default", + Labels: map[string]string{ + "name": "global-default", + "env": "customers", + }, + Annotations: map[string]string{ + "ingressclass.kubernetes.io/is-default-class": "true", + }, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: "k8s.io/ingress-nginx", + }, + } + + disallowedGlobalDefault := networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "disallowed", + Labels: map[string]string{ + "name": "disallowed-global-default", + "env": "e2e", + }, + Annotations: map[string]string{ + "ingressclass.kubernetes.io/is-default-class": "true", + }, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: "k8s.io/ingress-nginx", + }, + } + JustBeforeEach(func() { - EventuallyCreation(func() error { - tnt.ResourceVersion = "" - return k8sClient.Create(context.TODO(), tnt) - }).Should(Succeed()) + for _, tnt := range []*capsulev1beta2.Tenant{tntWithDefault, tntNoDefault} { + EventuallyCreation(func() error { + tnt.ResourceVersion = "" + + return k8sClient.Create(context.TODO(), tnt) + }).Should(Succeed()) + } }) + JustAfterEach(func() { - Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + for _, tnt := range []*capsulev1beta2.Tenant{tntWithDefault, tntNoDefault} { + Eventually(func() error { + return k8sClient.Delete(context.TODO(), tnt) + }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) + } + + Eventually(func() (err error) { + req, _ := labels.NewRequirement("env", selection.Exists, nil) + + return k8sClient.DeleteAllOf(context.TODO(), &networkingv1.IngressClass{}, &client.DeleteAllOfOptions{ + ListOptions: client.ListOptions{ + LabelSelector: labels.NewSelector().Add(*req), + }, + }) + }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) It("should block a non allowed class", func() { @@ -66,10 +165,10 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" } ns := NewNamespace("") - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefault.Spec.Owners[0]) - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + NamespaceCreation(ns, tntNoDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) By("non-specifying at all", func() { Eventually(func() (err error) { @@ -149,12 +248,12 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" } ns := NewNamespace("") - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefault.Spec.Owners[0]) - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + NamespaceCreation(ns, tntNoDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) - for _, c := range tnt.Spec.IngressOptions.AllowedClasses.Exact { + for _, c := range tntNoDefault.Spec.IngressOptions.AllowedClasses.Exact { Eventually(func() (err error) { i := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ @@ -189,12 +288,12 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" } ns := NewNamespace("") - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefault.Spec.Owners[0]) - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + NamespaceCreation(ns, tntNoDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) - for _, c := range tnt.Spec.IngressOptions.AllowedClasses.Exact { + for _, c := range tntNoDefault.Spec.IngressOptions.AllowedClasses.Exact { Eventually(func() (err error) { i := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ @@ -227,11 +326,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" } ns := NewNamespace("") - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefault.Spec.Owners[0]) ingressClass := "oil-ingress" - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + NamespaceCreation(ns, tntNoDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) Eventually(func() (err error) { i := &networkingv1.Ingress{ @@ -266,11 +365,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" } ns := NewNamespace("") - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefault.Spec.Owners[0]) ingressClass := "oil-haproxy" - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + NamespaceCreation(ns, tntNoDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) Eventually(func() (err error) { i := &networkingv1.Ingress{ @@ -293,4 +392,276 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" return }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) + + It("should allow enabled Ingress by selector using the deprecated annotation", func() { + if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil { + missingAPIError := &meta.NoKindMatchError{} + if errors.As(err, &missingAPIError) { + Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error())) + } + } + + for i, sc := range []string{"customer-nginx", "customer-haproxy"} { + ingressClass := strings.Join([]string{sc, "-", strconv.Itoa(i)}, "") + class := &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingressClass, + Labels: map[string]string{ + "name": ingressClass, + "env": "customers", + }, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: "k8s.io/ingress-nginx", + }, + } + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + + i := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("allowed-%s", ingressClass), + Annotations: map[string]string{ + "kubernetes.io/ingress.class": ingressClass, + }, + }, + Spec: networkingv1.IngressSpec{ + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + } + + ns := NewNamespace("") + cs := ownerClient(tntNoDefault.Spec.Owners[0]) + + NamespaceCreation(ns, tntNoDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + EventuallyCreation(func() error { + _, err := cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), i, metav1.CreateOptions{}) + return err + }).Should(Succeed()) + } + }) + + It("should allow enabled Ingress by selector using the ingressClassName field", func() { + if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil { + missingAPIError := &meta.NoKindMatchError{} + if errors.As(err, &missingAPIError) { + Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error())) + } + } + + for i, sc := range []string{"customer-nginx", "customer-haproxy"} { + ingressClass := strings.Join([]string{sc, "-", strconv.Itoa(i)}, "") + class := &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingressClass, + Labels: map[string]string{ + "name": ingressClass, + "env": "customers", + }, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: "k8s.io/ingress-nginx", + }, + } + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + + i := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("allowed-%s", ingressClass), + }, + Spec: networkingv1.IngressSpec{ + IngressClassName: &ingressClass, + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + } + + ns := NewNamespace("") + cs := ownerClient(tntNoDefault.Spec.Owners[0]) + + NamespaceCreation(ns, tntNoDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + EventuallyCreation(func() error { + _, err := cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), i, metav1.CreateOptions{}) + return err + }).Should(Succeed()) + } + }) + + It("should mutate to default tenant IngressClass (class not does not exist)", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + i := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-default-ingress", + Namespace: ns.GetName(), + }, + Spec: networkingv1.IngressSpec{ + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), i) + }).Should(Succeed()) + + Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: i.GetName(), Namespace: ns.GetName()}, i)) + Expect(*i.Spec.IngressClassName).To(Equal("tenant-default")) + }) + + It("should mutate to default tenant IngressClass (class exists)", func() { + if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil { + missingAPIError := &meta.NoKindMatchError{} + if errors.As(err, &missingAPIError) { + Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error())) + } + } + + class := tenantDefault + Expect(k8sClient.Create(context.TODO(), &class)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + i := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-default-ingress", + Namespace: ns.GetName(), + }, + Spec: networkingv1.IngressSpec{ + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), i) + }).Should(Succeed()) + + Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: i.GetName(), Namespace: ns.GetName()}, i)) + Expect(*i.Spec.IngressClassName).To(Equal(class.GetName())) + }) + + It("shoult mutate to default tenant IngressClass although the cluster global one is not allowed", func() { + if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil { + missingAPIError := &meta.NoKindMatchError{} + if errors.As(err, &missingAPIError) { + Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error())) + } + } + + class := tenantDefault + global := disallowedGlobalDefault + + Expect(k8sClient.Create(context.TODO(), &class)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), &global)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + i := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-default-global-ingress", + Namespace: ns.GetName(), + }, + Spec: networkingv1.IngressSpec{ + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), i) + }).Should(Succeed()) + Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: i.GetName(), Namespace: ns.GetName()}, i)) + Expect(*i.Spec.IngressClassName).To(Equal(class.GetName())) + // Run Patch To verify same happens on Update + i.Spec.IngressClassName = nil + Expect(k8sClient.Update(context.Background(), i)).Should(Succeed()) + Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: i.GetName(), Namespace: ns.GetName()}, i)) + Expect(*i.Spec.IngressClassName).To(Equal(class.GetName())) + }) + + It("should mutate to default tenant IngressClass although the cluster global one is allowed", func() { + if err := k8sClient.List(context.Background(), &networkingv1.IngressList{}); err != nil { + missingAPIError := &meta.NoKindMatchError{} + if errors.As(err, &missingAPIError) { + Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error())) + } + } + + class := tenantDefault + global := globalDefault + + Expect(k8sClient.Create(context.TODO(), &class)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), &global)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + i := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-default-global-ingress", + Namespace: ns.GetName(), + }, + Spec: networkingv1.IngressSpec{ + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), i) + }).Should(Succeed()) + Expect(*i.Spec.IngressClassName).To(Equal(class.GetName())) + // Run Patch To verify same happens on Update + i.Spec.IngressClassName = nil + Expect(k8sClient.Update(context.Background(), i)).Should(Succeed()) + Expect(*i.Spec.IngressClassName).To(Equal(class.GetName())) + }) }) diff --git a/e2e/owner_webhooks_test.go b/e2e/owner_webhooks_test.go index b20772b6..57a990b5 100644 --- a/e2e/owner_webhooks_test.go +++ b/e2e/owner_webhooks_test.go @@ -33,11 +33,13 @@ var _ = Describe("when Tenant owner interacts with the webhooks", func() { Kind: "User", }, }, - StorageClasses: &api.SelectorAllowedListSpec{ - AllowedListSpec: api.AllowedListSpec{ - Exact: []string{ - "cephfs", - "glusterfs", + StorageClasses: &api.DefaultAllowedListSpec{ + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + AllowedListSpec: api.AllowedListSpec{ + Exact: []string{ + "cephfs", + "glusterfs", + }, }, }, }, diff --git a/e2e/pod_priority_class_test.go b/e2e/pod_priority_class_test.go index f2251f79..d4737650 100644 --- a/e2e/pod_priority_class_test.go +++ b/e2e/pod_priority_class_test.go @@ -13,17 +13,44 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/scheduling/v1" + schedulingv1 "k8s.io/api/scheduling/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "sigs.k8s.io/controller-runtime/pkg/client" capsulev1beta2 "github.com/clastix/capsule/api/v1beta2" "github.com/clastix/capsule/pkg/api" ) var _ = Describe("enforcing a Priority Class", func() { - tnt := &capsulev1beta2.Tenant{ + tntWithDefaults := &capsulev1beta2.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "priority-class", + Name: "priority-class-defaults", + }, + Spec: capsulev1beta2.TenantSpec{ + Owners: capsulev1beta2.OwnerListSpec{ + { + Name: "paul", + Kind: "User", + }, + }, + PriorityClasses: &api.DefaultAllowedListSpec{ + Default: "tenant-default", + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "customer", + }, + }, + }, + }, + }, + } + + tntNoDefaults := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "priority-class-no-defaults", }, Spec: capsulev1beta2.TenantSpec{ Owners: capsulev1beta2.OwnerListSpec{ @@ -32,33 +59,89 @@ var _ = Describe("enforcing a Priority Class", func() { Kind: "User", }, }, - PriorityClasses: &api.SelectorAllowedListSpec{ - AllowedListSpec: api.AllowedListSpec{ - Exact: []string{"gold"}, - Regex: "pc\\-\\w+", - }, - LabelSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "env": "customers", + PriorityClasses: &api.DefaultAllowedListSpec{ + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + AllowedListSpec: api.AllowedListSpec{ + Exact: []string{"gold"}, + Regex: "pc\\-\\w+", + }, + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "customer", + }, }, }, }, }, } + pcTenantPreemption := corev1.PreemptionPolicy("PreemptLowerPriority") + tenantDefault := schedulingv1.PriorityClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-default", + Labels: map[string]string{ + "env": "e2e", + }, + }, + Description: "tenant default priorityclass", + Value: 1212, + PreemptionPolicy: &pcTenantPreemption, + GlobalDefault: false, + } + + globalDefault := schedulingv1.PriorityClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "global-default", + Labels: map[string]string{ + "env": "customer", + }, + }, + Description: "global default priorityclass", + Value: 100000, + GlobalDefault: true, + } + + disallowedGlobalDefault := schedulingv1.PriorityClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "disallowed-global-default", + Labels: map[string]string{ + "env": "e2e", + }, + }, + Description: "global default priorityclass", + Value: 100000, + GlobalDefault: true, + } + JustBeforeEach(func() { - EventuallyCreation(func() error { - tnt.ResourceVersion = "" - return k8sClient.Create(context.TODO(), tnt) - }).Should(Succeed()) + for _, tnt := range []*capsulev1beta2.Tenant{tntWithDefaults, tntNoDefaults} { + EventuallyCreation(func() error { + tnt.ResourceVersion = "" + + return k8sClient.Create(context.TODO(), tnt) + }).Should(Succeed()) + } }) + JustAfterEach(func() { - Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + for _, tnt := range []*capsulev1beta2.Tenant{tntWithDefaults, tntNoDefaults} { + Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + } + + Eventually(func() (err error) { + req, _ := labels.NewRequirement("env", selection.Exists, nil) + + return k8sClient.DeleteAllOf(context.TODO(), &schedulingv1.PriorityClass{}, &client.DeleteAllOfOptions{ + ListOptions: client.ListOptions{ + LabelSelector: labels.NewSelector().Add(*req), + }, + }) + }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) It("should block non allowed Priority Class", func() { ns := NewNamespace("") - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + NamespaceCreation(ns, tntNoDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -75,29 +158,71 @@ var _ = Describe("enforcing a Priority Class", func() { }, } - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) EventuallyCreation(func() error { _, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{}) return err }).ShouldNot(Succeed()) }) + It("should block non matching selector match", func() { + for i, pc := range []string{"internal-bronze", "internal-silver", "internal-gold"} { + priorityName := strings.Join([]string{pc, "-", strconv.Itoa(i)}, "") + class := &schedulingv1.PriorityClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: priorityName, + Labels: map[string]string{ + "env": "internal", + }, + }, + Description: "fake PriorityClass for e2e", + Value: int32(10000 * (i + 2)), + } + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: pc, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "quay.io/google-containers/pause-amd64:3.0", + }, + }, + PriorityClassName: class.GetName(), + }, + } + + ns := NewNamespace("") + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) + + NamespaceCreation(ns, tntNoDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + EventuallyCreation(func() error { + _, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{}) + return err + }).ShouldNot(Succeed()) + } + }) + It("should allow exact match", func() { - pc := &v1.PriorityClass{ + pc := &schedulingv1.PriorityClass{ ObjectMeta: metav1.ObjectMeta{ Name: "gold", + Labels: map[string]string{ + "env": "e2e", + }, }, Description: "fake PriorityClass for e2e", Value: 10000, } Expect(k8sClient.Create(context.TODO(), pc)).Should(Succeed()) - defer func() { - Expect(k8sClient.Delete(context.TODO(), pc)).Should(Succeed()) - }() - ns := NewNamespace("") - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + NamespaceCreation(ns, tntNoDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -114,7 +239,7 @@ var _ = Describe("enforcing a Priority Class", func() { }, } - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) EventuallyCreation(func() error { _, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{}) return err @@ -123,13 +248,15 @@ var _ = Describe("enforcing a Priority Class", func() { It("should allow regex match", func() { ns := NewNamespace("") - - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + NamespaceCreation(ns, tntNoDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) for i, pc := range []string{"pc-bronze", "pc-silver", "pc-gold"} { - class := &v1.PriorityClass{ + class := &schedulingv1.PriorityClass{ ObjectMeta: metav1.ObjectMeta{ Name: pc, + Labels: map[string]string{ + "env": "e2e", + }, }, Description: "fake PriorityClass for e2e", Value: int32(10000 * (i + 2)), @@ -137,45 +264,36 @@ var _ = Describe("enforcing a Priority Class", func() { Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: pc, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "container", - Image: "quay.io/google-containers/pause-amd64:3.0", - }, - }, - PriorityClassName: class.GetName(), - }, - } - - cs := ownerClient(tnt.Spec.Owners[0]) - EventuallyCreation(func() error { - _, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{}) - return err - }).Should(Succeed()) + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: pc, + Namespace: ns.GetName(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "quay.io/google-containers/pause-amd64:3.0", + }, + }, + PriorityClassName: class.GetName(), + }, + } - Expect(k8sClient.Delete(context.TODO(), class)).Should(Succeed()) + return k8sClient.Create(context.Background(), pod) + }).Should(Succeed()) } }) It("should allow selector match", func() { - ns := NewNamespace("priority-selector-match") - - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - for i, pc := range []string{"customer-bronze", "customer-silver", "customer-gold"} { priorityName := strings.Join([]string{pc, "-", strconv.Itoa(i)}, "") - class := &v1.PriorityClass{ + class := &schedulingv1.PriorityClass{ ObjectMeta: metav1.ObjectMeta{ - Name: pc, + Name: priorityName, Labels: map[string]string{ - "name": priorityName, - "env": "customers", + "env": "customer", }, }, Description: "fake PriorityClass for e2e", @@ -198,15 +316,153 @@ var _ = Describe("enforcing a Priority Class", func() { }, } - cs := ownerClient(tnt.Spec.Owners[0]) + ns := NewNamespace("") + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) + + NamespaceCreation(ns, tntNoDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) EventuallyCreation(func() error { _, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{}) return err }).Should(Succeed()) - - Expect(k8sClient.Delete(context.TODO(), class)).Should(Succeed()) } }) + It("fail if default tenant PriorityClass is absent", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "quay.io/google-containers/pause-amd64:3.0", + }, + }, + }, + } + + ns := NewNamespace("") + cs := ownerClient(tntWithDefaults.Spec.Owners[0]) + + NamespaceCreation(ns, tntWithDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + EventuallyCreation(func() error { + _, err := cs.CoreV1().Pods(ns.GetName()).Create(context.Background(), pod, metav1.CreateOptions{}) + + return err + }).ShouldNot(Succeed()) + + Expect(k8sClient.Delete(context.TODO(), ns)).Should(Succeed()) + }) + + It("should mutate to default tenant PriorityClass", func() { + class := tenantDefault.DeepCopy() + class.SetResourceVersion("") + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-default-present", + Namespace: ns.GetName(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "quay.io/google-containers/pause-amd64:3.0", + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), &pod) + }).Should(Succeed()) + // Check if correct mutated + Expect(pod.Spec.PriorityClassName).To(Equal(class.GetName())) + Expect(pod.Spec.Priority).To(Equal(&class.Value)) + Expect(pod.Spec.PreemptionPolicy).To(Equal(class.PreemptionPolicy)) + }) + + It("should mutate to default tenant PriorityClass although the cluster global one is not allowed", func() { + class := tenantDefault.DeepCopy() + class.SetResourceVersion("") + + global := disallowedGlobalDefault.DeepCopy() + global.SetResourceVersion("") + + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), global)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-default-global-default", + Namespace: ns.GetName(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "quay.io/google-containers/pause-amd64:3.0", + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), pod) + }).Should(Succeed()) + // Check if correct applied + Expect(pod.Spec.PriorityClassName).To(Equal(class.GetName())) + Expect(pod.Spec.Priority).To(Equal(&class.Value)) + Expect(pod.Spec.PreemptionPolicy).To(Equal(class.PreemptionPolicy)) + }) + + It("should mutate to default tenant PriorityClass although the cluster global one is allowed", func() { + class := tenantDefault.DeepCopy() + class.SetResourceVersion("") + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + + global := globalDefault.DeepCopy() + global.SetResourceVersion("") + Expect(k8sClient.Create(context.TODO(), global)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-default-allowed", + Namespace: ns.GetName(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container", + Image: "quay.io/google-containers/pause-amd64:3.0", + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), pod) + }).Should(Succeed()) + // Check if correctly applied + Expect(pod.Spec.PriorityClassName).To(Equal(class.GetName())) + Expect(*pod.Spec.Priority).To(Equal(class.Value)) + }) }) diff --git a/e2e/storage_class_test.go b/e2e/storage_class_test.go index 2469a871..d40d81df 100644 --- a/e2e/storage_class_test.go +++ b/e2e/storage_class_test.go @@ -3,64 +3,148 @@ // Copyright 2020-2021 Clastix Labs // SPDX-License-Identifier: Apache-2.0 +// "sigs.k8s.io/controller-runtime/pkg/client" + package e2e import ( "context" + "fmt" + "strconv" + "strings" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" capsulev1beta2 "github.com/clastix/capsule/api/v1beta2" "github.com/clastix/capsule/pkg/api" ) var _ = Describe("when Tenant handles Storage classes", func() { - tnt := &capsulev1beta2.Tenant{ + tntNoDefaults := &capsulev1beta2.Tenant{ ObjectMeta: metav1.ObjectMeta{ - Name: "storage-class", + Name: "storage-class-selector", }, Spec: capsulev1beta2.TenantSpec{ Owners: capsulev1beta2.OwnerListSpec{ { - Name: "storage", + Name: "selector", Kind: "User", }, }, - StorageClasses: &api.SelectorAllowedListSpec{ - AllowedListSpec: api.AllowedListSpec{ - Exact: []string{ - "cephfs", - "glusterfs", + StorageClasses: &api.DefaultAllowedListSpec{ + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + AllowedListSpec: api.AllowedListSpec{ + Exact: []string{"cephfs", "glusterfs"}, + Regex: "^oil-.*$", + }, + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "customer", + }, }, - Regex: "^oil-.*$", }, }, }, } + tntWithDefault := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "storage-class-default", + }, + Spec: capsulev1beta2.TenantSpec{ + Owners: capsulev1beta2.OwnerListSpec{ + { + Name: "default", + Kind: "User", + }, + }, + StorageClasses: &api.DefaultAllowedListSpec{ + Default: "tenant-default", + SelectorAllowedListSpec: api.SelectorAllowedListSpec{ + LabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": "tenant-default", + }, + }, + }, + }, + }, + } + + tenantDefault := storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-default", + Labels: map[string]string{ + "name": "tenant-default", + "env": "e2e", + }, + }, + Provisioner: "kubernetes.io/no-provisioner", + } + globalDefault := storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "global-default", + Labels: map[string]string{ + "env": "customer", + }, + }, + Provisioner: "kubernetes.io/no-provisioner", + } + disallowedGlobalDefault := storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "disallowed-global-default", + Labels: map[string]string{ + "name": "disallowed-global-default", + "env": "e2e", + }, + }, + Provisioner: "kubernetes.io/no-provisioner", + } + JustBeforeEach(func() { - EventuallyCreation(func() error { - tnt.ResourceVersion = "" - return k8sClient.Create(context.TODO(), tnt) - }).Should(Succeed()) + for _, tnt := range []*capsulev1beta2.Tenant{tntNoDefaults, tntWithDefault} { + EventuallyCreation(func() error { + tnt.ResourceVersion = "" + + return k8sClient.Create(context.TODO(), tnt) + }).Should(Succeed()) + } }) JustAfterEach(func() { - Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + for _, tnt := range []*capsulev1beta2.Tenant{tntNoDefaults, tntWithDefault} { + Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + } + + Eventually(func() (err error) { + req, _ := labels.NewRequirement("env", selection.Exists, nil) + + return k8sClient.DeleteAllOf(context.TODO(), &storagev1.StorageClass{}, &client.DeleteAllOfOptions{ + ListOptions: client.ListOptions{ + LabelSelector: labels.NewSelector().Add(*req), + }, + }) + }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) - It("should fails", func() { + It("should fail", func() { + k8sClient.Create(context.TODO(), tntNoDefaults) + ns := NewNamespace("") - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + NamespaceCreation(ns, tntNoDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) By("non-specifying it", func() { Eventually(func() (err error) { - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) p := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "denied-pvc", @@ -80,7 +164,7 @@ var _ = Describe("when Tenant handles Storage classes", func() { }) By("specifying a forbidden one", func() { Eventually(func() (err error) { - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) p := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "mighty-storage", @@ -98,16 +182,54 @@ var _ = Describe("when Tenant handles Storage classes", func() { return }, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed()) }) + By("specifying with not matching label", func() { + for i, sc := range []string{"internal-hdd", "internal-ssd"} { + storageName := strings.Join([]string{sc, "-", strconv.Itoa(i)}, "") + class := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("sc-%s", storageName), + Labels: map[string]string{ + "env": "internal", + }, + }, + Provisioner: "kubernetes.io/no-provisioner", + } + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + + p := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: storageName, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: &storageName, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceStorage: resource.MustParse("3Gi"), + }, + }, + }, + } + + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) + + EventuallyCreation(func() error { + _, err := cs.CoreV1().PersistentVolumeClaims(ns.GetName()).Create(context.Background(), p, metav1.CreateOptions{}) + return err + }).ShouldNot(Succeed()) + } + }) + }) It("should allow", func() { ns := NewNamespace("") - cs := ownerClient(tnt.Spec.Owners[0]) + cs := ownerClient(tntNoDefaults.Spec.Owners[0]) - NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) - TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + NamespaceCreation(ns, tntNoDefaults.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntNoDefaults, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) By("using exact matches", func() { - for _, c := range tnt.Spec.StorageClasses.Exact { + for _, c := range tntNoDefaults.Spec.StorageClasses.Exact { Eventually(func() (err error) { p := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -149,5 +271,156 @@ var _ = Describe("when Tenant handles Storage classes", func() { return }, defaultTimeoutInterval, defaultPollInterval).Should(Succeed()) }) + By("using a selector match", func() { + for i, sc := range []string{"customer-hdd", "customer-ssd"} { + storageName := strings.Join([]string{sc, "-", strconv.Itoa(i)}, "") + class := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: storageName, + Labels: map[string]string{ + "env": "customer", + }, + }, + Provisioner: "kubernetes.io/no-provisioner", + } + Expect(k8sClient.Create(context.TODO(), class)).Should(Succeed()) + + EventuallyCreation(func() error { + p := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: storageName, + Namespace: ns.GetName(), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: &storageName, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceStorage: resource.MustParse("3Gi"), + }, + }, + }, + } + + return k8sClient.Create(context.Background(), p) + }).Should(Succeed()) + } + }) + }) + + It("should mutate to default tenant StorageClass (class does not exists)", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + p := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-default-sc", + Namespace: ns.GetName(), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceStorage: resource.MustParse("3Gi"), + }, + }, + }, + } + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), p) + }).Should(Succeed()) + Expect(*p.Spec.StorageClassName).To(Equal("tenant-default")) + }) + + It("should mutate to default tenant StorageClass (class exists)", func() { + class := tenantDefault + Expect(k8sClient.Create(context.TODO(), &class)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + p := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-default-sc-present", + Namespace: ns.GetName(), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceStorage: resource.MustParse("3Gi"), + }, + }, + }, + } + + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), p) + }).Should(Succeed()) + Expect(*p.Spec.StorageClassName).To(Equal(class.GetName())) + }) + + It("should mutate to default tenant StorageClass although cluster global ons is not allowed", func() { + class := tenantDefault + global := disallowedGlobalDefault + + Expect(k8sClient.Create(context.TODO(), &class)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), &global)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + p := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-default-sc-present", + Namespace: ns.GetName(), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceStorage: resource.MustParse("3Gi"), + }, + }, + }, + } + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), p) + }).Should(Succeed()) + Expect(*p.Spec.StorageClassName).To(Equal(class.GetName())) + }) + + It("should mutate to default tenant StorageClass although cluster global ons is allowed", func() { + class := tenantDefault + global := globalDefault + + Expect(k8sClient.Create(context.TODO(), &class)).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), &global)).Should(Succeed()) + + ns := NewNamespace("") + NamespaceCreation(ns, tntWithDefault.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tntWithDefault, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + p := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-default-sc-present", + Namespace: ns.GetName(), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceStorage: resource.MustParse("3Gi"), + }, + }, + }, + } + EventuallyCreation(func() error { + return k8sClient.Create(context.Background(), p) + }).Should(Succeed()) + Expect(*p.Spec.StorageClassName).To(Equal(class.GetName())) }) })