diff --git a/go.mod b/go.mod index 38efc596..1a57d146 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/chai2010/jsonv v1.1.3 github.com/chai2010/protorpc v1.1.4 + github.com/getkin/kin-openapi v0.122.0 github.com/goccy/go-yaml v1.11.0 github.com/gofrs/flock v0.8.1 github.com/golang/protobuf v1.5.3 @@ -16,6 +17,7 @@ require ( github.com/qri-io/jsonpointer v0.1.1 github.com/stretchr/testify v1.8.4 github.com/wk8/go-ordered-map/v2 v2.1.8 + github.com/yuin/goldmark v1.4.13 google.golang.org/grpc v1.56.3 google.golang.org/protobuf v1.30.0 gopkg.in/yaml.v3 v3.0.1 @@ -52,13 +54,17 @@ require ( github.com/go-git/go-git/v5 v5.11.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.3 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect + github.com/invopop/yaml v0.2.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.16.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -67,10 +73,12 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc4 // indirect github.com/otiai10/copy v1.9.0 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index f61d2704..3ab1f430 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,7 @@ github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaD github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg= github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -121,6 +122,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10= +github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= @@ -145,10 +148,16 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 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.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +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-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= @@ -231,8 +240,11 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -255,10 +267,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -281,6 +295,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -297,6 +313,8 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6 github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4= github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= @@ -355,14 +373,20 @@ github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -371,6 +395,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= @@ -702,6 +727,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= @@ -713,6 +739,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= diff --git a/pkg/tools/gen/gendoc.go b/pkg/tools/gen/gendoc.go index dd36fda0..61f09921 100644 --- a/pkg/tools/gen/gendoc.go +++ b/pkg/tools/gen/gendoc.go @@ -11,6 +11,8 @@ import ( "sort" "strings" "text/template" + + "github.com/yuin/goldmark" ) //go:embed templates/doc/schemaDoc.gotmpl @@ -71,6 +73,7 @@ type Format string const ( Html Format = "html" Markdown Format = "md" + OpenAPI Format = "openapi" ) // KclPackage contains package information of package metadata(such as name, version, description, ...) and exported models(such as schemas) @@ -90,12 +93,8 @@ func (g *GenContext) render(spec *SwaggerV2Spec) error { if err != nil { return fmt.Errorf("failed to create docs/ directory under the target directory: %s", err) } - // extract kcl package from swaggerV2 spec - rootPkg := spec.toKclPackage() - // sort schemas and subpackages by their names - rootPkg.sortSchemasAndPkgs() // render the package - err = g.renderPackage(rootPkg, g.Target) + err = g.renderPackage(spec, g.Target) if err != nil { return err } @@ -231,28 +230,72 @@ func (pkg *KclPackage) getIndexContent(level int, indentation string) string { return content } -func (g *GenContext) renderPackage(pkg *KclPackage, parentDir string) error { +func (g *GenContext) renderPackage(spec *SwaggerV2Spec, parentDir string) error { + // extract kcl package from swaggerV2 spec + pkg := spec.toKclPackage() + // sort schemas and subpackages by their names + pkg.sortSchemasAndPkgs() pkgName := pkg.Name if pkg.Name == "" { pkgName = "main" } - fmt.Println(fmt.Sprintf("generating doc for package %s", pkgName)) - docFileName := fmt.Sprintf("%s.%s", pkgName, g.Format) - var contentBuf bytes.Buffer - err := g.Template.ExecuteTemplate(&contentBuf, "packageDoc", struct { - EscapeHtml bool - Data *KclPackage - }{ - EscapeHtml: g.EscapeHtml, - Data: pkg, - }) - if err != nil { - return fmt.Errorf("failed to render package %s with template, err: %s", pkg.Name, err) - } - // write content to file - err = os.WriteFile(filepath.Join(parentDir, docFileName), contentBuf.Bytes(), 0644) - if err != nil { - return fmt.Errorf("failed to write file %s in %s: %v", docFileName, parentDir, err) + fmt.Printf("generating doc for package %s\n", pkgName) + // --- format --- + switch strings.ToLower(string(g.Format)) { + case string(Markdown): + docFileName := fmt.Sprintf("%s.%s", pkgName, g.Format) + var buf bytes.Buffer + err := g.Template.ExecuteTemplate(&buf, "packageDoc", struct { + EscapeHtml bool + Data *KclPackage + }{ + EscapeHtml: g.EscapeHtml, + Data: pkg, + }) + if err != nil { + return fmt.Errorf("failed to render package %s with template, err: %s", pkg.Name, err) + } + // write content to file + err = os.WriteFile(filepath.Join(parentDir, docFileName), buf.Bytes(), 0644) + if err != nil { + return fmt.Errorf("failed to write file %s in %s: %v", docFileName, parentDir, err) + } + case string(Html): + var mdBuf bytes.Buffer + err := g.Template.ExecuteTemplate(&mdBuf, "packageDoc", struct { + EscapeHtml bool + Data *KclPackage + }{ + EscapeHtml: g.EscapeHtml, + Data: pkg, + }) + if err != nil { + return fmt.Errorf("failed to render package %s with template, err: %s", pkg.Name, err) + } + var htmlBuf bytes.Buffer + if err := goldmark.Convert(mdBuf.Bytes(), &htmlBuf); err != nil { + panic(err) + } + docFileName := fmt.Sprintf("%s.%s", pkgName, g.Format) + // write content to file + err = os.WriteFile(filepath.Join(parentDir, docFileName), htmlBuf.Bytes(), 0644) + if err != nil { + return fmt.Errorf("failed to write file %s in %s: %v", docFileName, parentDir, err) + } + case string(OpenAPI): + docFileName := fmt.Sprintf("%s.%s", pkgName, "json") + spec := SwaggerV2TotOpenAPIV3Spec(spec) + json, err := spec.MarshalJSON() + if err != nil { + return err + } + // write content to file + err = os.WriteFile(filepath.Join(parentDir, docFileName), json, 0644) + if err != nil { + return fmt.Errorf("failed to write file %s in %s: %v", docFileName, parentDir, err) + } + default: + return fmt.Errorf("invalid generate format. Allow values: %s", []Format{Markdown, Html, OpenAPI}) } return nil } @@ -263,12 +306,12 @@ func (opts *GenOpts) ValidateComplete() (*GenContext, error) { switch strings.ToLower(opts.Format) { case string(Markdown): g.Format = Markdown - break case string(Html): g.Format = Html - break + case string(OpenAPI): + g.Format = OpenAPI default: - return nil, fmt.Errorf("invalid generate format. Allow values: %s", []Format{Markdown, Html}) + return nil, fmt.Errorf("invalid generate format. Allow values: %s", []Format{Markdown, Html, OpenAPI}) } // --- package path --- @@ -374,7 +417,7 @@ func (opts *GenOpts) ValidateComplete() (*GenContext, error) { g.Target = path.Join(g.Target, "docs") if _, err := os.Stat(g.Target); err == nil { // check and warn if the docs directory already exists - fmt.Println(fmt.Sprintf("[Warn] path %s exists, all the content will be overwritten", g.Target)) + fmt.Printf("[Warn] path %s exists, all the content will be overwritten\n", g.Target) if err := os.RemoveAll(g.Target); err != nil { return nil, fmt.Errorf("failed to remove existing content in %s:%s", g.Target, err) } @@ -385,7 +428,7 @@ func (opts *GenOpts) ValidateComplete() (*GenContext, error) { // GenDoc generate document files from KCL source files func (g *GenContext) GenDoc() error { - spec, err := KclPackageToSwaggerV2Spec(g.PackagePath) + spec, err := ExportSwaggerV2Spec(g.PackagePath) if err != nil { return err } diff --git a/pkg/tools/gen/genopenapi.go b/pkg/tools/gen/genopenapi.go index fed55d6e..e4b66950 100644 --- a/pkg/tools/gen/genopenapi.go +++ b/pkg/tools/gen/genopenapi.go @@ -3,6 +3,8 @@ package gen import ( "fmt" htmlTmpl "html/template" + + "github.com/getkin/kin-openapi/openapi3" kpm "kcl-lang.io/kpm/pkg/api" "os" @@ -12,17 +14,62 @@ import ( kcl "kcl-lang.io/kcl-go" ) -// ExportSwaggerV2Spec export swagger v2 spec of a kcl package -func ExportSwaggerV2Spec(pkgPath string) (string, error) { - spec, err := KclPackageToSwaggerV2Spec(pkgPath) +const ( + ExtensionKclType = "x-kcl-type" + ExtensionKclDecorators = "x-kcl-decorators" + ExtensionKclUnionTypes = "x-kcl-union-types" + ExtensionKclDictKeyType = "x-kcl-dict-key-type" +) + +// ExportOpenAPIV3Spec exports open api v3 spec of a kcl package +func ExportOpenAPIV3Spec(pkgPath string) (*openapi3.T, error) { + s, err := ExportSwaggerV2Spec(pkgPath) if err != nil { - return "", err + return nil, err } - return jsonString(spec), nil + return SwaggerV2TotOpenAPIV3Spec(s), nil } -// KclPackageToSwaggerV2Spec extracts the swagger v2 representation of a kcl package -func KclPackageToSwaggerV2Spec(pkgPath string) (*SwaggerV2Spec, error) { +// ExportOpenAPITypeToSchema exports open api v3 schema ref from the kcl type. +func ExportOpenAPITypeToSchema(ty *KclOpenAPIType) *openapi3.SchemaRef { + s := &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: string(ty.Type), + Format: string(ty.Format), + Default: ty.Default, + Enum: ty.GetAnyEnum(), + ReadOnly: ty.ReadOnly, + Description: ty.Description, + Properties: make(openapi3.Schemas), + Required: ty.Required, + Extensions: ty.GetExtensionsMapping(), + }, + Ref: ty.Ref, + } + for i, t := range ty.Properties { + s.Value.Properties[i] = ExportOpenAPITypeToSchema(t) + } + if ty.Items != nil { + s.Value.Items = ExportOpenAPITypeToSchema(ty.Items) + } + if ty.AdditionalProperties != nil { + s.Value.AdditionalProperties = openapi3.AdditionalProperties{ + Schema: ExportOpenAPITypeToSchema(ty.AdditionalProperties), + } + } + if ty.Examples != nil && len(ty.Examples) > 0 { + s.Value.Example = ty.Examples + } + if len(ty.ExternalDocs) > 0 { + s.Value.ExternalDocs = &openapi3.ExternalDocs{ + Description: ty.ExternalDocs, + } + } + return s +} + +// ExportSwaggerV2Spec extracts the swagger v2 representation of a kcl package +func ExportSwaggerV2Spec(pkgPath string) (*SwaggerV2Spec, error) { pkg, err := kpm.GetKclPackage(pkgPath) if err != nil { return nil, fmt.Errorf("filePath is not a KCL package: %s", err) @@ -47,12 +94,41 @@ func KclPackageToSwaggerV2Spec(pkgPath string) (*SwaggerV2Spec, error) { for _, t := range p { id := SchemaId(packagePath, t.KclType) spec.Definitions[id] = GetKclOpenAPIType(packagePath, t.KclType, false) - fmt.Println(fmt.Sprintf("exporting openAPI spec from schema %s", id)) + fmt.Printf("exporting openAPI spec from schema %s\n", id) } } return spec, nil } +// SwaggerV2TotOpenAPIV3Spec converts swagger v2 spec to open api v3 spec. +func SwaggerV2TotOpenAPIV3Spec(s *SwaggerV2Spec) *openapi3.T { + t := &openapi3.T{ + OpenAPI: "3.0", + Info: &openapi3.Info{ + Title: s.Info.Title, + Description: s.Info.Description, + Version: s.Info.Version, + }, + Paths: &openapi3.Paths{}, + Components: &openapi3.Components{ + Schemas: make(openapi3.Schemas), + }, + } + for i, d := range s.Definitions { + t.Components.Schemas[i] = ExportOpenAPITypeToSchema(d) + } + return t +} + +// ExportSwaggerV2SpecString exports swagger v2 spec of a kcl package +func ExportSwaggerV2SpecString(pkgPath string) (string, error) { + spec, err := ExportSwaggerV2Spec(pkgPath) + if err != nil { + return "", err + } + return jsonString(spec), nil +} + // SwaggerV2Spec defines KCL OpenAPI Spec based on Swagger v2.0 type SwaggerV2Spec struct { Definitions map[string]*KclOpenAPIType `json:"definitions"` @@ -151,7 +227,7 @@ type KclExample struct { // KclExtensions defines all the KCL specific extensions patched to OpenAPI type KclExtensions struct { XKclModelType *XKclModelType `json:"x-kcl-type,omitempty"` - XKclDecorators *XKclDecorators `json:"x-kcl-decorators,omitempty"` + XKclDecorators XKclDecorators `json:"x-kcl-decorators,omitempty"` XKclUnionTypes []*KclOpenAPIType `json:"x-kcl-union-types,omitempty"` XKclDictKeyType *KclOpenAPIType `json:"x-kcl-dict-key-type,omitempty"` // dict key type } @@ -169,10 +245,13 @@ type KclModelImportInfo struct { } // XKclDecorators defines the `x-kcl-decorators` extension -type XKclDecorators struct { - Name string - Arguments []string - Keywords map[string]string +type XKclDecorators []*XKclDecorator + +// XKclDecorator definition +type XKclDecorator struct { + Name string `json:"name,omitempty"` + Arguments []string `json:"arguments,omitempty"` + Keywords map[string]string `json:"keywords,omitempty"` } // GetKclTypeName get the string representation of a KclOpenAPIType @@ -263,6 +342,33 @@ func (tpe *KclOpenAPIType) GetSchemaPkgDir(base string) string { return GetPkgDir(base, tpe.KclExtensions.XKclModelType.Import.Package) } +func (tpe *KclOpenAPIType) GetExtensionsMapping() map[string]interface{} { + m := make(map[string]interface{}) + if tpe.KclExtensions != nil { + if tpe.XKclModelType != nil { + m[ExtensionKclType] = tpe.XKclModelType + } + if tpe.XKclDecorators != nil { + m[ExtensionKclDecorators] = tpe.XKclDecorators + } + if tpe.XKclUnionTypes != nil { + m[ExtensionKclUnionTypes] = tpe.XKclUnionTypes + } + if tpe.XKclDictKeyType != nil { + m[ExtensionKclDictKeyType] = tpe.XKclDictKeyType + } + } + return m +} + +func (tpe *KclOpenAPIType) GetAnyEnum() []interface{} { + e := make([]interface{}, 0) + for _, v := range tpe.Enum { + e = append(e, v) + } + return e +} + func GetPkgDir(base string, pkgName string) string { return filepath.Join(append([]string{base}, strings.Split(pkgName, ".")...)...) } @@ -273,6 +379,20 @@ func GetKclOpenAPIType(pkgPath string, from *kcl.KclType, nested bool) *KclOpenA Description: from.Description, Default: from.Default, } + // Get decorators + decorators := from.GetDecorators() + if len(decorators) > 0 { + t.KclExtensions = &KclExtensions{ + XKclDecorators: make(XKclDecorators, 0), + } + } + for _, d := range decorators { + t.KclExtensions.XKclDecorators = append(t.KclExtensions.XKclDecorators, &XKclDecorator{ + Name: d.Name, + Arguments: d.Arguments, + Keywords: d.Keywords, + }) + } switch from.Type { case typInt: t.Type = Integer @@ -302,8 +422,13 @@ func GetKclOpenAPIType(pkgPath string, from *kcl.KclType, nested bool) *KclOpenA case typDict: t.Type = Object t.AdditionalProperties = GetKclOpenAPIType(pkgPath, from.Item, true) - t.KclExtensions = &KclExtensions{ - XKclDictKeyType: GetKclOpenAPIType(pkgPath, from.Key, true), + ty := GetKclOpenAPIType(pkgPath, from.Key, true) + if t.KclExtensions == nil { + t.KclExtensions = &KclExtensions{ + XKclDictKeyType: ty, + } + } else { + t.KclExtensions.XKclDictKeyType = ty } return &t case typSchema: @@ -322,14 +447,19 @@ func GetKclOpenAPIType(pkgPath string, from *kcl.KclType, nested bool) *KclOpenA } t.Required = from.Required packageName := PackageName(pkgPath, from) - t.KclExtensions = &KclExtensions{ - XKclModelType: &XKclModelType{ - Import: &KclModelImportInfo{ - Package: packageName, - Alias: filepath.Base(from.Filename), - }, - Type: from.SchemaName, + ty := &XKclModelType{ + Import: &KclModelImportInfo{ + Package: packageName, + Alias: filepath.Base(from.Filename), }, + Type: from.SchemaName, + } + if t.KclExtensions == nil { + t.KclExtensions = &KclExtensions{ + XKclModelType: ty, + } + } else { + t.KclExtensions.XKclModelType = ty } t.Examples = make(map[string]KclExample, len(from.GetExamples())) for name, example := range from.GetExamples() { @@ -339,7 +469,6 @@ func GetKclOpenAPIType(pkgPath string, from *kcl.KclType, nested bool) *KclOpenA Value: example.Value, } } - // todo newT.KclExtensions.XKclDecorators = from.Decorators // todo externalDocs(see also) return &t case typUnion: @@ -348,8 +477,12 @@ func GetKclOpenAPIType(pkgPath string, from *kcl.KclType, nested bool) *KclOpenA for i, unionType := range from.UnionTypes { tps[i] = GetKclOpenAPIType(pkgPath, unionType, true) } - t.KclExtensions = &KclExtensions{ - XKclUnionTypes: tps, + if t.KclExtensions == nil { + t.KclExtensions = &KclExtensions{ + XKclUnionTypes: tps, + } + } else { + t.KclExtensions.XKclUnionTypes = tps } return &t default: @@ -382,12 +515,10 @@ func GetKclOpenAPIType(pkgPath string, from *kcl.KclType, nested bool) *KclOpenA } panic(fmt.Errorf("unexpected KCL type: %s", from.Type)) } - return &t } // PackageName resolves the package name from the PkgPath and the PkgRoot of the type func PackageName(pkgPath string, t *kcl.KclType) string { - // todo after kpm support the correct pkgPath recursively in KclType, refactor the following logic // pkgPath is the relative path to the package root path // t.PkgPath is the "." joined path from the package root diff --git a/pkg/tools/gen/genopenapi_test.go b/pkg/tools/gen/genopenapi_test.go index 5396e568..04e2b76e 100644 --- a/pkg/tools/gen/genopenapi_test.go +++ b/pkg/tools/gen/genopenapi_test.go @@ -1,19 +1,41 @@ package gen import ( - assert2 "github.com/stretchr/testify/assert" "os" "path/filepath" "testing" + + assert2 "github.com/stretchr/testify/assert" ) +func TestExportOpenAPIV3Spec(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal("get work directory failed") + } + pkgPath := filepath.Join(cwd, "testdata", "openapi", "app") + spec, err := ExportOpenAPIV3Spec(pkgPath) + if err != nil { + t.Fatal(err) + } + json, err := spec.Components.Schemas["models.schema.v1.AppConfiguration"].MarshalJSON() + if err != nil { + t.Fatal(err) + } + got := string(json) + + expect := `{"default":"","description":"AppConfiguration is a developer-centric definition that describes how to run an Application. This application model builds upon a decade of experience at AntGroup running super large scale internal developer platform, combined with best-of-breed ideas and practices from the community.","example":{"Default example":{"value":"# Instantiate an App with a long-running service and its image is \"nginx:v1\"\n\nimport models.schema.v1 as ac\nimport models.schema.v1.workload as wl\nimport models.schema.v1.workload.container as c\n\nappConfiguration = ac.AppConfiguration {\n workload: wl.Service {\n containers: {\n \"nginx\": c.Container {\n image: \"nginx:v1\"\n }\n }\n }\n}"}},"properties":{"annotations":{"additionalProperties":{"default":"","type":"string"},"default":"","description":"Annotations are key/value pairs that attach arbitrary non-identifying metadata to resources.","type":"object","x-kcl-decorators":[{"name":"info","keywords":{"hidden":"True"}}],"x-kcl-dict-key-type":{"type":"string"}},"database":{"$ref":"#/definitions/models.schema.v1.accessories.Database"},"labels":{"additionalProperties":{"default":"","type":"string"},"default":"","description":"Labels can be used to attach arbitrary metadata as key-value pairs to resources.","type":"object","x-kcl-decorators":[{"name":"info","keywords":{"hidden":"True"}}],"x-kcl-dict-key-type":{"type":"string"}},"monitoring":{"$ref":"#/definitions/models.schema.v1.monitoring.Prometheus"},"opsRule":{"$ref":"#/definitions/models.schema.v1.trait.OpsRule"},"workload":{"default":"","description":"Workload defines how to run your application code. Currently supported workload profile\nincludes Service and Job.","type":"object","x-kcl-union-types":[{"description":"Service is a kind of workload profile that describes how to run your application code. This is typically used for long-running web applications that should \"never\" go down, and handle short-lived latency-sensitive web requests, or events.","ref":"#/definitions/models.schema.v1.workload.Service"},{"description":"Job is a kind of workload profile that describes how to run your application code. This is typically used for tasks that take from a few seconds to a few days to complete.","ref":"#/definitions/models.schema.v1.workload.Job"}]}},"required":["workload"],"type":"object","x-kcl-type":{"type":"AppConfiguration","import":{"package":"models.schema.v1","alias":"app_configuration.k"}}}` + + assert2.Equal(t, expect, got) +} + func TestExportSwaggerV2Spec(t *testing.T) { cwd, err := os.Getwd() if err != nil { t.Fatal("get work directory failed") } pkgPath := filepath.Join(cwd, "testdata", "doc", "k8s") - got, err := ExportSwaggerV2Spec(pkgPath) + got, err := ExportSwaggerV2SpecString(pkgPath) if err != nil { t.Fatal(err) } diff --git a/pkg/tools/gen/testdata/doc/k8s/kcl.mod b/pkg/tools/gen/testdata/doc/k8s/kcl.mod index e69de29b..c51d42ca 100644 --- a/pkg/tools/gen/testdata/doc/k8s/kcl.mod +++ b/pkg/tools/gen/testdata/doc/k8s/kcl.mod @@ -0,0 +1,2 @@ +[package] + diff --git a/pkg/tools/gen/testdata/doc/pkg/kcl.mod b/pkg/tools/gen/testdata/doc/pkg/kcl.mod index e69de29b..c51d42ca 100644 --- a/pkg/tools/gen/testdata/doc/pkg/kcl.mod +++ b/pkg/tools/gen/testdata/doc/pkg/kcl.mod @@ -0,0 +1,2 @@ +[package] + diff --git a/pkg/tools/gen/testdata/doc/reimport/kcl.mod b/pkg/tools/gen/testdata/doc/reimport/kcl.mod index e69de29b..c51d42ca 100644 --- a/pkg/tools/gen/testdata/doc/reimport/kcl.mod +++ b/pkg/tools/gen/testdata/doc/reimport/kcl.mod @@ -0,0 +1,2 @@ +[package] + diff --git a/pkg/tools/gen/testdata/openapi/app/kcl.mod b/pkg/tools/gen/testdata/openapi/app/kcl.mod new file mode 100644 index 00000000..c51d42ca --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/kcl.mod @@ -0,0 +1,2 @@ +[package] + diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/accessories/database.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/accessories/database.k new file mode 100644 index 00000000..35af56e0 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/accessories/database.k @@ -0,0 +1,94 @@ +schema Database: + """ As an important supporting accessory, Database describes the attributes + to locally deploy or create a cloud provider managed database instance for + the workload. + + Attributes + ---------- + type: str, default is Undefined, required. + Type defines the local deployment mode or the specific cloud vendor that + provides the relational database service (rds). + engine: str, default is Undefined, required. + Engine defines the database engine to use. + version: str, default is Undefined, required. + Version defines the database engine version to use. + instanceType: str, default is Undefined, optional. + InstanceType defines the type of the database which is required when + creating an rds instance provided by the cloud vendor. + size: int, default is 10, optional. + Size defines the allocated storage size of the rds instance provided by + the cloud vendor in GB. + category: str, default is "Basic", optional. + Category defines the edition of the rds instance provided by the cloud + vendor. + username: str, default is "root", optional. + Username defines the operation account for the database. + securityIPs: [str], default is ["0.0.0.0/0"], optional. + SecurityIPs defines the list of IP addresses allowed to access the rds + instance provided by the cloud vendor. + subnetID: str, default is Undefined, optional. + SubnetID defines the virtual subnet ID associated with the VPC that the rds + instance will be created in. The rds instance won't be created in user's own VPC + if this field is not provided. + privateRouting: bool, default is True, optional. + PrivateRouting defines whether the host address of the rds instance for the workload + to connect with is via public network or private network of the cloud vendor. + extraMap: {str:str}, default is Undefined, optional. + ExtraMap defines the diversified rds configuration items from different + cloud vendors. + + Examples + -------- + Instantiate an aws rds with mysql 5.7. + + import models.schema.v1.accessories.database as db + + database: db.Database { + type: "aws" + engine: "mysql" + version: "5.7" + instanceType: "db.t3.micro" + } + """ + + # The local deployment mode or the specific cloud vendor that provides the + # relational database service (rds). + type: str + + # The database engine to use. + engine: str + + # The database engine version to use. + version: str + + # The type of the database which is required when creating an rds instance + # provided by the cloud vendor. + instanceType?: str + + # The allocated storage size of the rds instance provided by the cloud vendor + # in GB. + size?: int = 10 + + # The edition of the rds instance provided by the cloud vendor. + category?: str = "Basic" + + # The operation account for the database. + username?: str = "root" + + # The list of IP addresses allowed to access the rds instance provided by the + # cloud vendor. + securityIPs?: [str] = ["0.0.0.0/0"] + + # The virtual subnet ID associated with the VPC that the rds instance will be + # created in. + subnetID?: str + + # Whether the host address of the rds instance for the workload to connect with + # is via public network or private network of the cloud vendor. + privateRouting?: bool = True + + # The diversified rds configuration items from different cloud vendors. + extraMap?: {str:str} + + check: + instanceType if type != "local", "instanceType is required for cloud provider managed database" \ No newline at end of file diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/app_configuration.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/app_configuration.k new file mode 100644 index 00000000..7a23c57e --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/app_configuration.k @@ -0,0 +1,63 @@ +import models.schema.v1.workload as wl +import models.schema.v1.trait as t +import models.schema.v1.accessories.database as db +import models.schema.v1.monitoring as m + +schema AppConfiguration: + """ AppConfiguration is a developer-centric definition that describes how to run an Application. + This application model builds upon a decade of experience at AntGroup running super large scale + internal developer platform, combined with best-of-breed ideas and practices from the community. + + Attributes + ---------- + workload: wl.Service | wl.Job, default is Undefined, required. + Workload defines how to run your application code. Currently supported workload profile + includes Service and Job. + monitoring: m.Prometheus, default is Undefined, optional. + Monitoring specifies how to scrape Prometheus metrics for the workload. + database: db.Database, default is Undefined, optional. + Database describes a locally deployed or a cloud provider managed database instance for the workload. + opsRule: t.OpsRule, default is Undefined, optional. + OpsRule specifies collection of rules that will be checked for Day-2 operation. + labels: {str:str}, default is Undefined, optional. + Labels can be used to attach arbitrary metadata as key-value pairs to resources. + annotations: {str:str}, default is Undefined, optional. + Annotations are key/value pairs that attach arbitrary non-identifying metadata to resources. + + Examples + -------- + # Instantiate an App with a long-running service and its image is "nginx:v1" + + import models.schema.v1 as ac + import models.schema.v1.workload as wl + import models.schema.v1.workload.container as c + + appConfiguration = ac.AppConfiguration { + workload: wl.Service { + containers: { + "nginx": c.Container { + image: "nginx:v1" + } + } + } + } + """ + + # Workload defines how to run your application code. + workload: wl.Service | wl.Job + + # Monitoring specifies how to scrape metrics for the workload. + monitoring?: m.Prometheus + + # OpsRule specifies collection of rules that will be checked for Day-2 operation. + opsRule?: t.OpsRule + + # Database describes a locally deployed or a cloud provider managed database instance for the workload. + database?: db.Database + + ###### Other metadata info + # Labels and annotations can be used to attach arbitrary metadata as key-value pairs to resources. + @info(hidden=True) + labels?: {str:str} + @info(hidden=True) + annotations?: {str:str} diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/monitoring/prometheus.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/monitoring/prometheus.k new file mode 100644 index 00000000..ceb35a03 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/monitoring/prometheus.k @@ -0,0 +1,52 @@ +import regex + +schema Prometheus: + """ Prometheus can be used to define monitoring requirements + + Attributes + ---------- + interval: str, default to the Prometheus global scraping interval, optional + The time interval which Prometheus scrapes metrics data. Only applicable when operator mode is set to true. + When operator mode is set to false, the scraping interval can only be set in the scraping job configuration, which kusion does not have permission to manage directly. + timeout: str, default to the Prometheus global scraping timeout, optional + The timeout when Prometheus scrapes metrics data. Only applicable when operator mode is set to true. + When operator mode is set to false, the scraping timeout can only be set in the scraping job configuration, which kusion does not have permission to manage directly. + path: str, default to the Prometheus global scraping path, which should be /metrics if not explicitly set in the Prometheus scrape config, optional + The path to scrape metrics from. + port: str, default to container ports when scraping pod (monitorType is pod), default to service port when scraping service (monitorType is service), optional + The port to scrape metrics from. When using Prometheus operator, this needs to be the port NAME. Otherwise, this can be a port name or a number. + scheme: str, default to http, optional + The scheme to scrape metrics from. Possible values are http and https. + + Examples + -------- + import models.schema.v1.monitoring as m + + monitoring: m.Prometheus{ + interval: "30s" + timeout: "15s" + path: "/metrics" + port: "web" + scheme: "http" + } + """ + + # Interval defines the time interval Prometheus scrapes for metrics. If not provided, this will default to the global scraping configuration for that Prometheus instance. + interval?: str + + # Timeout defines the time interval before Prometheus considers the scraping timed out. If not provided, this will default to the global scraping configuration for that Prometheus instance. + timeout?: str + + # Path defines the path from which Prometheus scrapes the target. + path?: str + + # Path defines the port from which Prometheus scrapes the target. + port?: str + + # Scheme defines the protocol Prometheus scrapes for metrics. Possible values are http and https. + scheme?: "http" | "https" = "http" + + check: + regex.match(interval, r"[A-Za-z0-9_.-]*"), "a valid interval must starts with alphanumeric and end with y/w/d/h/m/s/ms" + regex.match(timeout, r"[A-Za-z0-9_.-]*"), "a valid timeout must starts with alphanumeric and end with y/w/d/h/m/s/ms" + port and path if operatorMode == False, "port and path must be set only when operator mode is false" \ No newline at end of file diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/trait/opsrule.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/trait/opsrule.k new file mode 100644 index 00000000..a6001267 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/trait/opsrule.k @@ -0,0 +1,22 @@ +schema OpsRule: + """ OpsRule describes operation rules for various Day-2 Operations. Once declared, these + operation rules will be checked before any Day-2 operations. + + Attributes + ---------- + maxUnavailable: str or int, default is Undefined, optional. + The maximum percentage of the total pod instances in the component that can be + simultaneously unhealthy. + + Examples + -------- + import models.schema.v1.trait as t + + opsRule = t.OpsRule { + maxUnavailable: "30%" + } + """ + + # The maximum percentage of the total pod instances in the component that can be + # simultaneously unhealthy. + maxUnavailable?: int | str = "25%" \ No newline at end of file diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/common.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/common.k new file mode 100644 index 00000000..af8f344b --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/common.k @@ -0,0 +1,36 @@ +import models.schema.v1.workload.container as c +import models.schema.v1.workload.secret as sec + +schema WorkloadBase: + """ WorkloadBase defines set of attributes shared by different workload profile, e.g Service + and Job. You can inherit this Schema to reuse these common attributes. + + Attributes + ---------- + containers: {str:c.Container}, default is Undefined, required. + Containers defines the templates of containers to be ran. + More info: https://kubernetes.io/docs/concepts/containers + secrets: {str:sec.BasicAuthSecret|sec.TokenSecret|sec.OpaqueSecret|sec.TLSSecret|sec.ExternalSecret}, + default is Undefined, optional. + Secrets can be used to store small amount of sensitive data e.g. password, token + replicas: int, default is 2, required. + Number of container replicas based on this configuration that should be ran. + labels: {str:str}, default is Undefined, optional. + Labels are key/value pairs that are attached to the workload. + annotations: {str:str}, default is Undefined, optional. + Annotations are key/value pairs that attach arbitrary non-identifying metadata to the workload. + """ + + # The templates of containers to be ran. + containers: {str:c.Container} + # Secrets store small amount of sensitive data such as a password, a token, or a key. + secrets?: {str:sec.Secret} + + # The number of containers that should be ran. + # Default is 2 to meet high availability requirements. + replicas: int = 2 + + ###### Other metadata info + # Labels and annotations can be used to attach arbitrary metadata as key-value pairs to resources. + labels?: {str:str} + annotations?: {str:str} diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/container.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/container.k new file mode 100644 index 00000000..6cb0ce98 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/container.k @@ -0,0 +1,146 @@ +import models.schema.v1.workload.container.probe as p +import models.schema.v1.workload.container.lifecycle as lc + +import regex + +schema Container: + """ Container describes how the Application's tasks are expected to be run. Depending on + the replicas parameter 1 or more containers can be created from each template. + + Attributes + ---------- + image: str, default is Undefined, required. + Image refers to the Docker image name to run for this container. + More info: https://kubernetes.io/docs/concepts/containers/images + command: [str], default is Undefined, optional. + Entrypoint array. Not executed within a shell. + Command will overwrite the ENTRYPOINT value set in the Dockfile, otherwise the Docker + image's ENTRYPOINT is used if this is not provided. + args: [str], default is Undefined, optional. + Arguments to the entrypoint. + Args will overwrite the CMD value set in the Dockfile, otherwise the Docker + image's CMD is used if this is not provided. + env: {str:str}, default is Undefined, optional. + List of environment variables to set in the container. + The value of the environment variable may be static text or a value from a secret. + workingDir: str, default is Undefined, optional. + The working directory of the running process defined in entrypoint. + Default container runtime will be used if this is not specified. + resources: {str:str}, default is Undefined, optional. + Map of resource requirements the container should run with. + The resources parameter is a dict with the key being the resource name and the value being + the resource value. + files: {str:str}, default is Undefined, optional. + List of files to create in the container. + The files parameter is a dict with the key being the file name in the container and the value + being the target file specification. + dirs: {str:str}, default is Undefined, optional. + Collection of volumes mount into the container's filesystem. + The dirs parameter is a dict with the key being the folder name in the container and the value + being the referenced volume. + livenessProbe: p.Probe, default is Undefined, optional. + LivenessProbe indicates if a running process is healthy. + Container will be restarted if the probe fails. + readinessProbe: p.Probe, default is Undefined, optional. + ReadinessProbe indicates whether an application is available to handle requests. + startupProbe: p.Probe, default is Undefined, optional. + StartupProbe indicates that the container has started for the first time. + Container will be restarted if the probe fails. + lifecycle: lc.Lifecycle, default is Undefined, optional. + Lifecycle refers to actions that the management system should take in response to container lifecycle events. + + Examples + -------- + import models.schema.v1.workload.container as c + + web = c.Container { + image: "nginx:latest" + command: ["/bin/sh", "-c", "echo hi"] + env: { + "name": "value" + } + resources: { + "cpu": "2" + "memory": "4Gi" + } + } + """ + + # Image to run for this container. + image: str + + # Entrypoint array. + # The image's ENTRYPOINT is used if this is not provided. + command?: [str] + # Arguments to the entrypoint. + # The image's CMD is used if this is not provided. + args?: [str] + # Collection of environment variables to set in the container. + # The value of environment variable may be static text or a value from a secret. + env?: {str:str} + # The current working directory of the running process defined in entrypoint. + workingDir?: str + + # Resource requirements for this container. + resources?: {str:str} + + # Files configures one or more files to be created in the container. + files?: {str:FileSpec} + # Dirs configures one or more volumes to be mounted to the specified folder. + dirs?: {str:str} + + # Liveness probe for this container. + # Liveness probe indicates if a running process is healthy. + livenessProbe?: p.Probe + # Readiness probe for this container. + # Readiness probe indicates whether an application is available to handle requests. + readinessProbe?: p.Probe + # Startup probe for this container. + # Startup probe indicates that the container has started for the first time. + startupProbe?: p.Probe + + # Lifecycle configures actions which should be taken response to container lifecycle + # events. + lifecycle?: lc.Lifecycle + + check: + all e in env { + regex.match(e, r"^[-._a-zA-Z][-._a-zA-Z0-9]*$") + } if env, "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit" + +schema FileSpec: + """ FileSpec defines the target file in a Container. + + Attributes + ---------- + content: str, default is Undefined, optional. + File content in plain text. + contentFrom: str, default is Undefined, optional. + Source for the file content, reference to a secret of configmap value. + mode: str, default is Undefined, optional. + Mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511 + + Examples + -------- + import models.schema.v1.workload.container as c + + tmpFile = c.FileSpec { + content: "some file contents" + mode: "0777" + } + """ + + # The content of target file in plain text. + content?: str + + # Source for the file content, might be a reference to a secret value. + contentFrom?: str + + # Mode bits used to set permissions on this file. + # Defaults to 0644. + mode: str = "0644" + + check: + not content or not contentFrom, "content and contentFrom are mutually exclusive" + regex.match(mode, r"^[0-7]{3,4}$"), "valid mode must between 0000 and 0777, both inclusive" diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/lifecycle/lifecycle.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/lifecycle/lifecycle.k new file mode 100644 index 00000000..cbd38a02 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/lifecycle/lifecycle.k @@ -0,0 +1,36 @@ +import models.schema.v1.workload.container.probe as p + +schema Lifecycle: + """ Lifecycle describes actions that the management system should take in response + to container lifecycle events. + + Attributes + ---------- + preStop: p.Exec | p.Http, default is Undefined, optional. + The action to be taken before a container is terminated due to an API request or + management event such as liveness/startup probe failure, preemption, resource contention, etc. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + postStart: p.Exec | p.Http, default is Undefined, optional. + The action to be taken after a container is created. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + + Examples + -------- + import models.schema.v1.workload.container.probe as p + import models.schema.v1.workload.container.lifecycle as lc + + lifecycleHook = lc.Lifecycle { + preStop: p.Exec { + command: ["preStop.sh"] + } + postStart: p.Http { + url: "http://localhost:80" + } + } + """ + + # The action to be taken before a container is terminated. + preStop?: p.Exec | p.Http + + # The action to be taken after a container is created. + postStart?: p.Exec | p.Http diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/probe/probe.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/probe/probe.k new file mode 100644 index 00000000..95a982f8 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/container/probe/probe.k @@ -0,0 +1,142 @@ +import regex + +schema Probe: + """ Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. There are three probe types: readiness, liveness, and startup. + + Attributes + ---------- + probeHandler: Exec | Http | Tcp, default is Undefined, required. + The action taken to determine the alive or health of a container + initialDelaySeconds: int, default is Undefined, optional. + The number of seconds before health checking is activated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + timeoutSeconds: int, default is Undefined, optional. + The number of seconds after which the probe times out. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + periodSeconds: int, default is Undefined, optional. + How often (in seconds) to perform the probe. + successThreshold: int, default is Undefined, optional. + Minimum consecutive successes for the probe to be considered successful after having failed. + failureThreshold: int, default is Undefined, optional. + Minimum consecutive failures for the probe to be considered failed after having succeeded. + terminationGracePeriod: int, default is Undefined, optional. + Duration in seconds before terminate gracefully upon probe failure. + + Examples + -------- + import models.schema.v1.workload.container.probe as p + + probe = p.Probe { + probeHandler: p.Http { + path: "/healthz" + } + initialDelaySeconds: 10 + } + """ + + # The action taken to determine the health of a container + probeHandler: Exec | Http | Tcp + + # Number of seconds after the container has started before liveness probes are initiated. + initialDelaySeconds?: int + + # Number of seconds after which the probe times out. + timeoutSeconds?: int + + # How often (in seconds) to perform the probe. + periodSeconds?: int + + # Minimum consecutive successes for the probe to be considered successful after having failed. + successThreshold?: int + + # Minimum consecutive failures for the probe to be considered failed after having succeeded. + failureThreshold?: int + + # Duration in seconds before terminate gracefully upon probe failure. + terminationGracePeriod?: int + + check: + initialDelaySeconds >= 0 if initialDelaySeconds, "initialDelaySeconds must be greater than or equal to 0" + timeoutSeconds >= 0 if timeoutSeconds, "timeoutSeconds must be greater than or equal to 0" + periodSeconds >= 0 if periodSeconds, "periodSeconds must be greater than or equal to 0" + successThreshold >= 0 if successThreshold, "successThreshold must be greater than or equal to 0" + failureThreshold >= 0 if failureThreshold, "failureThreshold must be greater than or equal to 0" + terminationGracePeriod >= 0 if terminationGracePeriod, "terminationGracePeriod must be greater than or equal to 0" + +schema Exec: + """ Exec describes a "run in container" action. + + Attributes + ---------- + command: str, default is Undefined, required. + The command line to execute inside the container. + + Examples + -------- + import models.schema.v1.workload.container.probe as p + + execProbe = p.Exec { + command: ["probe.sh"] + } + """ + + # The command line to execute inside the container. + # Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + command: [str] + + check: + len(command) > 0, "command must be specified" + +schema Http: + """ Http describes an action based on HTTP Get requests. + + Attributes + ---------- + url: str, default is Undefined, required. + The full qualified url to send HTTP requests. + headers: {str:str}, default is Undefined, optional. + Collection of custom headers to set in the request + + Examples + -------- + import models.schema.v1.workload.container.probe as p + + httpProbe = p.Http { + url: "http://localhost:80" + headers: { + "X-HEADER": "VALUE" + } + } + """ + + # The full qualified url to send HTTP requests. + url: str + + # Custom headers to set in the request. + headers?: {str:str} + + check: + all header in headers { + regex.match(header, r"^[-A-Za-z0-9]+$") + } if headers, "a valid HTTP header must consist of alphanumeric characters or '-' e.g X-Header-Name" + +schema Tcp: + """ Tcp describes an action based on opening a socket. + + Attributes + ---------- + url: str, default is Undefined, required. + The full qualified url to open a socket. + + Examples + -------- + import models.schema.v1.workload.container.probe as p + + tcpProbe = p.Tcp { + url: "tcp://localhost:1234" + } + """ + + # The full qualified url to open a socket. + url: str diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/job.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/job.k new file mode 100644 index 00000000..6f7c8955 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/job.k @@ -0,0 +1,30 @@ +schema Job(WorkloadBase): + """ Job is a kind of workload profile that describes how to run your application code. This + is typically used for tasks that take from a few seconds to a few days to complete. + + Attributes + ---------- + schedule: str, default is Undefined, required. + The scheduling strategy in Cron format. More info: https://en.wikipedia.org/wiki/Cron. + + Examples + -------- + Instantiate a job with busybox image and runs every hour + + import models.schema.v1.workload as wl + import models.schema.v1.workload.container as c + + job: wl.Job { + containers: { + "busybox": c.Container{ + image: "busybox:1.28" + command: ["/bin/sh", "-c", "echo hello"] + } + } + schedule: "0 * * * *" + } + """ + + # The scheduling strategy in Cron format. + # More info: https://en.wikipedia.org/wiki/Cron. + schedule: str diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/network/port.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/network/port.k new file mode 100644 index 00000000..c7beb2b3 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/network/port.k @@ -0,0 +1,42 @@ +schema Port: + """ Port defines the exposed port of Service, which can be used to describe how the Service + get accessed. + + Attributes + ---------- + port: int, default is 80, required. + The exposed port of the Service. + targetPort: int, default is Undefined, optional. + The backend container port. If empty, set it the same as the port. + protocol: "TCP" | "UDP", default is "TCP", required. + The protocol to access the port. + public: bool, default is False, required. + Public defines whether the port can be accessed through Internet. + + Examples + -------- + import models.schema.v1.workload.network as n + + port = n.Port { + port: 80 + targetPort: 8080 + protocol: "TCP" + public: True + } + """ + + # The exposed port of the Service. + port: int = 80 + + # The backend container port. + targetPort?: int + + # The protocol of port. + protocol: "TCP" | "UDP" = "TCP" + + # Public defines whether to expose the port through Internet. + public: bool = False + + check: + 1 <= port <= 65535, "port must be between 1 and 65535, inclusive" + 1 <= targetPort <= 65535 if targetPort, "targetPort must be between 1 and 65535, inclusive" \ No newline at end of file diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/secret/secret.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/secret/secret.k new file mode 100644 index 00000000..aff5e31a --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/secret/secret.k @@ -0,0 +1,57 @@ +import regex + +# mapping between secret type and valid data key +SECRET_TYPE_DATA_MAPPING: {str:[str]} = { + "basic": ["username", "password"] + "token": ["token"] + "certificate": ["tls.crt", "tls.key"] +} + +schema Secret: + """ Secrets are used to provide data that is considered sensitive like passwords, API keys, + TLS certificates, tokens or other credentials. + + Attributes + ---------- + type: str, default is Undefined, required. + Type of secret, used to facilitate programmatic handling of secret data. + params: {str,str}, default is Undefined, optional. + Collection of parameters used to facilitate programmatic handling of secret data. + data: {str,str}, default is Undefined, optional. + Data contains the non-binary secret data in string form. + immutable: bool, default is Undefined, optional. + Immutable, if set to true, ensures that data stored in the Secret cannot be updated. + + Examples + -------- + import models.schema.v1.workload.secret as sec + + basicAuth = sec.Secret { + type: "basic" + data: { + "username": "" + "password": "" + } + } + """ + + # Types of secrets available to use. + type: "basic" | "token" | "opaque" | "certificate" | "external" + + # Params defines extra parameters used to customize secret handling. + params?: {str:str} + + # Data defines the keys and data that will be used by secret. + data?: {str:str} + + # If immutable set to true, ensures that data stored in the Secret cannot be updated. + immutable?: bool + + check: + all k in data { + regex.match(k, r"[A-Za-z0-9_.-]*") + } if data, "a valid secret data key must consist of alphanumeric characters, '-', '_' or '.'" + all k in data { + k in SECRET_TYPE_DATA_MAPPING[type] if len(SECRET_TYPE_DATA_MAPPING[type]) > 0 + } if data, "a valid secret data key name must be one of ${SECRET_TYPE_DATA_MAPPING[type]} for ${type} type secret" + diff --git a/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/service.k b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/service.k new file mode 100644 index 00000000..25e6be19 --- /dev/null +++ b/pkg/tools/gen/testdata/openapi/app/models/schema/v1/workload/service.k @@ -0,0 +1,39 @@ +import models.schema.v1.workload.network as n + +schema Service(WorkloadBase): + """ Service is a kind of workload profile that describes how to run your application code. This + is typically used for long-running web applications that should "never" go down, and handle + short-lived latency-sensitive web requests, or events. + + Attributes + ---------- + ports: [n.Port], default is Undefined, optional. + The list of ports which the Service should get exposed. + + Examples + -------- + # Instantiate a long-running service and its image is "nginx:v1" + + import models.schema.v1.workload as wl + import models.schema.v1.workload.container as c + + svc = wl.Service { + containers: { + "nginx": c.Container { + image: "nginx:v1" + } + } + ports: [ + n.Port { + port: 80 + public: True + } + n.Port { + port: 9090 + } + ] + } + """ + + # The list of ports get exposed. + ports?: [n.Port]