diff --git a/api/v1alpha1/limitador_types.go b/api/v1alpha1/limitador_types.go index 8937cbbb..bcdded5d 100644 --- a/api/v1alpha1/limitador_types.go +++ b/api/v1alpha1/limitador_types.go @@ -36,6 +36,9 @@ type LimitadorSpec struct { // +optional Listener *Listener `json:"listener,omitempty"` + + // +optional + Limits []RateLimit `json:"limits,omitempty"` } // LimitadorStatus defines the observed state of Limitador @@ -80,6 +83,15 @@ type TransportProtocol struct { // We could describe TLS within this type } +// RateLimit defines the desired Limitador limit +type RateLimit struct { + Conditions []string `json:"conditions"` + MaxValue int `json:"max_value"` + Namespace string `json:"namespace"` + Seconds int `json:"seconds"` + Variables []string `json:"variables"` +} + func init() { SchemeBuilder.Register(&Limitador{}, &LimitadorList{}) } diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go deleted file mode 100644 index 64a43939..00000000 --- a/api/v1alpha1/ratelimit_types.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2020 Red Hat. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - -// RateLimitSpec defines the desired state of RateLimit -type RateLimitSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - Conditions []string `json:"conditions"` - MaxValue int `json:"max_value"` - Namespace string `json:"namespace"` - Seconds int `json:"seconds"` - Variables []string `json:"variables"` - // +optional - LimitadorRef NamespacedName `json:"limitador-ref,omitempty"` -} - -// RateLimitStatus defines the observed state of RateLimit -type RateLimitStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// RateLimit is the Schema for the ratelimits API -type RateLimit struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec RateLimitSpec `json:"spec,omitempty"` - Status RateLimitStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// RateLimitList contains a list of RateLimit -type RateLimitList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []RateLimit `json:"items"` -} - -type NamespacedName struct { - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` -} - -func init() { - SchemeBuilder.Register(&RateLimit{}, &RateLimitList{}) -} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6d4e92ba..8c14bea7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -102,6 +102,13 @@ func (in *LimitadorSpec) DeepCopyInto(out *LimitadorSpec) { *out = new(Listener) (*in).DeepCopyInto(*out) } + if in.Limits != nil { + in, out := &in.Limits, &out.Limits + *out = make([]RateLimit, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitadorSpec. @@ -146,82 +153,8 @@ func (in *Listener) DeepCopy() *Listener { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NamespacedName) DeepCopyInto(out *NamespacedName) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedName. -func (in *NamespacedName) DeepCopy() *NamespacedName { - if in == nil { - return nil - } - out := new(NamespacedName) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimit) DeepCopyInto(out *RateLimit) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimit. -func (in *RateLimit) DeepCopy() *RateLimit { - if in == nil { - return nil - } - out := new(RateLimit) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RateLimit) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RateLimitList) DeepCopyInto(out *RateLimitList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]RateLimit, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitList. -func (in *RateLimitList) DeepCopy() *RateLimitList { - if in == nil { - return nil - } - out := new(RateLimitList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RateLimitList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RateLimitSpec) DeepCopyInto(out *RateLimitSpec) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -233,30 +166,14 @@ func (in *RateLimitSpec) DeepCopyInto(out *RateLimitSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - out.LimitadorRef = in.LimitadorRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitSpec. -func (in *RateLimitSpec) DeepCopy() *RateLimitSpec { - if in == nil { - return nil - } - out := new(RateLimitSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RateLimitStatus) DeepCopyInto(out *RateLimitStatus) { - *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitStatus. -func (in *RateLimitStatus) DeepCopy() *RateLimitStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimit. +func (in *RateLimit) DeepCopy() *RateLimit { if in == nil { return nil } - out := new(RateLimitStatus) + out := new(RateLimit) in.DeepCopyInto(out) return out } diff --git a/bundle/manifests/limitador-operator.clusterserviceversion.yaml b/bundle/manifests/limitador-operator.clusterserviceversion.yaml index 4997ae09..0efe2c13 100644 --- a/bundle/manifests/limitador-operator.clusterserviceversion.yaml +++ b/bundle/manifests/limitador-operator.clusterserviceversion.yaml @@ -11,6 +11,25 @@ metadata: "name": "limitador-sample" }, "spec": { + "limits": [ + { + "conditions": [ + "get-toy == yes" + ], + "max_value": 2, + "namespace": "toystore-app", + "seconds": 30, + "variables": [] + } + ], + "listener": { + "grpc": { + "port": 8081 + }, + "http": { + "port": 8080 + } + }, "replicas": 1, "version": "latest" } @@ -60,9 +79,9 @@ spec: clusterPermissions: - rules: - apiGroups: - - apps + - "" resources: - - deployments + - configmaps verbs: - create - delete @@ -71,9 +90,9 @@ spec: - update - watch - apiGroups: - - "" + - apps resources: - - services + - deployments verbs: - create - delete @@ -82,35 +101,20 @@ spec: - update - watch - apiGroups: - - limitador.kuadrant.io + - "" resources: - - limitadors + - services verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - limitador.kuadrant.io resources: - - limitadors/finalizers - verbs: - - update - - apiGroups: - - limitador.kuadrant.io - resources: - - limitadors/status - verbs: - - get - - patch - - update - - apiGroups: - - limitador.kuadrant.io - resources: - - ratelimits + - limitadors verbs: - create - delete @@ -122,13 +126,13 @@ spec: - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/finalizers + - limitadors/finalizers verbs: - update - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/status + - limitadors/status verbs: - get - patch diff --git a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml index ea9bf35c..97b544f7 100644 --- a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml +++ b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml @@ -34,6 +34,32 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + limits: + items: + description: RateLimit defines the desired Limitador limit + properties: + conditions: + items: + type: string + type: array + max_value: + type: integer + namespace: + type: string + seconds: + type: integer + variables: + items: + type: string + type: array + required: + - conditions + - max_value + - namespace + - seconds + - variables + type: object + type: array listener: properties: grpc: diff --git a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml index 33cd02fb..ce1395ea 100644 --- a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml +++ b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml @@ -36,6 +36,32 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + limits: + items: + description: RateLimit defines the desired Limitador limit + properties: + conditions: + items: + type: string + type: array + max_value: + type: integer + namespace: + type: string + seconds: + type: integer + variables: + items: + type: string + type: array + required: + - conditions + - max_value + - namespace + - seconds + - variables + type: object + type: array listener: properties: grpc: diff --git a/config/deploy/manfiests.yaml b/config/deploy/manfiests.yaml index d810ab8d..c0c32a3a 100644 --- a/config/deploy/manfiests.yaml +++ b/config/deploy/manfiests.yaml @@ -41,6 +41,32 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + limits: + items: + description: RateLimit defines the desired Limitador limit + properties: + conditions: + items: + type: string + type: array + max_value: + type: integer + namespace: + type: string + seconds: + type: integer + variables: + items: + type: string + type: array + required: + - conditions + - max_value + - namespace + - seconds + - variables + type: object + type: array listener: properties: grpc: @@ -199,9 +225,9 @@ metadata: name: limitador-operator-manager-role rules: - apiGroups: - - apps + - "" resources: - - deployments + - configmaps verbs: - create - delete @@ -210,9 +236,9 @@ rules: - update - watch - apiGroups: - - "" + - apps resources: - - services + - deployments verbs: - create - delete @@ -221,35 +247,20 @@ rules: - update - watch - apiGroups: - - limitador.kuadrant.io + - "" resources: - - limitadors + - services verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - limitador.kuadrant.io resources: - - limitadors/finalizers - verbs: - - update -- apiGroups: - - limitador.kuadrant.io - resources: - - limitadors/status - verbs: - - get - - patch - - update -- apiGroups: - - limitador.kuadrant.io - resources: - - ratelimits + - limitadors verbs: - create - delete @@ -261,13 +272,13 @@ rules: - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/finalizers + - limitadors/finalizers verbs: - update - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/status + - limitadors/status verbs: - get - patch diff --git a/config/install/manifests.yaml b/config/install/manifests.yaml index 87f624f3..eb011651 100644 --- a/config/install/manifests.yaml +++ b/config/install/manifests.yaml @@ -34,6 +34,32 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + limits: + items: + description: RateLimit defines the desired Limitador limit + properties: + conditions: + items: + type: string + type: array + max_value: + type: integer + namespace: + type: string + seconds: + type: integer + variables: + items: + type: string + type: array + required: + - conditions + - max_value + - namespace + - seconds + - variables + type: object + type: array listener: properties: grpc: @@ -192,9 +218,9 @@ metadata: name: limitador-operatormanager-role rules: - apiGroups: - - apps + - "" resources: - - deployments + - configmaps verbs: - create - delete @@ -203,9 +229,9 @@ rules: - update - watch - apiGroups: - - "" + - apps resources: - - services + - deployments verbs: - create - delete @@ -214,35 +240,20 @@ rules: - update - watch - apiGroups: - - limitador.kuadrant.io + - "" resources: - - limitadors + - services verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - limitador.kuadrant.io resources: - - limitadors/finalizers - verbs: - - update -- apiGroups: - - limitador.kuadrant.io - resources: - - limitadors/status - verbs: - - get - - patch - - update -- apiGroups: - - limitador.kuadrant.io - resources: - - ratelimits + - limitadors verbs: - create - delete @@ -254,13 +265,13 @@ rules: - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/finalizers + - limitadors/finalizers verbs: - update - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/status + - limitadors/status verbs: - get - patch diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 31151e6f..0eed8028 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -7,9 +7,9 @@ metadata: name: manager-role rules: - apiGroups: - - apps + - "" resources: - - deployments + - configmaps verbs: - create - delete @@ -18,9 +18,9 @@ rules: - update - watch - apiGroups: - - "" + - apps resources: - - services + - deployments verbs: - create - delete @@ -29,35 +29,20 @@ rules: - update - watch - apiGroups: - - limitador.kuadrant.io + - "" resources: - - limitadors + - services verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - limitador.kuadrant.io resources: - - limitadors/finalizers - verbs: - - update -- apiGroups: - - limitador.kuadrant.io - resources: - - limitadors/status - verbs: - - get - - patch - - update -- apiGroups: - - limitador.kuadrant.io - resources: - - ratelimits + - limitadors verbs: - create - delete @@ -69,13 +54,13 @@ rules: - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/finalizers + - limitadors/finalizers verbs: - update - apiGroups: - limitador.kuadrant.io resources: - - ratelimits/status + - limitadors/status verbs: - get - patch diff --git a/config/samples/limitador_v1alpha1_limitador.yaml b/config/samples/limitador_v1alpha1_limitador.yaml index 90788359..469ed1b4 100644 --- a/config/samples/limitador_v1alpha1_limitador.yaml +++ b/config/samples/limitador_v1alpha1_limitador.yaml @@ -5,3 +5,14 @@ metadata: spec: replicas: 1 version: latest + listener: + http: + port: 8080 + grpc: + port: 8081 + limits: + - conditions: ["get-toy == yes"] + max_value: 2 + namespace: toystore-app + seconds: 30 + variables: [] diff --git a/controllers/limitador_controller.go b/controllers/limitador_controller.go index 83a9236c..b81d82cc 100644 --- a/controllers/limitador_controller.go +++ b/controllers/limitador_controller.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "github.com/kuadrant/limitador-operator/pkg/helpers" + v1 "k8s.io/api/core/v1" "strconv" "github.com/go-logr/logr" @@ -43,6 +44,7 @@ type LimitadorReconciler struct { //+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors/finalizers,verbs=update //+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;delete //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete func (r *LimitadorReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := r.Logger().WithValues("limitador", req.NamespacedName) @@ -86,6 +88,17 @@ func (r *LimitadorReconciler) Reconcile(eventCtx context.Context, req ctrl.Reque return ctrl.Result{}, err } + // Reconcile Limits ConfigMap + limitsConfigMap, err := limitador.LimitsConfigMap(limitadorObj) + if err != nil { + return ctrl.Result{}, err + } + err = r.ReconcileConfigMap(ctx, limitsConfigMap, mutateLimitsConfigMap) + logger.V(1).Info("reconcile limits ConfigMap", "error", err) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } @@ -116,6 +129,30 @@ func buildServiceUrl(limitadorObj *limitadorv1alpha1.Limitador) string { strconv.Itoa(int(helpers.GetValueOrDefault(*limitadorObj.Spec.Listener.HTTP.Port, limitador.DefaultServiceHTTPPort).(int32))) } +func mutateLimitsConfigMap(existingObj, desiredObj client.Object) (bool, error) { + existing, ok := existingObj.(*v1.ConfigMap) + if !ok { + return false, fmt.Errorf("%T is not a *v1.ConfigMap", existingObj) + } + desired, ok := desiredObj.(*v1.ConfigMap) + if !ok { + return false, fmt.Errorf("%T is not a *v1.ConfigMap", desiredObj) + } + + updated := false + + if existing.Data[limitador.LimitadorCMHash] != desired.Data[limitador.LimitadorCMHash] { + for k, v := range map[string]string{ + limitador.LimitadorCMHash: desired.Data[limitador.LimitadorCMHash], + limitador.LimitadorConfigFileName: string(desired.Data[limitador.LimitadorConfigFileName]), + } { + existing.Data[k] = v + } + updated = true + } + return updated, nil +} + func mutateLimitadorDeployment(existingObj, desiredObj client.Object) (bool, error) { existing, ok := existingObj.(*appsv1.Deployment) if !ok { diff --git a/controllers/limitador_controller_test.go b/controllers/limitador_controller_test.go index d9167ac2..696ab6b8 100644 --- a/controllers/limitador_controller_test.go +++ b/controllers/limitador_controller_test.go @@ -2,6 +2,7 @@ package controllers import ( "context" + "github.com/kuadrant/limitador-operator/pkg/limitador" "time" . "github.com/onsi/ginkgo" @@ -19,13 +20,12 @@ import ( var _ = Describe("Limitador controller", func() { const ( - LimitadorNamespace = "default" - LimitadorReplicas = 2 - LimitadorImage = "quay.io/3scale/limitador" - LimitadorVersion = "0.3.0" - LimitadorServiceName = "limitador-service" - LimitadorHttpPort = 8000 - LimitadorGttpPort = 8001 + LimitadorNamespace = "default" + LimitadorReplicas = 2 + LimitadorImage = "quay.io/3scale/limitador" + LimitadorVersion = "0.3.0" + LimitadorHttpPort = 8000 + LimitadorGttpPort = 8001 timeout = time.Second * 10 interval = time.Millisecond * 250 @@ -38,6 +38,24 @@ var _ = Describe("Limitador controller", func() { version := LimitadorVersion httpPort := limitadorv1alpha1.TransportProtocol{Port: &httpPortNumber} grpcPort := limitadorv1alpha1.TransportProtocol{Port: &grpcPortNumber} + + limits := []limitadorv1alpha1.RateLimit{ + { + Conditions: []string{"req.method == GET"}, + MaxValue: 10, + Namespace: "test-namespace", + Seconds: 60, + Variables: []string{"user_id"}, + }, + { + Conditions: []string{"req.method == POST"}, + MaxValue: 5, + Namespace: "test-namespace", + Seconds: 60, + Variables: []string{"user_id"}, + }, + } + newLimitador := func() *limitadorv1alpha1.Limitador { // The name can't start with a number. name := "a" + string(uuid.NewUUID()) @@ -58,6 +76,7 @@ var _ = Describe("Limitador controller", func() { HTTP: httpPort, GRPC: grpcPort, }, + Limits: limits, }, } } @@ -75,7 +94,7 @@ var _ = Describe("Limitador controller", func() { Expect(k8sClient.Create(context.TODO(), limitadorObj)).Should(Succeed()) }) - It("Should create a new deployment with the right number of replicas and version", func() { + It("Should create a new deployment with the right number of replicas, version and config file", func() { createdLimitadorDeployment := appsv1.Deployment{} Eventually(func() bool { err := k8sClient.Get( @@ -95,6 +114,15 @@ var _ = Describe("Limitador controller", func() { Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Image).Should( Equal(LimitadorImage + ":" + LimitadorVersion), ) + Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Env[1]).Should( + Equal(v1.EnvVar{Name: "LIMITS_FILE", Value: "/home/limitador/etc/limitador-config.yaml", ValueFrom: nil}), + ) + Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath).Should( + Equal("/home/limitador/etc/"), + ) + Expect(createdLimitadorDeployment.Spec.Template.Spec.Volumes[0].VolumeSource.ConfigMap.Name).Should( + Equal(limitador.LimitsCMNamePrefix + limitadorObj.Name), + ) }) It("Should create a Limitador service", func() { @@ -125,6 +153,27 @@ var _ = Describe("Limitador controller", func() { }, timeout, interval).Should(Equal("http://" + limitadorObj.Name + ".default.svc.cluster.local:8000")) }) + It("Should create a ConfigMap with the correct limits and hash", func() { + createdConfigMap := v1.ConfigMap{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitador.LimitsCMNamePrefix + limitadorObj.Name, + }, + &createdConfigMap) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(createdConfigMap.Data[limitador.LimitadorCMHash]).Should( + Equal("a00c9940ae6bb8de702633ce453e6a97"), + ) + Expect(createdConfigMap.Data[limitador.LimitadorConfigFileName]).Should( + Equal("- conditions:\n - req.method == GET\n max_value: 10\n namespace: test-namespace\n seconds: 60\n variables:\n - user_id\n- conditions:\n - req.method == POST\n max_value: 5\n namespace: test-namespace\n seconds: 60\n variables:\n - user_id\n"), + ) + }) }) Context("Updating a limitador object", func() { @@ -178,5 +227,51 @@ var _ = Describe("Limitador controller", func() { return correctReplicas && correctImage }, timeout, interval).Should(BeTrue()) }) + It("Should modify the ConfigMap accordingly", func() { + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitadorObj.Name, + }, + &updatedLimitador) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + limits := []limitadorv1alpha1.RateLimit{ + { + Conditions: []string{"req.method == GET"}, + MaxValue: 100, + Namespace: "test-namespace", + Seconds: 60, + Variables: []string{"user_id"}, + }, + } + updatedLimitador.Spec.Limits = limits + + Expect(k8sClient.Update(context.TODO(), &updatedLimitador)).Should(Succeed()) + updatedLimitadorConfigMap := v1.ConfigMap{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitador.LimitsCMNamePrefix + limitadorObj.Name, + }, + &updatedLimitadorConfigMap) + + if err != nil { + return false + } + + return true + }, timeout, interval).Should(BeTrue()) + Expect(updatedLimitadorConfigMap.Data[limitador.LimitadorCMHash]).Should(Equal("69b3eab828208274d4200aedc6fd8b19")) + Expect(updatedLimitadorConfigMap.Data[limitador.LimitadorConfigFileName]).Should(Equal("- conditions:\n - req.method == GET\n max_value: 100\n namespace: test-namespace\n seconds: 60\n variables:\n - user_id\n")) + + }) }) }) diff --git a/controllers/ratelimit_controller.go b/controllers/ratelimit_controller.go deleted file mode 100644 index c77d4207..00000000 --- a/controllers/ratelimit_controller.go +++ /dev/null @@ -1,216 +0,0 @@ -/* -Copyright 2020 Red Hat. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "net/url" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - "github.com/kuadrant/limitador-operator/pkg/helpers" - "github.com/kuadrant/limitador-operator/pkg/limitador" - "github.com/kuadrant/limitador-operator/pkg/reconcilers" -) - -const ( - rateLimitFinalizer = "finalizer.ratelimit.limitador.kuadrant.io" - DefaultLimitadorName = "limitador" -) - -// Assumes that there's only one Limitador per namespace. We might want to -// change this in the future. -type LimitadorServiceDiscovery interface { - URL(*limitadorv1alpha1.RateLimit) (*url.URL, error) -} - -type defaultLimitadorServiceDiscovery struct { - client.Client -} - -func (d *defaultLimitadorServiceDiscovery) URL(limit *limitadorv1alpha1.RateLimit) (*url.URL, error) { - limitadorNamespacedName := helpers.GetValueOrDefault(limit.Spec.LimitadorRef, - types.NamespacedName{ - Namespace: DefaultLimitadorName, - Name: limit.Namespace, - }).(types.NamespacedName) - var limitador limitadorv1alpha1.Limitador - if err := d.Get(context.Background(), limitadorNamespacedName, &limitador); err != nil { - return nil, err - } - return url.Parse(limitador.Status.ServiceURL) -} - -// RateLimitReconciler reconciles a RateLimit object -type RateLimitReconciler struct { - *reconcilers.BaseReconciler - LimitadorDiscovery LimitadorServiceDiscovery -} - -//+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=ratelimits,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=ratelimits/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=ratelimits/finalizers,verbs=update -//+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors,verbs=get;list;watch -//+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors/status,verbs=get - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the RateLimit object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile -func (r *RateLimitReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("ratelimit", req.NamespacedName) - logger.V(1).Info("Reconciling RateLimit") - ctx := logr.NewContext(eventCtx, logger) - - limit := &limitadorv1alpha1.RateLimit{} - if err := r.Client().Get(ctx, req.NamespacedName, limit); err != nil { - if errors.IsNotFound(err) { - return ctrl.Result{}, nil - } - - logger.Error(err, "Failed to get RateLimit object.") - return ctrl.Result{}, err - } - - isLimitMarkedToBeDeleted := limit.GetDeletionTimestamp() != nil - if isLimitMarkedToBeDeleted { - if helpers.Contains(limit.GetFinalizers(), rateLimitFinalizer) { - if err := r.finalizeRateLimit(limit); err != nil { - return ctrl.Result{}, err - } - - // Remove finalizer. Once all finalizers have been removed, the - // object will be deleted. - controllerutil.RemoveFinalizer(limit, rateLimitFinalizer) - if err := r.Client().Update(ctx, limit); err != nil { - return ctrl.Result{}, err - } - } - - return ctrl.Result{}, nil - } - - if err := r.ensureFinalizerIsAdded(ctx, limit); err != nil { - return ctrl.Result{}, err - } - - if err := r.createLimitInLimitador(limit); err != nil { - logger.Error(err, "Failed to create rate limit in Limitador.") - return ctrl.Result{}, err - } - - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *RateLimitReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&limitadorv1alpha1.RateLimit{}). - WithEventFilter(r.updateLimitPredicate()). - Complete(r) -} - -// This should be temporary. This is not how a filter should be used. However, -// with the current Limitador API, when updating a limit, we need both the -// current and the previous version. After updating the Limitador API to work -// with IDs, this won't be needed. -func (r *RateLimitReconciler) updateLimitPredicate() predicate.Predicate { - return predicate.Funcs{ - UpdateFunc: func(e event.UpdateEvent) bool { - oldVersion := e.ObjectOld.(*limitadorv1alpha1.RateLimit) - newVersion := e.ObjectNew.(*limitadorv1alpha1.RateLimit) - - if oldVersion.ObjectMeta.Generation == newVersion.ObjectMeta.Generation { - return false - } - - // The namespace should be the same in the old and the new version, - // so we can use either. - limitadorUrl, err := r.limitadorDiscovery().URL(newVersion) - if err != nil { - return false - } - - limitadorClient := limitador.NewClient(*limitadorUrl) - - // Try to create the new version even if the old one can't be - // deleted. This might leave in Limitador limits that should no - // longer be there. As this function should only be temporary this - // should be fine for a first version of the controller. - _ = limitadorClient.DeleteLimit(&oldVersion.Spec) - - return true - }, - } -} - -func (r *RateLimitReconciler) createLimitInLimitador(limit *limitadorv1alpha1.RateLimit) error { - limitadorUrl, err := r.limitadorDiscovery().URL(limit) - if err != nil { - return err - } - - limitadorClient := limitador.NewClient(*limitadorUrl) - return limitadorClient.CreateLimit(&limit.Spec) -} - -func (r *RateLimitReconciler) ensureFinalizerIsAdded(ctx context.Context, limit *limitadorv1alpha1.RateLimit) error { - logger := logr.FromContext(ctx) - numberOfFinalizers := len(limit.GetFinalizers()) - controllerutil.AddFinalizer(limit, rateLimitFinalizer) - if numberOfFinalizers == len(limit.GetFinalizers()) { - // The finalizer was already there, no need to update - return nil - } - - if err := r.Client().Update(ctx, limit); err != nil { - logger.Error(err, "Failed to update the rate limit with finalizer") - return err - } - - return nil -} - -func (r *RateLimitReconciler) finalizeRateLimit(rateLimit *limitadorv1alpha1.RateLimit) error { - limitadorUrl, err := r.limitadorDiscovery().URL(rateLimit) - if err != nil { - return err - } - - limitadorClient := limitador.NewClient(*limitadorUrl) - return limitadorClient.DeleteLimit(&rateLimit.Spec) -} - -func (r *RateLimitReconciler) limitadorDiscovery() LimitadorServiceDiscovery { - if r.LimitadorDiscovery == nil { - r.LimitadorDiscovery = &defaultLimitadorServiceDiscovery{} - } - - return r.LimitadorDiscovery -} diff --git a/controllers/ratelimit_controller_test.go b/controllers/ratelimit_controller_test.go deleted file mode 100644 index e9561f2b..00000000 --- a/controllers/ratelimit_controller_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package controllers - -import ( - "context" - "sync" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/ghttp" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/json" - "k8s.io/apimachinery/pkg/util/uuid" - "sigs.k8s.io/controller-runtime/pkg/client" - - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" -) - -var _ = Describe("RateLimit controller", func() { - const ( - timeout = time.Second * 10 - interval = time.Millisecond * 250 - ) - - // Used to generate a different limit on every test so they don't collide. - var newRateLimit = func() limitadorv1alpha1.RateLimit { - // The name can't start with a number. - name := "a" + string(uuid.NewUUID()) - - return limitadorv1alpha1.RateLimit{ - TypeMeta: metav1.TypeMeta{ - Kind: "RateLimit", - APIVersion: "limitador.kuadrant.io/v1alpha1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - Spec: limitadorv1alpha1.RateLimitSpec{ - Conditions: []string{"req.method == GET"}, - MaxValue: 10, - Namespace: "test-namespace", - Seconds: 60, - Variables: []string{"user_id"}, - }, - } - } - - // The next couple of functions are useful to verify that an HTTP request is - // made after a call to the kubernetesClient. - // The functions are wrappers for k8sClient.Create and k8sClient.Delete, so - // the signature is the same. - // We know that after creating, deleting, etc. a RateLimit CR, an HTTP - // request is made to create, delete, etc. the limit in Limitador. These - // functions are useful for waiting until the state is synchronized. - - // Wraps a function with the same signature as k8sClient.Create and waits - // for an HTTP request. - var runCreateAndWaitHTTPReq = func(f func(ctx context.Context, - object client.Object, - opts ...client.CreateOption, - ) error) func(ctx context.Context, object client.Object, opts ...client.CreateOption) error { - return func(ctx context.Context, object client.Object, opts ...client.CreateOption) error { - reqsAtStart := len(mockedHTTPServer.ReceivedRequests()) - - err := f(ctx, object, opts...) - if err != nil { - return err - } - - Eventually(func() bool { - return len(mockedHTTPServer.ReceivedRequests()) > reqsAtStart - }, timeout, interval).Should(BeTrue()) - - return nil - } - } - - // Wraps a function with the same signature as k8sClient.Delete and waits - // for an HTTP request. - var runDeleteAndWaitHTTPReq = func(f func(ctx context.Context, - object client.Object, - opts ...client.DeleteOption, - ) error) func(ctx context.Context, object client.Object, opts ...client.DeleteOption) error { - return func(ctx context.Context, object client.Object, opts ...client.DeleteOption) error { - reqsAtStart := len(mockedHTTPServer.ReceivedRequests()) - - err := f(ctx, object, opts...) - if err != nil { - return err - } - - Eventually(func() bool { - return len(mockedHTTPServer.ReceivedRequests()) > reqsAtStart - }, timeout, interval).Should(BeTrue()) - - return nil - } - } - - var addHandlerForLimitCreation = func(limitSpecJson string) { - mockedHTTPServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.VerifyRequest("POST", "/limits"), - ghttp.VerifyJSON(limitSpecJson), - ), - ) - } - - var addHandlerForLimitDeletion = func(limitSpecJson string) { - mockedHTTPServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.VerifyRequest("DELETE", "/limits"), - ghttp.VerifyJSON(limitSpecJson), - ), - ) - } - - // These tests make HTTP requests to the same mocked server. Running them in - // parallel makes it difficult to reason about them. - var sequentialTestLock sync.Mutex - - BeforeEach(func() { - sequentialTestLock.Lock() - defer sequentialTestLock.Unlock() - mockedHTTPServer.Reset() - }) - - Context("Creating a new RateLimit object", func() { - testLimit := newRateLimit() - testLimitSpecJson, _ := json.Marshal(testLimit.Spec) - - BeforeEach(func() { - addHandlerForLimitCreation(string(testLimitSpecJson)) - }) - - AfterEach(func() { - Expect(runDeleteAndWaitHTTPReq(k8sClient.Delete)( - context.TODO(), &testLimit, - )).Should(Succeed()) - }) - - It("Should create a limit in Limitador", func() { - Expect(runCreateAndWaitHTTPReq(k8sClient.Create)( - context.TODO(), &testLimit, - )).Should(Succeed()) - }) - }) - - Context("Deleting a RateLimit object", func() { - testLimit := newRateLimit() - testLimitSpecJson, _ := json.Marshal(testLimit.Spec) - - BeforeEach(func() { - addHandlerForLimitCreation(string(testLimitSpecJson)) - - Expect(runCreateAndWaitHTTPReq(k8sClient.Create)( - context.TODO(), &testLimit, - )).Should(Succeed()) - - addHandlerForLimitDeletion(string(testLimitSpecJson)) - }) - - It("Should delete the limit in Limitador", func() { - Expect(runDeleteAndWaitHTTPReq(k8sClient.Delete)( - context.TODO(), &testLimit, - )).Should(Succeed()) - }) - }) -}) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 249ceaea..dbfac337 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -94,33 +94,12 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - rateLimitBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - ctrl.Log.WithName("controllers").WithName("ratelimit"), - mgr.GetEventRecorderFor("RateLimit"), - ) - limitadorBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), ctrl.Log.WithName("controllers").WithName("limitador"), mgr.GetEventRecorderFor("Limitador"), ) - mockedHTTPServer = ghttp.NewServer() - mockedHTTPServerURL, err := url.Parse(mockedHTTPServer.URL()) - Expect(err).ToNot(HaveOccurred()) - - // Set this to true so we don't have to specify all the requests, including - // the ones for example done for cleanup in AfterEach() functions. - mockedHTTPServer.SetAllowUnhandledRequests(true) - - // Register reconcilers - err = (&RateLimitReconciler{ - BaseReconciler: rateLimitBaseReconciler, - LimitadorDiscovery: &TestLimitadorServiceDiscovery{url: *mockedHTTPServerURL}, - }).SetupWithManager(mgr) - Expect(err).ToNot(HaveOccurred()) - err = (&LimitadorReconciler{ BaseReconciler: limitadorBaseReconciler, }).SetupWithManager(mgr) diff --git a/go.mod b/go.mod index ecae2a23..aa2c0b02 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/go-logr/logr v0.4.0 github.com/onsi/ginkgo v1.16.4 github.com/onsi/gomega v1.15.0 - github.com/stretchr/testify v1.7.0 go.uber.org/zap v1.19.0 gotest.tools v2.2.0+incompatible k8s.io/api v0.22.1 @@ -14,4 +13,5 @@ require ( k8s.io/client-go v0.22.1 k8s.io/klog/v2 v2.9.0 sigs.k8s.io/controller-runtime v0.10.0 + sigs.k8s.io/yaml v1.2.0 ) diff --git a/main.go b/main.go index 1ec43934..878443e6 100644 --- a/main.go +++ b/main.go @@ -94,19 +94,6 @@ func main() { os.Exit(1) } - rateLimitBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), - log.Log.WithName("ratelimit"), - mgr.GetEventRecorderFor("RateLimit"), - ) - - if err = (&controllers.RateLimitReconciler{ - BaseReconciler: rateLimitBaseReconciler, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create RateLimit controller") - os.Exit(1) - } - limitadorBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), log.Log.WithName("limitador"), diff --git a/pkg/limitador/client.go b/pkg/limitador/client.go deleted file mode 100644 index 6395069d..00000000 --- a/pkg/limitador/client.go +++ /dev/null @@ -1,68 +0,0 @@ -package limitador - -import ( - "bytes" - "fmt" - "net/http" - "net/url" - - "k8s.io/apimachinery/pkg/util/json" - - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - "github.com/kuadrant/limitador-operator/pkg/helpers" - "github.com/kuadrant/limitador-operator/pkg/log" -) - -type Client struct { - httpClient *http.Client - url url.URL -} - -func NewClient(url url.URL) Client { - var transport http.RoundTripper - if log.Log.V(1).Enabled() { - transport = &helpers.VerboseTransport{} - } - - return Client{ - url: url, - httpClient: &http.Client{Transport: transport}, - } -} - -func (client *Client) CreateLimit(rateLimitSpec *limitadorv1alpha1.RateLimitSpec) error { - jsonLimit, err := json.Marshal(rateLimitSpec) - if err != nil { - return err - } - - _, err = client.httpClient.Post( - fmt.Sprintf("%s/limits", client.url.String()), - "application/json", - bytes.NewBuffer(jsonLimit), - ) - - return err -} - -func (client *Client) DeleteLimit(rateLimitSpec *limitadorv1alpha1.RateLimitSpec) error { - jsonLimit, err := json.Marshal(rateLimitSpec) - if err != nil { - return err - } - - req, err := http.NewRequest( - "DELETE", - fmt.Sprintf("%s/limits", client.url.String()), - bytes.NewBuffer(jsonLimit), - ) - if err != nil { - return err - } - - req.Header.Add("Content-Type", "application/json") - - _, err = client.httpClient.Do(req) - - return err -} diff --git a/pkg/limitador/client_test.go b/pkg/limitador/client_test.go deleted file mode 100644 index 10ea21b1..00000000 --- a/pkg/limitador/client_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package limitador - -import ( - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - "github.com/stretchr/testify/assert" - "io/ioutil" - "k8s.io/apimachinery/pkg/util/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" -) - -func TestCreateLimit(t *testing.T) { - rateLimitSpec := exampleRateLimitSpec() - rateLimitSpecJson, err := json.Marshal(rateLimitSpec) - assert.NoError(t, err) - - testServerUrl, closeServerFunc := newTestServer(t, "POST", "/limits", string(rateLimitSpecJson)) - defer closeServerFunc() - - limitadorClient := NewClient(*testServerUrl) - err = limitadorClient.CreateLimit(rateLimitSpec) - - assert.NoError(t, err) -} - -func TestDeleteLimit(t *testing.T) { - rateLimitSpec := exampleRateLimitSpec() - rateLimitSpecJson, err := json.Marshal(rateLimitSpec) - assert.NoError(t, err) - - testServerUrl, closeServerFunc := newTestServer(t, "DELETE", "/limits", string(rateLimitSpecJson)) - defer closeServerFunc() - - limitadorClient := NewClient(*testServerUrl) - err = limitadorClient.DeleteLimit(rateLimitSpec) - - assert.NoError(t, err) -} - -// Creates a test server that checks the given HTTP request fields -func newTestServer(t *testing.T, expectedMethod string, expectedPath string, expectedBody string) (*url.URL, func()) { - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, expectedMethod, r.Method) - - assert.Equal(t, expectedPath, r.URL.Path) - - body, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - err = r.Body.Close() - assert.NoError(t, err) - assert.Equal(t, expectedBody, string(body)) - })) - - serverUrl, err := url.Parse(testServer.URL) - assert.Nil(t, err) - - return serverUrl, testServer.Close -} - -func exampleRateLimitSpec() *limitadorv1alpha1.RateLimitSpec { - return &limitadorv1alpha1.RateLimitSpec{ - Conditions: []string{"req.method == GET"}, - MaxValue: 10, - Namespace: "test-namespace", - Seconds: 60, - Variables: []string{"user_id"}, - } -} diff --git a/pkg/limitador/k8s_objects.go b/pkg/limitador/k8s_objects.go index 02107c72..4d318e09 100644 --- a/pkg/limitador/k8s_objects.go +++ b/pkg/limitador/k8s_objects.go @@ -1,21 +1,29 @@ package limitador import ( + "crypto/md5" + "fmt" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "github.com/kuadrant/limitador-operator/pkg/helpers" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/yaml" ) const ( - DefaultVersion = "latest" - DefaultReplicas = 1 - Image = "quay.io/3scale/limitador" - StatusEndpoint = "/status" - DefaultServiceHTTPPort = 8080 - DefaultServiceGRPCPort = 8081 + DefaultVersion = "latest" + DefaultReplicas = 1 + Image = "quay.io/3scale/limitador" + StatusEndpoint = "/status" + DefaultServiceHTTPPort = 8080 + DefaultServiceGRPCPort = 8081 + LimitadorConfigFileName = "limitador-config.yaml" + LimitadorCMHash = "hash" + LimitsCMNamePrefix = "limits-config-" + LimitadorCMMountPath = "/home/limitador/etc/" + LimitadorLimitsFileEnv = "LIMITS_FILE" ) func LimitadorService(limitador *limitadorv1alpha1.Limitador) *v1.Service { @@ -105,6 +113,10 @@ func LimitadorDeployment(limitador *limitadorv1alpha1.Limitador) *appsv1.Deploym Name: "RUST_LOG", Value: "info", }, + { + Name: LimitadorLimitsFileEnv, + Value: LimitadorCMMountPath + LimitadorConfigFileName, + }, }, LivenessProbe: &v1.Probe{ Handler: v1.Handler{ @@ -134,15 +146,52 @@ func LimitadorDeployment(limitador *limitadorv1alpha1.Limitador) *appsv1.Deploym SuccessThreshold: 1, FailureThreshold: 3, }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "config-file", + MountPath: LimitadorCMMountPath, + }, + }, ImagePullPolicy: v1.PullIfNotPresent, }, }, + Volumes: []v1.Volume{ + { + Name: "config-file", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: LimitsCMNamePrefix + limitador.Name, + }, + }, + }, + }, + }, }, }, }, } } +func LimitsConfigMap(limitador *limitadorv1alpha1.Limitador) (*v1.ConfigMap, error) { + limitsMarshalled, marshallErr := yaml.Marshal(limitador.Spec.Limits) + if marshallErr != nil { + return nil, marshallErr + } + + return &v1.ConfigMap{ + Data: map[string]string{ + LimitadorConfigFileName: string(limitsMarshalled), + LimitadorCMHash: fmt.Sprintf("%x", md5.Sum(limitsMarshalled)), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: LimitsCMNamePrefix + limitador.Name, + Namespace: limitador.Namespace, + Labels: map[string]string{"app": "limitador"}, + }, + }, nil +} + func labels() map[string]string { return map[string]string{"app": "limitador"} } diff --git a/pkg/limitador/k8s_objects_test.go b/pkg/limitador/k8s_objects_test.go new file mode 100644 index 00000000..55f82ae6 --- /dev/null +++ b/pkg/limitador/k8s_objects_test.go @@ -0,0 +1,22 @@ +package limitador + +import ( + "gotest.tools/assert" + "testing" +) + +func TestConstants(t *testing.T) { + assert.Check(t, "latest" == DefaultVersion) + assert.Check(t, 1 == DefaultReplicas) + assert.Check(t, "quay.io/3scale/limitador" == Image) + assert.Check(t, "/status" == StatusEndpoint) + assert.Check(t, 8080 == DefaultServiceHTTPPort) + assert.Check(t, 8081 == DefaultServiceGRPCPort) + assert.Check(t, "limitador-config.yaml" == LimitadorConfigFileName) + assert.Check(t, "hash" == LimitadorCMHash) + assert.Check(t, "limits-config-" == LimitsCMNamePrefix) + assert.Check(t, "/home/limitador/etc/" == LimitadorCMMountPath) + assert.Check(t, "LIMITS_FILE" == LimitadorLimitsFileEnv) +} + +//TODO: Test individual k8s objects. Extract limitadorObj creation from controller_test diff --git a/pkg/reconcilers/base_reconciler.go b/pkg/reconcilers/base_reconciler.go index d374549e..42b80341 100644 --- a/pkg/reconcilers/base_reconciler.go +++ b/pkg/reconcilers/base_reconciler.go @@ -18,7 +18,6 @@ package reconcilers import ( "context" - "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -144,6 +143,10 @@ func (b *BaseReconciler) ReconcileDeployment(ctx context.Context, desired *appsv return b.ReconcileResource(ctx, &appsv1.Deployment{}, desired, mutatefn) } +func (b *BaseReconciler) ReconcileConfigMap(ctx context.Context, desired *corev1.ConfigMap, mutatefn MutateFn) error { + return b.ReconcileResource(ctx, &corev1.ConfigMap{}, desired, mutatefn) +} + func (b *BaseReconciler) GetResource(ctx context.Context, objKey types.NamespacedName, obj client.Object) error { logger := logr.FromContext(ctx) logger.Info("get object", "GKV", obj.GetObjectKind().GroupVersionKind(), "name", obj.GetName(), "namespace", obj.GetNamespace())