diff --git a/PROJECT b/PROJECT index 82eec1a0c..069115752 100644 --- a/PROJECT +++ b/PROJECT @@ -34,8 +34,8 @@ resources: namespaced: true controller: true domain: kyma-project.io - group: ratelimit + group: gateway kind: RateLimit - path: github.com/kyma-project/api-gateway/apis/ratelimit/v1alpha1 + path: github.com/kyma-project/api-gateway/apis/gateway/ratelimit/v1alpha1 version: v1alpha1 version: "3" diff --git a/apis/ratelimit/v1alpha1/groupversion_info.go b/apis/gateway/ratelimit/v1alpha1/groupversion_info.go similarity index 89% rename from apis/ratelimit/v1alpha1/groupversion_info.go rename to apis/gateway/ratelimit/v1alpha1/groupversion_info.go index e8407259b..20d57d3f8 100644 --- a/apis/ratelimit/v1alpha1/groupversion_info.go +++ b/apis/gateway/ratelimit/v1alpha1/groupversion_info.go @@ -16,7 +16,7 @@ limitations under the License. // Package v1alpha1 contains API Schema definitions for the ratelimit v1alpha1 API group // +kubebuilder:object:generate=true -// +groupName=ratelimit.kyma-project.io +// +groupName=gateway.kyma-project.io package v1alpha1 import ( @@ -26,7 +26,7 @@ import ( var ( // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "ratelimit.kyma-project.io", Version: "v1alpha1"} + GroupVersion = schema.GroupVersion{Group: "gateway.kyma-project.io", Version: "v1alpha1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} diff --git a/apis/ratelimit/v1alpha1/ratelimit_types.go b/apis/gateway/ratelimit/v1alpha1/ratelimit_types.go similarity index 53% rename from apis/ratelimit/v1alpha1/ratelimit_types.go rename to apis/gateway/ratelimit/v1alpha1/ratelimit_types.go index 2352d611b..a801d8d16 100644 --- a/apis/ratelimit/v1alpha1/ratelimit_types.go +++ b/apis/gateway/ratelimit/v1alpha1/ratelimit_types.go @@ -20,16 +20,42 @@ 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. +// Bucket represents a rate limit bucket configuration. +// +kubebuilder:validation:XValidation:rule="((has(self.path)?1:0)+(has(self.headers)?1:0))==1",message="path or headers must be set" +type Bucket struct { + Path string `json:"path,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + // +kubebuilder:validation:Required + DefaultBucket BucketTokenSpec `json:"bucket"` +} + +// BucketTokenSpec defines the token bucket specification. +type BucketTokenSpec struct { + // +kubebuilder:validation:Required + MaxTokens int64 `json:"maxTokens"` + // +kubebuilder:validation:Required + TokensPerFill int64 `json:"tokensPerFill"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:Format=duration + FillInterval *metav1.Duration `json:"fillInterval"` +} + +// Local represents the local rate limit configuration. +type Local struct { + // +kubebuilder:validation:Required + DefaultBucket BucketTokenSpec `json:"defaultBucket"` + Buckets []Bucket `json:"buckets,omitempty"` +} // 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 - - // Foo is an example field of RateLimit. Edit ratelimit_types.go to remove/update - Foo string `json:"foo,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinProperties=1 + SelectorLabels map[string]string `json:"selectorLabels"` + // +kubebuilder:validation:Required + Local Local `json:"local"` + EnableResponseHeaders bool `json:"enableResponseHeaders,omitempty"` + Enforce bool `json:"enforce,omitempty"` } // RateLimitStatus defines the observed state of RateLimit diff --git a/apis/ratelimit/v1alpha1/zz_generated.deepcopy.go b/apis/gateway/ratelimit/v1alpha1/zz_generated.deepcopy.go similarity index 60% rename from apis/ratelimit/v1alpha1/zz_generated.deepcopy.go rename to apis/gateway/ratelimit/v1alpha1/zz_generated.deepcopy.go index b915f6b35..a91ce83af 100644 --- a/apis/ratelimit/v1alpha1/zz_generated.deepcopy.go +++ b/apis/gateway/ratelimit/v1alpha1/zz_generated.deepcopy.go @@ -21,15 +21,82 @@ limitations under the License. package v1alpha1 import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" 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 *Bucket) DeepCopyInto(out *Bucket) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.DefaultBucket.DeepCopyInto(&out.DefaultBucket) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bucket. +func (in *Bucket) DeepCopy() *Bucket { + if in == nil { + return nil + } + out := new(Bucket) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BucketTokenSpec) DeepCopyInto(out *BucketTokenSpec) { + *out = *in + if in.FillInterval != nil { + in, out := &in.FillInterval, &out.FillInterval + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BucketTokenSpec. +func (in *BucketTokenSpec) DeepCopy() *BucketTokenSpec { + if in == nil { + return nil + } + out := new(BucketTokenSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Local) DeepCopyInto(out *Local) { + *out = *in + in.DefaultBucket.DeepCopyInto(&out.DefaultBucket) + if in.Buckets != nil { + in, out := &in.Buckets, &out.Buckets + *out = make([]Bucket, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Local. +func (in *Local) DeepCopy() *Local { + if in == nil { + return nil + } + out := new(Local) + 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) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -86,6 +153,14 @@ func (in *RateLimitList) DeepCopyObject() runtime.Object { // 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.SelectorLabels != nil { + in, out := &in.SelectorLabels, &out.SelectorLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Local.DeepCopyInto(&out.Local) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateLimitSpec. diff --git a/config/crd/bases/gateway.kyma-project.io_ratelimits.yaml b/config/crd/bases/gateway.kyma-project.io_ratelimits.yaml new file mode 100644 index 000000000..c9e861a5b --- /dev/null +++ b/config/crd/bases/gateway.kyma-project.io_ratelimits.yaml @@ -0,0 +1,119 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: ratelimits.gateway.kyma-project.io +spec: + group: gateway.kyma-project.io + names: + kind: RateLimit + listKind: RateLimitList + plural: ratelimits + singular: ratelimit + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: RateLimit is the Schema for the ratelimits 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: RateLimitSpec defines the desired state of RateLimit + properties: + enableResponseHeaders: + type: boolean + enforce: + type: boolean + local: + description: Local represents the local rate limit configuration. + properties: + buckets: + items: + description: Bucket represents a rate limit bucket configuration. + properties: + bucket: + description: BucketTokenSpec defines the token bucket specification. + properties: + fillInterval: + format: duration + type: string + maxTokens: + format: int64 + type: integer + tokensPerFill: + format: int64 + type: integer + required: + - fillInterval + - maxTokens + - tokensPerFill + type: object + headers: + additionalProperties: + type: string + type: object + path: + type: string + required: + - bucket + type: object + x-kubernetes-validations: + - message: path or headers must be set + rule: ((has(self.path)?1:0)+(has(self.headers)?1:0))==1 + type: array + defaultBucket: + description: BucketTokenSpec defines the token bucket specification. + properties: + fillInterval: + format: duration + type: string + maxTokens: + format: int64 + type: integer + tokensPerFill: + format: int64 + type: integer + required: + - fillInterval + - maxTokens + - tokensPerFill + type: object + required: + - defaultBucket + type: object + selectorLabels: + additionalProperties: + type: string + minProperties: 1 + type: object + required: + - local + - selectorLabels + type: object + status: + description: RateLimitStatus defines the observed state of RateLimit + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/ratelimit.kyma-project.io_ratelimits.yaml b/config/crd/bases/ratelimit.kyma-project.io_ratelimits.yaml deleted file mode 100644 index 12460e164..000000000 --- a/config/crd/bases/ratelimit.kyma-project.io_ratelimits.yaml +++ /dev/null @@ -1,54 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.14.0 - name: ratelimits.ratelimit.kyma-project.io -spec: - group: ratelimit.kyma-project.io - names: - kind: RateLimit - listKind: RateLimitList - plural: ratelimits - singular: ratelimit - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: RateLimit is the Schema for the ratelimits 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: RateLimitSpec defines the desired state of RateLimit - properties: - foo: - description: Foo is an example field of RateLimit. Edit ratelimit_types.go - to remove/update - type: string - type: object - status: - description: RateLimitStatus defines the observed state of RateLimit - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index d0e4f98e7..6d73d3b09 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -4,7 +4,7 @@ resources: - bases/gateway.kyma-project.io_apirules.yaml - bases/operator.kyma-project.io_apigateways.yaml -- bases/ratelimit.kyma-project.io_ratelimits.yaml +- bases/gateway.kyma-project.io_ratelimits.yaml #+kubebuilder:scaffold:crdkustomizeresource labels: diff --git a/config/dev/kustomization.yaml b/config/dev/kustomization.yaml index 3434b990b..bca3dc825 100644 --- a/config/dev/kustomization.yaml +++ b/config/dev/kustomization.yaml @@ -10,7 +10,7 @@ patches: path: /rules/- value: apiGroups: - - ratelimit.kyma-project.io + - gateway.kyma-project.io resources: - ratelimits verbs: @@ -31,7 +31,7 @@ patches: path: /rules/- value: apiGroups: - - ratelimit.kyma-project.io + - gateway.kyma-project.io resources: - ratelimits/finalizers verbs: @@ -46,7 +46,7 @@ patches: path: /rules/- value: apiGroups: - - ratelimit.kyma-project.io + - gateway.kyma-project.io resources: - ratelimits/status verbs: diff --git a/config/prod/kustomization.yaml b/config/prod/kustomization.yaml index d7a88a225..2774491fd 100644 --- a/config/prod/kustomization.yaml +++ b/config/prod/kustomization.yaml @@ -10,5 +10,5 @@ patchesStrategicMerge: apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: ratelimits.ratelimit.kyma-project.io + name: ratelimits.gateway.kyma-project.io $patch: delete diff --git a/config/samples/ratelimit_v1alpha1_ratelimit.yaml b/config/samples/gateway_v1alpha1_ratelimit.yaml similarity index 85% rename from config/samples/ratelimit_v1alpha1_ratelimit.yaml rename to config/samples/gateway_v1alpha1_ratelimit.yaml index 9d4b37609..66f30adda 100644 --- a/config/samples/ratelimit_v1alpha1_ratelimit.yaml +++ b/config/samples/gateway_v1alpha1_ratelimit.yaml @@ -1,4 +1,4 @@ -apiVersion: ratelimit.kyma-project.io/v1alpha1 +apiVersion: gateway.kyma-project.io/v1alpha1 kind: RateLimit metadata: labels: @@ -9,4 +9,3 @@ metadata: app.kubernetes.io/created-by: api-gateway name: ratelimit-sample spec: - diff --git a/controllers/ratelimit/ratelimit_controller.go b/controllers/gateway/ratelimit/ratelimit_controller.go similarity index 51% rename from controllers/ratelimit/ratelimit_controller.go rename to controllers/gateway/ratelimit/ratelimit_controller.go index aa1e2eba5..06de65177 100644 --- a/controllers/ratelimit/ratelimit_controller.go +++ b/controllers/gateway/ratelimit/ratelimit_controller.go @@ -18,15 +18,19 @@ package ratelimit import ( "context" - ratelimitv1alpha1 "github.com/kyma-project/api-gateway/apis/ratelimit/v1alpha1" + ratelimitv1alpha1 "github.com/kyma-project/api-gateway/apis/gateway/ratelimit/v1alpha1" + "github.com/kyma-project/api-gateway/internal/ratelimit" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + "time" ) -// Reconciler reconciles a RateLimit object -type Reconciler struct { +const defaultReconciliationPeriod = 30 * time.Minute + +// RateLimitReconciler reconciles a RateLimit object +type RateLimitReconciler struct { client.Client Scheme *runtime.Scheme } @@ -35,22 +39,30 @@ type Reconciler struct { // kustomize. The roles are managed in the file config/dev/kustomization.yaml. Once this feature is ready for release, // the markers can be added again. -// Reconcile is part of the main kubernetes reconciliation loop which aims to +// Reconcile is part of the main Kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// 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.14.1/pkg/reconcile -func (r *Reconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - - return ctrl.Result{}, nil +// In this function, the RateLimit object is fetched and validated. +// If the object is not found, it is ignored. If validation fails, an error is returned. +// Otherwise, the function returns a result with a requeue period. +func (r *RateLimitReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx).WithValues("namespace", req.Namespace, "RateLimit", req.Name) + l.Info("Starting reconciliation") + + rateLimit := ratelimitv1alpha1.RateLimit{} + if err := r.Get(ctx, req.NamespacedName, &rateLimit); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + err := ratelimit.Validate(ctx, r.Client, rateLimit) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{RequeueAfter: defaultReconciliationPeriod}, nil } // SetupWithManager sets up the controller with the Manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *RateLimitReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&ratelimitv1alpha1.RateLimit{}). Complete(r) diff --git a/controllers/gateway/ratelimit/ratelimit_controller_test.go b/controllers/gateway/ratelimit/ratelimit_controller_test.go new file mode 100644 index 000000000..866ac5335 --- /dev/null +++ b/controllers/gateway/ratelimit/ratelimit_controller_test.go @@ -0,0 +1,50 @@ +package ratelimit_test + +import ( + "context" + ratelimitv1alpha1 "github.com/kyma-project/api-gateway/apis/gateway/ratelimit/v1alpha1" + "github.com/kyma-project/api-gateway/controllers/gateway/ratelimit" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "testing" +) + +var _ = Describe("Rate Limit Controller", func() { + It("Finish reconciliation if there is no RateLimit CR in the cluster", func() { + + fakeClient := fake.NewClientBuilder().WithScheme(getTestScheme()).WithObjects().Build() + + r := ratelimit.RateLimitReconciler{ + Scheme: getTestScheme(), + Client: fakeClient, + } + + result, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "test", Name: "test"}}) + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).Should(Equal(reconcile.Result{})) + }) + +}) + +func getTestScheme() *runtime.Scheme { + s := runtime.NewScheme() + utilruntime.Must(ratelimitv1alpha1.AddToScheme(s)) + Expect(corev1.AddToScheme(s)).Should(Succeed()) + Expect(apiextensionsv1.AddToScheme(s)).Should(Succeed()) + + return s +} + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "RateLimit Controller Suite") +} diff --git a/controllers/ratelimit/feature_disabled.go b/controllers/gateway/ratelimit/ratelimit_disabled.go similarity index 100% rename from controllers/ratelimit/feature_disabled.go rename to controllers/gateway/ratelimit/ratelimit_disabled.go diff --git a/controllers/ratelimit/feature_enabled.go b/controllers/gateway/ratelimit/ratelimit_enabled.go similarity index 76% rename from controllers/ratelimit/feature_enabled.go rename to controllers/gateway/ratelimit/ratelimit_enabled.go index 565957eec..bccd23e63 100644 --- a/controllers/ratelimit/feature_enabled.go +++ b/controllers/gateway/ratelimit/ratelimit_enabled.go @@ -3,7 +3,7 @@ package ratelimit import ( - ratelimitv1alpha1 "github.com/kyma-project/api-gateway/apis/ratelimit/v1alpha1" + ratelimitv1alpha1 "github.com/kyma-project/api-gateway/apis/gateway/ratelimit/v1alpha1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -11,7 +11,7 @@ import ( func Setup(mgr manager.Manager, scheme *runtime.Scheme) error { utilruntime.Must(ratelimitv1alpha1.AddToScheme(scheme)) - return (&Reconciler{ + return (&RateLimitReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr) diff --git a/controllers/gateway/suite_test.go b/controllers/gateway/suite_test.go index 2b31d1738..f219f6aa3 100644 --- a/controllers/gateway/suite_test.go +++ b/controllers/gateway/suite_test.go @@ -10,13 +10,14 @@ import ( "testing" "time" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + gatewayv1beta1 "github.com/kyma-project/api-gateway/apis/gateway/v1beta1" gatewayv2alpha1 "github.com/kyma-project/api-gateway/apis/gateway/v2alpha1" "github.com/kyma-project/api-gateway/controllers" "github.com/kyma-project/api-gateway/controllers/gateway" "github.com/kyma-project/api-gateway/internal/builders" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" rulev1alpha1 "github.com/ory/oathkeeper-maester/api/v1alpha1" "istio.io/api/networking/v1beta1" @@ -27,12 +28,13 @@ import ( "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" - "github.com/kyma-project/api-gateway/internal/helpers" . "github.com/onsi/ginkgo/v2" "github.com/onsi/ginkgo/v2/reporters" "github.com/onsi/ginkgo/v2/types" . "github.com/onsi/gomega" + "github.com/kyma-project/api-gateway/internal/helpers" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" diff --git a/controllers/ratelimit/ratelimit_controller_test.go b/controllers/ratelimit/ratelimit_controller_test.go deleted file mode 100644 index c5afcb005..000000000 --- a/controllers/ratelimit/ratelimit_controller_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package ratelimit_test - -import ( - "context" - "github.com/kyma-project/api-gateway/controllers/ratelimit" - "k8s.io/apimachinery/pkg/runtime" - - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Rate Limit Controller", func() { - It("Dummy test", func() { - - r := ratelimit.Reconciler{ - Scheme: runtime.NewScheme(), - } - - result, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "test", Name: "test"}}) - - Expect(err).ShouldNot(HaveOccurred()) - Expect(result).Should(Equal(reconcile.Result{ - Requeue: false, - })) - }) - -}) diff --git a/controllers/ratelimit/suite_test.go b/controllers/ratelimit/suite_test.go deleted file mode 100644 index cc1bc24b8..000000000 --- a/controllers/ratelimit/suite_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package ratelimit_test - -import ( - "github.com/kyma-project/api-gateway/tests" - . "github.com/onsi/ginkgo/v2" - "github.com/onsi/ginkgo/v2/types" - . "github.com/onsi/gomega" - "testing" -) - -func Test(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Ratelimit Controller Suite") -} - -var _ = ReportAfterSuite("custom reporter", func(report types.Report) { - tests.GenerateGinkgoJunitReport("ratelimit-controller-suite", report) -}) diff --git a/docs/assets/operator-contributor-skr-overview.svg b/docs/assets/operator-contributor-skr-overview.svg index fb51c1345..205e5f624 100644 --- a/docs/assets/operator-contributor-skr-overview.svg +++ b/docs/assets/operator-contributor-skr-overview.svg @@ -1,4 +1,4 @@ -
creates
creates
applies Gateway configuration
applies Gateway configuration
Gardener
Gardener
Kyma cluster
Kyma cluster
1
1
references
references
APIGateway CR
APIGateway CR
APIGateway Controller
APIGateway Controller
API Gateway Operator
API Gateway Operator
APIRule Controller
APIRule Controller
watches
watches
watches
watches
APIRule CR
APIRule CR
api-gateway-config ConfigMap
api-gateway-c...
Ory Oathkeeper
Ory Oathkeeper
reconciles
reconciles
Istio Pilot
Istio Pilot
DNS Provider Credential Secret
DNS Prov...
Gateway
Gateway
Gateway
Gateway
Istio Gateway
Istio Gateway
Ory Rule
Ory Rule
Istio VirtualService
Istio Virtua...
Istio Authorization Policy
Istio Author...
Istio Request Authentication
Istio Request...
Istio Ingress Gateway
Istio Ingress Gateway
reconciles
reconciles
reconciles
reconciles
Istio Service Proxy
Istio Servic...
User workload
User workload
Istio Service Proxy
Istio Servic...
User workload
User workload
User Workload Pod
User Workload Pod
Istio Service Proxy
Istio Service...
User workload
User workload
applies configuration
applies configuration
exposes workload endpoints
exposes workload endpoints
sends requests
to exposed API endpoints
sends requests...
reconciles
reconciles
creates
creates
Certificate Secret
Certific...
references
references
Gateway
Gateway
Gateway
Gateway
Default Kyma gateway (kyma-system/kyma-gateway)
Default Kyma...
uses
uses
uses
uses
Developer
Developer
User
User
2
2
Notes:
Notes:
In the managed Kyma, Lifecycle Manager creates the default APIGateway CR 
In the managed Kyma, Lifecycle Manager creates the default APIGateway CR 
Used conditionally if APIGateway CR doesn't define the default host
Used conditionally if APIGateway CR doesn't define the default...
1
1
2
2
Gardener DNSEntry
Gardener DNS...
Gardener Certificate
Gardener Cer...
Gardener DNSProvider
Gardener DNS...
Text is not SVG - cannot display
\ No newline at end of file +
creates
creates
applies Gateway configuration
applies Gateway configuration
Gardener
Gardener
Kyma cluster
Kyma cluster
1
1
references
references
APIGateway CR
APIGateway CR
APIGateway Controller
APIGateway Controller
API Gateway Operator
API Gateway Operator
APIRule Controller
APIRule Controller
reconciles
reconciles
RateLimit Controller
RateLimit Controller
watches
watches
watches
watches
APIRule CR
APIRule CR
api-gateway-config ConfigMap
api-gateway-c...
Ory Oathkeeper
Ory Oathkeeper
reconciles
reconciles
Istio Pilot
Istio Pilot
DNS Provider Credential Secret
DNS Prov...
Gateway
Gateway
Gateway
Gateway
Istio Gateway
Istio Gateway
Ory Rule
Ory Rule
Istio VirtualService
Istio Virtua...
Istio Authorization Policy
Istio Author...
Istio Request Authentication
Istio Request...
Istio Ingress Gateway
Istio Ingress Gateway
reconciles
reconciles
reconciles
reconciles
Istio Service Proxy
Istio Servic...
User workload
User workload
Istio Service Proxy
Istio Servic...
User workload
User workload
User Workload Pod
User Workload Pod
Istio Service Proxy
Istio Service...
User workload
User workload
applies configuration
applies configuration
exposes workload endpoints
exposes workload endpoints
sends requests
to exposed API endpoints
sends requests...
reconciles
reconciles
creates
creates
Certificate Secret
Certific...
references
references
Gateway
Gateway
Gateway
Gateway
Default Kyma gateway (kyma-system/kyma-gateway)
Default Kyma...
uses
uses
uses
uses
Developer
Developer
User
User
2
2
Notes:
Notes:
In the managed Kyma, Lifecycle Manager creates the default APIGateway CR 
In the managed Kyma, Lifecycle Manager creates the default APIGateway CR 
Used conditionally if APIGateway CR doesn't define the default host
Used conditionally if APIGateway CR doesn't define the default...
1
1
2
2
Gardener DNSEntry
Gardener DNS...
Gardener Certificate
Gardener Cer...
Gardener DNSProvider
Gardener DNS...
Istio EnvoyFilter
Istio EnvoyFi...
RateLimit CR
RateLimit CR
watches
watches
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/assets/operator-overview.svg b/docs/assets/operator-overview.svg index d4e807e30..63b8d309f 100644 --- a/docs/assets/operator-overview.svg +++ b/docs/assets/operator-overview.svg @@ -1,4 +1,4 @@ -
creates
Developer
Kyma cluster
Gateway
Kyma Istio Gateway
Gateway
Istio
Gateway
Certificates
APIGateway CR
watches
Istio Virtual Service
Istio Authorization Policy
Istio Request Authentication
reconciles
APIRule CR
Ory Oathkeeper Installation
api-gateway-config ConfigMap
Ory Rule
API Gateway Operator
1
APIGateway Controller
APIRule Controller
Note:
In the managed Kyma, Lifecycle Manager creates the default APIGateway CR 
1
watches
reconciles
Certificate Controller
Certificate Secret
reconciles
\ No newline at end of file +
creates
creates
Developer
Developer
Kyma cluster
Kyma cluster
Gateway
Gateway
Kyma Istio Gateway
Kyma Istio G...
Gateway
Gateway
Istio
Gateway
Istio...
Certificates
Certificates
APIGateway CR
APIGateway CR
watches
watches
Istio Virtual Service
Istio Virtua...
Istio Authorization Policy
Istio Author...
Istio Request Authentication
Istio Request...
reconciles
reconciles
APIRule CR
APIRule CR
Ory Oathkeeper Installation
Ory Oathkeep...
api-gateway-config ConfigMap
api-gateway-c...
Ory Rule
Ory Rule
API Gateway Operator
API Gateway Operator
1
1
APIGateway Controller
APIGateway Controller
APIRule Controller
APIRule Controller
Note:
Note:
In the managed Kyma, Lifecycle Manager creates the default APIGateway CR 
In the managed Kyma, Lifecycle Manager creates the default APIGateway CR 
1
1
watches
watches
reconciles
reconciles
Certificate Controller
Certificate Controller
Certificate Secret
Certificate...
reconciles
reconciles
reconciles
reconciles
RateLimit Controller
RateLimit Controller
watches
watches
RateLimit CR
RateLimit CR
Istio EnvoyFilter
Istio EnvoyF...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/contributor/04-10-technical-design.md b/docs/contributor/04-10-technical-design.md index b110012da..80a4f20f2 100644 --- a/docs/contributor/04-10-technical-design.md +++ b/docs/contributor/04-10-technical-design.md @@ -33,7 +33,7 @@ APIRule Controller has a conditional dependency to APIGateway Controller in term >**NOTE:** For now, you can only use the default domain in APIGateway CR. The option to configure your own domain will be added at a later time. See the [epic task](https://github.com/kyma-project/api-gateway/issues/130). ### Reconciliation -APIRule Controller reconciles APIRule CR with each change. If you don't make any changes, the process occurs at the default interval of 10 hours. +APIRule Controller reconciles APIRule CR with each change. If you don't make any changes, the process occurs at the default interval of 30 minutes. You can use the [API Gateway Operator parameters](../user/technical-reference/05-00-api-gateway-operator-parameters.md) to adjust this interval. In the event of a failure during the reconciliation, APIRule Controller performs the reconciliation again after one minute. @@ -58,3 +58,12 @@ The controller is responsible for handling the Secret `api-gateway-webhook-certi ### Reconciliation Certificate Controller reconciles a Secret CR with each change. If you don't make any changes, the process occurs at the default interval of 1 hour. This code verifies whether the Certificate is currently valid and will not expire within the next 14 days. If the Certificate does not meet these criteria, it is renewed. In the event of a failure during the reconciliation, Certificate Controller performs the reconciliation again with the predefined rate limiter. + +## RateLimit Controller + +RateLimit Controller is a [Kubernetes controller](https://kubernetes.io/docs/concepts/architecture/controller/), which is implemented using the [Kubebuilder](https://book.kubebuilder.io/) framework. +The controller is responsible for handling the [RateLimit CR](../user/custom-resources/ratelimit/04-00-ratelimit.md). + +### Reconciliation +RateLimit Controller reconciles the RateLimit CR with each change. If you don't make any changes, the process occurs at the default interval of 30 minutes. +In the event of a failure during the reconciliation, RateLimit Controller performs the reconciliation again with the predefined rate limiter. diff --git a/docs/user/custom-resources/apigateway/04-00-apigateway-custom-resource.md b/docs/user/custom-resources/apigateway/04-00-apigateway-custom-resource.md index d0fca91d0..023858a66 100644 --- a/docs/user/custom-resources/apigateway/04-00-apigateway-custom-resource.md +++ b/docs/user/custom-resources/apigateway/04-00-apigateway-custom-resource.md @@ -16,9 +16,9 @@ This table lists the parameters of the given resource together with their descri **Spec:** -| Parameter | Type | Description | -|-----------------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------| -| **enableKymaGateway** | **NO** | Specifies whether the default [Kyma Gateway](./04-10-kyma-gateway.md), named `kyma-gateway`, should be created in the `kyma-system` namespace. | +| Field | Required | Description | +|-----------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| +| **enableKymaGateway** | **NO** | Specifies whether the default [Kyma Gateway](./04-10-kyma-gateway.md), named `kyma-gateway`, should be created in the `kyma-system` namespace. | **Status:** diff --git a/docs/user/custom-resources/ratelimit/04-00-ratelimit.md b/docs/user/custom-resources/ratelimit/04-00-ratelimit.md new file mode 100644 index 000000000..f09e4da2e --- /dev/null +++ b/docs/user/custom-resources/ratelimit/04-00-ratelimit.md @@ -0,0 +1,64 @@ +# RateLimit Custom Resource + +The `ratelimits.gateway.kyma-project.io` CustomResourceDefinition (CRD) describes the kind and the format of data that +RateLimit Controller uses to configure the request rate limit for applications. + +To get the up-to-date CRD in the `yaml` format, run the following command: + +```shell +kubectl get crd ratelimits.gateway.kyma-project.io -o yaml +``` + +## Specification + +| Field | Required | Description | +|---------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **selectorLabels** | **YES** | Labels that specify the set of Pods to which the configuration applies.
Each Pod must match only one RateLimit CR.
The label scope is limited to the namespace where the resource is located. | +| **local** | **YES** | Local rate limit configuration. | +| **local.defaultBucket** | **YES** | The default token bucket for rate limiting requests.
If additional local buckets are configured in the same RateLimit CR, this bucket serves as a fallback for requests that don't match any other bucket's criteria.
Each request consumes a single token. If a token is available, the request is allowed. If no tokens are available, the request is rejected with status code `429`. | +| **local.defaultBucket.maxTokens** | **YES** | The maximum number of tokens that the bucket can hold. This is also the number of tokens that the bucket initially contains. | +| **local.defaultBucket.tokensPerFill** | **YES** | The number of tokens added to the bucket during each fill interval. | +| **local.defaultBucket.fillInterval** | **YES** | The fill interval during which tokens are added to the bucket.
During each fill interval, `tokensPerFill` are added to the bucket. The bucket will never contain more than `maxTokens` tokens. The `fillInterval` must be greater than or equal to 50ms to avoid excessive refills. | +| **local.buckets** | **NO** | Specifies a list of additional rate limit buckets for requests.
Each bucket must specify either a `path` or `headers`.
For each request matching the bucket's criteria, a single token is consumed. If a token is available, the request is allowed. If no tokens are available, the request is rejected with status code `429`. | +| **local.buckets.path** | **NO** | Specifies the path to be rate limited starting with `/`.
For example, `/foo`. | +| **local.buckets.headers** | **NO** | Specifies the request headers to be rate limited. The key is the header name, and the value is the header value. All specified headers must be present in the request for this configuration to match. For example, `x-api-usage: BASIC`. | +| **local.buckets.maxTokens** | **YES** | The maximum number of tokens that the bucket can hold. This is also the number of tokens that the bucket initially contains. | +| **local.buckets.tokensPerFill** | **YES** | The number of tokens added to the bucket during each fill interval. | +| **local.buckets.fillInterval** | **YES** | The fill interval that tokens are added to the bucket.
During each fill interval, `tokensPerFill` are added to the bucket. The bucket cannot contain more than `maxTokens` tokens. The `fillInterval` must be greater than or equal to 50ms to avoid excessive refills. | +| **enableResponseHeaders** | **NO** | Enables **x-rate-limit** response headers. The default value is `false`. | +| **enforce** | **NO** | Specifies whether the rate limit should be enforced. The default value is `true`. | + + +## Sample Custom Resource + +The following example illustrates a RateLimit CR that limits the rate of requests to the `httpbin` application in the `default` namespace. +The CR defines two local buckets: one for the `/headers` path and one for the `/ip` path. The `/headers` bucket limits only requests with the `x-api-version: v1` header. +The default bucket is used for requests that don't match any other bucket's criteria. +```yaml +apiVersion: gateway.kyma-project.io/v1alpha1 +kind: RateLimit +metadata: + name: httpbin-local-rate-limit + namespace: default +spec: + selectorLabels: + app: httpbin + local: + defaultBucket: + maxTokens: 10 + tokensPerFill: 5 + fillInterval: 30s + buckets: + - path: /headers + headers: + x-api-version: v1 + bucket: + maxTokens: 2 + tokensPerFill: 2 + fillInterval: 30s + - path: /ip + bucket: + maxTokens: 20 + tokensPerFill: 10 + fillInterval: 30s +``` \ No newline at end of file diff --git a/internal/ratelimit/validate.go b/internal/ratelimit/validate.go new file mode 100644 index 000000000..1e592412b --- /dev/null +++ b/internal/ratelimit/validate.go @@ -0,0 +1,135 @@ +package ratelimit + +import ( + "context" + "fmt" + "github.com/kyma-project/api-gateway/apis/gateway/ratelimit/v1alpha1" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" +) + +// Validate checks the validity of the given RateLimit custom resource. +// It ensures that there are pods matching the specified selector labels in the same namespace. +// If no matching pods are found, or if the sidecar injection is not enabled for the pods, +// or if there are conflicting RateLimit resources, an error is returned. +// +// Parameters: +// - ctx: The context for the validation operation. +// - k8sClient: The Kubernetes client used to interact with the cluster. +// - rl: The RateLimit custom resource to validate. +// +// Returns: +// - An error if the validation fails, otherwise nil. +func Validate(ctx context.Context, k8sClient client.Client, rl v1alpha1.RateLimit) error { + selectors := rl.Spec.SelectorLabels + + matchingPods := v1.PodList{} + err := k8sClient.List(ctx, &matchingPods, client.InNamespace(rl.Namespace), client.MatchingLabels(selectors)) + if err != nil { + return err + } + + if len(matchingPods.Items) == 0 { + // in case there is no pods matching for the given selectors declared in the RateLimit CR + // we want to set the RateLimit CR to the warning state, therefore we fail validation returning an error + return fmt.Errorf("no pods found with the given selectors: %v in namespace %s", selectors, rl.Namespace) + } + + if !isIngressGateway(matchingPods.Items) { + err = validateSidecarInjectionEnabled(matchingPods.Items) + if err != nil { + return err + } + } + + err = validateConflicts(ctx, k8sClient, rl, matchingPods) + if err != nil { + return err + } + + return nil +} + +func validateConflicts(ctx context.Context, k8sClient client.Client, rl v1alpha1.RateLimit, matchingPods v1.PodList) error { + otherRateLimitsInTheNamespace := v1alpha1.RateLimitList{} + err := k8sClient.List(ctx, &otherRateLimitsInTheNamespace, client.InNamespace(rl.Namespace)) + if err != nil { + return err + } + + podMap := map[string]v1.Pod{} + var conflictingRateLimits []v1alpha1.RateLimit + + for _, pod := range matchingPods.Items { + podMap[pod.Name] = pod + } + + for _, otherRL := range otherRateLimitsInTheNamespace.Items { + if otherRL.Name == rl.Name { + continue + } + + otherRLSelectors := otherRL.Spec.SelectorLabels + otherRLMatchingPods := v1.PodList{} + err := k8sClient.List(ctx, &otherRLMatchingPods, client.InNamespace(rl.Namespace), client.MatchingLabels(otherRLSelectors)) + if err != nil { + return err + } + + for _, pod := range otherRLMatchingPods.Items { + if _, ok := podMap[pod.Name]; ok { + conflictingRateLimits = append(conflictingRateLimits, otherRL) + break + } + } + } + + if len(conflictingRateLimits) > 0 { + return conflictError(conflictingRateLimits) + } + return nil +} + +func validateSidecarInjectionEnabled(podList []v1.Pod) error { + var nonInjectedPods []v1.Pod + for _, pod := range podList { + _, ok := pod.Annotations["sidecar.istio.io/status"] + if !ok { + nonInjectedPods = append(nonInjectedPods, pod) + } + } + + if len(nonInjectedPods) > 0 { + return sidecarInjectionError(nonInjectedPods) + } + + return nil +} + +func isIngressGateway(pods []v1.Pod) bool { + for _, p := range pods { + v, ok := p.Labels["istio"] + if !ok || v != "ingressgateway" { + return false + } + } + return true +} + +func sidecarInjectionError(nonCompliantPods []v1.Pod) error { + var invalidPodsMessages []string + for _, pod := range nonCompliantPods { + msg := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name) + invalidPodsMessages = append(invalidPodsMessages, msg) + } + return fmt.Errorf("sidecar injection is not enabled for the following pods: %s", strings.Join(invalidPodsMessages, ", ")) +} + +func conflictError(conflictingRateLimits []v1alpha1.RateLimit) error { + var conflictingRateLimitsMessages []string + for _, rl := range conflictingRateLimits { + conflictingRateLimitsMessages = append(conflictingRateLimitsMessages, rl.Name) + } + return fmt.Errorf("conflicting with the following RateLimit CRs: %s", strings.Join(conflictingRateLimitsMessages, ", ")) +} diff --git a/internal/ratelimit/validate_test.go b/internal/ratelimit/validate_test.go new file mode 100644 index 000000000..33b58b7fb --- /dev/null +++ b/internal/ratelimit/validate_test.go @@ -0,0 +1,306 @@ +package ratelimit_test + +import ( + "context" + "fmt" + ratelimitv1alpha1 "github.com/kyma-project/api-gateway/apis/gateway/ratelimit/v1alpha1" + ratelimitvalidator "github.com/kyma-project/api-gateway/internal/ratelimit" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var sc *runtime.Scheme + +func init() { + sc = runtime.NewScheme() + _ = scheme.AddToScheme(sc) + _ = ratelimitv1alpha1.AddToScheme(sc) +} + +var _ = Describe("RateLimit CR Validation", func() { + It("Should pass if there is only one RateLimit CR and matching pod", func() { + commonSelectors := map[string]string{ + "app": "test", + } + + rlCR := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rl", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: commonSelectors, + }, + } + + testPod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Labels: commonSelectors, + Annotations: map[string]string{ + "sidecar.istio.io/status": "test", + }, + }, + } + c := fake.NewClientBuilder().WithScheme(sc).WithObjects(&rlCR, &testPod).Build() + + err := ratelimitvalidator.Validate(context.Background(), c, rlCR) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should pass if there is more than one RateLimit CR and matching pods without conflicts", func() { + commonSelectors := map[string]string{ + "app": "test", + } + + rlCR := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rl", + Namespace: "test-namespace", + Annotations: map[string]string{ + "sidecar.istio.io/status": "test", + }, + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: commonSelectors, + }, + } + rlCR2 := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rl2", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: map[string]string{ + "app": "test2", + }, + }, + } + + testPod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Labels: commonSelectors, + Annotations: map[string]string{ + "sidecar.istio.io/status": "test", + }, + }, + } + c := fake.NewClientBuilder().WithScheme(sc).WithObjects(&rlCR, &rlCR2, &testPod).Build() + + err := ratelimitvalidator.Validate(context.Background(), c, rlCR) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should fail if there is no pods matching for the selectors in RateLimit CR", func() { + rlCR := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rl", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: map[string]string{ + "app": "test", + }, + }, + } + c := fake.NewClientBuilder().WithScheme(sc).WithObjects(&rlCR).Build() + + err := ratelimitvalidator.Validate(context.Background(), c, rlCR) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf("no pods found with the given selectors: %v in namespace %s", rlCR.Spec.SelectorLabels, rlCR.Namespace))) + }) + + It("Should fail if there are already a RateLimit CRs assigned to the matching pods", func() { + //given + testPod1 := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod1", + Namespace: "test-namespace", + Labels: map[string]string{ + "rateLimitSelector": "ratelimit", + "otherSelector": "other1", + }, + Annotations: map[string]string{ + "sidecar.istio.io/status": "test", + }, + }, + } + testPod2 := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod2", + Namespace: "test-namespace", + Labels: map[string]string{ + "rateLimitSelector": "ratelimit", + "otherSelector2": "other2", + }, + Annotations: map[string]string{ + "sidecar.istio.io/status": "test", + }, + }, + } + existingRL1 := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existingRL1", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: map[string]string{ + "otherSelector": "other1", + }, + }, + } + existingRL2 := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existingRL2", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: map[string]string{ + "otherSelector2": "other2", + }, + }, + } + c := fake.NewClientBuilder().WithScheme(sc).WithObjects(&existingRL1, &existingRL2, &testPod1, &testPod2).Build() + + // when + newRL := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-rl", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: map[string]string{ + "rateLimitSelector": "ratelimit", // should be common for both pods + }, + }, + } + err := ratelimitvalidator.Validate(context.Background(), c, newRL) + + //then + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf("conflicting with the following RateLimit CRs: %s, %s", existingRL1.Name, existingRL2.Name))) + }) + + It("Should fail if the pod is not sidecar-enabled", func() { + commonSelectors := map[string]string{ + "app": "test", + } + testPod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Labels: commonSelectors, + }, + } + testPod2 := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod2", + Namespace: "test-namespace", + Labels: commonSelectors, + }, + } + rlCR := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rl", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: commonSelectors, + }, + } + c := fake.NewClientBuilder().WithScheme(sc).WithObjects(&rlCR, &testPod, &testPod2).Build() + + err := ratelimitvalidator.Validate(context.Background(), c, rlCR) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("sidecar injection is not enabled for the following pods: test-namespace/test-pod, test-namespace/test-pod2")) + }) + + It("Should pass if the selected pod is a part of the istio ingress-gateway and RateLimit CR is in the ingress gateway namespace", func() { + ingressGatewaySelectors := map[string]string{ + "app": "istio-ingressgateway", + "istio": "ingressgateway", + } + + rlCR := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rl", + Namespace: "istio-system", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: map[string]string{ + "istio": "ingressgateway", + }, + }, + } + + ingressGatewayPod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "istio-ingressgateway-test", + Namespace: "istio-system", + Labels: ingressGatewaySelectors, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "istio-proxy", + }, + }, + }, + } + + c := fake.NewClientBuilder().WithScheme(sc).WithObjects(&rlCR, &ingressGatewayPod).Build() + + err := ratelimitvalidator.Validate(context.Background(), c, rlCR) + + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should fail if the selected pod is a part of the Istio ingress-gateway but the RateLimit CR is in a different namespace", func() { + ingressGatewaySelectors := map[string]string{ + "app": "istio-ingressgateway", + "istio": "ingressgateway", + } + + rlCR := ratelimitv1alpha1.RateLimit{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rl", + Namespace: "test-namespace", + }, + Spec: ratelimitv1alpha1.RateLimitSpec{ + SelectorLabels: map[string]string{ + "istio": "ingressgateway", + }, + }, + } + + ingressGatewayPod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "istio-ingressgateway-test", + Namespace: "istio-system", + Labels: ingressGatewaySelectors, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "istio-proxy", + }, + }, + }, + } + + c := fake.NewClientBuilder().WithScheme(sc).WithObjects(&rlCR, &ingressGatewayPod).Build() + + err := ratelimitvalidator.Validate(context.Background(), c, rlCR) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf("no pods found with the given selectors: %v in namespace %s", rlCR.Spec.SelectorLabels, rlCR.Namespace))) + }) +}) diff --git a/main.go b/main.go index e51dd2a32..408d388e6 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "context" "crypto/tls" "flag" + "github.com/kyma-project/api-gateway/controllers/gateway/ratelimit" "os" "time" @@ -62,7 +63,6 @@ import ( securityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" operatorv1alpha1 "github.com/kyma-project/api-gateway/apis/operator/v1alpha1" - "github.com/kyma-project/api-gateway/controllers/ratelimit" //+kubebuilder:scaffold:imports )