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/bundle/manifests/limitador-operator.clusterserviceversion.yaml b/bundle/manifests/limitador-operator.clusterserviceversion.yaml index 382967ac..779a7150 100644 --- a/bundle/manifests/limitador-operator.clusterserviceversion.yaml +++ b/bundle/manifests/limitador-operator.clusterserviceversion.yaml @@ -37,7 +37,7 @@ metadata: capabilities: Basic Install categories: Integration & Delivery containerImage: quay.io/kuadrant/limitador-operator:latest - createdAt: "2023-11-15T10:55:48Z" + createdAt: "2023-11-21T15:45:03Z" operators.operatorframework.io/builder: operator-sdk-v1.28.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/Kuadrant/limitador-operator diff --git a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml index 609382c4..56f88a13 100644 --- a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml +++ b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml @@ -1091,7 +1091,7 @@ spec: properties: conditions: description: 'Represents the observations of a foo''s current state. - Known .status.conditions.type are: "Available"' + Known .status.conditions.type are: "Ready"' items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct diff --git a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml index 35c7fb08..edd3b573 100644 --- a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml +++ b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml @@ -1092,7 +1092,7 @@ spec: properties: conditions: description: 'Represents the observations of a foo''s current state. - Known .status.conditions.type are: "Available"' + Known .status.conditions.type are: "Ready"' items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct diff --git a/controllers/limitador_controller.go b/controllers/limitador_controller.go index c94d0aac..5e40ad86 100644 --- a/controllers/limitador_controller.go +++ b/controllers/limitador_controller.go @@ -200,6 +200,9 @@ func (r *LimitadorReconciler) reconcileDeployment(ctx context.Context, limitador reconcilers.DeploymentVolumesMutator, reconcilers.DeploymentVolumeMountsMutator, reconcilers.DeploymentEnvMutator, + reconcilers.DeploymentPortsMutator, + reconcilers.DeploymentLivenessProbeMutator, + reconcilers.DeploymentReadinessProbeMutator, ) deployment := limitador.Deployment(limitadorObj, deploymentOptions) @@ -229,7 +232,9 @@ func (r *LimitadorReconciler) reconcileService(ctx context.Context, limitadorObj return err } - err = r.ReconcileService(ctx, limitadorService, reconcilers.CreateOnlyMutator) + serviceMutator := reconcilers.ServiceMutator(reconcilers.ServicePortsMutator) + + err = r.ReconcileService(ctx, limitadorService, serviceMutator) logger.V(1).Info("reconcile service", "error", err) if err != nil { return err 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..b1403384 --- /dev/null +++ b/controllers/limitador_controller_ports_test.go @@ -0,0 +1,325 @@ +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 + + httpPortNumber := limitadorv1alpha1.DefaultServiceHTTPPort + 100 + grpcPortNumber := limitadorv1alpha1.DefaultServiceGRPCPort + 100 + + 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 + + httpPortNumber := limitadorv1alpha1.DefaultServiceHTTPPort + 100 + grpcPortNumber := limitadorv1alpha1.DefaultServiceGRPCPort + 100 + + 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(service.Spec.Ports).To(ContainElements( + v1.ServicePort{ + Name: "http", Port: limitadorv1alpha1.DefaultServiceHTTPPort, + Protocol: v1.ProtocolTCP, TargetPort: intstr.FromString("http"), + }, + v1.ServicePort{ + Name: "grpc", Port: limitadorv1alpha1.DefaultServiceGRPCPort, + Protocol: v1.ProtocolTCP, TargetPort: intstr.FromString("grpc"), + }, + )) + + // 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 := reflect.DeepEqual(newDeployment.Spec.Template.Spec.Containers[0].LivenessProbe. + ProbeHandler.HTTPGet.Port, intstr.FromInt(int(httpPortNumber))) + readinessProbeMatch := reflect.DeepEqual(newDeployment.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 7ec0a6c3..6de4ed56 100644 --- a/controllers/limitador_controller_test.go +++ b/controllers/limitador_controller_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "strconv" "time" . "github.com/onsi/ginkgo/v2" @@ -11,7 +12,8 @@ import ( 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" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -25,118 +27,31 @@ import ( ) const ( - LimitadorNamespace = "default" - timeout = time.Second * 10 - interval = time.Millisecond * 250 + timeout = time.Second * 10 + interval = time.Millisecond * 250 ) -var _ = Describe("Limitador controller", func() { - const ( - LimitadorReplicas = 2 - LimitadorImage = "quay.io/kuadrant/limitador" - LimitadorVersion = "0.3.0" - 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, - } - - replicas := LimitadorReplicas - 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", - }, - }, - }, - }, - } +var ( + ExpectedDefaultImage = fmt.Sprintf("%s:%s", limitador.LimitadorRepository, "latest") +) - 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 _ = Describe("Limitador controller", func() { - newLimitador := func() *limitadorv1alpha1.Limitador { - // The name can't start with a number. - name := "a" + string(uuid.NewUUID()) + var testNamespace string - return &limitadorv1alpha1.Limitador{ - TypeMeta: metav1.TypeMeta{ - Kind: "Limitador", - APIVersion: "limitador.kuadrant.io/v1alpha1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: LimitadorNamespace, - }, - Spec: limitadorv1alpha1.LimitadorSpec{ - Replicas: &replicas, - Version: &version, - Affinity: affinity, - Listener: &limitadorv1alpha1.Listener{ - HTTP: httpPort, - GRPC: grpcPort, - }, - Limits: limits, - PodDisruptionBudget: &limitadorv1alpha1.PodDisruptionBudgetType{ - MaxUnavailable: maxUnavailable, - }, - }, - } - } + BeforeEach(func() { + CreateNamespace(&testNamespace) + }) - deletePropagationPolicy := client.PropagationPolicy(metav1.DeletePropagationForeground) + AfterEach(DeleteNamespaceCallback(&testNamespace)) - Context("Creating a new empty Limitador object", func() { + Context("Creating a new basic limitador CR", func() { var limitadorObj *limitadorv1alpha1.Limitador BeforeEach(func() { - limitadorObj = newLimitador() - limitadorObj.Spec = limitadorv1alpha1.LimitadorSpec{} - + limitadorObj = basicLimitador(testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), limitadorObj, deletePropagationPolicy) - Expect(err == nil || errors.IsNotFound(err)) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) }) It("Should create a Limitador service with default ports", func() { @@ -145,40 +60,38 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.ServiceName(limitadorObj), }, &createdLimitadorService) return err == nil }, timeout, interval).Should(BeTrue()) - Expect(len(createdLimitadorService.Spec.Ports)).Should(Equal(2)) + Expect(createdLimitadorService.Spec.Ports).To(HaveLen(2)) Expect(createdLimitadorService.Spec.Ports[0].Name).Should(Equal("http")) Expect(createdLimitadorService.Spec.Ports[0].Port).Should(Equal(limitadorv1alpha1.DefaultServiceHTTPPort)) Expect(createdLimitadorService.Spec.Ports[1].Name).Should(Equal("grpc")) Expect(createdLimitadorService.Spec.Ports[1].Port).Should(Equal(limitadorv1alpha1.DefaultServiceGRPCPort)) }) - }) - - Context("Creating a new Limitador object", func() { - var limitadorObj *limitadorv1alpha1.Limitador - BeforeEach(func() { - limitadorObj = newLimitador() - Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), limitadorObj, deletePropagationPolicy) - Expect(err == nil || errors.IsNotFound(err)) - }) + It("Should create a new deployment with default settings", func() { + var expectedDefaultReplicas int32 = 1 + 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"), + }, + } - It("Should create a new deployment with the right settings", func() { createdLimitadorDeployment := appsv1.Deployment{} Eventually(func() bool { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &createdLimitadorDeployment) @@ -186,16 +99,10 @@ var _ = Describe("Limitador controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) - Expect(*createdLimitadorDeployment.Spec.Replicas).Should( - Equal((int32)(LimitadorReplicas)), - ) + 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(LimitadorImage + ":" + LimitadorVersion), - ) - // 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"), @@ -204,34 +111,20 @@ var _ = Describe("Limitador controller", func() { Equal(limitador.LimitsConfigMapName(limitadorObj)), ) Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Command).Should( - // asserts request headers command line arg is not there - Equal( - []string{ - "limitador-server", - "/home/limitador/etc/limitador-config.yaml", - "memory", - }, + // asserts no additional command line arg is added + 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).Should( - Equal(*limitadorObj.GetResourceRequirements())) - Expect(createdLimitadorDeployment.Spec.Template.Spec.Affinity).Should( - Equal(affinity), - ) - }) - - It("Should create a Limitador service", func() { - createdLimitadorService := v1.Service{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.ServiceName(limitadorObj), - }, - &createdLimitadorService) - return err == nil - }, timeout, interval).Should(BeTrue()) + Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Resources).To( + Equal(expectedDefaultResourceRequirements)) + Expect(createdLimitadorDeployment.Spec.Template.Spec.Affinity).To(BeNil()) }) It("Should build the correct Status", func() { @@ -249,22 +142,21 @@ var _ = Describe("Limitador controller", func() { } return createdLimitador.Status.Service }, timeout, interval).Should(Equal(&limitadorv1alpha1.LimitadorService{ - Host: "limitador-" + limitadorObj.Name + ".default.svc.cluster.local", + Host: fmt.Sprintf("%s.%s.svc.cluster.local", limitador.ServiceName(limitadorObj), testNamespace), Ports: limitadorv1alpha1.Ports{ - GRPC: grpcPortNumber, - HTTP: httpPortNumber, + GRPC: limitadorv1alpha1.DefaultServiceGRPCPort, + HTTP: limitadorv1alpha1.DefaultServiceHTTPPort, }, })) - }) - It("Should create a ConfigMap with the correct limits", func() { + It("Should create a ConfigMap with empty limits", func() { createdConfigMap := v1.ConfigMap{} Eventually(func() bool { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.LimitsConfigMapName(limitadorObj), }, &createdConfigMap) @@ -274,251 +166,19 @@ var _ = Describe("Limitador controller", func() { var cmLimits []limitadorv1alpha1.RateLimit err := yaml.Unmarshal([]byte(createdConfigMap.Data[limitador.LimitadorConfigFileName]), &cmLimits) - Expect(err == nil) - Expect(cmLimits).To(Equal(limits)) - }) - - It("Should create a PodDisruptionBudget", func() { - createdPdb := policyv1.PodDisruptionBudget{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.PodDisruptionBudgetName(limitadorObj), - }, - &createdPdb) - - return err == nil - }, timeout, interval).Should(BeTrue()) - Expect(createdPdb.Spec.MaxUnavailable).To(Equal(maxUnavailable)) - Expect(createdPdb.Spec.Selector.MatchLabels).To(Equal(limitador.Labels(limitadorObj))) - }) - }) - - Context("Updating a limitador object", func() { - var limitadorObj *limitadorv1alpha1.Limitador - - BeforeEach(func() { - limitadorObj = newLimitador() - Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), limitadorObj, deletePropagationPolicy) - Expect(err == nil || errors.IsNotFound(err)) - }) - - It("Should modify the limitador deployment", func() { - updatedLimitador := limitadorv1alpha1.Limitador{} - replicas = LimitadorReplicas + 1 - version = "latest" - resourceRequirements := &v1.ResourceRequirements{ - Requests: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("200m"), - v1.ResourceMemory: resource.MustParse("30Mi"), - }, - Limits: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("400m"), - v1.ResourceMemory: resource.MustParse("60Mi"), - }, - } - - // Sometimes there can be a conflict due to stale resource if controller is still reconciling resource - // from create event - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitadorObj.Name, - }, - &updatedLimitador) - if err != nil { - return false - } - - updatedLimitador.Spec.Replicas = &replicas - updatedLimitador.Spec.Version = &version - updatedLimitador.Spec.ResourceRequirements = resourceRequirements - affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight = 99 - updatedLimitador.Spec.Affinity = affinity - - return k8sClient.Update(context.TODO(), &updatedLimitador) == nil - }, timeout, interval).Should(BeTrue()) - - updatedLimitadorDeployment := appsv1.Deployment{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.DeploymentName(limitadorObj), - }, - &updatedLimitadorDeployment) - - if err != nil { - return false - } - - correctReplicas := *updatedLimitadorDeployment.Spec.Replicas == LimitadorReplicas+1 - correctImage := updatedLimitadorDeployment.Spec.Template.Spec.Containers[0].Image == LimitadorImage+":latest" - correctResources := reflect.DeepEqual(updatedLimitadorDeployment.Spec.Template.Spec.Containers[0].Resources, *resourceRequirements) - correctAffinity := updatedLimitadorDeployment.Spec.Template.Spec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight == 99 - - return correctReplicas && correctImage && correctResources && correctAffinity - }, timeout, interval).Should(BeTrue()) - }) - - It("Should modify limitador deployments if nil object set", func() { - updatedLimitador := limitadorv1alpha1.Limitador{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitadorObj.Name, - }, - &updatedLimitador) - - if err != nil { - return false - } - updatedLimitador.Spec.Affinity = nil - - return k8sClient.Update(context.TODO(), &updatedLimitador) == nil - - }, timeout, interval).Should(BeTrue()) - - updatedLimitadorDeployment := appsv1.Deployment{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.DeploymentName(limitadorObj), - }, - &updatedLimitadorDeployment) - - if err != nil { - return false - } - - correctAffinity := updatedLimitadorDeployment.Spec.Template.Spec.Affinity == nil - - return correctAffinity - }, timeout, interval).Should(BeTrue()) - }) - - It("Should modify the ConfigMap accordingly", func() { - originalCM := v1.ConfigMap{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.LimitsConfigMapName(limitadorObj), - }, - &originalCM) - - return err == nil - }, timeout, interval).Should(BeTrue()) - - updatedLimitador := limitadorv1alpha1.Limitador{} - newLimits := []limitadorv1alpha1.RateLimit{ - { - Conditions: []string{"req.method == GET"}, - MaxValue: 100, - Namespace: "test-namespace", - Seconds: 60, - Variables: []string{"user_id"}, - }, - } - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitadorObj.Name, - }, - &updatedLimitador) - - updatedLimitador.Spec.Limits = newLimits - - if err != nil { - return false - } - - return k8sClient.Update(context.TODO(), &updatedLimitador) == nil - }, timeout, interval).Should(BeTrue()) - - updatedLimitadorConfigMap := v1.ConfigMap{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.LimitsConfigMapName(limitadorObj), - }, - &updatedLimitadorConfigMap) - // wait until the CM has changed - return err == nil && updatedLimitadorConfigMap.ResourceVersion != originalCM.ResourceVersion - }, timeout, interval).Should(BeTrue()) - - var cmLimits []limitadorv1alpha1.RateLimit - err := yaml.Unmarshal([]byte(updatedLimitadorConfigMap.Data[limitador.LimitadorConfigFileName]), &cmLimits) - Expect(err == nil) - Expect(cmLimits).To(Equal(newLimits)) + Expect(err).ToNot(HaveOccurred()) + Expect(cmLimits).To(BeEmpty()) }) - It("Updates the PodDisruptionBudget accordingly", func() { - originalPdb := policyv1.PodDisruptionBudget{} - - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.PodDisruptionBudgetName(limitadorObj), - }, - &originalPdb) - - return err == nil - }, timeout, interval).Should(BeTrue()) - - updatedLimitador := limitadorv1alpha1.Limitador{} - - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitadorObj.Name, - }, - &updatedLimitador) - - if err != nil { - return false - } - updatedLimitador.Spec.PodDisruptionBudget.MaxUnavailable = updatedMaxUnavailable - - return k8sClient.Update(context.TODO(), &updatedLimitador) == nil - }, timeout, interval).Should(BeTrue()) - - updatedPdb := policyv1.PodDisruptionBudget{} - Eventually(func() bool { - err := k8sClient.Get( - context.TODO(), - types.NamespacedName{ - Namespace: LimitadorNamespace, - Name: limitador.PodDisruptionBudgetName(limitadorObj), - }, - &updatedPdb) - - return err == nil && updatedPdb.ResourceVersion != originalPdb.ResourceVersion - }, timeout, interval).Should(BeTrue()) - - Expect(updatedPdb.Spec.MaxUnavailable).To(Equal(updatedMaxUnavailable)) + 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(apierrors.IsNotFound(err)).To(BeTrue()) }) }) @@ -526,15 +186,11 @@ var _ = Describe("Limitador controller", func() { var limitadorObj *limitadorv1alpha1.Limitador BeforeEach(func() { - limitadorObj = newLimitador() + limitadorObj = basicLimitador(testNamespace) limitadorObj.Spec.RateLimitHeaders = &[]limitadorv1alpha1.RateLimitHeadersType{"DRAFT_VERSION_03"}[0] Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), limitadorObj, deletePropagationPolicy) - Expect(err == nil || errors.IsNotFound(err)) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) }) It("Should create a new deployment with rate limit headers command line arg", func() { @@ -543,7 +199,7 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &createdLimitadorDeployment) @@ -551,17 +207,17 @@ var _ = Describe("Limitador controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) - // It should contain at least the limits file - Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Command).Should( - // asserts request headers command line arg is not there - 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", ), ) }) @@ -571,14 +227,10 @@ var _ = Describe("Limitador controller", func() { var limitadorObj *limitadorv1alpha1.Limitador BeforeEach(func() { - limitadorObj = newLimitador() + limitadorObj = basicLimitador(testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), limitadorObj, deletePropagationPolicy) - Expect(err == nil || errors.IsNotFound(err)) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) }) It("Should modify the limitador deployment command line args", func() { @@ -587,7 +239,7 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitadorObj.Name, }, &updatedLimitador) @@ -596,9 +248,8 @@ var _ = Describe("Limitador controller", func() { return false } - if updatedLimitador.Spec.RateLimitHeaders != nil { - return false - } + Expect(updatedLimitador.Spec.RateLimitHeaders).To(BeNil()) + updatedLimitador.Spec.RateLimitHeaders = &[]limitadorv1alpha1.RateLimitHeadersType{"DRAFT_VERSION_03"}[0] return k8sClient.Update(context.TODO(), &updatedLimitador) == nil }, timeout, interval).Should(BeTrue()) @@ -608,7 +259,7 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &updatedLimitadorDeployment) @@ -622,6 +273,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", }) @@ -633,14 +288,10 @@ var _ = Describe("Limitador controller", func() { var limitadorObj *limitadorv1alpha1.Limitador BeforeEach(func() { - limitadorObj = newLimitador() + limitadorObj = basicLimitador(testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), limitadorObj, deletePropagationPolicy) - Expect(err == nil || errors.IsNotFound(err)) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) }) It("Should modify the limitador deployment command line args", func() { @@ -649,7 +300,7 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitadorObj.Name, }, &updatedLimitador) @@ -658,9 +309,8 @@ var _ = Describe("Limitador controller", func() { return false } - if updatedLimitador.Spec.Telemetry != nil { - return false - } + Expect(updatedLimitador.Spec.Telemetry).To(BeNil()) + telemetry := limitadorv1alpha1.Telemetry("exhaustive") updatedLimitador.Spec.Telemetry = &telemetry return k8sClient.Update(context.TODO(), &updatedLimitador) == nil @@ -671,7 +321,7 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &updatedLimitadorDeployment) @@ -684,6 +334,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", }) @@ -695,13 +349,9 @@ var _ = Describe("Limitador controller", func() { var limitadorObj *limitadorv1alpha1.Limitador BeforeEach(func() { - limitadorObj = newLimitador() + limitadorObj = basicLimitador(testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), limitadorObj, deletePropagationPolicy) - Expect(err == nil || errors.IsNotFound(err)) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) }) It("User tries adding side-cars to deployment CR", func() { @@ -710,7 +360,7 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &deploymentObj) @@ -718,8 +368,8 @@ var _ = Describe("Limitador controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) - Expect(len(deploymentObj.Spec.Template.Spec.Containers)).To(Equal(1)) - containerObj := v1.Container{Name: LimitadorNamespace, Image: LimitadorNamespace} + Expect(deploymentObj.Spec.Template.Spec.Containers).To(HaveLen(1)) + containerObj := v1.Container{Name: "newcontainer", Image: "someImage"} deploymentObj.Spec.Template.Spec.Containers = append(deploymentObj.Spec.Template.Spec.Containers, containerObj) @@ -729,14 +379,13 @@ var _ = Describe("Limitador controller", func() { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &updateDeploymentObj) return err == nil && len(updateDeploymentObj.Spec.Template.Spec.Containers) == 1 }, timeout, interval).Should(BeTrue()) - }) }) @@ -745,35 +394,28 @@ var _ = Describe("Limitador controller", func() { // used to validate custom resources using Common Expression Language (CEL) // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules Context("Disk storage does not allow multiple replicas", func() { - AfterEach(func() { - limitadorObj := limitadorWithInvalidDiskReplicas() - err := k8sClient.Delete(context.TODO(), limitadorObj) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - var l limitadorv1alpha1.Limitador - err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(limitadorObj), &l) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) - }) - It("resource is rejected", func() { - limitadorObj := limitadorWithInvalidDiskReplicas() + limitadorObj := limitadorWithInvalidDiskReplicas(testNamespace) err := k8sClient.Create(context.TODO(), limitadorObj) Expect(err).To(HaveOccurred()) - Expect(errors.IsInvalid(err)).To(BeTrue()) + Expect(apierrors.IsInvalid(err)).To(BeTrue()) }) }) Context("Deploying limitador object with redis storage", func() { - redisSecret := &v1.Secret{ - TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, - ObjectMeta: metav1.ObjectMeta{Name: "redis", Namespace: LimitadorNamespace}, - StringData: map[string]string{"URL": "redis://example.com:6379"}, - Type: v1.SecretTypeOpaque, - } + var redisSecret *v1.Secret BeforeEach(func() { - deployRedis() + 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()) @@ -782,7 +424,7 @@ var _ = Describe("Limitador controller", func() { secret := &v1.Secret{} err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(redisSecret), secret) if err != nil { - if errors.IsNotFound(err) { + if apierrors.IsNotFound(err) { fmt.Fprintln(GinkgoWriter, "==== redis secret not found") } else { fmt.Fprintln(GinkgoWriter, "==== cannot read redis secret", "error", err) @@ -795,36 +437,17 @@ var _ = Describe("Limitador controller", func() { }, timeout, interval).Should(BeTrue()) }) - AfterEach(func() { - unDeployRedis() - limitadorObj := limitadorWithRedisStorage(client.ObjectKeyFromObject(redisSecret)) - err := k8sClient.Delete(context.TODO(), limitadorObj) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - var l limitadorv1alpha1.Limitador - err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(limitadorObj), &l) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) - - err = k8sClient.Delete(context.TODO(), redisSecret) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - var s v1.Secret - err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(redisSecret), &s) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) - }) - It("command line is correct", func() { - limitadorObj := limitadorWithRedisStorage(client.ObjectKeyFromObject(redisSecret)) + limitadorObj := limitadorWithRedisStorage(client.ObjectKeyFromObject(redisSecret), testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) deploymentObj := appsv1.Deployment{} Eventually(func() bool { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &deploymentObj) @@ -832,30 +455,36 @@ var _ = Describe("Limitador controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) - Expect(len(deploymentObj.Spec.Template.Spec.Containers)).To(Equal(1)) + 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: LimitadorNamespace}, - StringData: map[string]string{"URL": "redis://example.com:6379"}, - Type: v1.SecretTypeOpaque, - } + var redisSecret *v1.Secret BeforeEach(func() { - deployRedis() + 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()) @@ -864,7 +493,7 @@ var _ = Describe("Limitador controller", func() { secret := &v1.Secret{} err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(redisSecret), secret) if err != nil { - if errors.IsNotFound(err) { + if apierrors.IsNotFound(err) { fmt.Fprintln(GinkgoWriter, "redis secret not found") } else { fmt.Fprintln(GinkgoWriter, "cannot read redis secret", "error", err) @@ -877,37 +506,17 @@ var _ = Describe("Limitador controller", func() { }, timeout, interval).Should(BeTrue()) }) - AfterEach(func() { - unDeployRedis() - limitadorObj := limitadorWithRedisCachedStorage(client.ObjectKeyFromObject(redisSecret)) - err := k8sClient.Delete(context.TODO(), limitadorObj) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - var l limitadorv1alpha1.Limitador - err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(limitadorObj), &l) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) - - err = k8sClient.Delete(context.TODO(), redisSecret) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - var s v1.Secret - err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(redisSecret), &s) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) - - }) - It("command line is correct", func() { - limitadorObj := limitadorWithRedisCachedStorage(client.ObjectKeyFromObject(redisSecret)) + limitadorObj := limitadorWithRedisCachedStorage(client.ObjectKeyFromObject(redisSecret), testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) deploymentObj := appsv1.Deployment{} Eventually(func() bool { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &deploymentObj) @@ -915,46 +524,38 @@ var _ = Describe("Limitador controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) - Expect(len(deploymentObj.Spec.Template.Spec.Containers)).To(Equal(1)) + 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", ), ) }) }) Context("Deploying limitador object with disk storage", func() { - AfterEach(func() { - limitadorObj := limitadorWithDiskStorage() - err := k8sClient.Delete(context.TODO(), limitadorObj) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - var l limitadorv1alpha1.Limitador - err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(limitadorObj), &l) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) - }) - It("deployment is correct", func() { - limitadorObj := limitadorWithDiskStorage() + limitadorObj := limitadorWithDiskStorage(testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) deploymentObj := appsv1.Deployment{} Eventually(func() bool { err := k8sClient.Get( context.TODO(), types.NamespacedName{ - Namespace: LimitadorNamespace, + Namespace: testNamespace, Name: limitador.DeploymentName(limitadorObj), }, &deploymentObj) @@ -962,7 +563,7 @@ var _ = Describe("Limitador controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) - Expect(len(deploymentObj.Spec.Template.Spec.Volumes)).To(Equal(2)) + Expect(deploymentObj.Spec.Template.Spec.Volumes).To(HaveLen(2)) Expect(deploymentObj.Spec.Template.Spec.Volumes[1]).To( Equal( v1.Volume{ @@ -976,18 +577,20 @@ var _ = Describe("Limitador controller", func() { }, )) - Expect(len(deploymentObj.Spec.Template.Spec.Containers)).To(Equal(1)) + 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(len(deploymentObj.Spec.Template.Spec.Containers[0].VolumeMounts)).To(Equal(2)) + Expect(deploymentObj.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(2)) Expect(deploymentObj.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To( Equal( v1.VolumeMount{ @@ -1000,8 +603,9 @@ var _ = Describe("Limitador controller", func() { }) It("pvc is correct", func() { - limitadorObj := limitadorWithDiskStorage() + limitadorObj := limitadorWithDiskStorage(testNamespace) Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) + Eventually(testLimitadorIsReady(limitadorObj), time.Minute, 5*time.Second).Should(BeTrue()) pvc := &v1.PersistentVolumeClaim{} Eventually(func() bool { @@ -1009,22 +613,36 @@ var _ = Describe("Limitador controller", func() { context.TODO(), types.NamespacedName{ Name: limitador.PVCName(limitadorObj), - Namespace: LimitadorNamespace, + Namespace: testNamespace, }, pvc) return err == nil }, timeout, interval).Should(BeTrue()) - Expect(len(pvc.GetOwnerReferences())).To(Equal(1)) + Expect(pvc.GetOwnerReferences()).To(HaveLen(1)) }) }) }) -func limitadorWithRedisStorage(redisKey client.ObjectKey) *limitadorv1alpha1.Limitador { +func basicLimitador(ns string) *limitadorv1alpha1.Limitador { + // The name can't start with a number. + name := "a" + string(uuid.NewUUID()) + + return &limitadorv1alpha1.Limitador{ + TypeMeta: metav1.TypeMeta{ + Kind: "Limitador", + APIVersion: "limitador.kuadrant.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns}, + Spec: limitadorv1alpha1.LimitadorSpec{}, + } +} + +func limitadorWithRedisStorage(redisKey client.ObjectKey, ns string) *limitadorv1alpha1.Limitador { return &limitadorv1alpha1.Limitador{ TypeMeta: metav1.TypeMeta{Kind: "Limitador", APIVersion: "limitador.kuadrant.io/v1alpha1"}, - ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-redis-storage", Namespace: LimitadorNamespace}, + ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-redis-storage", Namespace: ns}, Spec: limitadorv1alpha1.LimitadorSpec{ Storage: &limitadorv1alpha1.Storage{ Redis: &limitadorv1alpha1.Redis{ @@ -1037,10 +655,10 @@ func limitadorWithRedisStorage(redisKey client.ObjectKey) *limitadorv1alpha1.Lim } } -func limitadorWithRedisCachedStorage(key client.ObjectKey) *limitadorv1alpha1.Limitador { +func limitadorWithRedisCachedStorage(key client.ObjectKey, ns string) *limitadorv1alpha1.Limitador { return &limitadorv1alpha1.Limitador{ TypeMeta: metav1.TypeMeta{Kind: "Limitador", APIVersion: "limitador.kuadrant.io/v1alpha1"}, - ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-redis-cached-storage", Namespace: LimitadorNamespace}, + ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-redis-cached-storage", Namespace: ns}, Spec: limitadorv1alpha1.LimitadorSpec{ Storage: &limitadorv1alpha1.Storage{ RedisCached: &limitadorv1alpha1.RedisCached{ @@ -1059,10 +677,10 @@ func limitadorWithRedisCachedStorage(key client.ObjectKey) *limitadorv1alpha1.Li } } -func limitadorWithDiskStorage() *limitadorv1alpha1.Limitador { +func limitadorWithDiskStorage(ns string) *limitadorv1alpha1.Limitador { return &limitadorv1alpha1.Limitador{ TypeMeta: metav1.TypeMeta{Kind: "Limitador", APIVersion: "limitador.kuadrant.io/v1alpha1"}, - ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-disk-storage", Namespace: LimitadorNamespace}, + ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-disk-storage", Namespace: ns}, Spec: limitadorv1alpha1.LimitadorSpec{ Storage: &limitadorv1alpha1.Storage{ Disk: &limitadorv1alpha1.DiskSpec{}, @@ -1071,10 +689,10 @@ func limitadorWithDiskStorage() *limitadorv1alpha1.Limitador { } } -func limitadorWithInvalidDiskReplicas() *limitadorv1alpha1.Limitador { +func limitadorWithInvalidDiskReplicas(ns string) *limitadorv1alpha1.Limitador { return &limitadorv1alpha1.Limitador{ TypeMeta: metav1.TypeMeta{Kind: "Limitador", APIVersion: "limitador.kuadrant.io/v1alpha1"}, - ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-invalid-disk-replicas", Namespace: LimitadorNamespace}, + ObjectMeta: metav1.ObjectMeta{Name: "limitador-with-invalid-disk-replicas", Namespace: ns}, Spec: limitadorv1alpha1.LimitadorSpec{ Replicas: &[]int{2}[0], Storage: &limitadorv1alpha1.Storage{ @@ -1084,8 +702,8 @@ func limitadorWithInvalidDiskReplicas() *limitadorv1alpha1.Limitador { } } -func deployRedis() { - deployment := redisDeployment() +func deployRedis(ns string) { + deployment := redisDeployment(ns) Expect(k8sClient.Create(context.TODO(), deployment)).Should(Succeed()) Eventually(func() bool { d := &appsv1.Deployment{} @@ -1093,7 +711,7 @@ func deployRedis() { return err == nil }, timeout, interval).Should(BeTrue()) - service := redisService() + service := redisService(ns) Expect(k8sClient.Create(context.TODO(), service)).Should(Succeed()) Eventually(func() bool { s := &v1.Service{} @@ -1102,32 +720,12 @@ func deployRedis() { }, timeout, interval).Should(BeTrue()) } -func unDeployRedis() { - deployment := redisDeployment() - err := k8sClient.Delete(context.TODO(), deployment) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - var d appsv1.Deployment - err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(deployment), &d) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) - - service := redisService() - err = k8sClient.Delete(context.TODO(), service) - Expect(err == nil || errors.IsNotFound(err)) - Eventually(func() bool { - s := &v1.Service{} - err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(service), s) - return errors.IsNotFound(err) - }, timeout, interval).Should(BeTrue()) -} - -func redisDeployment() *appsv1.Deployment { +func redisDeployment(ns string) *appsv1.Deployment { return &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, ObjectMeta: metav1.ObjectMeta{ Name: "redis", - Namespace: LimitadorNamespace, + Namespace: ns, Labels: map[string]string{"app": "redis"}, }, Spec: appsv1.DeploymentSpec{ @@ -1146,12 +744,12 @@ func redisDeployment() *appsv1.Deployment { } } -func redisService() *v1.Service { +func redisService(ns string) *v1.Service { return &v1.Service{ TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "v1"}, ObjectMeta: metav1.ObjectMeta{ Name: "redis", - Namespace: LimitadorNamespace, + Namespace: ns, }, Spec: v1.ServiceSpec{ Selector: map[string]string{"app": "redis"}, @@ -1166,3 +764,49 @@ func redisService() *v1.Service { }, } } + +func testLimitadorIsReady(l *limitadorv1alpha1.Limitador) func() bool { + return func() bool { + existing := &limitadorv1alpha1.Limitador{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(l), existing) + return err == nil && meta.IsStatusConditionTrue(existing.Status.Conditions, "Ready") + } +} + +func CreateNamespace(namespace *string) { + var generatedTestNamespace = "test-namespace-" + string(uuid.NewUUID()) + + nsObject := &v1.Namespace{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, + ObjectMeta: metav1.ObjectMeta{Name: generatedTestNamespace}, + } + + err := k8sClient.Create(context.Background(), nsObject) + Expect(err).ToNot(HaveOccurred()) + + existingNamespace := &v1.Namespace{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), types.NamespacedName{Name: generatedTestNamespace}, existingNamespace) + return err == nil + }, time.Minute, 5*time.Second).Should(BeTrue()) + + *namespace = existingNamespace.Name +} + +func DeleteNamespaceCallback(namespace *string) func() { + return func() { + desiredTestNamespace := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: *namespace}} + err := k8sClient.Delete(context.Background(), desiredTestNamespace, client.PropagationPolicy(metav1.DeletePropagationForeground)) + + Expect(err).ToNot(HaveOccurred()) + + existingNamespace := &v1.Namespace{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), types.NamespacedName{Name: *namespace}, existingNamespace) + if err != nil && apierrors.IsNotFound(err) { + return true + } + return false + }, 3*time.Minute, 2*time.Second).Should(BeTrue()) + } +} 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/controllers/limitador_status.go b/controllers/limitador_status.go index b481a4f3..10ae7acf 100644 --- a/controllers/limitador_status.go +++ b/controllers/limitador_status.go @@ -112,7 +112,7 @@ func (r *LimitadorReconciler) checkLimitadorAvailable(ctx context.Context, limit deployment := &appsv1.Deployment{} dKey := client.ObjectKey{ // Its deployment is built after the same name and namespace Namespace: limitadorObj.Namespace, - Name: limitadorObj.Name, + Name: limitador.DeploymentName(limitadorObj), } err := r.Client().Get(ctx, dKey, deployment) if err != nil && !apierrors.IsNotFound(err) { diff --git a/go.mod b/go.mod index 0c43bdaf..c7cae24b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 go.uber.org/zap v1.25.0 + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e gotest.tools v2.2.0+incompatible k8s.io/api v0.28.3 k8s.io/apimachinery v0.28.3 @@ -53,7 +54,6 @@ require ( github.com/prometheus/procfs v0.10.1 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sys v0.13.0 // indirect diff --git a/pkg/limitador/deployment_options.go b/pkg/limitador/deployment_options.go index 00337180..284a0dc8 100644 --- a/pkg/limitador/deployment_options.go +++ b/pkg/limitador/deployment_options.go @@ -2,6 +2,7 @@ package limitador import ( "path/filepath" + "strconv" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" @@ -42,6 +43,14 @@ func DeploymentCommand(limObj *limitadorv1alpha1.Limitador, storageOptions Deplo command = append(command, "--limit-name-in-labels") } + // let's set explicitly the HTTP port, + // as it is being set in the readiness and liveness probe and in the service + command = append(command, "--http-port", strconv.Itoa(int(limObj.HTTPPort()))) + + // let's set explicitly the GRPC port, + // as it is being set in the service + command = append(command, "--rls-port", strconv.Itoa(int(limObj.GRPCPort()))) + command = append(command, filepath.Join(LimitadorCMMountPath, LimitadorConfigFileName)) command = append(command, storageOptions.Command...) 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", }) diff --git a/pkg/reconcilers/deployment.go b/pkg/reconcilers/deployment.go index 987aa68f..7bddf8ff 100644 --- a/pkg/reconcilers/deployment.go +++ b/pkg/reconcilers/deployment.go @@ -147,3 +147,45 @@ func DeploymentVolumeMountsMutator(desired, existing *appsv1.Deployment) bool { return update } + +func DeploymentPortsMutator(desired, existing *appsv1.Deployment) bool { + update := false + + existingContainer := &existing.Spec.Template.Spec.Containers[0] + desiredContainer := &desired.Spec.Template.Spec.Containers[0] + + if !reflect.DeepEqual(existingContainer.Ports, desiredContainer.Ports) { + existingContainer.Ports = desiredContainer.Ports + update = true + } + + return update +} + +func DeploymentLivenessProbeMutator(desired, existing *appsv1.Deployment) bool { + update := false + + existingContainer := &existing.Spec.Template.Spec.Containers[0] + desiredContainer := &desired.Spec.Template.Spec.Containers[0] + + if !reflect.DeepEqual(existingContainer.LivenessProbe, desiredContainer.LivenessProbe) { + existingContainer.LivenessProbe = desiredContainer.LivenessProbe + update = true + } + + return update +} + +func DeploymentReadinessProbeMutator(desired, existing *appsv1.Deployment) bool { + update := false + + existingContainer := &existing.Spec.Template.Spec.Containers[0] + desiredContainer := &desired.Spec.Template.Spec.Containers[0] + + if !reflect.DeepEqual(existingContainer.ReadinessProbe, desiredContainer.ReadinessProbe) { + existingContainer.ReadinessProbe = desiredContainer.ReadinessProbe + update = true + } + + return update +} diff --git a/pkg/reconcilers/service.go b/pkg/reconcilers/service.go new file mode 100644 index 00000000..892c4cb0 --- /dev/null +++ b/pkg/reconcilers/service.go @@ -0,0 +1,46 @@ +package reconcilers + +import ( + "fmt" + "reflect" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ServiceMutateFn is a function which mutates the existing Service into it's desired state. +type ServiceMutateFn func(desired, existing *v1.Service) bool + +func ServiceMutator(opts ...ServiceMutateFn) MutateFn { + return func(existingObj, desiredObj client.Object) (bool, error) { + existing, ok := existingObj.(*v1.Service) + if !ok { + return false, fmt.Errorf("%T is not a *v1.Service", existingObj) + } + desired, ok := desiredObj.(*v1.Service) + if !ok { + return false, fmt.Errorf("%T is not a *v1.Service", desiredObj) + } + + update := false + + // Loop through each option + for _, opt := range opts { + tmpUpdate := opt(desired, existing) + update = update || tmpUpdate + } + + return update, nil + } +} + +func ServicePortsMutator(desired, existing *v1.Service) bool { + update := false + + if !reflect.DeepEqual(existing.Spec.Ports, desired.Spec.Ports) { + existing.Spec.Ports = desired.Spec.Ports + update = true + } + + return update +}