From 9ae6a70e7bdcf4052e88277e110fdcde7051207e Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Sat, 11 Nov 2023 20:37:33 +0100 Subject: [PATCH] ratelimitpolicy from OAS command --- cmd/generate_kuadrant.go | 2 + cmd/generate_kuadrant_ratelimitpolicy.go | 93 ++++++++++++++++ doc/generate-gateway-api-httproute.md | 14 +-- doc/openapi-kuadrant-extensions.md | 3 + .../petstore-wiht-kuadrant-extensions.yaml | 27 +++++ go.mod | 16 ++- go.sum | 20 ++++ pkg/gatewayapi/http_route.go | 79 +------------ pkg/kuadrantapi/rate_limit_policy.go | 88 +++++++++++++++ pkg/utils/kuadrant_oas_extension_types.go | 10 ++ pkg/utils/oas_utils.go | 104 ++++++++++++++++++ 11 files changed, 368 insertions(+), 88 deletions(-) create mode 100644 cmd/generate_kuadrant_ratelimitpolicy.go create mode 100644 doc/openapi-kuadrant-extensions.md create mode 100644 pkg/kuadrantapi/rate_limit_policy.go create mode 100644 pkg/utils/oas_utils.go diff --git a/cmd/generate_kuadrant.go b/cmd/generate_kuadrant.go index 0cc73f3..8da869e 100644 --- a/cmd/generate_kuadrant.go +++ b/cmd/generate_kuadrant.go @@ -11,5 +11,7 @@ func generateKuadrantCommand() *cobra.Command { Long: "Generate Kuadrant resources", } + cmd.AddCommand(generateKuadrantRateLimitPolicyCommand()) + return cmd } diff --git a/cmd/generate_kuadrant_ratelimitpolicy.go b/cmd/generate_kuadrant_ratelimitpolicy.go new file mode 100644 index 0000000..ed998f9 --- /dev/null +++ b/cmd/generate_kuadrant_ratelimitpolicy.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrantctl/pkg/gatewayapi" + "github.com/kuadrant/kuadrantctl/pkg/kuadrantapi" + "github.com/kuadrant/kuadrantctl/pkg/utils" +) + +//kuadrantctl generate kuadrant httproute --oas [OAS_FILE_PATH | OAS_URL | @] + +func generateKuadrantRateLimitPolicyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "ratelimitpolicy", + Short: "Generate Kuadrant RateLimitPolicy from OpenAPI 3.0.X", + Long: "Generate Kuadrant RateLimitPolicy from OpenAPI 3.0.X", + RunE: runGenerateKuadrantRateLimitPolicy, + } + + // OpenAPI ref + cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteOAS, "oas", "", "/path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required)") + err := cmd.MarkFlagRequired("oas") + if err != nil { + panic(err) + } + + return cmd +} + +func runGenerateKuadrantRateLimitPolicy(cmd *cobra.Command, args []string) error { + oasDataRaw, err := utils.ReadExternalResource(generateGatewayAPIHTTPRouteOAS) + if err != nil { + return err + } + + openapiLoader := openapi3.NewLoader() + doc, err := openapiLoader.LoadFromData(oasDataRaw) + if err != nil { + return err + } + + err = doc.Validate(openapiLoader.Context) + if err != nil { + return fmt.Errorf("OpenAPI validation error: %w", err) + } + + rlp := buildRateLimitPolicy(doc) + + jsonData, err := json.Marshal(rlp) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), string(jsonData)) + return nil +} + +func buildRateLimitPolicy(doc *openapi3.T) *kuadrantapiv1beta2.RateLimitPolicy { + routeMeta := gatewayapi.HTTPRouteObjectMetaFromOAS(doc) + + rlp := &kuadrantapiv1beta2.RateLimitPolicy{ + TypeMeta: v1.TypeMeta{ + APIVersion: "kuadrant.io/v1beta2", + Kind: "RateLimitPolicy", + }, + ObjectMeta: kuadrantapi.RateLimitPolicyObjectMetaFromOAS(doc), + Spec: kuadrantapiv1beta2.RateLimitPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: gatewayapiv1beta1.Group("gateway.networking.k8s.io"), + Kind: gatewayapiv1beta1.Kind("HTTPRoute"), + Name: gatewayapiv1beta1.ObjectName(routeMeta.Name), + }, + Limits: kuadrantapi.RateLimitPolicyLimitsFromOAS(doc), + }, + } + + if routeMeta.Namespace != "" { + rlp.Spec.TargetRef.Namespace = &[]gatewayapiv1beta1.Namespace{ + gatewayapiv1beta1.Namespace(routeMeta.Namespace), + }[0] + } + + return rlp +} diff --git a/doc/generate-gateway-api-httproute.md b/doc/generate-gateway-api-httproute.md index 23f69c3..5385332 100644 --- a/doc/generate-gateway-api-httproute.md +++ b/doc/generate-gateway-api-httproute.md @@ -1,7 +1,7 @@ ## Generate Gateway API HTTPRoute object from OpenAPI 3 The `kuadrantctl generate gatewayapi httproute` command generates an [Gateway API HTTPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/guides/http-routing/) -from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) and kubernetes service information. +from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) powered with [kuadrant extensions](openapi-kuadrant-extensions.md). ### OpenAPI specification @@ -12,10 +12,9 @@ OpenAPI document resource can be provided by one of the following channels: * URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address. * Read from stdin standard input stream. -### Usage : +### Usage ```shell -// TODO $ kuadrantctl generate gatewayapi httproute -h Generate Gateway API HTTPRoute from OpenAPI 3.0.X @@ -23,13 +22,8 @@ Usage: kuadrantctl generate gatewayapi httproute [flags] Flags: - --gateway strings Gateways (required) - -h, --help help for httproute - -n, --namespace string Service namespace (required) - --oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR - (required) - -p, --port int32 Service Port (required) (default 80) - --public-host string Public host (required) - --service-name string Service name (required) + -h, --help help for httproute + --oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required) Global Flags: -v, --verbose verbose output diff --git a/doc/openapi-kuadrant-extensions.md b/doc/openapi-kuadrant-extensions.md new file mode 100644 index 0000000..6eabec3 --- /dev/null +++ b/doc/openapi-kuadrant-extensions.md @@ -0,0 +1,3 @@ +## OpenAPI 3.0.X Kuadrant Extensions + +TODO diff --git a/examples/oas3/petstore-wiht-kuadrant-extensions.yaml b/examples/oas3/petstore-wiht-kuadrant-extensions.yaml index 7181807..29795a4 100644 --- a/examples/oas3/petstore-wiht-kuadrant-extensions.yaml +++ b/examples/oas3/petstore-wiht-kuadrant-extensions.yaml @@ -21,6 +21,13 @@ paths: backendRefs: - name: petstore namespace: petstore + rate_limit: + rates: + - limit: 1 + duration: 10 + unit: second + counters: + - auth.identity.username get: operationId: "getCat" responses: @@ -32,6 +39,13 @@ paths: backendRefs: - name: petstore namespace: petstore + rate_limit: + rates: + - limit: 2 + duration: 10 + unit: second + counters: + - auth.identity.username operationId: "postCat" responses: 405: @@ -43,7 +57,20 @@ paths: backendRefs: - name: petstore namespace: petstore + rate_limit: + rates: + - limit: 3 + duration: 10 + unit: second + counters: + - auth.identity.username operationId: "getDog" responses: 405: description: "invalid input" + /mouse: + get: + operationId: "getMouse" + responses: + 405: + description: "invalid input" diff --git a/go.mod b/go.mod index 91be402..7e91dfd 100644 --- a/go.mod +++ b/go.mod @@ -13,13 +13,17 @@ require ( k8s.io/apiextensions-apiserver v0.28.3 k8s.io/apimachinery v0.28.3 k8s.io/client-go v0.28.3 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/controller-runtime v0.16.3 sigs.k8s.io/gateway-api v0.6.2 ) require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -40,8 +44,10 @@ require ( github.com/invopop/yaml v0.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kuadrant/authorino v0.15.0 // indirect github.com/kuadrant/authorino-operator v0.9.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -49,8 +55,15 @@ require ( github.com/nxadm/tail v1.4.8 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect github.com/sirupsen/logrus v1.9.2 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/gjson v1.14.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect @@ -60,6 +73,7 @@ require ( golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect @@ -69,9 +83,9 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect istio.io/api v0.0.0-20230712174848-a2b2de508c88 // indirect + k8s.io/component-base v0.28.3 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 139f8ee..f840224 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,17 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk= +github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= @@ -85,6 +89,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kuadrant/authorino v0.15.0 h1:Xw/buh/wTINdL+IpLSxhlpet4hpleMxZzfx39c4VQng= +github.com/kuadrant/authorino v0.15.0/go.mod h1:vXkHKrntn8DR7kt8a8Ohxq+2lgAD0jWivThoP+7ASew= github.com/kuadrant/authorino-operator v0.9.0 h1:EV7zrYBNcd53HPQMivvTwe/+DIATTK7O4znJzh4xON8= github.com/kuadrant/authorino-operator v0.9.0/go.mod h1:VkUqS4CHNiaHMrjSFQ5V71DN829kPnqT3FQxqlOntEI= github.com/kuadrant/kuadrant-operator v0.4.1 h1:nGk7786goNzItxbIifmGWj6/Al8S7U+eT0fTcgEZphU= @@ -93,6 +99,7 @@ github.com/kuadrant/limitador-operator v0.4.0 h1:HgJi7LuOsenCUMs2ACCfKMKsKpfHcqm github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -124,9 +131,13 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= @@ -145,6 +156,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= +github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -185,6 +202,7 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -234,6 +252,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= @@ -281,6 +300,7 @@ k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= diff --git a/pkg/gatewayapi/http_route.go b/pkg/gatewayapi/http_route.go index eb8b2ab..dd5438a 100644 --- a/pkg/gatewayapi/http_route.go +++ b/pkg/gatewayapi/http_route.go @@ -92,7 +92,7 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { } if !ptr.Deref(kuadrantOperationExtension.Enable, pathEnabled) { - // not enabled for the HTTPRoute + // not enabled for the operation continue } @@ -114,85 +114,10 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { } func buildHTTPRouteRule(path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, backendRefs []gatewayapiv1beta1.HTTPBackendRef) gatewayapiv1beta1.HTTPRouteRule { - pathHeadersMatch := headersMatchFromParams(pathItem.Parameters) - operationHeadersMatch := headersMatchFromParams(op.Parameters) - - // default headersMatch at the path level - headersMatch := pathHeadersMatch - if len(operationHeadersMatch) > 0 { - headersMatch = operationHeadersMatch - } - - pathQueryParamsMatch := queryParamsMatchFromParams(pathItem.Parameters) - operationQueryParamsMatch := queryParamsMatchFromParams(op.Parameters) - - // default queryParams at the path level - queryParams := pathQueryParamsMatch - if len(operationQueryParamsMatch) > 0 { - queryParams = operationQueryParamsMatch - } - - match := gatewayapiv1beta1.HTTPRouteMatch{ - Method: &[]gatewayapiv1beta1.HTTPMethod{gatewayapiv1beta1.HTTPMethod(verb)}[0], - Path: &gatewayapiv1beta1.HTTPPathMatch{ - // TODO(eguzki): consider other path match types like PathPrefix - Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchExact}[0], - Value: &[]string{path}[0], - }, - Headers: headersMatch, - QueryParams: queryParams, - } + match := utils.OpenAPIMatcherFromOASOperations(path, pathItem, verb, op) return gatewayapiv1beta1.HTTPRouteRule{ BackendRefs: backendRefs, Matches: []gatewayapiv1beta1.HTTPRouteMatch{match}, } - -} - -func headersMatchFromParams(params openapi3.Parameters) []gatewayapiv1beta1.HTTPHeaderMatch { - matches := make([]gatewayapiv1beta1.HTTPHeaderMatch, 0) - - for _, parameter := range params { - if !parameter.Value.Required { - continue - } - - if parameter.Value.In == openapi3.ParameterInHeader { - matches = append(matches, gatewayapiv1beta1.HTTPHeaderMatch{ - Type: &[]gatewayapiv1beta1.HeaderMatchType{gatewayapiv1beta1.HeaderMatchExact}[0], - Name: gatewayapiv1beta1.HTTPHeaderName(parameter.Value.Name), - }) - } - } - - if len(matches) == 0 { - return nil - } - - return matches -} - -func queryParamsMatchFromParams(params openapi3.Parameters) []gatewayapiv1beta1.HTTPQueryParamMatch { - matches := make([]gatewayapiv1beta1.HTTPQueryParamMatch, 0) - - for _, parameter := range params { - if !parameter.Value.Required { - continue - } - - if parameter.Value.In == openapi3.ParameterInQuery { - matches = append(matches, gatewayapiv1beta1.HTTPQueryParamMatch{ - Type: &[]gatewayapiv1beta1.QueryParamMatchType{gatewayapiv1beta1.QueryParamMatchExact}[0], - Name: parameter.Value.Name, - }) - } - } - - if len(matches) == 0 { - return nil - } - - return matches - } diff --git a/pkg/kuadrantapi/rate_limit_policy.go b/pkg/kuadrantapi/rate_limit_policy.go new file mode 100644 index 0000000..e55cc50 --- /dev/null +++ b/pkg/kuadrantapi/rate_limit_policy.go @@ -0,0 +1,88 @@ +package kuadrantapi + +import ( + "github.com/getkin/kin-openapi/openapi3" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrantctl/pkg/gatewayapi" + "github.com/kuadrant/kuadrantctl/pkg/utils" +) + +func RateLimitPolicyObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { + return gatewayapi.HTTPRouteObjectMetaFromOAS(doc) +} + +func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2.Limit { + // Current implementation, one limit per operation + // TODO(eguzki): consider about grouping operations in fewer RLP limits + + limits := make(map[string]kuadrantapiv1beta2.Limit) + + // Paths + for path, pathItem := range doc.Paths { + kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) + if err != nil { + panic(err) + } + + pathEnabled := kuadrantPathExtension.IsEnabled() + + // Operations + for verb, operation := range pathItem.Operations() { + kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) + if err != nil { + panic(err) + } + + if !ptr.Deref(kuadrantOperationExtension.Enable, pathEnabled) { + // not enabled for the operation + //fmt.Printf("OUT not enabled: path: %s, method: %s\n", path, verb) + continue + } + + // default backendrefs at the path level + rateLimit := kuadrantPathExtension.RateLimit + if kuadrantOperationExtension.RateLimit != nil { + rateLimit = kuadrantOperationExtension.RateLimit + } + + if rateLimit == nil { + // no rate limit defined for this operation + //fmt.Printf("OUT no rate limit defined: path: %s, method: %s\n", path, verb) + continue + } + + limitName := utils.OpenAPIOperationName(path, verb, operation) + + limits[limitName] = buildRateLimitPolicyLimit(path, pathItem, verb, operation, rateLimit) + } + } + + if len(limits) == 0 { + return nil + } + + return limits +} + +func buildRateLimitPolicyLimit(path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, rateLimit *utils.KuadrantRateLimitExtension) kuadrantapiv1beta2.Limit { + return kuadrantapiv1beta2.Limit{ + RouteSelectors: buildLimitRouteSelectors(path, pathItem, verb, op), + When: rateLimit.When, + Counters: rateLimit.Counters, + Rates: rateLimit.Rates, + } +} + +func buildLimitRouteSelectors(path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) []kuadrantapiv1beta2.RouteSelector { + match := utils.OpenAPIMatcherFromOASOperations(path, pathItem, verb, op) + + return []kuadrantapiv1beta2.RouteSelector{ + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{match}, + }, + } +} diff --git a/pkg/utils/kuadrant_oas_extension_types.go b/pkg/utils/kuadrant_oas_extension_types.go index 6c58a32..95eca2d 100644 --- a/pkg/utils/kuadrant_oas_extension_types.go +++ b/pkg/utils/kuadrant_oas_extension_types.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/getkin/kin-openapi/openapi3" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" "k8s.io/utils/ptr" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -38,9 +39,18 @@ func NewKuadrantOASInfoExtension(info *openapi3.Info) (*KuadrantOASInfoExtension return x.Kuadrant, nil } +type KuadrantRateLimitExtension struct { + When []kuadrantapiv1beta2.WhenCondition `json:"when,omitempty"` + + Counters []kuadrantapiv1beta2.ContextSelector `json:"counters,omitempty"` + + Rates []kuadrantapiv1beta2.Rate `json:"rates,omitempty"` +} + type KuadrantOASPathExtension struct { Enable *bool `json:"enable,omitempty"` BackendRefs []gatewayapiv1beta1.HTTPBackendRef `json:"backendRefs,omitempty"` + RateLimit *KuadrantRateLimitExtension `json:"rate_limit,omitempty"` } func (k *KuadrantOASPathExtension) IsEnabled() bool { diff --git a/pkg/utils/oas_utils.go b/pkg/utils/oas_utils.go new file mode 100644 index 0000000..985cb7e --- /dev/null +++ b/pkg/utils/oas_utils.go @@ -0,0 +1,104 @@ +package utils + +import ( + "fmt" + "regexp" + + "github.com/getkin/kin-openapi/openapi3" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +var ( + // NonWordCharRegexp not word characters (== [^0-9A-Za-z_]) + NonWordCharRegexp = regexp.MustCompile(`\W`) +) + +func OpenAPIMatcherFromOASOperations(path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) gatewayapiv1beta1.HTTPRouteMatch { + pathHeadersMatch := headersMatchFromParams(pathItem.Parameters) + operationHeadersMatch := headersMatchFromParams(op.Parameters) + + // default headersMatch at the path level + headersMatch := pathHeadersMatch + if len(operationHeadersMatch) > 0 { + headersMatch = operationHeadersMatch + } + + pathQueryParamsMatch := queryParamsMatchFromParams(pathItem.Parameters) + operationQueryParamsMatch := queryParamsMatchFromParams(op.Parameters) + + // default queryParams at the path level + queryParams := pathQueryParamsMatch + if len(operationQueryParamsMatch) > 0 { + queryParams = operationQueryParamsMatch + } + + return gatewayapiv1beta1.HTTPRouteMatch{ + Method: &[]gatewayapiv1beta1.HTTPMethod{gatewayapiv1beta1.HTTPMethod(verb)}[0], + Path: &gatewayapiv1beta1.HTTPPathMatch{ + // TODO(eguzki): consider other path match types like PathPrefix + Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchExact}[0], + Value: &[]string{path}[0], + }, + Headers: headersMatch, + QueryParams: queryParams, + } +} + +func headersMatchFromParams(params openapi3.Parameters) []gatewayapiv1beta1.HTTPHeaderMatch { + matches := make([]gatewayapiv1beta1.HTTPHeaderMatch, 0) + + for _, parameter := range params { + if !parameter.Value.Required { + continue + } + + if parameter.Value.In == openapi3.ParameterInHeader { + matches = append(matches, gatewayapiv1beta1.HTTPHeaderMatch{ + Type: &[]gatewayapiv1beta1.HeaderMatchType{gatewayapiv1beta1.HeaderMatchExact}[0], + Name: gatewayapiv1beta1.HTTPHeaderName(parameter.Value.Name), + }) + } + } + + if len(matches) == 0 { + return nil + } + + return matches +} + +func queryParamsMatchFromParams(params openapi3.Parameters) []gatewayapiv1beta1.HTTPQueryParamMatch { + matches := make([]gatewayapiv1beta1.HTTPQueryParamMatch, 0) + + for _, parameter := range params { + if !parameter.Value.Required { + continue + } + + if parameter.Value.In == openapi3.ParameterInQuery { + matches = append(matches, gatewayapiv1beta1.HTTPQueryParamMatch{ + Type: &[]gatewayapiv1beta1.QueryParamMatchType{gatewayapiv1beta1.QueryParamMatchExact}[0], + Name: parameter.Value.Name, + }) + } + } + + if len(matches) == 0 { + return nil + } + + return matches + +} + +func OpenAPIOperationName(path, opVerb string, op *openapi3.Operation) string { + sanitizedPath := NonWordCharRegexp.ReplaceAllString(path, "") + + name := fmt.Sprintf("%s%s", opVerb, sanitizedPath) + + if op.OperationID != "" { + name = op.OperationID + } + + return name +}