From 3c66c66e25a2c5356c574ee13009b10dcc815f62 Mon Sep 17 00:00:00 2001 From: Prashansa Kulshrestha Date: Wed, 14 Aug 2024 16:57:24 +0530 Subject: [PATCH 1/3] chore: replaced kin-openapi library with libopenapi deck file openapi2kong command uses kin-openapi for all its OpenAPI requirements. This library does not support OpenAPI 3.1, which is a requirement from the users. Thus, this change updates the library to libopenapi which can help us to adopt OpenAPI 3.1 For APIOps #1324 https://github.com/Kong/deck/issues/1324 --- go.mod | 14 +- go.sum | 58 ++-- namespace/namespace_host.go | 3 + openapi2kong/jsonschema.go | 75 +++-- openapi2kong/openapi2kong.go | 521 +++++++++++++++++------------------ openapi2kong/service.go | 22 +- openapi2kong/service_test.go | 47 ++-- openapi2kong/utils.go | 105 +++++++ openapi2kong/validator.go | 69 +++-- patch/deckpatch.go | 1 + yamlbasics/selectors.go | 1 + 11 files changed, 530 insertions(+), 386 deletions(-) create mode 100644 openapi2kong/utils.go diff --git a/go.mod b/go.mod index f3ff934..2efc927 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/kong/go-apiops go 1.21 require ( - github.com/getkin/kin-openapi v0.108.0 github.com/go-logr/logr v1.4.2 github.com/go-logr/stdr v1.2.2 github.com/google/go-cmp v0.6.0 @@ -11,6 +10,7 @@ require ( github.com/kong/go-slugify v1.0.0 github.com/onsi/ginkgo/v2 v2.20.0 github.com/onsi/gomega v1.34.1 + github.com/pb33f/libopenapi v0.16.13 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/vmware-labs/yaml-jsonpath v0.3.2 @@ -21,25 +21,21 @@ require ( ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/swag v0.19.5 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/yaml v0.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mozillazg/go-unidecode v0.2.0 // indirect - github.com/onsi/ginkgo v1.16.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.24.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index a4a9b69..2fa91e1 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,25 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 h1:aRd8M7HJVZOqn/vhOzrGcQH0lNAMkqMn+pXUYkatmcA= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/getkin/kin-openapi v0.108.0 h1:EYf0GtsKa4hQNIlplGS+Au7NEfGQ1F7MoHD2kcVevPQ= -github.com/getkin/kin-openapi v0.108.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -28,23 +30,24 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 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.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= -github.com/invopop/yaml v0.1.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/kong/go-slugify v1.0.0 h1:vCFAyf2sdoSlBtLcrmDWUFn0ohlpKiKvQfXZkO5vSKY= github.com/kong/go-slugify v1.0.0/go.mod h1:dbR2h3J2QKXQ1k0aww6cN7o4cIcwlWflr6RKRdcoaiw= @@ -53,12 +56,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/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/mozillazg/go-unidecode v0.2.0 h1:vFGEzAH9KSwyWmXCOblazEWDh7fOkpmy/Z4ArmamSUc= github.com/mozillazg/go-unidecode v0.2.0/go.mod h1:zB48+/Z5toiRolOZy9ksLryJ976VIwmDmpQ2quyt1aA= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -69,13 +68,18 @@ github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pb33f/libopenapi v0.16.13 h1:uR/W3Rit/yxRWG5DWal26PdEnEq4mu/3cYjbkK6LHm0= +github.com/pb33f/libopenapi v0.16.13/go.mod h1:8/lZGTZmxybpTPOggS6LefdrYvsQ5kbirD364TceyQo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -86,18 +90,14 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= @@ -112,6 +112,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -123,15 +125,24 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -149,10 +160,11 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -162,11 +174,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/namespace/namespace_host.go b/namespace/namespace_host.go index b4e913d..9d93c25 100644 --- a/namespace/namespace_host.go +++ b/namespace/namespace_host.go @@ -15,6 +15,7 @@ func ApplyNamespaceHost( deckfile *yaml.Node, // the deckFile to operate on selectors yamlbasics.SelectorSet, // the selectors to use to select the routes hosts []string, // the hosts to add to the routes + //nolint:predeclared clear bool, // if true, clear the hosts field before adding the hosts allowEmptySelection bool, // if true, do not return an error if no routes are selected ) error { @@ -54,6 +55,8 @@ func ApplyNamespaceHost( // updateRouteHosts updates the hosts field of the provided routes. // If clear is true, the hosts field is cleared before adding the hosts. +// +//nolint:predeclared func updateRouteHosts(routes yamlbasics.NodeSet, hosts []string, clear bool) error { for _, route := range routes { if err := yamlbasics.CheckType(route, yamlbasics.TypeObject); err != nil { diff --git a/openapi2kong/jsonschema.go b/openapi2kong/jsonschema.go index 997e9a6..d0f28c6 100644 --- a/openapi2kong/jsonschema.go +++ b/openapi2kong/jsonschema.go @@ -4,53 +4,79 @@ import ( "encoding/json" "strings" - "github.com/getkin/kin-openapi/openapi3" + "github.com/pb33f/libopenapi/datamodel/high/base" ) // dereferenceSchema walks the schema and adds every subschema to the seenBefore map. // This is safe to recursive schemas. -func dereferenceSchema(sr *openapi3.SchemaRef, seenBefore map[string]*openapi3.Schema) { +func dereferenceSchema(sr *base.SchemaProxy, seenBefore map[string]*base.SchemaProxy) { if sr == nil { return } - if sr.Ref != "" { - if seenBefore[sr.Ref] != nil { + srRef := sr.GetReference() + + if srRef != "" { + if seenBefore[srRef] != nil { return } - seenBefore[sr.Ref] = sr.Value + seenBefore[srRef] = sr } - s := sr.Value + s := sr.Schema() - for _, list := range []openapi3.SchemaRefs{s.AllOf, s.AnyOf, s.OneOf} { - for _, s2 := range list { - dereferenceSchema(s2, seenBefore) - } + for _, schema := range s.AllOf { + dereferenceSchema(schema, seenBefore) + } + + for _, schema := range s.AnyOf { + dereferenceSchema(schema, seenBefore) + } + + for _, schema := range s.OneOf { + dereferenceSchema(schema, seenBefore) + } + + schemaMap := s.Properties + schema := schemaMap.First() + + for schema != nil { + dereferenceSchema(schema.Value(), seenBefore) + schema = schema.Next() } - for _, s2 := range s.Properties { - dereferenceSchema(s2, seenBefore) + + dereferenceSchema(s.Not, seenBefore) + + if s.AdditionalProperties != nil && s.AdditionalProperties.IsA() { + dereferenceSchema(s.AdditionalProperties.A, seenBefore) } - for _, ref := range []*openapi3.SchemaRef{s.Not, s.AdditionalProperties, s.Items} { - dereferenceSchema(ref, seenBefore) + + if s.Items != nil && s.Items.IsA() { + dereferenceSchema(s.Items.A, seenBefore) } } // extractSchema will extract a schema, including all sub-schemas/references and // return it as a single JSONschema string. All components will be moved under the // "#/definitions/" key. -func extractSchema(s *openapi3.SchemaRef) string { - if s == nil || s.Value == nil { +func extractSchema(s *base.SchemaProxy) string { + if s == nil || s.Schema() == nil { return "" } - seenBefore := make(map[string]*openapi3.Schema) + seenBefore := make(map[string]*base.SchemaProxy) dereferenceSchema(s, seenBefore) - var finalSchema map[string]interface{} - // copy the primary schema - jConf, _ := s.MarshalJSON() - _ = json.Unmarshal(jConf, &finalSchema) + finalSchema := make(map[string]interface{}) + + if s.IsReference() { + // Add ref key and string + finalSchema["$ref"] = s.GetReference() + } else { + // copy the primary schema, if no ref string is present + jConf, _ := s.Schema().MarshalJSON() + _ = json.Unmarshal(jConf, &finalSchema) + } // inject subschema's referenced if len(seenBefore) > 0 { @@ -58,7 +84,12 @@ func extractSchema(s *openapi3.SchemaRef) string { for key, schema := range seenBefore { // copy the subschema var copySchema map[string]interface{} - jConf, _ := schema.MarshalJSON() + + if schema.Schema() == nil { + continue + } + + jConf, _ := schema.Schema().MarshalJSON() _ = json.Unmarshal(jConf, ©Schema) // store under new key diff --git a/openapi2kong/openapi2kong.go b/openapi2kong/openapi2kong.go index 0ca6496..7275426 100644 --- a/openapi2kong/openapi2kong.go +++ b/openapi2kong/openapi2kong.go @@ -9,12 +9,15 @@ import ( "sort" "strings" - "github.com/getkin/kin-openapi/openapi3" "github.com/google/uuid" "github.com/kong/go-apiops/filebasics" "github.com/kong/go-apiops/jsonbasics" "github.com/kong/go-apiops/logbasics" - "github.com/kong/go-slugify" + "github.com/pb33f/libopenapi" + openapibase "github.com/pb33f/libopenapi/datamodel/high/base" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi/orderedmap" + "gopkg.in/yaml.v3" ) const ( @@ -53,207 +56,160 @@ func (opts *O2kOptions) setDefaults() { } } -// Slugify converts a name to a valid Kong name by removing and replacing unallowed characters -// and sanitizing non-latin characters. Multiple inputs will be concatenated using '_'. -func Slugify(insoCompat bool, name ...string) string { - var ( - slugifier *slugify.Slugifier - concatBy string - ) - if insoCompat { - slugifier = (&slugify.Slugifier{}).ToLower(false).InvalidChar("_").WordSeparator("_") - slugifier.AllowedSet("a-zA-Z0-9\\-") - concatBy = "-" - } else { - slugifier = (&slugify.Slugifier{}).ToLower(true).InvalidChar("-").WordSeparator("-") - concatBy = "_" - } - - for i, elem := range name { - name[i] = slugifier.Slugify(elem) - } - - // drop empty strings from the array - for i := 0; i < len(name); i++ { - if name[i] == "" { - name = append(name[:i], name[i+1:]...) - i-- - } - } - - return strings.Join(name, concatBy) -} - -// sanitizeRegexCapture will remove illegal characters from the path-variable name. -// The returned name will be valid for PCRE regex captures; Alphanumeric + '_', starting -// with [a-zA-Z]. -func sanitizeRegexCapture(varName string, insoCompat bool) string { - var regexName *slugify.Slugifier - if insoCompat { - regexName = (&slugify.Slugifier{}).ToLower(false).InvalidChar("_").WordSeparator("_") - } else { - regexName = (&slugify.Slugifier{}).ToLower(true).InvalidChar("_").WordSeparator("_") - } - return regexName.Slugify(varName) -} - // getKongTags returns the provided tags or if nil, then the `x-kong-tags` property, // validated to be a string array. If there is no error, then there will always be // an array returned for safe access later in the process. -func getKongTags(doc *openapi3.T, tagsProvided []string) ([]string, error) { +func getKongTags(doc v3.Document, tagsProvided []string) ([]string, error) { if tagsProvided != nil { // the provided tags take precedence, return them return tagsProvided, nil } - if doc.ExtensionProps.Extensions == nil || doc.ExtensionProps.Extensions["x-kong-tags"] == nil { + if doc.Extensions == nil { // there is no extension, so return an empty array return make([]string, 0), nil } - var tagsValue interface{} - err := json.Unmarshal(doc.ExtensionProps.Extensions["x-kong-tags"].(json.RawMessage), &tagsValue) - if err != nil { - return nil, fmt.Errorf("expected 'x-kong-tags' to be an array of strings: %w", err) - } - var tagsArray []interface{} - switch tags := tagsValue.(type) { - case []interface{}: - // got a proper array - tagsArray = tags - default: - return nil, fmt.Errorf("expected 'x-kong-tags' to be an array of strings") + kongTags, ok := doc.Extensions.Get("x-kong-tags") + + if !ok { + // there is no extension by the name "x-kong-tag", so return an empty array + return make([]string, 0), nil } - resultArray := make([]string, len(tagsArray)) - for i := 0; i < len(tagsArray); i++ { - switch tag := tagsArray[i].(type) { + resultArray := make([]string, len(kongTags.Content)) + + for i, v := range kongTags.Content { + var tagsValue interface{} + err := yaml.Unmarshal([]byte(v.Value), &tagsValue) + if err != nil { + return nil, fmt.Errorf("expected 'x-kong-tags' to be an array of strings: %w", err) + } + + switch tag := tagsValue.(type) { case string: resultArray[i] = tag default: return nil, fmt.Errorf("expected 'x-kong-tags' to be an array of strings") } } + return resultArray, nil } // getKongName returns the `x-kong-name` property, validated to be a string -func getKongName(props openapi3.ExtensionProps) (string, error) { - if props.Extensions != nil && props.Extensions["x-kong-name"] != nil { - var name string - err := json.Unmarshal(props.Extensions["x-kong-name"].(json.RawMessage), &name) - if err != nil { - return "", fmt.Errorf("expected 'x-kong-name' to be a string: %w", err) - } - return name, nil +func getKongName(extensions *orderedmap.Map[string, *yaml.Node]) (string, error) { + if extensions == nil { + return "", nil } - return "", nil -} - -func dereferenceJSONObject( - value map[string]interface{}, - components *map[string]interface{}, -) (map[string]interface{}, error) { - var pointer string - switch value["$ref"].(type) { - case nil: // it is not a reference, so return the object - return value, nil + xKongName, ok := extensions.Get("x-kong-name") - case string: // it is a json pointer - pointer = value["$ref"].(string) - if !strings.HasPrefix(pointer, "#/components/x-kong/") { - return nil, fmt.Errorf("all 'x-kong-...' references must be at '#/components/x-kong/...'") - } - - default: // bad pointer - return nil, fmt.Errorf("expected '$ref' pointer to be a string") + if !ok { + return "", nil } - // walk the tree to find the reference - segments := strings.Split(pointer, "/") - path := "#/components/x-kong" - result := components - - for i := 3; i < len(segments); i++ { - segment := segments[i] - path = path + "/" + segment - - switch (*result)[segment].(type) { - case nil: - return nil, fmt.Errorf("reference '%s' not found", pointer) - case map[string]interface{}: - target := (*result)[segment].(map[string]interface{}) - result = &target - default: - return nil, fmt.Errorf("expected '%s' to be a JSON object", path) - } + var name string + err := yaml.Unmarshal([]byte(xKongName.Value), &name) + if err != nil { + return "", fmt.Errorf("expected 'x-kong-name' to be a string: %w", err) } - return *result, nil + return name, nil } // getXKongObject returns specified 'key' from the extension properties if available. // returns nil if it wasn't found, an error if it wasn't an object or couldn't be // dereferenced. The returned object will be json encoded again. -func getXKongObject(props openapi3.ExtensionProps, key string, components *map[string]interface{}) ([]byte, error) { - if props.Extensions != nil && props.Extensions[key] != nil { - var jsonBlob interface{} - _ = json.Unmarshal(props.Extensions[key].(json.RawMessage), &jsonBlob) - jsonObject, err := jsonbasics.ToObject(jsonBlob) - if err != nil { - return nil, fmt.Errorf("expected '%s' to be a JSON object", key) - } +func getXKongObject( + extensions *orderedmap.Map[string, *yaml.Node], + key string, components *map[string]interface{}, +) ([]byte, error) { + if extensions == nil { + return nil, nil + } - object, err := dereferenceJSONObject(jsonObject, components) - if err != nil { - return nil, err - } - return json.Marshal(object) + xKongObject, ok := extensions.Get(key) + if !ok || xKongObject == nil { + return nil, nil } - return nil, nil + + xKongObjectBytes, err := convertYamlNodeToBytes(xKongObject) + if err != nil { + return nil, fmt.Errorf("expected '%s' to be a YAML object", key) + } + + var jsonBlob interface{} + _ = yaml.Unmarshal(xKongObjectBytes, &jsonBlob) + jsonObject, err := jsonbasics.ToObject(jsonBlob) + if err != nil { + return nil, fmt.Errorf("expected '%s' to be a JSON/YAML object", key) + } + + object, err := dereferenceJSONObject(jsonObject, components) + if err != nil { + return nil, err + } + return json.Marshal(object) } // getXKongComponents will return a map of the '/components/x-kong/' object. If // the extension is not there it will return an empty map. If the entry is not a -// Json object, it will return an error. -func getXKongComponents(doc *openapi3.T) (*map[string]interface{}, error) { +// yaml object, it will return an error. +func getXKongComponents(doc v3.Document) (*map[string]interface{}, error) { var components map[string]interface{} - switch prop := doc.Components.ExtensionProps.Extensions["x-kong"].(type) { - case nil: - // not available, create empty map to do safe lookups down the line - components = make(map[string]interface{}) - default: - // we got some json blob - var xKong interface{} - _ = json.Unmarshal(prop.(json.RawMessage), &xKong) + if doc.Components == nil || doc.Components.Extensions == nil { + return &map[string]interface{}{}, nil + } - switch val := xKong.(type) { - case map[string]interface{}: - components = val + xKongComponents, ok := doc.Components.Extensions.Get("x-kong") - default: - return nil, fmt.Errorf("expected '/components/x-kong' to be a JSON object") - } + if !ok || xKongComponents == nil { + return &components, nil + } + + xKongComponentsBytes, err := convertYamlNodeToBytes(xKongComponents) + if err != nil { + return nil, fmt.Errorf("expected '/components/x-kong' to be a YAML object") + } + + var xKong interface{} + _ = yaml.Unmarshal(xKongComponentsBytes, &xKong) + + switch val := xKong.(type) { + case map[string]interface{}: + components = val + + default: + return nil, fmt.Errorf("expected '/components/x-kong' to be a YAML object") } return &components, nil } // getServiceDefaults returns a JSON string containing the defaults -func getServiceDefaults(props openapi3.ExtensionProps, components *map[string]interface{}) ([]byte, error) { - return getXKongObject(props, "x-kong-service-defaults", components) +func getServiceDefaults( + extensions *orderedmap.Map[string, *yaml.Node], + components *map[string]interface{}, +) ([]byte, error) { + return getXKongObject(extensions, "x-kong-service-defaults", components) } // getUpstreamDefaults returns a JSON string containing the defaults -func getUpstreamDefaults(props openapi3.ExtensionProps, components *map[string]interface{}) ([]byte, error) { - return getXKongObject(props, "x-kong-upstream-defaults", components) +func getUpstreamDefaults( + extensions *orderedmap.Map[string, *yaml.Node], + components *map[string]interface{}, +) ([]byte, error) { + return getXKongObject(extensions, "x-kong-upstream-defaults", components) } // getRouteDefaults returns a JSON string containing the defaults -func getRouteDefaults(props openapi3.ExtensionProps, components *map[string]interface{}) ([]byte, error) { - return getXKongObject(props, "x-kong-route-defaults", components) +func getRouteDefaults( + extensions *orderedmap.Map[string, *yaml.Node], + components *map[string]interface{}, +) ([]byte, error) { + return getXKongObject(extensions, "x-kong-route-defaults", components) } // getOIDCdefaults returns a JSON string containing the defaults from the SecurityRequirements. The type must @@ -261,55 +217,48 @@ func getRouteDefaults(props openapi3.ExtensionProps, components *map[string]inte // If the extension is not there it will return an empty map. If the entry is not a // Json object, it will return an error. func getOIDCdefaults( - requirementsp *openapi3.SecurityRequirements, // the security requirements to parse - doc *openapi3.T, // the complete OAS document + requirements []*openapibase.SecurityRequirement, // the security requirements to parse + doc v3.Document, // the complete OAS document inherited []byte, // the inherited OIDC defaults ignoreSecurityErrors bool, // ignore unsupported security requirements (return "inherited" instead of error) ) ([]byte, error) { // Collect the OAS specific properties var ( - requirements openapi3.SecurityRequirements // the security requirements to parse - schemeName string // the name of the security-scheme - scopes []string // the scopes required for the security-scheme - scheme *openapi3.SecurityScheme // the security-scheme object + // requirements openapi3.SecurityRequirements // the security requirements to parse + schemeName string // the name of the security-scheme + scopes []string // the scopes required for the security-scheme + scheme *v3.SecurityScheme // the security-scheme object ) { - if requirementsp != nil { - requirements = *requirementsp - } else { - // no security requirements, so return inherited (can be nil) + if len(requirements) == 0 || ignoreSecurityErrors { + // no security requirements or nothing is defined + // so return inherited (can be nil) return inherited, nil } - if len(requirements) == 0 { - return inherited, nil // there is nothing defined, so return inherited (can be nil) - } - if len(requirements) > 1 { - // multiple requirements are a logical OR, which is not supported - if !ignoreSecurityErrors { - return nil, fmt.Errorf("only a single security-requirement is supported") - } else { - return inherited, nil - } + + if len(requirements) > 1 && !ignoreSecurityErrors { + return nil, fmt.Errorf("only a single security-requirement is supported") } - requirement := requirements[0] - if len(requirement) == 0 { + + requirement := requirements[0].Requirements + if requirement.Len() == 0 || ignoreSecurityErrors { return inherited, nil // there is nothing defined, so return inherited (can be nil) } - if len(requirement) > 1 { + + if requirement.Len() > 1 && !ignoreSecurityErrors { // multiple schemes are a logical AND, which is not supported - if !ignoreSecurityErrors { - return nil, fmt.Errorf("within a security-requirement only a single security-scheme is supported") - } else { - return inherited, nil - } + return nil, fmt.Errorf("within a security-requirement only a single security-scheme is supported") } - for k, v := range requirement { // has only 1 entry, so executes only once - schemeName = k - scopes = v - } + // requirement has only 1 entry + // So, we won't iterate + reqPair := requirement.First() + schemeName = reqPair.Key() + scopes = reqPair.Value() + + schemes := doc.Components.SecuritySchemes + scheme, _ = schemes.Get(schemeName) - scheme = doc.Components.SecuritySchemes[schemeName].Value if scheme.Type != "openIdConnect" { // non-OIDC security directives are not supported if !ignoreSecurityErrors { @@ -332,7 +281,7 @@ func getOIDCdefaults( } // grab the base plugin config from the x-kong-... directive - pluginBaseData, err := getXKongObject(scheme.ExtensionProps, "x-kong-security-openid-connect", kongComponents) + pluginBaseData, err := getXKongObject(scheme.Extensions, "x-kong-security-openid-connect", kongComponents) if err != nil { return nil, err } @@ -403,7 +352,8 @@ func createPluginID(uuidNamespace uuid.UUID, baseName string, config map[string] // (the 'x-kong-plugin' extensions). Applied on top of the optional // pluginsToInclude list. The result will be sorted by plugin name. func getPluginsList( - props openapi3.ExtensionProps, + extensions *orderedmap.Map[string, *yaml.Node], + componentExtensions *orderedmap.Map[string, *yaml.Node], pluginsToInclude *[]*map[string]interface{}, uuidNamespace uuid.UUID, baseName string, @@ -429,36 +379,42 @@ func getPluginsList( } } - if props.Extensions != nil { - // there are extensions, go check if there are plugins - for extensionName := range props.Extensions { - if strings.HasPrefix(extensionName, "x-kong-plugin-") { - pluginName := strings.TrimPrefix(extensionName, "x-kong-plugin-") + if extensions == nil && componentExtensions == nil { + return nil, nil + } - jsonstr, err := getXKongObject(props, extensionName, components) - if err != nil { - return nil, err - } + // there are extensions, go check if there are plugins + extension := extensions.First() + for extension != nil { + extensionName := extension.Key() + if strings.HasPrefix(extensionName, "x-kong-plugin-") { + pluginName := strings.TrimPrefix(extensionName, "x-kong-plugin-") - var pluginConfig map[string]interface{} - err = json.Unmarshal(jsonstr, &pluginConfig) - if err != nil { - return nil, fmt.Errorf(fmt.Sprintf("failed to parse JSON object for '%s': %%w", extensionName), err) - } - - pluginConfig["name"] = pluginName - if !skipID { - pluginConfig["id"] = createPluginID(uuidNamespace, baseName, pluginConfig) - } - pluginConfig["tags"] = tags + jsonstr, err := getXKongObject(extensions, extensionName, components) + if err != nil { + return nil, err + } - // foreign keys to service+route are not allowed (consumer is allowed) - delete(pluginConfig, "service") - delete(pluginConfig, "route") + var pluginConfig map[string]interface{} + err = json.Unmarshal(jsonstr, &pluginConfig) + if err != nil { + return nil, fmt.Errorf(fmt.Sprintf("failed to parse JSON object for '%s': %%w", extensionName), err) + } - plugins[pluginName] = &pluginConfig + pluginConfig["name"] = pluginName + if !skipID { + pluginConfig["id"] = createPluginID(uuidNamespace, baseName, pluginConfig) } + pluginConfig["tags"] = tags + + // foreign keys to service+route are not allowed (consumer is allowed) + delete(pluginConfig, "service") + delete(pluginConfig, "route") + + plugins[pluginName] = &pluginConfig } + + extension = extension.Next() } // the list is complete, sort to be deterministic in the output @@ -481,6 +437,11 @@ func getPluginsList( // and return it as a JSON string, along with the updated plugin list. If there // is none, the returned config will be the currentConfig. func getValidatorPlugin(list *[]*map[string]interface{}, currentConfig []byte) ([]byte, *[]*map[string]interface{}) { + // search for the request-validator plugin + if list == nil { + return currentConfig, list + } + for i, plugin := range *list { pluginName := (*plugin)["name"].(string) // safe because it was previously parsed if pluginName == "request-validator" { @@ -588,13 +549,13 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { var ( err error - doc *openapi3.T // the OAS3 document we're operating on + doc v3.Document // the OAS3 document we're operating on kongComponents *map[string]interface{} // contents of OAS key `/components/x-kong/` kongTags []string // tags to attach to Kong entities nameConcatChar string // character to use for concatenating names docBaseName string // the slugified basename for the document - docServers *openapi3.Servers // servers block on document level + docServers []*v3.Server // servers block on document level docServiceDefaults []byte // JSON string representation of service-defaults on document level docService map[string]interface{} // service entity in use on document level docUpstreamDefaults []byte // JSON string representation of upstream-defaults on document level @@ -606,7 +567,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { foreignKeyPlugins *[]*map[string]interface{} // top-level array of plugin configs, sorted by plugin name+id pathBaseName string // the slugified basename for the path - pathServers *openapi3.Servers // servers block on current path level + pathServers []*v3.Server // servers block on current path level pathServiceDefaults []byte // JSON string representation of service-defaults on path level pathService map[string]interface{} // service entity in use on path level pathUpstreamDefaults []byte // JSON string representation of upstream-defaults on path level @@ -616,7 +577,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { pathValidatorConfig []byte // JSON string representation of validator config to generate operationBaseName string // the slugified basename for the operation - operationServers *openapi3.Servers // servers block on current operation level + operationServers []*v3.Server // servers block on current operation level operationServiceDefaults []byte // JSON string representation of service-defaults on ops level operationService map[string]interface{} // service entity in use on operation level operationUpstreamDefaults []byte // JSON string representation of upstream-defaults on ops level @@ -625,6 +586,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { operationPluginList *[]*map[string]interface{} // array of plugin configs, sorted by plugin name operationValidatorConfig []byte // JSON string representation of validator config to generate ) + if opts.InsoCompat { nameConcatChar = "-" } else { @@ -632,12 +594,27 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } // Load and parse the OAS file - loader := openapi3.NewLoader() - doc, err = loader.LoadFromData(content) + openapiDoc, err := libopenapi.NewDocument(content) if err != nil { return nil, fmt.Errorf("error parsing OAS3 file: [%w]", err) } + // var errors []error + v3Model, errs := openapiDoc.BuildV3Model() + + // if anything went wrong when building the v3 model, + // a slice of errors will be returned + if len(errs) > 0 { + for i := range errs { + logbasics.Error(errs[i], "error while building v3 document model \n") + } + return nil, fmt.Errorf("cannot create v3 model from document: %d errors reported", len(errs)) + } + + if v3Model != nil { + doc = v3Model.Model + } + // // // Handle OAS Document level @@ -651,13 +628,13 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { logbasics.Info("tags after parsing x-kong-tags", "tags", kongTags) // set document level elements - docServers = &doc.Servers // this one is always set, but can be empty + docServers = doc.Servers // determine document name, precedence: specified -> x-kong-name -> Info.Title -> random docBaseName = opts.DocName if docBaseName == "" { logbasics.Debug("no document name specified, trying x-kong-name") - if docBaseName, err = getKongName(doc.ExtensionProps); err != nil { + if docBaseName, err = getKongName(doc.Extensions); err != nil { return nil, err } if docBaseName == "" { @@ -682,13 +659,13 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } // for defaults we keep strings, so deserializing them provides a copy right away - if docServiceDefaults, err = getServiceDefaults(doc.ExtensionProps, kongComponents); err != nil { + if docServiceDefaults, err = getServiceDefaults(doc.Extensions, kongComponents); err != nil { return nil, err } - if docUpstreamDefaults, err = getUpstreamDefaults(doc.ExtensionProps, kongComponents); err != nil { + if docUpstreamDefaults, err = getUpstreamDefaults(doc.Extensions, kongComponents); err != nil { return nil, err } - if docRouteDefaults, err = getRouteDefaults(doc.ExtensionProps, kongComponents); err != nil { + if docRouteDefaults, err = getRouteDefaults(doc.Extensions, kongComponents); err != nil { return nil, err } @@ -704,15 +681,20 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } // attach plugins - docPluginList, err = getPluginsList(doc.ExtensionProps, nil, opts.UUIDNamespace, docBaseName, + var componentExtensions *orderedmap.Map[string, *yaml.Node] + if doc.Components != nil && doc.Components.Extensions != nil { + componentExtensions = doc.Components.Extensions + } + + docPluginList, err = getPluginsList(doc.Extensions, componentExtensions, nil, opts.UUIDNamespace, docBaseName, kongComponents, kongTags, opts.SkipID) if err != nil { return nil, fmt.Errorf("failed to create plugins list from document root: %w", err) } - // get the OIDC stuff from top level, bail out if the requirements are unsupported + // // get the OIDC stuff from top level, bail out if the requirements are unsupported if opts.OIDC { - docOIDCdefaults, err = getOIDCdefaults(&doc.Security, doc, nil, opts.IgnoreSecurityErrors) + docOIDCdefaults, err = getOIDCdefaults(doc.Security, doc, nil, opts.IgnoreSecurityErrors) if err != nil { return nil, err } @@ -731,7 +713,6 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { foreignKeyPlugins, docPluginList, "service", docService["name"].(string)) docService["plugins"] = docPluginList - // // // Handle OAS Path level @@ -739,27 +720,34 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { // // create a sorted array of paths, to be deterministic in our output order - sortedPaths := make([]string, len(doc.Paths)) + allPaths := doc.Paths.PathItems + sortedPaths := make([]string, allPaths.Len()) + path := allPaths.First() i := 0 - for path := range doc.Paths { - sortedPaths[i] = path + for path != nil && i < allPaths.Len() { + sortedPaths[i] = path.Key() i++ + path = path.Next() } sort.Strings(sortedPaths) - for _, path := range sortedPaths { - logbasics.Info("processing path", "path", path) - pathitem := doc.Paths[path] + for _, pathKey := range sortedPaths { + pathitem, ok := allPaths.Get(pathKey) + if !ok { + continue + } + + logbasics.Info("processing path", "path", pathKey) // determine path name, precedence: specified -> x-kong-name -> actual-path - if pathBaseName, err = getKongName(pathitem.ExtensionProps); err != nil { + if pathBaseName, err = getKongName(pathitem.Extensions); err != nil { return nil, err } if pathBaseName == "" { // no given name, so use the path itself to construct the name if !opts.InsoCompat { - pathBaseName = Slugify(opts.InsoCompat, path) - if strings.HasSuffix(path, "/") { + pathBaseName = Slugify(opts.InsoCompat, pathKey) + if strings.HasSuffix(pathKey, "/") { // a common case is 2 paths, one with and one without a trailing "/" so to prevent // duplicate names being generated, we add a "~" suffix as a special case to cater // for different names. Better user solution is to use operation-id's. @@ -767,13 +755,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } } else { // we need inso compatibility - pathBaseName = Slugify(opts.InsoCompat, path) - // if strings.HasSuffix(path, "/") { - // // a common case is 2 paths, one with and one without a trailing "/" so to prevent - // // duplicate names being generated, we add a "~" suffix as a special case to cater - // // for different names. Better user solution is to use operation-id's. - // pathBaseName = pathBaseName + "~" - // } + pathBaseName = Slugify(opts.InsoCompat, pathKey) } } else { pathBaseName = Slugify(opts.InsoCompat, pathBaseName) @@ -787,7 +769,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { // Set up the defaults on the Path level newPathService := false - if pathServiceDefaults, err = getServiceDefaults(pathitem.ExtensionProps, kongComponents); err != nil { + if pathServiceDefaults, err = getServiceDefaults(pathitem.Extensions, kongComponents); err != nil { return nil, err } if pathServiceDefaults == nil { @@ -797,7 +779,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } newUpstream := false - if pathUpstreamDefaults, err = getUpstreamDefaults(pathitem.ExtensionProps, kongComponents); err != nil { + if pathUpstreamDefaults, err = getUpstreamDefaults(pathitem.Extensions, kongComponents); err != nil { return nil, err } if pathUpstreamDefaults == nil { @@ -807,7 +789,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { newPathService = true } - if pathRouteDefaults, err = getRouteDefaults(pathitem.ExtensionProps, kongComponents); err != nil { + if pathRouteDefaults, err = getRouteDefaults(pathitem.Extensions, kongComponents); err != nil { return nil, err } if pathRouteDefaults == nil { @@ -815,8 +797,8 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } // if there is no path level servers block, use the document one - pathServers = &pathitem.Servers - if len(*pathServers) == 0 { // it's always set, so we ignore it if empty + pathServers = pathitem.Servers + if len(pathServers) == 0 { // it's always set, so we ignore it if empty pathServers = docServers } else { newUpstream = true @@ -840,7 +822,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } // collect path plugins, including the doc-level plugins since we have a new service entity - pathPluginList, err = getPluginsList(pathitem.ExtensionProps, docPluginList, + pathPluginList, err = getPluginsList(pathitem.Extensions, nil, docPluginList, opts.UUIDNamespace, pathBaseName, kongComponents, kongTags, opts.SkipID) if err != nil { return nil, fmt.Errorf("failed to create plugins list from path item: %w", err) @@ -871,7 +853,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { pathService = docService // collect path plugins, only the path level, since we're on the doc-level service-entity - pathPluginList, err = getPluginsList(pathitem.ExtensionProps, nil, + pathPluginList, err = getPluginsList(pathitem.Extensions, componentExtensions, nil, opts.UUIDNamespace, pathBaseName, kongComponents, kongTags, opts.SkipID) if err != nil { return nil, fmt.Errorf("failed to create plugins list from path item: %w", err) @@ -888,24 +870,33 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { // // create a sorted array of operations, to be deterministic in our output order - operations := pathitem.Operations() - sortedMethods := make([]string, len(operations)) + operations := pathitem.GetOperations() + + sortedMethods := make([]string, operations.Len()) + method := operations.First() i := 0 - for method := range operations { - sortedMethods[i] = method + for method != nil && i < operations.Len() { + sortedMethods[i] = method.Key() i++ + method = method.Next() } sort.Strings(sortedMethods) // traverse all operations - for _, method := range sortedMethods { - operation := operations[method] - logbasics.Info("processing operation", "method", method, "path", path, "id", operation.OperationID) + for _, methodKey := range sortedMethods { + operation, ok := operations.Get(methodKey) + if !ok { + continue + } + + methodKey = strings.ToUpper(methodKey) + + logbasics.Info("processing operation", "method", methodKey, "path", path, "id", operation.OperationId) var operationRoutes []interface{} // the routes array we need to add to // determine operation name, precedence: specified -> operation-ID -> method-name - if operationBaseName, err = getKongName(operation.ExtensionProps); err != nil { + if operationBaseName, err = getKongName(operation.Extensions); err != nil { return nil, err } if operationBaseName != "" { @@ -917,13 +908,13 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { operationBaseName = pathBaseName + nameConcatChar + Slugify(opts.InsoCompat, operationBaseName) } } else { - operationBaseName = operation.OperationID + operationBaseName = operation.OperationId if operationBaseName == "" { // no operation ID provided, so build as "doc-path-method" if opts.InsoCompat { - operationBaseName = pathBaseName + nameConcatChar + Slugify(opts.InsoCompat, strings.ToLower(method)) + operationBaseName = pathBaseName + nameConcatChar + Slugify(opts.InsoCompat, strings.ToLower(methodKey)) } else { - operationBaseName = pathBaseName + nameConcatChar + Slugify(opts.InsoCompat, method) + operationBaseName = pathBaseName + nameConcatChar + Slugify(opts.InsoCompat, methodKey) } } else { // operation ID is provided, so build as "doc-operationid" @@ -934,7 +925,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { // Set up the defaults on the Operation level newOperationService := false - if operationServiceDefaults, err = getServiceDefaults(operation.ExtensionProps, kongComponents); err != nil { + if operationServiceDefaults, err = getServiceDefaults(operation.Extensions, kongComponents); err != nil { return nil, err } if operationServiceDefaults == nil { @@ -944,7 +935,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { } newUpstream := false - if operationUpstreamDefaults, err = getUpstreamDefaults(operation.ExtensionProps, kongComponents); err != nil { + if operationUpstreamDefaults, err = getUpstreamDefaults(operation.Extensions, kongComponents); err != nil { return nil, err } if operationUpstreamDefaults == nil { @@ -954,7 +945,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { newOperationService = true } - if operationRouteDefaults, err = getRouteDefaults(operation.ExtensionProps, kongComponents); err != nil { + if operationRouteDefaults, err = getRouteDefaults(operation.Extensions, kongComponents); err != nil { return nil, err } if operationRouteDefaults == nil { @@ -963,7 +954,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { // if there is no operation level servers block, use the path one operationServers = operation.Servers - if operationServers == nil || len(*operationServers) == 0 { + if len(operationServers) == 0 { operationServers = pathServers } else { newUpstream = true @@ -983,7 +974,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { opts.UUIDNamespace, opts.SkipID) if err != nil { - return nil, fmt.Errorf("failed to create service/updstream from operation '%s %s': %w", path, method, err) + return nil, fmt.Errorf("failed to create service/updstream from operation '%s %s': %w", pathKey, methodKey, err) } services = append(services, operationService) if operationUpstream != nil { @@ -1006,21 +997,21 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { if !newOperationService && !newPathService { // we're operating on the doc-level service entity, so we need the plugins // from the path and operation - operationPluginList, err = getPluginsList(operation.ExtensionProps, pathPluginList, + operationPluginList, err = getPluginsList(operation.Extensions, nil, pathPluginList, opts.UUIDNamespace, operationBaseName, kongComponents, kongTags, opts.SkipID) } else if newOperationService { // we're operating on an operation-level service entity, so we need the plugins // from the document, path, and operation. - operationPluginList, _ = getPluginsList(doc.ExtensionProps, nil, opts.UUIDNamespace, + operationPluginList, _ = getPluginsList(doc.Extensions, nil, nil, opts.UUIDNamespace, operationBaseName, kongComponents, kongTags, opts.SkipID) - operationPluginList, _ = getPluginsList(pathitem.ExtensionProps, operationPluginList, opts.UUIDNamespace, + operationPluginList, _ = getPluginsList(pathitem.Extensions, nil, operationPluginList, opts.UUIDNamespace, operationBaseName, kongComponents, kongTags, opts.SkipID) - operationPluginList, err = getPluginsList(operation.ExtensionProps, operationPluginList, opts.UUIDNamespace, + operationPluginList, err = getPluginsList(operation.Extensions, nil, operationPluginList, opts.UUIDNamespace, operationBaseName, kongComponents, kongTags, opts.SkipID) } else if newPathService { // we're operating on a path-level service entity, so we only need the plugins // from the operation. - operationPluginList, err = getPluginsList(operation.ExtensionProps, nil, opts.UUIDNamespace, + operationPluginList, err = getPluginsList(operation.Extensions, nil, nil, opts.UUIDNamespace, operationBaseName, kongComponents, kongTags, opts.SkipID) } if err != nil { @@ -1063,7 +1054,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { route["plugins"] = operationPluginList // Escape path contents for regex creation - convertedPath := path + convertedPath := pathKey charsToEscape := []string{"(", ")", ".", "+", "?", "*", "[", "$"} for _, char := range charsToEscape { convertedPath = strings.ReplaceAll(convertedPath, char, "\\"+char) @@ -1094,7 +1085,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { route["id"] = uuid.NewSHA1(opts.UUIDNamespace, []byte(operationBaseName+".route")).String() } route["name"] = operationBaseName - route["methods"] = []string{method} + route["methods"] = []string{methodKey} route["tags"] = kongTags if _, found := route["regex_priority"]; !found { route["regex_priority"] = regexPriority diff --git a/openapi2kong/service.go b/openapi2kong/service.go index 7ac8f72..bbe63a5 100644 --- a/openapi2kong/service.go +++ b/openapi2kong/service.go @@ -7,8 +7,8 @@ import ( "strconv" "strings" - "github.com/getkin/kin-openapi/openapi3" "github.com/google/uuid" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) const ( @@ -18,21 +18,27 @@ const ( // parseServerUris parses the server uri's after rendering the template variables. // result will always have at least 1 entry, but not necessarily a hostname/port/scheme -func parseServerUris(servers *openapi3.Servers) ([]*url.URL, error) { +func parseServerUris(servers []*v3.Server) ([]*url.URL, error) { var targets []*url.URL - if servers == nil || len(*servers) == 0 { + if len(servers) == 0 { uriObject, _ := url.ParseRequestURI("/") // path '/' is the default for empty server blocks targets = make([]*url.URL, 1) targets[0] = uriObject } else { - targets = make([]*url.URL, len(*servers)) + targets = make([]*url.URL, len(servers)) - for i, server := range *servers { + for i, server := range servers { uriString := server.URL - for name, svar := range server.Variables { + + pair := server.Variables.First() + for pair != nil { + name := pair.Key() + svar := pair.Value() uriString = strings.ReplaceAll(uriString, "{"+name+"}", svar.Default) + + pair = pair.Next() } uriObject, err := url.ParseRequestURI(uriString) @@ -118,7 +124,7 @@ func parseDefaultTargets(targets interface{}, tags []string) ([]map[string]inter // createKongUpstream create a new upstream entity. func createKongUpstream( baseName string, // slugified name of the upstream, and uuid input - servers *openapi3.Servers, // the OAS3 server block to use for generation + servers []*v3.Server, // the OAS3 server block to use for generation upstreamDefaults []byte, // defaults to use (JSON string) or empty if no defaults tags []string, // tags to attach to the new upstream uuidNamespace uuid.UUID, @@ -179,7 +185,7 @@ func createKongUpstream( // for the UUIDv5 generation. func CreateKongService( baseName string, // slugified name of the service, and uuid input - servers *openapi3.Servers, + servers []*v3.Server, serviceDefaults []byte, upstreamDefaults []byte, tags []string, diff --git a/openapi2kong/service_test.go b/openapi2kong/service_test.go index 3b78b36..51f92c5 100644 --- a/openapi2kong/service_test.go +++ b/openapi2kong/service_test.go @@ -4,17 +4,19 @@ import ( "net/url" "testing" - "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-cmp/cmp" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + "github.com/pb33f/libopenapi/orderedmap" ) func Test_parseServerUris(t *testing.T) { // basics - servers := &openapi3.Servers{ + servers := []*v3.Server{ { URL: "http://cookiemonster.com/chocolate/cookie", - }, { + }, + { URL: "https://konghq.com/bitter/sweet", }, } @@ -34,26 +36,27 @@ func Test_parseServerUris(t *testing.T) { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) + t.Errorf(diff) //nolint:govet } - // replaces variables with defaults + variables := orderedmap.New[string, *v3.ServerVariable]() + variables.Set("var1", &v3.ServerVariable{ + Default: "hello", + Enum: []string{"hello", "world"}, + }) + variables.Set("var2", &v3.ServerVariable{ + Default: "Welt", + Enum: []string{"hallo", "Welt"}, + }) - servers = &openapi3.Servers{ + // replaces variables with defaults + servers = []*v3.Server{ { - URL: "http://{var1}-{var2}.com/chocolate/cookie", - Variables: map[string]*openapi3.ServerVariable{ - "var1": { - Default: "hello", - Enum: []string{"hello", "world"}, - }, - "var2": { - Default: "Welt", - Enum: []string{"hallo", "Welt"}, - }, - }, + URL: "http://{var1}-{var2}.com/chocolate/cookie", + Variables: variables, }, } + expected = []*url.URL{ { Scheme: "http", @@ -66,12 +69,12 @@ func Test_parseServerUris(t *testing.T) { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) + t.Errorf(diff) //nolint:govet } // returns error on a bad URL - servers = &openapi3.Servers{ + servers = []*v3.Server{ { URL: "http://cookiemonster.com/chocolate/cookie", }, { @@ -90,12 +93,12 @@ func Test_parseServerUris(t *testing.T) { Path: "/", }, } - targets, err = parseServerUris(&openapi3.Servers{}) + targets, err = parseServerUris([]*v3.Server{}) if err != nil { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) + t.Errorf(diff) //nolint:govet } // returns no error if servers is nil @@ -110,7 +113,7 @@ func Test_parseServerUris(t *testing.T) { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) + t.Errorf(diff) //nolint:govet } } diff --git a/openapi2kong/utils.go b/openapi2kong/utils.go new file mode 100644 index 0000000..5cdc67e --- /dev/null +++ b/openapi2kong/utils.go @@ -0,0 +1,105 @@ +package openapi2kong + +import ( + "fmt" + "strings" + + "github.com/kong/go-slugify" + "gopkg.in/yaml.v3" +) + +// Slugify converts a name to a valid Kong name by removing and replacing unallowed characters +// and sanitizing non-latin characters. Multiple inputs will be concatenated using '_'. +func Slugify(insoCompat bool, name ...string) string { + var ( + slugifier *slugify.Slugifier + concatBy string + ) + if insoCompat { + slugifier = (&slugify.Slugifier{}).ToLower(false).InvalidChar("_").WordSeparator("_") + slugifier.AllowedSet("a-zA-Z0-9\\-") + concatBy = "-" + } else { + slugifier = (&slugify.Slugifier{}).ToLower(true).InvalidChar("-").WordSeparator("-") + concatBy = "_" + } + + for i, elem := range name { + name[i] = slugifier.Slugify(elem) + } + + // drop empty strings from the array + for i := 0; i < len(name); i++ { + if name[i] == "" { + name = append(name[:i], name[i+1:]...) + i-- + } + } + + return strings.Join(name, concatBy) +} + +// sanitizeRegexCapture will remove illegal characters from the path-variable name. +// The returned name will be valid for PCRE regex captures; Alphanumeric + '_', starting +// with [a-zA-Z]. +func sanitizeRegexCapture(varName string, insoCompat bool) string { + var regexName *slugify.Slugifier + if insoCompat { + regexName = (&slugify.Slugifier{}).ToLower(false).InvalidChar("_").WordSeparator("_") + } else { + regexName = (&slugify.Slugifier{}).ToLower(true).InvalidChar("_").WordSeparator("_") + } + return regexName.Slugify(varName) +} + +func dereferenceJSONObject( + value map[string]interface{}, + components *map[string]interface{}, +) (map[string]interface{}, error) { + var pointer string + + switch value["$ref"].(type) { + case nil: // it is not a reference, so return the object + return value, nil + + case string: // it is a json pointer + pointer = value["$ref"].(string) + if !strings.HasPrefix(pointer, "#/components/x-kong/") { + return nil, fmt.Errorf("all 'x-kong-...' references must be at '#/components/x-kong/...'") + } + + default: // bad pointer + return nil, fmt.Errorf("expected '$ref' pointer to be a string") + } + + // walk the tree to find the reference + segments := strings.Split(pointer, "/") + path := "#/components/x-kong" + result := components + + for i := 3; i < len(segments); i++ { + segment := segments[i] + path = path + "/" + segment + + switch (*result)[segment].(type) { + case nil: + return nil, fmt.Errorf("reference '%s' not found", pointer) + case map[string]interface{}: + target := (*result)[segment].(map[string]interface{}) + result = &target + default: + return nil, fmt.Errorf("expected '%s' to be a JSON object", path) + } + } + + return *result, nil +} + +func convertYamlNodeToBytes(node *yaml.Node) ([]byte, error) { + var data interface{} + err := node.Decode(&data) + if err != nil { + return nil, err + } + return yaml.Marshal(data) +} diff --git a/openapi2kong/validator.go b/openapi2kong/validator.go index edb613e..2dbc750 100644 --- a/openapi2kong/validator.go +++ b/openapi2kong/validator.go @@ -6,10 +6,10 @@ import ( "sort" "strings" - "github.com/getkin/kin-openapi/openapi3" "github.com/google/uuid" "github.com/kong/go-apiops/jsonbasics" "github.com/kong/go-apiops/logbasics" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" ) const JSONSchemaVersion = "draft4" @@ -33,7 +33,7 @@ func getDefaultParamStyle(givenStyle string, paramType string) string { // generateParameterSchema returns the given schema if there is one, a generated // schema if it was specified, or nil if there is none. // Parameters include path, query, and headers -func generateParameterSchema(operation *openapi3.Operation, insoCompat bool) []map[string]interface{} { +func generateParameterSchema(operation *v3.Operation, insoCompat bool) []map[string]interface{} { parameters := operation.Parameters if parameters == nil { return nil @@ -45,31 +45,29 @@ func generateParameterSchema(operation *openapi3.Operation, insoCompat bool) []m result := make([]map[string]interface{}, len(parameters)) i := 0 - for _, parameterRef := range parameters { - paramValue := parameterRef.Value - - if paramValue != nil { - style := getDefaultParamStyle(paramValue.Style, paramValue.In) + for _, parameter := range parameters { + if parameter != nil { + style := getDefaultParamStyle(parameter.Style, parameter.In) var explode bool - if paramValue.Explode == nil { + if parameter.Explode == nil { explode = (style == "form") // default to true for form style, false for all others } else { - explode = *paramValue.Explode + explode = *parameter.Explode } paramConf := make(map[string]interface{}) paramConf["style"] = style paramConf["explode"] = explode - paramConf["in"] = paramValue.In - if paramValue.In == "path" { - paramConf["name"] = sanitizeRegexCapture(paramValue.Name, insoCompat) + paramConf["in"] = parameter.In + if parameter.In == "path" { + paramConf["name"] = sanitizeRegexCapture(parameter.Name, insoCompat) } else { - paramConf["name"] = paramValue.Name + paramConf["name"] = parameter.Name } - paramConf["required"] = paramValue.Required + paramConf["required"] = parameter.Required - schema := extractSchema(paramValue.Schema) + schema := extractSchema(parameter.Schema) if schema != "" { paramConf["schema"] = schema } @@ -93,31 +91,33 @@ func parseMediaType(mediaType string) (string, string, error) { // generateBodySchema returns the given schema if there is one, a generated // schema if it was specified, or "" if there is none. -func generateBodySchema(operation *openapi3.Operation) string { +func generateBodySchema(operation *v3.Operation) string { requestBody := operation.RequestBody if requestBody == nil { return "" } - requestBodyValue := requestBody.Value - if requestBodyValue == nil { - return "" - } - - content := requestBodyValue.Content + content := requestBody.Content if content == nil { return "" } - for contentType, content := range content { + contentItem := content.First() + + for contentItem != nil { + contentType := contentItem.Key() + contentValue := contentItem.Value() + typ, subtype, err := parseMediaType(contentType) if err != nil { logbasics.Info("invalid MediaType '" + contentType + "' will be ignored") return "" } if typ == "application" && (subtype == "json" || strings.HasSuffix(subtype, "+json")) { - return extractSchema((*content).Schema) + return extractSchema((*contentValue).Schema) } + + contentItem = contentItem.Next() } return "" @@ -125,31 +125,28 @@ func generateBodySchema(operation *openapi3.Operation) string { // generateContentTypes returns an array of allowed content types. nil if none. // Returned array will be sorted by name for deterministic comparisons. -func generateContentTypes(operation *openapi3.Operation) []string { +func generateContentTypes(operation *v3.Operation) []string { requestBody := operation.RequestBody if requestBody == nil { return nil } - requestBodyValue := requestBody.Value - if requestBodyValue == nil { - return nil - } - - content := requestBodyValue.Content + content := requestBody.Content if content == nil { return nil } - if len(content) == 0 { + if content.Len() == 0 { return nil } - list := make([]string, len(content)) + list := make([]string, content.Len()) i := 0 - for contentType := range content { - list[i] = contentType + contentItem := content.First() + for contentItem != nil && i < len(list) { + list[i] = contentItem.Key() i++ + contentItem = contentItem.Next() } sort.Strings(list) @@ -158,7 +155,7 @@ func generateContentTypes(operation *openapi3.Operation) []string { // generateValidatorPlugin generates the validator plugin configuration, based // on the JSON snippet, and the OAS inputs. This can return nil -func generateValidatorPlugin(configJSON []byte, operation *openapi3.Operation, +func generateValidatorPlugin(configJSON []byte, operation *v3.Operation, uuidNamespace uuid.UUID, baseName string, skipID bool, insoCompat bool, ) *map[string]interface{} { if len(configJSON) == 0 { diff --git a/patch/deckpatch.go b/patch/deckpatch.go index 6ab7a4a..33f9cc6 100644 --- a/patch/deckpatch.go +++ b/patch/deckpatch.go @@ -157,6 +157,7 @@ func (patch *DeckPatch) ApplyToNodes(yamlData *yaml.Node) (err error) { logbasics.Info("Patch has no selectors specified") } + //nolint:gosimple if patch.Selectors == nil || len(patch.Selectors) == 0 { patch.Selectors = make([]*yamlpath.Path, len(patch.SelectorSources)) for i, selector := range patch.SelectorSources { diff --git a/yamlbasics/selectors.go b/yamlbasics/selectors.go index 76990f0..23fd61c 100644 --- a/yamlbasics/selectors.go +++ b/yamlbasics/selectors.go @@ -46,6 +46,7 @@ func NewSelectorSet(selectors []string) (SelectorSet, error) { // IsEmpty returns true if the selector set is empty. func (set *SelectorSet) IsEmpty() bool { + //nolint:gosimple return set.selectors == nil || len(set.selectors) == 0 } From 4af8ef39a81cca76cbdc951fbb95083283394246 Mon Sep 17 00:00:00 2001 From: Prashansa Kulshrestha Date: Wed, 14 Aug 2024 17:00:53 +0530 Subject: [PATCH 2/3] chore: lint check corrections --- openapi2kong/service_test.go | 8 ++++---- yamlbasics/selectors.go | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openapi2kong/service_test.go b/openapi2kong/service_test.go index 51f92c5..1bee0a7 100644 --- a/openapi2kong/service_test.go +++ b/openapi2kong/service_test.go @@ -36,7 +36,7 @@ func Test_parseServerUris(t *testing.T) { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) //nolint:govet + t.Errorf(diff) //nolint:govet,staticcheck } variables := orderedmap.New[string, *v3.ServerVariable]() @@ -69,7 +69,7 @@ func Test_parseServerUris(t *testing.T) { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) //nolint:govet + t.Errorf(diff) //nolint:govet,staticcheck } // returns error on a bad URL @@ -98,7 +98,7 @@ func Test_parseServerUris(t *testing.T) { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) //nolint:govet + t.Errorf(diff) //nolint:govet,staticcheck } // returns no error if servers is nil @@ -113,7 +113,7 @@ func Test_parseServerUris(t *testing.T) { t.Errorf("did not expect error: %v", err) } if diff := cmp.Diff(targets, expected); diff != "" { - t.Errorf(diff) //nolint:govet + t.Errorf(diff) //nolint:govet,staticcheck } } diff --git a/yamlbasics/selectors.go b/yamlbasics/selectors.go index 23fd61c..6a95ec2 100644 --- a/yamlbasics/selectors.go +++ b/yamlbasics/selectors.go @@ -68,6 +68,7 @@ func (set *SelectorSet) Find(nodeToSearch *yaml.Node) (NodeSet, error) { if nodeToSearch == nil { panic("expected nodeToSearch to be non-nil") } + //nolint:gosimple if set.selectors == nil || len(set.selectors) == 0 { return make(NodeSet, 0), nil } From d14c3ed6f3164a241cebfb4b26960454c1c06613 Mon Sep 17 00:00:00 2001 From: Prashansa Kulshrestha Date: Mon, 19 Aug 2024 12:25:42 +0530 Subject: [PATCH 3/3] chore: corrections for line spaces, error handling, stray comments --- openapi2kong/jsonschema.go | 18 +++++------------- openapi2kong/openapi2kong.go | 16 +++++----------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/openapi2kong/jsonschema.go b/openapi2kong/jsonschema.go index d0f28c6..809acc8 100644 --- a/openapi2kong/jsonschema.go +++ b/openapi2kong/jsonschema.go @@ -24,22 +24,15 @@ func dereferenceSchema(sr *base.SchemaProxy, seenBefore map[string]*base.SchemaP } s := sr.Schema() - - for _, schema := range s.AllOf { - dereferenceSchema(schema, seenBefore) - } - - for _, schema := range s.AnyOf { - dereferenceSchema(schema, seenBefore) - } - - for _, schema := range s.OneOf { - dereferenceSchema(schema, seenBefore) + allSchemas := [][]*base.SchemaProxy{s.AllOf, s.AnyOf, s.OneOf} + for _, schemas := range allSchemas { + for _, schema := range schemas { + dereferenceSchema(schema, seenBefore) + } } schemaMap := s.Properties schema := schemaMap.First() - for schema != nil { dereferenceSchema(schema.Value(), seenBefore) schema = schema.Next() @@ -70,7 +63,6 @@ func extractSchema(s *base.SchemaProxy) string { finalSchema := make(map[string]interface{}) if s.IsReference() { - // Add ref key and string finalSchema["$ref"] = s.GetReference() } else { // copy the primary schema, if no ref string is present diff --git a/openapi2kong/openapi2kong.go b/openapi2kong/openapi2kong.go index 7275426..98f07ff 100644 --- a/openapi2kong/openapi2kong.go +++ b/openapi2kong/openapi2kong.go @@ -71,14 +71,12 @@ func getKongTags(doc v3.Document, tagsProvided []string) ([]string, error) { } kongTags, ok := doc.Extensions.Get("x-kong-tags") - if !ok { // there is no extension by the name "x-kong-tag", so return an empty array return make([]string, 0), nil } resultArray := make([]string, len(kongTags.Content)) - for i, v := range kongTags.Content { var tagsValue interface{} err := yaml.Unmarshal([]byte(v.Value), &tagsValue) @@ -176,13 +174,9 @@ func getXKongComponents(doc v3.Document) (*map[string]interface{}, error) { var xKong interface{} _ = yaml.Unmarshal(xKongComponentsBytes, &xKong) - - switch val := xKong.(type) { - case map[string]interface{}: - components = val - - default: - return nil, fmt.Errorf("expected '/components/x-kong' to be a YAML object") + components, err = jsonbasics.ToObject(xKong) + if err != nil { + return nil, fmt.Errorf("expected '/components/x-kong' to be a JSON/YAML object") } return &components, nil @@ -398,7 +392,7 @@ func getPluginsList( var pluginConfig map[string]interface{} err = json.Unmarshal(jsonstr, &pluginConfig) if err != nil { - return nil, fmt.Errorf(fmt.Sprintf("failed to parse JSON object for '%s': %%w", extensionName), err) + return nil, fmt.Errorf("failed to parse JSON object for '%s': %w", extensionName, err) } pluginConfig["name"] = pluginName @@ -692,7 +686,7 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) { return nil, fmt.Errorf("failed to create plugins list from document root: %w", err) } - // // get the OIDC stuff from top level, bail out if the requirements are unsupported + // get the OIDC stuff from top level, bail out if the requirements are unsupported if opts.OIDC { docOIDCdefaults, err = getOIDCdefaults(doc.Security, doc, nil, opts.IgnoreSecurityErrors) if err != nil {