From b2efd21c374a857d53d5d90be8c0821b77f3f0fb Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 26 Apr 2024 11:11:18 +0200 Subject: [PATCH 01/26] Base openapi provider implementation Signed-off-by: Guilherme Cassolato --- cmd/print.go | 1 + go.mod | 8 +- go.sum | 15 + pkg/i2gw/providers/openapi/README.md | 20 + pkg/i2gw/providers/openapi/converter.go | 394 +++++++++ pkg/i2gw/providers/openapi/converter_test.go | 173 ++++ .../openapi/fixtures/input/1-petstore3.yaml | 803 ++++++++++++++++++ .../openapi/fixtures/input/2-hostnames.yaml | 76 ++ .../openapi/fixtures/input/3-parameters.yaml | 60 ++ .../fixtures/input/4-too-many-rules.json | 138 +++ .../openapi/fixtures/output/1-petstore3.yaml | 88 ++ .../openapi/fixtures/output/2-hostnames.yaml | 71 ++ .../openapi/fixtures/output/3-parameters.yaml | 28 + .../fixtures/output/4-too-many-rules.json | 227 +++++ pkg/i2gw/providers/openapi/openapi.go | 70 ++ pkg/i2gw/providers/openapi/resource_reader.go | 54 ++ pkg/i2gw/providers/openapi/storage.go | 47 + pkg/i2gw/providers/openapi/utils.go | 37 + 18 files changed, 2308 insertions(+), 2 deletions(-) create mode 100644 pkg/i2gw/providers/openapi/README.md create mode 100644 pkg/i2gw/providers/openapi/converter.go create mode 100644 pkg/i2gw/providers/openapi/converter_test.go create mode 100644 pkg/i2gw/providers/openapi/fixtures/input/1-petstore3.yaml create mode 100644 pkg/i2gw/providers/openapi/fixtures/input/2-hostnames.yaml create mode 100644 pkg/i2gw/providers/openapi/fixtures/input/3-parameters.yaml create mode 100644 pkg/i2gw/providers/openapi/fixtures/input/4-too-many-rules.json create mode 100644 pkg/i2gw/providers/openapi/fixtures/output/1-petstore3.yaml create mode 100644 pkg/i2gw/providers/openapi/fixtures/output/2-hostnames.yaml create mode 100644 pkg/i2gw/providers/openapi/fixtures/output/3-parameters.yaml create mode 100644 pkg/i2gw/providers/openapi/fixtures/output/4-too-many-rules.json create mode 100644 pkg/i2gw/providers/openapi/openapi.go create mode 100644 pkg/i2gw/providers/openapi/resource_reader.go create mode 100644 pkg/i2gw/providers/openapi/storage.go create mode 100644 pkg/i2gw/providers/openapi/utils.go diff --git a/cmd/print.go b/cmd/print.go index 997a3ed37..95ea0f0ab 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -32,6 +32,7 @@ import ( _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/ingressnginx" _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/istio" _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/kong" + _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/openapi" ) type PrintRunner struct { diff --git a/go.mod b/go.mod index 63a073a18..c632afa5d 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,11 @@ require ( ) require ( + github.com/getkin/kin-openapi v0.124.0 // indirect + github.com/invopop/yaml v0.2.0 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect ) @@ -29,9 +33,9 @@ require ( github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/swag v0.22.8 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect diff --git a/go.sum b/go.sum index 408d27397..97e6f7929 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0n github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 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.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= +github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= @@ -25,11 +27,15 @@ github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNa 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/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 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.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-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= +github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -51,6 +57,7 @@ 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-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= @@ -61,6 +68,8 @@ 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= @@ -87,6 +96,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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -96,6 +107,8 @@ 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.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -113,6 +126,7 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -223,6 +237,7 @@ gopkg.in/yaml.v2 v2.2.8/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= istio.io/api v1.20.0 h1:heE1eQoMsuZlwWOf7Xm8TKqKLNKVs11G/zMe5QyR1u4= diff --git a/pkg/i2gw/providers/openapi/README.md b/pkg/i2gw/providers/openapi/README.md new file mode 100644 index 000000000..40f255b75 --- /dev/null +++ b/pkg/i2gw/providers/openapi/README.md @@ -0,0 +1,20 @@ +# OpenAPI Provider + +The provider translates OpenAPI Specification (OAS) 3.x documents to Kubernetes Gateway API resources Gateway and HTTPRoute. + +## Example + +```sh +./ingress2gateway print --providers openapi --input-file=petstore3-openapi.json +``` + +## Known limitations + +* Only offline translation supported – i.e. `--input-file` required +* All API operation [paths](https://swagger.io/specification/v3/#paths-object) treated as `Exact` type – i.e. no support for [path templating](https://swagger.io/specification/v3/#path-templating), therefore no `PathPrefix`, nor `RegularExpression` path types output +* Limited support for [parameters](https://swagger.io/specification/v3/#parameter-object) – only required `header` and `query` parameters supported +* Limited support to [server variables](https://swagger.io/specification/v3/#server-variable-object) – only limited sets (`enum`) supported +* No support to [references](https://swagger.io/specification/v3/#reference-object) (`$ref`) +* No support to [external documents](https://swagger.io/specification/v3/#external-documentation-object) + +Additionally, no support to any OpenAPI feature with no direct equivalent to core Gateway API fields, such as [request bodies](https://swagger.io/specification/v3/#request-body-object), [examples](https://swagger.io/specification/v3/#example-object), [security schemes](https://swagger.io/specification/v3/#security-scheme-object), [callbacks](https://swagger.io/specification/v3/#callback-object), [extensions](https://swagger.io/specification/v3/#specification-extensions), etc. diff --git a/pkg/i2gw/providers/openapi/converter.go b/pkg/i2gw/providers/openapi/converter.go new file mode 100644 index 000000000..4427776c6 --- /dev/null +++ b/pkg/i2gw/providers/openapi/converter.go @@ -0,0 +1,394 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "fmt" + "slices" + "sort" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" +) + +const ( + HostSeparator = "," + ParamSeparator = "," + + HTTPRouteRulesMax = 16 + HTTPRouteMatchesMax = 8 + HTTPRouteMatchesMaxMax = HTTPRouteRulesMax * HTTPRouteMatchesMax +) + +// converter implements the ToGatewayAPI function of i2gw.ResourceConverter interface. +type converter struct { + conf *i2gw.ProviderConf + + featureParsers []i2gw.FeatureParser + implementationSpecificOptions i2gw.ProviderImplementationSpecificOptions +} + +// newConverter returns an ingress-nginx converter instance. +func newConverter(conf *i2gw.ProviderConf) *converter { + return &converter{ + conf: conf, + featureParsers: []i2gw.FeatureParser{ + // The list of feature parsers comes here. + }, + implementationSpecificOptions: i2gw.ProviderImplementationSpecificOptions{ + // The list of the implementationSpecific ingress fields options comes here. + }, + } +} + +func (c *converter) convert(storage *storage) (i2gw.GatewayResources, field.ErrorList) { + gatewayResources := i2gw.GatewayResources{ + Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), + HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), + TLSRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TLSRoute), + TCPRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TCPRoute), + ReferenceGrants: make(map[types.NamespacedName]gatewayv1beta1.ReferenceGrant), + } + + var errors field.ErrorList + + for _, spec := range storage.getResources() { + httpRoutes := toHTTPRoutes(spec, errors) + for _, httpRoute := range httpRoutes { + gatewayResources.HTTPRoutes[types.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}] = httpRoute + } + // TODO: build Gateways + // TODO: add parentRefs to HTTPRoutes + } + + return gatewayResources, errors +} + +type httpRouteRuleMatcher struct { + path string + method string + headers string + params string +} + +type httpRouteRuleMatchers []httpRouteRuleMatcher + +func (m httpRouteRuleMatchers) Len() int { return len(m) } +func (m httpRouteRuleMatchers) Swap(i, j int) { m[i], m[j] = m[j], m[i] } +func (m httpRouteRuleMatchers) Less(i, j int) bool { + if m[i].path != m[j].path { + return m[i].path < m[j].path + } + return m[i].method < m[j].method +} + +type httpRouteMatcher struct { + host string + httpRouteRuleMatcher +} + +func toHTTPRoutes(spec *openapi3.T, errors field.ErrorList) []gatewayv1.HTTPRoute { + var matchers []httpRouteMatcher + + servers := spec.Servers + if len(servers) == 0 { + servers = openapi3.Servers{{URL: "/"}} + } + + paths := spec.Paths.Map() + for _, relativePath := range spec.Paths.InMatchingOrder() { + pathItem := paths[relativePath] + matchers = append(matchers, pathItemToHTTPMatchers(pathItem, relativePath, servers, errors)...) + } + + hostsByHTTPRouteRuleMatcher := make(map[httpRouteRuleMatcher][]string) + for _, matcher := range matchers { + hostsByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher] = append(hostsByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher], matcher.host) + } + + var hostGroups []string + httpRouteRuleMatchersByHosts := make(map[string]httpRouteRuleMatchers) + for matcher, hosts := range hostsByHTTPRouteRuleMatcher { + group := strings.Join(hosts, HostSeparator) + if _, exists := httpRouteRuleMatchersByHosts[group]; !exists { + hostGroups = append(hostGroups, group) + } + httpRouteRuleMatchersByHosts[group] = append(httpRouteRuleMatchersByHosts[group], matcher) + } + + var routes []gatewayv1.HTTPRoute + + // sort host groups for deterministic output + sort.Strings(hostGroups) + + i := 0 + for _, group := range hostGroups { + hosts := strings.Split(group, HostSeparator) + matchers := httpRouteRuleMatchersByHosts[group] + + // sort hostnames and matchers for deterministic output inside each route object + sort.Strings(hosts) + sort.Sort(matchers) + + nMatchers := len(matchers) + nRoutes := nMatchers / HTTPRouteMatchesMaxMax + if nMatchers%HTTPRouteMatchesMaxMax != 0 { + nRoutes++ + } + for j := 0; j < nRoutes; j++ { + routeName := fmt.Sprintf("route-%d-%d", i+1, j+1) + // TODO: name the route after the spec + last := (j + 1) * HTTPRouteMatchesMaxMax + if last > nMatchers { + last = nMatchers + } + routes = append(routes, toHTTPRoute(routeName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last])) + } + i++ + } + + return routes +} + +func toHTTPRoute(name string, hostnames []string, matchers httpRouteRuleMatchers) gatewayv1.HTTPRoute { + route := gatewayv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: gatewayv1.HTTPRouteSpec{ + Rules: toHTTPRouteRules(matchers), + }, + } + if len(hostnames) > 1 || !slices.Contains(hostnames, "") { + route.Spec.Hostnames = Map(hostnames, toGatewayAPIHostname) + } + return route +} + +func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule { + var rules []gatewayv1.HTTPRouteRule + nMatches := len(matchers) + nRules := nMatches / HTTPRouteMatchesMax + if len(matchers)%HTTPRouteMatchesMax != 0 { + nRules++ + } + for i := 0; i < nRules; i++ { + rule := gatewayv1.HTTPRouteRule{} + offfset := i * HTTPRouteMatchesMax + for j := 0; j < HTTPRouteMatchesMax && offfset+j < nMatches; j++ { + matcher := matchers[offfset+j] + ruleMatch := gatewayv1.HTTPRouteMatch{ + Path: &gatewayv1.HTTPPathMatch{ + Type: common.PtrTo(gatewayv1.PathMatchExact), + Value: &matcher.path, + }, + Method: common.PtrTo(gatewayv1.HTTPMethod(matcher.method)), + } + if matcher.headers != "" { + ruleMatch.Headers = Map(strings.Split(matcher.headers, ParamSeparator), func(header string) gatewayv1.HTTPHeaderMatch { + return gatewayv1.HTTPHeaderMatch{ + Name: gatewayv1.HTTPHeaderName(header), + Type: common.PtrTo(gatewayv1.HeaderMatchExact), + } + }) + } + if matcher.params != "" { + ruleMatch.QueryParams = Map(strings.Split(matcher.params, ParamSeparator), func(param string) gatewayv1.HTTPQueryParamMatch { + return gatewayv1.HTTPQueryParamMatch{ + Name: gatewayv1.HTTPHeaderName(param), + Type: common.PtrTo(gatewayv1.QueryParamMatchExact), + } + }) + } + rule.Matches = append(rule.Matches, ruleMatch) + } + // TODO: backendRefs + rules = append(rules, rule) + } + return rules +} + +func pathItemToHTTPMatchers(pathItem *openapi3.PathItem, relativePath string, servers openapi3.Servers, errors field.ErrorList) []httpRouteMatcher { + var matchers []httpRouteMatcher + + if len(pathItem.Servers) > 0 { + servers = pathItem.Servers + } + + operations := map[string]*openapi3.Operation{ + "CONNECT": pathItem.Connect, + "DELETE": pathItem.Delete, + "GET": pathItem.Get, + "HEAD": pathItem.Head, + "OPTIONS": pathItem.Options, + "PATCH": pathItem.Patch, + "POST": pathItem.Post, + "PUT": pathItem.Put, + "TRACE": pathItem.Trace, + } + + for method, operation := range operations { + if operation == nil { + continue + } + matchers = append(matchers, operationToHTTPMatchers(operation, relativePath, method, pathItem.Parameters, servers, errors)...) + } + + return matchers +} + +func operationToHTTPMatchers(operation *openapi3.Operation, relativePath string, method string, parameters openapi3.Parameters, servers openapi3.Servers, errors field.ErrorList) []httpRouteMatcher { + if operation.Servers != nil && len(*operation.Servers) > 0 { + servers = *operation.Servers + } + + if operation.Parameters != nil { + parameters = operation.Parameters + } + + var expandedServers []openapi3.Server + expandedHosts := make(map[string]struct{}) + for _, server := range servers { + for _, expandedServer := range expandServerVariables(*server) { + basePath, err := expandedServer.BasePath() + if err != nil { + errors = append(errors, field.Invalid(field.NewPath("servers"), expandedServer, err.Error())) + } + host := uriToHostname(expandedServer.URL) + basePath + if _, exists := expandedHosts[host]; !exists { + expandedServers = append(expandedServers, expandedServer) + expandedHosts[host] = struct{}{} + } + } + } + + return Map(expandedServers, toHTTPMatcher(relativePath, method, parameters, errors)) +} + +func toHTTPMatcher(relativePath string, method string, parameters openapi3.Parameters, errors field.ErrorList) func(server openapi3.Server) httpRouteMatcher { + paramInFunc := func(in string) func(p *openapi3.ParameterRef) bool { + return func(p *openapi3.ParameterRef) bool { + return p.Value != nil && p.Value.Required && p.Value.In == in + } + } + paramNameFunc := func(p *openapi3.ParameterRef) string { return p.Value.Name } + + headers := strings.Join(Map(Filter(parameters, paramInFunc("header")), paramNameFunc), ParamSeparator) + params := strings.Join(Map(Filter(parameters, paramInFunc("query")), paramNameFunc), ParamSeparator) + + return func(server openapi3.Server) httpRouteMatcher { + basePath, err := server.BasePath() + if err != nil { + errors = append(errors, field.Invalid(field.NewPath("servers"), server, err.Error())) + } + if basePath == "/" { + basePath = "" + } + return httpRouteMatcher{ + host: uriToHostname(server.URL), + httpRouteRuleMatcher: httpRouteRuleMatcher{ + path: basePath + relativePath, + method: method, + headers: headers, + params: params, + }, + } + } +} + +func expandNonEnumServerVariables(server openapi3.Server) openapi3.Server { + if len(server.Variables) == 0 { + return server + } + // non-enum variables + uri := server.URL + variables := make(map[string]*openapi3.ServerVariable) + for name, svar := range server.Variables { + if len(svar.Enum) > 0 { + variables[name] = svar + continue + } + uri = strings.ReplaceAll(uri, "{"+name+"}", svar.Default) + } + return openapi3.Server{ + URL: uri, + Variables: variables, + } +} + +func expandServerVariables(server openapi3.Server) []openapi3.Server { + servers := []openapi3.Server{expandNonEnumServerVariables(server)} + for { + var newServers []openapi3.Server + for _, server := range servers { + if len(server.Variables) == 0 { + newServers = append(newServers, server) + continue + } + var name string + var svar *openapi3.ServerVariable + for name, svar = range server.Variables { break } + var uris []string + for _, enum := range svar.Enum { + uri := strings.ReplaceAll(server.URL, "{"+name+"}", enum) + uris = append(uris, uri) + } + variables := make(map[string]*openapi3.ServerVariable, len(server.Variables)-1) + for n, v := range server.Variables { + if n != name { + variables[n] = v + } + } + for _, uri := range uris { + newServers = append(newServers, openapi3.Server{ + URL: uri, + Variables: variables, + }) + } + } + servers = newServers + if slices.IndexFunc(servers, func(server openapi3.Server) bool { return len(server.Variables) > 0 }) == -1 { + break + } + } + return servers +} + +func uriToHostname(uri string) string { + host := uri + if strings.Contains(host, "://") { + host = strings.SplitN(host, "://", 2)[1] + } + return strings.SplitN(host, "/", 2)[0] +} + +func toGatewayAPIHostname(hostname string) gatewayv1.Hostname { + return gatewayv1.Hostname(hostname) +} diff --git a/pkg/i2gw/providers/openapi/converter_test.go b/pkg/i2gw/providers/openapi/converter_test.go new file mode 100644 index 000000000..e16eca64b --- /dev/null +++ b/pkg/i2gw/providers/openapi/converter_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "bytes" + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "testing" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/google/go-cmp/cmp" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" +) + +const fixturesDir = "./fixtures" + +func TestFileConvertion(t *testing.T) { + ctx := context.Background() + + filepath.WalkDir(filepath.Join(fixturesDir, "input"), func(path string, d fs.DirEntry, err error) error { + if err != nil { + t.Fatalf(err.Error()) + } + if d.IsDir() { + return nil + } + + provider := NewProvider(&i2gw.ProviderConf{}) + + err = provider.ReadResourcesFromFile(ctx, path) + if err != nil { + t.Fatalf("Failed to read input from file %v: %v", d.Name(), err.Error()) + } + + gotGatewayResources, errList := provider.ToGatewayAPI(i2gw.InputResources{}) + if len(errList) > 0 { + t.Fatalf("unexpected errors during input conversion for file %v: %v", d.Name(), errList.ToAggregate().Error()) + } + + outputFile := filepath.Join(fixturesDir, "output", d.Name()) + wantGatewayResources, err := readGatewayResourcesFromFile(t, outputFile) + if err != nil { + t.Fatalf("failed to read wantGatewayResources from file %v: %v", outputFile, err.Error()) + } + + if !apiequality.Semantic.DeepEqual(gotGatewayResources.Gateways, wantGatewayResources.Gateways) { + t.Errorf("Gateways diff for file %v (-want +got): %s", d.Name(), cmp.Diff(wantGatewayResources.Gateways, gotGatewayResources.Gateways)) + } + + if !apiequality.Semantic.DeepEqual(gotGatewayResources.HTTPRoutes, wantGatewayResources.HTTPRoutes) { + t.Errorf("HTTPRoutes diff for file %v (-want +got): %s", d.Name(), cmp.Diff(wantGatewayResources.HTTPRoutes, gotGatewayResources.HTTPRoutes)) + } + + if !apiequality.Semantic.DeepEqual(gotGatewayResources.TLSRoutes, wantGatewayResources.TLSRoutes) { + t.Errorf("TLSRoutes diff for file %v (-want +got): %s", d.Name(), cmp.Diff(wantGatewayResources.TLSRoutes, gotGatewayResources.TLSRoutes)) + } + + if !apiequality.Semantic.DeepEqual(gotGatewayResources.TCPRoutes, wantGatewayResources.TCPRoutes) { + t.Errorf("TCPRoutes diff for file %v (-want +got): %s", d.Name(), cmp.Diff(wantGatewayResources.TCPRoutes, gotGatewayResources.TCPRoutes)) + } + + if !apiequality.Semantic.DeepEqual(gotGatewayResources.ReferenceGrants, wantGatewayResources.ReferenceGrants) { + t.Errorf("ReferenceGrants diff for file %v (-want +got): %s", d.Name(), cmp.Diff(wantGatewayResources.ReferenceGrants, gotGatewayResources.ReferenceGrants)) + } + + return nil + }) +} + +func readGatewayResourcesFromFile(t *testing.T, filename string) (*i2gw.GatewayResources, error) { + t.Helper() + + stream, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file %v: %w", filename, err) + } + + unstructuredObjects, err := common.ExtractObjectsFromReader(bytes.NewReader(stream), "") + if err != nil { + return nil, fmt.Errorf("failed to extract objects: %w", err) + } + + res := i2gw.GatewayResources{ + Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), + HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), + TLSRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TLSRoute), + TCPRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TCPRoute), + ReferenceGrants: make(map[types.NamespacedName]gatewayv1beta1.ReferenceGrant), + } + + for _, obj := range unstructuredObjects { + switch objKind := obj.GetKind(); objKind { + case "Gateway": + var gw gatewayv1.Gateway + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &gw); err != nil { + return nil, fmt.Errorf("failed to parse k8s gateway object: %w", err) + } + res.Gateways[types.NamespacedName{ + Namespace: gw.Namespace, + Name: gw.Name, + }] = gw + case "HTTPRoute": + var httpRoute gatewayv1.HTTPRoute + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &httpRoute); err != nil { + return nil, fmt.Errorf("failed to parse k8s gateway HTTPRoute object: %w", err) + } + + res.HTTPRoutes[types.NamespacedName{ + Namespace: httpRoute.Namespace, + Name: httpRoute.Name, + }] = httpRoute + case "TLSRoute": + var tlsRoute gatewayv1alpha2.TLSRoute + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &tlsRoute); err != nil { + return nil, fmt.Errorf("failed to parse k8s gateway TLSRoute object: %w", err) + } + + res.TLSRoutes[types.NamespacedName{ + Namespace: tlsRoute.Namespace, + Name: tlsRoute.Name, + }] = tlsRoute + case "TCPRoute": + var tcpRoute gatewayv1alpha2.TCPRoute + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &tcpRoute); err != nil { + return nil, fmt.Errorf("failed to parse k8s gateway TCPRoute object: %w", err) + } + + res.TCPRoutes[types.NamespacedName{ + Namespace: tcpRoute.Namespace, + Name: tcpRoute.Name, + }] = tcpRoute + case "ReferenceGrant": + var referenceGrant gatewayv1beta1.ReferenceGrant + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &referenceGrant); err != nil { + return nil, fmt.Errorf("failed to parse k8s gateway ReferenceGrant object: %w", err) + } + + res.ReferenceGrants[types.NamespacedName{ + Namespace: referenceGrant.Namespace, + Name: referenceGrant.Name, + }] = referenceGrant + default: + return nil, fmt.Errorf("unknown object kind: %v", objKind) + } + } + + return &res, nil +} diff --git a/pkg/i2gw/providers/openapi/fixtures/input/1-petstore3.yaml b/pkg/i2gw/providers/openapi/fixtures/input/1-petstore3.yaml new file mode 100644 index 000000000..3eca6fb3c --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/input/1-petstore3.yaml @@ -0,0 +1,803 @@ +openapi: 3.0.2 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: http://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.19 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io +- name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io +- name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: "Multiple tags can be provided with comma separated strings. Use\ + \ tag1, tag2, tag3 for testing." + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: false + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid tag value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: "" + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet + description: "" + operationId: deletePet + parameters: + - name: api_key + in: header + description: "" + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "400": + description: Invalid pet value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: uploads an image + description: "" + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: Place a new order in the store + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + "405": + description: Invalid input + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID + description: For valid response try integer IDs with value <= 5 or > 10. Other + values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + "400": + description: Invalid ID supplied + "404": + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: For valid response try integer IDs with value < 1000. Anything + above 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + "400": + description: Invalid ID supplied + "404": + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: Creates list of users with given input array + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + default: + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: "" + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: "" + operationId: logoutUser + parameters: [] + responses: + default: + description: successful operation + /user/{username}: + get: + tags: + - user + summary: Get user by user name + description: "" + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + "400": + description: Invalid username supplied + "404": + description: User not found + put: + tags: + - user + summary: Update user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that needs to be updated + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid username supplied + "404": + description: User not found +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Customer: + type: object + properties: + id: + type: integer + format: int64 + example: 100000 + username: + type: string + example: fehguy + address: + type: array + xml: + name: addresses + wrapped: true + items: + $ref: '#/components/schemas/Address' + xml: + name: customer + Address: + type: object + properties: + street: + type: string + example: 437 Lytton + city: + type: string + example: Palo Alto + state: + type: string + example: CA + zip: + type: string + example: "94301" + xml: + name: address + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: "12345" + phone: + type: string + example: "12345" + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/pkg/i2gw/providers/openapi/fixtures/input/2-hostnames.yaml b/pkg/i2gw/providers/openapi/fixtures/input/2-hostnames.yaml new file mode 100644 index 000000000..434f94f0a --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/input/2-hostnames.yaml @@ -0,0 +1,76 @@ +openapi: 3.0.2 +info: + title: Sample API + version: 1.0.0 +servers: +- url: /api/v1 +- url: "{scheme}://api.example.com/{version}" + variables: + scheme: + enum: + - http + - https + default: https + version: + enum: + - v2 + - v3 + default: v3 +paths: + /resource: + post: + operationId: createResource + responses: + "200": + description: Successful operation + "405": + description: Invalid input + /resource/{id}: + get: + operationId: readResource + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Successful operation + "400": + description: Invalid ID supplied + "404": + description: Resource not found + patch: + operationId: updateResource + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Successful operation + "400": + description: Invalid ID supplied + "404": + description: Resource not found + delete: + operationId: deleteResource + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Successful operation + "400": + description: Invalid ID supplied + "404": + description: Resource not found diff --git a/pkg/i2gw/providers/openapi/fixtures/input/3-parameters.yaml b/pkg/i2gw/providers/openapi/fixtures/input/3-parameters.yaml new file mode 100644 index 000000000..2b7566636 --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/input/3-parameters.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.2 +info: + title: Sample API + version: 1.0.0 +paths: + /resources: + parameters: + - name: digest + in: header + required: true + schema: + type: string + post: + operationId: createResource + responses: + "200": + description: Successful operation + "400": + description: Invalid name supplied + get: + operationId: listResources + parameters: # overrides upper level parameters + - name: q + in: query + required: true + schema: + type: string + allowEmptyValue: true + - name: page + in: query + required: false # ignored + schema: + type: string + responses: + "200": + description: Successful operation + /resource/{id}: + parameters: + - name: id + in: path # ignored + required: true + schema: + type: integer + format: int64 + get: + operationId: readResource + parameters: + - name: id + in: path # ignored + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Successful operation + "400": + description: Invalid ID supplied + "404": + description: Resource not found diff --git a/pkg/i2gw/providers/openapi/fixtures/input/4-too-many-rules.json b/pkg/i2gw/providers/openapi/fixtures/input/4-too-many-rules.json new file mode 100644 index 000000000..ae381af24 --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/input/4-too-many-rules.json @@ -0,0 +1,138 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Sample API", + "version": "1.0.0" + }, + "paths": { + "/path-001": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-002": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-003": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-004": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-005": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-006": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-007": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-008": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-009": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-010": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-011": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-012": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-013": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-014": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-015": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-016": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-017": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-018": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-019": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-020": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-021": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-022": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-023": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-024": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-025": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-026": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-027": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-028": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-029": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-030": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-031": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-032": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-033": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-034": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-035": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-036": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-037": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-038": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-039": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-040": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-041": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-042": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-043": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-044": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-045": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-046": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-047": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-048": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-049": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-050": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-051": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-052": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-053": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-054": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-055": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-056": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-057": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-058": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-059": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-060": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-061": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-062": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-063": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-064": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-065": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-066": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-067": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-068": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-069": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-070": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-071": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-072": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-073": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-074": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-075": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-076": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-077": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-078": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-079": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-080": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-081": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-082": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-083": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-084": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-085": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-086": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-087": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-088": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-089": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-090": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-091": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-092": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-093": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-094": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-095": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-096": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-097": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-098": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-099": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-100": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-101": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-102": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-103": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-104": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-105": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-106": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-107": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-108": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-109": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-110": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-111": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-112": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-113": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-114": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-115": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-116": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-117": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-118": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-119": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-120": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-121": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-122": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-123": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-124": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-125": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-126": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-127": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-128": { "get": { "responses": { "200": { "description": "Successful operation" } } } }, + "/path-129": { "get": { "responses": { "200": { "description": "Successful operation" } } } } + } +} diff --git a/pkg/i2gw/providers/openapi/fixtures/output/1-petstore3.yaml b/pkg/i2gw/providers/openapi/fixtures/output/1-petstore3.yaml new file mode 100644 index 000000000..1bc97a65b --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/output/1-petstore3.yaml @@ -0,0 +1,88 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: route-1-1 +spec: + rules: + - matches: + - method: POST + path: + type: Exact + value: /api/v3/pet + - method: PUT + path: + type: Exact + value: /api/v3/pet + - method: GET + path: + type: Exact + value: /api/v3/pet/findByStatus + - method: GET + path: + type: Exact + value: /api/v3/pet/findByTags + - method: DELETE + path: + type: Exact + value: /api/v3/pet/{petId} + - method: GET + path: + type: Exact + value: /api/v3/pet/{petId} + - method: POST + path: + type: Exact + value: /api/v3/pet/{petId} + - method: POST + path: + type: Exact + value: /api/v3/pet/{petId}/uploadImage + - matches: + - method: GET + path: + type: Exact + value: /api/v3/store/inventory + - method: POST + path: + type: Exact + value: /api/v3/store/order + - method: DELETE + path: + type: Exact + value: /api/v3/store/order/{orderId} + - method: GET + path: + type: Exact + value: /api/v3/store/order/{orderId} + - method: POST + path: + type: Exact + value: /api/v3/user + - method: POST + path: + type: Exact + value: /api/v3/user/createWithList + - method: GET + path: + type: Exact + value: /api/v3/user/login + - method: GET + path: + type: Exact + value: /api/v3/user/logout + - matches: + - method: DELETE + path: + type: Exact + value: /api/v3/user/{username} + - method: GET + path: + type: Exact + value: /api/v3/user/{username} + - method: PUT + path: + type: Exact + value: /api/v3/user/{username} +status: + parents: null diff --git a/pkg/i2gw/providers/openapi/fixtures/output/2-hostnames.yaml b/pkg/i2gw/providers/openapi/fixtures/output/2-hostnames.yaml new file mode 100644 index 000000000..9c0be1e9a --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/output/2-hostnames.yaml @@ -0,0 +1,71 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: route-1-1 +spec: + rules: + - matches: + - method: POST + path: + type: Exact + value: /api/v1/resource + - method: DELETE + path: + type: Exact + value: /api/v1/resource/{id} + - method: GET + path: + type: Exact + value: /api/v1/resource/{id} + - method: PATCH + path: + type: Exact + value: /api/v1/resource/{id} +status: + parents: null +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: route-2-1 +spec: + hostnames: + - api.example.com + rules: + - matches: + - method: POST + path: + type: Exact + value: /v2/resource + - method: DELETE + path: + type: Exact + value: /v2/resource/{id} + - method: GET + path: + type: Exact + value: /v2/resource/{id} + - method: PATCH + path: + type: Exact + value: /v2/resource/{id} + - method: POST + path: + type: Exact + value: /v3/resource + - method: DELETE + path: + type: Exact + value: /v3/resource/{id} + - method: GET + path: + type: Exact + value: /v3/resource/{id} + - method: PATCH + path: + type: Exact + value: /v3/resource/{id} +status: + parents: null diff --git a/pkg/i2gw/providers/openapi/fixtures/output/3-parameters.yaml b/pkg/i2gw/providers/openapi/fixtures/output/3-parameters.yaml new file mode 100644 index 000000000..684b3e9b2 --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/output/3-parameters.yaml @@ -0,0 +1,28 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: route-1-1 +spec: + rules: + - matches: + - method: GET + path: + type: Exact + value: /resource/{id} + - method: GET + path: + type: Exact + value: /resources + queryParams: + - name: q + type: Exact + - method: POST + path: + type: Exact + value: /resources + headers: + - name: digest + type: Exact +status: + parents: null diff --git a/pkg/i2gw/providers/openapi/fixtures/output/4-too-many-rules.json b/pkg/i2gw/providers/openapi/fixtures/output/4-too-many-rules.json new file mode 100644 index 000000000..2bd4b9007 --- /dev/null +++ b/pkg/i2gw/providers/openapi/fixtures/output/4-too-many-rules.json @@ -0,0 +1,227 @@ +{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "HTTPRoute", + "metadata": { + "creationTimestamp": null, + "name": "route-1-1" + }, + "spec": { + "rules": [ + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-001" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-002" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-003" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-004" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-005" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-006" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-007" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-008" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-009" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-010" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-011" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-012" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-013" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-014" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-015" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-016" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-017" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-018" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-019" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-020" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-021" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-022" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-023" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-024" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-025" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-026" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-027" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-028" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-029" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-030" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-031" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-032" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-033" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-034" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-035" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-036" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-037" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-038" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-039" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-040" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-041" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-042" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-043" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-044" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-045" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-046" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-047" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-048" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-049" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-050" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-051" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-052" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-053" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-054" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-055" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-056" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-057" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-058" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-059" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-060" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-061" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-062" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-063" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-064" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-065" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-066" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-067" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-068" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-069" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-070" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-071" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-072" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-073" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-074" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-075" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-076" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-077" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-078" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-079" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-080" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-081" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-082" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-083" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-084" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-085" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-086" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-087" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-088" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-089" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-090" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-091" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-092" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-093" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-094" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-095" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-096" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-097" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-098" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-099" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-100" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-101" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-102" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-103" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-104" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-105" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-106" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-107" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-108" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-109" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-110" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-111" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-112" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-113" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-114" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-115" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-116" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-117" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-118" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-119" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-120" } } + ] + }, + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-121" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-122" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-123" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-124" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-125" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-126" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-127" } }, + { "method": "GET", "path": { "type": "Exact", "value": "/path-128" } } + ] + } + ] + }, + "status": { + "parents": null + } +} +{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "HTTPRoute", + "metadata": { + "creationTimestamp": null, + "name": "route-1-2" + }, + "spec": { + "rules": [ + { + "matches": [ + { "method": "GET", "path": { "type": "Exact", "value": "/path-129" } } + ] + } + ] + }, + "status": { + "parents": null + } +} diff --git a/pkg/i2gw/providers/openapi/openapi.go b/pkg/i2gw/providers/openapi/openapi.go new file mode 100644 index 000000000..01265e35b --- /dev/null +++ b/pkg/i2gw/providers/openapi/openapi.go @@ -0,0 +1,70 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" +) + +// The ProviderName returned to the provider's registry. +const ProviderName = "openapi" + +type Provider struct { + storage *storage + reader *resourceReader + converter *converter +} + +var _ i2gw.Provider = &Provider{} + +// NewProvider returns an implementation of i2gw.Provider that converts OpenAPI specs to Gateway API resources. +func NewProvider(conf *i2gw.ProviderConf) i2gw.Provider { + return &Provider{ + storage: newResourceStorage(), + reader: newResourceReader(conf), + converter: newConverter(conf), + } +} + +// ReadResourcesFromCluster reads OpenAPI specs stored in the Kubernetes cluster. UNIMPLEMENTED. +func (p *Provider) ReadResourcesFromCluster(_ context.Context) error { + return fmt.Errorf("provider does not support reading resources from cluster") +} + +// ReadResourcesFromFile reads OpenAPI specs from a JSON or YAML file. +func (p *Provider) ReadResourcesFromFile(ctx context.Context, filename string) error { + storage, err := p.reader.readResourcesFromFile(ctx, filename) + if err != nil { + return fmt.Errorf("failed to read resources from file: %w", err) + } + p.storage = storage + return nil +} + +// ToGatewayAPI converts stored OpenAPI specs to Gateway API resources. +func (p *Provider) ToGatewayAPI(_ i2gw.InputResources) (i2gw.GatewayResources, field.ErrorList) { + return p.converter.convert(p.storage) +} + +func init() { + i2gw.ProviderConstructorByName[ProviderName] = NewProvider +} diff --git a/pkg/i2gw/providers/openapi/resource_reader.go b/pkg/i2gw/providers/openapi/resource_reader.go new file mode 100644 index 000000000..12c623787 --- /dev/null +++ b/pkg/i2gw/providers/openapi/resource_reader.go @@ -0,0 +1,54 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "context" + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" +) + +type resourceReader struct { + conf *i2gw.ProviderConf +} + +// newResourceReader returns a reader instance. +func newResourceReader(conf *i2gw.ProviderConf) *resourceReader { + return &resourceReader{ + conf: conf, + } +} + +func (r *resourceReader) readResourcesFromFile(ctx context.Context, filename string) (*storage, error) { + loader := openapi3.NewLoader() + spec, err := loader.LoadFromFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to load OpenAPI spec: %w", err) + } + + if err := spec.Validate(ctx); err != nil { + return nil, fmt.Errorf("invalid OpenAPI 3.x spec: %w", err) + } + + storage := newResourceStorage() + storage.addResource(spec) + + return storage, nil +} diff --git a/pkg/i2gw/providers/openapi/storage.go b/pkg/i2gw/providers/openapi/storage.go new file mode 100644 index 000000000..797e58b15 --- /dev/null +++ b/pkg/i2gw/providers/openapi/storage.go @@ -0,0 +1,47 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "sync" + + "github.com/getkin/kin-openapi/openapi3" +) + +type storage struct { + mu sync.RWMutex + + resources []*openapi3.T +} + +func newResourceStorage() *storage { + return &storage{} +} + +func (s *storage) addResource(resource *openapi3.T) { + s.mu.Lock() + defer s.mu.Unlock() + + s.resources = append(s.resources, resource) +} + +func (s *storage) getResources() []*openapi3.T { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.resources +} diff --git a/pkg/i2gw/providers/openapi/utils.go b/pkg/i2gw/providers/openapi/utils.go new file mode 100644 index 000000000..0baba7f7b --- /dev/null +++ b/pkg/i2gw/providers/openapi/utils.go @@ -0,0 +1,37 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +// Map applies the given mapper function to each element in the input slice and returns a new slice with the results. +func Map[T, U any](slice []T, f func(T) U) []U { + arr := make([]U, len(slice)) + for i, e := range slice { + arr[i] = f(e) + } + return arr +} + +// Filter filters the input slice using the given predicate function and returns a new slice with the results. +func Filter[T any](slice []T, f func(T) bool) []T { + arr := make([]T, 0) + for _, e := range slice { + if f(e) { + arr = append(arr, e) + } + } + return arr +} From d52c6282df5cb443c90136b00ee45e2760554364 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Mon, 6 May 2024 11:00:12 +0200 Subject: [PATCH 02/26] Return without error from unimplemented ReadResourcesFromCluster func --- pkg/i2gw/providers/openapi/openapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/i2gw/providers/openapi/openapi.go b/pkg/i2gw/providers/openapi/openapi.go index 01265e35b..9813a4b90 100644 --- a/pkg/i2gw/providers/openapi/openapi.go +++ b/pkg/i2gw/providers/openapi/openapi.go @@ -47,7 +47,7 @@ func NewProvider(conf *i2gw.ProviderConf) i2gw.Provider { // ReadResourcesFromCluster reads OpenAPI specs stored in the Kubernetes cluster. UNIMPLEMENTED. func (p *Provider) ReadResourcesFromCluster(_ context.Context) error { - return fmt.Errorf("provider does not support reading resources from cluster") + return nil } // ReadResourcesFromFile reads OpenAPI specs from a JSON or YAML file. From f23ab1e5a1b5a684d1c26142469d10846f6a0339 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Mon, 6 May 2024 11:27:01 +0200 Subject: [PATCH 03/26] Move slice helper functions Map nd Filter into the providers/common package --- pkg/i2gw/providers/common/utils.go | 20 +++++++++++++ pkg/i2gw/providers/common/utils_test.go | 39 +++++++++++++++++++++++++ pkg/i2gw/providers/openapi/converter.go | 12 ++++---- pkg/i2gw/providers/openapi/utils.go | 37 ----------------------- 4 files changed, 65 insertions(+), 43 deletions(-) delete mode 100644 pkg/i2gw/providers/openapi/utils.go diff --git a/pkg/i2gw/providers/common/utils.go b/pkg/i2gw/providers/common/utils.go index 13154f971..dd9978bd5 100644 --- a/pkg/i2gw/providers/common/utils.go +++ b/pkg/i2gw/providers/common/utils.go @@ -200,3 +200,23 @@ func removeBackendRefsDuplicates(backendRefs []gatewayv1.HTTPBackendRef) []gatew } return uniqueBackendRefs } + +// Map applies the given mapper function to each element in the input slice and returns a new slice with the results. +func Map[T, U any](slice []T, f func(T) U) []U { + arr := make([]U, len(slice)) + for i, e := range slice { + arr[i] = f(e) + } + return arr +} + +// Filter filters the input slice using the given predicate function and returns a new slice with the results. +func Filter[T any](slice []T, f func(T) bool) []T { + arr := make([]T, 0) + for _, e := range slice { + if f(e) { + arr = append(arr, e) + } + } + return arr +} diff --git a/pkg/i2gw/providers/common/utils_test.go b/pkg/i2gw/providers/common/utils_test.go index 93e713481..439a9bfd9 100644 --- a/pkg/i2gw/providers/common/utils_test.go +++ b/pkg/i2gw/providers/common/utils_test.go @@ -530,3 +530,42 @@ func TestGroupIngressPathsByMatchKey(t *testing.T) { }) } } + +func TestMap(t *testing.T) { + slice1 := []int{1, 2, 3, 4} + expected1 := []int{2, 3, 4, 5} + result1 := Map(slice1, func(x int) int { return x + 1 }) + t.Run("maps a slice of type T into another slice of type T", func(t *testing.T) { + require.Equal(t, result1, expected1) + }) + + slice2 := []string{"hello", "world", "buz", "a"} + expected2 := []int{5, 5, 3, 1} + result2 := Map(slice2, func(s string) int { return len(s) }) + t.Run("maps a slice of type T into another slice of type U", func(t *testing.T) { + require.Equal(t, result2, expected2) + }) + + slice3 := []int{} + expected3 := []float32{} + result3 := Map(slice3, func(x int) float32 { return float32(x) / 2 }) + t.Run("maps an empty slice", func(t *testing.T) { + require.Equal(t, result3, expected3) + }) +} + +func TestFilter(t *testing.T) { + slice1 := []int{1, 2, 3, 4} + expected1 := []int{2, 4} + result1 := Filter(slice1, func(x int) bool { return x % 2 == 0 }) + t.Run("filters elements of a slice where a strict subset of the elements match", func(t *testing.T) { + require.Equal(t, result1, expected1) + }) + + slice2 := []int{1, 2, 3, 4} + expected2 := []int{1, 2, 3, 4} + result2 := Filter(slice2, func(x int) bool { return x > 0 }) + t.Run("filters elements of a slice where all elements match", func(t *testing.T) { + require.Equal(t, result2, expected2) + }) +} diff --git a/pkg/i2gw/providers/openapi/converter.go b/pkg/i2gw/providers/openapi/converter.go index 4427776c6..f4dace388 100644 --- a/pkg/i2gw/providers/openapi/converter.go +++ b/pkg/i2gw/providers/openapi/converter.go @@ -187,7 +187,7 @@ func toHTTPRoute(name string, hostnames []string, matchers httpRouteRuleMatchers }, } if len(hostnames) > 1 || !slices.Contains(hostnames, "") { - route.Spec.Hostnames = Map(hostnames, toGatewayAPIHostname) + route.Spec.Hostnames = common.Map(hostnames, toGatewayAPIHostname) } return route } @@ -212,7 +212,7 @@ func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule Method: common.PtrTo(gatewayv1.HTTPMethod(matcher.method)), } if matcher.headers != "" { - ruleMatch.Headers = Map(strings.Split(matcher.headers, ParamSeparator), func(header string) gatewayv1.HTTPHeaderMatch { + ruleMatch.Headers = common.Map(strings.Split(matcher.headers, ParamSeparator), func(header string) gatewayv1.HTTPHeaderMatch { return gatewayv1.HTTPHeaderMatch{ Name: gatewayv1.HTTPHeaderName(header), Type: common.PtrTo(gatewayv1.HeaderMatchExact), @@ -220,7 +220,7 @@ func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule }) } if matcher.params != "" { - ruleMatch.QueryParams = Map(strings.Split(matcher.params, ParamSeparator), func(param string) gatewayv1.HTTPQueryParamMatch { + ruleMatch.QueryParams = common.Map(strings.Split(matcher.params, ParamSeparator), func(param string) gatewayv1.HTTPQueryParamMatch { return gatewayv1.HTTPQueryParamMatch{ Name: gatewayv1.HTTPHeaderName(param), Type: common.PtrTo(gatewayv1.QueryParamMatchExact), @@ -289,7 +289,7 @@ func operationToHTTPMatchers(operation *openapi3.Operation, relativePath string, } } - return Map(expandedServers, toHTTPMatcher(relativePath, method, parameters, errors)) + return common.Map(expandedServers, toHTTPMatcher(relativePath, method, parameters, errors)) } func toHTTPMatcher(relativePath string, method string, parameters openapi3.Parameters, errors field.ErrorList) func(server openapi3.Server) httpRouteMatcher { @@ -300,8 +300,8 @@ func toHTTPMatcher(relativePath string, method string, parameters openapi3.Param } paramNameFunc := func(p *openapi3.ParameterRef) string { return p.Value.Name } - headers := strings.Join(Map(Filter(parameters, paramInFunc("header")), paramNameFunc), ParamSeparator) - params := strings.Join(Map(Filter(parameters, paramInFunc("query")), paramNameFunc), ParamSeparator) + headers := strings.Join(common.Map(common.Filter(parameters, paramInFunc("header")), paramNameFunc), ParamSeparator) + params := strings.Join(common.Map(common.Filter(parameters, paramInFunc("query")), paramNameFunc), ParamSeparator) return func(server openapi3.Server) httpRouteMatcher { basePath, err := server.BasePath() diff --git a/pkg/i2gw/providers/openapi/utils.go b/pkg/i2gw/providers/openapi/utils.go deleted file mode 100644 index 0baba7f7b..000000000 --- a/pkg/i2gw/providers/openapi/utils.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2024 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package openapi - -// Map applies the given mapper function to each element in the input slice and returns a new slice with the results. -func Map[T, U any](slice []T, f func(T) U) []U { - arr := make([]U, len(slice)) - for i, e := range slice { - arr[i] = f(e) - } - return arr -} - -// Filter filters the input slice using the given predicate function and returns a new slice with the results. -func Filter[T any](slice []T, f func(T) bool) []T { - arr := make([]T, 0) - for _, e := range slice { - if f(e) { - arr = append(arr, e) - } - } - return arr -} From acc95e9d7158cea36add7a1eeeef12a7e50f37e9 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Mon, 6 May 2024 11:35:14 +0200 Subject: [PATCH 04/26] openapi package renamed openapi3 --- cmd/print.go | 2 +- pkg/i2gw/providers/{openapi => openapi3}/README.md | 2 +- pkg/i2gw/providers/{openapi => openapi3}/converter.go | 2 +- pkg/i2gw/providers/{openapi => openapi3}/converter_test.go | 2 +- .../{openapi => openapi3}/fixtures/input/1-petstore3.yaml | 0 .../{openapi => openapi3}/fixtures/input/2-hostnames.yaml | 0 .../{openapi => openapi3}/fixtures/input/3-parameters.yaml | 0 .../fixtures/input/4-too-many-rules.json | 0 .../{openapi => openapi3}/fixtures/output/1-petstore3.yaml | 0 .../{openapi => openapi3}/fixtures/output/2-hostnames.yaml | 0 .../{openapi => openapi3}/fixtures/output/3-parameters.yaml | 0 .../fixtures/output/4-too-many-rules.json | 0 pkg/i2gw/providers/{openapi => openapi3}/openapi.go | 4 ++-- pkg/i2gw/providers/{openapi => openapi3}/resource_reader.go | 2 +- pkg/i2gw/providers/{openapi => openapi3}/storage.go | 2 +- 15 files changed, 8 insertions(+), 8 deletions(-) rename pkg/i2gw/providers/{openapi => openapi3}/README.md (94%) rename pkg/i2gw/providers/{openapi => openapi3}/converter.go (99%) rename pkg/i2gw/providers/{openapi => openapi3}/converter_test.go (99%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/input/1-petstore3.yaml (100%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/input/2-hostnames.yaml (100%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/input/3-parameters.yaml (100%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/input/4-too-many-rules.json (100%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/output/1-petstore3.yaml (100%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/output/2-hostnames.yaml (100%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/output/3-parameters.yaml (100%) rename pkg/i2gw/providers/{openapi => openapi3}/fixtures/output/4-too-many-rules.json (100%) rename pkg/i2gw/providers/{openapi => openapi3}/openapi.go (97%) rename pkg/i2gw/providers/{openapi => openapi3}/resource_reader.go (98%) rename pkg/i2gw/providers/{openapi => openapi3}/storage.go (98%) diff --git a/cmd/print.go b/cmd/print.go index 95ea0f0ab..7ad3dea03 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -32,7 +32,7 @@ import ( _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/ingressnginx" _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/istio" _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/kong" - _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/openapi" + _ "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/openapi3" ) type PrintRunner struct { diff --git a/pkg/i2gw/providers/openapi/README.md b/pkg/i2gw/providers/openapi3/README.md similarity index 94% rename from pkg/i2gw/providers/openapi/README.md rename to pkg/i2gw/providers/openapi3/README.md index 40f255b75..1554c2462 100644 --- a/pkg/i2gw/providers/openapi/README.md +++ b/pkg/i2gw/providers/openapi3/README.md @@ -5,7 +5,7 @@ The provider translates OpenAPI Specification (OAS) 3.x documents to Kubernetes ## Example ```sh -./ingress2gateway print --providers openapi --input-file=petstore3-openapi.json +./ingress2gateway print --providers openapi3 --input-file=petstore3-openapi.json ``` ## Known limitations diff --git a/pkg/i2gw/providers/openapi/converter.go b/pkg/i2gw/providers/openapi3/converter.go similarity index 99% rename from pkg/i2gw/providers/openapi/converter.go rename to pkg/i2gw/providers/openapi3/converter.go index f4dace388..caaa8da74 100644 --- a/pkg/i2gw/providers/openapi/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package openapi +package openapi3 import ( "fmt" diff --git a/pkg/i2gw/providers/openapi/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go similarity index 99% rename from pkg/i2gw/providers/openapi/converter_test.go rename to pkg/i2gw/providers/openapi3/converter_test.go index e16eca64b..3a6042508 100644 --- a/pkg/i2gw/providers/openapi/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package openapi +package openapi3 import ( "bytes" diff --git a/pkg/i2gw/providers/openapi/fixtures/input/1-petstore3.yaml b/pkg/i2gw/providers/openapi3/fixtures/input/1-petstore3.yaml similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/input/1-petstore3.yaml rename to pkg/i2gw/providers/openapi3/fixtures/input/1-petstore3.yaml diff --git a/pkg/i2gw/providers/openapi/fixtures/input/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/input/2-hostnames.yaml similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/input/2-hostnames.yaml rename to pkg/i2gw/providers/openapi3/fixtures/input/2-hostnames.yaml diff --git a/pkg/i2gw/providers/openapi/fixtures/input/3-parameters.yaml b/pkg/i2gw/providers/openapi3/fixtures/input/3-parameters.yaml similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/input/3-parameters.yaml rename to pkg/i2gw/providers/openapi3/fixtures/input/3-parameters.yaml diff --git a/pkg/i2gw/providers/openapi/fixtures/input/4-too-many-rules.json b/pkg/i2gw/providers/openapi3/fixtures/input/4-too-many-rules.json similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/input/4-too-many-rules.json rename to pkg/i2gw/providers/openapi3/fixtures/input/4-too-many-rules.json diff --git a/pkg/i2gw/providers/openapi/fixtures/output/1-petstore3.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/output/1-petstore3.yaml rename to pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml diff --git a/pkg/i2gw/providers/openapi/fixtures/output/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/output/2-hostnames.yaml rename to pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml diff --git a/pkg/i2gw/providers/openapi/fixtures/output/3-parameters.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/output/3-parameters.yaml rename to pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml diff --git a/pkg/i2gw/providers/openapi/fixtures/output/4-too-many-rules.json b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json similarity index 100% rename from pkg/i2gw/providers/openapi/fixtures/output/4-too-many-rules.json rename to pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json diff --git a/pkg/i2gw/providers/openapi/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go similarity index 97% rename from pkg/i2gw/providers/openapi/openapi.go rename to pkg/i2gw/providers/openapi3/openapi.go index 9813a4b90..9168df7c7 100644 --- a/pkg/i2gw/providers/openapi/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package openapi +package openapi3 import ( "context" @@ -26,7 +26,7 @@ import ( ) // The ProviderName returned to the provider's registry. -const ProviderName = "openapi" +const ProviderName = "openapi3" type Provider struct { storage *storage diff --git a/pkg/i2gw/providers/openapi/resource_reader.go b/pkg/i2gw/providers/openapi3/resource_reader.go similarity index 98% rename from pkg/i2gw/providers/openapi/resource_reader.go rename to pkg/i2gw/providers/openapi3/resource_reader.go index 12c623787..ed50a5418 100644 --- a/pkg/i2gw/providers/openapi/resource_reader.go +++ b/pkg/i2gw/providers/openapi3/resource_reader.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package openapi +package openapi3 import ( "context" diff --git a/pkg/i2gw/providers/openapi/storage.go b/pkg/i2gw/providers/openapi3/storage.go similarity index 98% rename from pkg/i2gw/providers/openapi/storage.go rename to pkg/i2gw/providers/openapi3/storage.go index 797e58b15..5bc324997 100644 --- a/pkg/i2gw/providers/openapi/storage.go +++ b/pkg/i2gw/providers/openapi3/storage.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package openapi +package openapi3 import ( "sync" From 6a2207030e03c58cfccd39beee8c25f06d6539d3 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Mon, 6 May 2024 12:41:42 +0200 Subject: [PATCH 05/26] thread-safe storage of specs --- pkg/i2gw/providers/openapi3/converter.go | 4 +-- pkg/i2gw/providers/openapi3/openapi.go | 10 +++---- .../providers/openapi3/resource_reader.go | 20 +++++++------- pkg/i2gw/providers/openapi3/storage.go | 26 +++++++++++++++---- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index caaa8da74..af56f3a78 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -64,7 +64,7 @@ func newConverter(conf *i2gw.ProviderConf) *converter { } } -func (c *converter) convert(storage *storage) (i2gw.GatewayResources, field.ErrorList) { +func (c *converter) convert(storage Storage) (i2gw.GatewayResources, field.ErrorList) { gatewayResources := i2gw.GatewayResources{ Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), @@ -75,7 +75,7 @@ func (c *converter) convert(storage *storage) (i2gw.GatewayResources, field.Erro var errors field.ErrorList - for _, spec := range storage.getResources() { + for _, spec := range storage.GetResources() { httpRoutes := toHTTPRoutes(spec, errors) for _, httpRoute := range httpRoutes { gatewayResources.HTTPRoutes[types.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}] = httpRoute diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index 9168df7c7..df0ef9edc 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -29,7 +29,7 @@ import ( const ProviderName = "openapi3" type Provider struct { - storage *storage + storage Storage reader *resourceReader converter *converter } @@ -38,9 +38,10 @@ var _ i2gw.Provider = &Provider{} // NewProvider returns an implementation of i2gw.Provider that converts OpenAPI specs to Gateway API resources. func NewProvider(conf *i2gw.ProviderConf) i2gw.Provider { + storage := NewResourceStorage() return &Provider{ - storage: newResourceStorage(), - reader: newResourceReader(conf), + storage: storage, + reader: newResourceReader(storage), converter: newConverter(conf), } } @@ -52,11 +53,10 @@ func (p *Provider) ReadResourcesFromCluster(_ context.Context) error { // ReadResourcesFromFile reads OpenAPI specs from a JSON or YAML file. func (p *Provider) ReadResourcesFromFile(ctx context.Context, filename string) error { - storage, err := p.reader.readResourcesFromFile(ctx, filename) + err := p.reader.readResourcesFromFile(ctx, filename) if err != nil { return fmt.Errorf("failed to read resources from file: %w", err) } - p.storage = storage return nil } diff --git a/pkg/i2gw/providers/openapi3/resource_reader.go b/pkg/i2gw/providers/openapi3/resource_reader.go index ed50a5418..c9b5e61b8 100644 --- a/pkg/i2gw/providers/openapi3/resource_reader.go +++ b/pkg/i2gw/providers/openapi3/resource_reader.go @@ -21,34 +21,32 @@ import ( "fmt" "github.com/getkin/kin-openapi/openapi3" - - "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" ) type resourceReader struct { - conf *i2gw.ProviderConf + storage Storage } // newResourceReader returns a reader instance. -func newResourceReader(conf *i2gw.ProviderConf) *resourceReader { +func newResourceReader(storage Storage) *resourceReader { return &resourceReader{ - conf: conf, + storage: storage, } } -func (r *resourceReader) readResourcesFromFile(ctx context.Context, filename string) (*storage, error) { +func (r *resourceReader) readResourcesFromFile(ctx context.Context, filename string) error { loader := openapi3.NewLoader() spec, err := loader.LoadFromFile(filename) if err != nil { - return nil, fmt.Errorf("failed to load OpenAPI spec: %w", err) + return fmt.Errorf("failed to load OpenAPI spec: %w", err) } if err := spec.Validate(ctx); err != nil { - return nil, fmt.Errorf("invalid OpenAPI 3.x spec: %w", err) + return fmt.Errorf("invalid OpenAPI 3.x spec: %w", err) } - storage := newResourceStorage() - storage.addResource(spec) + r.storage.Clear() + r.storage.AddResource(spec) - return storage, nil + return nil } diff --git a/pkg/i2gw/providers/openapi3/storage.go b/pkg/i2gw/providers/openapi3/storage.go index 5bc324997..624208711 100644 --- a/pkg/i2gw/providers/openapi3/storage.go +++ b/pkg/i2gw/providers/openapi3/storage.go @@ -22,26 +22,42 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) +type Storage interface { + AddResource(resource *openapi3.T) + GetResources() []*openapi3.T + Clear() +} + +// NewResourceStorage returns a thread-safe storage for OpenAPI specs. +func NewResourceStorage() Storage { + return &storage{} +} + type storage struct { mu sync.RWMutex resources []*openapi3.T } -func newResourceStorage() *storage { - return &storage{} -} +var _ Storage = &storage{} -func (s *storage) addResource(resource *openapi3.T) { +func (s *storage) AddResource(resource *openapi3.T) { s.mu.Lock() defer s.mu.Unlock() s.resources = append(s.resources, resource) } -func (s *storage) getResources() []*openapi3.T { +func (s *storage) GetResources() []*openapi3.T { s.mu.RLock() defer s.mu.RUnlock() return s.resources } + +func (s *storage) Clear() { + s.mu.Lock() + defer s.mu.Unlock() + + s.resources = []*openapi3.T{} +} From 7151f6a56f50c22b596ad1bfad9bbb3eafa46e8c Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Mon, 6 May 2024 13:17:59 +0200 Subject: [PATCH 06/26] refactor: resource reader simplified as part of Provider and removing converter's unused fields --- pkg/i2gw/providers/openapi3/converter.go | 28 ++++------ pkg/i2gw/providers/openapi3/openapi.go | 36 +++++++++---- .../providers/openapi3/resource_reader.go | 52 ------------------- 3 files changed, 37 insertions(+), 79 deletions(-) delete mode 100644 pkg/i2gw/providers/openapi3/resource_reader.go diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index af56f3a78..9d6380891 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -43,28 +43,20 @@ const ( HTTPRouteMatchesMaxMax = HTTPRouteRulesMax * HTTPRouteMatchesMax ) -// converter implements the ToGatewayAPI function of i2gw.ResourceConverter interface. -type converter struct { - conf *i2gw.ProviderConf - - featureParsers []i2gw.FeatureParser - implementationSpecificOptions i2gw.ProviderImplementationSpecificOptions +type Converter interface { + Convert(Storage) (i2gw.GatewayResources, field.ErrorList) } -// newConverter returns an ingress-nginx converter instance. -func newConverter(conf *i2gw.ProviderConf) *converter { - return &converter{ - conf: conf, - featureParsers: []i2gw.FeatureParser{ - // The list of feature parsers comes here. - }, - implementationSpecificOptions: i2gw.ProviderImplementationSpecificOptions{ - // The list of the implementationSpecific ingress fields options comes here. - }, - } +// NewConverter returns a converter of OpenAPI Specifications 3.x from a storage into Gateway API resources. +func NewConverter() Converter { + return &converter{} } -func (c *converter) convert(storage Storage) (i2gw.GatewayResources, field.ErrorList) { +type converter struct {} + +var _ Converter = &converter{} + +func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.ErrorList) { gatewayResources := i2gw.GatewayResources{ Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index df0ef9edc..0926b608d 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "github.com/getkin/kin-openapi/openapi3" "k8s.io/apimachinery/pkg/util/validation/field" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" @@ -30,19 +31,16 @@ const ProviderName = "openapi3" type Provider struct { storage Storage - reader *resourceReader - converter *converter + converter Converter } var _ i2gw.Provider = &Provider{} // NewProvider returns an implementation of i2gw.Provider that converts OpenAPI specs to Gateway API resources. -func NewProvider(conf *i2gw.ProviderConf) i2gw.Provider { - storage := NewResourceStorage() +func NewProvider(_ *i2gw.ProviderConf) i2gw.Provider { return &Provider{ - storage: storage, - reader: newResourceReader(storage), - converter: newConverter(conf), + storage: NewResourceStorage(), + converter: NewConverter(), } } @@ -53,18 +51,38 @@ func (p *Provider) ReadResourcesFromCluster(_ context.Context) error { // ReadResourcesFromFile reads OpenAPI specs from a JSON or YAML file. func (p *Provider) ReadResourcesFromFile(ctx context.Context, filename string) error { - err := p.reader.readResourcesFromFile(ctx, filename) + spec, err := readSpecFromFile(ctx, filename) if err != nil { return fmt.Errorf("failed to read resources from file: %w", err) } + + p.storage.Clear() + if spec != nil { + p.storage.AddResource(spec) + } + return nil } // ToGatewayAPI converts stored OpenAPI specs to Gateway API resources. func (p *Provider) ToGatewayAPI(_ i2gw.InputResources) (i2gw.GatewayResources, field.ErrorList) { - return p.converter.convert(p.storage) + return p.converter.Convert(p.storage) } func init() { i2gw.ProviderConstructorByName[ProviderName] = NewProvider } + +func readSpecFromFile(ctx context.Context, filename string) (*openapi3.T, error) { + loader := openapi3.NewLoader() + spec, err := loader.LoadFromFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to load OpenAPI spec: %w", err) + } + + if err := spec.Validate(ctx); err != nil { + return nil, fmt.Errorf("invalid OpenAPI 3.x spec: %w", err) + } + + return spec, nil +} diff --git a/pkg/i2gw/providers/openapi3/resource_reader.go b/pkg/i2gw/providers/openapi3/resource_reader.go deleted file mode 100644 index c9b5e61b8..000000000 --- a/pkg/i2gw/providers/openapi3/resource_reader.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2024 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package openapi3 - -import ( - "context" - "fmt" - - "github.com/getkin/kin-openapi/openapi3" -) - -type resourceReader struct { - storage Storage -} - -// newResourceReader returns a reader instance. -func newResourceReader(storage Storage) *resourceReader { - return &resourceReader{ - storage: storage, - } -} - -func (r *resourceReader) readResourcesFromFile(ctx context.Context, filename string) error { - loader := openapi3.NewLoader() - spec, err := loader.LoadFromFile(filename) - if err != nil { - return fmt.Errorf("failed to load OpenAPI spec: %w", err) - } - - if err := spec.Validate(ctx); err != nil { - return fmt.Errorf("invalid OpenAPI 3.x spec: %w", err) - } - - r.storage.Clear() - r.storage.AddResource(spec) - - return nil -} From f9d8d139c8f05bc2b0bdbb999f61bbeb3c2dddca Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Mon, 6 May 2024 13:19:16 +0200 Subject: [PATCH 07/26] make provider resilient to invalid input openapi specs --- .../fixtures/input/5-invalid-spec.yaml | 19 +++++++++++++++++++ .../fixtures/output/5-invalid-spec.yaml | 0 pkg/i2gw/providers/openapi3/openapi.go | 4 +++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 pkg/i2gw/providers/openapi3/fixtures/input/5-invalid-spec.yaml create mode 100644 pkg/i2gw/providers/openapi3/fixtures/output/5-invalid-spec.yaml diff --git a/pkg/i2gw/providers/openapi3/fixtures/input/5-invalid-spec.yaml b/pkg/i2gw/providers/openapi3/fixtures/input/5-invalid-spec.yaml new file mode 100644 index 000000000..fa58d4221 --- /dev/null +++ b/pkg/i2gw/providers/openapi3/fixtures/input/5-invalid-spec.yaml @@ -0,0 +1,19 @@ +# Not a valid OpenAPI 3.0 spec +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: minimal-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx-example + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: test + port: + number: 80 diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/5-invalid-spec.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/5-invalid-spec.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index 0926b608d..562878028 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -19,6 +19,7 @@ package openapi3 import ( "context" "fmt" + "log" "github.com/getkin/kin-openapi/openapi3" "k8s.io/apimachinery/pkg/util/validation/field" @@ -81,7 +82,8 @@ func readSpecFromFile(ctx context.Context, filename string) (*openapi3.T, error) } if err := spec.Validate(ctx); err != nil { - return nil, fmt.Errorf("invalid OpenAPI 3.x spec: %w", err) + log.Printf("%s provider: invalid OpenAPI 3.x spec: %v", ProviderName, err) + return nil, nil } return spec, nil From 8f48eceb0465650153bc3c2b020c248f67da538f Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 7 May 2024 14:52:00 +0200 Subject: [PATCH 08/26] Gateway and parentRefs --- pkg/i2gw/providers/openapi3/converter.go | 161 +++++++++++++----- .../openapi3/fixtures/input/2-hostnames.yaml | 15 ++ .../openapi3/fixtures/output/1-petstore3.yaml | 13 ++ .../openapi3/fixtures/output/2-hostnames.yaml | 51 ++++++ .../fixtures/output/3-parameters.yaml | 13 ++ .../fixtures/output/4-too-many-rules.json | 27 +++ 6 files changed, 239 insertions(+), 41 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 9d6380891..a89a3ea6a 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -18,6 +18,7 @@ package openapi3 import ( "fmt" + "regexp" "slices" "sort" "strings" @@ -35,6 +36,7 @@ import ( ) const ( + HostWildcard = "*" HostSeparator = "," ParamSeparator = "," @@ -43,6 +45,8 @@ const ( HTTPRouteMatchesMaxMax = HTTPRouteRulesMax * HTTPRouteMatchesMax ) +var uriRegexp = regexp.MustCompile(`^((https?)://([^/]+))?(/.*)?$`) // [_][1] = scheme, [_][2] = host, [_][3] = path + type Converter interface { Convert(Storage) (i2gw.GatewayResources, field.ErrorList) } @@ -68,12 +72,13 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error var errors field.ErrorList for _, spec := range storage.GetResources() { - httpRoutes := toHTTPRoutes(spec, errors) + httpRoutes, gateways := toHTTPRoutesAndGateways(spec, errors) for _, httpRoute := range httpRoutes { gatewayResources.HTTPRoutes[types.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}] = httpRoute } - // TODO: build Gateways - // TODO: add parentRefs to HTTPRoutes + for _, gateway := range gateways { + gatewayResources.Gateways[types.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()}] = gateway + } } return gatewayResources, errors @@ -98,11 +103,12 @@ func (m httpRouteRuleMatchers) Less(i, j int) bool { } type httpRouteMatcher struct { - host string + protocol string + host string httpRouteRuleMatcher } -func toHTTPRoutes(spec *openapi3.T, errors field.ErrorList) []gatewayv1.HTTPRoute { +func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewayv1.HTTPRoute, []gatewayv1.Gateway) { var matchers []httpRouteMatcher servers := spec.Servers @@ -116,34 +122,65 @@ func toHTTPRoutes(spec *openapi3.T, errors field.ErrorList) []gatewayv1.HTTPRout matchers = append(matchers, pathItemToHTTPMatchers(pathItem, relativePath, servers, errors)...) } - hostsByHTTPRouteRuleMatcher := make(map[httpRouteRuleMatcher][]string) + listenersByHTTPRouteRuleMatcher := make(map[httpRouteRuleMatcher][]string) for _, matcher := range matchers { - hostsByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher] = append(hostsByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher], matcher.host) + listener := fmt.Sprintf("%s://%s", matcher.protocol, matcher.host) + listenersByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher] = append(listenersByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher], listener) } - var hostGroups []string - httpRouteRuleMatchersByHosts := make(map[string]httpRouteRuleMatchers) - for matcher, hosts := range hostsByHTTPRouteRuleMatcher { - group := strings.Join(hosts, HostSeparator) - if _, exists := httpRouteRuleMatchersByHosts[group]; !exists { - hostGroups = append(hostGroups, group) + var listenerGroups []string + httpRouteRuleMatchersByListeners := make(map[string]httpRouteRuleMatchers) + for matcher, listeners := range listenersByHTTPRouteRuleMatcher { + group := strings.Join(listeners, HostSeparator) + if _, exists := httpRouteRuleMatchersByListeners[group]; !exists { + listenerGroups = append(listenerGroups, group) } - httpRouteRuleMatchersByHosts[group] = append(httpRouteRuleMatchersByHosts[group], matcher) + httpRouteRuleMatchersByListeners[group] = append(httpRouteRuleMatchersByListeners[group], matcher) } - var routes []gatewayv1.HTTPRoute + // sort listener groups for deterministic output + sort.Strings(listenerGroups) + + gatewayName := "gateway-1" // TODO: name the gateway after the spec + gateway := gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayName, + }, + Spec: gatewayv1.GatewaySpec{ + // TODO: gateway class + }, + } + gateway.SetGroupVersionKind(common.GatewayGVK) + + uniqueListeners := make(map[string]struct{}) + for _, group := range listenerGroups { + listeners := common.Filter(strings.Split(group, HostSeparator), func (listener string) bool { + _, exists := uniqueListeners[listener] + if !exists { + uniqueListeners[listener] = struct{}{} + } + return !exists + }) + gateway.Spec.Listeners = append(gateway.Spec.Listeners, common.Map(listeners, toListener)...) // TODO: gateways cannot have more than 64 listeners + } - // sort host groups for deterministic output - sort.Strings(hostGroups) + var routes []gatewayv1.HTTPRoute i := 0 - for _, group := range hostGroups { - hosts := strings.Split(group, HostSeparator) - matchers := httpRouteRuleMatchersByHosts[group] + for _, group := range listenerGroups { + listeners := strings.Split(group, HostSeparator) + hosts := common.Map(listeners, uriToHostname) + matchers := httpRouteRuleMatchersByListeners[group] + + var listenerName gatewayv1.SectionName + if len(uniqueListeners) > 1 && len(listeners) == 1 { + listenerName, _, _ = toListenerName(listeners[0]) + } // sort hostnames and matchers for deterministic output inside each route object - sort.Strings(hosts) sort.Sort(matchers) + sort.Strings(hosts) + hosts = slices.Compact(hosts) nMatchers := len(matchers) nRoutes := nMatchers / HTTPRouteMatchesMaxMax @@ -157,15 +194,60 @@ func toHTTPRoutes(spec *openapi3.T, errors field.ErrorList) []gatewayv1.HTTPRout if last > nMatchers { last = nMatchers } - routes = append(routes, toHTTPRoute(routeName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last])) + routes = append(routes, toHTTPRoute(routeName, gatewayName, listenerName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last])) } i++ } - return routes + return routes, []gatewayv1.Gateway{gateway} +} + +func toListener(protocolAndHostname string) gatewayv1.Listener { + name, protocol, hostname := toListenerName(protocolAndHostname) + + listener := gatewayv1.Listener{ + Name: name, + Protocol: gatewayv1.ProtocolType(strings.ToUpper(protocol)), + Hostname: common.PtrTo(gatewayv1.Hostname(hostname)), + } + + switch protocol { + case "http": + listener.Port = 80 + case "https": + listener.Port = 443 + listener.TLS = &gatewayv1.GatewayTLSConfig{} + } + + return listener } -func toHTTPRoute(name string, hostnames []string, matchers httpRouteRuleMatchers) gatewayv1.HTTPRoute { +func toListenerName(protocolAndHostname string) (listenerName gatewayv1.SectionName, protocol string, hostname string) { + protocol = "http" + hostname = HostWildcard + + if s := uriRegexp.FindAllStringSubmatch(protocolAndHostname, 1); len(s) > 0 { + if s[0][2] != "" { + protocol = s[0][2] + } + if s[0][3] != "" { + hostname = s[0][3] + } + } + + var listenerNamePrefix string + if hostname != HostWildcard { + listenerNamePrefix = fmt.Sprintf("%s-", common.NameFromHost(hostname)) + } + + return gatewayv1.SectionName(listenerNamePrefix + protocol), protocol, hostname +} + +func toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, hostnames []string, matchers httpRouteRuleMatchers) gatewayv1.HTTPRoute { + parentRef := gatewayv1.ParentReference{Name: gatewayv1.ObjectName(gatewayName)} + if listenerName != "" { + parentRef.SectionName = common.PtrTo(listenerName) + } route := gatewayv1.HTTPRoute{ TypeMeta: metav1.TypeMeta{ APIVersion: gatewayv1.GroupVersion.String(), @@ -175,10 +257,13 @@ func toHTTPRoute(name string, hostnames []string, matchers httpRouteRuleMatchers Name: name, }, Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{parentRef}, + }, Rules: toHTTPRouteRules(matchers), }, } - if len(hostnames) > 1 || !slices.Contains(hostnames, "") { + if len(hostnames) > 1 || !slices.Contains(hostnames, HostWildcard) { route.Spec.Hostnames = common.Map(hostnames, toGatewayAPIHostname) } return route @@ -266,19 +351,8 @@ func operationToHTTPMatchers(operation *openapi3.Operation, relativePath string, } var expandedServers []openapi3.Server - expandedHosts := make(map[string]struct{}) for _, server := range servers { - for _, expandedServer := range expandServerVariables(*server) { - basePath, err := expandedServer.BasePath() - if err != nil { - errors = append(errors, field.Invalid(field.NewPath("servers"), expandedServer, err.Error())) - } - host := uriToHostname(expandedServer.URL) + basePath - if _, exists := expandedHosts[host]; !exists { - expandedServers = append(expandedServers, expandedServer) - expandedHosts[host] = struct{}{} - } - } + expandedServers = append(expandedServers, expandServerVariables(*server)...) } return common.Map(expandedServers, toHTTPMatcher(relativePath, method, parameters, errors)) @@ -303,7 +377,12 @@ func toHTTPMatcher(relativePath string, method string, parameters openapi3.Param if basePath == "/" { basePath = "" } + protocol := "http" + if s := uriRegexp.FindAllStringSubmatch(server.URL, 1); len(s) > 0 && s[0][2] != "" { + protocol = s[0][2] + } return httpRouteMatcher{ + protocol: strings.ToLower(protocol), host: uriToHostname(server.URL), httpRouteRuleMatcher: httpRouteRuleMatcher{ path: basePath + relativePath, @@ -374,11 +453,11 @@ func expandServerVariables(server openapi3.Server) []openapi3.Server { } func uriToHostname(uri string) string { - host := uri - if strings.Contains(host, "://") { - host = strings.SplitN(host, "://", 2)[1] + host := HostWildcard + if s := uriRegexp.FindAllStringSubmatch(uri, 1); len(s) > 0 && s[0][3] != "" { + host = s[0][3] } - return strings.SplitN(host, "/", 2)[0] + return host } func toGatewayAPIHostname(hostname string) gatewayv1.Hostname { diff --git a/pkg/i2gw/providers/openapi3/fixtures/input/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/input/2-hostnames.yaml index 434f94f0a..bdddcf658 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/input/2-hostnames.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/input/2-hostnames.yaml @@ -74,3 +74,18 @@ paths: description: Invalid ID supplied "404": description: Resource not found + /status: + get: + operationId: status + responses: + "200": + description: Successful operation + servers: + - url: http://api.example.com/{version} + variables: + version: + enum: + - v1 + - v2 + - v3 + default: v3 diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml index 1bc97a65b..3de329705 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml @@ -1,9 +1,22 @@ apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-1 +spec: + listeners: + - name: http + hostname: "*" + port: 80 + protocol: HTTP +--- +apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null name: route-1-1 spec: + parentRefs: + - name: gateway-1 rules: - matches: - method: POST diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml index 9c0be1e9a..d3db91e6d 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml @@ -1,9 +1,32 @@ apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-1 +spec: + listeners: + - name: http + hostname: "*" + port: 80 + protocol: HTTP + - name: api-example-com-http + hostname: api.example.com + port: 80 + protocol: HTTP + - name: api-example-com-https + hostname: api.example.com + port: 443 + protocol: HTTPS + tls: {} +--- +apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null name: route-1-1 spec: + parentRefs: + - name: gateway-1 + sectionName: http rules: - matches: - method: POST @@ -30,9 +53,37 @@ kind: HTTPRoute metadata: creationTimestamp: null name: route-2-1 +spec: + parentRefs: + - name: gateway-1 + sectionName: api-example-com-http + hostnames: + - api.example.com + rules: + - matches: + - method: GET + path: + type: Exact + value: /v1/status + - method: GET + path: + type: Exact + value: /v2/status + - method: GET + path: + type: Exact + value: /v3/status +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: route-3-1 spec: hostnames: - api.example.com + parentRefs: + - name: gateway-1 rules: - matches: - method: POST diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml index 684b3e9b2..b740c8982 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml @@ -1,9 +1,22 @@ apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-1 +spec: + listeners: + - name: http + hostname: "*" + port: 80 + protocol: HTTP +--- +apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null name: route-1-1 spec: + parentRefs: + - name: gateway-1 rules: - matches: - method: GET diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json index 2bd4b9007..05786a45e 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json +++ b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json @@ -1,3 +1,20 @@ +{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": { + "name": "gateway-1" + }, + "spec": { + "listeners": [ + { + "name": "http", + "hostname": "*", + "port": 80, + "protocol": "HTTP" + } + ] + } +} { "apiVersion": "gateway.networking.k8s.io/v1", "kind": "HTTPRoute", @@ -6,6 +23,11 @@ "name": "route-1-1" }, "spec": { + "parentRefs": [ + { + "name": "gateway-1" + } + ], "rules": [ { "matches": [ @@ -213,6 +235,11 @@ "name": "route-1-2" }, "spec": { + "parentRefs": [ + { + "name": "gateway-1" + } + ], "rules": [ { "matches": [ From 4c4ebaa303b2be8faac1f22d7cd6fc3cb9f03cd3 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 7 May 2024 14:56:34 +0200 Subject: [PATCH 09/26] Declare github.com/getkin/kin-openapi as a direct dependency --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c632afa5d..360fd8146 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/kubernetes-sigs/ingress2gateway go 1.21 require ( + github.com/getkin/kin-openapi v0.124.0 github.com/google/go-cmp v0.6.0 github.com/kong/kubernetes-ingress-controller/v2 v2.12.3 github.com/spf13/cobra v1.8.0 @@ -18,7 +19,6 @@ require ( ) require ( - github.com/getkin/kin-openapi v0.124.0 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect From 98db36ef4ce078821390767def9f947fdc4dcbc6 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Wed, 8 May 2024 13:29:16 +0200 Subject: [PATCH 10/26] Use github.com/samber/lo for handling slices based on common patterns (mapping, filtering), instead of custom implementation --- go.mod | 2 ++ go.sum | 2 ++ pkg/i2gw/providers/common/utils.go | 20 ----------- pkg/i2gw/providers/common/utils_test.go | 39 ---------------------- pkg/i2gw/providers/openapi3/converter.go | 42 +++++++++++++----------- 5 files changed, 26 insertions(+), 79 deletions(-) diff --git a/go.mod b/go.mod index 360fd8146..04fb14a0a 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,8 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/samber/lo v1.39.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect ) require ( diff --git a/go.sum b/go.sum index 97e6f7929..c000507f9 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= diff --git a/pkg/i2gw/providers/common/utils.go b/pkg/i2gw/providers/common/utils.go index dd9978bd5..13154f971 100644 --- a/pkg/i2gw/providers/common/utils.go +++ b/pkg/i2gw/providers/common/utils.go @@ -200,23 +200,3 @@ func removeBackendRefsDuplicates(backendRefs []gatewayv1.HTTPBackendRef) []gatew } return uniqueBackendRefs } - -// Map applies the given mapper function to each element in the input slice and returns a new slice with the results. -func Map[T, U any](slice []T, f func(T) U) []U { - arr := make([]U, len(slice)) - for i, e := range slice { - arr[i] = f(e) - } - return arr -} - -// Filter filters the input slice using the given predicate function and returns a new slice with the results. -func Filter[T any](slice []T, f func(T) bool) []T { - arr := make([]T, 0) - for _, e := range slice { - if f(e) { - arr = append(arr, e) - } - } - return arr -} diff --git a/pkg/i2gw/providers/common/utils_test.go b/pkg/i2gw/providers/common/utils_test.go index 439a9bfd9..93e713481 100644 --- a/pkg/i2gw/providers/common/utils_test.go +++ b/pkg/i2gw/providers/common/utils_test.go @@ -530,42 +530,3 @@ func TestGroupIngressPathsByMatchKey(t *testing.T) { }) } } - -func TestMap(t *testing.T) { - slice1 := []int{1, 2, 3, 4} - expected1 := []int{2, 3, 4, 5} - result1 := Map(slice1, func(x int) int { return x + 1 }) - t.Run("maps a slice of type T into another slice of type T", func(t *testing.T) { - require.Equal(t, result1, expected1) - }) - - slice2 := []string{"hello", "world", "buz", "a"} - expected2 := []int{5, 5, 3, 1} - result2 := Map(slice2, func(s string) int { return len(s) }) - t.Run("maps a slice of type T into another slice of type U", func(t *testing.T) { - require.Equal(t, result2, expected2) - }) - - slice3 := []int{} - expected3 := []float32{} - result3 := Map(slice3, func(x int) float32 { return float32(x) / 2 }) - t.Run("maps an empty slice", func(t *testing.T) { - require.Equal(t, result3, expected3) - }) -} - -func TestFilter(t *testing.T) { - slice1 := []int{1, 2, 3, 4} - expected1 := []int{2, 4} - result1 := Filter(slice1, func(x int) bool { return x % 2 == 0 }) - t.Run("filters elements of a slice where a strict subset of the elements match", func(t *testing.T) { - require.Equal(t, result1, expected1) - }) - - slice2 := []int{1, 2, 3, 4} - expected2 := []int{1, 2, 3, 4} - result2 := Filter(slice2, func(x int) bool { return x > 0 }) - t.Run("filters elements of a slice where all elements match", func(t *testing.T) { - require.Equal(t, result2, expected2) - }) -} diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index a89a3ea6a..d01b0c419 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/getkin/kin-openapi/openapi3" + "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" @@ -154,14 +155,14 @@ func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewa uniqueListeners := make(map[string]struct{}) for _, group := range listenerGroups { - listeners := common.Filter(strings.Split(group, HostSeparator), func (listener string) bool { + listeners := lo.Filter(strings.Split(group, HostSeparator), func (listener string, _ int) bool { _, exists := uniqueListeners[listener] if !exists { uniqueListeners[listener] = struct{}{} } return !exists }) - gateway.Spec.Listeners = append(gateway.Spec.Listeners, common.Map(listeners, toListener)...) // TODO: gateways cannot have more than 64 listeners + gateway.Spec.Listeners = append(gateway.Spec.Listeners, lo.Map(listeners, toListener)...) // TODO: gateways cannot have more than 64 listeners } var routes []gatewayv1.HTTPRoute @@ -169,7 +170,7 @@ func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewa i := 0 for _, group := range listenerGroups { listeners := strings.Split(group, HostSeparator) - hosts := common.Map(listeners, uriToHostname) + hosts := lo.Map(listeners, uriToHostname) matchers := httpRouteRuleMatchersByListeners[group] var listenerName gatewayv1.SectionName @@ -202,7 +203,7 @@ func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewa return routes, []gatewayv1.Gateway{gateway} } -func toListener(protocolAndHostname string) gatewayv1.Listener { +func toListener(protocolAndHostname string, _ int) gatewayv1.Listener { name, protocol, hostname := toListenerName(protocolAndHostname) listener := gatewayv1.Listener{ @@ -264,7 +265,7 @@ func toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, h }, } if len(hostnames) > 1 || !slices.Contains(hostnames, HostWildcard) { - route.Spec.Hostnames = common.Map(hostnames, toGatewayAPIHostname) + route.Spec.Hostnames = lo.Map(hostnames, toGatewayAPIHostname) } return route } @@ -289,7 +290,7 @@ func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule Method: common.PtrTo(gatewayv1.HTTPMethod(matcher.method)), } if matcher.headers != "" { - ruleMatch.Headers = common.Map(strings.Split(matcher.headers, ParamSeparator), func(header string) gatewayv1.HTTPHeaderMatch { + ruleMatch.Headers = lo.Map(strings.Split(matcher.headers, ParamSeparator), func(header string, _ int) gatewayv1.HTTPHeaderMatch { return gatewayv1.HTTPHeaderMatch{ Name: gatewayv1.HTTPHeaderName(header), Type: common.PtrTo(gatewayv1.HeaderMatchExact), @@ -297,7 +298,7 @@ func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule }) } if matcher.params != "" { - ruleMatch.QueryParams = common.Map(strings.Split(matcher.params, ParamSeparator), func(param string) gatewayv1.HTTPQueryParamMatch { + ruleMatch.QueryParams = lo.Map(strings.Split(matcher.params, ParamSeparator), func(param string, _ int) gatewayv1.HTTPQueryParamMatch { return gatewayv1.HTTPQueryParamMatch{ Name: gatewayv1.HTTPHeaderName(param), Type: common.PtrTo(gatewayv1.QueryParamMatchExact), @@ -355,21 +356,22 @@ func operationToHTTPMatchers(operation *openapi3.Operation, relativePath string, expandedServers = append(expandedServers, expandServerVariables(*server)...) } - return common.Map(expandedServers, toHTTPMatcher(relativePath, method, parameters, errors)) + return lo.Map(expandedServers, toHTTPMatcher(relativePath, method, parameters, errors)) } -func toHTTPMatcher(relativePath string, method string, parameters openapi3.Parameters, errors field.ErrorList) func(server openapi3.Server) httpRouteMatcher { - paramInFunc := func(in string) func(p *openapi3.ParameterRef) bool { - return func(p *openapi3.ParameterRef) bool { - return p.Value != nil && p.Value.Required && p.Value.In == in +func toHTTPMatcher(relativePath string, method string, parameters openapi3.Parameters, errors field.ErrorList) func(server openapi3.Server, _ int) httpRouteMatcher { + paramNameFunc := func(in string) func(p *openapi3.ParameterRef, _ int) (string, bool) { + return func(p *openapi3.ParameterRef, _ int) (string, bool) { + if p.Value != nil && p.Value.Required && p.Value.In == in { + return p.Value.Name, true + } + return "", false } } - paramNameFunc := func(p *openapi3.ParameterRef) string { return p.Value.Name } - - headers := strings.Join(common.Map(common.Filter(parameters, paramInFunc("header")), paramNameFunc), ParamSeparator) - params := strings.Join(common.Map(common.Filter(parameters, paramInFunc("query")), paramNameFunc), ParamSeparator) + headers := strings.Join(lo.FilterMap(parameters, paramNameFunc("header")), ParamSeparator) + params := strings.Join(lo.FilterMap(parameters, paramNameFunc("query")), ParamSeparator) - return func(server openapi3.Server) httpRouteMatcher { + return func(server openapi3.Server, _ int) httpRouteMatcher { basePath, err := server.BasePath() if err != nil { errors = append(errors, field.Invalid(field.NewPath("servers"), server, err.Error())) @@ -383,7 +385,7 @@ func toHTTPMatcher(relativePath string, method string, parameters openapi3.Param } return httpRouteMatcher{ protocol: strings.ToLower(protocol), - host: uriToHostname(server.URL), + host: uriToHostname(server.URL, 0), httpRouteRuleMatcher: httpRouteRuleMatcher{ path: basePath + relativePath, method: method, @@ -452,7 +454,7 @@ func expandServerVariables(server openapi3.Server) []openapi3.Server { return servers } -func uriToHostname(uri string) string { +func uriToHostname(uri string, _ int) string { host := HostWildcard if s := uriRegexp.FindAllStringSubmatch(uri, 1); len(s) > 0 && s[0][3] != "" { host = s[0][3] @@ -460,6 +462,6 @@ func uriToHostname(uri string) string { return host } -func toGatewayAPIHostname(hostname string) gatewayv1.Hostname { +func toGatewayAPIHostname(hostname string, _ int) gatewayv1.Hostname { return gatewayv1.Hostname(hostname) } From 808a044442ad71b89ac5df6bf55f7719e156f606 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Wed, 8 May 2024 13:43:52 +0200 Subject: [PATCH 11/26] Remove initialization of non converted kinds of resources TLSRoutes, TCPRoutes, ReferenceGrants --- pkg/i2gw/providers/openapi3/converter.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index d01b0c419..bc7bedf17 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -29,8 +29,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" @@ -63,11 +61,8 @@ var _ Converter = &converter{} func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.ErrorList) { gatewayResources := i2gw.GatewayResources{ - Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), - HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), - TLSRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TLSRoute), - TCPRoutes: make(map[types.NamespacedName]gatewayv1alpha2.TCPRoute), - ReferenceGrants: make(map[types.NamespacedName]gatewayv1beta1.ReferenceGrant), + Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), + HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), } var errors field.ErrorList From 5964eebd231a7f604cb65a27a329e01e8c4d86b8 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Wed, 8 May 2024 13:46:32 +0200 Subject: [PATCH 12/26] init func brought further upwards --- pkg/i2gw/providers/openapi3/openapi.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index 562878028..d4b9d3175 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -30,6 +30,10 @@ import ( // The ProviderName returned to the provider's registry. const ProviderName = "openapi3" +func init() { + i2gw.ProviderConstructorByName[ProviderName] = NewProvider +} + type Provider struct { storage Storage converter Converter @@ -70,10 +74,6 @@ func (p *Provider) ToGatewayAPI(_ i2gw.InputResources) (i2gw.GatewayResources, f return p.converter.Convert(p.storage) } -func init() { - i2gw.ProviderConstructorByName[ProviderName] = NewProvider -} - func readSpecFromFile(ctx context.Context, filename string) (*openapi3.T, error) { loader := openapi3.NewLoader() spec, err := loader.LoadFromFile(filename) From fd6a097d300f908fa98d81c82557f38517a5fa57 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Wed, 8 May 2024 13:47:18 +0200 Subject: [PATCH 13/26] code format --- pkg/i2gw/providers/openapi3/converter.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index bc7bedf17..5e388d9ce 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -35,7 +35,7 @@ import ( ) const ( - HostWildcard = "*" + HostWildcard = "*" HostSeparator = "," ParamSeparator = "," @@ -55,7 +55,7 @@ func NewConverter() Converter { return &converter{} } -type converter struct {} +type converter struct{} var _ Converter = &converter{} @@ -150,7 +150,7 @@ func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewa uniqueListeners := make(map[string]struct{}) for _, group := range listenerGroups { - listeners := lo.Filter(strings.Split(group, HostSeparator), func (listener string, _ int) bool { + listeners := lo.Filter(strings.Split(group, HostSeparator), func(listener string, _ int) bool { _, exists := uniqueListeners[listener] if !exists { uniqueListeners[listener] = struct{}{} @@ -160,7 +160,7 @@ func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewa gateway.Spec.Listeners = append(gateway.Spec.Listeners, lo.Map(listeners, toListener)...) // TODO: gateways cannot have more than 64 listeners } - var routes []gatewayv1.HTTPRoute + var routes []gatewayv1.HTTPRoute i := 0 for _, group := range listenerGroups { @@ -287,8 +287,8 @@ func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule if matcher.headers != "" { ruleMatch.Headers = lo.Map(strings.Split(matcher.headers, ParamSeparator), func(header string, _ int) gatewayv1.HTTPHeaderMatch { return gatewayv1.HTTPHeaderMatch{ - Name: gatewayv1.HTTPHeaderName(header), - Type: common.PtrTo(gatewayv1.HeaderMatchExact), + Name: gatewayv1.HTTPHeaderName(header), + Type: common.PtrTo(gatewayv1.HeaderMatchExact), } }) } @@ -346,7 +346,7 @@ func operationToHTTPMatchers(operation *openapi3.Operation, relativePath string, parameters = operation.Parameters } - var expandedServers []openapi3.Server + var expandedServers []openapi3.Server for _, server := range servers { expandedServers = append(expandedServers, expandServerVariables(*server)...) } @@ -380,7 +380,7 @@ func toHTTPMatcher(relativePath string, method string, parameters openapi3.Param } return httpRouteMatcher{ protocol: strings.ToLower(protocol), - host: uriToHostname(server.URL, 0), + host: uriToHostname(server.URL, 0), httpRouteRuleMatcher: httpRouteRuleMatcher{ path: basePath + relativePath, method: method, @@ -422,7 +422,9 @@ func expandServerVariables(server openapi3.Server) []openapi3.Server { } var name string var svar *openapi3.ServerVariable - for name, svar = range server.Variables { break } + for name, svar = range server.Variables { + break + } var uris []string for _, enum := range svar.Enum { uri := strings.ReplaceAll(server.URL, "{"+name+"}", enum) @@ -451,7 +453,7 @@ func expandServerVariables(server openapi3.Server) []openapi3.Server { func uriToHostname(uri string, _ int) string { host := HostWildcard - if s := uriRegexp.FindAllStringSubmatch(uri, 1); len(s) > 0 && s[0][3] != "" { + if s := uriRegexp.FindAllStringSubmatch(uri, 1); len(s) > 0 && s[0][3] != "" { host = s[0][3] } return host From 12dd6a09cceb21f537a2c134e01d64f012665630 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Thu, 9 May 2024 14:50:41 +0200 Subject: [PATCH 14/26] Provider-specific options --openapi3-backend and --openapi3-gateway-class-name Defined using a newly introduced system of dynamically registered provider-specific configuration flags. --- PROVIDER.md | 36 +++++++++-- cmd/print.go | 24 ++++++- pkg/i2gw/ingress2gateway.go | 7 +- pkg/i2gw/provider.go | 38 ++++++++++- pkg/i2gw/providers/openapi3/converter.go | 64 +++++++++++++++---- pkg/i2gw/providers/openapi3/converter_test.go | 9 ++- .../openapi3/fixtures/output/1-petstore3.yaml | 7 ++ .../openapi3/fixtures/output/2-hostnames.yaml | 7 ++ .../fixtures/output/3-parameters.yaml | 3 + .../fixtures/output/4-too-many-rules.json | 52 ++++++++++----- pkg/i2gw/providers/openapi3/openapi.go | 23 +++++-- 11 files changed, 227 insertions(+), 43 deletions(-) diff --git a/PROVIDER.md b/PROVIDER.md index 54aea7013..ea77d8542 100644 --- a/PROVIDER.md +++ b/PROVIDER.md @@ -96,7 +96,7 @@ func newConverter(conf *i2gw.ProviderConf) *converter { } } ``` -4. Create a new struct named after the provider you are implementing. This struct should embed the previous 2 structs +4. Create a new struct named after the provider you are implementing. This struct should embed the previous 2 structs you created. ```go package examplegateway @@ -152,12 +152,12 @@ import ( In case you want to add support for the conversion of a specific feature within a provider (see for example the canary feature of ingress-nginx) you'll want to implement a `FeatureParser` function. -Different `FeatureParsers` within the same provider will run in undetermined order. This means that when building a +Different `FeatureParsers` within the same provider will run in undetermined order. This means that when building a `Gateway API` resource manifest, you cannot assume anything about previously initialized fields. The function must modify / create only the required fields of the resource manifest and nothing else. For example, lets say we are implementing the canary feature of some provider. When building the `HTTPRoute`, we cannot -assume that the `BackendRefs` is already initialized with every `BackendRef` required. The canary `FeatureParser` +assume that the `BackendRefs` is already initialized with every `BackendRef` required. The canary `FeatureParser` function must add every missing `BackendRef` and update existing ones. ### Testing the feature parser @@ -165,4 +165,32 @@ There are 2 main things that needs to be tested when creating a feature parser: 1. The conversion logic is actually correct. 2. The new function doesn't override other functions modifications. For example, if one implemented the mirror backend feature and it deletes canary weight from `BackendRefs`, we have a -problem. \ No newline at end of file +problem. + +## Provider-specific user input +To define provider-specific configuration that the user can supply in the `print` command, call the +`i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf)` function in the init function of the +provider. E.g.: +```go +const Name = "example-gateway-provider" + +func init() { + i2gw.ProviderConstructorByName[Name] = NewProvider + + i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + Name: "infrastructure-labels", + Description: "Comma-separated list of Gateway infrastructure key=value labels", + DefaultValue: "", + }) +} +``` +Users can supply values the provider-specific configuration flag defined above as follows: +```sh +./ingress2gateway print --providers=example-gateway-provider --example-gateway-provider-infrastructure-labels="app=my-app" +``` +The values all provider-specific flags supplied by the user can be retrieved from the provider `conf`: +```go +if ps := conf.ProviderSpecific[ProviderName]; ps != nil { + labels := ps["infrastructure-labels"] +} +``` diff --git a/cmd/print.go b/cmd/print.go index 7ad3dea03..ddcbd02cd 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -60,6 +60,9 @@ type PrintRunner struct { // providers indicates which providers are used to execute convert action. providers []string + + // Provider specific conf. Value assigned via provider specific flags ---. + providerSpecificConf map[string]*string } // PrintGatewayAPIObjects performs necessary steps to digest and print @@ -76,7 +79,18 @@ func (pr *PrintRunner) PrintGatewayAPIObjects(cmd *cobra.Command, _ []string) er return fmt.Errorf("failed to initialize namespace filter: %w", err) } - gatewayResources, err := i2gw.ToGatewayAPIResources(cmd.Context(), pr.namespaceFilter, pr.inputFile, pr.providers) + providerSpecificConf := make(map[string]map[string]string) + for flagName, value := range pr.providerSpecificConf { + parts := strings.SplitN(flagName, "-", 2) + provider := parts[0] + conf := parts[1] + if providerSpecificConf[provider] == nil { + providerSpecificConf[provider] = make(map[string]string) + } + providerSpecificConf[provider][conf] = *value + } + + gatewayResources, err := i2gw.ToGatewayAPIResources(cmd.Context(), pr.namespaceFilter, pr.inputFile, pr.providers, providerSpecificConf) if err != nil { return err } @@ -250,6 +264,14 @@ if specified with --namespace.`) cmd.Flags().StringSliceVar(&pr.providers, "providers", i2gw.GetSupportedProviders(), fmt.Sprintf("If present, the tool will try to convert only resources related to the specified providers, supported values are %v.", i2gw.GetSupportedProviders())) + pr.providerSpecificConf = make(map[string]*string) + for provider, flags := range i2gw.GetProviderSpecificConfDefinitions() { + for _, flag := range flags { + flagName := fmt.Sprintf("%s-%s", provider, flag.Name) + pr.providerSpecificConf[flagName] = cmd.Flags().String(flagName, flag.DefaultValue, fmt.Sprintf("Provider-specific: %s. %s", provider, flag.Description)) + } + } + cmd.MarkFlagsMutuallyExclusive("namespace", "all-namespaces") return cmd } diff --git a/pkg/i2gw/ingress2gateway.go b/pkg/i2gw/ingress2gateway.go index d4987930c..060b811e4 100644 --- a/pkg/i2gw/ingress2gateway.go +++ b/pkg/i2gw/ingress2gateway.go @@ -30,7 +30,7 @@ import ( gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) -func ToGatewayAPIResources(ctx context.Context, namespace string, inputFile string, providers []string) ([]GatewayResources, error) { +func ToGatewayAPIResources(ctx context.Context, namespace string, inputFile string, providers []string, providerSpecificConf map[string]map[string]string) ([]GatewayResources, error) { var clusterClient client.Client if inputFile == "" { @@ -47,8 +47,9 @@ func ToGatewayAPIResources(ctx context.Context, namespace string, inputFile stri } providerByName, err := constructProviders(&ProviderConf{ - Client: clusterClient, - Namespace: namespace, + Client: clusterClient, + Namespace: namespace, + ProviderSpecific: providerSpecificConf, }, providers) if err != nil { return nil, err diff --git a/pkg/i2gw/provider.go b/pkg/i2gw/provider.go index b60c4a16e..a3dc32a35 100644 --- a/pkg/i2gw/provider.go +++ b/pkg/i2gw/provider.go @@ -18,6 +18,7 @@ package i2gw import ( "context" + "sync" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" @@ -43,8 +44,9 @@ type ProviderConstructor func(conf *ProviderConf) Provider // ProviderConf contains all the configuration required for every concrete // Provider implementation. type ProviderConf struct { - Client client.Client - Namespace string + Client client.Client + Namespace string + ProviderSpecific map[string]map[string]string } // The Provider interface specifies the required functionality which needs to be @@ -110,3 +112,35 @@ type GatewayResources struct { // Different FeatureParsers will run in undetermined order. The function must // modify / create only the required fields of the gateway resources and nothing else. type FeatureParser func(InputResources, *GatewayResources) field.ErrorList + +type ProviderSpecificConf struct { + Name string + Description string + DefaultValue string +} + +var ( + providerSpecificConfs = map[ProviderName]map[string]ProviderSpecificConf{} + providerSpecificConfsMutex = sync.RWMutex{} +) + +// RegisterProviderSpecificConf registers a provider-specific conf. +// Each provider-specific conf is exposed to the user as a command-line flag, defined as optional. +// If the flag is not provided, it is up for the provider to decide to use the default value or raise an error. +// The provider can read the values of provider-specific confs input by the user from the ProviderConf, +// prefixed by the provider name. +func RegisterProviderSpecificConf(provider ProviderName, conf ProviderSpecificConf) { + providerSpecificConfsMutex.Lock() + defer providerSpecificConfsMutex.Unlock() + if providerSpecificConfs[provider] == nil { + providerSpecificConfs[provider] = map[string]ProviderSpecificConf{} + } + providerSpecificConfs[provider][conf.Name] = conf +} + +// GetProviderSpecificConfDefinitions returns the provider specific confs registered by the providers. +func GetProviderSpecificConfDefinitions() map[ProviderName]map[string]ProviderSpecificConf { + providerSpecificConfsMutex.RLock() + defer providerSpecificConfsMutex.RUnlock() + return providerSpecificConfs +} diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 5e388d9ce..60d9e76a1 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -51,11 +51,26 @@ type Converter interface { } // NewConverter returns a converter of OpenAPI Specifications 3.x from a storage into Gateway API resources. -func NewConverter() Converter { - return &converter{} +func NewConverter(conf *i2gw.ProviderConf) Converter { + var backendName, gatewayClassName string + + if ps := conf.ProviderSpecific[ProviderName]; ps != nil { + backendName = ps[BackendFlag] + gatewayClassName = ps[GatewayClassFlag] + } + + return &converter{ + namespace: conf.Namespace, + backendName: backendName, + gatewayClassName: gatewayClassName, + } } -type converter struct{} +type converter struct { + namespace string + backendName string + gatewayClassName string +} var _ Converter = &converter{} @@ -68,10 +83,11 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error var errors field.ErrorList for _, spec := range storage.GetResources() { - httpRoutes, gateways := toHTTPRoutesAndGateways(spec, errors) + httpRoutes, gateways := c.toHTTPRoutesAndGateways(spec, errors) for _, httpRoute := range httpRoutes { gatewayResources.HTTPRoutes[types.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}] = httpRoute } + // TODO: ReferenceGrants for _, gateway := range gateways { gatewayResources.Gateways[types.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()}] = gateway } @@ -104,7 +120,7 @@ type httpRouteMatcher struct { httpRouteRuleMatcher } -func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewayv1.HTTPRoute, []gatewayv1.Gateway) { +func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewayv1.HTTPRoute, []gatewayv1.Gateway) { var matchers []httpRouteMatcher servers := spec.Servers @@ -143,10 +159,13 @@ func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewa Name: gatewayName, }, Spec: gatewayv1.GatewaySpec{ - // TODO: gateway class + GatewayClassName: gatewayv1.ObjectName(c.gatewayClassName), }, } gateway.SetGroupVersionKind(common.GatewayGVK) + if c.namespace != "" { + gateway.SetNamespace(c.namespace) + } uniqueListeners := make(map[string]struct{}) for _, group := range listenerGroups { @@ -190,7 +209,7 @@ func toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewa if last > nMatchers { last = nMatchers } - routes = append(routes, toHTTPRoute(routeName, gatewayName, listenerName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last])) + routes = append(routes, c.toHTTPRoute(routeName, gatewayName, listenerName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last])) } i++ } @@ -239,7 +258,7 @@ func toListenerName(protocolAndHostname string) (listenerName gatewayv1.SectionN return gatewayv1.SectionName(listenerNamePrefix + protocol), protocol, hostname } -func toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, hostnames []string, matchers httpRouteRuleMatchers) gatewayv1.HTTPRoute { +func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, hostnames []string, matchers httpRouteRuleMatchers) gatewayv1.HTTPRoute { parentRef := gatewayv1.ParentReference{Name: gatewayv1.ObjectName(gatewayName)} if listenerName != "" { parentRef.SectionName = common.PtrTo(listenerName) @@ -256,22 +275,26 @@ func toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, h CommonRouteSpec: gatewayv1.CommonRouteSpec{ ParentRefs: []gatewayv1.ParentReference{parentRef}, }, - Rules: toHTTPRouteRules(matchers), + Rules: c.toHTTPRouteRules(matchers), }, } + if c.namespace != "" { + route.SetNamespace(c.namespace) + } if len(hostnames) > 1 || !slices.Contains(hostnames, HostWildcard) { route.Spec.Hostnames = lo.Map(hostnames, toGatewayAPIHostname) } return route } -func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule { +func (c *converter) toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule { var rules []gatewayv1.HTTPRouteRule nMatches := len(matchers) nRules := nMatches / HTTPRouteMatchesMax if len(matchers)%HTTPRouteMatchesMax != 0 { nRules++ } + backendRef := c.toBackendRef() for i := 0; i < nRules; i++ { rule := gatewayv1.HTTPRouteRule{} offfset := i * HTTPRouteMatchesMax @@ -302,12 +325,31 @@ func toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule } rule.Matches = append(rule.Matches, ruleMatch) } - // TODO: backendRefs + + rule.BackendRefs = []gatewayv1.HTTPBackendRef{backendRef} rules = append(rules, rule) } return rules } +func (c *converter) toBackendRef() gatewayv1.HTTPBackendRef { + obj := gatewayv1.BackendObjectReference{} + + backendKey := strings.SplitN(c.backendName, "/", 2) + if len(backendKey) == 2 { + obj.Name = gatewayv1.ObjectName(backendKey[1]) + obj.Namespace = common.PtrTo(gatewayv1.Namespace(backendKey[0])) + } else { + obj.Name = gatewayv1.ObjectName(backendKey[0]) + } + + return gatewayv1.HTTPBackendRef{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: obj, + }, + } +} + func pathItemToHTTPMatchers(pathItem *openapi3.PathItem, relativePath string, servers openapi3.Servers, errors field.ErrorList) []httpRouteMatcher { var matchers []httpRouteMatcher diff --git a/pkg/i2gw/providers/openapi3/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go index 3a6042508..37e03219a 100644 --- a/pkg/i2gw/providers/openapi3/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -50,7 +50,14 @@ func TestFileConvertion(t *testing.T) { return nil } - provider := NewProvider(&i2gw.ProviderConf{}) + provider := NewProvider(&i2gw.ProviderConf{ + ProviderSpecific: map[string]map[string]string{ + "openapi3": { + "backend": "backend-1", + "gateway-class-name": "external", + }, + }, + }) err = provider.ReadResourcesFromFile(ctx, path) if err != nil { diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml index 3de329705..586574515 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml @@ -3,6 +3,7 @@ kind: Gateway metadata: name: gateway-1 spec: + gatewayClassName: external listeners: - name: http hostname: "*" @@ -51,6 +52,8 @@ spec: path: type: Exact value: /api/v3/pet/{petId}/uploadImage + backendRefs: + - name: backend-1 - matches: - method: GET path: @@ -84,6 +87,8 @@ spec: path: type: Exact value: /api/v3/user/logout + backendRefs: + - name: backend-1 - matches: - method: DELETE path: @@ -97,5 +102,7 @@ spec: path: type: Exact value: /api/v3/user/{username} + backendRefs: + - name: backend-1 status: parents: null diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml index d3db91e6d..b26addc92 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml @@ -3,6 +3,7 @@ kind: Gateway metadata: name: gateway-1 spec: + gatewayClassName: external listeners: - name: http hostname: "*" @@ -45,6 +46,8 @@ spec: path: type: Exact value: /api/v1/resource/{id} + backendRefs: + - name: backend-1 status: parents: null --- @@ -73,6 +76,8 @@ spec: path: type: Exact value: /v3/status + backendRefs: + - name: backend-1 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute @@ -118,5 +123,7 @@ spec: path: type: Exact value: /v3/resource/{id} + backendRefs: + - name: backend-1 status: parents: null diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml index b740c8982..79caa9d8b 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml @@ -3,6 +3,7 @@ kind: Gateway metadata: name: gateway-1 spec: + gatewayClassName: external listeners: - name: http hostname: "*" @@ -37,5 +38,7 @@ spec: headers: - name: digest type: Exact + backendRefs: + - name: backend-1 status: parents: null diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json index 05786a45e..f5ba54c33 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json +++ b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json @@ -5,6 +5,7 @@ "name": "gateway-1" }, "spec": { + "gatewayClassName": "external", "listeners": [ { "name": "http", @@ -39,7 +40,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-006" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-007" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-008" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -51,7 +53,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-014" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-015" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-016" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -63,7 +66,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-022" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-023" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-024" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -75,7 +79,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-030" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-031" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-032" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -87,7 +92,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-038" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-039" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-040" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -99,7 +105,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-046" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-047" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-048" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -111,7 +118,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-054" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-055" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-056" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -123,7 +131,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-062" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-063" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-064" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -135,7 +144,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-070" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-071" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-072" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -147,7 +157,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-078" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-079" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-080" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -159,7 +170,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-086" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-087" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-088" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -171,7 +183,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-094" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-095" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-096" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -183,7 +196,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-102" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-103" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-104" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -195,7 +209,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-110" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-111" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-112" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -207,7 +222,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-118" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-119" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-120" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] }, { "matches": [ @@ -219,7 +235,8 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-126" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-127" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-128" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] } ] }, @@ -244,7 +261,8 @@ { "matches": [ { "method": "GET", "path": { "type": "Exact", "value": "/path-129" } } - ] + ], + "backendRefs": [{ "name": "backend-1" } ] } ] }, diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index d4b9d3175..40bcd8b0f 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -27,11 +27,26 @@ import ( "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" ) -// The ProviderName returned to the provider's registry. -const ProviderName = "openapi3" +const ( + // The ProviderName returned to the provider's registry. + ProviderName = "openapi3" + + BackendFlag = "backend" + GatewayClassFlag = "gateway-class-name" +) func init() { i2gw.ProviderConstructorByName[ProviderName] = NewProvider + + i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + Name: BackendFlag, + Description: "The name of the backend service to use in the HTTPRoutes", + }) + + i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + Name: GatewayClassFlag, + Description: "The name of the gateway class to use in the Gateways", + }) } type Provider struct { @@ -42,10 +57,10 @@ type Provider struct { var _ i2gw.Provider = &Provider{} // NewProvider returns an implementation of i2gw.Provider that converts OpenAPI specs to Gateway API resources. -func NewProvider(_ *i2gw.ProviderConf) i2gw.Provider { +func NewProvider(conf *i2gw.ProviderConf) i2gw.Provider { return &Provider{ storage: NewResourceStorage(), - converter: NewConverter(), + converter: NewConverter(conf), } } From b568bc9555af4bd9f8f598967b9fecdef04159dd Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Thu, 9 May 2024 15:48:28 +0200 Subject: [PATCH 15/26] provider-specific flag: --openapi3-gateway-tls-secret --- pkg/i2gw/providers/openapi3/converter.go | 34 +++++++++++++------ pkg/i2gw/providers/openapi3/converter_test.go | 1 + .../openapi3/fixtures/output/2-hostnames.yaml | 4 ++- pkg/i2gw/providers/openapi3/openapi.go | 6 ++++ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 60d9e76a1..9b6b9fb75 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -52,24 +52,24 @@ type Converter interface { // NewConverter returns a converter of OpenAPI Specifications 3.x from a storage into Gateway API resources. func NewConverter(conf *i2gw.ProviderConf) Converter { - var backendName, gatewayClassName string + converter := &converter{ + namespace: conf.Namespace, + } if ps := conf.ProviderSpecific[ProviderName]; ps != nil { - backendName = ps[BackendFlag] - gatewayClassName = ps[GatewayClassFlag] + converter.backendName = ps[BackendFlag] + converter.gatewayClassName = ps[GatewayClassFlag] + converter.tlsSecretName = ps[TlsSecretFlag] } - return &converter{ - namespace: conf.Namespace, - backendName: backendName, - gatewayClassName: gatewayClassName, - } + return converter } type converter struct { namespace string backendName string gatewayClassName string + tlsSecretName string } var _ Converter = &converter{} @@ -176,7 +176,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, errors field.Error } return !exists }) - gateway.Spec.Listeners = append(gateway.Spec.Listeners, lo.Map(listeners, toListener)...) // TODO: gateways cannot have more than 64 listeners + gateway.Spec.Listeners = append(gateway.Spec.Listeners, lo.Map(listeners, c.toListener)...) // TODO: gateways cannot have more than 64 listeners } var routes []gatewayv1.HTTPRoute @@ -217,7 +217,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, errors field.Error return routes, []gatewayv1.Gateway{gateway} } -func toListener(protocolAndHostname string, _ int) gatewayv1.Listener { +func (c *converter) toListener(protocolAndHostname string, _ int) gatewayv1.Listener { name, protocol, hostname := toListenerName(protocolAndHostname) listener := gatewayv1.Listener{ @@ -231,7 +231,19 @@ func toListener(protocolAndHostname string, _ int) gatewayv1.Listener { listener.Port = 80 case "https": listener.Port = 443 - listener.TLS = &gatewayv1.GatewayTLSConfig{} + + tlsSecretRef := gatewayv1.SecretObjectReference{} + tlsSecretKey := strings.SplitN(c.tlsSecretName, "/", 2) + if len(tlsSecretKey) == 2 { + tlsSecretRef.Namespace = common.PtrTo(gatewayv1.Namespace(tlsSecretKey[0])) + tlsSecretRef.Name = gatewayv1.ObjectName(tlsSecretKey[1]) + } else { + tlsSecretRef.Name = gatewayv1.ObjectName(tlsSecretKey[0]) + } + + listener.TLS = &gatewayv1.GatewayTLSConfig{ + CertificateRefs: []gatewayv1.SecretObjectReference{tlsSecretRef}, + } } return listener diff --git a/pkg/i2gw/providers/openapi3/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go index 51fcdf969..163e02aed 100644 --- a/pkg/i2gw/providers/openapi3/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -55,6 +55,7 @@ func TestFileConvertion(t *testing.T) { "openapi3": { "backend": "backend-1", "gateway-class-name": "external", + "gateway-tls-secret": "gateway-tls-cert", }, }, }) diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml index b26addc92..8a4261066 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml @@ -17,7 +17,9 @@ spec: hostname: api.example.com port: 443 protocol: HTTPS - tls: {} + tls: + certificateRefs: + - name: gateway-tls-cert --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index 8350fca85..2d8ed842d 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -33,6 +33,7 @@ const ( BackendFlag = "backend" GatewayClassFlag = "gateway-class-name" + TlsSecretFlag = "gateway-tls-secret" ) func init() { @@ -47,6 +48,11 @@ func init() { Name: GatewayClassFlag, Description: "The name of the gateway class to use in the Gateways", }) + + i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + Name: TlsSecretFlag, + Description: "The name of the secret for the TLS certificate references in the Gateways", + }) } type Provider struct { From deb54dcce84aa0985ad1899a9d66c11f74acd22e Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Thu, 9 May 2024 17:31:11 +0200 Subject: [PATCH 16/26] ReferenceGrants for HTTPRoute to Backends and Gateway to TLS Secrets --- pkg/i2gw/providers/openapi3/converter.go | 121 +++++++++++++----- pkg/i2gw/providers/openapi3/converter_test.go | 37 ++++-- .../fixtures/input/6-reference-grants.yaml | 13 ++ .../fixtures/output/6-reference-grants.yaml | 67 ++++++++++ 4 files changed, 194 insertions(+), 44 deletions(-) create mode 100644 pkg/i2gw/providers/openapi3/fixtures/input/6-reference-grants.yaml create mode 100644 pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 9b6b9fb75..9ac67a7ae 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -26,9 +26,11 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" @@ -53,13 +55,15 @@ type Converter interface { // NewConverter returns a converter of OpenAPI Specifications 3.x from a storage into Gateway API resources. func NewConverter(conf *i2gw.ProviderConf) Converter { converter := &converter{ - namespace: conf.Namespace, + namespace: conf.Namespace, + tlsSecretRef: &types.NamespacedName{}, + backendRef: &types.NamespacedName{}, } if ps := conf.ProviderSpecific[ProviderName]; ps != nil { - converter.backendName = ps[BackendFlag] converter.gatewayClassName = ps[GatewayClassFlag] - converter.tlsSecretName = ps[TlsSecretFlag] + converter.tlsSecretRef = toNamespacedName(ps[TlsSecretFlag]) + converter.backendRef = toNamespacedName(ps[BackendFlag]) } return converter @@ -67,17 +71,18 @@ func NewConverter(conf *i2gw.ProviderConf) Converter { type converter struct { namespace string - backendName string gatewayClassName string - tlsSecretName string + tlsSecretRef *types.NamespacedName + backendRef *types.NamespacedName } var _ Converter = &converter{} func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.ErrorList) { gatewayResources := i2gw.GatewayResources{ - Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), - HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), + Gateways: make(map[types.NamespacedName]gatewayv1.Gateway), + HTTPRoutes: make(map[types.NamespacedName]gatewayv1.HTTPRoute), + ReferenceGrants: make(map[types.NamespacedName]gatewayv1beta1.ReferenceGrant), } var errors field.ErrorList @@ -87,9 +92,14 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error for _, httpRoute := range httpRoutes { gatewayResources.HTTPRoutes[types.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}] = httpRoute } - // TODO: ReferenceGrants + if referenceGrant := c.buildHTTPRouteBackendReferenceGrant(); referenceGrant != nil { + gatewayResources.ReferenceGrants[types.NamespacedName{Name: referenceGrant.GetName(), Namespace: referenceGrant.GetNamespace()}] = *referenceGrant + } for _, gateway := range gateways { gatewayResources.Gateways[types.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()}] = gateway + if referenceGrant := c.buildGatewayTlsSecretReferenceGrant(gateway); referenceGrant != nil { + gatewayResources.ReferenceGrants[types.NamespacedName{Name: referenceGrant.GetName(), Namespace: referenceGrant.GetNamespace()}] = *referenceGrant + } } } @@ -232,13 +242,11 @@ func (c *converter) toListener(protocolAndHostname string, _ int) gatewayv1.List case "https": listener.Port = 443 - tlsSecretRef := gatewayv1.SecretObjectReference{} - tlsSecretKey := strings.SplitN(c.tlsSecretName, "/", 2) - if len(tlsSecretKey) == 2 { - tlsSecretRef.Namespace = common.PtrTo(gatewayv1.Namespace(tlsSecretKey[0])) - tlsSecretRef.Name = gatewayv1.ObjectName(tlsSecretKey[1]) - } else { - tlsSecretRef.Name = gatewayv1.ObjectName(tlsSecretKey[0]) + tlsSecretRef := gatewayv1.SecretObjectReference{ + Name: gatewayv1.ObjectName(c.tlsSecretRef.Name), + } + if c.tlsSecretRef.Namespace != "" { + tlsSecretRef.Namespace = common.PtrTo(gatewayv1.Namespace(c.tlsSecretRef.Namespace)) } listener.TLS = &gatewayv1.GatewayTLSConfig{ @@ -306,7 +314,11 @@ func (c *converter) toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1 if len(matchers)%HTTPRouteMatchesMax != 0 { nRules++ } - backendRef := c.toBackendRef() + + backendObjectReference := gatewayv1.BackendObjectReference{Name: gatewayv1.ObjectName(c.backendRef.Name)} + if c.backendRef.Namespace != "" { + backendObjectReference.Namespace = common.PtrTo(gatewayv1.Namespace(c.backendRef.Namespace)) + } for i := 0; i < nRules; i++ { rule := gatewayv1.HTTPRouteRule{} offfset := i * HTTPRouteMatchesMax @@ -338,30 +350,18 @@ func (c *converter) toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1 rule.Matches = append(rule.Matches, ruleMatch) } - rule.BackendRefs = []gatewayv1.HTTPBackendRef{backendRef} + rule.BackendRefs = []gatewayv1.HTTPBackendRef{ + gatewayv1.HTTPBackendRef{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: backendObjectReference, + }, + }, + } rules = append(rules, rule) } return rules } -func (c *converter) toBackendRef() gatewayv1.HTTPBackendRef { - obj := gatewayv1.BackendObjectReference{} - - backendKey := strings.SplitN(c.backendName, "/", 2) - if len(backendKey) == 2 { - obj.Name = gatewayv1.ObjectName(backendKey[1]) - obj.Namespace = common.PtrTo(gatewayv1.Namespace(backendKey[0])) - } else { - obj.Name = gatewayv1.ObjectName(backendKey[0]) - } - - return gatewayv1.HTTPBackendRef{ - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: obj, - }, - } -} - func pathItemToHTTPMatchers(pathItem *openapi3.PathItem, relativePath string, servers openapi3.Servers, errors field.ErrorList) []httpRouteMatcher { var matchers []httpRouteMatcher @@ -445,6 +445,46 @@ func toHTTPMatcher(relativePath string, method string, parameters openapi3.Param } } +func (c *converter) buildHTTPRouteBackendReferenceGrant() *gatewayv1beta1.ReferenceGrant { + return c.buildReferenceGrant(common.HTTPRouteGVK, gatewayv1.Kind("Service"), c.backendRef) +} + +func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gateway) *gatewayv1beta1.ReferenceGrant { + if slices.IndexFunc(gateway.Spec.Listeners, func(listener gatewayv1.Listener) bool { return listener.TLS != nil }) == -1 { + return nil + } + return c.buildReferenceGrant(common.GatewayGVK, gatewayv1.Kind("Secret"), c.tlsSecretRef) +} + +func (c *converter) buildReferenceGrant(fromGVK schema.GroupVersionKind, toKind gatewayv1.Kind, toRef *types.NamespacedName) *gatewayv1beta1.ReferenceGrant { + if c.namespace == "" || toRef.Namespace == "" { + return nil + } + rg := &gatewayv1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("from-%s-to-%s-%s", c.namespace, strings.ToLower(string(toKind)), toRef.Name), + Namespace: toRef.Namespace, + }, + Spec: gatewayv1beta1.ReferenceGrantSpec{ + From: []gatewayv1beta1.ReferenceGrantFrom{ + { + Group: gatewayv1.Group(fromGVK.Group), + Kind: gatewayv1.Kind(fromGVK.Kind), + Namespace: gatewayv1.Namespace(c.namespace), + }, + }, + To: []gatewayv1beta1.ReferenceGrantTo{ + { + Kind: toKind, + Name: common.PtrTo(gatewayv1.ObjectName(toRef.Name)), + }, + }, + }, + } + rg.SetGroupVersionKind(common.ReferenceGrantGVK) + return rg +} + func expandNonEnumServerVariables(server openapi3.Server) openapi3.Server { if len(server.Variables) == 0 { return server @@ -516,3 +556,14 @@ func uriToHostname(uri string, _ int) string { func toGatewayAPIHostname(hostname string, _ int) gatewayv1.Hostname { return gatewayv1.Hostname(hostname) } + +func toNamespacedName(s string) *types.NamespacedName { + if s == "" { + return nil + } + parts := strings.SplitN(s, "/", 2) + if len(parts) == 1 { + return &types.NamespacedName{Name: parts[0]} + } + return &types.NamespacedName{Namespace: parts[0], Name: parts[1]} +} diff --git a/pkg/i2gw/providers/openapi3/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go index 163e02aed..40dd747f5 100644 --- a/pkg/i2gw/providers/openapi3/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -23,6 +23,7 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "testing" apiequality "k8s.io/apimachinery/pkg/api/equality" @@ -42,6 +43,28 @@ const fixturesDir = "./fixtures" func TestFileConvertion(t *testing.T) { ctx := context.Background() + providerConf := map[string]*i2gw.ProviderConf{ + "default": &i2gw.ProviderConf{ + ProviderSpecific: map[string]map[string]string{ + "openapi3": { + "gateway-class-name": "external", + "gateway-tls-secret": "gateway-tls-cert", + "backend": "backend-1", + }, + }, + }, + "reference-grants.yaml": &i2gw.ProviderConf{ + Namespace: "networking", + ProviderSpecific: map[string]map[string]string{ + "openapi3": { + "gateway-class-name": "external", + "gateway-tls-secret": "secrets/gateway-tls-cert", + "backend": "apps/backend-1", + }, + }, + }, + } + filepath.WalkDir(filepath.Join(fixturesDir, "input"), func(path string, d fs.DirEntry, err error) error { if err != nil { t.Fatalf(err.Error()) @@ -50,15 +73,11 @@ func TestFileConvertion(t *testing.T) { return nil } - provider := NewProvider(&i2gw.ProviderConf{ - ProviderSpecific: map[string]map[string]string{ - "openapi3": { - "backend": "backend-1", - "gateway-class-name": "external", - "gateway-tls-secret": "gateway-tls-cert", - }, - }, - }) + conf, ok := providerConf[regexp.MustCompile(`\d+-(.+\.(json|yaml))$`).FindAllStringSubmatch(d.Name(), -1)[0][1]] + if !ok { + conf = providerConf["default"] + } + provider := NewProvider(conf) err = provider.ReadResourcesFromFile(ctx, path) if err != nil { diff --git a/pkg/i2gw/providers/openapi3/fixtures/input/6-reference-grants.yaml b/pkg/i2gw/providers/openapi3/fixtures/input/6-reference-grants.yaml new file mode 100644 index 000000000..f31c92ca4 --- /dev/null +++ b/pkg/i2gw/providers/openapi3/fixtures/input/6-reference-grants.yaml @@ -0,0 +1,13 @@ +openapi: 3.0.2 +info: + title: Sample API + version: 1.0.0 +servers: +- url: "https://api.example.com" +paths: + /resources: + post: + operationId: createResource + responses: + "200": + description: Successful operation diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml new file mode 100644 index 000000000..57ce15b1e --- /dev/null +++ b/pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml @@ -0,0 +1,67 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-1 + namespace: networking +spec: + gatewayClassName: external + listeners: + - name: api-example-com-https + hostname: api.example.com + port: 443 + protocol: HTTPS + tls: + certificateRefs: + - name: gateway-tls-cert + namespace: secrets +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: from-networking-to-secret-gateway-tls-cert + namespace: secrets +spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: networking + to: + - kind: Secret + name: gateway-tls-cert +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: route-1-1 + namespace: networking +spec: + parentRefs: + - name: gateway-1 + hostnames: + - api.example.com + rules: + - matches: + - method: POST + path: + type: Exact + value: /resources + backendRefs: + - name: backend-1 + namespace: apps +status: + parents: null +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: from-networking-to-service-backend-1 + namespace: apps +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: networking + to: + - kind: Service + name: backend-1 From 0de2c2412578caf273fcba04f3a7834fd97e2b7a Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 May 2024 12:05:37 +0200 Subject: [PATCH 17/26] fix: provider-specific configs for providers with dashes in the name --- cmd/print.go | 30 +++++++++++++++---------- cmd/print_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/cmd/print.go b/cmd/print.go index ddcbd02cd..98e2b9bf0 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" + "github.com/samber/lo" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" @@ -79,18 +80,7 @@ func (pr *PrintRunner) PrintGatewayAPIObjects(cmd *cobra.Command, _ []string) er return fmt.Errorf("failed to initialize namespace filter: %w", err) } - providerSpecificConf := make(map[string]map[string]string) - for flagName, value := range pr.providerSpecificConf { - parts := strings.SplitN(flagName, "-", 2) - provider := parts[0] - conf := parts[1] - if providerSpecificConf[provider] == nil { - providerSpecificConf[provider] = make(map[string]string) - } - providerSpecificConf[provider][conf] = *value - } - - gatewayResources, err := i2gw.ToGatewayAPIResources(cmd.Context(), pr.namespaceFilter, pr.inputFile, pr.providers, providerSpecificConf) + gatewayResources, err := i2gw.ToGatewayAPIResources(cmd.Context(), pr.namespaceFilter, pr.inputFile, pr.providers, pr.getProviderSpecificConf()) if err != nil { return err } @@ -285,3 +275,19 @@ func getNamespaceInCurrentContext() (string, error) { return currentNamespace, err } + +func (pr *PrintRunner) getProviderSpecificConf() map[string]map[string]string { + providerSpecificConf := make(map[string]map[string]string) + for flagName, value := range pr.providerSpecificConf { + provider, found := lo.Find(pr.providers, func(p string) bool { return strings.HasPrefix(flagName, p+"-") }) + if !found { + continue + } + conf := strings.SplitN(flagName, provider+"-", 2)[1] + if providerSpecificConf[provider] == nil { + providerSpecificConf[provider] = make(map[string]string) + } + providerSpecificConf[provider][conf] = *value + } + return providerSpecificConf +} diff --git a/cmd/print_test.go b/cmd/print_test.go index 3c95dcc92..60e33427d 100644 --- a/cmd/print_test.go +++ b/cmd/print_test.go @@ -236,3 +236,60 @@ func Test_getNamespaceInCurrentContext(t *testing.T) { actualNamespace, err, expectedNamespace, nil) } } + +func Test_getProviderSpecificConf(t *testing.T) { + value1 := "value1" + value2 := "value2" + testCases := []struct { + name string + providerSpecificConf map[string]*string + providers []string + expected map[string]map[string]string + }{ + { + name: "No provider specific configuration", + providerSpecificConf: make(map[string]*string), + providers: []string{"provider"}, + expected: map[string]map[string]string{}, + }, + { + name: "Provider specific configuration matching provider in the list", + providerSpecificConf: map[string]*string{"provider-conf": &value1}, + providers: []string{"provider"}, + expected: map[string]map[string]string{ + "provider": {"conf": value1}, + }, + }, + { + name: "Provider specific configuration matching providers in the list with multiple providers", + providerSpecificConf: map[string]*string{ + "provider-a-conf1": &value1, + "provider-b-conf2": &value2, + }, + providers: []string{"provider-a", "provider-b", "provider-c"}, + expected: map[string]map[string]string{ + "provider-a": {"conf1": value1}, + "provider-b": {"conf2": value2}, + }, + }, + { + name: "Provider specific configuration not matching provider in the list", + providerSpecificConf: map[string]*string{"provider-conf": &value1}, + providers: []string{"provider-a", "provider-b", "provider-c"}, + expected: map[string]map[string]string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pr := PrintRunner{ + providerSpecificConf: tc.providerSpecificConf, + providers: tc.providers, + } + actual := pr.getProviderSpecificConf() + if !reflect.DeepEqual(actual, tc.expected) { + t.Errorf("getProviderSpecificConf() = %v, expected %v", actual, tc.expected) + } + }) + } +} From 0f4712135a2cf36f97a5a92e159b6feb7680e187 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 May 2024 12:58:17 +0200 Subject: [PATCH 18/26] name Gateway and HTTPRoutes after the OAS title --- pkg/i2gw/providers/openapi3/converter.go | 194 ++++++++++-------- .../openapi3/fixtures/output/1-petstore3.yaml | 6 +- .../openapi3/fixtures/output/2-hostnames.yaml | 14 +- .../fixtures/output/3-parameters.yaml | 6 +- .../fixtures/output/4-too-many-rules.json | 10 +- .../fixtures/output/6-reference-grants.yaml | 6 +- 6 files changed, 129 insertions(+), 107 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 9ac67a7ae..c2ccd50df 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -86,9 +86,19 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error } var errors field.ErrorList + resourcesNamePrefixes := make(map[string]int) for _, spec := range storage.GetResources() { - httpRoutes, gateways := c.toHTTPRoutesAndGateways(spec, errors) + resourcesNamePrefix := toResourcesNamePrefix(spec) + if _, exists := resourcesNamePrefixes[resourcesNamePrefix]; !exists { + resourcesNamePrefixes[resourcesNamePrefix] = 0 + } + resourcesNamePrefixes[resourcesNamePrefix]++ + if resourcesNamePrefixes[resourcesNamePrefix] > 1 { + resourcesNamePrefix = fmt.Sprintf("%s-%d", resourcesNamePrefix, resourcesNamePrefixes[resourcesNamePrefix]+1) + } + + httpRoutes, gateways := c.toHTTPRoutesAndGateways(spec, resourcesNamePrefix, errors) for _, httpRoute := range httpRoutes { gatewayResources.HTTPRoutes[types.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}] = httpRoute } @@ -106,31 +116,7 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error return gatewayResources, errors } -type httpRouteRuleMatcher struct { - path string - method string - headers string - params string -} - -type httpRouteRuleMatchers []httpRouteRuleMatcher - -func (m httpRouteRuleMatchers) Len() int { return len(m) } -func (m httpRouteRuleMatchers) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -func (m httpRouteRuleMatchers) Less(i, j int) bool { - if m[i].path != m[j].path { - return m[i].path < m[j].path - } - return m[i].method < m[j].method -} - -type httpRouteMatcher struct { - protocol string - host string - httpRouteRuleMatcher -} - -func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, errors field.ErrorList) ([]gatewayv1.HTTPRoute, []gatewayv1.Gateway) { +func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefix string, errors field.ErrorList) ([]gatewayv1.HTTPRoute, []gatewayv1.Gateway) { var matchers []httpRouteMatcher servers := spec.Servers @@ -163,7 +149,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, errors field.Error // sort listener groups for deterministic output sort.Strings(listenerGroups) - gatewayName := "gateway-1" // TODO: name the gateway after the spec + gatewayName := fmt.Sprintf("%s-gateway", resourcesNamePrefix) gateway := gatewayv1.Gateway{ ObjectMeta: metav1.ObjectMeta{ Name: gatewayName, @@ -191,6 +177,19 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, errors field.Error var routes []gatewayv1.HTTPRoute + backendRefs := []gatewayv1.HTTPBackendRef{ + gatewayv1.HTTPBackendRef{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: gatewayv1.ObjectName(c.backendRef.Name), + }, + }, + }, + } + if c.backendRef.Namespace != "" { + backendRefs[0].BackendRef.BackendObjectReference.Namespace = common.PtrTo(gatewayv1.Namespace(c.backendRef.Namespace)) + } + i := 0 for _, group := range listenerGroups { listeners := strings.Split(group, HostSeparator) @@ -213,13 +212,18 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, errors field.Error nRoutes++ } for j := 0; j < nRoutes; j++ { - routeName := fmt.Sprintf("route-%d-%d", i+1, j+1) - // TODO: name the route after the spec + routeName := fmt.Sprintf("%s-route", resourcesNamePrefix) + if len(listenerGroups) > 1 { + routeName = fmt.Sprintf("%s-%d", routeName, i+1) + } + if nRoutes > 1 { + routeName = fmt.Sprintf("%s-%d", routeName, j+1) + } last := (j + 1) * HTTPRouteMatchesMaxMax if last > nMatchers { last = nMatchers } - routes = append(routes, c.toHTTPRoute(routeName, gatewayName, listenerName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last])) + routes = append(routes, c.toHTTPRoute(routeName, gatewayName, listenerName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last], backendRefs)) } i++ } @@ -278,7 +282,7 @@ func toListenerName(protocolAndHostname string) (listenerName gatewayv1.SectionN return gatewayv1.SectionName(listenerNamePrefix + protocol), protocol, hostname } -func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, hostnames []string, matchers httpRouteRuleMatchers) gatewayv1.HTTPRoute { +func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, hostnames []string, matchers httpRouteRuleMatchers, backendRefs []gatewayv1.HTTPBackendRef) gatewayv1.HTTPRoute { parentRef := gatewayv1.ParentReference{Name: gatewayv1.ObjectName(gatewayName)} if listenerName != "" { parentRef.SectionName = common.PtrTo(listenerName) @@ -295,7 +299,7 @@ func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1 CommonRouteSpec: gatewayv1.CommonRouteSpec{ ParentRefs: []gatewayv1.ParentReference{parentRef}, }, - Rules: c.toHTTPRouteRules(matchers), + Rules: toHTTPRouteRules(matchers, backendRefs), }, } if c.namespace != "" { @@ -307,7 +311,71 @@ func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1 return route } -func (c *converter) toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1.HTTPRouteRule { +func (c *converter) buildHTTPRouteBackendReferenceGrant() *gatewayv1beta1.ReferenceGrant { + return c.buildReferenceGrant(common.HTTPRouteGVK, gatewayv1.Kind("Service"), c.backendRef) +} + +func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gateway) *gatewayv1beta1.ReferenceGrant { + if slices.IndexFunc(gateway.Spec.Listeners, func(listener gatewayv1.Listener) bool { return listener.TLS != nil }) == -1 { + return nil + } + return c.buildReferenceGrant(common.GatewayGVK, gatewayv1.Kind("Secret"), c.tlsSecretRef) +} + +func (c *converter) buildReferenceGrant(fromGVK schema.GroupVersionKind, toKind gatewayv1.Kind, toRef *types.NamespacedName) *gatewayv1beta1.ReferenceGrant { + if c.namespace == "" || toRef.Namespace == "" { + return nil + } + rg := &gatewayv1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("from-%s-to-%s-%s", c.namespace, strings.ToLower(string(toKind)), toRef.Name), + Namespace: toRef.Namespace, + }, + Spec: gatewayv1beta1.ReferenceGrantSpec{ + From: []gatewayv1beta1.ReferenceGrantFrom{ + { + Group: gatewayv1.Group(fromGVK.Group), + Kind: gatewayv1.Kind(fromGVK.Kind), + Namespace: gatewayv1.Namespace(c.namespace), + }, + }, + To: []gatewayv1beta1.ReferenceGrantTo{ + { + Kind: toKind, + Name: common.PtrTo(gatewayv1.ObjectName(toRef.Name)), + }, + }, + }, + } + rg.SetGroupVersionKind(common.ReferenceGrantGVK) + return rg +} + +type httpRouteRuleMatcher struct { + path string + method string + headers string + params string +} + +type httpRouteRuleMatchers []httpRouteRuleMatcher + +func (m httpRouteRuleMatchers) Len() int { return len(m) } +func (m httpRouteRuleMatchers) Swap(i, j int) { m[i], m[j] = m[j], m[i] } +func (m httpRouteRuleMatchers) Less(i, j int) bool { + if m[i].path != m[j].path { + return m[i].path < m[j].path + } + return m[i].method < m[j].method +} + +type httpRouteMatcher struct { + protocol string + host string + httpRouteRuleMatcher +} + +func toHTTPRouteRules(matchers httpRouteRuleMatchers, backendRefs []gatewayv1.HTTPBackendRef) []gatewayv1.HTTPRouteRule { var rules []gatewayv1.HTTPRouteRule nMatches := len(matchers) nRules := nMatches / HTTPRouteMatchesMax @@ -315,12 +383,10 @@ func (c *converter) toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1 nRules++ } - backendObjectReference := gatewayv1.BackendObjectReference{Name: gatewayv1.ObjectName(c.backendRef.Name)} - if c.backendRef.Namespace != "" { - backendObjectReference.Namespace = common.PtrTo(gatewayv1.Namespace(c.backendRef.Namespace)) - } for i := 0; i < nRules; i++ { - rule := gatewayv1.HTTPRouteRule{} + rule := gatewayv1.HTTPRouteRule{ + BackendRefs: backendRefs, + } offfset := i * HTTPRouteMatchesMax for j := 0; j < HTTPRouteMatchesMax && offfset+j < nMatches; j++ { matcher := matchers[offfset+j] @@ -349,14 +415,6 @@ func (c *converter) toHTTPRouteRules(matchers httpRouteRuleMatchers) []gatewayv1 } rule.Matches = append(rule.Matches, ruleMatch) } - - rule.BackendRefs = []gatewayv1.HTTPBackendRef{ - gatewayv1.HTTPBackendRef{ - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: backendObjectReference, - }, - }, - } rules = append(rules, rule) } return rules @@ -445,46 +503,6 @@ func toHTTPMatcher(relativePath string, method string, parameters openapi3.Param } } -func (c *converter) buildHTTPRouteBackendReferenceGrant() *gatewayv1beta1.ReferenceGrant { - return c.buildReferenceGrant(common.HTTPRouteGVK, gatewayv1.Kind("Service"), c.backendRef) -} - -func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gateway) *gatewayv1beta1.ReferenceGrant { - if slices.IndexFunc(gateway.Spec.Listeners, func(listener gatewayv1.Listener) bool { return listener.TLS != nil }) == -1 { - return nil - } - return c.buildReferenceGrant(common.GatewayGVK, gatewayv1.Kind("Secret"), c.tlsSecretRef) -} - -func (c *converter) buildReferenceGrant(fromGVK schema.GroupVersionKind, toKind gatewayv1.Kind, toRef *types.NamespacedName) *gatewayv1beta1.ReferenceGrant { - if c.namespace == "" || toRef.Namespace == "" { - return nil - } - rg := &gatewayv1beta1.ReferenceGrant{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("from-%s-to-%s-%s", c.namespace, strings.ToLower(string(toKind)), toRef.Name), - Namespace: toRef.Namespace, - }, - Spec: gatewayv1beta1.ReferenceGrantSpec{ - From: []gatewayv1beta1.ReferenceGrantFrom{ - { - Group: gatewayv1.Group(fromGVK.Group), - Kind: gatewayv1.Kind(fromGVK.Kind), - Namespace: gatewayv1.Namespace(c.namespace), - }, - }, - To: []gatewayv1beta1.ReferenceGrantTo{ - { - Kind: toKind, - Name: common.PtrTo(gatewayv1.ObjectName(toRef.Name)), - }, - }, - }, - } - rg.SetGroupVersionKind(common.ReferenceGrantGVK) - return rg -} - func expandNonEnumServerVariables(server openapi3.Server) openapi3.Server { if len(server.Variables) == 0 { return server @@ -557,6 +575,10 @@ func toGatewayAPIHostname(hostname string, _ int) gatewayv1.Hostname { return gatewayv1.Hostname(hostname) } +func toResourcesNamePrefix(spec *openapi3.T) string { + return strings.ToLower(common.NameFromHost(spec.Info.Title)) +} + func toNamespacedName(s string) *types.NamespacedName { if s == "" { return nil diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml index 586574515..5bea02733 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: - name: gateway-1 + name: swagger-petstore-openapi-3-0-gateway spec: gatewayClassName: external listeners: @@ -14,10 +14,10 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null - name: route-1-1 + name: swagger-petstore-openapi-3-0-route spec: parentRefs: - - name: gateway-1 + - name: swagger-petstore-openapi-3-0-gateway rules: - matches: - method: POST diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml index 8a4261066..7322152db 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: - name: gateway-1 + name: sample-api-gateway spec: gatewayClassName: external listeners: @@ -25,10 +25,10 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null - name: route-1-1 + name: sample-api-route-1 spec: parentRefs: - - name: gateway-1 + - name: sample-api-gateway sectionName: http rules: - matches: @@ -57,10 +57,10 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null - name: route-2-1 + name: sample-api-route-2 spec: parentRefs: - - name: gateway-1 + - name: sample-api-gateway sectionName: api-example-com-http hostnames: - api.example.com @@ -85,12 +85,12 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null - name: route-3-1 + name: sample-api-route-3 spec: hostnames: - api.example.com parentRefs: - - name: gateway-1 + - name: sample-api-gateway rules: - matches: - method: POST diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml index 79caa9d8b..32d1ba154 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: - name: gateway-1 + name: sample-api-gateway spec: gatewayClassName: external listeners: @@ -14,10 +14,10 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null - name: route-1-1 + name: sample-api-route spec: parentRefs: - - name: gateway-1 + - name: sample-api-gateway rules: - matches: - method: GET diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json index f5ba54c33..512de8e27 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json +++ b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json @@ -2,7 +2,7 @@ "apiVersion": "gateway.networking.k8s.io/v1", "kind": "Gateway", "metadata": { - "name": "gateway-1" + "name": "sample-api-gateway" }, "spec": { "gatewayClassName": "external", @@ -21,12 +21,12 @@ "kind": "HTTPRoute", "metadata": { "creationTimestamp": null, - "name": "route-1-1" + "name": "sample-api-route-1" }, "spec": { "parentRefs": [ { - "name": "gateway-1" + "name": "sample-api-gateway" } ], "rules": [ @@ -249,12 +249,12 @@ "kind": "HTTPRoute", "metadata": { "creationTimestamp": null, - "name": "route-1-2" + "name": "sample-api-route-2" }, "spec": { "parentRefs": [ { - "name": "gateway-1" + "name": "sample-api-gateway" } ], "rules": [ diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml index 57ce15b1e..9a93ebb1f 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/6-reference-grants.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: - name: gateway-1 + name: sample-api-gateway namespace: networking spec: gatewayClassName: external @@ -33,11 +33,11 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: creationTimestamp: null - name: route-1-1 + name: sample-api-route namespace: networking spec: parentRefs: - - name: gateway-1 + - name: sample-api-gateway hostnames: - api.example.com rules: From 170c24cd19fc9df8d80bff28f69e2e49c0dd9d45 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 May 2024 13:30:29 +0200 Subject: [PATCH 19/26] fix: missing backend ref argument --- pkg/i2gw/providers/openapi3/converter.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index c2ccd50df..5327f76a0 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -56,8 +56,8 @@ type Converter interface { func NewConverter(conf *i2gw.ProviderConf) Converter { converter := &converter{ namespace: conf.Namespace, - tlsSecretRef: &types.NamespacedName{}, - backendRef: &types.NamespacedName{}, + tlsSecretRef: types.NamespacedName{}, + backendRef: types.NamespacedName{}, } if ps := conf.ProviderSpecific[ProviderName]; ps != nil { @@ -72,8 +72,8 @@ func NewConverter(conf *i2gw.ProviderConf) Converter { type converter struct { namespace string gatewayClassName string - tlsSecretRef *types.NamespacedName - backendRef *types.NamespacedName + tlsSecretRef types.NamespacedName + backendRef types.NamespacedName } var _ Converter = &converter{} @@ -322,7 +322,7 @@ func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gatewa return c.buildReferenceGrant(common.GatewayGVK, gatewayv1.Kind("Secret"), c.tlsSecretRef) } -func (c *converter) buildReferenceGrant(fromGVK schema.GroupVersionKind, toKind gatewayv1.Kind, toRef *types.NamespacedName) *gatewayv1beta1.ReferenceGrant { +func (c *converter) buildReferenceGrant(fromGVK schema.GroupVersionKind, toKind gatewayv1.Kind, toRef types.NamespacedName) *gatewayv1beta1.ReferenceGrant { if c.namespace == "" || toRef.Namespace == "" { return nil } @@ -579,13 +579,13 @@ func toResourcesNamePrefix(spec *openapi3.T) string { return strings.ToLower(common.NameFromHost(spec.Info.Title)) } -func toNamespacedName(s string) *types.NamespacedName { +func toNamespacedName(s string) types.NamespacedName { if s == "" { - return nil + return types.NamespacedName{} } parts := strings.SplitN(s, "/", 2) if len(parts) == 1 { - return &types.NamespacedName{Name: parts[0]} + return types.NamespacedName{Name: parts[0]} } - return &types.NamespacedName{Namespace: parts[0], Name: parts[1]} + return types.NamespacedName{Namespace: parts[0], Name: parts[1]} } From ce7e0b1d1555040f41ae629b63e4f8d130520638 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Fri, 10 May 2024 13:44:32 +0200 Subject: [PATCH 20/26] update README --- pkg/i2gw/providers/openapi3/README.md | 319 +++++++++++++++++++++++++- 1 file changed, 313 insertions(+), 6 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/README.md b/pkg/i2gw/providers/openapi3/README.md index 1554c2462..94060eccf 100644 --- a/pkg/i2gw/providers/openapi3/README.md +++ b/pkg/i2gw/providers/openapi3/README.md @@ -1,20 +1,327 @@ # OpenAPI Provider -The provider translates OpenAPI Specification (OAS) 3.x documents to Kubernetes Gateway API resources Gateway and HTTPRoute. +The provider translates OpenAPI Specification (OAS) 3.x documents to Kubernetes Gateway API resources – Gateway, HTTPRoutes and ReferenceGrants. -## Example +## Usage ```sh -./ingress2gateway print --providers openapi3 --input-file=petstore3-openapi.json +./ingress2gateway print --providers=openapi3 --input-file=FILEPATH ``` -## Known limitations +Where `FILEPATH` is the path to a file containing a valid OpenAPI Specification in YAML or JSON format. -* Only offline translation supported – i.e. `--input-file` required -* All API operation [paths](https://swagger.io/specification/v3/#paths-object) treated as `Exact` type – i.e. no support for [path templating](https://swagger.io/specification/v3/#path-templating), therefore no `PathPrefix`, nor `RegularExpression` path types output +**Gateway class name** + +To specify the name of the gateway class for the Gateway resources, use `--openapi3-gateway-class-name=NAME`. + +**Gateways with TLS configuration** + +If one or more servers specified in the OAS start with `https`, TLS configuration will be added to the corresponding gateway listener. +To specify the reference to the gateway TLS secret, use `--openapi3-gateway-tls-secret=SECRET-NAME` or `--openapi3-gateway-tls-secret=SECRET-NAMESPACE/SECRET-NAME`. + +**Backend references** + +All routes generated will point to a single backend service. +To specify the backend reference, use `--openapi3-backend=SERVICE-NAME` or `--openapi3-backend=SERVICE-NAMESPACE/SERVICE-NAME`. + +Specifying the port number to the backend service is currently not supported. + +**Resource names** + +Gateway and HTTPRoute names are prefixed with the [title](https://swagger.io/specification/v3/) of the OAS converted to Kubernetes object name format. + +In case of multiple resources of a kind, the names are suffixed with the corresponding sequential number from 1. + +In all cases, ensure the title of the OAS is not long enough that would cause invalid [Kubernetes object names](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/). + +## Examples + +The examples below are based on the [Swagger Petstore Sample API](https://petstore3.swagger.io). + +```sh +./ingress2gateway print --providers=openapi3 \ + --openapi3-gateway-class-name=istio \ + --openapi3-backend=my-app \ + --input-file=petstore3-openapi.json +``` + +
+ Expected output + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + creationTimestamp: null + name: swagger-petstore-openapi-3-0-gateway + namespace: default +spec: + gatewayClassName: istio + listeners: + - hostname: '*' + name: http + port: 80 + protocol: HTTP +status: {} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: swagger-petstore-openapi-3-0-route + namespace: default +spec: + parentRefs: + - name: swagger-petstore-openapi-3-0-gateway + rules: + - backendRefs: + - name: my-app + matches: + - method: POST + path: + type: Exact + value: /api/v3/pet + - method: PUT + path: + type: Exact + value: /api/v3/pet + - method: GET + path: + type: Exact + value: /api/v3/pet/findByStatus + - method: GET + path: + type: Exact + value: /api/v3/pet/findByTags + - method: DELETE + path: + type: Exact + value: /api/v3/pet/{petId} + - method: GET + path: + type: Exact + value: /api/v3/pet/{petId} + - method: POST + path: + type: Exact + value: /api/v3/pet/{petId} + - method: POST + path: + type: Exact + value: /api/v3/pet/{petId}/uploadImage + - backendRefs: + - name: my-app + matches: + - method: GET + path: + type: Exact + value: /api/v3/store/inventory + - method: POST + path: + type: Exact + value: /api/v3/store/order + - method: DELETE + path: + type: Exact + value: /api/v3/store/order/{orderId} + - method: GET + path: + type: Exact + value: /api/v3/store/order/{orderId} + - method: POST + path: + type: Exact + value: /api/v3/user + - method: POST + path: + type: Exact + value: /api/v3/user/createWithList + - method: GET + path: + type: Exact + value: /api/v3/user/login + - method: GET + path: + type: Exact + value: /api/v3/user/logout + - backendRefs: + - name: my-app + matches: + - method: DELETE + path: + type: Exact + value: /api/v3/user/{username} + - method: GET + path: + type: Exact + value: /api/v3/user/{username} + - method: PUT + path: + type: Exact + value: /api/v3/user/{username} +status: + parents: null +``` + +
+ +ReferenceGrants are only generated if a namespace is specified and/or the references to gateway TLS secrets or backends do not match the target namespace, which can occasionally unspecified. E.g.: + +```sh +./ingress2gateway print --providers=openapi3 \ + --namespace=networking \ + --openapi3-gateway-class-name=istio \ + --openapi3-backend=apps/my-app \ + --input-file=petstore3-openapi.json +``` + +
+ Expected output + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + creationTimestamp: null + name: swagger-petstore-openapi-3-0-gateway + namespace: networking +spec: + gatewayClassName: istio + listeners: + - hostname: '*' + name: http + port: 80 + protocol: HTTP +status: {} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + creationTimestamp: null + name: swagger-petstore-openapi-3-0-route + namespace: networking +spec: + parentRefs: + - name: swagger-petstore-openapi-3-0-gateway + rules: + - backendRefs: + - name: my-app + namespace: apps + matches: + - method: POST + path: + type: Exact + value: /api/v3/pet + - method: PUT + path: + type: Exact + value: /api/v3/pet + - method: GET + path: + type: Exact + value: /api/v3/pet/findByStatus + - method: GET + path: + type: Exact + value: /api/v3/pet/findByTags + - method: DELETE + path: + type: Exact + value: /api/v3/pet/{petId} + - method: GET + path: + type: Exact + value: /api/v3/pet/{petId} + - method: POST + path: + type: Exact + value: /api/v3/pet/{petId} + - method: POST + path: + type: Exact + value: /api/v3/pet/{petId}/uploadImage + - backendRefs: + - name: my-app + namespace: apps + matches: + - method: GET + path: + type: Exact + value: /api/v3/store/inventory + - method: POST + path: + type: Exact + value: /api/v3/store/order + - method: DELETE + path: + type: Exact + value: /api/v3/store/order/{orderId} + - method: GET + path: + type: Exact + value: /api/v3/store/order/{orderId} + - method: POST + path: + type: Exact + value: /api/v3/user + - method: POST + path: + type: Exact + value: /api/v3/user/createWithList + - method: GET + path: + type: Exact + value: /api/v3/user/login + - method: GET + path: + type: Exact + value: /api/v3/user/logout + - backendRefs: + - name: my-app + namespace: apps + matches: + - method: DELETE + path: + type: Exact + value: /api/v3/user/{username} + - method: GET + path: + type: Exact + value: /api/v3/user/{username} + - method: PUT + path: + type: Exact + value: /api/v3/user/{username} +status: + parents: null +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + creationTimestamp: null + name: from-networking-to-service-my-app + namespace: apps +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: networking + to: + - group: "" + kind: Service + name: my-app +``` +
+ +## Limitations + +* Only offline translation supported – i.e. `--input-file` is required +* An input file can only declare one OpenAPI Specification +* All API operation [paths](https://swagger.io/specification/v3/#paths-object) are treated as `Exact` type – i.e. no support for [path templating](https://swagger.io/specification/v3/#path-templating), therefore no `PathPrefix`, nor `RegularExpression` path types output * Limited support for [parameters](https://swagger.io/specification/v3/#parameter-object) – only required `header` and `query` parameters supported * Limited support to [server variables](https://swagger.io/specification/v3/#server-variable-object) – only limited sets (`enum`) supported * No support to [references](https://swagger.io/specification/v3/#reference-object) (`$ref`) * No support to [external documents](https://swagger.io/specification/v3/#external-documentation-object) +* OpenAPI Specification with a large number of server combinations may generate Gateway resources with more listeners than allowed Additionally, no support to any OpenAPI feature with no direct equivalent to core Gateway API fields, such as [request bodies](https://swagger.io/specification/v3/#request-body-object), [examples](https://swagger.io/specification/v3/#example-object), [security schemes](https://swagger.io/specification/v3/#security-scheme-object), [callbacks](https://swagger.io/specification/v3/#callback-object), [extensions](https://swagger.io/specification/v3/#specification-extensions), etc. From 455fbbb42ff684b13d6fbc4c3b92d5acefbda513 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 14 May 2024 11:59:20 +0200 Subject: [PATCH 21/26] Support for backend port numbers --- pkg/i2gw/providers/openapi3/README.md | 13 ++++-- pkg/i2gw/providers/openapi3/converter.go | 40 ++++++++++++++++--- pkg/i2gw/providers/openapi3/converter_test.go | 2 +- .../openapi3/fixtures/output/1-petstore3.yaml | 3 ++ .../openapi3/fixtures/output/2-hostnames.yaml | 3 ++ .../fixtures/output/3-parameters.yaml | 1 + .../fixtures/output/4-too-many-rules.json | 34 ++++++++-------- 7 files changed, 69 insertions(+), 27 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/README.md b/pkg/i2gw/providers/openapi3/README.md index 94060eccf..f8bbba79d 100644 --- a/pkg/i2gw/providers/openapi3/README.md +++ b/pkg/i2gw/providers/openapi3/README.md @@ -17,14 +17,18 @@ To specify the name of the gateway class for the Gateway resources, use `--opena **Gateways with TLS configuration** If one or more servers specified in the OAS start with `https`, TLS configuration will be added to the corresponding gateway listener. + To specify the reference to the gateway TLS secret, use `--openapi3-gateway-tls-secret=SECRET-NAME` or `--openapi3-gateway-tls-secret=SECRET-NAMESPACE/SECRET-NAME`. **Backend references** All routes generated will point to a single backend service. -To specify the backend reference, use `--openapi3-backend=SERVICE-NAME` or `--openapi3-backend=SERVICE-NAMESPACE/SERVICE-NAME`. -Specifying the port number to the backend service is currently not supported. +To specify the backend reference, use `--openapi3-backend=[namespace/]name[:port]`. Examples of valid values: +* `my-service` +* `my-namespace/my-service` +* `my-service:3000` +* `my-namespace/my-service:3000` **Resource names** @@ -41,7 +45,7 @@ The examples below are based on the [Swagger Petstore Sample API](https://petsto ```sh ./ingress2gateway print --providers=openapi3 \ --openapi3-gateway-class-name=istio \ - --openapi3-backend=my-app \ + --openapi3-backend=my-app:3000 \ --input-file=petstore3-openapi.json ``` @@ -76,6 +80,7 @@ spec: rules: - backendRefs: - name: my-app + port: 3000 matches: - method: POST path: @@ -111,6 +116,7 @@ spec: value: /api/v3/pet/{petId}/uploadImage - backendRefs: - name: my-app + port: 3000 matches: - method: GET path: @@ -146,6 +152,7 @@ spec: value: /api/v3/user/logout - backendRefs: - name: my-app + port: 3000 matches: - method: DELETE path: diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 5327f76a0..a52995614 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -18,9 +18,11 @@ package openapi3 import ( "fmt" + "log" "regexp" "slices" "sort" + "strconv" "strings" "github.com/getkin/kin-openapi/openapi3" @@ -57,23 +59,28 @@ func NewConverter(conf *i2gw.ProviderConf) Converter { converter := &converter{ namespace: conf.Namespace, tlsSecretRef: types.NamespacedName{}, - backendRef: types.NamespacedName{}, + backendRef: toBackendRef(""), } if ps := conf.ProviderSpecific[ProviderName]; ps != nil { converter.gatewayClassName = ps[GatewayClassFlag] converter.tlsSecretRef = toNamespacedName(ps[TlsSecretFlag]) - converter.backendRef = toNamespacedName(ps[BackendFlag]) + converter.backendRef = toBackendRef(ps[BackendFlag]) } return converter } +type backendRef struct { + types.NamespacedName + port *gatewayv1.PortNumber +} + type converter struct { namespace string gatewayClassName string tlsSecretRef types.NamespacedName - backendRef types.NamespacedName + backendRef backendRef } var _ Converter = &converter{} @@ -186,8 +193,11 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi }, }, } - if c.backendRef.Namespace != "" { - backendRefs[0].BackendRef.BackendObjectReference.Namespace = common.PtrTo(gatewayv1.Namespace(c.backendRef.Namespace)) + if ns := c.backendRef.Namespace; ns != "" { + backendRefs[0].Namespace = common.PtrTo(gatewayv1.Namespace(ns)) + } + if port := c.backendRef.port; port != nil { + backendRefs[0].Port = port } i := 0 @@ -312,7 +322,7 @@ func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1 } func (c *converter) buildHTTPRouteBackendReferenceGrant() *gatewayv1beta1.ReferenceGrant { - return c.buildReferenceGrant(common.HTTPRouteGVK, gatewayv1.Kind("Service"), c.backendRef) + return c.buildReferenceGrant(common.HTTPRouteGVK, gatewayv1.Kind("Service"), c.backendRef.NamespacedName) } func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gateway) *gatewayv1beta1.ReferenceGrant { @@ -589,3 +599,21 @@ func toNamespacedName(s string) types.NamespacedName { } return types.NamespacedName{Namespace: parts[0], Name: parts[1]} } + +func toBackendRef(s string) backendRef { + backendRef := backendRef{NamespacedName: types.NamespacedName{}} + if s == "" { + return backendRef + } + parts := strings.SplitN(s, ":", 2) + backendRef.NamespacedName = toNamespacedName(parts[0]) + if len(parts) > 1 { + port, err := strconv.ParseUint(parts[1], 10, 32) + if err != nil { + log.Printf("%s provider: invalid backend: %v", ProviderName, err) + return backendRef + } + backendRef.port = common.PtrTo(gatewayv1.PortNumber(port)) + } + return backendRef +} diff --git a/pkg/i2gw/providers/openapi3/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go index 40dd747f5..a36d00568 100644 --- a/pkg/i2gw/providers/openapi3/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -49,7 +49,7 @@ func TestFileConvertion(t *testing.T) { "openapi3": { "gateway-class-name": "external", "gateway-tls-secret": "gateway-tls-cert", - "backend": "backend-1", + "backend": "backend-1:3000", }, }, }, diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml index 5bea02733..ceb49127e 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/1-petstore3.yaml @@ -54,6 +54,7 @@ spec: value: /api/v3/pet/{petId}/uploadImage backendRefs: - name: backend-1 + port: 3000 - matches: - method: GET path: @@ -89,6 +90,7 @@ spec: value: /api/v3/user/logout backendRefs: - name: backend-1 + port: 3000 - matches: - method: DELETE path: @@ -104,5 +106,6 @@ spec: value: /api/v3/user/{username} backendRefs: - name: backend-1 + port: 3000 status: parents: null diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml index 7322152db..79b5fc73e 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/2-hostnames.yaml @@ -50,6 +50,7 @@ spec: value: /api/v1/resource/{id} backendRefs: - name: backend-1 + port: 3000 status: parents: null --- @@ -80,6 +81,7 @@ spec: value: /v3/status backendRefs: - name: backend-1 + port: 3000 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute @@ -127,5 +129,6 @@ spec: value: /v3/resource/{id} backendRefs: - name: backend-1 + port: 3000 status: parents: null diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml index 32d1ba154..516e70b82 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml +++ b/pkg/i2gw/providers/openapi3/fixtures/output/3-parameters.yaml @@ -40,5 +40,6 @@ spec: type: Exact backendRefs: - name: backend-1 + port: 3000 status: parents: null diff --git a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json index 512de8e27..761f6a6f2 100644 --- a/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json +++ b/pkg/i2gw/providers/openapi3/fixtures/output/4-too-many-rules.json @@ -41,7 +41,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-007" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-008" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -54,7 +54,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-015" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-016" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -67,7 +67,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-023" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-024" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -80,7 +80,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-031" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-032" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -93,7 +93,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-039" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-040" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -106,7 +106,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-047" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-048" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -119,7 +119,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-055" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-056" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -132,7 +132,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-063" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-064" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -145,7 +145,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-071" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-072" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -158,7 +158,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-079" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-080" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -171,7 +171,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-087" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-088" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -184,7 +184,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-095" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-096" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -197,7 +197,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-103" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-104" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -210,7 +210,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-111" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-112" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -223,7 +223,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-119" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-120" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] }, { "matches": [ @@ -236,7 +236,7 @@ { "method": "GET", "path": { "type": "Exact", "value": "/path-127" } }, { "method": "GET", "path": { "type": "Exact", "value": "/path-128" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] } ] }, @@ -262,7 +262,7 @@ "matches": [ { "method": "GET", "path": { "type": "Exact", "value": "/path-129" } } ], - "backendRefs": [{ "name": "backend-1" } ] + "backendRefs": [{ "name": "backend-1", "port": 3000 } ] } ] }, From 74711bc915133d1ed37d78c988814670cd5d4c43 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 21 May 2024 15:04:59 +0200 Subject: [PATCH 22/26] refactor: addressed comments from the pr * provider-specific conf renamed as provided-specific flags * mutex to read/write provider-specific flag definitions wrapped within a type along with the definitions themselves * minor string handling enhancements (concatenation, trim prefix) * additional comments explaining logics and reasoning throughout the code (thread-safety, helper funcs and expressions, etc) --- PROVIDER.md | 14 ++--- cmd/print.go | 32 +++++----- cmd/print_test.go | 45 ++++++------- pkg/i2gw/ingress2gateway.go | 8 +-- pkg/i2gw/provider.go | 63 +++++++++++-------- pkg/i2gw/providers/openapi3/converter.go | 11 +++- pkg/i2gw/providers/openapi3/converter_test.go | 4 +- pkg/i2gw/providers/openapi3/openapi.go | 6 +- pkg/i2gw/providers/openapi3/storage.go | 5 +- 9 files changed, 107 insertions(+), 81 deletions(-) diff --git a/PROVIDER.md b/PROVIDER.md index ea77d8542..1de05c20b 100644 --- a/PROVIDER.md +++ b/PROVIDER.md @@ -167,9 +167,9 @@ There are 2 main things that needs to be tested when creating a feature parser: For example, if one implemented the mirror backend feature and it deletes canary weight from `BackendRefs`, we have a problem. -## Provider-specific user input -To define provider-specific configuration that the user can supply in the `print` command, call the -`i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf)` function in the init function of the +## Provider-specific flags +To define provider-specific flags the user can supply in the `print` command, call the +`i2gw.RegisterProviderSpecificFlag(ProviderName, i2gw.ProviderSpecificFlag)` function in the init function of the provider. E.g.: ```go const Name = "example-gateway-provider" @@ -177,20 +177,20 @@ const Name = "example-gateway-provider" func init() { i2gw.ProviderConstructorByName[Name] = NewProvider - i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + i2gw.RegisterProviderSpecificFlag(ProviderName, i2gw.ProviderSpecificFlag{ Name: "infrastructure-labels", Description: "Comma-separated list of Gateway infrastructure key=value labels", DefaultValue: "", }) } ``` -Users can supply values the provider-specific configuration flag defined above as follows: +Users can provide a value to the flag as follows: ```sh ./ingress2gateway print --providers=example-gateway-provider --example-gateway-provider-infrastructure-labels="app=my-app" ``` -The values all provider-specific flags supplied by the user can be retrieved from the provider `conf`: +The values of all provider-specific flags supplied by the user can be retrieved from the provider `conf`: ```go -if ps := conf.ProviderSpecific[ProviderName]; ps != nil { +if ps := conf.ProviderSpecificFlags[ProviderName]; ps != nil { labels := ps["infrastructure-labels"] } ``` diff --git a/cmd/print.go b/cmd/print.go index 98e2b9bf0..3dc0415bd 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -62,8 +62,8 @@ type PrintRunner struct { // providers indicates which providers are used to execute convert action. providers []string - // Provider specific conf. Value assigned via provider specific flags ---. - providerSpecificConf map[string]*string + // Provider specific flags ---. + providerSpecificFlags map[string]*string } // PrintGatewayAPIObjects performs necessary steps to digest and print @@ -80,7 +80,7 @@ func (pr *PrintRunner) PrintGatewayAPIObjects(cmd *cobra.Command, _ []string) er return fmt.Errorf("failed to initialize namespace filter: %w", err) } - gatewayResources, err := i2gw.ToGatewayAPIResources(cmd.Context(), pr.namespaceFilter, pr.inputFile, pr.providers, pr.getProviderSpecificConf()) + gatewayResources, err := i2gw.ToGatewayAPIResources(cmd.Context(), pr.namespaceFilter, pr.inputFile, pr.providers, pr.getProviderSpecificFlags()) if err != nil { return err } @@ -254,11 +254,11 @@ if specified with --namespace.`) cmd.Flags().StringSliceVar(&pr.providers, "providers", i2gw.GetSupportedProviders(), fmt.Sprintf("If present, the tool will try to convert only resources related to the specified providers, supported values are %v.", i2gw.GetSupportedProviders())) - pr.providerSpecificConf = make(map[string]*string) - for provider, flags := range i2gw.GetProviderSpecificConfDefinitions() { + pr.providerSpecificFlags = make(map[string]*string) + for provider, flags := range i2gw.GetProviderSpecificFlagDefinitions() { for _, flag := range flags { flagName := fmt.Sprintf("%s-%s", provider, flag.Name) - pr.providerSpecificConf[flagName] = cmd.Flags().String(flagName, flag.DefaultValue, fmt.Sprintf("Provider-specific: %s. %s", provider, flag.Description)) + pr.providerSpecificFlags[flagName] = cmd.Flags().String(flagName, flag.DefaultValue, fmt.Sprintf("Provider-specific: %s. %s", provider, flag.Description)) } } @@ -276,18 +276,20 @@ func getNamespaceInCurrentContext() (string, error) { return currentNamespace, err } -func (pr *PrintRunner) getProviderSpecificConf() map[string]map[string]string { - providerSpecificConf := make(map[string]map[string]string) - for flagName, value := range pr.providerSpecificConf { - provider, found := lo.Find(pr.providers, func(p string) bool { return strings.HasPrefix(flagName, p+"-") }) +// getProviderSpecificFlags returns the provider specific flags input by the user. +// The flags are returned in a map where the key is the provider name and the value is a map of flag name to flag value. +func (pr *PrintRunner) getProviderSpecificFlags() map[string]map[string]string { + providerSpecificFlags := make(map[string]map[string]string) + for flagName, value := range pr.providerSpecificFlags { + provider, found := lo.Find(pr.providers, func(p string) bool { return strings.HasPrefix(flagName, fmt.Sprintf("%s-", p)) }) if !found { continue } - conf := strings.SplitN(flagName, provider+"-", 2)[1] - if providerSpecificConf[provider] == nil { - providerSpecificConf[provider] = make(map[string]string) + flagNameWithoutProvider := strings.TrimPrefix(flagName, fmt.Sprintf("%s-", provider)) + if providerSpecificFlags[provider] == nil { + providerSpecificFlags[provider] = make(map[string]string) } - providerSpecificConf[provider][conf] = *value + providerSpecificFlags[provider][flagNameWithoutProvider] = *value } - return providerSpecificConf + return providerSpecificFlags } diff --git a/cmd/print_test.go b/cmd/print_test.go index 60e33427d..fc10d2738 100644 --- a/cmd/print_test.go +++ b/cmd/print_test.go @@ -24,6 +24,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "k8s.io/cli-runtime/pkg/printers" ) @@ -237,32 +238,32 @@ func Test_getNamespaceInCurrentContext(t *testing.T) { } } -func Test_getProviderSpecificConf(t *testing.T) { +func Test_getProviderSpecificFlags(t *testing.T) { value1 := "value1" value2 := "value2" testCases := []struct { - name string - providerSpecificConf map[string]*string - providers []string - expected map[string]map[string]string + name string + providerSpecificFlags map[string]*string + providers []string + expected map[string]map[string]string }{ { - name: "No provider specific configuration", - providerSpecificConf: make(map[string]*string), - providers: []string{"provider"}, - expected: map[string]map[string]string{}, + name: "No provider specific configuration", + providerSpecificFlags: make(map[string]*string), + providers: []string{"provider"}, + expected: map[string]map[string]string{}, }, { - name: "Provider specific configuration matching provider in the list", - providerSpecificConf: map[string]*string{"provider-conf": &value1}, - providers: []string{"provider"}, + name: "Provider specific configuration matching provider in the list", + providerSpecificFlags: map[string]*string{"provider-conf": &value1}, + providers: []string{"provider"}, expected: map[string]map[string]string{ "provider": {"conf": value1}, }, }, { name: "Provider specific configuration matching providers in the list with multiple providers", - providerSpecificConf: map[string]*string{ + providerSpecificFlags: map[string]*string{ "provider-a-conf1": &value1, "provider-b-conf2": &value2, }, @@ -273,22 +274,22 @@ func Test_getProviderSpecificConf(t *testing.T) { }, }, { - name: "Provider specific configuration not matching provider in the list", - providerSpecificConf: map[string]*string{"provider-conf": &value1}, - providers: []string{"provider-a", "provider-b", "provider-c"}, - expected: map[string]map[string]string{}, + name: "Provider specific configuration not matching provider in the list", + providerSpecificFlags: map[string]*string{"provider-conf": &value1}, + providers: []string{"provider-a", "provider-b", "provider-c"}, + expected: map[string]map[string]string{}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { pr := PrintRunner{ - providerSpecificConf: tc.providerSpecificConf, - providers: tc.providers, + providerSpecificFlags: tc.providerSpecificFlags, + providers: tc.providers, } - actual := pr.getProviderSpecificConf() - if !reflect.DeepEqual(actual, tc.expected) { - t.Errorf("getProviderSpecificConf() = %v, expected %v", actual, tc.expected) + actual := pr.getProviderSpecificFlags() + if diff := cmp.Diff(tc.expected, actual); diff != "" { + t.Errorf("Unexpected provider-specific flags, \n want: %+v\n got: %+v\n diff (-want +got):\n%s", tc.expected, actual, diff) } }) } diff --git a/pkg/i2gw/ingress2gateway.go b/pkg/i2gw/ingress2gateway.go index 72c8a51cc..dc4c7cefe 100644 --- a/pkg/i2gw/ingress2gateway.go +++ b/pkg/i2gw/ingress2gateway.go @@ -30,7 +30,7 @@ import ( gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) -func ToGatewayAPIResources(ctx context.Context, namespace string, inputFile string, providers []string, providerSpecificConf map[string]map[string]string) ([]GatewayResources, error) { +func ToGatewayAPIResources(ctx context.Context, namespace string, inputFile string, providers []string, providerSpecificFlags map[string]map[string]string) ([]GatewayResources, error) { var clusterClient client.Client if inputFile == "" { @@ -47,9 +47,9 @@ func ToGatewayAPIResources(ctx context.Context, namespace string, inputFile stri } providerByName, err := constructProviders(&ProviderConf{ - Client: clusterClient, - Namespace: namespace, - ProviderSpecific: providerSpecificConf, + Client: clusterClient, + Namespace: namespace, + ProviderSpecificFlags: providerSpecificFlags, }, providers) if err != nil { return nil, err diff --git a/pkg/i2gw/provider.go b/pkg/i2gw/provider.go index 55e50e435..6723033b4 100644 --- a/pkg/i2gw/provider.go +++ b/pkg/i2gw/provider.go @@ -44,9 +44,9 @@ type ProviderConstructor func(conf *ProviderConf) Provider // ProviderConf contains all the configuration required for every concrete // Provider implementation. type ProviderConf struct { - Client client.Client - Namespace string - ProviderSpecific map[string]map[string]string + Client client.Client + Namespace string + ProviderSpecificFlags map[string]map[string]string } // The Provider interface specifies the required functionality which needs to be @@ -108,34 +108,47 @@ type GatewayResources struct { // modify / create only the required fields of the gateway resources and nothing else. type FeatureParser func([]networkingv1.Ingress, *GatewayResources) field.ErrorList -type ProviderSpecificConf struct { +var providerSpecificFlagDefinitions = providerSpecificFlags{ + flags: make(map[ProviderName]map[string]ProviderSpecificFlag), + mu: sync.RWMutex{}, +} + +type providerSpecificFlags struct { + flags map[ProviderName]map[string]ProviderSpecificFlag + mu sync.RWMutex // thread-safe, so provider-specific flags can be registered concurrently. +} + +type ProviderSpecificFlag struct { Name string Description string DefaultValue string } -var ( - providerSpecificConfs = map[ProviderName]map[string]ProviderSpecificConf{} - providerSpecificConfsMutex = sync.RWMutex{} -) - -// RegisterProviderSpecificConf registers a provider-specific conf. -// Each provider-specific conf is exposed to the user as a command-line flag, defined as optional. -// If the flag is not provided, it is up for the provider to decide to use the default value or raise an error. -// The provider can read the values of provider-specific confs input by the user from the ProviderConf, -// prefixed by the provider name. -func RegisterProviderSpecificConf(provider ProviderName, conf ProviderSpecificConf) { - providerSpecificConfsMutex.Lock() - defer providerSpecificConfsMutex.Unlock() - if providerSpecificConfs[provider] == nil { - providerSpecificConfs[provider] = map[string]ProviderSpecificConf{} +func (f *providerSpecificFlags) add(provider ProviderName, flag ProviderSpecificFlag) { + f.mu.Lock() + defer f.mu.Unlock() + if f.flags[provider] == nil { + f.flags[provider] = map[string]ProviderSpecificFlag{} } - providerSpecificConfs[provider][conf.Name] = conf + f.flags[provider][flag.Name] = flag +} + +func (f *providerSpecificFlags) all() map[ProviderName]map[string]ProviderSpecificFlag { + f.mu.RLock() + defer f.mu.RUnlock() + return f.flags +} + +// RegisterProviderSpecificFlag registers a provider-specific flag. +// Each provider-specific flag is exposed to the user as an optional command-line flag ---. +// If the flag is not provided, it is up to the provider to decide to use the default value or raise an error. +// The provider can read the values of provider-specific flags input by the user from the ProviderConf. +// RegisterProviderSpecificFlag is thread-safe. +func RegisterProviderSpecificFlag(provider ProviderName, flag ProviderSpecificFlag) { + providerSpecificFlagDefinitions.add(provider, flag) } -// GetProviderSpecificConfDefinitions returns the provider specific confs registered by the providers. -func GetProviderSpecificConfDefinitions() map[ProviderName]map[string]ProviderSpecificConf { - providerSpecificConfsMutex.RLock() - defer providerSpecificConfsMutex.RUnlock() - return providerSpecificConfs +// GetProviderSpecificFlagDefinitions returns the provider specific confs registered by the providers. +func GetProviderSpecificFlagDefinitions() map[ProviderName]map[string]ProviderSpecificFlag { + return providerSpecificFlagDefinitions.all() } diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index a52995614..2ff809ec8 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -48,7 +48,14 @@ const ( HTTPRouteMatchesMaxMax = HTTPRouteRulesMax * HTTPRouteMatchesMax ) -var uriRegexp = regexp.MustCompile(`^((https?)://([^/]+))?(/.*)?$`) // [_][1] = scheme, [_][2] = host, [_][3] = path +// uriRegexp allows parsing HTTP URIs where, for each string submatch, the following values are returned +// respectivelly to each index position in the slice: +// 0: full match +// 1: full match without the path +// 2: http scheme +// 3: host name +// 4: path +var uriRegexp = regexp.MustCompile(`^((https?)://([^/]+))?(/.*)?$`) type Converter interface { Convert(Storage) (i2gw.GatewayResources, field.ErrorList) @@ -62,7 +69,7 @@ func NewConverter(conf *i2gw.ProviderConf) Converter { backendRef: toBackendRef(""), } - if ps := conf.ProviderSpecific[ProviderName]; ps != nil { + if ps := conf.ProviderSpecificFlags[ProviderName]; ps != nil { converter.gatewayClassName = ps[GatewayClassFlag] converter.tlsSecretRef = toNamespacedName(ps[TlsSecretFlag]) converter.backendRef = toBackendRef(ps[BackendFlag]) diff --git a/pkg/i2gw/providers/openapi3/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go index a36d00568..d58333a79 100644 --- a/pkg/i2gw/providers/openapi3/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -45,7 +45,7 @@ func TestFileConvertion(t *testing.T) { providerConf := map[string]*i2gw.ProviderConf{ "default": &i2gw.ProviderConf{ - ProviderSpecific: map[string]map[string]string{ + ProviderSpecificFlags: map[string]map[string]string{ "openapi3": { "gateway-class-name": "external", "gateway-tls-secret": "gateway-tls-cert", @@ -55,7 +55,7 @@ func TestFileConvertion(t *testing.T) { }, "reference-grants.yaml": &i2gw.ProviderConf{ Namespace: "networking", - ProviderSpecific: map[string]map[string]string{ + ProviderSpecificFlags: map[string]map[string]string{ "openapi3": { "gateway-class-name": "external", "gateway-tls-secret": "secrets/gateway-tls-cert", diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index 2d8ed842d..d8800cdba 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -39,17 +39,17 @@ const ( func init() { i2gw.ProviderConstructorByName[ProviderName] = NewProvider - i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + i2gw.RegisterProviderSpecificFlag(ProviderName, i2gw.ProviderSpecificFlag{ Name: BackendFlag, Description: "The name of the backend service to use in the HTTPRoutes", }) - i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + i2gw.RegisterProviderSpecificFlag(ProviderName, i2gw.ProviderSpecificFlag{ Name: GatewayClassFlag, Description: "The name of the gateway class to use in the Gateways", }) - i2gw.RegisterProviderSpecificConf(ProviderName, i2gw.ProviderSpecificConf{ + i2gw.RegisterProviderSpecificFlag(ProviderName, i2gw.ProviderSpecificFlag{ Name: TlsSecretFlag, Description: "The name of the secret for the TLS certificate references in the Gateways", }) diff --git a/pkg/i2gw/providers/openapi3/storage.go b/pkg/i2gw/providers/openapi3/storage.go index 624208711..526f4c7f0 100644 --- a/pkg/i2gw/providers/openapi3/storage.go +++ b/pkg/i2gw/providers/openapi3/storage.go @@ -34,13 +34,15 @@ func NewResourceStorage() Storage { } type storage struct { - mu sync.RWMutex + mu sync.RWMutex // thread-safe, so we can read and write to the storage concurrently resources []*openapi3.T } var _ Storage = &storage{} +// AddResource adds a new OpenAPI spec to the storage. +// AddResource is thread-safe and therefore can be called for multiple resources concurrently. func (s *storage) AddResource(resource *openapi3.T) { s.mu.Lock() defer s.mu.Unlock() @@ -48,6 +50,7 @@ func (s *storage) AddResource(resource *openapi3.T) { s.resources = append(s.resources, resource) } +// GetResources returns all OpenAPI specs stored in the storage. func (s *storage) GetResources() []*openapi3.T { s.mu.RLock() defer s.mu.RUnlock() From febc49a35012abd667997502f980db497e79fc1c Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 21 May 2024 15:52:24 +0200 Subject: [PATCH 23/26] log message in case of provider-specific flag supplied without a matching provider --- cmd/print.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/print.go b/cmd/print.go index 3dc0415bd..ec0a96ca8 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "log" "os" "strings" @@ -283,6 +284,7 @@ func (pr *PrintRunner) getProviderSpecificFlags() map[string]map[string]string { for flagName, value := range pr.providerSpecificFlags { provider, found := lo.Find(pr.providers, func(p string) bool { return strings.HasPrefix(flagName, fmt.Sprintf("%s-", p)) }) if !found { + log.Printf("Warning: Ignoring flag %s as it does not match any of the providers", flagName) continue } flagNameWithoutProvider := strings.TrimPrefix(flagName, fmt.Sprintf("%s-", provider)) From d9a40abecacbc4ccb65380ab1cf3342a9f957671 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Tue, 21 May 2024 16:59:08 +0200 Subject: [PATCH 24/26] more comments to explain the flow and decision of the converter --- pkg/i2gw/providers/openapi3/converter.go | 73 +++++++++++++++++++++--- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 2ff809ec8..5bd937475 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -50,11 +50,12 @@ const ( // uriRegexp allows parsing HTTP URIs where, for each string submatch, the following values are returned // respectivelly to each index position in the slice: -// 0: full match -// 1: full match without the path -// 2: http scheme -// 3: host name -// 4: path +// +// 0: full match +// 1: full match without the path +// 2: http scheme +// 3: host name +// 4: path var uriRegexp = regexp.MustCompile(`^((https?)://([^/]+))?(/.*)?$`) type Converter interface { @@ -103,6 +104,9 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error resourcesNamePrefixes := make(map[string]int) for _, spec := range storage.GetResources() { + // prefixes all resource names with the title of the spec to avoid conflicts between resources from different specs + // in case of multiple specs with the same title, a counter, starting at 1, is appended to the prefix from the 2nd + // spec and onwards resourcesNamePrefix := toResourcesNamePrefix(spec) if _, exists := resourcesNamePrefixes[resourcesNamePrefix]; !exists { resourcesNamePrefixes[resourcesNamePrefix] = 0 @@ -112,10 +116,13 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error resourcesNamePrefix = fmt.Sprintf("%s-%d", resourcesNamePrefix, resourcesNamePrefixes[resourcesNamePrefix]+1) } + // convert the spec to Gateway API resources httpRoutes, gateways := c.toHTTPRoutesAndGateways(spec, resourcesNamePrefix, errors) for _, httpRoute := range httpRoutes { gatewayResources.HTTPRoutes[types.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}] = httpRoute } + + // build reference grants for the resources if referenceGrant := c.buildHTTPRouteBackendReferenceGrant(); referenceGrant != nil { gatewayResources.ReferenceGrants[types.NamespacedName{Name: referenceGrant.GetName(), Namespace: referenceGrant.GetNamespace()}] = *referenceGrant } @@ -130,6 +137,7 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error return gatewayResources, errors } +// toHTTPRoutesAndGateways converts an OpenAPI Specification 3.x to Gateway API HTTPRoutes and Gateways. func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefix string, errors field.ErrorList) ([]gatewayv1.HTTPRoute, []gatewayv1.Gateway) { var matchers []httpRouteMatcher @@ -138,18 +146,22 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi servers = openapi3.Servers{{URL: "/"}} } + // get a list of http matchers for all path items in the spec. + // servers are expanded to account for all enum variables paths := spec.Paths.Map() for _, relativePath := range spec.Paths.InMatchingOrder() { pathItem := paths[relativePath] matchers = append(matchers, pathItemToHTTPMatchers(pathItem, relativePath, servers, errors)...) } + // group each expected listener (given by the hostnames) by the sets of http matchers related to the listener listenersByHTTPRouteRuleMatcher := make(map[httpRouteRuleMatcher][]string) for _, matcher := range matchers { listener := fmt.Sprintf("%s://%s", matcher.protocol, matcher.host) listenersByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher] = append(listenersByHTTPRouteRuleMatcher[matcher.httpRouteRuleMatcher], listener) } + // invert the grouping into a map of listener groups as keys and their corresponding common http matchers as values var listenerGroups []string httpRouteRuleMatchersByListeners := make(map[string]httpRouteRuleMatchers) for matcher, listeners := range listenersByHTTPRouteRuleMatcher { @@ -163,6 +175,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi // sort listener groups for deterministic output sort.Strings(listenerGroups) + // build the gateway object gatewayName := fmt.Sprintf("%s-gateway", resourcesNamePrefix) gateway := gatewayv1.Gateway{ ObjectMeta: metav1.ObjectMeta{ @@ -177,6 +190,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi gateway.SetNamespace(c.namespace) } + // declare unique listeners in the gateway for each hostname in the listener groups uniqueListeners := make(map[string]struct{}) for _, group := range listenerGroups { listeners := lo.Filter(strings.Split(group, HostSeparator), func(listener string, _ int) bool { @@ -191,6 +205,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi var routes []gatewayv1.HTTPRoute + // build the unique backend reference to be used in all route rules backendRefs := []gatewayv1.HTTPBackendRef{ gatewayv1.HTTPBackendRef{ BackendRef: gatewayv1.BackendRef{ @@ -207,6 +222,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi backendRefs[0].Port = port } + // build the HTTPRoutes respectively to the listener groups i := 0 for _, group := range listenerGroups { listeners := strings.Split(group, HostSeparator) @@ -223,23 +239,26 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi sort.Strings(hosts) hosts = slices.Compact(hosts) + // split the matchers into nRoutes HTTPRoutes, each with a maximum of HTTPRouteMatchesMaxMax matchers nMatchers := len(matchers) nRoutes := nMatchers / HTTPRouteMatchesMaxMax if nMatchers%HTTPRouteMatchesMaxMax != 0 { nRoutes++ } for j := 0; j < nRoutes; j++ { + // generate a unique name for the route object routeName := fmt.Sprintf("%s-route", resourcesNamePrefix) if len(listenerGroups) > 1 { - routeName = fmt.Sprintf("%s-%d", routeName, i+1) + routeName = fmt.Sprintf("%s-%d", routeName, i+1) // appends a grouping counter to the route name, starting at 1, if there are multiple listener groups, to avoid conflicts } if nRoutes > 1 { - routeName = fmt.Sprintf("%s-%d", routeName, j+1) + routeName = fmt.Sprintf("%s-%d", routeName, j+1) // appends a counter to the route name, starting at 1, if there are more multiple routes, to avoid conflicts } last := (j + 1) * HTTPRouteMatchesMaxMax if last > nMatchers { last = nMatchers } + // build the route object for the given slice of route matchers routes = append(routes, c.toHTTPRoute(routeName, gatewayName, listenerName, hosts, matchers[j*HTTPRouteMatchesMaxMax:last], backendRefs)) } i++ @@ -248,6 +267,10 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi return routes, []gatewayv1.Gateway{gateway} } +// toListener converts a http scheme (protocol) and host string to a Gateway API Listener. +// The listener name is derived from the protocol and hostname. +// The listener port is assumed 80 for http protocol and 443 for https. +// If the protocol is https, the listener TLS configuration is set from the general TLS secret reference. func (c *converter) toListener(protocolAndHostname string, _ int) gatewayv1.Listener { name, protocol, hostname := toListenerName(protocolAndHostname) @@ -278,6 +301,9 @@ func (c *converter) toListener(protocolAndHostname string, _ int) gatewayv1.List return listener } +// toListenerName extract a listener name, protocol and hostname from a protocol (http scheme) and hostname string. +// If the protocol is not provided, "http" is assumed by default. +// If the hostname is not provided, "*" is assumed by default. func toListenerName(protocolAndHostname string) (listenerName gatewayv1.SectionName, protocol string, hostname string) { protocol = "http" hostname = HostWildcard @@ -299,6 +325,9 @@ func toListenerName(protocolAndHostname string) (listenerName gatewayv1.SectionN return gatewayv1.SectionName(listenerNamePrefix + protocol), protocol, hostname } +// toHTTPRoute builds a Gateway API HTTPRoute object with a given name, for a given gateway parent, set of hostnames, +// and HTTP route matchers out of which HTTPRouteMatches are built for the rules. +// All HTTPRouteRules in the HTTPRoute are built with the same set of backendRefs, provided as argument. func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1.SectionName, hostnames []string, matchers httpRouteRuleMatchers, backendRefs []gatewayv1.HTTPBackendRef) gatewayv1.HTTPRoute { parentRef := gatewayv1.ParentReference{Name: gatewayv1.ObjectName(gatewayName)} if listenerName != "" { @@ -328,10 +357,14 @@ func (c *converter) toHTTPRoute(name, gatewayName string, listenerName gatewayv1 return route } +// buildHTTPRouteBackendReferenceGrant builds a Gateway API ReferenceGrant object for the general backend reference +// to be used in all HTTPRoute rules. func (c *converter) buildHTTPRouteBackendReferenceGrant() *gatewayv1beta1.ReferenceGrant { return c.buildReferenceGrant(common.HTTPRouteGVK, gatewayv1.Kind("Service"), c.backendRef.NamespacedName) } +// buildGatewayTlsSecretReferenceGrant builds a Gateway API ReferenceGrant object for the general TLS secret +// reference to be used in all https gateway listeners. func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gateway) *gatewayv1beta1.ReferenceGrant { if slices.IndexFunc(gateway.Spec.Listeners, func(listener gatewayv1.Listener) bool { return listener.TLS != nil }) == -1 { return nil @@ -339,6 +372,8 @@ func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gatewa return c.buildReferenceGrant(common.GatewayGVK, gatewayv1.Kind("Secret"), c.tlsSecretRef) } +// buildReferenceGrant builds a Gateway API ReferenceGrant object for a given source and destination resource. +// The name of the reference grant is derived from the source resource namespace and the destination resource kind and name. func (c *converter) buildReferenceGrant(fromGVK schema.GroupVersionKind, toKind gatewayv1.Kind, toRef types.NamespacedName) *gatewayv1beta1.ReferenceGrant { if c.namespace == "" || toRef.Namespace == "" { return nil @@ -368,6 +403,7 @@ func (c *converter) buildReferenceGrant(fromGVK schema.GroupVersionKind, toKind return rg } +// httpRouteRuleMatcher is abstraction from which to build Gateway API HTTPRouteRules. type httpRouteRuleMatcher struct { path string method string @@ -386,14 +422,19 @@ func (m httpRouteRuleMatchers) Less(i, j int) bool { return m[i].method < m[j].method } +// httpRouteMatcher is an abstraction used to associate a http route match to a hostname and protocol that +// will be used to build gateway listeners and references from the routes. type httpRouteMatcher struct { protocol string host string httpRouteRuleMatcher } +// toHTTPRouteRules builds Gateway API HTTPRouteRules from a list of httpRouteRuleMatchers and fixed backendRefs. func toHTTPRouteRules(matchers httpRouteRuleMatchers, backendRefs []gatewayv1.HTTPBackendRef) []gatewayv1.HTTPRouteRule { var rules []gatewayv1.HTTPRouteRule + + // split the matchers into nRules HTTPRouteRules, each with a maximum of HTTPRouteMatchesMax matchers nMatches := len(matchers) nRules := nMatches / HTTPRouteMatchesMax if len(matchers)%HTTPRouteMatchesMax != 0 { @@ -437,6 +478,8 @@ func toHTTPRouteRules(matchers httpRouteRuleMatchers, backendRefs []gatewayv1.HT return rules } +// pathItemToHTTPMatchers converts an OpenAPI Specification 3.x PathItem to a list of httpRouteMatchers. +// The servers are expanded to account for all enum variables. func pathItemToHTTPMatchers(pathItem *openapi3.PathItem, relativePath string, servers openapi3.Servers, errors field.ErrorList) []httpRouteMatcher { var matchers []httpRouteMatcher @@ -456,6 +499,7 @@ func pathItemToHTTPMatchers(pathItem *openapi3.PathItem, relativePath string, se "TRACE": pathItem.Trace, } + // build httpRouteMatchers for each operation of the path item for method, operation := range operations { if operation == nil { continue @@ -466,6 +510,8 @@ func pathItemToHTTPMatchers(pathItem *openapi3.PathItem, relativePath string, se return matchers } +// pathItemToHTTPMatchers converts an OpenAPI Specification 3.x Operation (http method + relative path) to a list of +// httpRouteMatchers. The servers are expanded to account for all enum variables. func operationToHTTPMatchers(operation *openapi3.Operation, relativePath string, method string, parameters openapi3.Parameters, servers openapi3.Servers, errors field.ErrorList) []httpRouteMatcher { if operation.Servers != nil && len(*operation.Servers) > 0 { servers = *operation.Servers @@ -480,9 +526,11 @@ func operationToHTTPMatchers(operation *openapi3.Operation, relativePath string, expandedServers = append(expandedServers, expandServerVariables(*server)...) } + // build httpRouteMatchers for each expanded server return lo.Map(expandedServers, toHTTPMatcher(relativePath, method, parameters, errors)) } +// toHTTPMatcher converts a HTTP method and relative path to a httpRouteMatcher. func toHTTPMatcher(relativePath string, method string, parameters openapi3.Parameters, errors field.ErrorList) func(server openapi3.Server, _ int) httpRouteMatcher { paramNameFunc := func(in string) func(p *openapi3.ParameterRef, _ int) (string, bool) { return func(p *openapi3.ParameterRef, _ int) (string, bool) { @@ -520,6 +568,9 @@ func toHTTPMatcher(relativePath string, method string, parameters openapi3.Param } } +// expandNonEnumServerVariables expands all non-enum variables in an OpenAPI Specification 3.x Server. +// Each variable is replaced by its default value. +// Values other than the default for non-enum variables are not supported. func expandNonEnumServerVariables(server openapi3.Server) openapi3.Server { if len(server.Variables) == 0 { return server @@ -540,6 +591,7 @@ func expandNonEnumServerVariables(server openapi3.Server) openapi3.Server { } } +// expandServerVariables expands an OpenAPI Specification 3.x Server into N servers with all enum variables resolved. func expandServerVariables(server openapi3.Server) []openapi3.Server { servers := []openapi3.Server{expandNonEnumServerVariables(server)} for { @@ -580,6 +632,8 @@ func expandServerVariables(server openapi3.Server) []openapi3.Server { return servers } +// uriToHostname converts a URI string to a hostname. +// If the URI does not contain a hostname, "*" is returned. func uriToHostname(uri string, _ int) string { host := HostWildcard if s := uriRegexp.FindAllStringSubmatch(uri, 1); len(s) > 0 && s[0][3] != "" { @@ -588,14 +642,17 @@ func uriToHostname(uri string, _ int) string { return host } +// toGatewayAPIHostname converts a hostname string to a Gateway API Hostname. func toGatewayAPIHostname(hostname string, _ int) gatewayv1.Hostname { return gatewayv1.Hostname(hostname) } +// toResourcesNamePrefix returns a base common prefix for the names of ther esources, from the title of a spec. func toResourcesNamePrefix(spec *openapi3.T) string { return strings.ToLower(common.NameFromHost(spec.Info.Title)) } +// toNamespacedName converts a string in the format "namespace/name" to a types.NamespacedName object. func toNamespacedName(s string) types.NamespacedName { if s == "" { return types.NamespacedName{} @@ -607,6 +664,8 @@ func toNamespacedName(s string) types.NamespacedName { return types.NamespacedName{Namespace: parts[0], Name: parts[1]} } +// toBackendRef converts a backend reference string to a backendRef object, including namespaced reference to the +// Backend and port number if available. func toBackendRef(s string) backendRef { backendRef := backendRef{NamespacedName: types.NamespacedName{}} if s == "" { From 48dc906c393c0c0797e6c49d8f4fa00ce0cf394b Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Wed, 29 May 2024 11:31:25 +0200 Subject: [PATCH 25/26] lint: typos, gofmt and false positives --- pkg/i2gw/provider.go | 2 +- pkg/i2gw/providers/openapi3/converter.go | 26 +++++++++---------- pkg/i2gw/providers/openapi3/converter_test.go | 1 + pkg/i2gw/providers/openapi3/openapi.go | 4 +-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/pkg/i2gw/provider.go b/pkg/i2gw/provider.go index 6723033b4..c71419985 100644 --- a/pkg/i2gw/provider.go +++ b/pkg/i2gw/provider.go @@ -110,7 +110,7 @@ type FeatureParser func([]networkingv1.Ingress, *GatewayResources) field.ErrorLi var providerSpecificFlagDefinitions = providerSpecificFlags{ flags: make(map[ProviderName]map[string]ProviderSpecificFlag), - mu: sync.RWMutex{}, + mu: sync.RWMutex{}, } type providerSpecificFlags struct { diff --git a/pkg/i2gw/providers/openapi3/converter.go b/pkg/i2gw/providers/openapi3/converter.go index 5bd937475..1b559c440 100644 --- a/pkg/i2gw/providers/openapi3/converter.go +++ b/pkg/i2gw/providers/openapi3/converter.go @@ -49,7 +49,7 @@ const ( ) // uriRegexp allows parsing HTTP URIs where, for each string submatch, the following values are returned -// respectivelly to each index position in the slice: +// respectively to each index position in the slice: // // 0: full match // 1: full match without the path @@ -72,7 +72,7 @@ func NewConverter(conf *i2gw.ProviderConf) Converter { if ps := conf.ProviderSpecificFlags[ProviderName]; ps != nil { converter.gatewayClassName = ps[GatewayClassFlag] - converter.tlsSecretRef = toNamespacedName(ps[TlsSecretFlag]) + converter.tlsSecretRef = toNamespacedName(ps[TLSSecretFlag]) converter.backendRef = toBackendRef(ps[BackendFlag]) } @@ -128,7 +128,7 @@ func (c *converter) Convert(storage Storage) (i2gw.GatewayResources, field.Error } for _, gateway := range gateways { gatewayResources.Gateways[types.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()}] = gateway - if referenceGrant := c.buildGatewayTlsSecretReferenceGrant(gateway); referenceGrant != nil { + if referenceGrant := c.buildGatewayTLSSecretReferenceGrant(gateway); referenceGrant != nil { gatewayResources.ReferenceGrants[types.NamespacedName{Name: referenceGrant.GetName(), Namespace: referenceGrant.GetNamespace()}] = *referenceGrant } } @@ -207,7 +207,7 @@ func (c *converter) toHTTPRoutesAndGateways(spec *openapi3.T, resourcesNamePrefi // build the unique backend reference to be used in all route rules backendRefs := []gatewayv1.HTTPBackendRef{ - gatewayv1.HTTPBackendRef{ + gatewayv1.HTTPBackendRef{ //nolint:gofmt BackendRef: gatewayv1.BackendRef{ BackendObjectReference: gatewayv1.BackendObjectReference{ Name: gatewayv1.ObjectName(c.backendRef.Name), @@ -363,9 +363,9 @@ func (c *converter) buildHTTPRouteBackendReferenceGrant() *gatewayv1beta1.Refere return c.buildReferenceGrant(common.HTTPRouteGVK, gatewayv1.Kind("Service"), c.backendRef.NamespacedName) } -// buildGatewayTlsSecretReferenceGrant builds a Gateway API ReferenceGrant object for the general TLS secret +// buildGatewayTLSSecretReferenceGrant builds a Gateway API ReferenceGrant object for the general TLS secret // reference to be used in all https gateway listeners. -func (c *converter) buildGatewayTlsSecretReferenceGrant(gateway gatewayv1.Gateway) *gatewayv1beta1.ReferenceGrant { +func (c *converter) buildGatewayTLSSecretReferenceGrant(gateway gatewayv1.Gateway) *gatewayv1beta1.ReferenceGrant { if slices.IndexFunc(gateway.Spec.Listeners, func(listener gatewayv1.Listener) bool { return listener.TLS != nil }) == -1 { return nil } @@ -647,7 +647,7 @@ func toGatewayAPIHostname(hostname string, _ int) gatewayv1.Hostname { return gatewayv1.Hostname(hostname) } -// toResourcesNamePrefix returns a base common prefix for the names of ther esources, from the title of a spec. +// toResourcesNamePrefix returns a base common prefix for the names of the resources, from the title of a spec. func toResourcesNamePrefix(spec *openapi3.T) string { return strings.ToLower(common.NameFromHost(spec.Info.Title)) } @@ -667,19 +667,19 @@ func toNamespacedName(s string) types.NamespacedName { // toBackendRef converts a backend reference string to a backendRef object, including namespaced reference to the // Backend and port number if available. func toBackendRef(s string) backendRef { - backendRef := backendRef{NamespacedName: types.NamespacedName{}} + ref := backendRef{NamespacedName: types.NamespacedName{}} if s == "" { - return backendRef + return ref } parts := strings.SplitN(s, ":", 2) - backendRef.NamespacedName = toNamespacedName(parts[0]) + ref.NamespacedName = toNamespacedName(parts[0]) if len(parts) > 1 { port, err := strconv.ParseUint(parts[1], 10, 32) if err != nil { log.Printf("%s provider: invalid backend: %v", ProviderName, err) - return backendRef + return ref } - backendRef.port = common.PtrTo(gatewayv1.PortNumber(port)) + ref.port = common.PtrTo(gatewayv1.PortNumber(port)) } - return backendRef + return ref } diff --git a/pkg/i2gw/providers/openapi3/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go index d58333a79..69bcb18bf 100644 --- a/pkg/i2gw/providers/openapi3/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -43,6 +43,7 @@ const fixturesDir = "./fixtures" func TestFileConvertion(t *testing.T) { ctx := context.Background() + //nolint:gofmt providerConf := map[string]*i2gw.ProviderConf{ "default": &i2gw.ProviderConf{ ProviderSpecificFlags: map[string]map[string]string{ diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index d8800cdba..4e5f35927 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -33,7 +33,7 @@ const ( BackendFlag = "backend" GatewayClassFlag = "gateway-class-name" - TlsSecretFlag = "gateway-tls-secret" + TLSSecretFlag = "gateway-tls-secret" //nolint:gosec ) func init() { @@ -50,7 +50,7 @@ func init() { }) i2gw.RegisterProviderSpecificFlag(ProviderName, i2gw.ProviderSpecificFlag{ - Name: TlsSecretFlag, + Name: TLSSecretFlag, Description: "The name of the secret for the TLS certificate references in the Gateways", }) } From b174c23f9761c8dcabba5a8a9465751a9effe119 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Wed, 29 May 2024 11:36:50 +0200 Subject: [PATCH 26/26] return error in case of invalid OpenAPI 3.x spec --- pkg/i2gw/providers/openapi3/converter_test.go | 64 ++++++++++++++----- pkg/i2gw/providers/openapi3/openapi.go | 4 +- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/pkg/i2gw/providers/openapi3/converter_test.go b/pkg/i2gw/providers/openapi3/converter_test.go index 69bcb18bf..d994b8f7d 100644 --- a/pkg/i2gw/providers/openapi3/converter_test.go +++ b/pkg/i2gw/providers/openapi3/converter_test.go @@ -24,6 +24,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "testing" apiequality "k8s.io/apimachinery/pkg/api/equality" @@ -43,9 +44,13 @@ const fixturesDir = "./fixtures" func TestFileConvertion(t *testing.T) { ctx := context.Background() - //nolint:gofmt - providerConf := map[string]*i2gw.ProviderConf{ - "default": &i2gw.ProviderConf{ + type testData struct { + providerConf *i2gw.ProviderConf + expectedReadFileError error + } + + defaultTestData := testData{ + providerConf: &i2gw.ProviderConf{ ProviderSpecificFlags: map[string]map[string]string{ "openapi3": { "gateway-class-name": "external", @@ -54,16 +59,24 @@ func TestFileConvertion(t *testing.T) { }, }, }, - "reference-grants.yaml": &i2gw.ProviderConf{ - Namespace: "networking", - ProviderSpecificFlags: map[string]map[string]string{ - "openapi3": { - "gateway-class-name": "external", - "gateway-tls-secret": "secrets/gateway-tls-cert", - "backend": "apps/backend-1", + } + + customTestData := map[string]testData{ + "reference-grants.yaml": { + providerConf: &i2gw.ProviderConf{ + Namespace: "networking", + ProviderSpecificFlags: map[string]map[string]string{ + "openapi3": { + "gateway-class-name": "external", + "gateway-tls-secret": "secrets/gateway-tls-cert", + "backend": "apps/backend-1", + }, }, }, }, + "invalid-spec.yaml": { + expectedReadFileError: fmt.Errorf("failed to read resources from file: invalid OpenAPI 3.x spec"), + }, } filepath.WalkDir(filepath.Join(fixturesDir, "input"), func(path string, d fs.DirEntry, err error) error { @@ -74,15 +87,32 @@ func TestFileConvertion(t *testing.T) { return nil } - conf, ok := providerConf[regexp.MustCompile(`\d+-(.+\.(json|yaml))$`).FindAllStringSubmatch(d.Name(), -1)[0][1]] - if !ok { - conf = providerConf["default"] + providerConf := defaultTestData.providerConf + expectedReadFileError := defaultTestData.expectedReadFileError + + inputFileName := regexp.MustCompile(`\d+-(.+\.(json|yaml))$`).FindAllStringSubmatch(d.Name(), -1)[0][1] + data, ok := customTestData[inputFileName] + if ok { + if data.providerConf != nil { + providerConf = data.providerConf + } + if data.expectedReadFileError != nil { + expectedReadFileError = data.expectedReadFileError + } } - provider := NewProvider(conf) - err = provider.ReadResourcesFromFile(ctx, path) - if err != nil { - t.Fatalf("Failed to read input from file %v: %v", d.Name(), err.Error()) + provider := NewProvider(providerConf) + + if readFileErr := provider.ReadResourcesFromFile(ctx, path); readFileErr != nil { + if expectedReadFileError == nil { + t.Fatalf("unexpected error during reading test file %v: %v", d.Name(), readFileErr.Error()) + } else if !strings.Contains(readFileErr.Error(), expectedReadFileError.Error()) { + t.Fatalf("unexpected error during reading test file %v: '%v' does not contain expected '%v'", d.Name(), readFileErr.Error(), expectedReadFileError.Error()) + } else { + return nil // success + } + } else if expectedReadFileError != nil { + t.Fatalf("missing expected error during reading test file %v: %v", d.Name(), expectedReadFileError.Error()) } gotGatewayResources, errList := provider.ToGatewayAPI() diff --git a/pkg/i2gw/providers/openapi3/openapi.go b/pkg/i2gw/providers/openapi3/openapi.go index 4e5f35927..bb8da559c 100644 --- a/pkg/i2gw/providers/openapi3/openapi.go +++ b/pkg/i2gw/providers/openapi3/openapi.go @@ -19,7 +19,6 @@ package openapi3 import ( "context" "fmt" - "log" "github.com/getkin/kin-openapi/openapi3" "k8s.io/apimachinery/pkg/util/validation/field" @@ -103,8 +102,7 @@ func readSpecFromFile(ctx context.Context, filename string) (*openapi3.T, error) } if err := spec.Validate(ctx); err != nil { - log.Printf("%s provider: invalid OpenAPI 3.x spec: %v", ProviderName, err) - return nil, nil + return nil, fmt.Errorf("invalid OpenAPI 3.x spec: %w", err) } return spec, nil