From ec9b0030b2901f810ab7de1de47057dee7e413ca Mon Sep 17 00:00:00 2001 From: ahmedrowaihi Date: Mon, 19 May 2025 02:40:39 +0300 Subject: [PATCH 1/8] Implement OpenAPI specification and Scalar API reference page --- go.mod | 15 +- go.sum | 44 +-- pkg/api/openapi.go | 825 +++++++++++++++++++++++++++++++++++++++++++++ pkg/api/scalar.go | 59 ++++ pkg/api/server.go | 2 + 5 files changed, 920 insertions(+), 25 deletions(-) create mode 100644 pkg/api/openapi.go create mode 100644 pkg/api/scalar.go diff --git a/go.mod b/go.mod index 390fa24c2d..85fb5f435b 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,8 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/docker/docker v28.1.1+incompatible github.com/docker/go-connections v0.5.0 - github.com/go-chi/chi/v5 v5.2.1 + github.com/getkin/kin-openapi v0.132.0 + github.com/go-chi/chi/v5 v5.0.12 github.com/gofrs/flock v0.12.1 github.com/google/go-containerregistry v0.20.3 github.com/google/uuid v1.6.0 @@ -43,9 +44,9 @@ require ( github.com/extism/go-sdk v1.7.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -56,15 +57,19 @@ require ( github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect diff --git a/go.sum b/go.sum index 232a7a881c..91afca6100 100644 --- a/go.sum +++ b/go.sum @@ -22,7 +22,6 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -57,8 +56,10 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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= @@ -66,16 +67,16 @@ 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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 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= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -123,11 +124,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -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/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= @@ -144,8 +142,8 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= @@ -161,12 +159,18 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= @@ -177,6 +181,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -213,15 +219,11 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -238,6 +240,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/api/openapi.go b/pkg/api/openapi.go new file mode 100644 index 0000000000..2a852cf6ec --- /dev/null +++ b/pkg/api/openapi.go @@ -0,0 +1,825 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" +) + +var openapiSpec *openapi3.T + +func init() { + openapiSpec = &openapi3.T{ + OpenAPI: "3.1.1", + Info: &openapi3.Info{ + Title: "ToolHive API", + Description: "A REST API for managing ToolHive servers and containers. This API allows you to create, manage, and monitor containerized servers in your ToolHive environment.", + Version: "1.0.0", + TermsOfService: "https://github.com/stacklok/toolhive/blob/main/LICENSE", + Contact: &openapi3.Contact{ + Name: "ToolHive Support", + URL: "https://github.com/stacklok/toolhive", + Email: "support@stacklok.com", + }, + License: &openapi3.License{ + Name: "Apache 2.0", + URL: "http://www.apache.org/licenses/LICENSE-2.0.html", + }, + }, + Servers: openapi3.Servers{ + &openapi3.Server{ + URL: "http://localhost:8080", + Description: "Local development server", + }, + }, + Paths: openapi3.NewPaths(), + Tags: []*openapi3.Tag{ + { + Name: "system", + Description: "System management endpoints", + }, + { + Name: "servers", + Description: "Server management endpoints", + }, + }, + } + + addServerPaths() +} + +func addServerPaths() { + openapiSpec.Paths.Set("/health", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "getHealth", + Summary: "Health check", + Description: "Check if the API is healthy", + Tags: []string{"system"}, + Responses: openapi3.NewResponses(), + }, + }) + + openapiSpec.Paths.Set("/api/v1beta/version", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "getVersion", + Summary: "Get version", + Description: "Get the API version information", + Tags: []string{"system"}, + Responses: openapi3.NewResponses(), + }, + }) + + openapiSpec.Paths.Set("/api/v1beta/servers", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "listServers", + Summary: "List all servers", + Description: "Get a list of all running servers", + Tags: []string{"servers"}, + Parameters: []*openapi3.ParameterRef{ + { + Value: &openapi3.Parameter{ + Name: "all", + In: "query", + Description: "Include stopped servers", + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"boolean"}, + }, + }, + }, + }, + }, + Responses: openapi3.NewResponses(), + }, + Post: &openapi3.Operation{ + OperationID: "createServer", + Summary: "Create a new server", + Description: "Create and start a new server", + Tags: []string{"servers"}, + RequestBody: &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Required: true, + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "name": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Example: "puppeteer", + }, + }, + "image": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Example: "mcp/puppeteer:latest", + }, + }, + "cmd_arguments": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + Example: []string{}, + }, + }, + "target_port": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"integer"}, + Example: 3000, + }, + }, + "env_vars": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + Example: []string{"DOCKER_CONTAINER=true"}, + }, + }, + "secrets": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "name": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "value": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + }, + }, + "volumes": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + "transport": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Example: "stdio", + }, + }, + "authz_config": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "oidc": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "issuer": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "audience": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "jwks_url": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "client_id": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + "permission_profile": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + Required: []string{"name", "image", "target_port"}, + }), + }, + }, + Responses: openapi3.NewResponses(), + }, + }) + + openapiSpec.Paths.Set("/api/v1beta/servers/{name}", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "getServer", + Summary: "Get server details", + Description: "Get details of a specific server", + Tags: []string{"servers"}, + Parameters: []*openapi3.ParameterRef{ + { + Value: &openapi3.Parameter{ + Name: "name", + In: "path", + Required: true, + Description: "Server name", + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + Responses: openapi3.NewResponses(), + }, + Delete: &openapi3.Operation{ + OperationID: "deleteServer", + Summary: "Delete a server", + Description: "Delete a server", + Tags: []string{"servers"}, + Parameters: []*openapi3.ParameterRef{ + { + Value: &openapi3.Parameter{ + Name: "name", + In: "path", + Required: true, + Description: "Server name", + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + { + Value: &openapi3.Parameter{ + Name: "force", + In: "query", + Description: "Force deletion", + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"boolean"}, + }, + }, + }, + }, + }, + Responses: openapi3.NewResponses(), + }, + }) + + openapiSpec.Paths.Set("/api/v1beta/servers/{name}/stop", &openapi3.PathItem{ + Post: &openapi3.Operation{ + OperationID: "stopServer", + Summary: "Stop a server", + Description: "Stop a running server", + Tags: []string{"servers"}, + Parameters: []*openapi3.ParameterRef{ + { + Value: &openapi3.Parameter{ + Name: "name", + In: "path", + Required: true, + Description: "Server name", + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + Responses: openapi3.NewResponses(), + }, + }) + + openapiSpec.Paths.Set("/api/v1beta/servers/{name}/restart", &openapi3.PathItem{ + Post: &openapi3.Operation{ + OperationID: "restartServer", + Summary: "Restart a server", + Description: "Restart a running server", + Tags: []string{"servers"}, + Parameters: []*openapi3.ParameterRef{ + { + Value: &openapi3.Parameter{ + Name: "name", + In: "path", + Required: true, + Description: "Server name", + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + Responses: openapi3.NewResponses(), + }, + }) + + addResponses() +} + +func addResponses() { + healthCheck := openapiSpec.Paths.Find("/health").Get + healthCheck.Responses.Set("204", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("No Content"), + }, + }) + healthCheck.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + + version := openapiSpec.Paths.Find("/api/v1beta/version").Get + version.Responses.Set("200", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("OK"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "version": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + version.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + version.Responses.Set("500", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Internal Server Error"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + + listServers := openapiSpec.Paths.Find("/api/v1beta/servers").Get + listServers.Responses.Set("200", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("OK"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "servers": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "id": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "name": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "image": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "status": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "state": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "created": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Format: "date-time", + }, + }, + "labels": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + AdditionalProperties: openapi3.AdditionalProperties{ + Has: boolPtr(true), + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + "ports": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "container_port": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"integer"}, + }, + }, + "host_port": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"integer"}, + }, + }, + "protocol": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }) + listServers.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + + createServer := openapiSpec.Paths.Find("/api/v1beta/servers").Post + createServer.Responses.Set("201", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Created"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "name": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "port": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"integer"}, + }, + }, + }, + }), + }, + }) + createServer.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + createServer.Responses.Set("400", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Bad Request"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + createServer.Responses.Set("409", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Conflict"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + + getServer := openapiSpec.Paths.Find("/api/v1beta/servers/{name}").Get + getServer.Responses.Set("200", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("OK"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "id": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "name": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "image": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "status": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "state": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + "created": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Format: "date-time", + }, + }, + "labels": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + AdditionalProperties: openapi3.AdditionalProperties{ + Has: boolPtr(true), + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + "ports": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "container_port": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"integer"}, + }, + }, + "host_port": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"integer"}, + }, + }, + "protocol": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }), + }, + }) + getServer.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + getServer.Responses.Set("404", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Not Found"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + + deleteServer := openapiSpec.Paths.Find("/api/v1beta/servers/{name}").Delete + deleteServer.Responses.Set("204", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("No Content"), + }, + }) + deleteServer.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + deleteServer.Responses.Set("404", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Not Found"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + + stopServer := openapiSpec.Paths.Find("/api/v1beta/servers/{name}/stop").Post + stopServer.Responses.Set("204", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("No Content"), + }, + }) + stopServer.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + stopServer.Responses.Set("404", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Not Found"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + + restartServer := openapiSpec.Paths.Find("/api/v1beta/servers/{name}/restart").Post + restartServer.Responses.Set("204", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("No Content"), + }, + }) + restartServer.Responses.Set("405", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Method Not Allowed"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) + restartServer.Responses.Set("404", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: stringPtr("Not Found"), + Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: map[string]*openapi3.SchemaRef{ + "error": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + }, + }, + }, + }), + }, + }) +} + +func stringPtr(s string) *string { + return &s +} + +func boolPtr(b bool) *bool { + return &b +} + +func ServeOpenAPI(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(openapiSpec) +} diff --git a/pkg/api/scalar.go b/pkg/api/scalar.go new file mode 100644 index 0000000000..2cd0e704c6 --- /dev/null +++ b/pkg/api/scalar.go @@ -0,0 +1,59 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" +) + +const scalarHTML = ` + + + ToolHive API Reference + + + + + + + + +` + +// ServeScalar serves the Scalar API reference page +func ServeScalar(w http.ResponseWriter, r *http.Request) { + // Get the OpenAPI specification + spec, err := json.Marshal(openapiSpec) + if err != nil { + http.Error(w, "Failed to marshal OpenAPI specification", http.StatusInternalServerError) + return + } + + // Insert the OpenAPI specification into the HTML template + html := fmt.Sprintf(scalarHTML, spec) + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(html)) +} diff --git a/pkg/api/server.go b/pkg/api/server.go index 8dde9444b1..a7329e3a7d 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -49,6 +49,8 @@ func Serve(ctx context.Context, address string, debugMode bool) error { "/health": v1.HealthcheckRouter(), "/api/v1beta/version": v1.VersionRouter(), "/api/v1beta/servers": v1.ServerRouter(manager, rt, debugMode), + "/openapi.json": http.HandlerFunc(ServeOpenAPI), + "/": http.HandlerFunc(ServeScalar), } for prefix, router := range routers { r.Mount(prefix, router) From ada46639800023e6c8e3e966830438e338c71620 Mon Sep 17 00:00:00 2001 From: ahmedrowaihi Date: Mon, 19 May 2025 02:50:04 +0300 Subject: [PATCH 2/8] chore: golangci-lint run --- pkg/api/openapi.go | 12 ++++++++---- pkg/api/scalar.go | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/api/openapi.go b/pkg/api/openapi.go index 2a852cf6ec..77ff4a5b66 100644 --- a/pkg/api/openapi.go +++ b/pkg/api/openapi.go @@ -13,8 +13,9 @@ func init() { openapiSpec = &openapi3.T{ OpenAPI: "3.1.1", Info: &openapi3.Info{ - Title: "ToolHive API", - Description: "A REST API for managing ToolHive servers and containers. This API allows you to create, manage, and monitor containerized servers in your ToolHive environment.", + Title: "ToolHive API", + Description: "A REST API for managing ToolHive servers and containers. " + + "This API allows you to create, manage, and monitor containerized servers in your ToolHive environment.", Version: "1.0.0", TermsOfService: "https://github.com/stacklok/toolhive/blob/main/LICENSE", Contact: &openapi3.Contact{ @@ -819,7 +820,10 @@ func boolPtr(b bool) *bool { return &b } -func ServeOpenAPI(w http.ResponseWriter, r *http.Request) { +// ServeOpenAPI writes the OpenAPI specification as JSON to the response. +func ServeOpenAPI(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(openapiSpec) + if err := json.NewEncoder(w).Encode(openapiSpec); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } } diff --git a/pkg/api/scalar.go b/pkg/api/scalar.go index 2cd0e704c6..9b183604b1 100644 --- a/pkg/api/scalar.go +++ b/pkg/api/scalar.go @@ -43,7 +43,7 @@ const scalarHTML = ` ` // ServeScalar serves the Scalar API reference page -func ServeScalar(w http.ResponseWriter, r *http.Request) { +func ServeScalar(w http.ResponseWriter, _ *http.Request) { // Get the OpenAPI specification spec, err := json.Marshal(openapiSpec) if err != nil { @@ -55,5 +55,7 @@ func ServeScalar(w http.ResponseWriter, r *http.Request) { html := fmt.Sprintf(scalarHTML, spec) w.Header().Set("Content-Type", "text/html") - w.Write([]byte(html)) + if _, err := w.Write([]byte(html)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } } From 615b68335bde089da48e440258e768dd398620d3 Mon Sep 17 00:00:00 2001 From: ahmedrowaihi Date: Tue, 20 May 2025 15:50:00 +0300 Subject: [PATCH 3/8] Enhance OpenAPI specification and Scalar API reference page - schema sync - Scalar source url - Scalar multiple server --- pkg/api/openapi.go | 134 ++++++++++----------------------------------- pkg/api/scalar.go | 60 +++++++++++--------- 2 files changed, 63 insertions(+), 131 deletions(-) diff --git a/pkg/api/openapi.go b/pkg/api/openapi.go index 77ff4a5b66..5827d41d65 100644 --- a/pkg/api/openapi.go +++ b/pkg/api/openapi.go @@ -116,6 +116,12 @@ func addServerPaths() { Example: "mcp/puppeteer:latest", }, }, + "host": { + Value: &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Example: "localhost", + }, + }, "cmd_arguments": { Value: &openapi3.Schema{ Type: &openapi3.Types{"array"}, @@ -156,7 +162,7 @@ func addServerPaths() { Type: &openapi3.Types{"string"}, }, }, - "value": { + "target": { Value: &openapi3.Schema{ Type: &openapi3.Types{"string"}, }, @@ -424,47 +430,18 @@ func addResponses() { Value: &openapi3.Schema{ Type: &openapi3.Types{"object"}, Properties: map[string]*openapi3.SchemaRef{ - "id": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "name": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "image": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "status": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "state": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "created": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - Format: "date-time", - }, - }, + "id": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "name": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "image": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "status": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "state": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "created": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}, Format: "date-time"}}, "labels": { Value: &openapi3.Schema{ Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{ - Has: boolPtr(true), - Schema: &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, + Has: boolPtr(true), + Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, }, }, }, @@ -475,21 +452,9 @@ func addResponses() { Value: &openapi3.Schema{ Type: &openapi3.Types{"object"}, Properties: map[string]*openapi3.SchemaRef{ - "container_port": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"integer"}, - }, - }, - "host_port": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"integer"}, - }, - }, - "protocol": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, + "container_port": {Value: &openapi3.Schema{Type: &openapi3.Types{"integer"}}}, + "host_port": {Value: &openapi3.Schema{Type: &openapi3.Types{"integer"}}}, + "protocol": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, }, }, }, @@ -594,47 +559,18 @@ func addResponses() { Content: openapi3.NewContentWithJSONSchema(&openapi3.Schema{ Type: &openapi3.Types{"object"}, Properties: map[string]*openapi3.SchemaRef{ - "id": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "name": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "image": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "status": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "state": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, - "created": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - Format: "date-time", - }, - }, + "id": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "name": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "image": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "status": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "state": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "created": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}, Format: "date-time"}}, "labels": { Value: &openapi3.Schema{ Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{ - Has: boolPtr(true), - Schema: &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, + Has: boolPtr(true), + Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, }, }, }, @@ -645,21 +581,9 @@ func addResponses() { Value: &openapi3.Schema{ Type: &openapi3.Types{"object"}, Properties: map[string]*openapi3.SchemaRef{ - "container_port": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"integer"}, - }, - }, - "host_port": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"integer"}, - }, - }, - "protocol": { - Value: &openapi3.Schema{ - Type: &openapi3.Types{"string"}, - }, - }, + "container_port": {Value: &openapi3.Schema{Type: &openapi3.Types{"integer"}}}, + "host_port": {Value: &openapi3.Schema{Type: &openapi3.Types{"integer"}}}, + "protocol": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, }, }, }, diff --git a/pkg/api/scalar.go b/pkg/api/scalar.go index 9b183604b1..adb6284696 100644 --- a/pkg/api/scalar.go +++ b/pkg/api/scalar.go @@ -1,8 +1,6 @@ package api import ( - "encoding/json" - "fmt" "net/http" ) @@ -14,26 +12,46 @@ const scalarHTML = ` - + +