From 42d9d757453f93f6477548c3c374325ced44a524 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Sat, 11 Nov 2023 12:46:09 +0100 Subject: [PATCH] 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