From 42d9d757453f93f6477548c3c374325ced44a524 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Sat, 11 Nov 2023 12:46:09 +0100 Subject: [PATCH 01/28] parse OAS kuadrant extensions --- cmd/generate_gatewayapi_httproute.go | 109 ++-------- doc/generate-gateway-api-httproute.md | 7 +- .../petstore-wiht-kuadrant-extensions.yaml | 49 +++++ examples/oas3/petstore.yaml | 2 +- go.mod | 6 +- go.sum | 20 +- pkg/gatewayapi/http_route.go | 191 +++++++++++++++--- pkg/utils/external_resource_reader.go | 4 +- pkg/utils/kuadrant_oas_extension_types.go | 93 +++++++++ pkg/utils/oas3.go | 53 ----- pkg/utils/utils.coverprofile | 68 ------- 11 files changed, 343 insertions(+), 259 deletions(-) create mode 100644 examples/oas3/petstore-wiht-kuadrant-extensions.yaml create mode 100644 pkg/utils/kuadrant_oas_extension_types.go delete mode 100644 pkg/utils/oas3.go delete mode 100644 pkg/utils/utils.coverprofile diff --git a/cmd/generate_gatewayapi_httproute.go b/cmd/generate_gatewayapi_httproute.go index 3ec64f3..fea188d 100644 --- a/cmd/generate_gatewayapi_httproute.go +++ b/cmd/generate_gatewayapi_httproute.go @@ -13,73 +13,30 @@ import ( ) var ( - generateGatewayAPIHTTPRouteOAS string - generateGatewayAPIHTTPRouteHost string - generateGatewayAPIHTTPRouteSvcName string - generateGatewayAPIHTTPRouteSvcNamespace string - generateGatewayAPIHTTPRouteSvcPort int32 - generateGatewayAPIHTTPRouteGateways []string + generateGatewayAPIHTTPRouteOAS string ) -//kuadrantctl generate istio virtualservice --namespace myns --oas petstore.yaml --public-host www.kuadrant.io --service-name myservice --gateway kuadrant-gateway -// --namespace myns -// --service-name myservice -// --public-host www.kuadrant.io -// --gateway kuadrant-gateway -// -- service-port 80 +//kuadrantctl generate gatewayapi httproute --oas [OAS_FILE_PATH | OAS_URL | @] func generateGatewayApiHttpRouteCommand() *cobra.Command { cmd := &cobra.Command{ Use: "httproute", - Short: "Generate Gateway API HTTPRoute from OpenAPI 3.x", - Long: "Generate Gateway API HTTPRoute from OpenAPI 3.x", - RunE: func(cmd *cobra.Command, args []string) error { - return generateGatewayApiHttpRoute(cmd, args) - }, + Short: "Generate Gateway API HTTPRoute from OpenAPI 3.0.X", + Long: "Generate Gateway API HTTPRoute from OpenAPI 3.0.X", + RunE: runGenerateGatewayApiHttpRoute, } // OpenAPI ref - cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteOAS, "oas", "", "/path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR - (required)") + 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) } - // service ref - cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteSvcName, "service-name", "", "Service name (required)") - err = cmd.MarkFlagRequired("service-name") - if err != nil { - panic(err) - } - - // service namespace - cmd.Flags().StringVarP(&generateGatewayAPIHTTPRouteSvcNamespace, "namespace", "n", "", "Service namespace (required)") - err = cmd.MarkFlagRequired("namespace") - if err != nil { - panic(err) - } - - // service host - cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteHost, "public-host", "", "Public host (required)") - err = cmd.MarkFlagRequired("public-host") - if err != nil { - panic(err) - } - - // service port - cmd.Flags().Int32VarP(&generateGatewayAPIHTTPRouteSvcPort, "port", "p", 80, "Service Port (required)") - - // gateway - cmd.Flags().StringSliceVar(&generateGatewayAPIHTTPRouteGateways, "gateway", []string{}, "Gateways (required)") - err = cmd.MarkFlagRequired("gateway") - if err != nil { - panic(err) - } - return cmd } -func generateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { +func runGenerateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { oasDataRaw, err := utils.ReadExternalResource(generateGatewayAPIHTTPRouteOAS) if err != nil { return err @@ -96,10 +53,7 @@ func generateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { return fmt.Errorf("OpenAPI validation error: %w", err) } - httpRoute, err := generateGatewayAPIHTTPRoute(cmd, doc) - if err != nil { - return err - } + httpRoute := buildHTTPRoute(doc) jsonData, err := json.Marshal(httpRoute) if err != nil { @@ -110,52 +64,19 @@ func generateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { return nil } -func generateGatewayAPIHTTPRoute(cmd *cobra.Command, doc *openapi3.T) (*gatewayapiv1beta1.HTTPRoute, error) { - - //loop through gateway - // https://github.com/getkin/kin-openapi - gatewaysRef := []gatewayapiv1beta1.ParentReference{} - for _, gateway := range generateGatewayAPIHTTPRouteGateways { - gatewaysRef = append(gatewaysRef, gatewayapiv1beta1.ParentReference{ - Name: gatewayapiv1beta1.ObjectName(gateway), - }) - } - - port := gatewayapiv1beta1.PortNumber(generateGatewayAPIHTTPRouteSvcPort) - service := fmt.Sprintf("%s.%s.svc", generateGatewayAPIHTTPRouteSvcName, generateGatewayAPIHTTPRouteSvcNamespace) - matches, err := gatewayapi.HTTPRouteMatchesFromOAS(doc) - if err != nil { - return nil, err - } - - httpRoute := gatewayapiv1beta1.HTTPRoute{ +func buildHTTPRoute(doc *openapi3.T) *gatewayapiv1beta1.HTTPRoute { + return &gatewayapiv1beta1.HTTPRoute{ TypeMeta: v1.TypeMeta{ - Kind: "HTTPRoute", APIVersion: "gateway.networking.k8s.io/v1beta1", + Kind: "HTTPRoute", }, + ObjectMeta: gatewayapi.HTTPRouteObjectMetaFromOAS(doc), Spec: gatewayapiv1beta1.HTTPRouteSpec{ CommonRouteSpec: gatewayapiv1beta1.CommonRouteSpec{ - ParentRefs: gatewaysRef, - }, - Hostnames: []gatewayapiv1beta1.Hostname{ - gatewayapiv1beta1.Hostname(generateGatewayAPIHTTPRouteHost), - }, - Rules: []gatewayapiv1beta1.HTTPRouteRule{ - { - BackendRefs: []gatewayapiv1beta1.HTTPBackendRef{ - { - BackendRef: gatewayapiv1beta1.BackendRef{ - BackendObjectReference: gatewayapiv1beta1.BackendObjectReference{ - Name: gatewayapiv1beta1.ObjectName(service), - Port: &port, - }, - }, - }, - }, - Matches: matches, - }, + ParentRefs: gatewayapi.HTTPRouteGatewayParentRefsFromOAS(doc), }, + Hostnames: gatewayapi.HTTPRouteHostnamesFromOAS(doc), + Rules: gatewayapi.HTTPRouteRulesFromOAS(doc), }, } - return &httpRoute, nil } diff --git a/doc/generate-gateway-api-httproute.md b/doc/generate-gateway-api-httproute.md index be15a4d..23f69c3 100644 --- a/doc/generate-gateway-api-httproute.md +++ b/doc/generate-gateway-api-httproute.md @@ -5,6 +5,8 @@ from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Speci ### OpenAPI specification +[OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) + OpenAPI document resource can be provided by one of the following channels: * Filename in the available path. * URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address. @@ -13,8 +15,9 @@ OpenAPI document resource can be provided by one of the following channels: ### Usage : ```shell +// TODO $ kuadrantctl generate gatewayapi httproute -h -Generate Gateway API HTTPRoute from OpenAPI 3.x +Generate Gateway API HTTPRoute from OpenAPI 3.0.X Usage: kuadrantctl generate gatewayapi httproute [flags] @@ -32,4 +35,4 @@ Global Flags: -v, --verbose verbose output ``` -> Under the example folder there are examples of OAS 3 that can be used to generate the resources +> Under the example folder there are examples of OAS 3 that can be used to generate the resources diff --git a/examples/oas3/petstore-wiht-kuadrant-extensions.yaml b/examples/oas3/petstore-wiht-kuadrant-extensions.yaml new file mode 100644 index 0000000..7181807 --- /dev/null +++ b/examples/oas3/petstore-wiht-kuadrant-extensions.yaml @@ -0,0 +1,49 @@ +--- +openapi: "3.0.3" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: apiGateway + namespace: gateways +servers: + - url: https://example.io/v1 +paths: + /cat: + x-kuadrant: + enable: true + backendRefs: + - name: petstore + namespace: petstore + get: + operationId: "getCat" + responses: + 405: + description: "invalid input" + post: + x-kuadrant: + enable: false + backendRefs: + - name: petstore + namespace: petstore + operationId: "postCat" + responses: + 405: + description: "invalid input" + /dog: + get: + x-kuadrant: + enable: true + backendRefs: + - name: petstore + namespace: petstore + operationId: "getDog" + responses: + 405: + description: "invalid input" diff --git a/examples/oas3/petstore.yaml b/examples/oas3/petstore.yaml index d141f96..1d5e3e2 100644 --- a/examples/oas3/petstore.yaml +++ b/examples/oas3/petstore.yaml @@ -1,5 +1,5 @@ --- -openapi: "3.0.0" +openapi: "3.0.2" info: title: "Pet Store API" version: "1.0.0" diff --git a/go.mod b/go.mod index b4c9f3d..91be402 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/kuadrant/kuadrantctl go 1.20 require ( - github.com/getkin/kin-openapi v0.76.0 + github.com/getkin/kin-openapi v0.120.0 github.com/kuadrant/kuadrant-operator v0.4.1 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.10 @@ -23,7 +23,6 @@ require ( 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 - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect @@ -38,14 +37,17 @@ require ( github.com/google/uuid v1.3.1 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + 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-operator v0.9.0 // indirect github.com/mailru/easyjson v0.7.7 // 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 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 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/sirupsen/logrus v1.9.2 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index 75f2101..139f8ee 100644 --- a/go.sum +++ b/go.sum @@ -17,27 +17,24 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/getkin/kin-openapi v0.76.0 h1:j77zg3Ec+k+r+GA3d8hBoXpAc6KX9TbBPrwQGBIy2sY= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= +github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -68,12 +65,13 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93 h1:D5iJJZKAi0rU4e/5E58BkrnN+xeCDjAIqcm1GGxAGSI= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -92,8 +90,6 @@ github.com/kuadrant/authorino-operator v0.9.0/go.mod h1:VkUqS4CHNiaHMrjSFQ5V71DN github.com/kuadrant/kuadrant-operator v0.4.1 h1:nGk7786goNzItxbIifmGWj6/Al8S7U+eT0fTcgEZphU= github.com/kuadrant/kuadrant-operator v0.4.1/go.mod h1:iD+CMYKOfcpSts2JxscTlkeBgsusBwEhVsuJw832EAY= github.com/kuadrant/limitador-operator v0.4.0 h1:HgJi7LuOsenCUMs2ACCfKMKsKpfHcqmmwVmqpci0hw4= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 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= @@ -102,6 +98,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -118,6 +116,8 @@ github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/operator-framework/api v0.19.0 h1:QU1CTJU+CufoeneA5rsNlP/uP96s8vDHWUYDFZTauzA= github.com/operator-framework/api v0.19.0/go.mod h1:SCCslqke6AVOJ5JM+NqNE1CHuAgJLScsL66pnPaSMXs= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -145,6 +145,7 @@ 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/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= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -265,6 +266,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/pkg/gatewayapi/http_route.go b/pkg/gatewayapi/http_route.go index 4558a19..eb8b2ab 100644 --- a/pkg/gatewayapi/http_route.go +++ b/pkg/gatewayapi/http_route.go @@ -2,43 +2,156 @@ package gatewayapi import ( "github.com/getkin/kin-openapi/openapi3" + 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/utils" ) -func HTTPRouteMatchesFromOAS(doc *openapi3.T) ([]gatewayapiv1beta1.HTTPRouteMatch, error) { - httpRouteMatches := []gatewayapiv1beta1.HTTPRouteMatch{} - pathMatchExactPath := gatewayapiv1beta1.PathMatchExact +func HTTPRouteObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { + if doc.Info == nil { + return metav1.ObjectMeta{} + } + + kuadrantInfoExtension, err := utils.NewKuadrantOASInfoExtension(doc.Info) + if err != nil { + panic(err) + } + + if kuadrantInfoExtension.Route == nil { + panic("info kuadrant extension route not found") + } + + if kuadrantInfoExtension.Route.Name == nil { + panic("info kuadrant extension route name not found") + } + + om := metav1.ObjectMeta{Name: *kuadrantInfoExtension.Route.Name} + + if kuadrantInfoExtension.Route.Namespace != nil { + om.Namespace = *kuadrantInfoExtension.Route.Namespace + } + + return om +} + +func HTTPRouteGatewayParentRefsFromOAS(doc *openapi3.T) []gatewayapiv1beta1.ParentReference { + if doc.Info == nil { + return nil + } + + kuadrantInfoExtension, err := utils.NewKuadrantOASInfoExtension(doc.Info) + if err != nil { + panic(err) + } + + if kuadrantInfoExtension.Route == nil { + panic("info kuadrant extension route not found") + } + + return kuadrantInfoExtension.Route.ParentRefs +} + +func HTTPRouteHostnamesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.Hostname { + if doc.Info == nil { + return nil + } + + kuadrantInfoExtension, err := utils.NewKuadrantOASInfoExtension(doc.Info) + if err != nil { + panic(err) + } + + if kuadrantInfoExtension.Route == nil { + panic("info kuadrant extension route not found") + } + + return kuadrantInfoExtension.Route.Hostnames +} + +func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { + // Current implementation, one rule per operation + // TODO(eguzki): consider about grouping operations as HTTPRouteMatch objects in fewer HTTPRouteRule objects + rules := make([]gatewayapiv1beta1.HTTPRouteRule, 0) + // Paths for path, pathItem := range doc.Paths { + kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) + if err != nil { + panic(err) + } - headers := []gatewayapiv1beta1.HTTPHeaderMatch{} - queryParams := []gatewayapiv1beta1.HTTPQueryParamMatch{} - headers, queryParams = addRuleMatcherFromParams(pathItem.Parameters, headers, queryParams) + pathEnabled := kuadrantPathExtension.IsEnabled() + // Operations for verb, operation := range pathItem.Operations() { + kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) + if err != nil { + panic(err) + } - headers, queryParams = addRuleMatcherFromParams(operation.Parameters, headers, queryParams) - - pathValue := path - httpMethod := gatewayapiv1beta1.HTTPMethod(verb) - httpRouteMatches = append(httpRouteMatches, gatewayapiv1beta1.HTTPRouteMatch{ - Method: &httpMethod, - Path: &gatewayapiv1beta1.HTTPPathMatch{ - Type: &pathMatchExactPath, - Value: &pathValue, - }, - Headers: headers, - QueryParams: queryParams, - }) + if !ptr.Deref(kuadrantOperationExtension.Enable, pathEnabled) { + // not enabled for the HTTPRoute + continue + } + + // default backendrefs at the path level + backendRefs := kuadrantPathExtension.BackendRefs + if len(kuadrantOperationExtension.BackendRefs) > 0 { + backendRefs = kuadrantOperationExtension.BackendRefs + } + + rules = append(rules, buildHTTPRouteRule(path, pathItem, verb, operation, backendRefs)) } } - return httpRouteMatches, nil + if len(rules) == 0 { + return nil + } + + return rules +} + +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, + } + + return gatewayapiv1beta1.HTTPRouteRule{ + BackendRefs: backendRefs, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{match}, + } + } -func addRuleMatcherFromParams(params openapi3.Parameters, headers []gatewayapiv1beta1.HTTPHeaderMatch, queryParams []gatewayapiv1beta1.HTTPQueryParamMatch) ([]gatewayapiv1beta1.HTTPHeaderMatch, []gatewayapiv1beta1.HTTPQueryParamMatch) { - headerMatchType := gatewayapiv1beta1.HeaderMatchExact - queryParamMatchExact := gatewayapiv1beta1.QueryParamMatchExact +func headersMatchFromParams(params openapi3.Parameters) []gatewayapiv1beta1.HTTPHeaderMatch { + matches := make([]gatewayapiv1beta1.HTTPHeaderMatch, 0) for _, parameter := range params { if !parameter.Value.Required { @@ -46,18 +159,40 @@ func addRuleMatcherFromParams(params openapi3.Parameters, headers []gatewayapiv1 } if parameter.Value.In == openapi3.ParameterInHeader { - headers = append(headers, gatewayapiv1beta1.HTTPHeaderMatch{ - Type: &headerMatchType, + 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 { - queryParams = append(queryParams, gatewayapiv1beta1.HTTPQueryParamMatch{ - Type: &queryParamMatchExact, + matches = append(matches, gatewayapiv1beta1.HTTPQueryParamMatch{ + Type: &[]gatewayapiv1beta1.QueryParamMatchType{gatewayapiv1beta1.QueryParamMatchExact}[0], Name: parameter.Value.Name, }) } } - return headers, queryParams + if len(matches) == 0 { + return nil + } + + return matches + } diff --git a/pkg/utils/external_resource_reader.go b/pkg/utils/external_resource_reader.go index 6fdbc94..d60642a 100644 --- a/pkg/utils/external_resource_reader.go +++ b/pkg/utils/external_resource_reader.go @@ -21,11 +21,11 @@ import ( ) // ReadExternalResource reads data streams from external resources. Currently implemented: -// - '-' for STDIN +// - '@' for STDIN // - URLs (HTTP[S]) // - Files func ReadExternalResource(resource string) ([]byte, error) { - if resource == "-" { + if resource == "@" { return ioutil.ReadAll(os.Stdin) } diff --git a/pkg/utils/kuadrant_oas_extension_types.go b/pkg/utils/kuadrant_oas_extension_types.go new file mode 100644 index 0000000..6c58a32 --- /dev/null +++ b/pkg/utils/kuadrant_oas_extension_types.go @@ -0,0 +1,93 @@ +package utils + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" + "k8s.io/utils/ptr" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +type RouteObject struct { + Name *string `json:"name,omitempty"` + Namespace *string `json:"namespace,omitempty"` + Hostnames []gatewayapiv1beta1.Hostname `json:"hostnames,omitempty"` + ParentRefs []gatewayapiv1beta1.ParentReference `json:"parentRefs,omitempty"` +} + +type KuadrantOASInfoExtension struct { + Route *RouteObject `json:"route,omitempty"` +} + +func NewKuadrantOASInfoExtension(info *openapi3.Info) (*KuadrantOASInfoExtension, error) { + type KuadrantOASInfoObject struct { + // Kuadrant extension + Kuadrant *KuadrantOASInfoExtension `json:"x-kuadrant,omitempty"` + } + + data, err := info.MarshalJSON() + if err != nil { + return nil, err + } + + var x KuadrantOASInfoObject + if err := json.Unmarshal(data, &x); err != nil { + return nil, err + } + + return x.Kuadrant, nil +} + +type KuadrantOASPathExtension struct { + Enable *bool `json:"enable,omitempty"` + BackendRefs []gatewayapiv1beta1.HTTPBackendRef `json:"backendRefs,omitempty"` +} + +func (k *KuadrantOASPathExtension) IsEnabled() bool { + // Set default + return ptr.Deref(k.Enable, false) +} + +func NewKuadrantOASPathExtension(pathItem *openapi3.PathItem) (*KuadrantOASPathExtension, error) { + type KuadrantOASPathObject struct { + // Kuadrant extension + Kuadrant *KuadrantOASPathExtension `json:"x-kuadrant,omitempty"` + } + + data, err := pathItem.MarshalJSON() + if err != nil { + return nil, err + } + + var x KuadrantOASPathObject + if err := json.Unmarshal(data, &x); err != nil { + return nil, err + } + + kuadrantExtension := ptr.Deref(x.Kuadrant, KuadrantOASPathExtension{}) + + return &kuadrantExtension, nil +} + +type KuadrantOASOperationExtension KuadrantOASPathExtension + +func NewKuadrantOASOperationExtension(operation *openapi3.Operation) (*KuadrantOASOperationExtension, error) { + type KuadrantOASOperationObject struct { + // Kuadrant extension + Kuadrant *KuadrantOASOperationExtension `json:"x-kuadrant,omitempty"` + } + + data, err := operation.MarshalJSON() + if err != nil { + return nil, err + } + + var x KuadrantOASOperationObject + if err := json.Unmarshal(data, &x); err != nil { + return nil, err + } + + kuadrantExtension := ptr.Deref(x.Kuadrant, KuadrantOASOperationExtension{}) + + return &kuadrantExtension, nil +} diff --git a/pkg/utils/oas3.go b/pkg/utils/oas3.go deleted file mode 100644 index a9d0fa2..0000000 --- a/pkg/utils/oas3.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "fmt" - "regexp" - "strings" - - "github.com/getkin/kin-openapi/openapi3" - "k8s.io/apimachinery/pkg/util/validation" -) - -var ( - // NonAlphanumRegexp not alphanumeric - NonAlphanumRegexp = regexp.MustCompile(`[^0-9A-Za-z]`) -) - -func K8sNameFromOpenAPITitle(obj *openapi3.T) (string, error) { - openapiTitle := obj.Info.Title - openapiTitleToLower := strings.ToLower(openapiTitle) - objName := NonAlphanumRegexp.ReplaceAllString(openapiTitleToLower, "") - - // DNS Subdomain Names - // If the name would be part of some label, validation would be DNS Label Names (validation.IsDNS1123Label) - // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ - errStrings := validation.IsDNS1123Subdomain(objName) - if len(errStrings) > 0 { - errStr := strings.Join(errStrings, ",") - return "", fmt.Errorf("k8s name from OAS not valid: %s", errStr) - } - return objName, nil -} - -func ValidateOAS3(docRaw []byte) error { - openapiLoader := openapi3.NewLoader() - doc, err := openapiLoader.LoadFromData(docRaw) - if err != nil { - return err - } - - err = doc.Validate(openapiLoader.Context) - if err != nil { - return fmt.Errorf("OpenAPI validation error: %w", err) - } - - return nil -} - -func OpenAPIOperationSecRequirements(oasDoc *openapi3.T, operation *openapi3.Operation) *openapi3.SecurityRequirements { - if operation.Security == nil { - return &oasDoc.Security - } - return operation.Security -} diff --git a/pkg/utils/utils.coverprofile b/pkg/utils/utils.coverprofile deleted file mode 100644 index dbcac2a..0000000 --- a/pkg/utils/utils.coverprofile +++ /dev/null @@ -1,68 +0,0 @@ -mode: atomic -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:27.60,28.21 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:28.21,30.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:32.2,32.45 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:32.45,34.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:37.2,37.34 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:25.44,28.2 2 9 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:30.49,32.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:32.16,34.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:35.2,37.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:37.16,39.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:40.2,40.18 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:32.81,34.9 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:34.9,36.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:38.2,40.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:40.16,42.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:44.2,44.37 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:44.37,46.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:49.2,51.9 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:51.9,53.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:54.2,55.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:55.16,57.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:59.2,62.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:62.16,64.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:66.2,69.9 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:69.9,71.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:73.2,75.12 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:78.77,80.9 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:80.9,82.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:83.2,87.16 4 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:87.16,88.37 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:88.37,91.4 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:91.9,93.4 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:95.2,95.12 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:98.73,100.9 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:100.9,102.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:103.2,107.46 4 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:107.46,110.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:111.2,111.12 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:116.56,118.43 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:118.43,119.99 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:119.99,121.4 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:123.2,123.14 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:126.96,129.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:129.16,130.32 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:130.32,133.4 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:135.3,135.20 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:138.2,138.48 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:138.48,143.3 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:145.2,146.18 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:17.63,26.25 5 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:26.25,29.3 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:30.2,30.21 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:33.40,36.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:36.16,38.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:40.2,41.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:41.16,43.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:45.2,45.12 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:48.120,49.31 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:49.31,51.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:52.2,52.27 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:10.26,12.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:12.16,14.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:16.2,17.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:17.16,19.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:21.2,22.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:22.16,24.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:26.2,26.12 1 0 From 9ae6a70e7bdcf4052e88277e110fdcde7051207e Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Sat, 11 Nov 2023 20:37:33 +0100 Subject: [PATCH 02/28] 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 +} From e0bef38a65cf50b4ef4ab605c04b1469769594bb Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Mon, 13 Nov 2023 13:35:15 +0100 Subject: [PATCH 03/28] fix lint issues --- cmd/install.go | 19 +++++++++++-------- make/lint.mk | 2 +- pkg/utils/external_resource_reader.go | 6 +++--- pkg/utils/http_utils.go | 4 ++-- pkg/utils/k8s_utils.go | 4 ++-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/cmd/install.go b/cmd/install.go index 9d1741b..755664f 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -101,8 +101,9 @@ func waitForDeployments(k8sClient client.Client) error { } for _, key := range deploymentKeys { - err := wait.Poll(retryInterval, timeout, func() (bool, error) { - return utils.CheckDeploymentAvailable(k8sClient, key) + immediate := true + err := wait.PollUntilContextTimeout(context.Background(), retryInterval, timeout, immediate, func(ctx context.Context) (bool, error) { + return utils.CheckDeploymentAvailable(ctx, k8sClient, key) }) if err != nil { @@ -147,10 +148,11 @@ func deployKuadrantOperator(k8sClient client.Client) error { var installPlanKey client.ObjectKey // Wait for the install process to be completed + immediate := true logf.Log.Info("Waiting for the kuadrant operator installation") - err = wait.Poll(time.Second*2, time.Second*20, func() (bool, error) { + err = wait.PollUntilContextTimeout(context.Background(), time.Second*2, time.Second*20, immediate, func(ctx context.Context) (bool, error) { existingSubs := &operators.Subscription{} - err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(subs), existingSubs) + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(subs), existingSubs) if err != nil { if apierrors.IsNotFound(err) { logf.Log.Info("Subscription not available", "name", client.ObjectKeyFromObject(subs)) @@ -176,9 +178,9 @@ func deployKuadrantOperator(k8sClient client.Client) error { } logf.Log.Info("Waiting for the install plan", "name", installPlanKey) - err = wait.Poll(time.Second*5, time.Minute*2, func() (bool, error) { + err = wait.PollUntilContextTimeout(context.Background(), time.Second*5, time.Minute*2, immediate, func(ctx context.Context) (bool, error) { ip := &operators.InstallPlan{} - err := k8sClient.Get(context.Background(), installPlanKey, ip) + err := k8sClient.Get(ctx, installPlanKey, ip) if err != nil { if apierrors.IsNotFound(err) { logf.Log.Info("Install plan not available", "name", installPlanKey) @@ -230,8 +232,9 @@ func createNamespace(k8sClient client.Client) error { retryInterval := time.Second * 2 timeout := time.Second * 20 - return wait.Poll(retryInterval, timeout, func() (bool, error) { - err := k8sClient.Get(context.Background(), types.NamespacedName{Name: installNamespace}, &corev1.Namespace{}) + immediate := true + return wait.PollUntilContextTimeout(context.Background(), retryInterval, timeout, immediate, func(ctx context.Context) (bool, error) { + err := k8sClient.Get(ctx, types.NamespacedName{Name: installNamespace}, &corev1.Namespace{}) if err != nil && apierrors.IsNotFound(err) { return false, nil } diff --git a/make/lint.mk b/make/lint.mk index 45842b0..c190494 100644 --- a/make/lint.mk +++ b/make/lint.mk @@ -1,7 +1,7 @@ GOLANGCI-LINT=$(PROJECT_PATH)/bin/golangci-lint $(GOLANGCI-LINT): mkdir -p $(PROJECT_PATH)/bin - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_PATH)/bin v1.41.1 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_PATH)/bin v1.55.2 .PHONY: golangci-lint golangci-lint: $(GOLANGCI-LINT) diff --git a/pkg/utils/external_resource_reader.go b/pkg/utils/external_resource_reader.go index d60642a..43dd6bf 100644 --- a/pkg/utils/external_resource_reader.go +++ b/pkg/utils/external_resource_reader.go @@ -16,7 +16,7 @@ limitations under the License. package utils import ( - "io/ioutil" + "io" "os" ) @@ -26,7 +26,7 @@ import ( // - Files func ReadExternalResource(resource string) ([]byte, error) { if resource == "@" { - return ioutil.ReadAll(os.Stdin) + return io.ReadAll(os.Stdin) } if url, isURL := ParseURL(resource); isURL { @@ -34,5 +34,5 @@ func ReadExternalResource(resource string) ([]byte, error) { } // Defaulting to filepath - return ioutil.ReadFile(resource) + return os.ReadFile(resource) } diff --git a/pkg/utils/http_utils.go b/pkg/utils/http_utils.go index 4a90cd7..9d2195c 100644 --- a/pkg/utils/http_utils.go +++ b/pkg/utils/http_utils.go @@ -16,7 +16,7 @@ limitations under the License. package utils import ( - "io/ioutil" + "io" "net/http" "net/url" ) @@ -32,7 +32,7 @@ func ReadURL(location *url.URL) ([]byte, error) { if err != nil { return nil, err } - data, err := ioutil.ReadAll(resp.Body) + data, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err diff --git a/pkg/utils/k8s_utils.go b/pkg/utils/k8s_utils.go index 075b9d4..80e87d6 100644 --- a/pkg/utils/k8s_utils.go +++ b/pkg/utils/k8s_utils.go @@ -123,9 +123,9 @@ func IsDeploymentAvailable(dc *appsv1.Deployment) bool { return false } -func CheckDeploymentAvailable(k8sClient client.Client, key types.NamespacedName) (bool, error) { +func CheckDeploymentAvailable(ctx context.Context, k8sClient client.Client, key types.NamespacedName) (bool, error) { existingDeployment := &appsv1.Deployment{} - err := k8sClient.Get(context.Background(), key, existingDeployment) + err := k8sClient.Get(ctx, key, existingDeployment) if err != nil { if apierrors.IsNotFound(err) { logf.Log.Info("Deployment not available", "name", key.Name) From e33fe738fd60709e4f57110614a97b82e6855013 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Mon, 13 Nov 2023 13:43:58 +0100 Subject: [PATCH 04/28] fix lint issues --- .github/workflows/code-style.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-style.yaml b/.github/workflows/code-style.yaml index 091a7bf..f9e5f96 100644 --- a/.github/workflows/code-style.yaml +++ b/.github/workflows/code-style.yaml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.16.x - uses: actions/setup-go@v2 + - name: Set up Go 1.20.x + uses: actions/setup-go@v4 with: - go-version: 1.16.x + go-version: 1.20.x id: go - name: Check out code From 3024658e2a2aa4b87bf54c83000641b7f21e1468 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Mon, 13 Nov 2023 14:25:58 +0100 Subject: [PATCH 05/28] some doc --- doc/openapi-kuadrant-extensions.md | 71 ++++++++++++++++++- .../petstore-wiht-kuadrant-extensions.yaml | 24 +++++-- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/doc/openapi-kuadrant-extensions.md b/doc/openapi-kuadrant-extensions.md index 6eabec3..594d800 100644 --- a/doc/openapi-kuadrant-extensions.md +++ b/doc/openapi-kuadrant-extensions.md @@ -1,3 +1,72 @@ ## OpenAPI 3.0.X Kuadrant Extensions -TODO +### Info level kuadrant extension + +Kuadrant extension that can be added at the info level of the OpenAPI spec. + +```yaml +info: + x-kuadrant: + route: ## HTTPRoute metadata + name: "petstore" + namespace: "petstore" + hostnames: ## []gateway.networking.k8s.io/v1beta1.Hostname + - example.com + parentRefs: ## []gateway.networking.k8s.io/v1beta1.ParentReference + - name: apiGateway + namespace: gateways +``` + +### Path level kuadrant extension + +Kuadrant extension that can be added at the path level of the OpenAPI spec. +This configuration at the path level +is the default when there is no operation level configuration. + +```yaml +paths: + /cat: + x-kuadrant: ## Path level Kuadrant Extension + enable: true ## Add to the HTTPRoute. Optional. Default: false + backendRefs: ## Backend references to be included in the HTTPRoute. []gateway.networking.k8s.io/v1beta1.HTTPBackendRef. Optional. + - name: petstore + namespace: petstore + rate_limit: ## Rate limit config. Optional. + rates: ## Kuadrant API []github.com/kuadrant/kuadrant-operator/api/v1beta2.Rate + - limit: 1 + duration: 10 + unit: second + counters: ## Kuadrant API []github.com/kuadrant/kuadrant-operator/api/v1beta2.CountextSelector + - auth.identity.username + when: ## Kuadrant API []github.com/kuadrant/kuadrant-operator/api/v1beta2.WhenCondition + - selector: metadata.filter_metadata.envoy\.filters\.http\.ext_authz.identity.userid + operator: eq + value: alice +``` + +### Operation level kuadrant extension + +Kuadrant extension that can be added at the operation level of the OpenAPI spec. +Same schema as path level kuadrant extension. + +```yaml +paths: + /cat: + get: + x-kuadrant: ## Path level Kuadrant Extension + enable: true ## Add to the HTTPRoute. Optional. Default: false + backendRefs: ## Backend references to be included in the HTTPRoute. Optional. + - name: petstore + namespace: petstore + rate_limit: ## Rate limit config. Optional. + rates: ## Kuadrant API github.com/kuadrant/kuadrant-operator/api/v1beta2.Rate + - limit: 1 + duration: 10 + unit: second + counters: ## Kuadrant API github.com/kuadrant/kuadrant-operator/api/v1beta2.CountextSelector + - auth.identity.username + when: ## Kuadrant API github.com/kuadrant/kuadrant-operator/api/v1beta2.WhenCondition + - selector: metadata.filter_metadata.envoy\.filters\.http\.ext_authz.identity.userid + operator: eq + value: alice +``` diff --git a/examples/oas3/petstore-wiht-kuadrant-extensions.yaml b/examples/oas3/petstore-wiht-kuadrant-extensions.yaml index 29795a4..76a4574 100644 --- a/examples/oas3/petstore-wiht-kuadrant-extensions.yaml +++ b/examples/oas3/petstore-wiht-kuadrant-extensions.yaml @@ -16,7 +16,7 @@ servers: - url: https://example.io/v1 paths: /cat: - x-kuadrant: + x-kuadrant: ## Path level Kuadrant Extension enable: true backendRefs: - name: petstore @@ -28,13 +28,13 @@ paths: unit: second counters: - auth.identity.username - get: + get: # Added to the route and rate limited operationId: "getCat" responses: 405: description: "invalid input" - post: - x-kuadrant: + post: # NOT added to the route + x-kuadrant: ## Operation level Kuadrant Extension enable: false backendRefs: - name: petstore @@ -51,8 +51,8 @@ paths: 405: description: "invalid input" /dog: - get: - x-kuadrant: + get: # Added to the route and rate limited + x-kuadrant: ## Operation level Kuadrant Extension enable: true backendRefs: - name: petstore @@ -68,8 +68,18 @@ paths: responses: 405: description: "invalid input" + post: # Added to the route and NOT rate limited + x-kuadrant: ## Operation level Kuadrant Extension + enable: true + backendRefs: + - name: petstore + namespace: petstore + operationId: "postDog" + responses: + 405: + description: "invalid input" /mouse: - get: + get: # NOT added to the route operationId: "getMouse" responses: 405: From 6ddbbbd7dee3022db05ef8c80d2b7960918db8b7 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Mon, 13 Nov 2023 17:40:35 +0100 Subject: [PATCH 06/28] additional fixes --- Makefile | 1 + cmd/install.go | 2 +- config/gateway-api/gateway/gateway.yaml | 19 +++++++ config/gateway-api/gateway/kustomization.yaml | 5 ++ ...=> petstore-with-kuadrant-extensions.yaml} | 14 +++-- examples/petstore/petstore.yaml | 39 ++++++++++++++ make/gateway-api.mk | 4 ++ make/istio.mk | 2 +- utils/istio-operator.yaml | 53 +++++++++++++++++++ utils/kind-cluster.yaml | 6 +++ 10 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 config/gateway-api/gateway/gateway.yaml create mode 100644 config/gateway-api/gateway/kustomization.yaml rename examples/oas3/{petstore-wiht-kuadrant-extensions.yaml => petstore-with-kuadrant-extensions.yaml} (87%) create mode 100644 examples/petstore/petstore.yaml create mode 100644 utils/istio-operator.yaml diff --git a/Makefile b/Makefile index 0f84025..2a23925 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,7 @@ env-setup: $(MAKE) olm-install $(MAKE) gateway-api-install $(MAKE) istio-install + $(MAKE) deploy-gateway ## local-setup: Sets up Kind cluster with GatewayAPI manifests and istio GW, nothing Kuadrant. Build and install kuadrantctl binary .PHONY: local-setup diff --git a/cmd/install.go b/cmd/install.go index 755664f..c36eaa0 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -150,7 +150,7 @@ func deployKuadrantOperator(k8sClient client.Client) error { // Wait for the install process to be completed immediate := true logf.Log.Info("Waiting for the kuadrant operator installation") - err = wait.PollUntilContextTimeout(context.Background(), time.Second*2, time.Second*20, immediate, func(ctx context.Context) (bool, error) { + err = wait.PollUntilContextTimeout(context.Background(), time.Second*5, time.Minute*2, immediate, func(ctx context.Context) (bool, error) { existingSubs := &operators.Subscription{} err := k8sClient.Get(ctx, client.ObjectKeyFromObject(subs), existingSubs) if err != nil { diff --git a/config/gateway-api/gateway/gateway.yaml b/config/gateway-api/gateway/gateway.yaml new file mode 100644 index 0000000..7886c7a --- /dev/null +++ b/config/gateway-api/gateway/gateway.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + labels: + istio: ingressgateway + name: istio-ingressgateway +spec: + gatewayClassName: istio + listeners: + - name: default + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All + addresses: + - value: istio-ingressgateway.istio-system.svc.cluster.local + type: Hostname diff --git a/config/gateway-api/gateway/kustomization.yaml b/config/gateway-api/gateway/kustomization.yaml new file mode 100644 index 0000000..b489ae5 --- /dev/null +++ b/config/gateway-api/gateway/kustomization.yaml @@ -0,0 +1,5 @@ +--- +# Adds namespace to all resources. +namespace: istio-system +resources: +- gateway.yaml diff --git a/examples/oas3/petstore-wiht-kuadrant-extensions.yaml b/examples/oas3/petstore-with-kuadrant-extensions.yaml similarity index 87% rename from examples/oas3/petstore-wiht-kuadrant-extensions.yaml rename to examples/oas3/petstore-with-kuadrant-extensions.yaml index 76a4574..114c748 100644 --- a/examples/oas3/petstore-wiht-kuadrant-extensions.yaml +++ b/examples/oas3/petstore-with-kuadrant-extensions.yaml @@ -10,8 +10,8 @@ info: hostnames: - example.com parentRefs: - - name: apiGateway - namespace: gateways + - name: istio-ingressgateway + namespace: istio-system servers: - url: https://example.io/v1 paths: @@ -20,6 +20,7 @@ paths: enable: true backendRefs: - name: petstore + port: 80 namespace: petstore rate_limit: rates: @@ -27,7 +28,7 @@ paths: duration: 10 unit: second counters: - - auth.identity.username + - request.headers.x-forwarded-for get: # Added to the route and rate limited operationId: "getCat" responses: @@ -38,6 +39,7 @@ paths: enable: false backendRefs: - name: petstore + port: 80 namespace: petstore rate_limit: rates: @@ -45,7 +47,7 @@ paths: duration: 10 unit: second counters: - - auth.identity.username + - request.headers.x-forwarded-for operationId: "postCat" responses: 405: @@ -56,6 +58,7 @@ paths: enable: true backendRefs: - name: petstore + port: 80 namespace: petstore rate_limit: rates: @@ -63,7 +66,7 @@ paths: duration: 10 unit: second counters: - - auth.identity.username + - request.headers.x-forwarded-for operationId: "getDog" responses: 405: @@ -73,6 +76,7 @@ paths: enable: true backendRefs: - name: petstore + port: 80 namespace: petstore operationId: "postDog" responses: diff --git a/examples/petstore/petstore.yaml b/examples/petstore/petstore.yaml new file mode 100644 index 0000000..fd733a5 --- /dev/null +++ b/examples/petstore/petstore.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: petstore + labels: + app: petstore +spec: + selector: + matchLabels: + app: petstore + template: + metadata: + labels: + app: petstore + spec: + containers: + - name: petstore + image: quay.io/3scale/authorino:echo-api + env: + - name: PORT + value: "3000" + ports: + - containerPort: 3000 + name: http + replicas: 1 +--- +apiVersion: v1 +kind: Service +metadata: + name: petstore +spec: + selector: + app: petstore + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 3000 diff --git a/make/gateway-api.mk b/make/gateway-api.mk index 81bd84e..e96f660 100644 --- a/make/gateway-api.mk +++ b/make/gateway-api.mk @@ -1,5 +1,9 @@ ##@ Gateway API resources +.PHONY: deploy-gateway +deploy-gateway: kustomize ## Deploy Gateway API gateway + $(KUSTOMIZE) build config/gateway-api/gateway | kubectl apply -f - + .PHONY: gateway-api-install gateway-api-install: kustomize ## Install Gateway API CRDs $(KUSTOMIZE) build config/gateway-api | kubectl apply -f - diff --git a/make/istio.mk b/make/istio.mk index 9644f45..a958dd9 100644 --- a/make/istio.mk +++ b/make/istio.mk @@ -18,7 +18,7 @@ istioctl: $(ISTIOCTL) ## Download istioctl locally if necessary. .PHONY: istio-install istio-install: istioctl ## Install istio. - $(ISTIOCTL) install --set profile=demo -y + $(ISTIOCTL) install -f utils/istio-operator.yaml -y .PHONY: istio-uninstall istio-uninstall: istioctl ## Uninstall istio. diff --git a/utils/istio-operator.yaml b/utils/istio-operator.yaml new file mode 100644 index 0000000..05db21c --- /dev/null +++ b/utils/istio-operator.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + profile: default + namespace: istio-system + components: + base: + enabled: true + cni: + enabled: false + egressGateways: + - enabled: false + name: istio-egressgateway + ingressGateways: + - enabled: true + name: istio-ingressgateway + k8s: + service: + type: NodePort + ports: + - name: status-port + port: 15021 + protocol: TCP + targetPort: 15021 + - name: http2 + port: 80 + protocol: TCP + targetPort: 8080 + nodePort: 30950 + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + nodePort: 30951 + resources: + requests: + cpu: "0" + pilot: + enabled: true + k8s: + resources: + requests: + cpu: "0" + values: + pilot: + autoscaleEnabled: false + gateways: + istio-ingressgateway: + type: ClusterIP + autoscaleEnabled: false + global: + istioNamespace: istio-system diff --git a/utils/kind-cluster.yaml b/utils/kind-cluster.yaml index 8e16566..a395098 100644 --- a/utils/kind-cluster.yaml +++ b/utils/kind-cluster.yaml @@ -5,3 +5,9 @@ nodes: - role: control-plane # port forward 80 on the host to 80 on this node image: kindest/node:v1.27.3 + # port forward 80 on the host to 80 on this node + extraPortMappings: + - containerPort: 30950 + hostPort: 9080 + - containerPort: 30951 + hostPort: 9443 From 03797911fbd5ced639e534a5201f7bf36863625a Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Mon, 13 Nov 2023 18:00:24 +0100 Subject: [PATCH 07/28] adding ports to backendrefs in openapi --- doc/openapi-kuadrant-extensions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/openapi-kuadrant-extensions.md b/doc/openapi-kuadrant-extensions.md index 594d800..f927fd4 100644 --- a/doc/openapi-kuadrant-extensions.md +++ b/doc/openapi-kuadrant-extensions.md @@ -30,6 +30,7 @@ paths: enable: true ## Add to the HTTPRoute. Optional. Default: false backendRefs: ## Backend references to be included in the HTTPRoute. []gateway.networking.k8s.io/v1beta1.HTTPBackendRef. Optional. - name: petstore + port: 80 namespace: petstore rate_limit: ## Rate limit config. Optional. rates: ## Kuadrant API []github.com/kuadrant/kuadrant-operator/api/v1beta2.Rate @@ -57,6 +58,7 @@ paths: enable: true ## Add to the HTTPRoute. Optional. Default: false backendRefs: ## Backend references to be included in the HTTPRoute. Optional. - name: petstore + port: 80 namespace: petstore rate_limit: ## Rate limit config. Optional. rates: ## Kuadrant API github.com/kuadrant/kuadrant-operator/api/v1beta2.Rate From 84982bc66ab3b333ab2186e5157fc5543a3f06f0 Mon Sep 17 00:00:00 2001 From: Jason Madigan Date: Wed, 15 Nov 2023 12:48:52 +0000 Subject: [PATCH 08/28] copy labels --- cmd/generate_gatewayapi_httproute.go | 10 +++++++++- pkg/gatewayapi/http_route.go | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cmd/generate_gatewayapi_httproute.go b/cmd/generate_gatewayapi_httproute.go index fea188d..93b7960 100644 --- a/cmd/generate_gatewayapi_httproute.go +++ b/cmd/generate_gatewayapi_httproute.go @@ -65,7 +65,7 @@ func runGenerateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { } func buildHTTPRoute(doc *openapi3.T) *gatewayapiv1beta1.HTTPRoute { - return &gatewayapiv1beta1.HTTPRoute{ + httpRoute := &gatewayapiv1beta1.HTTPRoute{ TypeMeta: v1.TypeMeta{ APIVersion: "gateway.networking.k8s.io/v1beta1", Kind: "HTTPRoute", @@ -79,4 +79,12 @@ func buildHTTPRoute(doc *openapi3.T) *gatewayapiv1beta1.HTTPRoute { Rules: gatewayapi.HTTPRouteRulesFromOAS(doc), }, } + + // Extract and set labels + labels, ok := gatewayapi.ExtractLabelsFromOAS(doc) + if ok { + httpRoute.ObjectMeta.Labels = labels + } + + return httpRoute } diff --git a/pkg/gatewayapi/http_route.go b/pkg/gatewayapi/http_route.go index dd5438a..b58470e 100644 --- a/pkg/gatewayapi/http_route.go +++ b/pkg/gatewayapi/http_route.go @@ -1,6 +1,8 @@ package gatewayapi import ( + "fmt" + "github.com/getkin/kin-openapi/openapi3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -113,6 +115,28 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { return rules } +func ExtractLabelsFromOAS(doc *openapi3.T) (map[string]string, bool) { + if doc.Info == nil || doc.Info.Extensions == nil { + return nil, false + } + + if extension, ok := doc.Info.Extensions["x-kuadrant"]; ok { + if extensionMap, ok := extension.(map[string]interface{}); ok { + if route, ok := extensionMap["route"].(map[string]interface{}); ok { + if labelsInterface, ok := route["labels"]; ok { + labels := make(map[string]string) + for key, value := range labelsInterface.(map[string]interface{}) { + labels[key] = fmt.Sprint(value) + } + return labels, true + } + } + } + } + + return nil, false +} + func buildHTTPRouteRule(path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, backendRefs []gatewayapiv1beta1.HTTPBackendRef) gatewayapiv1beta1.HTTPRouteRule { match := utils.OpenAPIMatcherFromOASOperations(path, pathItem, verb, op) From 64db41bc718e108355d0256a69b8d06977bdbc18 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Thu, 16 Nov 2023 11:38:52 +0100 Subject: [PATCH 09/28] OAS server path as base path for matchers --- .../petstore-with-kuadrant-extensions.yaml | 2 +- pkg/gatewayapi/http_route.go | 11 ++- pkg/kuadrantapi/rate_limit_policy.go | 25 ++--- pkg/utils/oas_utils.go | 95 ++++++++++++++++++- 4 files changed, 115 insertions(+), 18 deletions(-) diff --git a/examples/oas3/petstore-with-kuadrant-extensions.yaml b/examples/oas3/petstore-with-kuadrant-extensions.yaml index 114c748..1a3b444 100644 --- a/examples/oas3/petstore-with-kuadrant-extensions.yaml +++ b/examples/oas3/petstore-with-kuadrant-extensions.yaml @@ -13,7 +13,7 @@ info: - name: istio-ingressgateway namespace: istio-system servers: - - url: https://example.io/v1 + - url: https://example.io/api/v1 paths: /cat: x-kuadrant: ## Path level Kuadrant Extension diff --git a/pkg/gatewayapi/http_route.go b/pkg/gatewayapi/http_route.go index b58470e..743f8f4 100644 --- a/pkg/gatewayapi/http_route.go +++ b/pkg/gatewayapi/http_route.go @@ -77,6 +77,11 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { // TODO(eguzki): consider about grouping operations as HTTPRouteMatch objects in fewer HTTPRouteRule objects rules := make([]gatewayapiv1beta1.HTTPRouteRule, 0) + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + // Paths for path, pathItem := range doc.Paths { kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) @@ -104,7 +109,7 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { backendRefs = kuadrantOperationExtension.BackendRefs } - rules = append(rules, buildHTTPRouteRule(path, pathItem, verb, operation, backendRefs)) + rules = append(rules, buildHTTPRouteRule(basePath, path, pathItem, verb, operation, backendRefs)) } } @@ -137,8 +142,8 @@ func ExtractLabelsFromOAS(doc *openapi3.T) (map[string]string, bool) { return nil, false } -func buildHTTPRouteRule(path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, backendRefs []gatewayapiv1beta1.HTTPBackendRef) gatewayapiv1beta1.HTTPRouteRule { - match := utils.OpenAPIMatcherFromOASOperations(path, pathItem, verb, op) +func buildHTTPRouteRule(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, backendRefs []gatewayapiv1beta1.HTTPBackendRef) gatewayapiv1beta1.HTTPRouteRule { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op) return gatewayapiv1beta1.HTTPRouteRule{ BackendRefs: backendRefs, diff --git a/pkg/kuadrantapi/rate_limit_policy.go b/pkg/kuadrantapi/rate_limit_policy.go index e55cc50..8941b9e 100644 --- a/pkg/kuadrantapi/rate_limit_policy.go +++ b/pkg/kuadrantapi/rate_limit_policy.go @@ -21,6 +21,11 @@ func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2 limits := make(map[string]kuadrantapiv1beta2.Limit) + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + // Paths for path, pathItem := range doc.Paths { kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) @@ -57,7 +62,12 @@ func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2 limitName := utils.OpenAPIOperationName(path, verb, operation) - limits[limitName] = buildRateLimitPolicyLimit(path, pathItem, verb, operation, rateLimit) + limits[limitName] = kuadrantapiv1beta2.Limit{ + RouteSelectors: buildLimitRouteSelectors(basePath, path, pathItem, verb, operation), + When: rateLimit.When, + Counters: rateLimit.Counters, + Rates: rateLimit.Rates, + } } } @@ -68,17 +78,8 @@ func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2 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) +func buildLimitRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) []kuadrantapiv1beta2.RouteSelector { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op) return []kuadrantapiv1beta2.RouteSelector{ { diff --git a/pkg/utils/oas_utils.go b/pkg/utils/oas_utils.go index 985cb7e..e6ab9a2 100644 --- a/pkg/utils/oas_utils.go +++ b/pkg/utils/oas_utils.go @@ -1,7 +1,10 @@ package utils import ( + "bytes" "fmt" + "html/template" + "net/url" "regexp" "github.com/getkin/kin-openapi/openapi3" @@ -11,9 +14,97 @@ import ( var ( // NonWordCharRegexp not word characters (== [^0-9A-Za-z_]) NonWordCharRegexp = regexp.MustCompile(`\W`) + // TemplateRegexp used to render openapi server URLs + TemplateRegexp = regexp.MustCompile(`{([\w]+)}`) + // LastSlashRegexp matches the last slash + LastSlashRegexp = regexp.MustCompile(`/$`) ) -func OpenAPIMatcherFromOASOperations(path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) gatewayapiv1beta1.HTTPRouteMatch { +func FirstServerFromOpenAPI(obj *openapi3.T) *openapi3.Server { + if obj == nil { + return nil + } + + // take only first server + // From https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md + // If the servers property is not provided, or is an empty array, the default value would be a Server Object with a url value of /. + server := &openapi3.Server{ + URL: `/`, + Variables: map[string]*openapi3.ServerVariable{}, + } + + // Current constraint: only read the first item when there are multiple servers + // Maybe this should be user provided setting + if len(obj.Servers) > 0 { + server = obj.Servers[0] + } + + return server +} + +func RenderOpenAPIServerURLStr(server *openapi3.Server) (string, error) { + if server == nil { + return "", nil + } + + data := &struct { + Data map[string]string + }{ + map[string]string{}, + } + + for variableName, variable := range server.Variables { + data.Data[variableName] = variable.Default + } + + urlTemplate := TemplateRegexp.ReplaceAllString(server.URL, `{{ index .Data "$1" }}`) + + tObj, err := template.New(server.URL).Parse(urlTemplate) + if err != nil { + return "", err + } + + var tpl bytes.Buffer + err = tObj.Execute(&tpl, data) + if err != nil { + return "", err + } + + return tpl.String(), nil +} + +func RenderOpenAPIServerURL(server *openapi3.Server) (*url.URL, error) { + serverURLStr, err := RenderOpenAPIServerURLStr(server) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(serverURLStr) + if err != nil { + return nil, err + } + + return serverURL, nil +} + +func BasePathFromOpenAPI(obj *openapi3.T) (string, error) { + server := FirstServerFromOpenAPI(obj) + serverURL, err := RenderOpenAPIServerURL(server) + if err != nil { + return "", err + } + + return serverURL.Path, nil +} + +func OpenAPIMatcherFromOASOperations(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) gatewayapiv1beta1.HTTPRouteMatch { + + // remove the last slash of the Base Path + sanitizedBasePath := LastSlashRegexp.ReplaceAllString(basePath, "") + + // According OAS 3.0: path MUST begin with a slash + matchPath := fmt.Sprintf("%s%s", sanitizedBasePath, path) + pathHeadersMatch := headersMatchFromParams(pathItem.Parameters) operationHeadersMatch := headersMatchFromParams(op.Parameters) @@ -37,7 +128,7 @@ func OpenAPIMatcherFromOASOperations(path string, pathItem *openapi3.PathItem, v Path: &gatewayapiv1beta1.HTTPPathMatch{ // TODO(eguzki): consider other path match types like PathPrefix Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchExact}[0], - Value: &[]string{path}[0], + Value: &[]string{matchPath}[0], }, Headers: headersMatch, QueryParams: queryParams, From ce569cf935fa93785bea464ab517950389cd621e Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 22 Nov 2023 16:28:29 +0100 Subject: [PATCH 10/28] kuadrant extensions: disable and pathMatchType fields --- cmd/generate_gatewayapi_httproute.go | 10 +---- doc/openapi-kuadrant-extensions.md | 8 +++- pkg/gatewayapi/http_route.go | 45 +++++++---------------- pkg/kuadrantapi/rate_limit_policy.go | 16 +++++--- pkg/utils/kuadrant_oas_extension_types.go | 17 ++++++--- pkg/utils/oas_utils.go | 6 +-- 6 files changed, 45 insertions(+), 57 deletions(-) diff --git a/cmd/generate_gatewayapi_httproute.go b/cmd/generate_gatewayapi_httproute.go index 93b7960..fea188d 100644 --- a/cmd/generate_gatewayapi_httproute.go +++ b/cmd/generate_gatewayapi_httproute.go @@ -65,7 +65,7 @@ func runGenerateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { } func buildHTTPRoute(doc *openapi3.T) *gatewayapiv1beta1.HTTPRoute { - httpRoute := &gatewayapiv1beta1.HTTPRoute{ + return &gatewayapiv1beta1.HTTPRoute{ TypeMeta: v1.TypeMeta{ APIVersion: "gateway.networking.k8s.io/v1beta1", Kind: "HTTPRoute", @@ -79,12 +79,4 @@ func buildHTTPRoute(doc *openapi3.T) *gatewayapiv1beta1.HTTPRoute { Rules: gatewayapi.HTTPRouteRulesFromOAS(doc), }, } - - // Extract and set labels - labels, ok := gatewayapi.ExtractLabelsFromOAS(doc) - if ok { - httpRoute.ObjectMeta.Labels = labels - } - - return httpRoute } diff --git a/doc/openapi-kuadrant-extensions.md b/doc/openapi-kuadrant-extensions.md index f927fd4..3f2ca70 100644 --- a/doc/openapi-kuadrant-extensions.md +++ b/doc/openapi-kuadrant-extensions.md @@ -10,6 +10,8 @@ info: route: ## HTTPRoute metadata name: "petstore" namespace: "petstore" + labels: ## map[string]string + deployment: petstore hostnames: ## []gateway.networking.k8s.io/v1beta1.Hostname - example.com parentRefs: ## []gateway.networking.k8s.io/v1beta1.ParentReference @@ -27,7 +29,8 @@ is the default when there is no operation level configuration. paths: /cat: x-kuadrant: ## Path level Kuadrant Extension - enable: true ## Add to the HTTPRoute. Optional. Default: false + disable: true ## Remove from the HTTPRoute. Optional. Default: false + pathMatchType: Exact ## Specifies how to match against the path Value. Valid values: [Exact;PathPrefix]. Optional. Default: Exact backendRefs: ## Backend references to be included in the HTTPRoute. []gateway.networking.k8s.io/v1beta1.HTTPBackendRef. Optional. - name: petstore port: 80 @@ -55,7 +58,8 @@ paths: /cat: get: x-kuadrant: ## Path level Kuadrant Extension - enable: true ## Add to the HTTPRoute. Optional. Default: false + disable: true ## Remove from the HTTPRoute. Optional. Default: path level "disable" value + pathMatchType: Exact ## Specifies how to match against the path Value. Valid values: [Exact;PathPrefix]. Optional. Default: Exact backendRefs: ## Backend references to be included in the HTTPRoute. Optional. - name: petstore port: 80 diff --git a/pkg/gatewayapi/http_route.go b/pkg/gatewayapi/http_route.go index 743f8f4..68c2ea4 100644 --- a/pkg/gatewayapi/http_route.go +++ b/pkg/gatewayapi/http_route.go @@ -1,8 +1,6 @@ package gatewayapi import ( - "fmt" - "github.com/getkin/kin-openapi/openapi3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -29,7 +27,10 @@ func HTTPRouteObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { panic("info kuadrant extension route name not found") } - om := metav1.ObjectMeta{Name: *kuadrantInfoExtension.Route.Name} + om := metav1.ObjectMeta{ + Name: *kuadrantInfoExtension.Route.Name, + Labels: kuadrantInfoExtension.Route.Labels, + } if kuadrantInfoExtension.Route.Namespace != nil { om.Namespace = *kuadrantInfoExtension.Route.Namespace @@ -89,8 +90,6 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { panic(err) } - pathEnabled := kuadrantPathExtension.IsEnabled() - // Operations for verb, operation := range pathItem.Operations() { kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) @@ -98,7 +97,7 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { panic(err) } - if !ptr.Deref(kuadrantOperationExtension.Enable, pathEnabled) { + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { // not enabled for the operation continue } @@ -109,7 +108,13 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { backendRefs = kuadrantOperationExtension.BackendRefs } - rules = append(rules, buildHTTPRouteRule(basePath, path, pathItem, verb, operation, backendRefs)) + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + + rules = append(rules, buildHTTPRouteRule(basePath, path, pathItem, verb, operation, backendRefs, pathMatchType)) } } @@ -120,30 +125,8 @@ func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { return rules } -func ExtractLabelsFromOAS(doc *openapi3.T) (map[string]string, bool) { - if doc.Info == nil || doc.Info.Extensions == nil { - return nil, false - } - - if extension, ok := doc.Info.Extensions["x-kuadrant"]; ok { - if extensionMap, ok := extension.(map[string]interface{}); ok { - if route, ok := extensionMap["route"].(map[string]interface{}); ok { - if labelsInterface, ok := route["labels"]; ok { - labels := make(map[string]string) - for key, value := range labelsInterface.(map[string]interface{}) { - labels[key] = fmt.Sprint(value) - } - return labels, true - } - } - } - } - - return nil, false -} - -func buildHTTPRouteRule(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, backendRefs []gatewayapiv1beta1.HTTPBackendRef) gatewayapiv1beta1.HTTPRouteRule { - match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op) +func buildHTTPRouteRule(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, backendRefs []gatewayapiv1beta1.HTTPBackendRef, pathMatchType gatewayapiv1beta1.PathMatchType) gatewayapiv1beta1.HTTPRouteRule { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op, pathMatchType) return gatewayapiv1beta1.HTTPRouteRule{ BackendRefs: backendRefs, diff --git a/pkg/kuadrantapi/rate_limit_policy.go b/pkg/kuadrantapi/rate_limit_policy.go index 8941b9e..2509574 100644 --- a/pkg/kuadrantapi/rate_limit_policy.go +++ b/pkg/kuadrantapi/rate_limit_policy.go @@ -33,8 +33,6 @@ func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2 panic(err) } - pathEnabled := kuadrantPathExtension.IsEnabled() - // Operations for verb, operation := range pathItem.Operations() { kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) @@ -42,7 +40,7 @@ func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2 panic(err) } - if !ptr.Deref(kuadrantOperationExtension.Enable, pathEnabled) { + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { // not enabled for the operation //fmt.Printf("OUT not enabled: path: %s, method: %s\n", path, verb) continue @@ -60,10 +58,16 @@ func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2 continue } + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + limitName := utils.OpenAPIOperationName(path, verb, operation) limits[limitName] = kuadrantapiv1beta2.Limit{ - RouteSelectors: buildLimitRouteSelectors(basePath, path, pathItem, verb, operation), + RouteSelectors: buildLimitRouteSelectors(basePath, path, pathItem, verb, operation, pathMatchType), When: rateLimit.When, Counters: rateLimit.Counters, Rates: rateLimit.Rates, @@ -78,8 +82,8 @@ func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2 return limits } -func buildLimitRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) []kuadrantapiv1beta2.RouteSelector { - match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op) +func buildLimitRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType) []kuadrantapiv1beta2.RouteSelector { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op, pathMatchType) return []kuadrantapiv1beta2.RouteSelector{ { diff --git a/pkg/utils/kuadrant_oas_extension_types.go b/pkg/utils/kuadrant_oas_extension_types.go index 95eca2d..699fef1 100644 --- a/pkg/utils/kuadrant_oas_extension_types.go +++ b/pkg/utils/kuadrant_oas_extension_types.go @@ -14,6 +14,7 @@ type RouteObject struct { Namespace *string `json:"namespace,omitempty"` Hostnames []gatewayapiv1beta1.Hostname `json:"hostnames,omitempty"` ParentRefs []gatewayapiv1beta1.ParentReference `json:"parentRefs,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } type KuadrantOASInfoExtension struct { @@ -48,14 +49,20 @@ type KuadrantRateLimitExtension struct { } type KuadrantOASPathExtension struct { - Enable *bool `json:"enable,omitempty"` - BackendRefs []gatewayapiv1beta1.HTTPBackendRef `json:"backendRefs,omitempty"` - RateLimit *KuadrantRateLimitExtension `json:"rate_limit,omitempty"` + Disable *bool `json:"disable,omitempty"` + PathMatchType *gatewayapiv1beta1.PathMatchType `json:"pathMatchType,omitempty"` + BackendRefs []gatewayapiv1beta1.HTTPBackendRef `json:"backendRefs,omitempty"` + RateLimit *KuadrantRateLimitExtension `json:"rate_limit,omitempty"` } -func (k *KuadrantOASPathExtension) IsEnabled() bool { +func (k *KuadrantOASPathExtension) IsDisabled() bool { // Set default - return ptr.Deref(k.Enable, false) + return ptr.Deref(k.Disable, false) +} + +func (k *KuadrantOASPathExtension) GetPathMatchType() gatewayapiv1beta1.PathMatchType { + // Set default + return ptr.Deref(k.PathMatchType, gatewayapiv1beta1.PathMatchExact) } func NewKuadrantOASPathExtension(pathItem *openapi3.PathItem) (*KuadrantOASPathExtension, error) { diff --git a/pkg/utils/oas_utils.go b/pkg/utils/oas_utils.go index e6ab9a2..46c55c1 100644 --- a/pkg/utils/oas_utils.go +++ b/pkg/utils/oas_utils.go @@ -97,8 +97,7 @@ func BasePathFromOpenAPI(obj *openapi3.T) (string, error) { return serverURL.Path, nil } -func OpenAPIMatcherFromOASOperations(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) gatewayapiv1beta1.HTTPRouteMatch { - +func OpenAPIMatcherFromOASOperations(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType) gatewayapiv1beta1.HTTPRouteMatch { // remove the last slash of the Base Path sanitizedBasePath := LastSlashRegexp.ReplaceAllString(basePath, "") @@ -126,8 +125,7 @@ func OpenAPIMatcherFromOASOperations(basePath, path string, pathItem *openapi3.P 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], + Type: &pathMatchType, Value: &[]string{matchPath}[0], }, Headers: headersMatch, From 34da7400f29d1784753e464b86cdc8f501bc3394 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 22 Nov 2023 18:16:28 +0100 Subject: [PATCH 11/28] publish binary on release --- .github/workflows/release.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8e524a7 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,20 @@ +--- +on: + release: + types: [created] + +permissions: + contents: write + packages: write + +jobs: + release-linux-amd64: + name: release linux/amd64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: linux + goarch: amd64 From d60cf5f5e2a7b5a26ff97719a0e98558b68c600c Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 22 Nov 2023 19:31:56 +0100 Subject: [PATCH 12/28] fix release github action --- .github/workflows/release.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8e524a7..6a504ff 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,7 @@ --- + +name: Release + on: release: types: [created] @@ -18,3 +21,4 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: linux goarch: amd64 + goversion: go1.21.4 From 7b4c4fac50a4b3dee9cf452c0443f3c9a0b799e4 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 22 Nov 2023 19:37:27 +0100 Subject: [PATCH 13/28] fix go version --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6a504ff..28e14f2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,4 +21,4 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: linux goarch: amd64 - goversion: go1.21.4 + goversion: 1.21.4 From 8fca655b19b96d3e29283f5e053f99a5a632cd8e Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 22 Nov 2023 19:48:43 +0100 Subject: [PATCH 14/28] bump go 1.21.x --- .github/workflows/code-style.yaml | 4 ++-- .github/workflows/commands.yaml | 4 ++-- .github/workflows/testing.yaml | 8 ++++---- README.md | 2 +- doc/development.md | 2 +- go.mod | 2 +- go.sum | 13 +++++++++++++ 7 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/code-style.yaml b/.github/workflows/code-style.yaml index f9e5f96..5e5e226 100644 --- a/.github/workflows/code-style.yaml +++ b/.github/workflows/code-style.yaml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.20.x + - name: Set up Go 1.21.x uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x id: go - name: Check out code diff --git a/.github/workflows/commands.yaml b/.github/workflows/commands.yaml index 4d7b1c1..b69e4d4 100644 --- a/.github/workflows/commands.yaml +++ b/.github/workflows/commands.yaml @@ -13,10 +13,10 @@ jobs: name: Run kuadrantctl install runs-on: ubuntu-latest steps: - - name: Set up Go 1.20.x + - name: Set up Go 1.21.x uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x id: go - name: Check out code uses: actions/checkout@v3 diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 5366111..ce4a31d 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -13,10 +13,10 @@ jobs: name: Build executable runs-on: ubuntu-latest steps: - - name: Set up Go 1.20.x + - name: Set up Go 1.21.x uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x id: go - name: Check out code uses: actions/checkout@v2 @@ -30,10 +30,10 @@ jobs: env: KIND_CLUSTER_NAME: kuadrant-local steps: - - name: Set up Go 1.20.x + - name: Set up Go 1.21.x uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x id: go - name: Check out code uses: actions/checkout@v3 diff --git a/README.md b/README.md index ea119ba..c3af817 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Use `go install` to install the latest version of the library. This command will ``` go install github.com/kuadrant/kuadrantctl@latest ``` -> Golang 1.20+ required +> Golang 1.21+ required ## Commands * [Install Kuadrant](doc/install.md) diff --git a/doc/development.md b/doc/development.md index 4c39802..f02e2b1 100644 --- a/doc/development.md +++ b/doc/development.md @@ -3,7 +3,7 @@ ## Technology stack required for development * [git][git_tool] -* [go] version 1.20+ +* [go] version 1.21+ ## Build the CLI ``` diff --git a/go.mod b/go.mod index 7e91dfd..fa79738 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kuadrant/kuadrantctl -go 1.20 +go 1.21 require ( github.com/getkin/kin-openapi v0.120.0 diff --git a/go.sum b/go.sum index f840224..661e0eb 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,7 @@ github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7znc 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= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -38,7 +39,9 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -67,6 +70,7 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93 h1:D5iJJZKAi0rU4e/5E58BkrnN+xeCDjAIqcm1GGxAGSI= +github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -85,6 +89,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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= @@ -96,6 +101,7 @@ github.com/kuadrant/authorino-operator v0.9.0/go.mod h1:VkUqS4CHNiaHMrjSFQ5V71DN github.com/kuadrant/kuadrant-operator v0.4.1 h1:nGk7786goNzItxbIifmGWj6/Al8S7U+eT0fTcgEZphU= github.com/kuadrant/kuadrant-operator v0.4.1/go.mod h1:iD+CMYKOfcpSts2JxscTlkeBgsusBwEhVsuJw832EAY= github.com/kuadrant/limitador-operator v0.4.0 h1:HgJi7LuOsenCUMs2ACCfKMKsKpfHcqmmwVmqpci0hw4= +github.com/kuadrant/limitador-operator v0.4.0/go.mod h1:5fQo2XwxPr7bDObut9sK5sHCnK4hwAmTsTptaYvGfuc= 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= @@ -117,6 +123,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= @@ -139,6 +146,7 @@ github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO 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/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -156,6 +164,7 @@ 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/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= @@ -163,6 +172,7 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT 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/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -170,6 +180,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -247,6 +258,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -289,6 +301,7 @@ gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= istio.io/api v0.0.0-20230712174848-a2b2de508c88 h1:w7lSk+XcYNzGC5xtHBFvaV7QTBMQC5nM4l8xxFslGgk= istio.io/api v0.0.0-20230712174848-a2b2de508c88/go.mod h1:owGDRg9uqMob8CN1gxaOzk6nJxnbT8wrP7PmggpJHHY= k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= From 0e806469b82d16080d99b7654189c6170b39754e Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Thu, 23 Nov 2023 11:10:44 +0100 Subject: [PATCH 15/28] build and publish in parallel: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 --- .github/workflows/release.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 28e14f2..d667e88 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -11,14 +11,19 @@ permissions: packages: write jobs: - release-linux-amd64: - name: release linux/amd64 + release-matrix: + name: Release Go Binary runs-on: ubuntu-latest + strategy: + matrix: + # build and publish in parallel: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 + goos: [linux, darwin] + goarch: [amd64, arm64] steps: - uses: actions/checkout@v3 - uses: wangyoucao577/go-release-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} - goos: linux - goarch: amd64 + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} goversion: 1.21.4 From 2f14d1190457ff6181b0f3bb57460bd8812c3909 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Thu, 16 Nov 2023 15:37:04 +0100 Subject: [PATCH 16/28] kuadrant authpolicy command --- cmd/generate_kuadrant.go | 1 + cmd/generate_kuadrant_authpolicy.go | 96 +++++++++++++++ cmd/generate_kuadrant_ratelimitpolicy.go | 2 +- ...etstore-with-oidc-kuadrant-extensions.yaml | 55 +++++++++ ...-with-rate-limit-kuadrant-extensions.yaml} | 0 pkg/kuadrantapi/authpolicy.go | 112 ++++++++++++++++++ 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 cmd/generate_kuadrant_authpolicy.go create mode 100644 examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml rename examples/oas3/{petstore-with-kuadrant-extensions.yaml => petstore-with-rate-limit-kuadrant-extensions.yaml} (100%) create mode 100644 pkg/kuadrantapi/authpolicy.go diff --git a/cmd/generate_kuadrant.go b/cmd/generate_kuadrant.go index 8da869e..3e575d6 100644 --- a/cmd/generate_kuadrant.go +++ b/cmd/generate_kuadrant.go @@ -12,6 +12,7 @@ func generateKuadrantCommand() *cobra.Command { } cmd.AddCommand(generateKuadrantRateLimitPolicyCommand()) + cmd.AddCommand(generateKuadrantAuthPolicyCommand()) return cmd } diff --git a/cmd/generate_kuadrant_authpolicy.go b/cmd/generate_kuadrant_authpolicy.go new file mode 100644 index 0000000..a5c3268 --- /dev/null +++ b/cmd/generate_kuadrant_authpolicy.go @@ -0,0 +1,96 @@ +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 authpolicy --oas [OAS_FILE_PATH | OAS_URL | @] + +func generateKuadrantAuthPolicyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "authpolicy", + Short: "Generate Kuadrant AuthPolicy from OpenAPI 3.0.X", + Long: "Generate Kuadrant AuthPolicy from OpenAPI 3.0.X", + RunE: runGenerateKuadrantAuthPolicy, + } + + // 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 runGenerateKuadrantAuthPolicy(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) + } + + ap := buildAuthPolicy(doc) + + jsonData, err := json.Marshal(ap) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), string(jsonData)) + return nil +} + +func buildAuthPolicy(doc *openapi3.T) *kuadrantapiv1beta2.AuthPolicy { + routeMeta := gatewayapi.HTTPRouteObjectMetaFromOAS(doc) + + ap := &kuadrantapiv1beta2.AuthPolicy{ + TypeMeta: v1.TypeMeta{ + APIVersion: "kuadrant.io/v1beta2", + Kind: "AuthPolicy", + }, + ObjectMeta: kuadrantapi.AuthPolicyObjectMetaFromOAS(doc), + Spec: kuadrantapiv1beta2.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: gatewayapiv1beta1.Group("gateway.networking.k8s.io"), + Kind: gatewayapiv1beta1.Kind("HTTPRoute"), + Name: gatewayapiv1beta1.ObjectName(routeMeta.Name), + }, + // Currently only authentication rules enforced + AuthScheme: kuadrantapiv1beta2.AuthSchemeSpec{ + Authentication: kuadrantapi.AuthPolicyAuthenticationSchemeFromOAS(doc), + }, + }, + } + + if routeMeta.Namespace != "" { + ap.Spec.TargetRef.Namespace = &[]gatewayapiv1beta1.Namespace{ + gatewayapiv1beta1.Namespace(routeMeta.Namespace), + }[0] + } + + return ap +} diff --git a/cmd/generate_kuadrant_ratelimitpolicy.go b/cmd/generate_kuadrant_ratelimitpolicy.go index ed998f9..9115607 100644 --- a/cmd/generate_kuadrant_ratelimitpolicy.go +++ b/cmd/generate_kuadrant_ratelimitpolicy.go @@ -16,7 +16,7 @@ import ( "github.com/kuadrant/kuadrantctl/pkg/utils" ) -//kuadrantctl generate kuadrant httproute --oas [OAS_FILE_PATH | OAS_URL | @] +//kuadrantctl generate kuadrant ratelimitpolicy --oas [OAS_FILE_PATH | OAS_URL | @] func generateKuadrantRateLimitPolicyCommand() *cobra.Command { cmd := &cobra.Command{ diff --git a/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml b/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml new file mode 100644 index 0000000..e6ef951 --- /dev/null +++ b/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml @@ -0,0 +1,55 @@ +--- +openapi: "3.0.3" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system +servers: + - url: https://example.io/api/v1 +paths: + /cat: + x-kuadrant: ## Path level Kuadrant Extension + enable: true + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # Added to the route and public (not auth) + operationId: "getCat" + responses: + 405: + description: "invalid input" + post: # NOT added to the route + x-kuadrant: ## Operation level Kuadrant Extension + enable: false + operationId: "postCat" + responses: + 405: + description: "invalid input" + /dog: + get: # Added to the route and authenticated + x-kuadrant: ## Operation level Kuadrant Extension + enable: true + backendRefs: + - name: petstore + port: 80 + namespace: petstore + operationId: "getDog" + security: + - openIdConnect: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + openIdConnect: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration diff --git a/examples/oas3/petstore-with-kuadrant-extensions.yaml b/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml similarity index 100% rename from examples/oas3/petstore-with-kuadrant-extensions.yaml rename to examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go new file mode 100644 index 0000000..4dc84a5 --- /dev/null +++ b/pkg/kuadrantapi/authpolicy.go @@ -0,0 +1,112 @@ +package kuadrantapi + +import ( + "github.com/getkin/kin-openapi/openapi3" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + 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 AuthPolicyObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { + return gatewayapi.HTTPRouteObjectMetaFromOAS(doc) +} + +func buildAuthPolicyRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) []kuadrantapiv1beta2.RouteSelector { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op) + + return []kuadrantapiv1beta2.RouteSelector{ + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{match}, + }, + } +} + +func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2.AuthenticationSpec { + authentication := make(map[string]kuadrantapiv1beta2.AuthenticationSpec) + + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + + // 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 + } + + // Get operation level security requirements or fallback to global security requirements + secRequirements := ptr.Deref(operation.Security, doc.Security) + + if len(secRequirements) == 0 { + // no security + continue + } + + oidcScheme := findOIDCSecuritySchemesFromRequirements(doc, secRequirements) + + authName := utils.OpenAPIOperationName(path, verb, operation) + + authentication[authName] = kuadrantapiv1beta2.AuthenticationSpec{ + CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, operation), + }, + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + Jwt: &authorinoapi.JwtAuthenticationSpec{ + IssuerUrl: oidcScheme.OpenIdConnectUrl, + }, + }, + }, + } + } + } + + if len(authentication) == 0 { + return nil + } + + return authentication +} + +func findOIDCSecuritySchemesFromRequirements(doc *openapi3.T, secRequirements openapi3.SecurityRequirements) *openapi3.SecurityScheme { + for _, secReq := range secRequirements { + for secReqItemName, _ := range secReq { + secScheme, ok := doc.Components.SecuritySchemes[secReqItemName] + if !ok { + // should never happen. OpenAPI validation should detect this issue + continue + } + if secScheme == nil || secScheme.Value == nil { + continue + } + // Ref https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-23 + if secScheme.Value.Type == "openIdConnect" { + return secScheme.Value + } + } + } + + return nil +} From 1bc55f3e1d7f2f9c888bb76525fc6cb7bf0c8b2d Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Thu, 16 Nov 2023 15:46:19 +0100 Subject: [PATCH 17/28] fix lint issue --- pkg/kuadrantapi/authpolicy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go index 4dc84a5..23df49a 100644 --- a/pkg/kuadrantapi/authpolicy.go +++ b/pkg/kuadrantapi/authpolicy.go @@ -92,7 +92,7 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta func findOIDCSecuritySchemesFromRequirements(doc *openapi3.T, secRequirements openapi3.SecurityRequirements) *openapi3.SecurityScheme { for _, secReq := range secRequirements { - for secReqItemName, _ := range secReq { + for secReqItemName := range secReq { secScheme, ok := doc.Components.SecuritySchemes[secReqItemName] if !ok { // should never happen. OpenAPI validation should detect this issue From 4334377bf10b7cb560488555e701cd5e4177c30e Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Mon, 27 Nov 2023 22:28:03 +0100 Subject: [PATCH 18/28] rebase --- pkg/kuadrantapi/authpolicy.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go index 23df49a..4fdf391 100644 --- a/pkg/kuadrantapi/authpolicy.go +++ b/pkg/kuadrantapi/authpolicy.go @@ -16,8 +16,8 @@ func AuthPolicyObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { return gatewayapi.HTTPRouteObjectMetaFromOAS(doc) } -func buildAuthPolicyRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation) []kuadrantapiv1beta2.RouteSelector { - match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op) +func buildAuthPolicyRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType) []kuadrantapiv1beta2.RouteSelector { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op, pathMatchType) return []kuadrantapiv1beta2.RouteSelector{ { @@ -41,8 +41,6 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta panic(err) } - pathEnabled := kuadrantPathExtension.IsEnabled() - // Operations for verb, operation := range pathItem.Operations() { kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) @@ -50,7 +48,7 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta panic(err) } - if !ptr.Deref(kuadrantOperationExtension.Enable, pathEnabled) { + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { // not enabled for the operation //fmt.Printf("OUT not enabled: path: %s, method: %s\n", path, verb) continue @@ -64,13 +62,19 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta continue } + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + oidcScheme := findOIDCSecuritySchemesFromRequirements(doc, secRequirements) authName := utils.OpenAPIOperationName(path, verb, operation) authentication[authName] = kuadrantapiv1beta2.AuthenticationSpec{ CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ - RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, operation), + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, operation, pathMatchType), }, AuthenticationSpec: authorinoapi.AuthenticationSpec{ AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ From c07c147d51b7aa89b8a00095e3661339f9265667 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 00:58:24 +0100 Subject: [PATCH 19/28] change kind cluster name --- make/kind.mk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/make/kind.mk b/make/kind.mk index 4c26b5b..12f9aab 100644 --- a/make/kind.mk +++ b/make/kind.mk @@ -3,12 +3,12 @@ ## Targets to help install and use kind for development https://kind.sigs.k8s.io -KIND_CLUSTER_NAME ?= kuadrant-local +KIND_CLUSTER_NAME ?= kuadrantctl-local .PHONY: kind-create-cluster -kind-create-cluster: kind ## Create the "kuadrant-local" kind cluster. +kind-create-cluster: kind ## Create the "kuadrantctl-local" kind cluster. $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config utils/kind-cluster.yaml .PHONY: kind-delete-cluster -kind-delete-cluster: kind ## Delete the "kuadrant-local" kind cluster. +kind-delete-cluster: kind ## Delete the "kuadrantctl-local" kind cluster. - $(KIND) delete cluster --name $(KIND_CLUSTER_NAME) From 1f67bfb82c5374b9cef5c5828034b86ed557c462 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 11:28:03 +0100 Subject: [PATCH 20/28] kuadrant authpolicy command: top level route selectors --- cmd/generate_kuadrant_authpolicy.go | 1 + pkg/kuadrantapi/authpolicy.go | 54 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/cmd/generate_kuadrant_authpolicy.go b/cmd/generate_kuadrant_authpolicy.go index a5c3268..177e50a 100644 --- a/cmd/generate_kuadrant_authpolicy.go +++ b/cmd/generate_kuadrant_authpolicy.go @@ -83,6 +83,7 @@ func buildAuthPolicy(doc *openapi3.T) *kuadrantapiv1beta2.AuthPolicy { AuthScheme: kuadrantapiv1beta2.AuthSchemeSpec{ Authentication: kuadrantapi.AuthPolicyAuthenticationSchemeFromOAS(doc), }, + RouteSelectors: kuadrantapi.AuthPolicyTopRouteSelectorsFromOAS(doc), }, } diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go index 4fdf391..d6ffee5 100644 --- a/pkg/kuadrantapi/authpolicy.go +++ b/pkg/kuadrantapi/authpolicy.go @@ -26,6 +26,60 @@ func buildAuthPolicyRouteSelectors(basePath, path string, pathItem *openapi3.Pat } } +func AuthPolicyTopRouteSelectorsFromOAS(doc *openapi3.T) []kuadrantapiv1beta2.RouteSelector { + routeSelectors := make([]kuadrantapiv1beta2.RouteSelector, 0) + + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + + for path, pathItem := range doc.Paths { + kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) + if err != nil { + panic(err) + } + + // Operations + for verb, operation := range pathItem.Operations() { + kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) + if err != nil { + panic(err) + } + + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { + // not enabled for the operation + //fmt.Printf("OUT not enabled: path: %s, method: %s\n", path, verb) + continue + } + + // Get operation level security requirements or fallback to global security requirements + secRequirements := ptr.Deref(operation.Security, doc.Security) + + // Top RouteSelectors define the matching rules to call external auth service + // group together any routes that has at least one security requirement + if len(secRequirements) == 0 { + // no security + continue + } + + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + + routeSelectors = append(routeSelectors, buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, operation, pathMatchType)...) + } + } + + if len(routeSelectors) == 0 { + return nil + } + + return routeSelectors +} + func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2.AuthenticationSpec { authentication := make(map[string]kuadrantapiv1beta2.AuthenticationSpec) From e06a4e864b71a10d0b0c6660648973701c674679 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 14:16:18 +0100 Subject: [PATCH 21/28] doc/generate-kuadrant-rate-limit-policy.md --- doc/generate-kuadrant-rate-limit-policy.md | 183 +++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 doc/generate-kuadrant-rate-limit-policy.md diff --git a/doc/generate-kuadrant-rate-limit-policy.md b/doc/generate-kuadrant-rate-limit-policy.md new file mode 100644 index 0000000..7a5dbee --- /dev/null +++ b/doc/generate-kuadrant-rate-limit-policy.md @@ -0,0 +1,183 @@ +## Generate Kuadrant RateLimitPolicy object from OpenAPI 3 + +The `kuadrantctl generate kuadrant ratelimitpolicy` command generates an [Kuadrant RateLimitPolicy](https://github.com/Kuadrant/kuadrant-operator/blob/v0.4.1/doc/rate-limiting.md) +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 + +[OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) + +OpenAPI document resource can be provided by one of the following channels: +* Filename in the available path. +* 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 + +```shell +Generate Kuadrant RateLimitPolicy from OpenAPI 3.0.X + +Usage: + kuadrantctl generate kuadrant ratelimitpolicy [flags] + +Flags: + -h, --help help for ratelimitpolicy + --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 +``` + +> Under the example folder there are examples of OAS 3 that can be used to generate the resources + +### User Guide + +* Clone the repo +```bash +git clone https://github.com/Kuadrant/kuadrantctl.git +cd kuadrantctl +``` +* Setup cluster, istio and Gateway API CRDs +```bash +make local-setup +``` +* Build and install CLI in `bin/kuadrantctl` path +```bash +make install +``` +* Install Kuadrant service protection. The CLI can be used to install kuadrant v0.4.1 +```bash +bin/kuadrantctl install +``` +* Deploy petstore backend API +```bash +kubectl create namespace petstore +kubectl apply -n petstore -f examples/petstore/petstore.yaml +``` +* Let's create Petstore's OpenAPI spec + +
+ +```yaml +cat <petstore-openapi.yaml +--- +openapi: "3.0.3" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system +servers: + - url: https://example.io/v1 +paths: + /cat: + x-kuadrant: ## Path level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + rate_limit: + rates: + - limit: 1 + duration: 10 + unit: second + counters: + - request.headers.x-forwarded-for + get: # Added to the route and rate limited + operationId: "getCat" + responses: + 405: + description: "invalid input" + post: # NOT added to the route + x-kuadrant: + disable: true + operationId: "postCat" + responses: + 405: + description: "invalid input" + /dog: + get: # Added to the route and rate limited + x-kuadrant: ## Operation level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + rate_limit: + rates: + - limit: 3 + duration: 10 + unit: second + counters: + - request.headers.x-forwarded-for + operationId: "getDog" + responses: + 405: + description: "invalid input" + post: # Added to the route and NOT rate limited + x-kuadrant: ## Operation level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + operationId: "postDog" + responses: + 405: + description: "invalid input" +EOF +``` + +
+ +> **NOTE**: `servers` base path not included. WIP in following up PRs. + +| Operation | Applied config | +| --- | --- | +| `GET /cat` | It should return 200 Ok and be rate limited (1 req / 10 seconds) | +| `POST /cat` | Not added to the HTTPRoute. It should return 404 Not Found | +| `GET /dog` | It should return 200 Ok and be rate limited (3 req / 10 seconds) | +| `POST /dog` | It should return 200 Ok and NOT rate limited | + + +* Create the HTTPRoute using the CLI +```bash +bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +* Create the Rate Limit Policy +```bash +bin/kuadrantctl generate kuadrant ratelimitpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +* Test OpenAPI endpoints + * `GET /cat` -> It should return 200 Ok and be rate limited (1 req / 10 seconds) + +```bash +curl --resolve example.com:9080:127.0.0.1 -v "http://example.com:9080/cat" +``` + * `POST /cat` -> Not added to the HTTPRoute. It should return 404 Not Found +```bash +curl --resolve example.com:9080:127.0.0.1 -v -X POST "http://example.com:9080/cat" +``` + * `GET /dog` -> It should return 200 Ok and be rate limited (3 req / 10 seconds) + +```bash +curl --resolve example.com:9080:127.0.0.1 -v "http://example.com:9080/dog" +``` + + * `POST /dog` -> It should return 200 Ok and NOT rate limited + +```bash +curl --resolve example.com:9080:127.0.0.1 -v -X POST "http://example.com:9080/dog" +``` + +* Clean environment +```bash +make local-cleanup +``` From ff8fbfe9c02c12b2ec004b2ce8e11094ac7cff8d Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 14:38:15 +0100 Subject: [PATCH 22/28] added doc --- doc/generate-kuadrant-auth-policy.md | 251 ++++++++++++++++++ ...etstore-with-oidc-kuadrant-extensions.yaml | 2 - ...e-with-rate-limit-kuadrant-extensions.yaml | 22 +- 3 files changed, 252 insertions(+), 23 deletions(-) create mode 100644 doc/generate-kuadrant-auth-policy.md diff --git a/doc/generate-kuadrant-auth-policy.md b/doc/generate-kuadrant-auth-policy.md new file mode 100644 index 0000000..2566d50 --- /dev/null +++ b/doc/generate-kuadrant-auth-policy.md @@ -0,0 +1,251 @@ +## Generate Kuadrant AuthPolicy object from OpenAPI 3 + +The `kuadrantctl generate kuadrant authpolicy` command generates an [Kuadrant AuthPolicy](https://github.com/Kuadrant/kuadrant-operator/blob/v0.4.1/doc/auth.md) +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 + +[OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) + +OpenAPI document resource can be provided by one of the following channels: +* Filename in the available path. +* URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address. +* Read from stdin standard input stream. + +#### openIdConnect type +This initial version of the command only generates AuhPolicy when there is at least one security requirement referencing the +[Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) which type is `openIdConnect`. + +### Description + +The following OAS example has one protected endpoint `GET /dog` with OIDC sec scheme. + +```yaml +paths: + /dog: + get: + operationId: "getDog" + security: + - securedDog: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + securedDog: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration +``` + +Running the command + +``` +kuadrantctl generate kuadrant authpolicy --oas ./petstore-openapi.yaml | yq -P +``` + +The generated authpolicy (only relevan fields shown here): + +```yaml +kind: AuthPolicy +apiVersion: kuadrant.io/v1beta2 +metadata: + name: petstore + namespace: petstore + creationTimestamp: null +spec: + routeSelectors: + - matches: + - path: + type: Exact + value: /api/v1/dog + method: GET + rules: + authentication: + getDog: + credentials: {} + jwt: + issuerUrl: https://example.com/.well-known/openid-configuration + routeSelectors: + - matches: + - path: + type: Exact + value: /api/v1/dog + method: GET +``` + +### Usage + +```shell +Generate Kuadrant AuthPolicy from OpenAPI 3.0.X + +Usage: + kuadrantctl generate kuadrant authpolicy [flags] + +Flags: + -h, --help help for authpolicy + --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 +``` + +> Under the example folder there are examples of OAS 3 that can be used to generate the resources + +### User Guide + +* [Optional] Setup SSO service supporting OIDC. For this example, we will be using [keycloak](https://www.keycloak.org). + * Create a new realm `petstore` + * Create a client `petstore`. In the Client Protocol field, select `openid-connect`. + * Configure client settings. Access Type to public. Direct Access Grants Enabled to ON (for this example password will be used directly to generate the token). + * Add a user to the realm + * Click the Users menu on the left side of the window. Click Add user. + * Type the username `bob`, set the Email Verified switch to ON, and click Save. + * On the Credentials tab, set the password `p`. Enter the password in both the fields, set the Temporary switch to OFF to avoid the password reset at the next login, and click `Set Password`. + +Now, let's run local cluster to test the kuadrantctl new command to generate authpolicy. + +* Clone the repo + +```bash +git clone https://github.com/Kuadrant/kuadrantctl.git +cd kuadrantctl +``` + +* Setup cluster, istio and Gateway API CRDs + +```bash +make local-setup +``` + +* Build and install CLI in `bin/kuadrantctl` path + +```bash +make install +``` + +* Install Kuadrant service protection. The CLI can be used to install kuadrant v0.4.1 + +```bash +bin/kuadrantctl install +``` + +* Deploy petstore backend API + +```bash +kubectl create namespace petstore +kubectl apply -n petstore -f examples/petstore/petstore.yaml +``` + +* Let's create Petstore's OpenAPI spec + +
+ +```yaml +cat <petstore-openapi.yaml +--- +openapi: "3.0.3" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system +servers: + - url: https://example.io/api/v1 +paths: + /cat: + x-kuadrant: + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # public (not auth) + operationId: "getCat" + responses: + 405: + description: "invalid input" + /dog: + x-kuadrant: + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # secured + operationId: "getDog" + security: + - openIdConnect: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + openIdConnect: + type: openIdConnect + openIdConnectUrl: https://${KEYCLOAK_PUBLIC_DOMAIN}/realms/petstore +EOF +``` +
+ +> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain + +| Operation | Applied config | +| --- | --- | +| `GET /api/v1/cat` | public (not auth) | +| `GET /api/v1/dog` | OIDC authenticatred | + +* Create the HTTPRoute using the CLI +```bash +bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +* Create Kuadrant's Auth Policy +```bash +bin/kuadrantctl generate kuadrant authpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +Now, we are ready to test OpenAPI endpoints :exclamation: + +- `GET /api/v1/cat` -> It's a public endpoint, hence should return 200 Ok +```bash +curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/cat" +``` +- `GET /api/v1/dog` -> It's a secured endpoint, hence, without credentials, it should return 401 +```bash +curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/dog" +``` +``` +HTTP/1.1 401 Unauthorized +www-authenticate: Bearer realm="getDog" +x-ext-auth-reason: credential not found +date: Tue, 28 Nov 2023 09:38:26 GMT +server: istio-envoy +content-length: 0 +``` +- Get authentication token. This example is using Direct Access Grants oauth2 grant type (also known as Client Credentials grant type). When configuring the Keycloak (OIDC provider) client settings, we enabled Direct Access Grants to enable this procedure. We will be authenticating as `bob` user with `p` password. We previously created `bob` user in Keycloak in the `petstore` realm. +``` +export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencoded" \ + -d 'grant_type=password' \ + -d 'client_id=petstore' \ + -d 'scope=openid' \ + -d 'username=bob' \ + -d 'password=p' "https://${KEYCLOAK_PUBLIC_DOMAIN}/realms/petstore/protocol/openid-connect/token" | jq -r '.access_token') +``` +> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain + +With the access token in place, let's try to get those puppies + +```bash +curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/dog -i +``` +should return 200 Ok + +* Clean environment +```bash +make local-cleanup +``` diff --git a/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml b/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml index e6ef951..cdcc0b8 100644 --- a/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml +++ b/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml @@ -17,7 +17,6 @@ servers: paths: /cat: x-kuadrant: ## Path level Kuadrant Extension - enable: true backendRefs: - name: petstore port: 80 @@ -37,7 +36,6 @@ paths: /dog: get: # Added to the route and authenticated x-kuadrant: ## Operation level Kuadrant Extension - enable: true backendRefs: - name: petstore port: 80 diff --git a/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml b/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml index 1a3b444..f6e5548 100644 --- a/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml +++ b/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml @@ -17,7 +17,6 @@ servers: paths: /cat: x-kuadrant: ## Path level Kuadrant Extension - enable: true backendRefs: - name: petstore port: 80 @@ -36,18 +35,7 @@ paths: description: "invalid input" post: # NOT added to the route x-kuadrant: ## Operation level Kuadrant Extension - enable: false - backendRefs: - - name: petstore - port: 80 - namespace: petstore - rate_limit: - rates: - - limit: 2 - duration: 10 - unit: second - counters: - - request.headers.x-forwarded-for + disable: true operationId: "postCat" responses: 405: @@ -55,7 +43,6 @@ paths: /dog: get: # Added to the route and rate limited x-kuadrant: ## Operation level Kuadrant Extension - enable: true backendRefs: - name: petstore port: 80 @@ -73,7 +60,6 @@ paths: description: "invalid input" post: # Added to the route and NOT rate limited x-kuadrant: ## Operation level Kuadrant Extension - enable: true backendRefs: - name: petstore port: 80 @@ -82,9 +68,3 @@ paths: responses: 405: description: "invalid input" - /mouse: - get: # NOT added to the route - operationId: "getMouse" - responses: - 405: - description: "invalid input" From 039fccca608c8f94ba033f661d53f695dcd2538e Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 14:46:16 +0100 Subject: [PATCH 23/28] keycloak 18.0.9 still requires deprecated /auth prefix --- doc/generate-kuadrant-auth-policy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/generate-kuadrant-auth-policy.md b/doc/generate-kuadrant-auth-policy.md index 2566d50..39e99a6 100644 --- a/doc/generate-kuadrant-auth-policy.md +++ b/doc/generate-kuadrant-auth-policy.md @@ -187,7 +187,7 @@ components: securitySchemes: openIdConnect: type: openIdConnect - openIdConnectUrl: https://${KEYCLOAK_PUBLIC_DOMAIN}/realms/petstore + openIdConnectUrl: https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore EOF ``` @@ -234,7 +234,7 @@ export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencode -d 'client_id=petstore' \ -d 'scope=openid' \ -d 'username=bob' \ - -d 'password=p' "https://${KEYCLOAK_PUBLIC_DOMAIN}/realms/petstore/protocol/openid-connect/token" | jq -r '.access_token') + -d 'password=p' "https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore/protocol/openid-connect/token" | jq -r '.access_token') ``` > Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain From fdb047b3392246973677b4751c22df43421d7ccc Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 17:41:19 +0100 Subject: [PATCH 24/28] kuadrant authpolicy command: skip when oidc not found --- pkg/kuadrantapi/authpolicy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go index d6ffee5..b302edd 100644 --- a/pkg/kuadrantapi/authpolicy.go +++ b/pkg/kuadrantapi/authpolicy.go @@ -124,6 +124,11 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta oidcScheme := findOIDCSecuritySchemesFromRequirements(doc, secRequirements) + if oidcScheme == nil { + // no oidc sec scheme found + continue + } + authName := utils.OpenAPIOperationName(path, verb, operation) authentication[authName] = kuadrantapiv1beta2.AuthenticationSpec{ From 6f13b4867a1d1c60f696c9ab95f18fd02d0399bc Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 18:18:41 +0100 Subject: [PATCH 25/28] kuadrant authpolicy command: support apikey --- .../petstore-multiple-sec-requirements.yaml | 32 +++- pkg/kuadrantapi/authpolicy.go | 147 ++++++++++++++---- pkg/utils/maps.go | 12 ++ 3 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 pkg/utils/maps.go diff --git a/examples/oas3/petstore-multiple-sec-requirements.yaml b/examples/oas3/petstore-multiple-sec-requirements.yaml index 7afe0ff..91b2184 100644 --- a/examples/oas3/petstore-multiple-sec-requirements.yaml +++ b/examples/oas3/petstore-multiple-sec-requirements.yaml @@ -3,6 +3,15 @@ openapi: "3.1.0" info: title: "Pet Store API" version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system servers: - url: https://toplevel.example.io/v1 paths: @@ -15,7 +24,7 @@ paths: post: # API key operationId: "postCat" security: - - petstore_api_key: [] + - cat_api_key: [] responses: 405: description: "invalid input" @@ -23,17 +32,30 @@ paths: get: # OIDC operationId: "getDog" security: - - petstore_oidc: + - oidc: - read:dogs responses: 405: description: "invalid input" + /snake: + get: # OIDC or API key + operationId: "getSnake" + security: + - oidc: ["read:snakes"] + - snakes_api_key: [] + responses: + 405: + description: "invalid input" components: securitySchemes: - petstore_api_key: + cat_api_key: type: apiKey name: api_key in: header - petstore_oidc: + oidc: type: openIdConnect - openIdConnectUrl: http://example.org/auth/realms/myrealm + openIdConnectUrl: https://example.com/.well-known/openid-configuration + snakes_api_key: + type: apiKey + name: snake_token + in: query diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go index b302edd..07457b6 100644 --- a/pkg/kuadrantapi/authpolicy.go +++ b/pkg/kuadrantapi/authpolicy.go @@ -1,6 +1,9 @@ package kuadrantapi import ( + "errors" + "fmt" + "github.com/getkin/kin-openapi/openapi3" authorinoapi "github.com/kuadrant/authorino/api/v1beta2" kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" @@ -12,6 +15,10 @@ import ( "github.com/kuadrant/kuadrantctl/pkg/utils" ) +const ( + APIKeySecretLabel = "kuadrant.io/apikeys-by" +) + func AuthPolicyObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { return gatewayapi.HTTPRouteObjectMetaFromOAS(doc) } @@ -122,27 +129,10 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta kuadrantPathExtension.GetPathMatchType(), ) - oidcScheme := findOIDCSecuritySchemesFromRequirements(doc, secRequirements) - - if oidcScheme == nil { - // no oidc sec scheme found - continue - } - - authName := utils.OpenAPIOperationName(path, verb, operation) + operationAuthentication := buildOperationAuthentication(doc, basePath, path, pathItem, verb, operation, pathMatchType, secRequirements) - authentication[authName] = kuadrantapiv1beta2.AuthenticationSpec{ - CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ - RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, operation, pathMatchType), - }, - AuthenticationSpec: authorinoapi.AuthenticationSpec{ - AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ - Jwt: &authorinoapi.JwtAuthenticationSpec{ - IssuerUrl: oidcScheme.OpenIdConnectUrl, - }, - }, - }, - } + // Aggregate auth methods per operation + authentication = utils.MergeMaps(authentication, operationAuthentication) } } @@ -153,23 +143,112 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta return authentication } -func findOIDCSecuritySchemesFromRequirements(doc *openapi3.T, secRequirements openapi3.SecurityRequirements) *openapi3.SecurityScheme { +func buildOperationAuthentication(doc *openapi3.T, basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secRequirements openapi3.SecurityRequirements) map[string]kuadrantapiv1beta2.AuthenticationSpec { + // OpenAPI supports as security requirement to have multiple security schemes and ALL + // of the must be satisfied. + // From https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object + // Kuadrant does not support it yet: https://github.com/Kuadrant/authorino/issues/112 + // not supported (AND'ed) + // security: + // - petstore_api_key: [] + // petstore_oidc: [] + // supported (OR'ed) + // security: + // - petstore_api_key: [] + // - petstore_oidc: [] + + opAuth := make(map[string]kuadrantapiv1beta2.AuthenticationSpec, 0) for _, secReq := range secRequirements { - for secReqItemName := range secReq { - secScheme, ok := doc.Components.SecuritySchemes[secReqItemName] - if !ok { - // should never happen. OpenAPI validation should detect this issue - continue - } - if secScheme == nil || secScheme.Value == nil { - continue - } - // Ref https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-23 - if secScheme.Value.Type == "openIdConnect" { - return secScheme.Value + if len(secReq) > 1 { + panic(errors.New("multiple schemes that require ALL must be satisfied, currently not supported")) + } + + extractSecReqItemName := func(sr openapi3.SecurityRequirement) string { + for secReqItemName := range sr { + return secReqItemName } + + return "" + } + + secReqItemName := extractSecReqItemName(secReq) + + secScheme, ok := doc.Components.SecuritySchemes[secReqItemName] + if !ok { + // should never happen. OpenAPI validation should detect this issue + continue + } + + if secScheme == nil || secScheme.Value == nil { + continue } + + authName := fmt.Sprintf("%s_%s", utils.OpenAPIOperationName(path, verb, op), secReqItemName) + + // Ref https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-23 + switch secScheme.Value.Type { + case "openIdConnect": + opAuth[authName] = openIDAuthenticationSpec(basePath, path, pathItem, verb, op, pathMatchType, *secScheme.Value) + case "apiKey": + opAuth[authName] = apiKeyAuthenticationSpec(basePath, path, pathItem, verb, op, pathMatchType, secReqItemName, *secScheme.Value) + } + } + + if len(opAuth) == 0 { + return nil } - return nil + return opAuth +} + +func apiKeyAuthenticationSpec(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secSchemeName string, secScheme openapi3.SecurityScheme) kuadrantapiv1beta2.AuthenticationSpec { + // From https://github.com/Kuadrant/kuadrantctl/pull/46#issuecomment-1830278191 + // secScheme.In is required + // secScheme.Name is required + credentials := authorinoapi.Credentials{} + switch secScheme.In { + case "query": + credentials.QueryString = &authorinoapi.Named{Name: secScheme.Name} + case "header": + credentials.CustomHeader = &authorinoapi.CustomHeader{ + Named: authorinoapi.Named{Name: secScheme.Name}, + } + case "cookie": + credentials.Cookie = &authorinoapi.Named{Name: secScheme.Name} + } + + return kuadrantapiv1beta2.AuthenticationSpec{ + CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, op, pathMatchType), + }, + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + Credentials: credentials, + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + // label selector be like + // kuadrant.io/apikeys-by: ${SecuritySchemeName} + APIKeySecretLabel: secSchemeName, + }, + }, + }, + }, + }, + } +} + +func openIDAuthenticationSpec(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secScheme openapi3.SecurityScheme) kuadrantapiv1beta2.AuthenticationSpec { + return kuadrantapiv1beta2.AuthenticationSpec{ + CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, op, pathMatchType), + }, + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + Jwt: &authorinoapi.JwtAuthenticationSpec{ + IssuerUrl: secScheme.OpenIdConnectUrl, + }, + }, + }, + } } diff --git a/pkg/utils/maps.go b/pkg/utils/maps.go new file mode 100644 index 0000000..c423dc4 --- /dev/null +++ b/pkg/utils/maps.go @@ -0,0 +1,12 @@ +package utils + +func MergeMaps[K comparable, V any](MyMap1 map[K]V, MyMap2 map[K]V) map[K]V { + merged := make(map[K]V) + for key, val := range MyMap1 { + merged[key] = val + } + for key, val := range MyMap2 { + merged[key] = val + } + return merged +} From 95f56b3442b2da1b910315009e91eb4713c013fc Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 23:59:07 +0100 Subject: [PATCH 26/28] update GH actions --- .github/workflows/commands.yaml | 4 +++- .github/workflows/testing.yaml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/commands.yaml b/.github/workflows/commands.yaml index b69e4d4..272ff58 100644 --- a/.github/workflows/commands.yaml +++ b/.github/workflows/commands.yaml @@ -12,6 +12,8 @@ jobs: install: name: Run kuadrantctl install runs-on: ubuntu-latest + env: + KIND_CLUSTER_NAME: kuadrantctl-local steps: - name: Set up Go 1.21.x uses: actions/setup-go@v4 @@ -21,7 +23,7 @@ jobs: - name: Check out code uses: actions/checkout@v3 - name: Create k8s Kind Cluster - uses: helm/kind-action@v1.2.0 + uses: helm/kind-action@v1.8.0 with: version: v0.20.0 config: utils/kind-cluster.yaml diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index ce4a31d..394bb75 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -28,7 +28,7 @@ jobs: name: Run tests runs-on: ubuntu-latest env: - KIND_CLUSTER_NAME: kuadrant-local + KIND_CLUSTER_NAME: kuadrantctl-local steps: - name: Set up Go 1.21.x uses: actions/setup-go@v4 @@ -38,7 +38,7 @@ jobs: - name: Check out code uses: actions/checkout@v3 - name: Create k8s Kind Cluster - uses: helm/kind-action@v1.2.0 + uses: helm/kind-action@v1.8.0 with: version: v0.20.0 config: utils/kind-cluster.yaml From 546d66f102d87ebddc6d8dbfb76344fedbea9b57 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 29 Nov 2023 00:21:41 +0100 Subject: [PATCH 27/28] kuadrant authpolicy command: doc --- doc/generate-kuadrant-auth-policy.md | 292 +++++++++++++++++++++++++-- 1 file changed, 270 insertions(+), 22 deletions(-) diff --git a/doc/generate-kuadrant-auth-policy.md b/doc/generate-kuadrant-auth-policy.md index 39e99a6..732bb5d 100644 --- a/doc/generate-kuadrant-auth-policy.md +++ b/doc/generate-kuadrant-auth-policy.md @@ -12,13 +12,18 @@ 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. -#### openIdConnect type -This initial version of the command only generates AuhPolicy when there is at least one security requirement referencing the -[Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) which type is `openIdConnect`. +OpenAPI [Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) types -### Description +| Types | Implemented | +| --- | --- | +| `openIdConnect` | **YES** | +| `apiKey` | **YES** | +| `http` | NO | +| `oauth2` | NO | + +### `openIdConnect` Type Description -The following OAS example has one protected endpoint `GET /dog` with OIDC sec scheme. +The following OAS example has one protected endpoint `GET /dog` with `openIdConnect` security scheme type. ```yaml paths: @@ -61,7 +66,7 @@ spec: method: GET rules: authentication: - getDog: + getDog_securedDog: credentials: {} jwt: issuerUrl: https://example.com/.well-known/openid-configuration @@ -73,6 +78,91 @@ spec: method: GET ``` +### `apiKey` Type Description + +The following OAS example has one protected endpoint `GET /dog` with `apiKey` security scheme type. + +```yaml +paths: + /dog: + get: + operationId: "getDog" + security: + - securedDog: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + securedDog: + type: apiKey + name: dog_token + in: query +``` + +Running the command + +``` +kuadrantctl generate kuadrant authpolicy --oas ./petstore-openapi.yaml | yq -P +``` + +The generated authpolicy (only relevan fields shown here): + +```yaml +kind: AuthPolicy +apiVersion: kuadrant.io/v1beta2 +metadata: + name: petstore + namespace: petstore + creationTimestamp: null +spec: + routeSelectors: + - matches: + - path: + type: Exact + value: /dog + method: GET + rules: + authentication: + getDog_securedDog: + credentials: + queryString: + name: dog_token + apiKey: + selector: + matchLabels: + kuadrant.io/apikeys-by: securedDog + routeSelectors: + - matches: + - path: + type: Exact + value: /dog + method: GET +``` + +In this particular example, the endpoint `GET /dog` will be protected. +The token needs to be in the query string of the request included in a parameter named `dog_token`. +Kuadrant will validate received tokens against tokens found in kubernetes secrets with label `kuadrant.io/apikeys-by: ${sec scheme name}`. +In this particular example the label selector will be: `kuadrant.io/apikeys-by: securedDog`. + +Like the following example: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: api-key-1 + labels: + authorino.kuadrant.io/managed-by: authorino + kuadrant.io/apikeys-by: securedDog +stringData: + api_key: MYSECRETTOKENVALUE +type: Opaque +``` +> **Note**: Kuadrant validates tokens against api keys found in secrets. The label selector format `kuadrant.io/apikeys-by: ${sec scheme name}` is arbitrary and designed for this CLI command. + +For more information about Kuadrant auth based on api key: https://docs.kuadrant.io/authorino/docs/user-guides/api-key-authentication/ + ### Usage ```shell @@ -93,6 +183,16 @@ Global Flags: ### User Guide +The verification steps will lead you to the process of deploying and testing the following api with +endpoints protected using different security schemes: + +| Operation | Security Scheme | +| --- | --- | +| `GET /api/v1/cat` | public (not auth) | +| `POST /api/v1/cat` | ApiKey in header | +| `GET /api/v1/dog` | OpenIdConnect | +| `GET /api/v1/snake` | OpenIdConnect **OR** ApiKey in query string | + * [Optional] Setup SSO service supporting OIDC. For this example, we will be using [keycloak](https://www.keycloak.org). * Create a new realm `petstore` * Create a client `petstore`. In the Client Protocol field, select `openid-connect`. @@ -143,7 +243,7 @@ kubectl apply -n petstore -f examples/petstore/petstore.yaml ```yaml cat <petstore-openapi.yaml --- -openapi: "3.0.3" +openapi: "3.1.0" info: title: "Pet Store API" version: "1.0.0" @@ -165,46 +265,112 @@ paths: - name: petstore port: 80 namespace: petstore - get: # public (not auth) + get: # No sec requirements operationId: "getCat" responses: 405: description: "invalid input" + post: # API key + operationId: "postCat" + security: + - cat_api_key: [] + responses: + 405: + description: "invalid input" /dog: x-kuadrant: backendRefs: - name: petstore port: 80 namespace: petstore - get: # secured + get: # OIDC operationId: "getDog" security: - - openIdConnect: [] + - oidc: + - read:dogs + responses: + 405: + description: "invalid input" + /snake: + x-kuadrant: + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # OIDC or API key + operationId: "getSnake" + security: + - oidc: ["read:snakes"] + - snakes_api_key: [] responses: 405: description: "invalid input" components: securitySchemes: - openIdConnect: + cat_api_key: + type: apiKey + name: api_key + in: header + oidc: type: openIdConnect openIdConnectUrl: https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore + snakes_api_key: + type: apiKey + name: snake_token + in: query EOF ``` + > Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain -| Operation | Applied config | -| --- | --- | -| `GET /api/v1/cat` | public (not auth) | -| `GET /api/v1/dog` | OIDC authenticatred | +* Create an API key only valid for `POST /api/v1/cat` endpoint +```yaml +kubectl apply -f -< **Note**: the label's value of `kuadrant.io/apikeys-by: cat_api_key` is the name of the sec scheme of the OpenAPI spec. + +* Create an API key only valid for `GET /api/v1/snake` endpoint + +```yaml +kubectl apply -f -< **Note**: the label's value of `kuadrant.io/apikeys-by: snakes_api_key` is the name of the sec scheme of the OpenAPI spec. * Create the HTTPRoute using the CLI + ```bash bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f - ``` * Create Kuadrant's Auth Policy + ```bash bin/kuadrantctl generate kuadrant authpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f - ``` @@ -212,22 +378,76 @@ bin/kuadrantctl generate kuadrant authpolicy --oas petstore-openapi.yaml | kubec Now, we are ready to test OpenAPI endpoints :exclamation: - `GET /api/v1/cat` -> It's a public endpoint, hence should return 200 Ok + ```bash curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/cat" ``` -- `GET /api/v1/dog` -> It's a secured endpoint, hence, without credentials, it should return 401 + +- `POST /api/v1/cat` -> It's a protected endpoint with apikey + +Without any credentials, it should return `401 Unauthorized` + ```bash -curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/dog" +curl -H "Host: example.com" -X POST -i "http://127.0.0.1:9080/api/v1/cat" ``` + ``` HTTP/1.1 401 Unauthorized -www-authenticate: Bearer realm="getDog" -x-ext-auth-reason: credential not found -date: Tue, 28 Nov 2023 09:38:26 GMT +www-authenticate: Bearer realm="getDog_oidc" +www-authenticate: Bearer realm="getSnake_oidc" +www-authenticate: snake_token realm="getSnake_snakes_api_key" +www-authenticate: api_key realm="postCat_cat_api_key" +x-ext-auth-reason: {"postCat_cat_api_key":"credential not found"} +date: Tue, 28 Nov 2023 22:28:44 GMT +server: istio-envoy +content-length: 0 +``` + +The *reason* headers tell that `credential not found`. +Credentials satisfying `postCat_cat_api_key` authentication is needed. + +According to the OpenAPI spec, it should be a header named `api_key`. +What if we try a wrong token? one token assigned to other endpoint, +i.e. `I_LIKE_SNAKES` instead of the valid one `I_LIKE_CATS`. It should return `401 Unauthorized`. + +```bash +curl -H "Host: example.com" -H "api_key: I_LIKE_SNAKES" -X POST -i "http://127.0.0.1:9080/api/v1/cat" +``` + +``` +TTP/1.1 401 Unauthorized +www-authenticate: Bearer realm="getDog_oidc" +www-authenticate: Bearer realm="getSnake_oidc" +www-authenticate: snake_token realm="getSnake_snakes_api_key" +www-authenticate: api_key realm="postCat_cat_api_key" +x-ext-auth-reason: {"postCat_cat_api_key":"the API Key provided is invalid"} +date: Tue, 28 Nov 2023 22:32:55 GMT server: istio-envoy content-length: 0 ``` -- Get authentication token. This example is using Direct Access Grants oauth2 grant type (also known as Client Credentials grant type). When configuring the Keycloak (OIDC provider) client settings, we enabled Direct Access Grants to enable this procedure. We will be authenticating as `bob` user with `p` password. We previously created `bob` user in Keycloak in the `petstore` realm. + +The *reason* headers tell that `the API Key provided is invalid`. +Using valid token (from the secret `cat-api-key-1` assigned to `POST /api/v1/cats`) +in the `api_key` header should return 200 Ok + +``` +curl -H "Host: example.com" -H "api_key: I_LIKE_CATS" -X POST -i "http://127.0.0.1:9080/api/v1/cat" +``` + +- `GET /api/v1/dog` -> It's a protected endpoint with oidc (assigned to our keycloak instance and `petstore` realm) + +without credentials, it should return `401 Unauthorized` + +```bash +curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/dog" +``` + +To get the authentication token, this example is using Direct Access Grants oauth2 grant type +(also known as Client Credentials grant type). When configuring the Keycloak (OIDC provider) client +settings, we enabled Direct Access Grants to enable this procedure. +We will be authenticating as `bob` user with `p` password. +We previously created `bob` user in Keycloak in the `petstore` realm. + ``` export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencoded" \ -d 'grant_type=password' \ @@ -236,14 +456,42 @@ export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencode -d 'username=bob' \ -d 'password=p' "https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore/protocol/openid-connect/token" | jq -r '.access_token') ``` + > Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain + With the access token in place, let's try to get those puppies ```bash curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/dog -i ``` -should return 200 Ok + +it should return 200 OK + +- `GET /api/v1/snake` -> It's a protected endpoint with oidc (assigned to our keycloak instance and `petstore` realm) **OR** with apiKey + +This example is to show that multiple security requirements (with *OR* semantics) can be specified +for an OpenAPI operation. + +Without credentials, it should return `401 Unauthorized` + +```bash +curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/snake" +``` + +With the access token in place, it should return 200 OK (unless the token has expired). + +```bash +curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/snake -i +``` + +With apiKey it should also work. According to the OpenAPI spec security scheme, +it should be a query string named `snake_token` and the token needs to be valid token +(from the secret `snake-api-key-1` assigned to `GET /api/v1/snake`) + +```bash +curl -H 'Host: example.com' -i "http://127.0.0.1:9080/api/v1/snake?snake_token=I_LIKE_SNAKES" +``` * Clean environment ```bash From 675c19070ff41e54b8fccc05fb4306fa624e7b6b Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 29 Nov 2023 00:31:05 +0100 Subject: [PATCH 28/28] little fix in a comment --- pkg/kuadrantapi/authpolicy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go index 07457b6..0a87a46 100644 --- a/pkg/kuadrantapi/authpolicy.go +++ b/pkg/kuadrantapi/authpolicy.go @@ -151,11 +151,11 @@ func buildOperationAuthentication(doc *openapi3.T, basePath, path string, pathIt // not supported (AND'ed) // security: // - petstore_api_key: [] - // petstore_oidc: [] + // petstore_oidc: [] // supported (OR'ed) // security: // - petstore_api_key: [] - // - petstore_oidc: [] + // - petstore_oidc: [] opAuth := make(map[string]kuadrantapiv1beta2.AuthenticationSpec, 0) for _, secReq := range secRequirements {