From dec91fe17ffcb43771d8f99af8a26dc746873c26 Mon Sep 17 00:00:00 2001 From: Mahendra Paipuri Date: Sun, 5 May 2024 12:39:31 +0200 Subject: [PATCH 1/2] feat: Add automated swagger docs * Use generics to response model which helps to generate correct model in swagger docs * Add api version in paths * Define a ownership model to return correct response * Use swaggo to generate swagger docs * Add swagger endpoint to interact with swagger docs * Use 403 instead of 401 when regular user is attempting to access admin endpoint Signed-off-by: Mahendra Paipuri --- go.mod | 20 +- go.sum | 44 +- pkg/api/base/base.go | 3 + pkg/api/http/docs/docs.go | 1052 ++++++++++++++++++++++++++++++++ pkg/api/http/docs/swagger.json | 1026 +++++++++++++++++++++++++++++++ pkg/api/http/docs/swagger.yaml | 705 +++++++++++++++++++++ pkg/api/http/error.go | 7 +- pkg/api/http/middleware.go | 21 +- pkg/api/http/server.go | 272 +++++++-- pkg/api/http/server_test.go | 26 +- pkg/api/models/models.go | 7 + 11 files changed, 3092 insertions(+), 91 deletions(-) create mode 100644 pkg/api/http/docs/docs.go create mode 100644 pkg/api/http/docs/swagger.json create mode 100644 pkg/api/http/docs/swagger.yaml diff --git a/go.mod b/go.mod index e88ebffc..35d1d620 100644 --- a/go.mod +++ b/go.mod @@ -15,12 +15,15 @@ require ( github.com/prometheus/exporter-toolkit v0.10.0 github.com/prometheus/procfs v0.12.0 github.com/rotationalio/ensign v0.12.4 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/swaggo/swag v1.16.3 github.com/zeebo/xxh3 v1.0.2 - golang.org/x/sys v0.15.0 + golang.org/x/sys v0.19.0 gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -29,28 +32,37 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/go-units v0.5.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/opencontainers/runtime-spec v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/sirupsen/logrus v1.9.2 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/goleak v1.3.0 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect - golang.org/x/sync v0.5.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.20.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 86cc2666..1da7e03b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= @@ -24,6 +26,14 @@ github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +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.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -45,6 +55,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= @@ -55,6 +67,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= @@ -86,8 +100,14 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= +github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -101,22 +121,24 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -124,8 +146,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -137,6 +159,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= diff --git a/pkg/api/base/base.go b/pkg/api/base/base.go index 61bdada0..47e23d34 100644 --- a/pkg/api/base/base.go +++ b/pkg/api/base/base.go @@ -52,3 +52,6 @@ var ( GrafanaWebSkipTLSVerify bool GrafanaAdminTeamID string ) + +// APIVersion sets the version of API in paths +const APIVersion = "v1" diff --git a/pkg/api/http/docs/docs.go b/pkg/api/http/docs/docs.go new file mode 100644 index 00000000..9439554d --- /dev/null +++ b/pkg/api/http/docs/docs.go @@ -0,0 +1,1052 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Mahendra Paipuri", + "url": "https://github.com/mahendrapaipuri/ceems/issues", + "email": "mahendra.paipuri@gmail.com" + }, + "license": { + "name": "BSD-3-Clause license", + "url": "https://opensource.org/license/bsd-3-clause" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/health": { + "get": { + "description": "get health status of API server", + "produces": [ + "text/plain" + ], + "tags": [ + "health" + ], + "summary": "Health status", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "string" + } + } + } + } + }, + "/projects": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get list of projects that user belong to", + "produces": [ + "application/json" + ], + "tags": [ + "projects" + ], + "summary": "List projects", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Project" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/units": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get units queried by a user", + "produces": [ + "application/json" + ], + "tags": [ + "units" + ], + "summary": "User endpoint for fetching compute units", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Unit UUID", + "name": "uuid", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Project", + "name": "project", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to fetch running units", + "name": "running", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Unit" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/units/admin": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get units for admins that can query units of any user", + "produces": [ + "application/json" + ], + "tags": [ + "units" + ], + "summary": "Admin endpoint for fetching compute units", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Unit UUID", + "name": "uuid", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Project", + "name": "project", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "User name", + "name": "user", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to fetch running units", + "name": "running", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Unit" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/units/verify": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "verify ownership of the unit", + "produces": [ + "application/json" + ], + "tags": [ + "units" + ], + "summary": "Verify unit ownership", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Unit UUID", + "name": "uuid", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Ownership" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/usage/{mode}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get current/global usage statistics of a current user", + "produces": [ + "application/json" + ], + "tags": [ + "usage" + ], + "summary": "Usage statistics", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "enum": [ + "current", + "global" + ], + "type": "string", + "description": "Whether to get usage stats within a period or global", + "name": "mode", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "User name(s)", + "name": "user", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Usage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/usage/{mode}/admin": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get current/global usage statistics of a given user and/or project for admins", + "produces": [ + "application/json" + ], + "tags": [ + "usage" + ], + "summary": "Admin Usage statistics", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "enum": [ + "current", + "global" + ], + "type": "string", + "description": "Whether to get usage stats within a period or global", + "name": "mode", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "User name(s)", + "name": "user", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Usage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/{resource}/demo": { + "get": { + "description": "get units and/or usage response generated by mock data", + "produces": [ + "application/json" + ], + "tags": [ + "demo" + ], + "summary": "Demo Units/Usage endpoints", + "parameters": [ + { + "enum": [ + "units", + "usage" + ], + "type": "string", + "description": "Whether to return mock units or usage data", + "name": "resource", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Usage" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + } + }, + "definitions": { + "http.Response-any": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": {} + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Ownership": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Ownership" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Project": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Project" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Unit": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Unit" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Usage": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Usage" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.errorType": { + "type": "string", + "enum": [ + "", + "unauthorized", + "forbidden", + "timeout", + "canceled", + "execution", + "bad_data", + "internal", + "unavailable", + "not_found", + "not_acceptable" + ], + "x-enum-varnames": [ + "errorNone", + "errorUnauthorized", + "errorForbidden", + "errorTimeout", + "errorCanceled", + "errorExec", + "errorBadData", + "errorInternal", + "errorUnavailable", + "errorNotFound", + "errorNotAcceptable" + ] + }, + "models.Allocation": { + "type": "object", + "additionalProperties": true + }, + "models.Ownership": { + "type": "object", + "properties": { + "owner": { + "type": "boolean" + }, + "user": { + "type": "string" + }, + "uuids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "models.Project": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "models.Tag": { + "type": "object", + "additionalProperties": true + }, + "models.Unit": { + "type": "object", + "properties": { + "allocation": { + "description": "Allocation map of unit. Only string and int64 values are supported in map", + "allOf": [ + { + "$ref": "#/definitions/models.Allocation" + } + ] + }, + "avg_cpu_mem_usage": { + "description": "Average CPU memory during lifetime of unit", + "type": "number" + }, + "avg_cpu_usage": { + "description": "Average CPU usage during lifetime of unit", + "type": "number" + }, + "avg_gpu_mem_usage": { + "description": "Average GPU memory during lifetime of unit", + "type": "number" + }, + "avg_gpu_usage": { + "description": "Average GPU usage during lifetime of unit", + "type": "number" + }, + "created_at": { + "description": "Creation time", + "type": "string" + }, + "created_at_ts": { + "description": "Creation timestamp", + "type": "integer" + }, + "elapsed": { + "description": "Human readable total elapsed time string", + "type": "string" + }, + "ended_at": { + "description": "End time", + "type": "string" + }, + "ended_at_ts": { + "description": "End timestamp", + "type": "integer" + }, + "grp": { + "description": "User group", + "type": "string" + }, + "name": { + "description": "Name of compute unit", + "type": "string" + }, + "project": { + "description": "Account in batch systems, Tenant in Openstack, Namespace in k8s", + "type": "string" + }, + "resource_manager": { + "description": "Name of the resource manager that owns compute unit. Eg slurm, openstack, kubernetes, etc", + "type": "string" + }, + "started_at": { + "description": "Start time", + "type": "string" + }, + "started_at_ts": { + "description": "Start timestamp", + "type": "integer" + }, + "state": { + "description": "Current state of unit", + "type": "string" + }, + "tags": { + "description": "A map to store generic info. String and int64 are valid value types of map", + "allOf": [ + { + "$ref": "#/definitions/models.Tag" + } + ] + }, + "total_cpu_emissions_gms": { + "description": "Total CPU emissions in grams during lifetime of unit", + "type": "number" + }, + "total_cpu_energy_usage_kwh": { + "description": "Total CPU energy usage in kWh during lifetime of unit", + "type": "number" + }, + "total_cputime_seconds": { + "description": "Total number of CPU seconds consumed by the unit", + "type": "integer" + }, + "total_gpu_emissions_gms": { + "description": "Total GPU emissions in grams during lifetime of unit", + "type": "number" + }, + "total_gpu_energy_usage_kwh": { + "description": "Total GPU energy usage in kWh during lifetime of unit", + "type": "number" + }, + "total_gputime_seconds": { + "description": "Total number of GPU seconds consumed by the unit", + "type": "integer" + }, + "total_ingress_in_gb": { + "description": "Total ingress traffic in GB of unit", + "type": "number" + }, + "total_io_read_cold_gb": { + "description": "Total IO read on cold storage in GB during lifetime of unit", + "type": "number" + }, + "total_io_read_hot_gb": { + "description": "Total IO read on hot storage in GB during lifetime of unit", + "type": "number" + }, + "total_io_write_cold_gb": { + "description": "Total IO write on cold storage in GB during lifetime of unit", + "type": "number" + }, + "total_io_write_hot_gb": { + "description": "Total IO write on hot storage in GB during lifetime of unit", + "type": "number" + }, + "total_outgress_in_gb": { + "description": "Total outgress traffic in GB of unit", + "type": "number" + }, + "total_walltime_seconds": { + "description": "Total elapsed wall time in seconds", + "type": "integer" + }, + "usr": { + "description": "Username", + "type": "string" + }, + "uuid": { + "description": "Unique identifier of unit. It can be Job ID for batch jobs, UUID for pods in k8s or VMs in Openstack", + "type": "string" + } + } + }, + "models.Usage": { + "type": "object", + "properties": { + "avg_cpu_mem_usage": { + "description": "Average CPU memory during lifetime of project", + "type": "number" + }, + "avg_cpu_usage": { + "description": "Average CPU usage during lifetime of project", + "type": "number" + }, + "avg_gpu_mem_usage": { + "description": "Average GPU memory during lifetime of project", + "type": "number" + }, + "avg_gpu_usage": { + "description": "Average GPU usage during lifetime of project", + "type": "number" + }, + "num_units": { + "description": "Number of consumed units", + "type": "integer" + }, + "project": { + "description": "Account in batch systems, Tenant in Openstack, Namespace in k8s", + "type": "string" + }, + "resource_manager": { + "description": "Name of the resource manager that owns project. Eg slurm, openstack, kubernetes, etc", + "type": "string" + }, + "total_cpu_emissions_gms": { + "description": "Total CPU emissions in grams during lifetime of project", + "type": "number" + }, + "total_cpu_energy_usage_kwh": { + "description": "Total CPU energy usage in kWh during lifetime of project", + "type": "number" + }, + "total_cpumemtime_seconds": { + "description": "Total number of CPU memory (in MB) seconds consumed by the project", + "type": "integer" + }, + "total_cputime_seconds": { + "description": "Total number of CPU seconds consumed by the project", + "type": "integer" + }, + "total_gpu_emissions_gms": { + "description": "Total GPU emissions in grams during lifetime of project", + "type": "number" + }, + "total_gpu_energy_usage_kwh": { + "description": "Total GPU energy usage in kWh during lifetime of project", + "type": "number" + }, + "total_gpumemtime_seconds": { + "description": "Total number of GPU memory (in MB) seconds consumed by the project", + "type": "integer" + }, + "total_gputime_seconds": { + "description": "Total number of GPU seconds consumed by the project", + "type": "integer" + }, + "total_ingress_in_gb": { + "description": "Total ingress traffic in GB of project", + "type": "number" + }, + "total_io_read_cold_gb": { + "description": "Total IO read on cold storage in GB during lifetime of project", + "type": "number" + }, + "total_io_read_hot_gb": { + "description": "Total IO read on hot storage in GB during lifetime of project", + "type": "number" + }, + "total_io_write_cold_gb": { + "description": "Total IO write on cold storage in GB during lifetime of project", + "type": "number" + }, + "total_io_write_hot_gb": { + "description": "Total IO write on hot storage in GB during lifetime of project", + "type": "number" + }, + "total_outgress_in_gb": { + "description": "Total outgress traffic in GB of project", + "type": "number" + }, + "total_walltime_seconds": { + "description": "Total elapsed wall time in seconds consumed by the project", + "type": "integer" + }, + "usr": { + "description": "Username", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "CEEMS API", + Description: "CEEMS REST API server.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/pkg/api/http/docs/swagger.json b/pkg/api/http/docs/swagger.json new file mode 100644 index 00000000..cb067111 --- /dev/null +++ b/pkg/api/http/docs/swagger.json @@ -0,0 +1,1026 @@ +{ + "swagger": "2.0", + "info": { + "description": "CEEMS REST API server.", + "title": "CEEMS API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Mahendra Paipuri", + "url": "https://github.com/mahendrapaipuri/ceems/issues", + "email": "mahendra.paipuri@gmail.com" + }, + "license": { + "name": "BSD-3-Clause license", + "url": "https://opensource.org/license/bsd-3-clause" + }, + "version": "1.0" + }, + "paths": { + "/health": { + "get": { + "description": "get health status of API server", + "produces": [ + "text/plain" + ], + "tags": [ + "health" + ], + "summary": "Health status", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "string" + } + } + } + } + }, + "/projects": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get list of projects that user belong to", + "produces": [ + "application/json" + ], + "tags": [ + "projects" + ], + "summary": "List projects", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Project" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/units": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get units queried by a user", + "produces": [ + "application/json" + ], + "tags": [ + "units" + ], + "summary": "User endpoint for fetching compute units", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Unit UUID", + "name": "uuid", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Project", + "name": "project", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to fetch running units", + "name": "running", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Unit" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/units/admin": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get units for admins that can query units of any user", + "produces": [ + "application/json" + ], + "tags": [ + "units" + ], + "summary": "Admin endpoint for fetching compute units", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Unit UUID", + "name": "uuid", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Project", + "name": "project", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "User name", + "name": "user", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to fetch running units", + "name": "running", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Unit" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/units/verify": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "verify ownership of the unit", + "produces": [ + "application/json" + ], + "tags": [ + "units" + ], + "summary": "Verify unit ownership", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Unit UUID", + "name": "uuid", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Ownership" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/usage/{mode}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get current/global usage statistics of a current user", + "produces": [ + "application/json" + ], + "tags": [ + "usage" + ], + "summary": "Usage statistics", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "enum": [ + "current", + "global" + ], + "type": "string", + "description": "Whether to get usage stats within a period or global", + "name": "mode", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "User name(s)", + "name": "user", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Usage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/usage/{mode}/admin": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "get current/global usage statistics of a given user and/or project for admins", + "produces": [ + "application/json" + ], + "tags": [ + "usage" + ], + "summary": "Admin Usage statistics", + "parameters": [ + { + "type": "string", + "description": "Current user name", + "name": "X-Grafana-User", + "in": "header", + "required": true + }, + { + "enum": [ + "current", + "global" + ], + "type": "string", + "description": "Whether to get usage stats within a period or global", + "name": "mode", + "in": "path", + "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "User name(s)", + "name": "user", + "in": "query" + }, + { + "type": "string", + "description": "From timestamp", + "name": "from", + "in": "query" + }, + { + "type": "string", + "description": "To timestamp", + "name": "to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Fields to return in response", + "name": "field", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Usage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + }, + "/{resource}/demo": { + "get": { + "description": "get units and/or usage response generated by mock data", + "produces": [ + "application/json" + ], + "tags": [ + "demo" + ], + "summary": "Demo Units/Usage endpoints", + "parameters": [ + { + "enum": [ + "units", + "usage" + ], + "type": "string", + "description": "Whether to return mock units or usage data", + "name": "resource", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/http.Response-models_Usage" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/http.Response-any" + } + } + } + } + } + }, + "definitions": { + "http.Response-any": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": {} + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Ownership": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Ownership" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Project": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Project" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Unit": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Unit" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.Response-models_Usage": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Usage" + } + }, + "error": { + "type": "string" + }, + "errorType": { + "$ref": "#/definitions/http.errorType" + }, + "status": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "http.errorType": { + "type": "string", + "enum": [ + "", + "unauthorized", + "forbidden", + "timeout", + "canceled", + "execution", + "bad_data", + "internal", + "unavailable", + "not_found", + "not_acceptable" + ], + "x-enum-varnames": [ + "errorNone", + "errorUnauthorized", + "errorForbidden", + "errorTimeout", + "errorCanceled", + "errorExec", + "errorBadData", + "errorInternal", + "errorUnavailable", + "errorNotFound", + "errorNotAcceptable" + ] + }, + "models.Allocation": { + "type": "object", + "additionalProperties": true + }, + "models.Ownership": { + "type": "object", + "properties": { + "owner": { + "type": "boolean" + }, + "user": { + "type": "string" + }, + "uuids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "models.Project": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "models.Tag": { + "type": "object", + "additionalProperties": true + }, + "models.Unit": { + "type": "object", + "properties": { + "allocation": { + "description": "Allocation map of unit. Only string and int64 values are supported in map", + "allOf": [ + { + "$ref": "#/definitions/models.Allocation" + } + ] + }, + "avg_cpu_mem_usage": { + "description": "Average CPU memory during lifetime of unit", + "type": "number" + }, + "avg_cpu_usage": { + "description": "Average CPU usage during lifetime of unit", + "type": "number" + }, + "avg_gpu_mem_usage": { + "description": "Average GPU memory during lifetime of unit", + "type": "number" + }, + "avg_gpu_usage": { + "description": "Average GPU usage during lifetime of unit", + "type": "number" + }, + "created_at": { + "description": "Creation time", + "type": "string" + }, + "created_at_ts": { + "description": "Creation timestamp", + "type": "integer" + }, + "elapsed": { + "description": "Human readable total elapsed time string", + "type": "string" + }, + "ended_at": { + "description": "End time", + "type": "string" + }, + "ended_at_ts": { + "description": "End timestamp", + "type": "integer" + }, + "grp": { + "description": "User group", + "type": "string" + }, + "name": { + "description": "Name of compute unit", + "type": "string" + }, + "project": { + "description": "Account in batch systems, Tenant in Openstack, Namespace in k8s", + "type": "string" + }, + "resource_manager": { + "description": "Name of the resource manager that owns compute unit. Eg slurm, openstack, kubernetes, etc", + "type": "string" + }, + "started_at": { + "description": "Start time", + "type": "string" + }, + "started_at_ts": { + "description": "Start timestamp", + "type": "integer" + }, + "state": { + "description": "Current state of unit", + "type": "string" + }, + "tags": { + "description": "A map to store generic info. String and int64 are valid value types of map", + "allOf": [ + { + "$ref": "#/definitions/models.Tag" + } + ] + }, + "total_cpu_emissions_gms": { + "description": "Total CPU emissions in grams during lifetime of unit", + "type": "number" + }, + "total_cpu_energy_usage_kwh": { + "description": "Total CPU energy usage in kWh during lifetime of unit", + "type": "number" + }, + "total_cputime_seconds": { + "description": "Total number of CPU seconds consumed by the unit", + "type": "integer" + }, + "total_gpu_emissions_gms": { + "description": "Total GPU emissions in grams during lifetime of unit", + "type": "number" + }, + "total_gpu_energy_usage_kwh": { + "description": "Total GPU energy usage in kWh during lifetime of unit", + "type": "number" + }, + "total_gputime_seconds": { + "description": "Total number of GPU seconds consumed by the unit", + "type": "integer" + }, + "total_ingress_in_gb": { + "description": "Total ingress traffic in GB of unit", + "type": "number" + }, + "total_io_read_cold_gb": { + "description": "Total IO read on cold storage in GB during lifetime of unit", + "type": "number" + }, + "total_io_read_hot_gb": { + "description": "Total IO read on hot storage in GB during lifetime of unit", + "type": "number" + }, + "total_io_write_cold_gb": { + "description": "Total IO write on cold storage in GB during lifetime of unit", + "type": "number" + }, + "total_io_write_hot_gb": { + "description": "Total IO write on hot storage in GB during lifetime of unit", + "type": "number" + }, + "total_outgress_in_gb": { + "description": "Total outgress traffic in GB of unit", + "type": "number" + }, + "total_walltime_seconds": { + "description": "Total elapsed wall time in seconds", + "type": "integer" + }, + "usr": { + "description": "Username", + "type": "string" + }, + "uuid": { + "description": "Unique identifier of unit. It can be Job ID for batch jobs, UUID for pods in k8s or VMs in Openstack", + "type": "string" + } + } + }, + "models.Usage": { + "type": "object", + "properties": { + "avg_cpu_mem_usage": { + "description": "Average CPU memory during lifetime of project", + "type": "number" + }, + "avg_cpu_usage": { + "description": "Average CPU usage during lifetime of project", + "type": "number" + }, + "avg_gpu_mem_usage": { + "description": "Average GPU memory during lifetime of project", + "type": "number" + }, + "avg_gpu_usage": { + "description": "Average GPU usage during lifetime of project", + "type": "number" + }, + "num_units": { + "description": "Number of consumed units", + "type": "integer" + }, + "project": { + "description": "Account in batch systems, Tenant in Openstack, Namespace in k8s", + "type": "string" + }, + "resource_manager": { + "description": "Name of the resource manager that owns project. Eg slurm, openstack, kubernetes, etc", + "type": "string" + }, + "total_cpu_emissions_gms": { + "description": "Total CPU emissions in grams during lifetime of project", + "type": "number" + }, + "total_cpu_energy_usage_kwh": { + "description": "Total CPU energy usage in kWh during lifetime of project", + "type": "number" + }, + "total_cpumemtime_seconds": { + "description": "Total number of CPU memory (in MB) seconds consumed by the project", + "type": "integer" + }, + "total_cputime_seconds": { + "description": "Total number of CPU seconds consumed by the project", + "type": "integer" + }, + "total_gpu_emissions_gms": { + "description": "Total GPU emissions in grams during lifetime of project", + "type": "number" + }, + "total_gpu_energy_usage_kwh": { + "description": "Total GPU energy usage in kWh during lifetime of project", + "type": "number" + }, + "total_gpumemtime_seconds": { + "description": "Total number of GPU memory (in MB) seconds consumed by the project", + "type": "integer" + }, + "total_gputime_seconds": { + "description": "Total number of GPU seconds consumed by the project", + "type": "integer" + }, + "total_ingress_in_gb": { + "description": "Total ingress traffic in GB of project", + "type": "number" + }, + "total_io_read_cold_gb": { + "description": "Total IO read on cold storage in GB during lifetime of project", + "type": "number" + }, + "total_io_read_hot_gb": { + "description": "Total IO read on hot storage in GB during lifetime of project", + "type": "number" + }, + "total_io_write_cold_gb": { + "description": "Total IO write on cold storage in GB during lifetime of project", + "type": "number" + }, + "total_io_write_hot_gb": { + "description": "Total IO write on hot storage in GB during lifetime of project", + "type": "number" + }, + "total_outgress_in_gb": { + "description": "Total outgress traffic in GB of project", + "type": "number" + }, + "total_walltime_seconds": { + "description": "Total elapsed wall time in seconds consumed by the project", + "type": "integer" + }, + "usr": { + "description": "Username", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + } +} \ No newline at end of file diff --git a/pkg/api/http/docs/swagger.yaml b/pkg/api/http/docs/swagger.yaml new file mode 100644 index 00000000..b9f5b740 --- /dev/null +++ b/pkg/api/http/docs/swagger.yaml @@ -0,0 +1,705 @@ +definitions: + http.Response-any: + properties: + data: + items: {} + type: array + error: + type: string + errorType: + $ref: '#/definitions/http.errorType' + status: + type: string + warnings: + items: + type: string + type: array + type: object + http.Response-models_Ownership: + properties: + data: + items: + $ref: '#/definitions/models.Ownership' + type: array + error: + type: string + errorType: + $ref: '#/definitions/http.errorType' + status: + type: string + warnings: + items: + type: string + type: array + type: object + http.Response-models_Project: + properties: + data: + items: + $ref: '#/definitions/models.Project' + type: array + error: + type: string + errorType: + $ref: '#/definitions/http.errorType' + status: + type: string + warnings: + items: + type: string + type: array + type: object + http.Response-models_Unit: + properties: + data: + items: + $ref: '#/definitions/models.Unit' + type: array + error: + type: string + errorType: + $ref: '#/definitions/http.errorType' + status: + type: string + warnings: + items: + type: string + type: array + type: object + http.Response-models_Usage: + properties: + data: + items: + $ref: '#/definitions/models.Usage' + type: array + error: + type: string + errorType: + $ref: '#/definitions/http.errorType' + status: + type: string + warnings: + items: + type: string + type: array + type: object + http.errorType: + enum: + - "" + - unauthorized + - forbidden + - timeout + - canceled + - execution + - bad_data + - internal + - unavailable + - not_found + - not_acceptable + type: string + x-enum-varnames: + - errorNone + - errorUnauthorized + - errorForbidden + - errorTimeout + - errorCanceled + - errorExec + - errorBadData + - errorInternal + - errorUnavailable + - errorNotFound + - errorNotAcceptable + models.Allocation: + additionalProperties: true + type: object + models.Ownership: + properties: + owner: + type: boolean + user: + type: string + uuids: + items: + type: string + type: array + type: object + models.Project: + properties: + name: + type: string + type: object + models.Tag: + additionalProperties: true + type: object + models.Unit: + properties: + allocation: + allOf: + - $ref: '#/definitions/models.Allocation' + description: Allocation map of unit. Only string and int64 values are supported + in map + avg_cpu_mem_usage: + description: Average CPU memory during lifetime of unit + type: number + avg_cpu_usage: + description: Average CPU usage during lifetime of unit + type: number + avg_gpu_mem_usage: + description: Average GPU memory during lifetime of unit + type: number + avg_gpu_usage: + description: Average GPU usage during lifetime of unit + type: number + created_at: + description: Creation time + type: string + created_at_ts: + description: Creation timestamp + type: integer + elapsed: + description: Human readable total elapsed time string + type: string + ended_at: + description: End time + type: string + ended_at_ts: + description: End timestamp + type: integer + grp: + description: User group + type: string + name: + description: Name of compute unit + type: string + project: + description: Account in batch systems, Tenant in Openstack, Namespace in k8s + type: string + resource_manager: + description: Name of the resource manager that owns compute unit. Eg slurm, + openstack, kubernetes, etc + type: string + started_at: + description: Start time + type: string + started_at_ts: + description: Start timestamp + type: integer + state: + description: Current state of unit + type: string + tags: + allOf: + - $ref: '#/definitions/models.Tag' + description: A map to store generic info. String and int64 are valid value + types of map + total_cpu_emissions_gms: + description: Total CPU emissions in grams during lifetime of unit + type: number + total_cpu_energy_usage_kwh: + description: Total CPU energy usage in kWh during lifetime of unit + type: number + total_cputime_seconds: + description: Total number of CPU seconds consumed by the unit + type: integer + total_gpu_emissions_gms: + description: Total GPU emissions in grams during lifetime of unit + type: number + total_gpu_energy_usage_kwh: + description: Total GPU energy usage in kWh during lifetime of unit + type: number + total_gputime_seconds: + description: Total number of GPU seconds consumed by the unit + type: integer + total_ingress_in_gb: + description: Total ingress traffic in GB of unit + type: number + total_io_read_cold_gb: + description: Total IO read on cold storage in GB during lifetime of unit + type: number + total_io_read_hot_gb: + description: Total IO read on hot storage in GB during lifetime of unit + type: number + total_io_write_cold_gb: + description: Total IO write on cold storage in GB during lifetime of unit + type: number + total_io_write_hot_gb: + description: Total IO write on hot storage in GB during lifetime of unit + type: number + total_outgress_in_gb: + description: Total outgress traffic in GB of unit + type: number + total_walltime_seconds: + description: Total elapsed wall time in seconds + type: integer + usr: + description: Username + type: string + uuid: + description: Unique identifier of unit. It can be Job ID for batch jobs, UUID + for pods in k8s or VMs in Openstack + type: string + type: object + models.Usage: + properties: + avg_cpu_mem_usage: + description: Average CPU memory during lifetime of project + type: number + avg_cpu_usage: + description: Average CPU usage during lifetime of project + type: number + avg_gpu_mem_usage: + description: Average GPU memory during lifetime of project + type: number + avg_gpu_usage: + description: Average GPU usage during lifetime of project + type: number + num_units: + description: Number of consumed units + type: integer + project: + description: Account in batch systems, Tenant in Openstack, Namespace in k8s + type: string + resource_manager: + description: Name of the resource manager that owns project. Eg slurm, openstack, + kubernetes, etc + type: string + total_cpu_emissions_gms: + description: Total CPU emissions in grams during lifetime of project + type: number + total_cpu_energy_usage_kwh: + description: Total CPU energy usage in kWh during lifetime of project + type: number + total_cpumemtime_seconds: + description: Total number of CPU memory (in MB) seconds consumed by the project + type: integer + total_cputime_seconds: + description: Total number of CPU seconds consumed by the project + type: integer + total_gpu_emissions_gms: + description: Total GPU emissions in grams during lifetime of project + type: number + total_gpu_energy_usage_kwh: + description: Total GPU energy usage in kWh during lifetime of project + type: number + total_gpumemtime_seconds: + description: Total number of GPU memory (in MB) seconds consumed by the project + type: integer + total_gputime_seconds: + description: Total number of GPU seconds consumed by the project + type: integer + total_ingress_in_gb: + description: Total ingress traffic in GB of project + type: number + total_io_read_cold_gb: + description: Total IO read on cold storage in GB during lifetime of project + type: number + total_io_read_hot_gb: + description: Total IO read on hot storage in GB during lifetime of project + type: number + total_io_write_cold_gb: + description: Total IO write on cold storage in GB during lifetime of project + type: number + total_io_write_hot_gb: + description: Total IO write on hot storage in GB during lifetime of project + type: number + total_outgress_in_gb: + description: Total outgress traffic in GB of project + type: number + total_walltime_seconds: + description: Total elapsed wall time in seconds consumed by the project + type: integer + usr: + description: Username + type: string + type: object +info: + contact: + email: mahendra.paipuri@gmail.com + name: Mahendra Paipuri + url: https://github.com/mahendrapaipuri/ceems/issues + description: CEEMS REST API server. + license: + name: BSD-3-Clause license + url: https://opensource.org/license/bsd-3-clause + termsOfService: http://swagger.io/terms/ + title: CEEMS API + version: "1.0" +paths: + /{resource}/demo: + get: + description: get units and/or usage response generated by mock data + parameters: + - description: Whether to return mock units or usage data + enum: + - units + - usage + in: path + name: resource + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.Response-models_Usage' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/http.Response-any' + summary: Demo Units/Usage endpoints + tags: + - demo + /health: + get: + description: get health status of API server + produces: + - text/plain + responses: + "200": + description: OK + schema: + type: string + "503": + description: Service Unavailable + schema: + type: string + summary: Health status + tags: + - health + /projects: + get: + description: get list of projects that user belong to + parameters: + - description: Current user name + in: header + name: X-Grafana-User + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.Response-models_Project' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/http.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/http.Response-any' + security: + - BasicAuth: [] + summary: List projects + tags: + - projects + /units: + get: + description: get units queried by a user + parameters: + - description: Current user name + in: header + name: X-Grafana-User + required: true + type: string + - collectionFormat: multi + description: Unit UUID + in: query + items: + type: string + name: uuid + type: array + - collectionFormat: multi + description: Project + in: query + items: + type: string + name: project + type: array + - description: Whether to fetch running units + in: query + name: running + type: boolean + - description: From timestamp + in: query + name: from + type: string + - description: To timestamp + in: query + name: to + type: string + - collectionFormat: multi + description: Fields to return in response + in: query + items: + type: string + name: field + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.Response-models_Unit' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/http.Response-any' + "403": + description: Forbidden + schema: + $ref: '#/definitions/http.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/http.Response-any' + security: + - BasicAuth: [] + summary: User endpoint for fetching compute units + tags: + - units + /units/admin: + get: + description: get units for admins that can query units of any user + parameters: + - description: Current user name + in: header + name: X-Grafana-User + required: true + type: string + - collectionFormat: multi + description: Unit UUID + in: query + items: + type: string + name: uuid + type: array + - collectionFormat: multi + description: Project + in: query + items: + type: string + name: project + type: array + - collectionFormat: multi + description: User name + in: query + items: + type: string + name: user + type: array + - description: Whether to fetch running units + in: query + name: running + type: boolean + - description: From timestamp + in: query + name: from + type: string + - description: To timestamp + in: query + name: to + type: string + - collectionFormat: multi + description: Fields to return in response + in: query + items: + type: string + name: field + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.Response-models_Unit' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/http.Response-any' + "403": + description: Forbidden + schema: + $ref: '#/definitions/http.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/http.Response-any' + security: + - BasicAuth: [] + summary: Admin endpoint for fetching compute units + tags: + - units + /units/verify: + get: + description: verify ownership of the unit + parameters: + - description: Current user name + in: header + name: X-Grafana-User + required: true + type: string + - collectionFormat: multi + description: Unit UUID + in: query + items: + type: string + name: uuid + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.Response-models_Ownership' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/http.Response-any' + "403": + description: Forbidden + schema: + $ref: '#/definitions/http.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/http.Response-any' + security: + - BasicAuth: [] + summary: Verify unit ownership + tags: + - units + /usage/{mode}: + get: + description: get current/global usage statistics of a current user + parameters: + - description: Current user name + in: header + name: X-Grafana-User + required: true + type: string + - description: Whether to get usage stats within a period or global + enum: + - current + - global + in: path + name: mode + required: true + type: string + - collectionFormat: multi + description: User name(s) + in: query + items: + type: string + name: user + type: array + - description: From timestamp + in: query + name: from + type: string + - description: To timestamp + in: query + name: to + type: string + - collectionFormat: multi + description: Fields to return in response + in: query + items: + type: string + name: field + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.Response-models_Usage' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/http.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/http.Response-any' + security: + - BasicAuth: [] + summary: Usage statistics + tags: + - usage + /usage/{mode}/admin: + get: + description: get current/global usage statistics of a given user and/or project + for admins + parameters: + - description: Current user name + in: header + name: X-Grafana-User + required: true + type: string + - description: Whether to get usage stats within a period or global + enum: + - current + - global + in: path + name: mode + required: true + type: string + - collectionFormat: multi + description: User name(s) + in: query + items: + type: string + name: user + type: array + - description: From timestamp + in: query + name: from + type: string + - description: To timestamp + in: query + name: to + type: string + - collectionFormat: multi + description: Fields to return in response + in: query + items: + type: string + name: field + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/http.Response-models_Usage' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/http.Response-any' + "403": + description: Forbidden + schema: + $ref: '#/definitions/http.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/http.Response-any' + security: + - BasicAuth: [] + summary: Admin Usage statistics + tags: + - usage +securityDefinitions: + BasicAuth: + type: basic +swagger: "2.0" diff --git a/pkg/api/http/error.go b/pkg/api/http/error.go index 97d5dea0..16c7206d 100644 --- a/pkg/api/http/error.go +++ b/pkg/api/http/error.go @@ -27,6 +27,7 @@ func (e *apiError) Error() string { const ( errorNone errorType = "" errorUnauthorized errorType = "unauthorized" + errorForbidden errorType = "forbidden" errorTimeout errorType = "timeout" errorCanceled errorType = "canceled" errorExec errorType = "execution" @@ -52,13 +53,15 @@ var ( ) // Return error response for by setting errorString and errorType in response -func errorResponse(w http.ResponseWriter, apiErr *apiError, logger log.Logger, data interface{}) { +func errorResponse[T any](w http.ResponseWriter, apiErr *apiError, logger log.Logger, data []T) { var code int switch apiErr.typ { case errorBadData: code = http.StatusBadRequest case errorUnauthorized: code = http.StatusUnauthorized + case errorForbidden: + code = http.StatusForbidden case errorExec: code = http.StatusUnprocessableEntity case errorCanceled: @@ -76,7 +79,7 @@ func errorResponse(w http.ResponseWriter, apiErr *apiError, logger log.Logger, d } w.WriteHeader(code) - response := Response{ + response := Response[T]{ Status: "error", ErrorType: apiErr.typ, Error: apiErr.err.Error(), diff --git a/pkg/api/http/middleware.go b/pkg/api/http/middleware.go index b84ed9ff..716a66d3 100644 --- a/pkg/api/http/middleware.go +++ b/pkg/api/http/middleware.go @@ -2,6 +2,7 @@ package http import ( "net/http" + "regexp" "slices" "strings" "time" @@ -53,8 +54,20 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var loggedUser string - // If requested URI is health, demo or "/" pass through - if strings.HasSuffix(r.URL.Path, "health") || strings.HasSuffix(r.URL.Path, "demo") || r.URL.Path == "/" { + // If requested URI is one of the following, skip checking for user header + // - Root document + // - /health endpoint + // - /demo endpoint + // - /swagger/* endpoints + // - /debug/* endpoints + // + // NOTE that we only skip checking X-Grafana-User header. In prod when + // basic auth is enabled, all these end points are under auth and hence an + // autorised user cannot access these end points + if r.URL.Path == "/" || + strings.HasSuffix(r.URL.Path, "health") || + strings.HasSuffix(r.URL.Path, "demo") || + regexp.MustCompile("/(swagger|debug)/(.*)").MatchString(r.URL.Path) { goto end } @@ -74,7 +87,7 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler Log("msg", "Grafana user Header not found. Denying authentication") // Write an error and stop the handler chain - errorResponse(w, &apiError{errorUnauthorized, errNoUser}, amw.logger, nil) + errorResponse[any](w, &apiError{errorUnauthorized, errNoUser}, amw.logger, nil) return } level.Info(amw.logger).Log("loggedUser", loggedUser, "url", r.URL) @@ -105,7 +118,7 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler level.Error(amw.logger).Log("msg", "Unprivileged user accessing admin endpoint", "user", loggedUser, "url", r.URL) // Write an error and stop the handler chain - errorResponse(w, &apiError{errorUnauthorized, errNoPrivs}, amw.logger, nil) + errorResponse[any](w, &apiError{errorForbidden, errNoPrivs}, amw.logger, nil) return } r.Header.Set(dashboardUserHeader, loggedUser) diff --git a/pkg/api/http/server.go b/pkg/api/http/server.go index bea12c9c..7f4b8374 100644 --- a/pkg/api/http/server.go +++ b/pkg/api/http/server.go @@ -20,10 +20,12 @@ import ( "github.com/gorilla/mux" "github.com/mahendrapaipuri/ceems/pkg/api/base" "github.com/mahendrapaipuri/ceems/pkg/api/db" + "github.com/mahendrapaipuri/ceems/pkg/api/http/docs" "github.com/mahendrapaipuri/ceems/pkg/api/models" "github.com/mahendrapaipuri/ceems/pkg/grafana" _ "github.com/mattn/go-sqlite3" "github.com/prometheus/exporter-toolkit/web" + httpSwagger "github.com/swaggo/http-swagger/v2" ) // API Resources names @@ -58,12 +60,12 @@ type CEEMSServer struct { } // Response defines the response model of CEEMSServer -type Response struct { - Status string `json:"status"` - Data interface{} `json:"data,omitempty"` - ErrorType errorType `json:"errorType,omitempty"` - Error string `json:"error,omitempty"` - Warnings []string `json:"warnings,omitempty"` +type Response[T any] struct { + Status string `json:"status"` + Data []T `json:"data,omitempty"` + ErrorType errorType `json:"errorType,omitempty"` + Error string `json:"error,omitempty"` + Warnings []string `json:"warnings,omitempty"` } var ( @@ -136,23 +138,34 @@ func NewCEEMSServer(c *Config) (*CEEMSServer, func(), error) { `)) }) + // Create a sub router with apiVersion as PathPrefix + subRouter := router.PathPrefix(fmt.Sprintf("/api/%s/", base.APIVersion)).Subrouter() + // Allow only GET methods - router.HandleFunc("/api/health", server.health).Methods("GET") - router.HandleFunc("/api/projects", server.projects).Methods("GET") - router.HandleFunc(fmt.Sprintf("/api/%s", unitsResourceName), server.units).Methods("GET") - router.HandleFunc(fmt.Sprintf("/api/%s/admin", unitsResourceName), server.unitsAdmin).Methods("GET") - router.HandleFunc(fmt.Sprintf("/api/%s/{query:(?:current|global)}", usageResourceName), server.usage). - Methods("GET") - router.HandleFunc(fmt.Sprintf("/api/%s/{query:(?:current|global)}/admin", usageResourceName), server.usageAdmin). - Methods("GET") - router.HandleFunc(fmt.Sprintf("/api/%s/verify", unitsResourceName), server.verifyUnitsOwnership).Methods("GET") + subRouter.HandleFunc("/health", server.health).Methods(http.MethodGet) + subRouter.HandleFunc("/projects", server.projects).Methods(http.MethodGet) + subRouter.HandleFunc(fmt.Sprintf("/%s", unitsResourceName), server.units).Methods(http.MethodGet) + subRouter.HandleFunc(fmt.Sprintf("/%s/admin", unitsResourceName), server.unitsAdmin).Methods(http.MethodGet) + subRouter.HandleFunc(fmt.Sprintf("/%s/{mode:(?:current|global)}", usageResourceName), server.usage). + Methods(http.MethodGet) + subRouter.HandleFunc(fmt.Sprintf("/%s/{mode:(?:current|global)}/admin", usageResourceName), server.usageAdmin). + Methods(http.MethodGet) + subRouter.HandleFunc(fmt.Sprintf("/%s/verify", unitsResourceName), server.verifyUnitsOwnership). + Methods(http.MethodGet) // A demo end point that returns mocked data for units and/or usage tables - router.HandleFunc("/api/{resource:(?:units|usage)}/demo", server.demo).Methods("GET") + subRouter.HandleFunc("/{resource:(?:units|usage)}/demo", server.demo).Methods(http.MethodGet) // pprof debug end points router.PathPrefix("/debug/").Handler(http.DefaultServeMux) + router.PathPrefix("/swagger/").Handler(httpSwagger.Handler( + httpSwagger.URL("doc.json"), // The url pointing to API definition + httpSwagger.DeepLinking(true), + httpSwagger.DocExpansion("list"), + httpSwagger.DomID("swagger-ui"), + )).Methods(http.MethodGet) + // Add a middleware that verifies headers and pass them in requests // The middleware will fetch admin users from Grafana periodically to update list amw := authenticationMiddleware{ @@ -170,8 +183,27 @@ func NewCEEMSServer(c *Config) (*CEEMSServer, func(), error) { return server, func() {}, nil } -// Start server +// Start launches CEEMS HTTP server godoc +// +// @title CEEMS API +// @version 1.0 +// @description CEEMS REST API server. +// @termsOfService http://swagger.io/terms/ +// +// @contact.name Mahendra Paipuri +// @contact.url https://github.com/mahendrapaipuri/ceems/issues +// @contact.email mahendra.paipuri@gmail.com +// +// @license.name BSD-3-Clause license +// @license.url https://opensource.org/license/bsd-3-clause +// +// @securityDefinitions.basic BasicAuth func (s *CEEMSServer) Start() error { + // Set swagger info + docs.SwaggerInfo.BasePath = fmt.Sprintf("/api/%s", base.APIVersion) + docs.SwaggerInfo.Schemes = []string{"http", "https"} + docs.SwaggerInfo.Host = s.server.Addr + level.Info(s.logger).Log("msg", fmt.Sprintf("Starting %s", base.CEEMSServerAppName)) if err := web.ListenAndServe(s.server, s.webConfig, s.logger); err != nil && err != http.ErrServerClosed { level.Error(s.logger).Log("msg", "Failed to Listen and Server HTTP server", "err", err) @@ -207,6 +239,16 @@ func (s *CEEMSServer) setHeaders(w http.ResponseWriter) { w.Header().Set("X-Content-Type-Options", "nosniff") } +// health godoc +// +// @Summary Health status +// @Description get health status of API server +// @Tags health +// @Produce plain +// @Success 200 {string} OK +// @Failure 503 {string} KO +// @Router /health [get] +// // Check status of server func (s *CEEMSServer) health(w http.ResponseWriter, r *http.Request) { if !s.HealthCheck(s.db, s.logger) { @@ -357,7 +399,7 @@ func (s *CEEMSServer) unitsQuerier( // Get query window time stamps queryWindowTS, err = s.getQueryWindow(r) if err != nil { - errorResponse(w, &apiError{errorBadData, err}, s.logger, nil) + errorResponse[any](w, &apiError{errorBadData, err}, s.logger, nil) return } @@ -373,13 +415,13 @@ queryUnits: units, err := s.Querier(s.db, q, unitsResourceName, s.logger) if err != nil { level.Error(s.logger).Log("msg", "Failed to fetch units", "loggedUser", loggedUser, "err", err) - errorResponse(w, &apiError{errorInternal, err}, s.logger, nil) + errorResponse[any](w, &apiError{errorInternal, err}, s.logger, nil) return } // Write response w.WriteHeader(http.StatusOK) - response := Response{ + response := Response[models.Unit]{ Status: "success", Data: units.([]models.Unit), } @@ -389,14 +431,55 @@ queryUnits: } } -// GET /api/units/admin +// unitsAdmin godoc +// +// @Summary Admin endpoint for fetching compute units +// @Description get units for admins that can query units of any user +// @Security BasicAuth +// @Tags units +// @Produce json +// @Param X-Grafana-User header string true "Current user name" +// @Param uuid query []string false "Unit UUID" collectionFormat(multi) +// @Param project query []string false "Project" collectionFormat(multi) +// @Param user query []string false "User name" collectionFormat(multi) +// @Param running query bool false "Whether to fetch running units" +// @Param from query string false "From timestamp" +// @Param to query string false "To timestamp" +// @Param field query []string false "Fields to return in response" collectionFormat(multi) +// @Success 200 {object} Response[models.Unit] +// @Failure 401 {object} Response[any] +// @Failure 403 {object} Response[any] +// @Failure 500 {object} Response[any] +// @Router /units/admin [get] +// +// GET /units/admin // Get any unit of any user func (s *CEEMSServer) unitsAdmin(w http.ResponseWriter, r *http.Request) { // Query for units and write response s.unitsQuerier(r.URL.Query()["user"], w, r) } -// GET /api/units +// units godoc +// +// @Summary User endpoint for fetching compute units +// @Description get units queried by a user +// @Security BasicAuth +// @Tags units +// @Produce json +// @Param X-Grafana-User header string true "Current user name" +// @Param uuid query []string false "Unit UUID" collectionFormat(multi) +// @Param project query []string false "Project" collectionFormat(multi) +// @Param running query bool false "Whether to fetch running units" +// @Param from query string false "From timestamp" +// @Param to query string false "To timestamp" +// @Param field query []string false "Fields to return in response" collectionFormat(multi) +// @Success 200 {object} Response[models.Unit] +// @Failure 401 {object} Response[any] +// @Failure 403 {object} Response[any] +// @Failure 500 {object} Response[any] +// @Router /units [get] +// +// GET /units // Get unit of dashboard user func (s *CEEMSServer) units(w http.ResponseWriter, r *http.Request) { // Get current logged user and dashboard user from headers @@ -406,7 +489,22 @@ func (s *CEEMSServer) units(w http.ResponseWriter, r *http.Request) { s.unitsQuerier([]string{dashboardUser}, w, r) } -// GET /api/units/verify +// verifyUnitsOwnership godoc +// +// @Summary Verify unit ownership +// @Description verify ownership of the unit +// @Security BasicAuth +// @Tags units +// @Produce json +// @Param X-Grafana-User header string true "Current user name" +// @Param uuid query []string false "Unit UUID" collectionFormat(multi) +// @Success 200 {object} Response[models.Ownership] +// @Failure 401 {object} Response[any] +// @Failure 403 {object} Response[any] +// @Failure 500 {object} Response[any] +// @Router /units/verify [get] +// +// GET /units/verify // Verify the user ownership for queried units func (s *CEEMSServer) verifyUnitsOwnership(w http.ResponseWriter, r *http.Request) { // Set headers @@ -421,12 +519,14 @@ func (s *CEEMSServer) verifyUnitsOwnership(w http.ResponseWriter, r *http.Reques // Check if user is owner of the queries uuids if VerifyOwnership(dashboardUser, uuids, s.db, s.logger) { w.WriteHeader(http.StatusOK) - response := Response{ + response := Response[models.Ownership]{ Status: "success", - Data: map[string]interface{}{ - "user": dashboardUser, - "uuids": uuids, - "verfiy": true, + Data: []models.Ownership{ + { + User: dashboardUser, + UUIDS: uuids, + Owner: true, + }, }, } if err := json.NewEncoder(w).Encode(&response); err != nil { @@ -434,11 +534,24 @@ func (s *CEEMSServer) verifyUnitsOwnership(w http.ResponseWriter, r *http.Reques w.Write([]byte("KO")) } } else { - errorResponse(w, &apiError{errorUnauthorized, fmt.Errorf("user do not have permissions on uuids")}, s.logger, nil) + errorResponse[any](w, &apiError{errorForbidden, fmt.Errorf("user do not have permissions on uuids")}, s.logger, nil) } } -// GET /api/projects +// projects godoc +// +// @Summary List projects +// @Description get list of projects that user belong to +// @Security BasicAuth +// @Tags projects +// @Produce json +// @Param X-Grafana-User header string true "Current user name" +// @Success 200 {object} Response[models.Project] +// @Failure 401 {object} Response[any] +// @Failure 500 {object} Response[any] +// @Router /projects [get] +// +// GET /projects // Get projects list of user func (s *CEEMSServer) projects(w http.ResponseWriter, r *http.Request) { // Set headers @@ -457,13 +570,13 @@ func (s *CEEMSServer) projects(w http.ResponseWriter, r *http.Request) { projects, err := s.Querier(s.db, q, "projects", s.logger) if err != nil { level.Error(s.logger).Log("msg", "Failed to fetch projects", "user", dashboardUser, "err", err) - errorResponse(w, &apiError{errorInternal, err}, s.logger, nil) + errorResponse[any](w, &apiError{errorInternal, err}, s.logger, nil) return } // Write response w.WriteHeader(http.StatusOK) - projectsResponse := Response{ + projectsResponse := Response[models.Project]{ Status: "success", Data: projects.([]models.Project), } @@ -487,7 +600,7 @@ func (s *CEEMSServer) projectsSubQuery(users []string) Query { return qSub } -// GET /api/usage/current +// GET /usage/current // Get current usage statistics func (s *CEEMSServer) currentUsage(users []string, fields []string, w http.ResponseWriter, r *http.Request) { // Get sub query for projects @@ -517,7 +630,7 @@ func (s *CEEMSServer) currentUsage(users []string, fields []string, w http.Respo // Get query window time stamps queryWindowTS, err := s.getQueryWindow(r) if err != nil { - errorResponse(w, &apiError{errorBadData, err}, s.logger, nil) + errorResponse[any](w, &apiError{errorBadData, err}, s.logger, nil) return } @@ -537,13 +650,13 @@ func (s *CEEMSServer) currentUsage(users []string, fields []string, w http.Respo if err != nil { level.Error(s.logger). Log("msg", "Failed to fetch current usage statistics", "users", strings.Join(users, ","), "err", err) - errorResponse(w, &apiError{errorInternal, err}, s.logger, nil) + errorResponse[any](w, &apiError{errorInternal, err}, s.logger, nil) return } // Write response w.WriteHeader(http.StatusOK) - projectsResponse := Response{ + projectsResponse := Response[models.Usage]{ Status: "success", Data: usage.([]models.Usage), } @@ -553,7 +666,7 @@ func (s *CEEMSServer) currentUsage(users []string, fields []string, w http.Respo } } -// GET /api/usage/global +// GET /usage/global // Get global usage statistics func (s *CEEMSServer) globalUsage(users []string, queriedFields []string, w http.ResponseWriter, r *http.Request) { // Get sub query for projects @@ -575,13 +688,13 @@ func (s *CEEMSServer) globalUsage(users []string, queriedFields []string, w http if err != nil { level.Error(s.logger). Log("msg", "Failed to fetch global usage statistics", "users", strings.Join(users, ","), "err", err) - errorResponse(w, &apiError{errorInternal, err}, s.logger, nil) + errorResponse[any](w, &apiError{errorInternal, err}, s.logger, nil) return } // Write response w.WriteHeader(http.StatusOK) - projectsResponse := Response{ + projectsResponse := Response[models.Usage]{ Status: "success", Data: usage.([]models.Usage), } @@ -591,7 +704,25 @@ func (s *CEEMSServer) globalUsage(users []string, queriedFields []string, w http } } -// GET /api/usage +// usage godoc +// +// @Summary Usage statistics +// @Description get current/global usage statistics of a current user +// @Security BasicAuth +// @Tags usage +// @Produce json +// @Param X-Grafana-User header string true "Current user name" +// @Param mode path string true "Whether to get usage stats within a period or global" Enums(current, global) +// @Param user query []string false "User name(s)" collectionFormat(multi) +// @Param from query string false "From timestamp" +// @Param to query string false "To timestamp" +// @Param field query []string false "Fields to return in response" collectionFormat(multi) +// @Success 200 {object} Response[models.Usage] +// @Failure 401 {object} Response[any] +// @Failure 500 {object} Response[any] +// @Router /usage/{mode} [get] +// +// GET /usage/{mode} // Get current/global usage statistics func (s *CEEMSServer) usage(w http.ResponseWriter, r *http.Request) { // Set headers @@ -601,10 +732,10 @@ func (s *CEEMSServer) usage(w http.ResponseWriter, r *http.Request) { _, dashboardUser := s.getUser(r) // Get path parameter type - var queryType string + var mode string var exists bool - if queryType, exists = mux.Vars(r)["query"]; !exists { - errorResponse(w, &apiError{errorBadData, errInvalidRequest}, s.logger, nil) + if mode, exists = mux.Vars(r)["mode"]; !exists { + errorResponse[any](w, &apiError{errorBadData, errInvalidRequest}, s.logger, nil) return } @@ -612,27 +743,46 @@ func (s *CEEMSServer) usage(w http.ResponseWriter, r *http.Request) { queriedFields := s.getQueriedFields(r.URL.Query(), base.UsageDBTableColNames) // handle current usage query - if queryType == "current" { + if mode == "current" { s.currentUsage([]string{dashboardUser}, queriedFields, w, r) } // handle global usage query - if queryType == "global" { + if mode == "global" { s.globalUsage([]string{dashboardUser}, queriedFields, w, r) } } -// GET /api/usage/admin +// usage godoc +// +// @Summary Admin Usage statistics +// @Description get current/global usage statistics of a given user and/or project for admins +// @Security BasicAuth +// @Tags usage +// @Produce json +// @Param X-Grafana-User header string true "Current user name" +// @Param mode path string true "Whether to get usage stats within a period or global" Enums(current, global) +// @Param user query []string false "User name(s)" collectionFormat(multi) +// @Param from query string false "From timestamp" +// @Param to query string false "To timestamp" +// @Param field query []string false "Fields to return in response" collectionFormat(multi) +// @Success 200 {object} Response[models.Usage] +// @Failure 401 {object} Response[any] +// @Failure 403 {object} Response[any] +// @Failure 500 {object} Response[any] +// @Router /usage/{mode}/admin [get] +// +// GET /usage/{mode}/admin // Get current/global usage statistics of any user func (s *CEEMSServer) usageAdmin(w http.ResponseWriter, r *http.Request) { // Set headers s.setHeaders(w) // Get path parameter type - var queryType string + var mode string var exists bool - if queryType, exists = mux.Vars(r)["query"]; !exists { - errorResponse(w, &apiError{errorBadData, errInvalidRequest}, s.logger, nil) + if mode, exists = mux.Vars(r)["mode"]; !exists { + errorResponse[any](w, &apiError{errorBadData, errInvalidRequest}, s.logger, nil) return } @@ -640,17 +790,29 @@ func (s *CEEMSServer) usageAdmin(w http.ResponseWriter, r *http.Request) { queriedFields := s.getQueriedFields(r.URL.Query(), base.UsageDBTableColNames) // handle current usage query - if queryType == "current" { + if mode == "current" { s.currentUsage(r.URL.Query()["user"], queriedFields, w, r) } // handle global usage query - if queryType == "global" { + if mode == "global" { s.globalUsage(r.URL.Query()["user"], queriedFields, w, r) } } -// GET /api/demo/{units,usage} +// usage godoc +// +// @Summary Demo Units/Usage endpoints +// @Description get units and/or usage response generated by mock data +// @Tags demo +// @Produce json +// @Param resource path string true "Whether to return mock units or usage data" Enums(units, usage) +// @Success 200 {object} Response[models.Unit] +// @Success 200 {object} Response[models.Usage] +// @Failure 500 {object} Response[any] +// @Router /{resource}/demo [get] +// +// GET /demo/{units,usage} // Return mocked data for different models func (s *CEEMSServer) demo(w http.ResponseWriter, r *http.Request) { // Set headers @@ -660,7 +822,7 @@ func (s *CEEMSServer) demo(w http.ResponseWriter, r *http.Request) { var resourceType string var exists bool if resourceType, exists = mux.Vars(r)["resource"]; !exists { - errorResponse(w, &apiError{errorBadData, errInvalidRequest}, s.logger, nil) + errorResponse[any](w, &apiError{errorBadData, errInvalidRequest}, s.logger, nil) return } @@ -669,7 +831,7 @@ func (s *CEEMSServer) demo(w http.ResponseWriter, r *http.Request) { units := mockUnits() // Write response w.WriteHeader(http.StatusOK) - unitsResponse := Response{ + unitsResponse := Response[models.Unit]{ Status: "success", Data: units, } @@ -684,7 +846,7 @@ func (s *CEEMSServer) demo(w http.ResponseWriter, r *http.Request) { usage := mockUsage() // Write response w.WriteHeader(http.StatusOK) - usageResponse := Response{ + usageResponse := Response[models.Usage]{ Status: "success", Data: usage, } diff --git a/pkg/api/http/server_test.go b/pkg/api/http/server_test.go index d4e4fd29..6a1b46df 100644 --- a/pkg/api/http/server_test.go +++ b/pkg/api/http/server_test.go @@ -102,18 +102,13 @@ func TestAccountsHandler(t *testing.T) { expectedAccounts, _ := mockQuerier(server.db, Query{}, "projects", server.logger) // Unmarshal byte into structs. - var response Response + var response Response[models.Project] json.Unmarshal(data, &response) - var projectData []models.Project - for _, name := range response.Data.([]interface{}) { - projectData = append(projectData, models.Project{Name: name.(map[string]interface{})["name"].(string)}) - } - if response.Status != "success" { t.Errorf("expected success status got %v", response.Status) } - if !reflect.DeepEqual(projectData, expectedAccounts) { - t.Errorf("expected %#v got %#v", expectedAccounts, projectData) + if !reflect.DeepEqual(response.Data, expectedAccounts) { + t.Errorf("expected %#v got %#v", expectedAccounts, response.Data) } } @@ -175,16 +170,15 @@ func TestUnitsHandler(t *testing.T) { expectedUnits, _ := getMockUnits(Query{}, server.logger) // Unmarshal byte into structs. - var response Response + var response Response[models.Unit] json.Unmarshal(data, &response) if response.Status != "success" { t.Errorf("expected success status got %v", response.Status) } - var unitData = response.Data.([]interface{}) - if len(unitData) != len(expectedUnits) { - t.Errorf("expected %d units, got %d", len(unitData), len(expectedUnits)) + if len(response.Data) != len(expectedUnits) { + t.Errorf("expected %d units, got %d", len(response.Data), len(expectedUnits)) } } @@ -250,7 +244,7 @@ func TestUnitsHandlerWithMalformedQueryParams(t *testing.T) { } // Unmarshal byte into structs. - var response Response + var response Response[any] json.Unmarshal(data, &response) if response.Status != "error" { @@ -290,7 +284,7 @@ func TestUnitsHandlerWithQueryWindowExceeded(t *testing.T) { } // Unmarshal byte into structs. - var response Response + var response Response[any] json.Unmarshal(data, &response) if response.Status != "error" { @@ -333,14 +327,14 @@ func TestUnitsHandlerWithUnituuidsQueryParams(t *testing.T) { expectedUnits, _ := getMockUnits(Query{}, server.logger) // Unmarshal byte into structs. - var response Response + var response Response[models.Unit] json.Unmarshal(data, &response) if response.Status != "success" { t.Errorf("expected success status got %v", response.Status) } - var unitData = response.Data.([]interface{}) + var unitData = response.Data if len(unitData) != len(expectedUnits) { t.Errorf("expected %d units, got %d", len(unitData), len(expectedUnits)) } diff --git a/pkg/api/models/models.go b/pkg/api/models/models.go index 44e82e9a..6f445452 100644 --- a/pkg/api/models/models.go +++ b/pkg/api/models/models.go @@ -118,3 +118,10 @@ func (u Usage) TagMap(keyTag string, valueTag string) map[string]string { type Project struct { Name string `json:"name,omitempty" sql:"project" sqlitetype:"text"` } + +// Ownership status of queried UUIDs +type Ownership struct { + User string `json:"user"` + UUIDS []string `json:"uuids"` + Owner bool `json:"owner"` +} From 7460af12812255e0c158da4a357ac6fd541eb5fa Mon Sep 17 00:00:00 2001 From: Mahendra Paipuri Date: Sun, 5 May 2024 12:41:12 +0200 Subject: [PATCH 2/2] test: Update tests with new API paths * Include swag init in make build target to regenerate swagger docs before building apps * Update e2e test outputs with modified 403 responses Signed-off-by: Mahendra Paipuri --- Makefile | 5 +- Makefile.common | 13 +++- pkg/api/cli/cli_test.go | 12 ++-- pkg/api/http/middleware_test.go | 8 +-- ...2e-test-api-server-admin--denied-query.txt | 2 +- ...erver-current-usage-admin-denied-query.txt | 2 +- .../output/e2e-test-api-verify-fail-query.txt | 2 +- .../output/e2e-test-api-verify-pass-query.txt | 2 +- pkg/lb/frontend/middleware.go | 8 +-- pkg/lb/frontend/middleware_test.go | 4 +- .../e2e-test-lb-forbid-user-query-api.txt | 2 +- .../e2e-test-lb-forbid-user-query-db.txt | 2 +- scripts/e2e-test.sh | 66 ++++++++++--------- 13 files changed, 73 insertions(+), 55 deletions(-) diff --git a/Makefile b/Makefile index 153fe37d..b6e966dd 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,10 @@ STATICCHECK_IGNORE = CGO_BUILD ?= 0 +# Swagger docs +SWAGGER_DIR ?= pkg/api/http +SWAGGER_MAIN ?= server.go + ifeq ($(GOHOSTOS), linux) test-e2e := test-e2e else @@ -240,7 +244,6 @@ skip-test-docker: .PHONY: promtool promtool: $(PROMTOOL) - $(PROMTOOL): mkdir -p $(FIRST_GOPATH)/bin curl -fsS -L $(PROMTOOL_URL) | tar -xvzf - -C $(FIRST_GOPATH)/bin --strip 1 "prometheus-$(PROMTOOL_VERSION).$(GO_BUILD_PLATFORM)/promtool" diff --git a/Makefile.common b/Makefile.common index 7da873d1..42e1da23 100644 --- a/Makefile.common +++ b/Makefile.common @@ -23,6 +23,7 @@ GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') PROMU := $(FIRST_GOPATH)/bin/promu +SWAG := $(FIRST_GOPATH)/bin/swag pkgs = ./... ifeq (arm, $(GOHOSTARCH)) @@ -181,7 +182,11 @@ common-unused: @git diff --exit-code -- go.sum go.mod .PHONY: common-build -common-build: promu +common-build: promu swag +ifeq ($(CGO_BUILD), 1) + @echo ">> updating swagger docs" + $(SWAG) init -d $(SWAGGER_DIR) -g $(SWAGGER_MAIN) -o $(SWAGGER_DIR)/docs --pd --quiet +endif @echo ">> building binaries" $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) @@ -222,7 +227,6 @@ common-docker-manifest: .PHONY: promu promu: $(PROMU) - $(PROMU): $(eval PROMU_TMP := $(shell mktemp -d)) curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) @@ -230,6 +234,11 @@ $(PROMU): cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu rm -r $(PROMU_TMP) +.PHONY: swag +swag: $(SWAG) +$(SWAG): + $(GO) install github.com/swaggo/swag/cmd/swag@latest + .PHONY: proto proto: @echo ">> generating code from proto files" diff --git a/pkg/api/cli/cli_test.go b/pkg/api/cli/cli_test.go index afc3c9d7..e8e681fb 100644 --- a/pkg/api/cli/cli_test.go +++ b/pkg/api/cli/cli_test.go @@ -8,17 +8,19 @@ import ( "path/filepath" "testing" "time" + + "github.com/mahendrapaipuri/ceems/pkg/api/base" ) func setCLIArgs() { - os.Args = append(os.Args, "--resource.manager.slurm") - os.Args = append(os.Args, "--slurm.sacct.path=../testdata/sacct") - os.Args = append(os.Args, "--log.level=error") + // os.Args = append(os.Args, "--resource.manager.slurm") + // os.Args = append(os.Args, "--slurm.sacct.path=../testdata/sacct") + os.Args = append(os.Args, "--log.level=debug") } func queryServer(address string) error { client := &http.Client{} - req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/api/health", address), nil) + req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/api/%s/health", address, base.APIVersion), nil) req.Header.Set("X-Grafana-User", "usr1") resp, err := client.Do(req) if err != nil { @@ -57,6 +59,8 @@ func TestBatchStatsServerMainNestedDirs(t *testing.T) { for i := 0; i < 10; i++ { if err := queryServer("localhost:9020"); err == nil { break + } else { + fmt.Printf("err %s", err) } time.Sleep(500 * time.Millisecond) if i == 9 { diff --git a/pkg/api/http/middleware_test.go b/pkg/api/http/middleware_test.go index 159f5fe0..e69fb425 100644 --- a/pkg/api/http/middleware_test.go +++ b/pkg/api/http/middleware_test.go @@ -101,8 +101,8 @@ func TestMiddlewareAdminFailure(t *testing.T) { handlerToTest.ServeHTTP(w, req) // Should pass test - if w.Result().StatusCode != 401 { - t.Errorf("expected 401 got %d", w.Result().StatusCode) + if w.Result().StatusCode != 403 { + t.Errorf("expected 403 got %d", w.Result().StatusCode) } } @@ -120,8 +120,8 @@ func TestMiddlewareAdminFailurePresetHeader(t *testing.T) { handlerToTest.ServeHTTP(w, req) // Should pass test - if w.Result().StatusCode != 401 { - t.Errorf("expected 401 got %d", w.Result().StatusCode) + if w.Result().StatusCode != 403 { + t.Errorf("expected 403 got %d", w.Result().StatusCode) } // Should not contain adminHeader if req.Header.Get(adminUserHeader) != "" { diff --git a/pkg/api/testdata/output/e2e-test-api-server-admin--denied-query.txt b/pkg/api/testdata/output/e2e-test-api-server-admin--denied-query.txt index 57c302f8..ccc7aa08 100644 --- a/pkg/api/testdata/output/e2e-test-api-server-admin--denied-query.txt +++ b/pkg/api/testdata/output/e2e-test-api-server-admin--denied-query.txt @@ -1 +1 @@ -{"status":"error","errorType":"unauthorized","error":"current user does not have admin privileges"} +{"status":"error","errorType":"forbidden","error":"current user does not have admin privileges"} diff --git a/pkg/api/testdata/output/e2e-test-api-server-current-usage-admin-denied-query.txt b/pkg/api/testdata/output/e2e-test-api-server-current-usage-admin-denied-query.txt index 57c302f8..ccc7aa08 100644 --- a/pkg/api/testdata/output/e2e-test-api-server-current-usage-admin-denied-query.txt +++ b/pkg/api/testdata/output/e2e-test-api-server-current-usage-admin-denied-query.txt @@ -1 +1 @@ -{"status":"error","errorType":"unauthorized","error":"current user does not have admin privileges"} +{"status":"error","errorType":"forbidden","error":"current user does not have admin privileges"} diff --git a/pkg/api/testdata/output/e2e-test-api-verify-fail-query.txt b/pkg/api/testdata/output/e2e-test-api-verify-fail-query.txt index ea766690..9e828293 100644 --- a/pkg/api/testdata/output/e2e-test-api-verify-fail-query.txt +++ b/pkg/api/testdata/output/e2e-test-api-verify-fail-query.txt @@ -1 +1 @@ -{"status":"error","errorType":"unauthorized","error":"user do not have permissions on uuids"} +{"status":"error","errorType":"forbidden","error":"user do not have permissions on uuids"} diff --git a/pkg/api/testdata/output/e2e-test-api-verify-pass-query.txt b/pkg/api/testdata/output/e2e-test-api-verify-pass-query.txt index d63ad864..d7f82459 100644 --- a/pkg/api/testdata/output/e2e-test-api-verify-pass-query.txt +++ b/pkg/api/testdata/output/e2e-test-api-verify-pass-query.txt @@ -1 +1 @@ -{"status":"success","data":{"user":"usr1","uuids":["1479763","1479765"],"verfiy":true}} +{"status":"success","data":[{"user":"usr1","uuids":["1479763","1479765"],"owner":true}]} diff --git a/pkg/lb/frontend/middleware.go b/pkg/lb/frontend/middleware.go index 7e0f2d01..5bc6461d 100644 --- a/pkg/lb/frontend/middleware.go +++ b/pkg/lb/frontend/middleware.go @@ -152,7 +152,7 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler // Write an error and stop the handler chain w.WriteHeader(http.StatusUnauthorized) - response := http_api.Response{ + response := http_api.Response[any]{ Status: "error", ErrorType: "unauthorized", Error: "no user header found", @@ -187,10 +187,10 @@ func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler // Check if user is querying for his/her own compute units by looking to DB if !amw.isUserUnit(loggedUser, queryParams.(*QueryParams).uuids) { // Write an error and stop the handler chain - w.WriteHeader(http.StatusUnauthorized) - response := http_api.Response{ + w.WriteHeader(http.StatusForbidden) + response := http_api.Response[any]{ Status: "error", - ErrorType: "unauthorized", + ErrorType: "forbidden", Error: "user do not have permissions to view unit metrics", } if err := json.NewEncoder(w).Encode(&response); err != nil { diff --git a/pkg/lb/frontend/middleware_test.go b/pkg/lb/frontend/middleware_test.go index f9859877..75d59ae2 100644 --- a/pkg/lb/frontend/middleware_test.go +++ b/pkg/lb/frontend/middleware_test.go @@ -136,7 +136,7 @@ func TestMiddlewareWithDB(t *testing.T) { req: "/test?query=foo{uuid=~\"1479765|1481510\"}", user: "usr1", header: true, - code: 401, + code: 403, }, { name: "allow query for admins", @@ -150,7 +150,7 @@ func TestMiddlewareWithDB(t *testing.T) { req: "/test?query=foo{uuid=~\"123|345\"}", user: "usr1", header: true, - code: 401, + code: 403, }, { name: "forbid due to missing header", diff --git a/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-api.txt b/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-api.txt index 75b16ef1..98810b5f 100644 --- a/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-api.txt +++ b/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-api.txt @@ -1 +1 @@ -{"status":"error","errorType":"unauthorized","error":"user do not have permissions to view unit metrics"} +{"status":"error","errorType":"forbidden","error":"user do not have permissions to view unit metrics"} diff --git a/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-db.txt b/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-db.txt index 75b16ef1..98810b5f 100644 --- a/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-db.txt +++ b/pkg/lb/testdata/output/e2e-test-lb-forbid-user-query-db.txt @@ -1 +1 @@ -{"status":"error","errorType":"unauthorized","error":"user do not have permissions to view unit metrics"} +{"status":"error","errorType":"forbidden","error":"user do not have permissions to view unit metrics"} diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index 8e2e07b1..c9006792 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -11,6 +11,8 @@ skip_re="^(go_|ceems_exporter_build_info|ceems_scrape_collector_duration_seconds arch="$(uname -m)" +api_version="v1" + scenario="exporter-cgroups-v1"; keep=0; update=0; verbose=0 while getopts 'hs:kuv' opt do @@ -84,67 +86,67 @@ then if [ "${scenario}" = "api-project-query" ] then - desc="/api/projects end point test" + desc="/projects end point test" fixture='pkg/api/testdata/output/e2e-test-api-server-project-query.txt' elif [ "${scenario}" = "api-uuid-query" ] then - desc="/api/units end point test with uuid query param" + desc="/units end point test with uuid query param" fixture='pkg/api/testdata/output/e2e-test-api-server-uuid-query.txt' elif [ "${scenario}" = "api-running-query" ] then - desc="/api/units end point test with running query param" + desc="/units end point test with running query param" fixture='pkg/api/testdata/output/e2e-test-api-server-running-query.txt' elif [ "${scenario}" = "api-admin-query" ] then - desc="/api/units/admin end point test for admin query" + desc="/units/admin end point test for admin query" fixture='pkg/api/testdata/output/e2e-test-api-server-admin-query.txt' elif [ "${scenario}" = "api-admin-query-all" ] then - desc="/api/units/admin end point test for admin query for all jobs" + desc="/units/admin end point test for admin query for all jobs" fixture='pkg/api/testdata/output/e2e-test-api-server-admin-query-all.txt' elif [ "${scenario}" = "api-admin-query-all-selected-fields" ] then - desc="/api/units/admin end point test for admin query for all jobs with selected fields" + desc="/units/admin end point test for admin query for all jobs with selected fields" fixture='pkg/api/testdata/output/e2e-test-api-server-admin-query-all-selected-fields.txt' elif [ "${scenario}" = "api-admin-denied-query" ] then - desc="/api/units/admin end point test for denied request" + desc="/units/admin end point test for denied request" fixture='pkg/api/testdata/output/e2e-test-api-server-admin--denied-query.txt' elif [ "${scenario}" = "api-current-usage-query" ] then - desc="/api/usage/current end point test" + desc="/usage/current end point test" fixture='pkg/api/testdata/output/e2e-test-api-server-current-usage-query.txt' elif [ "${scenario}" = "api-global-usage-query" ] then - desc="/api/usage/global end point test" + desc="/usage/global end point test" fixture='pkg/api/testdata/output/e2e-test-api-server-global-usage-query.txt' elif [ "${scenario}" = "api-current-usage-admin-query" ] then - desc="/api/usage/current/admin end point test" + desc="/usage/current/admin end point test" fixture='pkg/api/testdata/output/e2e-test-api-server-current-usage-admin-query.txt' elif [ "${scenario}" = "api-global-usage-admin-query" ] then - desc="/api/usage/global/admin end point test" + desc="/usage/global/admin end point test" fixture='pkg/api/testdata/output/e2e-test-api-server-global-usage-admin-query.txt' elif [ "${scenario}" = "api-current-usage-admin-denied-query" ] then - desc="/api/usage/current/admin end point test" + desc="/usage/current/admin end point test" fixture='pkg/api/testdata/output/e2e-test-api-server-current-usage-admin-denied-query.txt' elif [ "${scenario}" = "api-verify-pass-query" ] then - desc="/api/units/verify end point test with pass request" + desc="/units/verify end point test with pass request" fixture='pkg/api/testdata/output/e2e-test-api-verify-pass-query.txt' elif [ "${scenario}" = "api-verify-fail-query" ] then - desc="/api/units/verify end point test with fail request" + desc="/units/verify end point test with fail request" fixture='pkg/api/testdata/output/e2e-test-api-verify-fail-query.txt' elif [ "${scenario}" = "api-demo-units-query" ] then - desc="/api/units/demo end point test" + desc="/units/demo end point test" fixture='pkg/api/testdata/output/e2e-test-api-demo-units-query.txt' elif [ "${scenario}" = "api-demo-usage-query" ] then - desc="/api/usage/demo end point test" + desc="/usage/demo end point test" fixture='pkg/api/testdata/output/e2e-test-api-demo-usage-query.txt' fi @@ -388,52 +390,52 @@ then if [ "${scenario}" = "api-project-query" ] then - get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/projects" > "${fixture_output}" + get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/${api_version}/projects" > "${fixture_output}" elif [ "${scenario}" = "api-uuid-query" ] then - get -H "X-Grafana-User: usr2" "127.0.0.1:${port}/api/units?uuid=1481508&project=acc2" > "${fixture_output}" + get -H "X-Grafana-User: usr2" "127.0.0.1:${port}/api/${api_version}/units?uuid=1481508&project=acc2" > "${fixture_output}" elif [ "${scenario}" = "api-running-query" ] then - get -H "X-Grafana-User: usr3" "127.0.0.1:${port}/api/units?running&from=1676934000&to=1677538800&field=uuid&field=state&field=started_at&field=allocation&field=tags" > "${fixture_output}" + get -H "X-Grafana-User: usr3" "127.0.0.1:${port}/api/${api_version}/units?running&from=1676934000&to=1677538800&field=uuid&field=state&field=started_at&field=allocation&field=tags" > "${fixture_output}" elif [ "${scenario}" = "api-admin-query" ] then - get -H "X-Grafana-User: grafana" -H "X-Dashboard-User: usr3" "127.0.0.1:${port}/api/units?project=acc3&from=1676934000&to=1677538800" > "${fixture_output}" + get -H "X-Grafana-User: grafana" -H "X-Dashboard-User: usr3" "127.0.0.1:${port}/api/${api_version}/units?project=acc3&from=1676934000&to=1677538800" > "${fixture_output}" elif [ "${scenario}" = "api-admin-query-all" ] then - get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/units/admin?from=1676934000&to=1677538800" > "${fixture_output}" + get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/${api_version}/units/admin?from=1676934000&to=1677538800" > "${fixture_output}" elif [ "${scenario}" = "api-admin-query-all-selected-fields" ] then - get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/units/admin?from=1676934000&to=1677538800&field=uuid&field=started_at&field=ended_at&field=foo" > "${fixture_output}" + get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/${api_version}/units/admin?from=1676934000&to=1677538800&field=uuid&field=started_at&field=ended_at&field=foo" > "${fixture_output}" elif [ "${scenario}" = "api-admin-denied-query" ] then - get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/units/admin" > "${fixture_output}" + get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/${api_version}/units/admin" > "${fixture_output}" elif [ "${scenario}" = "api-current-usage-query" ] then - get -H "X-Grafana-User: usr3" "127.0.0.1:${port}/api/usage/current?from=1676934000&to=1677538800" > "${fixture_output}" + get -H "X-Grafana-User: usr3" "127.0.0.1:${port}/api/${api_version}/usage/current?from=1676934000&to=1677538800" > "${fixture_output}" elif [ "${scenario}" = "api-global-usage-query" ] then - get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/usage/global?field=usr&field=project&field=num_units" > "${fixture_output}" + get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/${api_version}/usage/global?field=usr&field=project&field=num_units" > "${fixture_output}" elif [ "${scenario}" = "api-current-usage-admin-query" ] then - get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/usage/current/admin?user=usr3&from=1676934000&to=1677538800" > "${fixture_output}" + get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/${api_version}/usage/current/admin?user=usr3&from=1676934000&to=1677538800" > "${fixture_output}" elif [ "${scenario}" = "api-global-usage-admin-query" ] then - get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/usage/global/admin?field=usr&field=project&field=num_units" > "${fixture_output}" + get -H "X-Grafana-User: grafana" "127.0.0.1:${port}/api/${api_version}/usage/global/admin?field=usr&field=project&field=num_units" > "${fixture_output}" elif [ "${scenario}" = "api-current-usage-admin-denied-query" ] then - get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/usage/global/admin?user=usr2" > "${fixture_output}" + get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/${api_version}/usage/global/admin?user=usr2" > "${fixture_output}" elif [ "${scenario}" = "api-verify-pass-query" ] then - get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/units/verify?uuid=1479763&uuid=1479765" > "${fixture_output}" + get -H "X-Grafana-User: usr1" "127.0.0.1:${port}/api/${api_version}/units/verify?uuid=1479763&uuid=1479765" > "${fixture_output}" elif [ "${scenario}" = "api-verify-fail-query" ] then - get -H "X-Grafana-User: usr2" "127.0.0.1:${port}/api/units/verify?uuid=1479763&uuid=11508" > "${fixture_output}" + get -H "X-Grafana-User: usr2" "127.0.0.1:${port}/api/${api_version}/units/verify?uuid=1479763&uuid=11508" > "${fixture_output}" elif [ "${scenario}" = "api-demo-units-query" ] then - get -s -o /dev/null -w "%{http_code}" "127.0.0.1:${port}/api/units/demo" > "${fixture_output}" + get -s -o /dev/null -w "%{http_code}" "127.0.0.1:${port}/api/${api_version}/units/demo" > "${fixture_output}" elif [ "${scenario}" = "api-demo-usage-query" ] then - get -s -o /dev/null -w "%{http_code}" "127.0.0.1:${port}/api/usage/demo" > "${fixture_output}" + get -s -o /dev/null -w "%{http_code}" "127.0.0.1:${port}/api/${api_version}/usage/demo" > "${fixture_output}" fi elif [[ "${scenario}" =~ ^"lb" ]]