From 64db41bc718e108355d0256a69b8d06977bdbc18 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Thu, 16 Nov 2023 11:38:52 +0100 Subject: [PATCH] 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,