Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add swagger ui API explorer #5970

Merged
merged 7 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
node_modules

# Docs
assets
# assets are useful for swagger-ui and relatively lightweight
# https://github.com/ChainSafe/lodestar/pull/5970#discussion_r1334420451
# assets
supporting-docs
docs
typedocs
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/beacon/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function registerRoutes(
}

for (const route of Object.values(routes())) {
registerRoute(server, route);
registerRoute(server, route, namespace);
}
}
}
6 changes: 4 additions & 2 deletions packages/api/src/utils/server/registerRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {ServerInstance, RouteConfig, ServerRoute} from "./types.js";
export function registerRoute(
server: ServerInstance,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
route: ServerRoute<any>
route: ServerRoute<any>,
namespace?: string
): void {
server.route({
url: route.url,
method: route.method,
handler: route.handler,
schema: route.schema,
// append the namespace as a tag for downstream consumption of our API schema, eg: for swagger UI
schema: {...route.schema, ...(namespace ? {tags: [namespace]} : undefined), operationId: route.id},
config: {operationId: route.id} as RouteConfig,
});
}
2 changes: 2 additions & 0 deletions packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
"@ethersproject/abi": "^5.7.0",
"@fastify/bearer-auth": "^9.0.0",
"@fastify/cors": "^8.2.1",
"@fastify/swagger": "^8.10.0",
"@fastify/swagger-ui": "^1.9.3",
"@libp2p/bootstrap": "^9.0.2",
"@libp2p/interface": "^0.1.1",
"@libp2p/mdns": "^9.0.2",
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/api/rest/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type RestApiServerOpts = {
bearerToken?: string;
headerLimit?: number;
bodyLimit?: number;
swaggerUI?: boolean;
};

export type RestApiServerModules = {
Expand All @@ -38,7 +39,7 @@ export class RestApiServer {
private readonly activeSockets: HttpActiveSocketsTracker;

constructor(
private readonly opts: RestApiServerOpts,
protected readonly opts: RestApiServerOpts,
modules: RestApiServerModules
) {
// Apply opts defaults
Expand Down
15 changes: 14 additions & 1 deletion packages/beacon-node/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {ErrorAborted, Logger} from "@lodestar/utils";
import {ChainForkConfig} from "@lodestar/config";
import {NodeIsSyncing} from "../impl/errors.js";
import {RestApiServer, RestApiServerModules, RestApiServerMetrics, RestApiServerOpts} from "./base.js";
import {registerSwaggerUIRoutes} from "./swaggerUI.js";

export {allNamespaces} from "@lodestar/api";

export type BeaconRestApiServerOpts = Omit<RestApiServerOpts, "bearerToken"> & {
Expand Down Expand Up @@ -33,13 +35,24 @@ export type BeaconRestApiServerModules = RestApiServerModules & {
* REST API powered by `fastify` server.
*/
export class BeaconRestApiServer extends RestApiServer {
readonly opts: BeaconRestApiServerOpts;
readonly modules: BeaconRestApiServerModules;

constructor(optsArg: Partial<BeaconRestApiServerOpts>, modules: BeaconRestApiServerModules) {
const opts = {...beaconRestApiServerOpts, ...optsArg};

super(opts, modules);

this.opts = opts;
this.modules = modules;
}

async registerRoutes(version?: string): Promise<void> {
if (this.opts.swaggerUI) {
await registerSwaggerUIRoutes(this.server, this.opts, version);
}
// Instantiate and register the routes with matching namespace in `opts.api`
registerRoutes(this.server, modules.config, modules.api, opts.api);
registerRoutes(this.server, this.modules.config, this.modules.api, this.opts.api);
}

protected shouldIgnoreError(err: Error): boolean {
Expand Down
81 changes: 81 additions & 0 deletions packages/beacon-node/src/api/rest/swaggerUI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {FastifyInstance} from "fastify";
import {BeaconRestApiServerOpts} from "./index.js";

export async function registerSwaggerUIRoutes(
server: FastifyInstance,
opts: BeaconRestApiServerOpts,
version = ""
): Promise<void> {
await server.register(await import("@fastify/swagger"), {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openapi: {
info: {
title: "Lodestar API",
description: "API specification for the Lodestar beacon node",
version,
contact: {
name: "Lodestar Github",
url: "https://github.com/chainsafe/lodestar",
},
},
externalDocs: {
url: "https://chainsafe.github.io/lodestar",
description: "Lodestar documentation",
},
tags: opts.api.map((namespace) => ({name: namespace})),
},
});
await server.register(await import("@fastify/swagger-ui"), {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theme: {
title: "Lodestar API",
favicon: await getFavicon(),
},
logo: await getLogo(),
});
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Fallback-friendly function to get an asset
*/
async function getAsset(name: string): Promise<Buffer | undefined> {
try {
const path = await import("node:path");
const fs = await import("node:fs/promises");
const url = await import("node:url");
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
return await fs.readFile(path.join(__dirname, "../../../../../assets/", name));
Copy link
Member

@nflaig nflaig Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed we don't copy assets in our docker build

assets

I think we can just include them, size impact is relatively low

1.2M    assets
288.1M  node_modules

} catch (e) {
return undefined;
}
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function getFavicon() {
const content = await getAsset("round-icon.ico");
if (!content) {
return undefined;
}

return [
{
filename: "round-icon.ico",
rel: "icon",
sizes: "16x16",
type: "image/x-icon",
content,
},
];
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function getLogo() {
const content = await getAsset("lodestar_icon_text_white.png");
if (!content) {
return undefined;
}

return {
type: "image/png",
content,
};
}
1 change: 1 addition & 0 deletions packages/beacon-node/src/node/nodejs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export class BeaconNode {
metrics: metrics ? metrics.apiRest : null,
});
if (opts.api.rest.enabled) {
await restApi.registerRoutes(opts.api.version);
await restApi.listen();
}

Expand Down
9 changes: 9 additions & 0 deletions packages/beacon-node/test/unit/api/impl/swaggerUI.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {expect} from "chai";
import {getFavicon, getLogo} from "../../../../src/api/rest/swaggerUI.js";

describe("swaggerUI", () => {
it("should find the favicon and logo", async () => {
expect(await getFavicon()).to.not.be.undefined;
expect(await getLogo()).to.not.be.undefined;
});
});
4 changes: 4 additions & 0 deletions packages/cli/src/cmds/dev/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ const externalOptionsOverrides: Partial<Record<"network" | keyof typeof beaconNo
defaultDescription: undefined,
default: true,
},
"rest.swaggerUI": {
...beaconNodeOptions["rest.swaggerUI"],
default: true,
},
};

export const devOptions = {
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/options/beaconNodeOptions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ApiArgs = {
"rest.port": number;
"rest.headerLimit"?: number;
"rest.bodyLimit"?: number;
"rest.swaggerUI"?: boolean;
};

export function parseArgs(args: ApiArgs): IBeaconNodeOptions["api"] {
Expand All @@ -25,6 +26,7 @@ export function parseArgs(args: ApiArgs): IBeaconNodeOptions["api"] {
port: args["rest.port"],
headerLimit: args["rest.headerLimit"],
bodyLimit: args["rest.bodyLimit"],
swaggerUI: args["rest.swaggerUI"],
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
},
};
}
Expand Down Expand Up @@ -89,4 +91,11 @@ export const options: CliCommandOptions<ApiArgs> = {
type: "number",
description: "Defines the maximum payload, in bytes, the server is allowed to accept",
},

"rest.swaggerUI": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also support this for the validator keymanager API?

"keymanager.bodyLimit"?: number;

For keymanager would also be nice to support bearer auth through the explorer but again might not be worth the effort.

type: "boolean",
description: "Enable Swagger UI for API exploration at http://{address}:{port}/documentation",
default: Boolean(defaultOptions.api.rest.swaggerUI),
group: "api",
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential other config options

  • path
  • server url (would be required if node runs behind proxy)

Depending on what use cases we see for this, might not be worth to add those now.

};
87 changes: 84 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,11 @@
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"

"@fastify/accept-negotiator@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz#c1c66b3b771c09742a54dd5bc87c582f6b0630ff"
integrity sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==

"@fastify/ajv-compiler@^3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz#459bff00fefbf86c96ec30e62e933d2379e46670"
Expand Down Expand Up @@ -1267,6 +1272,51 @@
dependencies:
fast-json-stringify "^5.7.0"

"@fastify/send@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@fastify/send/-/send-2.1.0.tgz#1aa269ccb4b0940a2dadd1f844443b15d8224ea0"
integrity sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==
dependencies:
"@lukeed/ms" "^2.0.1"
escape-html "~1.0.3"
fast-decode-uri-component "^1.0.1"
http-errors "2.0.0"
mime "^3.0.0"

"@fastify/static@^6.0.0":
version "6.11.2"
resolved "https://registry.yarnpkg.com/@fastify/static/-/static-6.11.2.tgz#1fe40c40daf055a28d29db807b459fcff431d9b6"
integrity sha512-EH7mh7q4MfNdT7N07ZVlwsX/ObngMvQ7KBP0FXAuPov99Fjn80KSJMdxQhhYKAKWW1jXiFdrk8X7d6uGWdZFxg==
dependencies:
"@fastify/accept-negotiator" "^1.0.0"
"@fastify/send" "^2.0.0"
content-disposition "^0.5.3"
fastify-plugin "^4.0.0"
glob "^8.0.1"
p-limit "^3.1.0"

"@fastify/swagger-ui@^1.9.3":
version "1.9.3"
resolved "https://registry.yarnpkg.com/@fastify/swagger-ui/-/swagger-ui-1.9.3.tgz#1ec03ea2595cb2e7d6de6ae7c949bebcff8370a5"
integrity sha512-YYqce4CydjDIEry6Zo4JLjVPe5rjS8iGnk3fHiIQnth9sFSLeyG0U1DCH+IyYmLddNDg1uWJOuErlVqnu/jI3w==
dependencies:
"@fastify/static" "^6.0.0"
fastify-plugin "^4.0.0"
openapi-types "^12.0.2"
rfdc "^1.3.0"
yaml "^2.2.2"

"@fastify/swagger@^8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@fastify/swagger/-/swagger-8.10.0.tgz#d978ae9f2d802ab652955d02be7a125f7f6d9f05"
integrity sha512-0o6nd0qWpJbVSv/vbK4bzHSYe7l+PTGPqrQVwWIXVGd7CvXr585SBx+h8EgrMOY80bcOnGreqnjYFOV0osGP5A==
dependencies:
fastify-plugin "^4.0.0"
json-schema-resolver "^2.0.0"
openapi-types "^12.0.0"
rfdc "^1.3.0"
yaml "^2.2.2"

"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3":
version "1.1.3"
resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz"
Expand Down Expand Up @@ -1732,6 +1782,11 @@
private-ip "^3.0.0"
uint8arraylist "^2.4.3"

"@lukeed/ms@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@lukeed/ms/-/ms-2.0.1.tgz#3c2bbc258affd9cc0e0cc7828477383c73afa6ee"
integrity sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==

"@multiformats/mafmt@^12.1.2":
version "12.1.6"
resolved "https://registry.yarnpkg.com/@multiformats/mafmt/-/mafmt-12.1.6.tgz#e7c1831c1e94c94932621826049afc89f3ad43b7"
Expand Down Expand Up @@ -5125,6 +5180,13 @@ constants-browserify@^1.0.0:
resolved "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz"
integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=

content-disposition@^0.5.3:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
dependencies:
safe-buffer "5.2.1"

content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
Expand Down Expand Up @@ -8747,6 +8809,15 @@ json-parse-even-better-errors@^3.0.0:
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz#2cb2ee33069a78870a0c7e3da560026b89669cf7"
integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==

json-schema-resolver@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/json-schema-resolver/-/json-schema-resolver-2.0.0.tgz#d17fdf53560e6bc9af084b930fee27f6ce4a03b6"
integrity sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==
dependencies:
debug "^4.1.1"
rfdc "^1.1.4"
uri-js "^4.2.2"

json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
Expand Down Expand Up @@ -9679,6 +9750,11 @@ mime@2.6.0, mime@^2.5.2:
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==

mime@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==

mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
Expand Down Expand Up @@ -10778,6 +10854,11 @@ open@^9.1.0:
is-inside-container "^1.0.0"
is-wsl "^2.2.0"

openapi-types@^12.0.0, openapi-types@^12.0.2:
version "12.1.3"
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==

optionator@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
Expand Down Expand Up @@ -10864,7 +10945,7 @@ p-limit@^2.2.0:
dependencies:
p-try "^2.0.0"

p-limit@^3.0.2:
p-limit@^3.0.2, p-limit@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
Expand Down Expand Up @@ -12010,7 +12091,7 @@ rewiremock@^3.14.5:
wipe-node-cache "^2.1.2"
wipe-webpack-cache "^2.1.0"

rfdc@^1.2.0, rfdc@^1.3.0:
rfdc@^1.1.4, rfdc@^1.2.0, rfdc@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
Expand Down Expand Up @@ -12108,7 +12189,7 @@ rxjs@^7.8.0:
dependencies:
tslib "^2.1.0"

safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
Expand Down
Loading