From 6b87c15df5429a02f7538e711f26cd330669563c Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 21 Nov 2023 16:37:21 +0100 Subject: [PATCH] more tests for the new features --- api/v1alpha1/limitador_types.go | 2 +- .../limitador_controller_affinity_test.go | 146 ++++++++ .../limitador_controller_limits_test.go | 157 +++++++++ controllers/limitador_controller_pdb_test.go | 112 ++++++ .../limitador_controller_ports_test.go | 323 ++++++++++++++++++ .../limitador_controller_replicas_test.go | 112 ++++++ .../limitador_controller_resources_test.go | 138 ++++++++ controllers/limitador_controller_test.go | 218 ++++++------ .../limitador_controller_version_test.go | 114 +++++++ pkg/limitador/deployment_options_test.go | 19 +- 10 files changed, 1219 insertions(+), 122 deletions(-) create mode 100644 controllers/limitador_controller_affinity_test.go create mode 100644 controllers/limitador_controller_limits_test.go create mode 100644 controllers/limitador_controller_pdb_test.go create mode 100644 controllers/limitador_controller_ports_test.go create mode 100644 controllers/limitador_controller_replicas_test.go create mode 100644 controllers/limitador_controller_resources_test.go create mode 100644 controllers/limitador_controller_version_test.go diff --git a/api/v1alpha1/limitador_types.go b/api/v1alpha1/limitador_types.go index 40dd2c85..2dd158ab 100644 --- a/api/v1alpha1/limitador_types.go +++ b/api/v1alpha1/limitador_types.go @@ -279,7 +279,7 @@ type LimitadorStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` // Represents the observations of a foo's current state. - // Known .status.conditions.type are: "Available" + // Known .status.conditions.type are: "Ready" // +patchMergeKey=type // +patchStrategy=merge // +listType=map diff --git a/controllers/limitador_controller_affinity_test.go b/controllers/limitador_controller_affinity_test.go new file mode 100644 index 00000000..82617bb8 --- /dev/null +++ b/controllers/limitador_controller_affinity_test.go @@ -0,0 +1,146 @@ +package controllers + +import ( + "context" + "reflect" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/limitador-operator/pkg/limitador" +) + +var _ = Describe("Limitador controller manages affinity", func() { + + var testNamespace string + + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) + + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Creating a new Limitador object with specific affinity", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + affinity := &v1.Affinity{ + PodAntiAffinity: &v1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{ + { + Weight: 100, + PodAffinityTerm: v1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "pod": "label", + }, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + } + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + limitadorObj.Spec.Affinity = affinity + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should create a new deployment with the custom affinity", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, + &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(deployment.Spec.Template.Spec.Affinity).To(Equal(affinity)) + }) + }) + + Context("Updating limitador object with new affinity settings", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + affinity := &v1.Affinity{ + PodAntiAffinity: &v1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{ + { + Weight: 100, + PodAffinityTerm: v1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "pod": "label", + }, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + } + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should modify the deployment with the affinity custom settings", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(deployment.Spec.Template.Spec.Affinity).To(BeNil()) + + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitadorObj.Name, + }, &updatedLimitador) + + if err != nil { + return false + } + + updatedLimitador.Spec.Affinity = affinity.DeepCopy() + + return k8sClient.Update(context.TODO(), &updatedLimitador) == nil + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newDeployment := appsv1.Deployment{} + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &newDeployment) + + if err != nil { + return false + } + + return reflect.DeepEqual(newDeployment.Spec.Template.Spec.Affinity, affinity) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/controllers/limitador_controller_limits_test.go b/controllers/limitador_controller_limits_test.go new file mode 100644 index 00000000..68bcb615 --- /dev/null +++ b/controllers/limitador_controller_limits_test.go @@ -0,0 +1,157 @@ +package controllers + +import ( + "context" + "reflect" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/yaml" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/limitador-operator/pkg/limitador" +) + +var _ = Describe("Limitador controller manages limits", func() { + + var testNamespace string + + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) + + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Creating a new Limitador object with specific limits", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + limits := []limitadorv1alpha1.RateLimit{ + { + Conditions: []string{"req.method == 'GET'"}, + MaxValue: 10, + Namespace: "test-namespace", + Seconds: 60, + Variables: []string{"user_id"}, + Name: "useless", + }, + { + Conditions: []string{"req.method == 'POST'"}, + MaxValue: 5, + Namespace: "test-namespace", + Seconds: 60, + Variables: []string{"user_id"}, + }, + } + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + limitadorObj.Spec.Limits = limits + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should create configmap with the custom limits", func() { + cm := &v1.ConfigMap{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.LimitsConfigMapName(limitadorObj), + }, cm) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + var cmLimits []limitadorv1alpha1.RateLimit + err := yaml.Unmarshal([]byte(cm.Data[limitador.LimitadorConfigFileName]), &cmLimits) + Expect(err).To(BeNil()) + Expect(cmLimits).To(Equal(limits)) + }) + }) + + Context("Updating limitador object with new limits", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + limits := []limitadorv1alpha1.RateLimit{ + { + Conditions: []string{"req.method == 'GET'"}, + MaxValue: 10, + Namespace: "test-namespace", + Seconds: 60, + Variables: []string{"user_id"}, + Name: "useless", + }, + { + Conditions: []string{"req.method == 'POST'"}, + MaxValue: 5, + Namespace: "test-namespace", + Seconds: 60, + Variables: []string{"user_id"}, + }, + } + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should modify configmap with the new limits", func() { + cm := &v1.ConfigMap{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.LimitsConfigMapName(limitadorObj), + }, cm) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + var cmLimits []limitadorv1alpha1.RateLimit + err := yaml.Unmarshal([]byte(cm.Data[limitador.LimitadorConfigFileName]), &cmLimits) + Expect(err).To(BeNil()) + Expect(cmLimits).To(BeEmpty()) + + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitadorObj.Name, + }, &updatedLimitador) + + if err != nil { + return false + } + + updatedLimitador.Spec.Limits = limits + + return k8sClient.Update(context.TODO(), &updatedLimitador) == nil + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newCM := &v1.ConfigMap{} + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.LimitsConfigMapName(limitadorObj), + }, newCM) + + if err != nil { + return false + } + + var cmLimits []limitadorv1alpha1.RateLimit + err = yaml.Unmarshal([]byte(newCM.Data[limitador.LimitadorConfigFileName]), &cmLimits) + if err != nil { + return false + } + + return reflect.DeepEqual(cmLimits, limits) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/controllers/limitador_controller_pdb_test.go b/controllers/limitador_controller_pdb_test.go new file mode 100644 index 00000000..f73f2820 --- /dev/null +++ b/controllers/limitador_controller_pdb_test.go @@ -0,0 +1,112 @@ +package controllers + +import ( + "context" + "reflect" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/limitador-operator/pkg/limitador" +) + +var _ = Describe("Limitador controller manages PodDisruptionBudget", func() { + + var testNamespace string + + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) + + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Creating a new Limitador object with specific pdb", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + maxUnavailable := &intstr.IntOrString{Type: 0, IntVal: 3} + pdbType := &limitadorv1alpha1.PodDisruptionBudgetType{MaxUnavailable: maxUnavailable} + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + limitadorObj.Spec.PodDisruptionBudget = pdbType + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should create PodDisruptionBudget", func() { + pdb := &policyv1.PodDisruptionBudget{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.PodDisruptionBudgetName(limitadorObj), + }, pdb) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(pdb.Spec.MaxUnavailable).To(Equal(maxUnavailable)) + }) + }) + + Context("Updating limitador object with new pdb", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + maxUnavailable := &intstr.IntOrString{Type: 0, IntVal: 3} + pdbType := &limitadorv1alpha1.PodDisruptionBudgetType{MaxUnavailable: maxUnavailable} + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should modify pdb object with the new limits", func() { + pdb := &policyv1.PodDisruptionBudget{} + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.PodDisruptionBudgetName(limitadorObj), + }, pdb) + // returns false when err is nil + Expect(errors.IsNotFound(err)).To(BeTrue()) + + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitadorObj.Name, + }, &updatedLimitador) + + if err != nil { + return false + } + + updatedLimitador.Spec.PodDisruptionBudget = pdbType + + return k8sClient.Update(context.TODO(), &updatedLimitador) == nil + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newPDB := &policyv1.PodDisruptionBudget{} + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.PodDisruptionBudgetName(limitadorObj), + }, newPDB) + + if err != nil { + return false + } + + return reflect.DeepEqual(newPDB.Spec.MaxUnavailable, maxUnavailable) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/controllers/limitador_controller_ports_test.go b/controllers/limitador_controller_ports_test.go new file mode 100644 index 00000000..b467a4e7 --- /dev/null +++ b/controllers/limitador_controller_ports_test.go @@ -0,0 +1,323 @@ +package controllers + +import ( + "context" + "reflect" + "strconv" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "golang.org/x/exp/slices" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/limitador-operator/pkg/limitador" +) + +var _ = Describe("Limitador controller manages ports", func() { + + var testNamespace string + + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) + + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Creating a new Limitador object with specific ports", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + var httpPortNumber int32 = 8000 + var grpcPortNumber int32 = 8001 + + httpPort := &limitadorv1alpha1.TransportProtocol{Port: &httpPortNumber} + grpcPort := &limitadorv1alpha1.TransportProtocol{Port: &grpcPortNumber} + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + limitadorObj.Spec.Listener = &limitadorv1alpha1.Listener{ + HTTP: httpPort, GRPC: grpcPort, + } + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should configure k8s resources with the custom ports", func() { + // Deployment ports + // Deployment command line + // Deployment probes + // Limitador CR status + // Service + + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, + &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Containers[0].Ports).To(ContainElements( + v1.ContainerPort{ + Name: "http", ContainerPort: httpPortNumber, Protocol: v1.ProtocolTCP, + }, + v1.ContainerPort{ + Name: "grpc", ContainerPort: grpcPortNumber, Protocol: v1.ProtocolTCP, + }, + )) + + Expect(deployment.Spec.Template.Spec.Containers[0].Command).To( + HaveExactElements( + "limitador-server", + "--http-port", + strconv.Itoa(int(httpPortNumber)), + "--rls-port", + strconv.Itoa(int(grpcPortNumber)), + "/home/limitador/etc/limitador-config.yaml", + "memory", + ), + ) + + Expect(deployment.Spec.Template.Spec.Containers[0].LivenessProbe).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].LivenessProbe. + ProbeHandler.HTTPGet).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].LivenessProbe. + ProbeHandler.HTTPGet.Port).To(Equal(intstr.FromInt(int(httpPortNumber)))) + + Expect(deployment.Spec.Template.Spec.Containers[0].ReadinessProbe).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].ReadinessProbe. + ProbeHandler.HTTPGet).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].ReadinessProbe. + ProbeHandler.HTTPGet.Port).To(Equal(intstr.FromInt(int(httpPortNumber)))) + + limitadorCR := &limitadorv1alpha1.Limitador{} + err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(limitadorObj), limitadorCR) + Expect(err).NotTo(HaveOccurred()) + Expect(limitadorCR.Status.Service).NotTo(BeNil()) + Expect(limitadorCR.Status.Service.Ports).To(Equal( + limitadorv1alpha1.Ports{GRPC: grpcPortNumber, HTTP: httpPortNumber}, + )) + + service := &v1.Service{} + err = k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.ServiceName(limitadorObj), + }, service) + Expect(err).NotTo(HaveOccurred()) + Expect(service.Spec.Ports).To(ContainElements( + v1.ServicePort{ + Name: "http", Port: httpPortNumber, Protocol: v1.ProtocolTCP, + TargetPort: intstr.FromString("http"), + }, + v1.ServicePort{ + Name: "grpc", Port: grpcPortNumber, Protocol: v1.ProtocolTCP, + TargetPort: intstr.FromString("grpc"), + }, + )) + }) + }) + + Context("Updating limitador object with new custom ports", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + var httpPortNumber int32 = 8000 + var grpcPortNumber int32 = 8001 + + httpPort := &limitadorv1alpha1.TransportProtocol{Port: &httpPortNumber} + grpcPort := &limitadorv1alpha1.TransportProtocol{Port: &grpcPortNumber} + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should modify the k8s resources with the custom ports", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Containers[0].Ports).To(ContainElements( + v1.ContainerPort{ + Name: "http", ContainerPort: limitadorv1alpha1.DefaultServiceHTTPPort, Protocol: v1.ProtocolTCP, + }, + v1.ContainerPort{ + Name: "grpc", ContainerPort: limitadorv1alpha1.DefaultServiceGRPCPort, Protocol: v1.ProtocolTCP, + }, + )) + + Expect(deployment.Spec.Template.Spec.Containers[0].Command).To( + HaveExactElements( + "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), + "/home/limitador/etc/limitador-config.yaml", + "memory", + ), + ) + + Expect(deployment.Spec.Template.Spec.Containers[0].LivenessProbe).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].LivenessProbe. + ProbeHandler.HTTPGet).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].LivenessProbe. + ProbeHandler.HTTPGet.Port).To(Equal( + intstr.FromInt(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + )) + + Expect(deployment.Spec.Template.Spec.Containers[0].ReadinessProbe).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].ReadinessProbe. + ProbeHandler.HTTPGet).NotTo(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers[0].ReadinessProbe. + ProbeHandler.HTTPGet.Port).To(Equal( + intstr.FromInt(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + )) + + limitadorCR := &limitadorv1alpha1.Limitador{} + err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(limitadorObj), limitadorCR) + Expect(err).NotTo(HaveOccurred()) + Expect(limitadorCR.Status.Service).NotTo(BeNil()) + Expect(limitadorCR.Status.Service.Ports).To(Equal( + limitadorv1alpha1.Ports{ + GRPC: limitadorv1alpha1.DefaultServiceGRPCPort, + HTTP: limitadorv1alpha1.DefaultServiceHTTPPort, + }, + )) + + service := &v1.Service{} + err = k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.ServiceName(limitadorObj), + }, service) + Expect(err).NotTo(HaveOccurred()) + Expect(deployment.Spec.Template.Spec.Containers[0].Ports).To(ContainElements( + v1.ContainerPort{ + Name: "http", ContainerPort: httpPortNumber, Protocol: v1.ProtocolTCP, + }, + v1.ContainerPort{ + Name: "grpc", ContainerPort: grpcPortNumber, Protocol: v1.ProtocolTCP, + }, + )) + + // Let's update limitador CR + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, Name: limitadorObj.Name, + }, &updatedLimitador) + + if err != nil { + return false + } + + updatedLimitador.Spec.Listener = &limitadorv1alpha1.Listener{ + HTTP: httpPort, GRPC: grpcPort, + } + + return k8sClient.Update(context.TODO(), &updatedLimitador) == nil + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newDeployment := appsv1.Deployment{} + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &newDeployment) + + if err != nil { + return false + } + + httpPortsMatch := slices.Index(newDeployment.Spec.Template.Spec.Containers[0].Ports, + v1.ContainerPort{ + Name: "http", ContainerPort: httpPortNumber, Protocol: v1.ProtocolTCP, + }) != -1 + + grpcPortsMatch := slices.Index(newDeployment.Spec.Template.Spec.Containers[0].Ports, + v1.ContainerPort{ + Name: "grpc", ContainerPort: grpcPortNumber, Protocol: v1.ProtocolTCP, + }) != -1 + commandMatch := reflect.DeepEqual(newDeployment.Spec.Template.Spec.Containers[0].Command, + []string{ + "limitador-server", + "--http-port", + strconv.Itoa(int(httpPortNumber)), + "--rls-port", + strconv.Itoa(int(grpcPortNumber)), + "/home/limitador/etc/limitador-config.yaml", + "memory", + }) + livenessProbeMatch := deployment.Spec.Template.Spec.Containers[0].LivenessProbe. + ProbeHandler.HTTPGet.Port == intstr.FromInt(int(httpPortNumber)) + readinessProbeMatch := deployment.Spec.Template.Spec.Containers[0].ReadinessProbe. + ProbeHandler.HTTPGet.Port == intstr.FromInt(int(httpPortNumber)) + + return !slices.Contains( + []bool{ + httpPortsMatch, grpcPortsMatch, commandMatch, + livenessProbeMatch, readinessProbeMatch, + }, false) + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newLimitador := &limitadorv1alpha1.Limitador{} + err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(limitadorObj), newLimitador) + if err != nil { + return false + } + + if newLimitador.Status.Service == nil { + return false + } + return reflect.DeepEqual(newLimitador.Status.Service.Ports, + limitadorv1alpha1.Ports{GRPC: grpcPortNumber, HTTP: httpPortNumber}, + ) + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newService := &v1.Service{} + err = k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.ServiceName(limitadorObj), + }, newService) + + if err != nil { + return false + } + + httpPortsMatch := slices.Index(newService.Spec.Ports, + v1.ServicePort{ + Name: "http", Port: httpPortNumber, Protocol: v1.ProtocolTCP, + TargetPort: intstr.FromString("http"), + }) != -1 + + grpcPortsMatch := slices.Index(newService.Spec.Ports, + v1.ServicePort{ + Name: "grpc", Port: grpcPortNumber, Protocol: v1.ProtocolTCP, + TargetPort: intstr.FromString("grpc"), + }) != -1 + + return !slices.Contains([]bool{httpPortsMatch, grpcPortsMatch}, false) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/controllers/limitador_controller_replicas_test.go b/controllers/limitador_controller_replicas_test.go new file mode 100644 index 00000000..c0e01c37 --- /dev/null +++ b/controllers/limitador_controller_replicas_test.go @@ -0,0 +1,112 @@ +package controllers + +import ( + "context" + "reflect" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/types" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/limitador-operator/pkg/limitador" +) + +var _ = Describe("Limitador controller manages replicas", func() { + + var testNamespace string + + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) + + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Creating a new Limitador object with specific replicas", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + var replicas int32 = 2 + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + limitadorObj.Spec.Replicas = &[]int{int(replicas)}[0] + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should create a new deployment with the custom replicas", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(*deployment.Spec.Replicas).To(Equal(replicas)) + }) + }) + + Context("Updating limitador object with new replicas", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + var replicas int32 = 2 + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should modify deployment replicas", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(*deployment.Spec.Replicas).To(Equal(int32(1))) + + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitadorObj.Name, + }, &updatedLimitador) + + if err != nil { + return false + } + + updatedLimitador.Spec.Replicas = &[]int{int(replicas)}[0] + + return k8sClient.Update(context.TODO(), &updatedLimitador) == nil + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newDeployment := &appsv1.Deployment{} + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, newDeployment) + + if err != nil { + return false + } + + return reflect.DeepEqual(*newDeployment.Spec.Replicas, replicas) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/controllers/limitador_controller_resources_test.go b/controllers/limitador_controller_resources_test.go new file mode 100644 index 00000000..8111de8a --- /dev/null +++ b/controllers/limitador_controller_resources_test.go @@ -0,0 +1,138 @@ +package controllers + +import ( + "context" + "reflect" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/types" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/limitador-operator/pkg/limitador" +) + +var _ = Describe("Limitador controller manages resource requirements", func() { + + var testNamespace string + + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) + + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Creating a new Limitador object with specific resource requirements", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + // empty resources, means no resource requirements, + // which is different from the default resource requirements + resourceRequirements := v1.ResourceRequirements{} + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + limitadorObj.Spec.ResourceRequirements = &resourceRequirements + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should create a new deployment with the custom resource requirements", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Containers[0].Resources).To( + Equal(resourceRequirements)) + }) + }) + + Context("Updating limitador object with new resource requirements", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + // empty resources, means no resource requirements, + // which is different from the default resource requirements + resourceRequirements := v1.ResourceRequirements{} + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should modify deployment resource requirements", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + expectedDefaultResourceRequirements := v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("250m"), + v1.ResourceMemory: resource.MustParse("32Mi"), + }, + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("500m"), + v1.ResourceMemory: resource.MustParse("64Mi"), + }, + } + + Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Containers[0].Resources).To(Equal( + expectedDefaultResourceRequirements, + )) + + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitadorObj.Name, + }, &updatedLimitador) + + if err != nil { + return false + } + + updatedLimitador.Spec.ResourceRequirements = &resourceRequirements + + return k8sClient.Update(context.TODO(), &updatedLimitador) == nil + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newDeployment := &appsv1.Deployment{} + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, newDeployment) + + if err != nil { + return false + } + + if len(newDeployment.Spec.Template.Spec.Containers) < 1 { + return false + } + + return reflect.DeepEqual(newDeployment.Spec.Template.Spec.Containers[0].Resources, resourceRequirements) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/controllers/limitador_controller_test.go b/controllers/limitador_controller_test.go index d50662c1..17c1f66c 100644 --- a/controllers/limitador_controller_test.go +++ b/controllers/limitador_controller_test.go @@ -4,12 +4,14 @@ import ( "context" "fmt" "reflect" + "strconv" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -30,66 +32,11 @@ const ( interval = time.Millisecond * 250 ) +var ( + ExpectedDefaultImage = fmt.Sprintf("%s:%s", limitador.LimitadorRepository, "latest") +) + var _ = Describe("Limitador controller", func() { - //const ( - // LimitadorImage = "quay.io/kuadrant/limitador" - // LimitadorVersion = "latest" - // LimitadorHTTPPort = 8000 - // LimitadorGRPCPort = 8001 - // LimitadorMaxUnavailable = 1 - // LimitdaorUpdatedMaxUnavailable = 3 - //) - - //httpPortNumber := int32(LimitadorHTTPPort) - //grpcPortNumber := int32(LimitadorGRPCPort) - - //maxUnavailable := &intstr.IntOrString{ - // Type: 0, - // IntVal: LimitadorMaxUnavailable, - //} - //updatedMaxUnavailable := &intstr.IntOrString{ - // Type: 0, - // IntVal: LimitdaorUpdatedMaxUnavailable, - //} - - //version := LimitadorVersion - //httpPort := &limitadorv1alpha1.TransportProtocol{Port: &httpPortNumber} - //grpcPort := &limitadorv1alpha1.TransportProtocol{Port: &grpcPortNumber} - //affinity := &v1.Affinity{ - // PodAntiAffinity: &v1.PodAntiAffinity{ - // PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{ - // { - // Weight: 100, - // PodAffinityTerm: v1.PodAffinityTerm{ - // LabelSelector: &metav1.LabelSelector{ - // MatchLabels: map[string]string{ - // "pod": "label", - // }, - // }, - // TopologyKey: "kubernetes.io/hostname", - // }, - // }, - // }, - // }, - //} - - //limits := []limitadorv1alpha1.RateLimit{ - // { - // Conditions: []string{"req.method == 'GET'"}, - // MaxValue: 10, - // Namespace: "test-namespace", - // Seconds: 60, - // Variables: []string{"user_id"}, - // Name: "useless", - // }, - // { - // Conditions: []string{"req.method == 'POST'"}, - // MaxValue: 5, - // Namespace: "test-namespace", - // Seconds: 60, - // Variables: []string{"user_id"}, - // }, - //} var testNamespace string @@ -128,8 +75,7 @@ var _ = Describe("Limitador controller", func() { }) It("Should create a new deployment with default settings", func() { - expectedDefaultReplicas := 1 - expectedDefaultImage := fmt.Sprintf("%s:%s", limitador.LimitadorRepository, "latest") + var expectedDefaultReplicas int32 = 1 expectedDefaultResourceRequirements := v1.ResourceRequirements{ Requests: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("250m"), @@ -157,12 +103,7 @@ var _ = Describe("Limitador controller", func() { Expect(*createdLimitadorDeployment.Spec.Replicas).Should(Equal(expectedDefaultReplicas)) Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers).To(HaveLen(1)) Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Image).Should( - Equal(expectedDefaultImage), - ) - // It should contain at least the limits file - Expect(len(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Command) > 1).Should(BeTrue()) - Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Command[1]).Should( - Equal("/home/limitador/etc/limitador-config.yaml"), + Equal(ExpectedDefaultImage), ) Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath).Should( Equal("/home/limitador/etc"), @@ -172,12 +113,14 @@ var _ = Describe("Limitador controller", func() { ) Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Command).Should( // asserts no additional command line arg is added - Equal( - []string{ - "limitador-server", - "/home/limitador/etc/limitador-config.yaml", - "memory", - }, + HaveExactElements( + "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), + "/home/limitador/etc/limitador-config.yaml", + "memory", ), ) Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Resources).To( @@ -227,6 +170,17 @@ var _ = Describe("Limitador controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(cmLimits).To(BeEmpty()) }) + + It("Should have not created PodDisruptionBudget", func() { + pdb := &policyv1.PodDisruptionBudget{} + err := k8sClient.Get(context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.PodDisruptionBudgetName(limitadorObj), + }, pdb) + // returns false when err is nil + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) }) Context("Creating a new Limitador object with rate limit headers", func() { @@ -254,15 +208,17 @@ var _ = Describe("Limitador controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) - Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Command).Should( - Equal( - []string{ - "limitador-server", - "--rate-limit-headers", - "DRAFT_VERSION_03", - "/home/limitador/etc/limitador-config.yaml", - "memory", - }, + Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Command).To( + HaveExactElements( + "limitador-server", + "--rate-limit-headers", + "DRAFT_VERSION_03", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), + "/home/limitador/etc/limitador-config.yaml", + "memory", ), ) }) @@ -318,6 +274,10 @@ var _ = Describe("Limitador controller", func() { "limitador-server", "--rate-limit-headers", "DRAFT_VERSION_03", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), "/home/limitador/etc/limitador-config.yaml", "memory", }) @@ -375,6 +335,10 @@ var _ = Describe("Limitador controller", func() { []string{ "limitador-server", "--limit-name-in-labels", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), "/home/limitador/etc/limitador-config.yaml", "memory", }) @@ -406,7 +370,7 @@ var _ = Describe("Limitador controller", func() { }, timeout, interval).Should(BeTrue()) Expect(deploymentObj.Spec.Template.Spec.Containers).To(HaveLen(1)) - containerObj := v1.Container{Name: "newContainer", Image: "someImage"} + containerObj := v1.Container{Name: "newcontainer", Image: "someImage"} deploymentObj.Spec.Template.Spec.Containers = append(deploymentObj.Spec.Template.Spec.Containers, containerObj) @@ -440,16 +404,20 @@ var _ = Describe("Limitador controller", func() { }) Context("Deploying limitador object with redis storage", func() { - redisSecret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, - ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: testNamespace}, - StringData: map[string]string{"URL": "redis://example.com:6379"}, - Type: v1.SecretTypeOpaque, - } + var redisSecret *v1.Secret BeforeEach(func() { deployRedis(testNamespace) + redisSecret = &v1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: testNamespace}, + StringData: map[string]string{ + "URL": fmt.Sprintf("redis://%s.%s.svc.cluster.local:6379", redisService(testNamespace).Name, testNamespace), + }, + Type: v1.SecretTypeOpaque, + } + err := k8sClient.Create(context.Background(), redisSecret) Expect(err).ToNot(HaveOccurred()) @@ -490,29 +458,35 @@ var _ = Describe("Limitador controller", func() { Expect(deploymentObj.Spec.Template.Spec.Containers).To(HaveLen(1)) Expect(deploymentObj.Spec.Template.Spec.Containers[0].Command).To( - Equal( - []string{ - "limitador-server", - "/home/limitador/etc/limitador-config.yaml", - "redis", - "$(LIMITADOR_OPERATOR_REDIS_URL)", - }, + HaveExactElements( + "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), + "/home/limitador/etc/limitador-config.yaml", + "redis", + "$(LIMITADOR_OPERATOR_REDIS_URL)", ), ) }) }) Context("Deploying limitador object with redis cached storage", func() { - redisSecret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, - ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: testNamespace}, - StringData: map[string]string{"URL": "redis://example.com:6379"}, - Type: v1.SecretTypeOpaque, - } + var redisSecret *v1.Secret BeforeEach(func() { deployRedis(testNamespace) + redisSecret = &v1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: testNamespace}, + StringData: map[string]string{ + "URL": fmt.Sprintf("redis://%s.%s.svc.cluster.local:6379", redisService(testNamespace).Name, testNamespace), + }, + Type: v1.SecretTypeOpaque, + } + err := k8sClient.Create(context.Background(), redisSecret) Expect(err).ToNot(HaveOccurred()) @@ -553,17 +527,19 @@ var _ = Describe("Limitador controller", func() { Expect(deploymentObj.Spec.Template.Spec.Containers).To(HaveLen(1)) Expect(deploymentObj.Spec.Template.Spec.Containers[0].Command).To( - Equal( - []string{ - "limitador-server", - "/home/limitador/etc/limitador-config.yaml", - "redis_cached", - "$(LIMITADOR_OPERATOR_REDIS_URL)", - "--ttl", "1", - "--ratio", "2", - "--flush-period", "3", - "--max-cached", "4", - }, + HaveExactElements( + "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), + "/home/limitador/etc/limitador-config.yaml", + "redis_cached", + "$(LIMITADOR_OPERATOR_REDIS_URL)", + "--ttl", "1", + "--ratio", "2", + "--flush-period", "3", + "--max-cached", "4", ), ) }) @@ -604,13 +580,15 @@ var _ = Describe("Limitador controller", func() { Expect(deploymentObj.Spec.Template.Spec.Containers).To(HaveLen(1)) Expect(deploymentObj.Spec.Template.Spec.Containers[0].Command).To( - Equal( - []string{ - "limitador-server", - "/home/limitador/etc/limitador-config.yaml", - "disk", - limitador.DiskPath, - }, + HaveExactElements( + "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), + "/home/limitador/etc/limitador-config.yaml", + "disk", + limitador.DiskPath, ), ) Expect(deploymentObj.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(2)) diff --git a/controllers/limitador_controller_version_test.go b/controllers/limitador_controller_version_test.go new file mode 100644 index 00000000..8f706b7f --- /dev/null +++ b/controllers/limitador_controller_version_test.go @@ -0,0 +1,114 @@ +package controllers + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/types" + + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + "github.com/kuadrant/limitador-operator/pkg/limitador" +) + +var _ = Describe("Limitador controller manages image version", func() { + + var testNamespace string + + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) + + AfterEach(DeleteNamespaceCallback(&testNamespace)) + + Context("Creating a new Limitador object with specific image version", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + limitadorObj.Spec.Version = &[]string{"otherversion"}[0] + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + // Do not expect to have limitador ready + }) + + It("Should create a new deployment with the custom image", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, + &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + expectedImage := fmt.Sprintf("%s:%s", limitador.LimitadorRepository, "otherversion") + Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal(expectedImage)) + }) + }) + + Context("Updating limitador object with a new image version", func() { + var limitadorObj *limitadorv1alpha1.Limitador + + BeforeEach(func() { + limitadorObj = basicLimitador(testNamespace) + + Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) + }) + + It("Should modify the deployment with the custom image", func() { + deployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &deployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(deployment.Spec.Template.Spec.Containers[0].Image).Should( + Equal(ExpectedDefaultImage), + ) + + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitadorObj.Name, + }, &updatedLimitador) + + if err != nil { + return false + } + + updatedLimitador.Spec.Version = &[]string{"otherversion"}[0] + + // the new deployment very likely will not be available (image does not exist) + return k8sClient.Update(context.TODO(), &updatedLimitador) == nil + }, timeout, interval).Should(BeTrue()) + + Eventually(func() bool { + newDeployment := appsv1.Deployment{} + err := k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: testNamespace, + Name: limitador.DeploymentName(limitadorObj), + }, &newDeployment) + + if err != nil { + return false + } + + expectedImage := fmt.Sprintf("%s:%s", limitador.LimitadorRepository, "otherversion") + return expectedImage == newDeployment.Spec.Template.Spec.Containers[0].Image + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/pkg/limitador/deployment_options_test.go b/pkg/limitador/deployment_options_test.go index da91f635..98583aa7 100644 --- a/pkg/limitador/deployment_options_test.go +++ b/pkg/limitador/deployment_options_test.go @@ -1,6 +1,7 @@ package limitador import ( + "strconv" "testing" "gotest.tools/assert" @@ -19,12 +20,16 @@ func TestDeploymentCommand(t *testing.T) { } } - t.Run("when no rate limit headers set in the spec command line args does not include --rate-limit-headers", func(subT *testing.T) { + t.Run("when default spec", func(subT *testing.T) { limObj := basicLimitador() command := DeploymentCommand(limObj, DeploymentStorageOptions{Command: []string{"memory"}}) assert.DeepEqual(subT, command, []string{ "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), "/home/limitador/etc/limitador-config.yaml", "memory", }) @@ -40,6 +45,10 @@ func TestDeploymentCommand(t *testing.T) { "limitador-server", "--rate-limit-headers", "DRAFT_VERSION_03", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), "/home/limitador/etc/limitador-config.yaml", "memory", }) @@ -51,6 +60,10 @@ func TestDeploymentCommand(t *testing.T) { assert.DeepEqual(subT, command, []string{ "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), "/home/limitador/etc/limitador-config.yaml", }) }) @@ -63,6 +76,10 @@ func TestDeploymentCommand(t *testing.T) { assert.DeepEqual(subT, command, []string{ "limitador-server", + "--http-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceHTTPPort)), + "--rls-port", + strconv.Itoa(int(limitadorv1alpha1.DefaultServiceGRPCPort)), "/home/limitador/etc/limitador-config.yaml", "a", "b", "c", })