Skip to content

Commit

Permalink
Support hosting swagger ui in apiserver (ray-project#344)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
Jeffwan authored Jul 7, 2022
1 parent f1f9721 commit 6ee7876
Show file tree
Hide file tree
Showing 22 changed files with 893 additions and 3 deletions.
8 changes: 5 additions & 3 deletions apiserver/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o kuberay-apiserver cmd/m
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/apiserver/kuberay-apiserver .
WORKDIR /workspace
COPY --from=builder /workspace/apiserver/kuberay-apiserver apiserver/
# Support serving swagger files
COPY proto/ proto/
USER 65532:65532

ENTRYPOINT ["/kuberay-apiserver"]
ENTRYPOINT ["/workspace/apiserver/kuberay-apiserver"]
16 changes: 16 additions & 0 deletions apiserver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,19 @@ GET {{baseUrl}}/apis/v1alpha2/namespaces/<namespace>/clusters/<cluster_name>
```
DELETE {{baseUrl}}/apis/v1alpha2/namespaces/<namespace>/clusters/<cluster_name>
```


## Swagger Support

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`
2. Unzip package and copy `dist` folder to `third_party` folder
3. Use `go-bindata` to generate go code from static files.

```
mkdir third_party
tar -zvxf ~/Downloads/swagger-ui-3.51.2.tar.gz /tmp
mv /tmp/swagger-ui-3.51.2/dist third_party/swagger-ui
cd apiserver/
go-bindata --nocompress --pkg swagger -o pkg/swagger/datafile.go ./third_party/swagger-ui/...
```
39 changes: 39 additions & 0 deletions apiserver/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ package main
import (
"context"
"flag"

"math"
"net"
"net/http"
"path"
"strings"

assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/grpc-ecosystem/grpc-gateway/runtime"

"google.golang.org/grpc"
Expand All @@ -19,6 +23,7 @@ import (
"github.com/ray-project/kuberay/apiserver/pkg/interceptor"
"github.com/ray-project/kuberay/apiserver/pkg/manager"
"github.com/ray-project/kuberay/apiserver/pkg/server"
"github.com/ray-project/kuberay/apiserver/pkg/swagger"
api "github.com/ray-project/kuberay/proto/go_client"
)

Expand Down Expand Up @@ -86,6 +91,8 @@ func startHttpProxy() {
// Seems /apis (matches /apis/v1alpha1/clusters) works fine
topMux.Handle("/", runtimeMux)
topMux.Handle("/metrics", promhttp.Handler())
topMux.HandleFunc("/swagger/", serveSwaggerFile)
serveSwaggerUI(topMux)

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

func serveSwaggerFile(w http.ResponseWriter, r *http.Request) {
klog.Info("start serveSwaggerFile")

if !strings.HasSuffix(r.URL.Path, "swagger.json") {
klog.Errorf("Not Found: %s", r.URL.Path)
http.NotFound(w, r)
return
}

p := strings.TrimPrefix(r.URL.Path, "/swagger/")
// Currently, we copy swagger.json to system root /workspace/proto/swagger/.
// For the development, you can change path to `../proto/swagger`.
// TODO(Jeffwan@): fix this later, we should not have dependency on system folder structure.
p = path.Join("/workspace/proto/swagger/", p)

klog.Infof("Serving swagger-file: %s", p)
http.ServeFile(w, r, p)
}

// go-bindata --nocompress --pkg swagger -o pkg/swagger/datafile.go third_party/swagger-ui/...
// We will need to copy third_party folder to `backend` folder when building images
func serveSwaggerUI(mux *http.ServeMux) {
fileServer := http.FileServer(&assetfs.AssetFS{
Asset: swagger.Asset,
AssetDir: swagger.AssetDir,
Prefix: "third_party/swagger-ui",
})

prefix := "/swagger-ui/"
mux.Handle(prefix, http.StripPrefix(prefix, fileServer))
}

func registerHttpHandlerFromEndpoint(handler RegisterHttpHandlerFromEndpoint, serviceName string, ctx context.Context, mux *runtime.ServeMux) {
endpoint := "localhost" + *rpcPortFlag
opts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32))}
Expand Down
1 change: 1 addition & 0 deletions apiserver/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-openapi/errors v0.19.6 // indirect
github.com/go-openapi/strfmt v0.19.5 // indirect
Expand Down
2 changes: 2 additions & 0 deletions apiserver/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
Expand Down
670 changes: 670 additions & 0 deletions apiserver/pkg/swagger/datafile.go

Large diffs are not rendered by default.

Binary file added third_party/swagger-ui/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added third_party/swagger-ui/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions third_party/swagger-ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}

*,
*:before,
*:after
{
box-sizing: inherit;
}

body
{
margin:0;
background: #fafafa;
}
</style>
</head>

<body>
<div id="swagger-ui"></div>

<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "https://petstore.swagger.io/v2/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region

window.ui = ui;
};
</script>
</body>
</html>
75 changes: 75 additions & 0 deletions third_party/swagger-ui/oauth2-redirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;

if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}

arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};

isValid = qp.state === sentState;

if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}

if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}

oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}

window.addEventListener('DOMContentLoaded', function () {
run();
});
</script>
</body>
</html>
3 changes: 3 additions & 0 deletions third_party/swagger-ui/swagger-ui-bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions third_party/swagger-ui/swagger-ui-bundle.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions third_party/swagger-ui/swagger-ui-es-bundle-core.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions third_party/swagger-ui/swagger-ui-es-bundle-core.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions third_party/swagger-ui/swagger-ui-es-bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions third_party/swagger-ui/swagger-ui-es-bundle.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions third_party/swagger-ui/swagger-ui-standalone-preset.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions third_party/swagger-ui/swagger-ui-standalone-preset.js.map

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions third_party/swagger-ui/swagger-ui.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions third_party/swagger-ui/swagger-ui.css.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions third_party/swagger-ui/swagger-ui.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions third_party/swagger-ui/swagger-ui.js.map

Large diffs are not rendered by default.

0 comments on commit 6ee7876

Please sign in to comment.