Skip to content

Commit

Permalink
feat(cmd-api-server): add GetOpenApiSpecV1Endpoint (HTTP GET)
Browse files Browse the repository at this point in the history
The new endpoint can serve up the OpenAPI specification file of the
API server via a get request without any parameters needed.

Under the hood it uses the re-usable common base endpoint class that
we've recently added to the core package.

The test case at this file path demonstrates how to use it via the API
client:
`packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts`

Signed-off-by: Peter Somogyvari <[email protected]>
  • Loading branch information
petermetz committed Sep 14, 2023
1 parent 6fd7ddc commit 13f666a
Show file tree
Hide file tree
Showing 15 changed files with 545 additions and 22 deletions.
1 change: 1 addition & 0 deletions packages/cactus-cmd-api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"prom-client": "13.2.0",
"run-time-error": "1.4.0",
"rxjs": "7.8.1",
"safe-stable-stringify": "2.4.3",
"semver": "7.5.2",
"socket.io": "4.5.4",
"socket.io-client": "4.5.4",
Expand Down
29 changes: 29 additions & 0 deletions packages/cactus-cmd-api-server/src/main/json/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
"PrometheusExporterMetricsResponse": {
"type": "string",
"nullable": false
},
"GetOpenApiSpecV1EndpointResponse": {
"type": "string",
"nullable": false
}
}
},
Expand Down Expand Up @@ -126,6 +130,31 @@
}
}
}
},
"/api/v1/api-server/get-open-api-spec": {
"get": {
"description": "Returns the openapi.json document of specific plugin.",
"x-hyperledger-cactus": {
"http": {
"verbLowerCase": "get",
"path": "/api/v1/api-server/get-open-api-spec"
}
},
"operationId": "getOpenApiSpecV1",
"parameters": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetOpenApiSpecV1EndpointResponse"
}
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ All URIs are relative to *http://localhost*
Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
*DefaultApi* | [**getHealthCheckV1**](docs/DefaultApi.md#gethealthcheckv1) | **GET** /api/v1/api-server/healthcheck | Can be used to verify liveness of an API server instance
*DefaultApi* | [**getOpenApiSpecV1**](docs/DefaultApi.md#getopenapispecv1) | **GET** /api/v1/api-server/get-open-api-spec |
*DefaultApi* | [**getPrometheusMetricsV1**](docs/DefaultApi.md#getprometheusmetricsv1) | **GET** /api/v1/api-server/get-prometheus-exporter-metrics | Get the Prometheus Metrics


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,74 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient
)
}

/**
*
* Returns the openapi.json document of specific plugin.
* @return kotlin.String
* @throws IllegalStateException If the request is not correctly configured
* @throws IOException Rethrows the OkHttp execute method exception
* @throws UnsupportedOperationException If the API returns an informational or redirection response
* @throws ClientException If the API returns a client error response
* @throws ServerException If the API returns a server error response
*/
@Suppress("UNCHECKED_CAST")
@Throws(IllegalStateException::class, IOException::class, UnsupportedOperationException::class, ClientException::class, ServerException::class)
fun getOpenApiSpecV1() : kotlin.String {
val localVarResponse = getOpenApiSpecV1WithHttpInfo()

return when (localVarResponse.responseType) {
ResponseType.Success -> (localVarResponse as Success<*>).data as kotlin.String
ResponseType.Informational -> throw UnsupportedOperationException("Client does not support Informational responses.")
ResponseType.Redirection -> throw UnsupportedOperationException("Client does not support Redirection responses.")
ResponseType.ClientError -> {
val localVarError = localVarResponse as ClientError<*>
throw ClientException("Client error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse)
}
ResponseType.ServerError -> {
val localVarError = localVarResponse as ServerError<*>
throw ServerException("Server error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse)
}
}
}

/**
*
* Returns the openapi.json document of specific plugin.
* @return ApiResponse<kotlin.String?>
* @throws IllegalStateException If the request is not correctly configured
* @throws IOException Rethrows the OkHttp execute method exception
*/
@Suppress("UNCHECKED_CAST")
@Throws(IllegalStateException::class, IOException::class)
fun getOpenApiSpecV1WithHttpInfo() : ApiResponse<kotlin.String?> {
val localVariableConfig = getOpenApiSpecV1RequestConfig()

return request<Unit, kotlin.String>(
localVariableConfig
)
}

/**
* To obtain the request config of the operation getOpenApiSpecV1
*
* @return RequestConfig
*/
fun getOpenApiSpecV1RequestConfig() : RequestConfig<Unit> {
val localVariableBody = null
val localVariableQuery: MultiValueMap = mutableMapOf()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf()
localVariableHeaders["Accept"] = "application/json"

return RequestConfig(
method = RequestMethod.GET,
path = "/api/v1/api-server/get-open-api-spec",
query = localVariableQuery,
headers = localVariableHeaders,
requiresAuthentication = false,
body = localVariableBody
)
}

/**
* Get the Prometheus Metrics
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ import "models/health_check_response_pb.proto";
service DefaultService {
rpc GetHealthCheckV1 (google.protobuf.Empty) returns (HealthCheckResponsePB);

rpc GetOpenApiSpecV1 (google.protobuf.Empty) returns (GetOpenApiSpecV1Response);

rpc GetPrometheusMetricsV1 (google.protobuf.Empty) returns (GetPrometheusMetricsV1Response);

}

message GetOpenApiSpecV1Response {
string data = 1;
}

message GetPrometheusMetricsV1Response {
string data = 1;
}
Expand Down
71 changes: 49 additions & 22 deletions packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { AddressInfo } from "net";
import type { Server as SecureServer } from "https";
import type { Request, Response, RequestHandler } from "express";
import type { ServerOptions as SocketIoServerOptions } from "socket.io";
import type { Socket as SocketIoSocket } from "socket.io";
import exitHook from "async-exit-hook";
import os from "os";
import path from "path";
Expand All @@ -13,7 +16,6 @@ import fs from "fs-extra";
import expressHttpProxy from "express-http-proxy";
import { Server as GrpcServer } from "@grpc/grpc-js";
import { ServerCredentials as GrpcServerCredentials } from "@grpc/grpc-js";
import type { Application, Request, Response, RequestHandler } from "express";
import express from "express";
import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types";
import compression from "compression";
Expand All @@ -22,8 +24,6 @@ import cors from "cors";

import rateLimit from "express-rate-limit";
import { Server as SocketIoServer } from "socket.io";
import type { ServerOptions as SocketIoServerOptions } from "socket.io";
import type { Socket as SocketIoSocket } from "socket.io";
import { authorize as authorizeSocket } from "@thream/socketio-jwt";

import {
Expand All @@ -37,7 +37,11 @@ import {
PluginImportAction,
} from "@hyperledger/cactus-core-api";

import { PluginRegistry } from "@hyperledger/cactus-core";
import {
PluginRegistry,
registerWebServiceEndpoint,
} from "@hyperledger/cactus-core";

import { installOpenapiValidationMiddleware } from "@hyperledger/cactus-core";

import {
Expand All @@ -49,7 +53,6 @@ import {

import { ICactusApiServerOptions } from "./config/config-service";
import OAS from "../json/openapi.json";
// import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types";

import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter";
import { AuthorizerFactory } from "./authzn/authorizer-factory";
Expand All @@ -58,6 +61,10 @@ import { WatchHealthcheckV1Endpoint } from "./web-services/watch-healthcheck-v1-
import * as default_service from "./generated/proto/protoc-gen-ts/services/default_service";
import { GrpcServerApiServer } from "./web-services/grpc/grpc-server-api-server";
import { determineAddressFamily } from "./common/determine-address-family";
import {
GetOpenApiSpecV1Endpoint,
IGetOpenApiSpecV1EndpointOptions,
} from "./web-services/get-open-api-spec-v1-endpoint";

export interface IApiServerConstructorOptions {
readonly pluginManagerOptions?: { pluginsPath: string };
Expand Down Expand Up @@ -91,8 +98,8 @@ export class ApiServer {
private readonly httpServerCockpit?: Server | SecureServer;
private readonly wsApi: SocketIoServer;
private readonly grpcServer: GrpcServer;
private readonly expressApi: Application;
private readonly expressCockpit: Application;
private readonly expressApi: express.Express;
private readonly expressCockpit: express.Express;
private readonly pluginsPath: string;
private readonly enableShutdownHook: boolean;

Expand Down Expand Up @@ -290,7 +297,9 @@ export class ApiServer {
this.pluginRegistry = await this.initPluginRegistry();
} else {
this.log.info(`getOrInitPluginRegistry() re-using injected one...`);
this.pluginRegistry = this.options.pluginRegistry;
this.pluginRegistry = await this.initPluginRegistry({
pluginRegistry: this.options.pluginRegistry,
});
}
}
await this.prometheusExporter.setTotalPluginImports(
Expand All @@ -305,17 +314,19 @@ export class ApiServer {
}
}

public async initPluginRegistry(): Promise<PluginRegistry> {
const registry = new PluginRegistry({ plugins: [] });
public async initPluginRegistry(req?: {
readonly pluginRegistry: PluginRegistry;
}): Promise<PluginRegistry> {
const { pluginRegistry = new PluginRegistry({ plugins: [] }) } = req || {};
const { plugins } = this.options.config;
this.log.info(`Instantiated empty registry, invoking plugin factories...`);

for (const pluginImport of plugins) {
const plugin = await this.instantiatePlugin(pluginImport, registry);
registry.add(plugin);
const plugin = await this.instantiatePlugin(pluginImport, pluginRegistry);
pluginRegistry.add(plugin);
}

return registry;
return pluginRegistry;
}

private async instantiatePlugin(
Expand Down Expand Up @@ -347,7 +358,8 @@ export class ApiServer {

// eslint-disable-next-line @typescript-eslint/no-var-requires
const pluginPackage = require(/* webpackIgnore: true */ packagePath);
const createPluginFactory = pluginPackage.createPluginFactory as PluginFactoryFactory;
const createPluginFactory =
pluginPackage.createPluginFactory as PluginFactoryFactory;
const pluginFactoryOptions: IPluginFactoryOptions = {
pluginImportType: pluginImport.type,
};
Expand Down Expand Up @@ -550,9 +562,27 @@ export class ApiServer {
* healthcheck and monitoring information.
* @param app
*/
async getOrCreateWebServices(app: express.Application): Promise<void> {
async getOrCreateWebServices(app: express.Express): Promise<void> {
const { log } = this;
const { logLevel } = this.options.config;
const pluginRegistry = await this.getOrInitPluginRegistry();

{
const oasPath = OAS.paths["/api/v1/api-server/get-open-api-spec"];

const operationId = oasPath.get.operationId;
const opts: IGetOpenApiSpecV1EndpointOptions = {
oas: OAS,
oasPath,
operationId,
path: oasPath.get["x-hyperledger-cactus"].http.path,
pluginRegistry,
verbLowerCase: oasPath.get["x-hyperledger-cactus"].http.verbLowerCase,
logLevel,
};
const endpoint = new GetOpenApiSpecV1Endpoint(opts);
await registerWebServiceEndpoint(app, endpoint);
}

const healthcheckHandler = (req: Request, res: Response) => {
res.json({
Expand Down Expand Up @@ -596,13 +626,10 @@ export class ApiServer {
const {
"/api/v1/api-server/get-prometheus-exporter-metrics": oasPathPrometheus,
} = OAS.paths;
const { http: httpPrometheus } = oasPathPrometheus.get[
"x-hyperledger-cactus"
];
const {
path: httpPathPrometheus,
verbLowerCase: httpVerbPrometheus,
} = httpPrometheus;
const { http: httpPrometheus } =
oasPathPrometheus.get["x-hyperledger-cactus"];
const { path: httpPathPrometheus, verbLowerCase: httpVerbPrometheus } =
httpPrometheus;
(app as any)[httpVerbPrometheus](
httpPathPrometheus,
prometheusExporterHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati



setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Returns the openapi.json document of specific plugin.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getOpenApiSpecV1: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/v1/api-server/get-open-api-spec`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}

const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;



setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
Expand Down Expand Up @@ -189,6 +218,15 @@ export const DefaultApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getHealthCheckV1(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Returns the openapi.json document of specific plugin.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getOpenApiSpecV1(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getOpenApiSpecV1(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Get the Prometheus Metrics
Expand Down Expand Up @@ -218,6 +256,14 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa
getHealthCheckV1(options?: any): AxiosPromise<HealthCheckResponse> {
return localVarFp.getHealthCheckV1(options).then((request) => request(axios, basePath));
},
/**
* Returns the openapi.json document of specific plugin.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getOpenApiSpecV1(options?: any): AxiosPromise<string> {
return localVarFp.getOpenApiSpecV1(options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get the Prometheus Metrics
Expand Down Expand Up @@ -248,6 +294,16 @@ export class DefaultApi extends BaseAPI {
return DefaultApiFp(this.configuration).getHealthCheckV1(options).then((request) => request(this.axios, this.basePath));
}

/**
* Returns the openapi.json document of specific plugin.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public getOpenApiSpecV1(options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).getOpenApiSpecV1(options).then((request) => request(this.axios, this.basePath));
}

/**
*
* @summary Get the Prometheus Metrics
Expand Down
Loading

0 comments on commit 13f666a

Please sign in to comment.