From 6163de4bc1a6f9a2b9043aa68df97ef21b53e20e Mon Sep 17 00:00:00 2001 From: David Ortiz Date: Tue, 5 Jan 2021 12:08:35 +0100 Subject: [PATCH 1/5] Add Limitador API Generated with "operator-sdk create-api ..." --- PROJECT | 3 + api/v1alpha1/limitador_types.go | 67 +++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 99 +++++++++++++++++++ .../limitador.3scale.net_limitadors.yaml | 58 +++++++++++ config/crd/kustomization.yaml | 3 + .../patches/cainjection_in_limitadors.yaml | 8 ++ config/crd/patches/webhook_in_limitadors.yaml | 17 ++++ config/rbac/limitador_editor_role.yaml | 24 +++++ config/rbac/limitador_viewer_role.yaml | 20 ++++ config/rbac/role.yaml | 20 ++++ config/samples/kustomization.yaml | 1 + .../samples/limitador_v1alpha1_limitador.yaml | 7 ++ controllers/limitador_controller.go | 53 ++++++++++ controllers/suite_test.go | 3 + main.go | 8 ++ 15 files changed, 391 insertions(+) create mode 100644 api/v1alpha1/limitador_types.go create mode 100644 config/crd/bases/limitador.3scale.net_limitadors.yaml create mode 100644 config/crd/patches/cainjection_in_limitadors.yaml create mode 100644 config/crd/patches/webhook_in_limitadors.yaml create mode 100644 config/rbac/limitador_editor_role.yaml create mode 100644 config/rbac/limitador_viewer_role.yaml create mode 100644 config/samples/limitador_v1alpha1_limitador.yaml create mode 100644 controllers/limitador_controller.go diff --git a/PROJECT b/PROJECT index dd2b0a83..6375621f 100644 --- a/PROJECT +++ b/PROJECT @@ -6,6 +6,9 @@ resources: - group: limitador kind: RateLimit version: v1alpha1 +- group: limitador + kind: Limitador + version: v1alpha1 version: 3-alpha plugins: go.sdk.operatorframework.io/v2-alpha: {} diff --git a/api/v1alpha1/limitador_types.go b/api/v1alpha1/limitador_types.go new file mode 100644 index 00000000..88afe7a5 --- /dev/null +++ b/api/v1alpha1/limitador_types.go @@ -0,0 +1,67 @@ +/* +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. + +// LimitadorSpec defines the desired state of Limitador +type LimitadorSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // +optional + Replicas *int `json:"replicas,omitempty"` + + // +optional + Version *string `json:"version,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 +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Limitador is the Schema for the limitadors API +type Limitador struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec LimitadorSpec `json:"spec,omitempty"` + Status LimitadorStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// LimitadorList contains a list of Limitador +type LimitadorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Limitador `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Limitador{}, &LimitadorList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 30752126..506d8774 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,105 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Limitador) DeepCopyInto(out *Limitador) { + *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 Limitador. +func (in *Limitador) DeepCopy() *Limitador { + if in == nil { + return nil + } + out := new(Limitador) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Limitador) 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 *LimitadorList) DeepCopyInto(out *LimitadorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Limitador, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitadorList. +func (in *LimitadorList) DeepCopy() *LimitadorList { + if in == nil { + return nil + } + out := new(LimitadorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LimitadorList) 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 *LimitadorSpec) DeepCopyInto(out *LimitadorSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitadorSpec. +func (in *LimitadorSpec) DeepCopy() *LimitadorSpec { + if in == nil { + return nil + } + out := new(LimitadorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LimitadorStatus) DeepCopyInto(out *LimitadorStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitadorStatus. +func (in *LimitadorStatus) DeepCopy() *LimitadorStatus { + if in == nil { + return nil + } + out := new(LimitadorStatus) + 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 diff --git a/config/crd/bases/limitador.3scale.net_limitadors.yaml b/config/crd/bases/limitador.3scale.net_limitadors.yaml new file mode 100644 index 00000000..c209ce7f --- /dev/null +++ b/config/crd/bases/limitador.3scale.net_limitadors.yaml @@ -0,0 +1,58 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.3.0 + creationTimestamp: null + name: limitadors.limitador.3scale.net +spec: + group: limitador.3scale.net + names: + kind: Limitador + listKind: LimitadorList + plural: limitadors + singular: limitador + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: Limitador is the Schema for the limitadors API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: LimitadorSpec defines the desired state of Limitador + properties: + replicas: + type: integer + version: + type: string + type: object + status: + description: LimitadorStatus defines the observed state of Limitador + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 35fe9d42..486d41bd 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,17 +3,20 @@ # It should be run by config/default resources: - bases/limitador.3scale.net_ratelimits.yaml +- bases/limitador.3scale.net_limitadors.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_ratelimits.yaml +#- patches/webhook_in_limitadors.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- patches/cainjection_in_ratelimits.yaml +#- patches/cainjection_in_limitadors.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_limitadors.yaml b/config/crd/patches/cainjection_in_limitadors.yaml new file mode 100644 index 00000000..f2a211fa --- /dev/null +++ b/config/crd/patches/cainjection_in_limitadors.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: limitadors.limitador.3scale.net diff --git a/config/crd/patches/webhook_in_limitadors.yaml b/config/crd/patches/webhook_in_limitadors.yaml new file mode 100644 index 00000000..9b29f533 --- /dev/null +++ b/config/crd/patches/webhook_in_limitadors.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: limitadors.limitador.3scale.net +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/rbac/limitador_editor_role.yaml b/config/rbac/limitador_editor_role.yaml new file mode 100644 index 00000000..37844e1c --- /dev/null +++ b/config/rbac/limitador_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit limitadors. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: limitador-editor-role +rules: +- apiGroups: + - limitador.3scale.net + resources: + - limitadors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - limitador.3scale.net + resources: + - limitadors/status + verbs: + - get diff --git a/config/rbac/limitador_viewer_role.yaml b/config/rbac/limitador_viewer_role.yaml new file mode 100644 index 00000000..29fbc02e --- /dev/null +++ b/config/rbac/limitador_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view limitadors. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: limitador-viewer-role +rules: +- apiGroups: + - limitador.3scale.net + resources: + - limitadors + verbs: + - get + - list + - watch +- apiGroups: + - limitador.3scale.net + resources: + - limitadors/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8bf51c1a..8f244f0b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -6,6 +6,26 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - limitador.3scale.net + resources: + - limitadors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - limitador.3scale.net + resources: + - limitadors/status + verbs: + - get + - patch + - update - apiGroups: - limitador.3scale.net resources: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 65c1be82..357b01c6 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,5 @@ ## Append samples you want in your CSV to this file as resources ## resources: - limitador_v1alpha1_ratelimit.yaml +- limitador_v1alpha1_limitador.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/limitador_v1alpha1_limitador.yaml b/config/samples/limitador_v1alpha1_limitador.yaml new file mode 100644 index 00000000..db5b5a33 --- /dev/null +++ b/config/samples/limitador_v1alpha1_limitador.yaml @@ -0,0 +1,7 @@ +apiVersion: limitador.3scale.net/v1alpha1 +kind: Limitador +metadata: + name: limitador-sample +spec: + replicas: 1 + version: latest diff --git a/controllers/limitador_controller.go b/controllers/limitador_controller.go new file mode 100644 index 00000000..0a87f133 --- /dev/null +++ b/controllers/limitador_controller.go @@ -0,0 +1,53 @@ +/* +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/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + limitadorv1alpha1 "github.com/3scale/limitador-operator/api/v1alpha1" +) + +// LimitadorReconciler reconciles a Limitador object +type LimitadorReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=limitador.3scale.net,resources=limitadors,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=limitador.3scale.net,resources=limitadors/status,verbs=get;update;patch + +func (r *LimitadorReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + _ = context.Background() + _ = r.Log.WithValues("limitador", req.NamespacedName) + + // your logic here + + return ctrl.Result{}, nil +} + +func (r *LimitadorReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&limitadorv1alpha1.Limitador{}). + Complete(r) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 898305b3..4cd36045 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -65,6 +65,9 @@ var _ = BeforeSuite(func(done Done) { err = limitadorv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = limitadorv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/main.go b/main.go index aa203bf2..c5f0978e 100644 --- a/main.go +++ b/main.go @@ -75,6 +75,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "RateLimit") os.Exit(1) } + if err = (&controllers.LimitadorReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Limitador"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Limitador") + os.Exit(1) + } // +kubebuilder:scaffold:builder setupLog.Info("starting manager") From 7886cee6d0a827a3d4b9a66941c29e823facf459 Mon Sep 17 00:00:00 2001 From: David Ortiz Date: Thu, 7 Jan 2021 19:12:12 +0100 Subject: [PATCH 2/5] go.mod: add k8s.io/api --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index f66ec45b..ad4d1622 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-logr/logr v0.1.0 github.com/onsi/ginkgo v1.12.1 github.com/onsi/gomega v1.10.1 + k8s.io/api v0.18.6 k8s.io/apimachinery v0.18.6 k8s.io/client-go v0.18.6 sigs.k8s.io/controller-runtime v0.6.3 From e7c5a1d134bafdfaf7674cfb8a8a93094323ca0e Mon Sep 17 00:00:00 2001 From: David Ortiz Date: Thu, 7 Jan 2021 19:14:26 +0100 Subject: [PATCH 3/5] Add Limitador Service and Deployment generators --- pkg/limitador/k8s_objects.go | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 pkg/limitador/k8s_objects.go diff --git a/pkg/limitador/k8s_objects.go b/pkg/limitador/k8s_objects.go new file mode 100644 index 00000000..2529278d --- /dev/null +++ b/pkg/limitador/k8s_objects.go @@ -0,0 +1,145 @@ +package limitador + +import ( + limitadorv1alpha1 "github.com/3scale/limitador-operator/api/v1alpha1" + 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" +) + +const ( + DefaultVersion = "latest" + DefaultReplicas = 1 + ServiceName = "limitador" + ServiceNamespace = "default" + Image = "quay.io/3scale/limitador" + StatusEndpoint = "/status" +) + +func LimitadorService() *v1.Service { + return &v1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: ServiceNamespace, + Labels: labels(), + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "http", + Protocol: v1.ProtocolTCP, + Port: 8080, + TargetPort: intstr.FromString("http"), + }, + { + Name: "grpc", + Protocol: v1.ProtocolTCP, + Port: 8081, + TargetPort: intstr.FromString("grpc"), + }, + }, + Selector: labels(), + ClusterIP: v1.ClusterIPNone, + Type: v1.ServiceTypeClusterIP, + }, + } +} + +func LimitadorDeployment(limitador *limitadorv1alpha1.Limitador) *appsv1.Deployment { + var replicas int32 = DefaultReplicas + if limitador.Spec.Replicas != nil { + replicas = int32(*limitador.Spec.Replicas) + } + + version := DefaultVersion + if limitador.Spec.Version != nil { + version = *limitador.Spec.Version + } + + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: limitador.ObjectMeta.Name, // TODO: revisit later. For now assume same. + Namespace: limitador.ObjectMeta.Namespace, // TODO: revisit later. For now assume same. + Labels: labels(), + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels(), + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels(), + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "limitador", + Image: Image + ":" + version, + Ports: []v1.ContainerPort{ + { + Name: "http", + ContainerPort: 8080, + Protocol: v1.ProtocolTCP, + }, + { + Name: "grpc", + ContainerPort: 8081, + Protocol: v1.ProtocolTCP, + }, + }, + Env: []v1.EnvVar{ + { + Name: "RUST_LOG", + Value: "info", + }, + }, + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + HTTPGet: &v1.HTTPGetAction{ + Path: StatusEndpoint, + Port: intstr.FromInt(8080), + Scheme: v1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: 5, + TimeoutSeconds: 2, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, + }, + ReadinessProbe: &v1.Probe{ + Handler: v1.Handler{ + HTTPGet: &v1.HTTPGetAction{ + Path: StatusEndpoint, + Port: intstr.FromInt(8080), + Scheme: v1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: 5, + TimeoutSeconds: 5, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, + }, + ImagePullPolicy: v1.PullIfNotPresent, + }, + }, + }, + }, + }, + } +} + +func labels() map[string]string { + return map[string]string{"app": "limitador"} +} From 291afb8e771878638bf4ac5decc147f13bc0f5cf Mon Sep 17 00:00:00 2001 From: David Ortiz Date: Thu, 7 Jan 2021 19:21:46 +0100 Subject: [PATCH 4/5] Add first version of Limitador reconciler --- controllers/limitador_controller.go | 118 +++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 4 deletions(-) diff --git a/controllers/limitador_controller.go b/controllers/limitador_controller.go index 0a87f133..2eb4e282 100644 --- a/controllers/limitador_controller.go +++ b/controllers/limitador_controller.go @@ -18,9 +18,13 @@ package controllers import ( "context" - + "github.com/3scale/limitador-operator/pkg/limitador" "github.com/go-logr/logr" + v1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,10 +42,39 @@ type LimitadorReconciler struct { // +kubebuilder:rbac:groups=limitador.3scale.net,resources=limitadors/status,verbs=get;update;patch func (r *LimitadorReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - _ = context.Background() - _ = r.Log.WithValues("limitador", req.NamespacedName) + reqLogger := r.Log.WithValues("limitador", req.NamespacedName) + + // Delete Limitador deployment and service if needed + limitadorObj := limitadorv1alpha1.Limitador{} + if err := r.Get(context.TODO(), req.NamespacedName, &limitadorObj); err != nil { + if errors.IsNotFound(err) { + if err = r.ensureLimitadorDeploymentIsDeleted(req.NamespacedName); err != nil { + reqLogger.Error(err, "Failed to delete Limitador deployment.") + return ctrl.Result{}, err + } + + if err = r.ensureLimitadorServiceIsDeleted(); err != nil { + reqLogger.Error(err, "Failed to delete Limitador service.") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil + } else { + reqLogger.Error(err, "Failed to get Limitador object.") + return ctrl.Result{}, err + } + } - // your logic here + if err := r.ensureLimitadorServiceExists(); err != nil { + return ctrl.Result{}, err + } + + desiredDeployment := limitador.LimitadorDeployment(&limitadorObj) + + if err := r.reconcileDeployment(desiredDeployment); err != nil { + reqLogger.Error(err, "Failed to update Limitador deployment.") + return ctrl.Result{}, err + } return ctrl.Result{}, nil } @@ -51,3 +84,80 @@ func (r *LimitadorReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&limitadorv1alpha1.Limitador{}). Complete(r) } + +func (r *LimitadorReconciler) reconcileDeployment(desiredDeployment *v1.Deployment) error { + currentDeployment := v1.Deployment{} + key, _ := client.ObjectKeyFromObject(desiredDeployment) + + err := r.Get(context.TODO(), key, ¤tDeployment) + if err != nil { + if errors.IsNotFound(err) { + return r.Create(context.TODO(), desiredDeployment) + } else { + return err + } + } + + updated := false + + if currentDeployment.Spec.Replicas != desiredDeployment.Spec.Replicas { + currentDeployment.Spec.Replicas = desiredDeployment.Spec.Replicas + updated = true + } + + if currentDeployment.Spec.Template.Spec.Containers[0].Image != + desiredDeployment.Spec.Template.Spec.Containers[0].Image { + currentDeployment.Spec.Template.Spec.Containers[0].Image = + desiredDeployment.Spec.Template.Spec.Containers[0].Image + updated = true + } + + if updated { + return r.Update(context.TODO(), ¤tDeployment) + } else { + return nil + } +} + +func (r *LimitadorReconciler) ensureLimitadorDeploymentIsDeleted(name types.NamespacedName) error { + currentLimitadorDeployment := v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name.Name, + Namespace: name.Namespace, + }, + } + + err := r.Delete(context.TODO(), ¤tLimitadorDeployment) + + if err != nil && !errors.IsNotFound(err) { + return err + } + + return nil +} + +func (r *LimitadorReconciler) ensureLimitadorServiceExists() error { + limitadorService := limitador.LimitadorService() + limitadorServiceKey, _ := client.ObjectKeyFromObject(limitadorService) + + err := r.Get(context.TODO(), limitadorServiceKey, limitadorService) + if err != nil { + if errors.IsNotFound(err) { + return r.Create(context.TODO(), limitadorService) + } else { + return err + } + } + + return nil +} + +func (r *LimitadorReconciler) ensureLimitadorServiceIsDeleted() error { + err := r.Delete(context.TODO(), limitador.LimitadorService()) + + if err != nil && !errors.IsNotFound(err) { + return err + } + + return nil +} From b9e0f7dc81b84f12739f54bb408428ea7460bf0d Mon Sep 17 00:00:00 2001 From: David Ortiz Date: Thu, 7 Jan 2021 16:14:55 +0100 Subject: [PATCH 5/5] Test limitador reconciler --- controllers/suite_test.go | 195 +++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 3 deletions(-) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 4cd36045..7e42e414 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -17,8 +17,16 @@ limitations under the License. package controllers import ( + "context" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "path/filepath" + ctrl "sigs.k8s.io/controller-runtime" "testing" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -65,15 +73,31 @@ var _ = BeforeSuite(func(done Done) { err = limitadorv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) - err = limitadorv1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).ToNot(HaveOccurred()) Expect(k8sClient).ToNot(BeNil()) + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + err = (&LimitadorReconciler{ + Client: k8sManager.GetClient(), + Log: ctrl.Log.WithName("limitador"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + k8sClient = k8sManager.GetClient() + Expect(k8sClient).ToNot(BeNil()) + close(done) }, 60) @@ -82,3 +106,168 @@ var _ = AfterSuite(func() { err := testEnv.Stop() Expect(err).ToNot(HaveOccurred()) }) + +var _ = Describe("Limitador controller", func() { + const ( + LimitadorName = "limitador-test" + LimitadorNamespace = "default" + LimitadorReplicas = 2 + LimitadorImage = "quay.io/3scale/limitador" + LimitadorVersion = "0.3.0" + + timeout = time.Second * 10 + interval = time.Millisecond * 250 + ) + + replicas := LimitadorReplicas + version := LimitadorVersion + limitador := limitadorv1alpha1.Limitador{ + TypeMeta: metav1.TypeMeta{ + Kind: "Limitador", + APIVersion: "limitador.3scale.net/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: LimitadorName, + Namespace: LimitadorNamespace, + }, + Spec: limitadorv1alpha1.LimitadorSpec{ + Replicas: &replicas, + Version: &version, + }, + } + + Context("Creating a new Limitador object", func() { + BeforeEach(func() { + err := k8sClient.Delete(context.TODO(), limitador.DeepCopy()) + Expect(err == nil || errors.IsNotFound(err)) + + Expect(k8sClient.Create(context.TODO(), limitador.DeepCopy())).Should(Succeed()) + }) + + It("Should create a new deployment with the right number of replicas and version", func() { + createdLimitadorDeployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: LimitadorName, + }, + &createdLimitadorDeployment) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(*createdLimitadorDeployment.Spec.Replicas).Should( + Equal((int32)(LimitadorReplicas)), + ) + Expect(createdLimitadorDeployment.Spec.Template.Spec.Containers[0].Image).Should( + Equal(LimitadorImage + ":" + LimitadorVersion), + ) + }) + + It("Should create a Limitador service", func() { + createdLimitadorService := v1.Service{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: "default", // Hardcoded for now + Name: "limitador", // Hardcoded for now + }, + &createdLimitadorService) + + return err == nil + }, timeout, interval).Should(BeTrue()) + }) + }) + + Context("Deleting a Limitador object", func() { + BeforeEach(func() { + err := k8sClient.Create(context.TODO(), limitador.DeepCopy()) + Expect(err == nil || errors.IsAlreadyExists(err)) + + Expect(k8sClient.Delete(context.TODO(), limitador.DeepCopy())).Should(Succeed()) + }) + + It("Should delete the limitador deployment", func() { + createdLimitadorDeployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: LimitadorName, + }, + &createdLimitadorDeployment) + + return errors.IsNotFound(err) + }, timeout, interval).Should(BeTrue()) + }) + + It("Should delete the limitador service", func() { + createdLimitadorService := v1.Service{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: "default", // Hardcoded for now + Name: "limitador", // Hardcoded for now + }, + &createdLimitadorService) + + return errors.IsNotFound(err) + }, timeout, interval).Should(BeTrue()) + }) + }) + + Context("Updating a limitador object", func() { + BeforeEach(func() { + err := k8sClient.Delete(context.TODO(), limitador.DeepCopy()) + Expect(err == nil || errors.IsNotFound(err)) + + Expect(k8sClient.Create(context.TODO(), limitador.DeepCopy())).Should(Succeed()) + }) + + It("Should modify the limitador deployment", func() { + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: LimitadorName, + }, + &updatedLimitador) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + replicas = LimitadorReplicas + 1 + updatedLimitador.Spec.Replicas = &replicas + version = "latest" + updatedLimitador.Spec.Version = &version + + Expect(k8sClient.Update(context.TODO(), &updatedLimitador)).Should(Succeed()) + updatedLimitadorDeployment := appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: LimitadorName, + }, + &updatedLimitadorDeployment) + + if err != nil { + return false + } + + correctReplicas := *updatedLimitadorDeployment.Spec.Replicas == LimitadorReplicas+1 + correctImage := updatedLimitadorDeployment.Spec.Template.Spec.Containers[0].Image == LimitadorImage+":latest" + + return correctReplicas && correctImage + }, timeout, interval).Should(BeTrue()) + }) + }) +})