Skip to content

Commit 267473f

Browse files
authored
Support hosting swagger ui in apiserver (ray-project#344)
* Support hosting swagger ui in apiserver - Copy swagger-ui dist to third-party folder - Support serving swagger-ui and swagger json in apiserver * Format auto-generated datafile.go apiserver/pkg/swagger/datafile.go fails the gofmt testing and I feel it's better to format the file instead of exclude it from the CI check.
1 parent 15cdd55 commit 267473f

22 files changed

+893
-3
lines changed

apiserver/Dockerfile

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o kuberay-apiserver cmd/m
2323
# Use distroless as minimal base image to package the manager binary
2424
# Refer to https://github.com/GoogleContainerTools/distroless for more details
2525
FROM gcr.io/distroless/static:nonroot
26-
WORKDIR /
27-
COPY --from=builder /workspace/apiserver/kuberay-apiserver .
26+
WORKDIR /workspace
27+
COPY --from=builder /workspace/apiserver/kuberay-apiserver apiserver/
28+
# Support serving swagger files
29+
COPY proto/ proto/
2830
USER 65532:65532
2931

30-
ENTRYPOINT ["/kuberay-apiserver"]
32+
ENTRYPOINT ["/workspace/apiserver/kuberay-apiserver"]

apiserver/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,19 @@ GET {{baseUrl}}/apis/v1alpha2/namespaces/<namespace>/clusters/<cluster_name>
165165
```
166166
DELETE {{baseUrl}}/apis/v1alpha2/namespaces/<namespace>/clusters/<cluster_name>
167167
```
168+
169+
170+
## Swagger Support
171+
172+
1. Download Swagger UI from [Swagger-UI](https://swagger.io/tools/swagger-ui/download/). In this case, we use `swagger-ui-3.51.2.tar.gz`
173+
2. Unzip package and copy `dist` folder to `third_party` folder
174+
3. Use `go-bindata` to generate go code from static files.
175+
176+
```
177+
mkdir third_party
178+
tar -zvxf ~/Downloads/swagger-ui-3.51.2.tar.gz /tmp
179+
mv /tmp/swagger-ui-3.51.2/dist third_party/swagger-ui
180+
181+
cd apiserver/
182+
go-bindata --nocompress --pkg swagger -o pkg/swagger/datafile.go ./third_party/swagger-ui/...
183+
```

apiserver/cmd/main.go

+39
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package main
33
import (
44
"context"
55
"flag"
6+
67
"math"
78
"net"
89
"net/http"
10+
"path"
11+
"strings"
912

13+
assetfs "github.com/elazarl/go-bindata-assetfs"
1014
"github.com/grpc-ecosystem/grpc-gateway/runtime"
1115

1216
"google.golang.org/grpc"
@@ -19,6 +23,7 @@ import (
1923
"github.com/ray-project/kuberay/apiserver/pkg/interceptor"
2024
"github.com/ray-project/kuberay/apiserver/pkg/manager"
2125
"github.com/ray-project/kuberay/apiserver/pkg/server"
26+
"github.com/ray-project/kuberay/apiserver/pkg/swagger"
2227
api "github.com/ray-project/kuberay/proto/go_client"
2328
)
2429

@@ -86,6 +91,8 @@ func startHttpProxy() {
8691
// Seems /apis (matches /apis/v1alpha1/clusters) works fine
8792
topMux.Handle("/", runtimeMux)
8893
topMux.Handle("/metrics", promhttp.Handler())
94+
topMux.HandleFunc("/swagger/", serveSwaggerFile)
95+
serveSwaggerUI(topMux)
8996

9097
if err := http.ListenAndServe(*httpPortFlag, topMux); err != nil {
9198
klog.Fatal(err)
@@ -94,6 +101,38 @@ func startHttpProxy() {
94101
klog.Info("Http Proxy started")
95102
}
96103

104+
func serveSwaggerFile(w http.ResponseWriter, r *http.Request) {
105+
klog.Info("start serveSwaggerFile")
106+
107+
if !strings.HasSuffix(r.URL.Path, "swagger.json") {
108+
klog.Errorf("Not Found: %s", r.URL.Path)
109+
http.NotFound(w, r)
110+
return
111+
}
112+
113+
p := strings.TrimPrefix(r.URL.Path, "/swagger/")
114+
// Currently, we copy swagger.json to system root /workspace/proto/swagger/.
115+
// For the development, you can change path to `../proto/swagger`.
116+
// TODO(Jeffwan@): fix this later, we should not have dependency on system folder structure.
117+
p = path.Join("/workspace/proto/swagger/", p)
118+
119+
klog.Infof("Serving swagger-file: %s", p)
120+
http.ServeFile(w, r, p)
121+
}
122+
123+
// go-bindata --nocompress --pkg swagger -o pkg/swagger/datafile.go third_party/swagger-ui/...
124+
// We will need to copy third_party folder to `backend` folder when building images
125+
func serveSwaggerUI(mux *http.ServeMux) {
126+
fileServer := http.FileServer(&assetfs.AssetFS{
127+
Asset: swagger.Asset,
128+
AssetDir: swagger.AssetDir,
129+
Prefix: "third_party/swagger-ui",
130+
})
131+
132+
prefix := "/swagger-ui/"
133+
mux.Handle(prefix, http.StripPrefix(prefix, fileServer))
134+
}
135+
97136
func registerHttpHandlerFromEndpoint(handler RegisterHttpHandlerFromEndpoint, serviceName string, ctx context.Context, mux *runtime.ServeMux) {
98137
endpoint := "localhost" + *rpcPortFlag
99138
opts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32))}

apiserver/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ require (
3030
github.com/beorn7/perks v1.0.1 // indirect
3131
github.com/cespare/xxhash/v2 v2.1.1 // indirect
3232
github.com/davecgh/go-spew v1.1.1 // indirect
33+
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
3334
github.com/go-logr/logr v1.2.0 // indirect
3435
github.com/go-openapi/errors v0.19.6 // indirect
3536
github.com/go-openapi/strfmt v0.19.5 // indirect

apiserver/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
116116
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
117117
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
118118
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
119+
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
120+
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
119121
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
120122
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
121123
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=

apiserver/pkg/swagger/datafile.go

+670
Large diffs are not rendered by default.
665 Bytes
Loading
628 Bytes
Loading

third_party/swagger-ui/index.html

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!-- HTML for static distribution bundle build -->
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="UTF-8">
6+
<title>Swagger UI</title>
7+
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
8+
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
9+
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
10+
<style>
11+
html
12+
{
13+
box-sizing: border-box;
14+
overflow: -moz-scrollbars-vertical;
15+
overflow-y: scroll;
16+
}
17+
18+
*,
19+
*:before,
20+
*:after
21+
{
22+
box-sizing: inherit;
23+
}
24+
25+
body
26+
{
27+
margin:0;
28+
background: #fafafa;
29+
}
30+
</style>
31+
</head>
32+
33+
<body>
34+
<div id="swagger-ui"></div>
35+
36+
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
37+
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
38+
<script>
39+
window.onload = function() {
40+
// Begin Swagger UI call region
41+
const ui = SwaggerUIBundle({
42+
url: "https://petstore.swagger.io/v2/swagger.json",
43+
dom_id: '#swagger-ui',
44+
deepLinking: true,
45+
presets: [
46+
SwaggerUIBundle.presets.apis,
47+
SwaggerUIStandalonePreset
48+
],
49+
plugins: [
50+
SwaggerUIBundle.plugins.DownloadUrl
51+
],
52+
layout: "StandaloneLayout"
53+
});
54+
// End Swagger UI call region
55+
56+
window.ui = ui;
57+
};
58+
</script>
59+
</body>
60+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<title>Swagger UI: OAuth2 Redirect</title>
5+
</head>
6+
<body>
7+
<script>
8+
'use strict';
9+
function run () {
10+
var oauth2 = window.opener.swaggerUIRedirectOauth2;
11+
var sentState = oauth2.state;
12+
var redirectUrl = oauth2.redirectUrl;
13+
var isValid, qp, arr;
14+
15+
if (/code|token|error/.test(window.location.hash)) {
16+
qp = window.location.hash.substring(1);
17+
} else {
18+
qp = location.search.substring(1);
19+
}
20+
21+
arr = qp.split("&");
22+
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
23+
qp = qp ? JSON.parse('{' + arr.join() + '}',
24+
function (key, value) {
25+
return key === "" ? value : decodeURIComponent(value);
26+
}
27+
) : {};
28+
29+
isValid = qp.state === sentState;
30+
31+
if ((
32+
oauth2.auth.schema.get("flow") === "accessCode" ||
33+
oauth2.auth.schema.get("flow") === "authorizationCode" ||
34+
oauth2.auth.schema.get("flow") === "authorization_code"
35+
) && !oauth2.auth.code) {
36+
if (!isValid) {
37+
oauth2.errCb({
38+
authId: oauth2.auth.name,
39+
source: "auth",
40+
level: "warning",
41+
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
42+
});
43+
}
44+
45+
if (qp.code) {
46+
delete oauth2.state;
47+
oauth2.auth.code = qp.code;
48+
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
49+
} else {
50+
let oauthErrorMsg;
51+
if (qp.error) {
52+
oauthErrorMsg = "["+qp.error+"]: " +
53+
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
54+
(qp.error_uri ? "More info: "+qp.error_uri : "");
55+
}
56+
57+
oauth2.errCb({
58+
authId: oauth2.auth.name,
59+
source: "auth",
60+
level: "error",
61+
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
62+
});
63+
}
64+
} else {
65+
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
66+
}
67+
window.close();
68+
}
69+
70+
window.addEventListener('DOMContentLoaded', function () {
71+
run();
72+
});
73+
</script>
74+
</body>
75+
</html>

third_party/swagger-ui/swagger-ui-bundle.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui-bundle.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui-es-bundle-core.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui-es-bundle-core.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui-es-bundle.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui-es-bundle.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui-standalone-preset.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui-standalone-preset.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui.css

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui.css.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

third_party/swagger-ui/swagger-ui.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)