diff --git a/api/v1alpha1/limitador_types.go b/api/v1alpha1/limitador_types.go index 3fc7b261..8937cbbb 100644 --- a/api/v1alpha1/limitador_types.go +++ b/api/v1alpha1/limitador_types.go @@ -33,12 +33,17 @@ type LimitadorSpec struct { // +optional Version *string `json:"version,omitempty"` + + // +optional + Listener *Listener `json:"listener,omitempty"` } // LimitadorStatus defines the observed state of Limitador type LimitadorStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + + ServiceURL string `json:"service-url,omitempty"` } //+kubebuilder:object:root=true @@ -62,6 +67,19 @@ type LimitadorList struct { Items []Limitador `json:"items"` } +type Listener struct { + // +optional + HTTP TransportProtocol `json:"http,omitempty"` + // +optional + GRPC TransportProtocol `json:"grpc,omitempty"` +} + +type TransportProtocol struct { + // +optional + Port *int32 `json:"port,omitempty"` + // We could describe TLS within this type +} + func init() { SchemeBuilder.Register(&Limitador{}, &LimitadorList{}) } diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go index d50b669d..64a43939 100644 --- a/api/v1alpha1/ratelimit_types.go +++ b/api/v1alpha1/ratelimit_types.go @@ -33,6 +33,8 @@ type RateLimitSpec struct { 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 @@ -62,6 +64,11 @@ type RateLimitList struct { 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 1c5e9ec2..6d4e92ba 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -97,6 +97,11 @@ func (in *LimitadorSpec) DeepCopyInto(out *LimitadorSpec) { *out = new(string) **out = **in } + if in.Listener != nil { + in, out := &in.Listener, &out.Listener + *out = new(Listener) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitadorSpec. @@ -124,6 +129,38 @@ func (in *LimitadorStatus) DeepCopy() *LimitadorStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Listener) DeepCopyInto(out *Listener) { + *out = *in + in.HTTP.DeepCopyInto(&out.HTTP) + in.GRPC.DeepCopyInto(&out.GRPC) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Listener. +func (in *Listener) DeepCopy() *Listener { + if in == nil { + return nil + } + out := new(Listener) + in.DeepCopyInto(out) + 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 @@ -196,6 +233,7 @@ 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. @@ -222,3 +260,23 @@ func (in *RateLimitStatus) DeepCopy() *RateLimitStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TransportProtocol) DeepCopyInto(out *TransportProtocol) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TransportProtocol. +func (in *TransportProtocol) DeepCopy() *TransportProtocol { + if in == nil { + return nil + } + out := new(TransportProtocol) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml index 9b04701b..ea9bf35c 100644 --- a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml +++ b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml @@ -34,6 +34,21 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + listener: + properties: + grpc: + properties: + port: + format: int32 + type: integer + type: object + http: + properties: + port: + format: int32 + type: integer + type: object + type: object replicas: type: integer version: @@ -41,6 +56,9 @@ spec: type: object status: description: LimitadorStatus defines the observed state of Limitador + properties: + service-url: + type: string type: object type: object served: true diff --git a/bundle/manifests/limitador.kuadrant.io_ratelimits.yaml b/bundle/manifests/limitador.kuadrant.io_ratelimits.yaml index 09e39c1e..ef1816e2 100644 --- a/bundle/manifests/limitador.kuadrant.io_ratelimits.yaml +++ b/bundle/manifests/limitador.kuadrant.io_ratelimits.yaml @@ -38,6 +38,13 @@ spec: items: type: string type: array + limitador-ref: + properties: + name: + type: string + namespace: + type: string + type: object max_value: type: integer namespace: diff --git a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml index d21f5c97..33cd02fb 100644 --- a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml +++ b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml @@ -36,6 +36,21 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + listener: + properties: + grpc: + properties: + port: + format: int32 + type: integer + type: object + http: + properties: + port: + format: int32 + type: integer + type: object + type: object replicas: type: integer version: @@ -43,6 +58,9 @@ spec: type: object status: description: LimitadorStatus defines the observed state of Limitador + properties: + service-url: + type: string type: object type: object served: true diff --git a/config/crd/bases/limitador.kuadrant.io_ratelimits.yaml b/config/crd/bases/limitador.kuadrant.io_ratelimits.yaml index 45d57b22..460df2cb 100644 --- a/config/crd/bases/limitador.kuadrant.io_ratelimits.yaml +++ b/config/crd/bases/limitador.kuadrant.io_ratelimits.yaml @@ -40,6 +40,13 @@ spec: items: type: string type: array + limitador-ref: + properties: + name: + type: string + namespace: + type: string + type: object max_value: type: integer namespace: diff --git a/config/deploy/manfiests.yaml b/config/deploy/manfiests.yaml index cf9d0478..d810ab8d 100644 --- a/config/deploy/manfiests.yaml +++ b/config/deploy/manfiests.yaml @@ -41,6 +41,21 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + listener: + properties: + grpc: + properties: + port: + format: int32 + type: integer + type: object + http: + properties: + port: + format: int32 + type: integer + type: object + type: object replicas: type: integer version: @@ -48,6 +63,9 @@ spec: type: object status: description: LimitadorStatus defines the observed state of Limitador + properties: + service-url: + type: string type: object type: object served: true @@ -101,6 +119,13 @@ spec: items: type: string type: array + limitador-ref: + properties: + name: + type: string + namespace: + type: string + type: object max_value: type: integer namespace: diff --git a/config/install/manifests.yaml b/config/install/manifests.yaml index ba62f033..87f624f3 100644 --- a/config/install/manifests.yaml +++ b/config/install/manifests.yaml @@ -34,6 +34,21 @@ spec: spec: description: LimitadorSpec defines the desired state of Limitador properties: + listener: + properties: + grpc: + properties: + port: + format: int32 + type: integer + type: object + http: + properties: + port: + format: int32 + type: integer + type: object + type: object replicas: type: integer version: @@ -41,6 +56,9 @@ spec: type: object status: description: LimitadorStatus defines the observed state of Limitador + properties: + service-url: + type: string type: object type: object served: true @@ -94,6 +112,13 @@ spec: items: type: string type: array + limitador-ref: + properties: + name: + type: string + namespace: + type: string + type: object max_value: type: integer namespace: diff --git a/controllers/limitador_controller.go b/controllers/limitador_controller.go index acb9c327..83a9236c 100644 --- a/controllers/limitador_controller.go +++ b/controllers/limitador_controller.go @@ -19,6 +19,8 @@ package controllers import ( "context" "fmt" + "github.com/kuadrant/limitador-operator/pkg/helpers" + "strconv" "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" @@ -79,6 +81,11 @@ func (r *LimitadorReconciler) Reconcile(eventCtx context.Context, req ctrl.Reque return ctrl.Result{}, err } + // Reconcile Status + if err = r.reconcileStatus(ctx, limitadorObj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } @@ -89,6 +96,26 @@ func (r *LimitadorReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } +func (r *LimitadorReconciler) reconcileStatus(ctx context.Context, limitadorObj *limitadorv1alpha1.Limitador) error { + logger := logr.FromContext(ctx) + // Simple enough now, we could implement a thorough check like with RateLimit with ObservedGeneration in the future + builtServiceUrl := buildServiceUrl(limitadorObj) + if builtServiceUrl != limitadorObj.Status.ServiceURL { + logger.V(1).Info("Updating the Status", "Name", limitadorObj.Name) + limitadorObj.Status.ServiceURL = builtServiceUrl + return r.Client().Status().Update(ctx, limitadorObj) + + } + return nil +} + +func buildServiceUrl(limitadorObj *limitadorv1alpha1.Limitador) string { + return "http://" + + limitadorObj.Name + "." + + limitadorObj.Namespace + ".svc.cluster.local:" + + strconv.Itoa(int(helpers.GetValueOrDefault(*limitadorObj.Spec.Listener.HTTP.Port, limitador.DefaultServiceHTTPPort).(int32))) +} + 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 b35e8ff4..d9167ac2 100644 --- a/controllers/limitador_controller_test.go +++ b/controllers/limitador_controller_test.go @@ -15,22 +15,29 @@ import ( "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", func() { const ( - LimitadorNamespace = "default" - LimitadorReplicas = 2 - LimitadorImage = "quay.io/3scale/limitador" - LimitadorVersion = "0.3.0" + LimitadorNamespace = "default" + LimitadorReplicas = 2 + LimitadorImage = "quay.io/3scale/limitador" + LimitadorVersion = "0.3.0" + LimitadorServiceName = "limitador-service" + LimitadorHttpPort = 8000 + LimitadorGttpPort = 8001 timeout = time.Second * 10 interval = time.Millisecond * 250 ) + httpPortNumber := int32(LimitadorHttpPort) + grpcPortNumber := int32(LimitadorGttpPort) + replicas := LimitadorReplicas version := LimitadorVersion + httpPort := limitadorv1alpha1.TransportProtocol{Port: &httpPortNumber} + grpcPort := limitadorv1alpha1.TransportProtocol{Port: &grpcPortNumber} newLimitador := func() *limitadorv1alpha1.Limitador { // The name can't start with a number. name := "a" + string(uuid.NewUUID()) @@ -47,6 +54,10 @@ var _ = Describe("Limitador controller", func() { Spec: limitadorv1alpha1.LimitadorSpec{ Replicas: &replicas, Version: &version, + Listener: &limitadorv1alpha1.Listener{ + HTTP: httpPort, + GRPC: grpcPort, + }, }, } } @@ -93,13 +104,27 @@ var _ = Describe("Limitador controller", func() { context.TODO(), types.NamespacedName{ Namespace: LimitadorNamespace, - Name: limitador.ServiceName, // Hardcoded for now + Name: limitadorObj.Name, }, &createdLimitadorService) - return err == nil }, timeout, interval).Should(BeTrue()) }) + It("Should build the correct Status", func() { + // Now checking only the ServiceURL, a more thorough test coming in the future when we have more fields + createdLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() string { + k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitadorObj.Name, + }, + &createdLimitador) + return createdLimitador.Status.ServiceURL + }, timeout, interval).Should(Equal("http://" + limitadorObj.Name + ".default.svc.cluster.local:8000")) + + }) }) Context("Updating a limitador object", func() { diff --git a/controllers/ratelimit_controller.go b/controllers/ratelimit_controller.go index 2ea2f537..c77d4207 100644 --- a/controllers/ratelimit_controller.go +++ b/controllers/ratelimit_controller.go @@ -18,12 +18,12 @@ package controllers import ( "context" - "net/url" - "strconv" - "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" @@ -34,21 +34,32 @@ import ( "github.com/kuadrant/limitador-operator/pkg/reconcilers" ) -const rateLimitFinalizer = "finalizer.ratelimit.limitador.kuadrant.io" +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(namespace string) (*url.URL, error) + URL(*limitadorv1alpha1.RateLimit) (*url.URL, error) } -type defaultLimitadorServiceDiscovery struct{} - -func (d *defaultLimitadorServiceDiscovery) URL(namespace string) (*url.URL, error) { - serviceUrl := "http://" + limitador.ServiceName + "." + namespace + ".svc.cluster.local:" + - strconv.Itoa(limitador.ServiceHTTPPort) +type defaultLimitadorServiceDiscovery struct { + client.Client +} - return url.Parse(serviceUrl) +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 @@ -60,6 +71,8 @@ type RateLimitReconciler struct { //+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. @@ -139,7 +152,7 @@ func (r *RateLimitReconciler) updateLimitPredicate() predicate.Predicate { // The namespace should be the same in the old and the new version, // so we can use either. - limitadorUrl, err := r.limitadorDiscovery().URL(newVersion.Namespace) + limitadorUrl, err := r.limitadorDiscovery().URL(newVersion) if err != nil { return false } @@ -158,7 +171,7 @@ func (r *RateLimitReconciler) updateLimitPredicate() predicate.Predicate { } func (r *RateLimitReconciler) createLimitInLimitador(limit *limitadorv1alpha1.RateLimit) error { - limitadorUrl, err := r.limitadorDiscovery().URL(limit.Namespace) + limitadorUrl, err := r.limitadorDiscovery().URL(limit) if err != nil { return err } @@ -185,7 +198,7 @@ func (r *RateLimitReconciler) ensureFinalizerIsAdded(ctx context.Context, limit } func (r *RateLimitReconciler) finalizeRateLimit(rateLimit *limitadorv1alpha1.RateLimit) error { - limitadorUrl, err := r.limitadorDiscovery().URL(rateLimit.Namespace) + limitadorUrl, err := r.limitadorDiscovery().URL(rateLimit) if err != nil { return err } diff --git a/controllers/suite_test.go b/controllers/suite_test.go index ba58c438..249ceaea 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -58,7 +58,7 @@ type TestLimitadorServiceDiscovery struct { url url.URL } -func (sd *TestLimitadorServiceDiscovery) URL(_ string) (*url.URL, error) { +func (sd *TestLimitadorServiceDiscovery) URL(_ *limitadorv1alpha1.RateLimit) (*url.URL, error) { return &sd.url, nil } diff --git a/pkg/helpers/helpers.go b/pkg/helpers/helpers.go index bd2c714e..b8ec09f1 100644 --- a/pkg/helpers/helpers.go +++ b/pkg/helpers/helpers.go @@ -52,3 +52,10 @@ func FetchEnv(key string, def string) string { return val } + +func GetValueOrDefault(expectedValue interface{}, defaultValue interface{}) interface{} { + if expectedValue != nil { + return expectedValue + } + return defaultValue +} diff --git a/pkg/limitador/k8s_objects.go b/pkg/limitador/k8s_objects.go index d2924b39..02107c72 100644 --- a/pkg/limitador/k8s_objects.go +++ b/pkg/limitador/k8s_objects.go @@ -2,6 +2,7 @@ package limitador import ( 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" @@ -9,13 +10,12 @@ import ( ) const ( - DefaultVersion = "latest" - DefaultReplicas = 1 - ServiceName = "limitador" - Image = "quay.io/3scale/limitador" - StatusEndpoint = "/status" - ServiceHTTPPort = 8080 - ServiceGRPCPort = 8081 + DefaultVersion = "latest" + DefaultReplicas = 1 + Image = "quay.io/3scale/limitador" + StatusEndpoint = "/status" + DefaultServiceHTTPPort = 8080 + DefaultServiceGRPCPort = 8081 ) func LimitadorService(limitador *limitadorv1alpha1.Limitador) *v1.Service { @@ -25,7 +25,7 @@ func LimitadorService(limitador *limitadorv1alpha1.Limitador) *v1.Service { APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: ServiceName, + Name: limitador.Name, Namespace: limitador.ObjectMeta.Namespace, // TODO: revisit later. For now assume same. Labels: labels(), OwnerReferences: []metav1.OwnerReference{ownerRefToLimitador(limitador)}, @@ -35,13 +35,13 @@ func LimitadorService(limitador *limitadorv1alpha1.Limitador) *v1.Service { { Name: "http", Protocol: v1.ProtocolTCP, - Port: ServiceHTTPPort, + Port: helpers.GetValueOrDefault(*limitador.Spec.Listener.HTTP.Port, DefaultServiceHTTPPort).(int32), TargetPort: intstr.FromString("http"), }, { Name: "grpc", Protocol: v1.ProtocolTCP, - Port: ServiceGRPCPort, + Port: helpers.GetValueOrDefault(*limitador.Spec.Listener.GRPC.Port, DefaultServiceGRPCPort).(int32), TargetPort: intstr.FromString("grpc"), }, }, @@ -91,12 +91,12 @@ func LimitadorDeployment(limitador *limitadorv1alpha1.Limitador) *appsv1.Deploym Ports: []v1.ContainerPort{ { Name: "http", - ContainerPort: ServiceHTTPPort, + ContainerPort: helpers.GetValueOrDefault(*limitador.Spec.Listener.HTTP.Port, DefaultServiceHTTPPort).(int32), Protocol: v1.ProtocolTCP, }, { Name: "grpc", - ContainerPort: ServiceGRPCPort, + ContainerPort: helpers.GetValueOrDefault(*limitador.Spec.Listener.GRPC.Port, DefaultServiceGRPCPort).(int32), Protocol: v1.ProtocolTCP, }, }, @@ -110,7 +110,7 @@ func LimitadorDeployment(limitador *limitadorv1alpha1.Limitador) *appsv1.Deploym Handler: v1.Handler{ HTTPGet: &v1.HTTPGetAction{ Path: StatusEndpoint, - Port: intstr.FromInt(ServiceHTTPPort), + Port: intstr.FromInt(int(helpers.GetValueOrDefault(*limitador.Spec.Listener.HTTP.Port, DefaultServiceHTTPPort).(int32))), Scheme: v1.URISchemeHTTP, }, }, @@ -124,7 +124,7 @@ func LimitadorDeployment(limitador *limitadorv1alpha1.Limitador) *appsv1.Deploym Handler: v1.Handler{ HTTPGet: &v1.HTTPGetAction{ Path: StatusEndpoint, - Port: intstr.FromInt(ServiceHTTPPort), + Port: intstr.FromInt(int(helpers.GetValueOrDefault(*limitador.Spec.Listener.HTTP.Port, DefaultServiceHTTPPort).(int32))), Scheme: v1.URISchemeHTTP, }, },