diff --git a/.cli.env b/.cli.env new file mode 100644 index 0000000..93d2e33 --- /dev/null +++ b/.cli.env @@ -0,0 +1,6 @@ + +CLI_VERSION=1.0.0 +CLI_NAME='Api Server CLI' + +# to trust jolokia certs +NODE_TLS_REJECT_UNAUTHORIZED='0' diff --git a/.gitignore b/.gitignore index dd38b46..2ffc718 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,7 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# tmp dir +tmp + diff --git a/README.md b/README.md index 59a06a6..9deb82d 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,212 @@ To undeploy, run ```sh ./undeploy.sh ``` + +# Jolokia api-server Cli tool (work in progress) + +The Jolokia api-server comes with a cli (command line interface) tool. When the cli tool starts it connects to the api-server and can access the jolokia endpoints. + +The cli tool takes a **command** and requests the api-server using [its api](src/config/openapi.yml) to invoke on the target jolokia endpint, gets back the response and printing the result to the console in JSON format. + +It can run in `interactive mode` or in `non-interactive` mode. + +To build the cli tool run + +``` +yarn build +``` + +which will build both the api-server and cli tool. + +To install the cli tool runnable locally run: + +``` +npm link +``` + +It will create the cli runnable `jolokia-api-server-cli` + +## Running the cli tool + +The cli tool needs a running api-server. + +To start the cli tool run + +``` +jolokia-api-server-cli [options] +``` + +or you can use `yarn` + +``` +yarn start-cli [options] +``` + +## Using Cli tool in non-interactive mode + +In this mode, the tool starts and execute a command and then exits. + +The syntax for non-interactive mode is: + +``` +jolokia-api-server-cli -l -e +``` + +If `-l` option is omitted the default is ` https://localhost:9443` + +The `-e` option is the target jolokia url. for example + +``` +-e http://user:password@127.0.0.1:8161 +``` + +If the port number part is omitted, the default +is `80` for http and `443` for https. + +The `command` is the command to be executed. + +Note in non-interactive mode the `command` need be quoted as it contains spaces. + +Example: + +``` +jolokia-api-server-cli "get queue TEST -a MessageCount RoutingType" -e http://user:pass@127.0.0.1:8161 +``` + +(the -s option can suppress yarn's own output) + +## Using Cli tool in interactive mode + +In interactive mode the tool starts into a command shell and +accepts user input as a command, then it executes it and went +back to the shell prompt to accept another, until you run the `exit` +command. + +The syntax to run the cli in this mode is + +``` +jolokia-api-server-cli -i +``` + +When it starts it print the cli title and ready to accept +commands. + +With interactive mode the cli can 'caches' a list of jolokia endpoints (added by the `add` command +only available in interactive mode). It takes one of them as `current endpoint` so when user types +a command without specifying target jolokia endpoint, the `current endpoint` will be used. + +## Using Cli Commands + +### The `get` command + +This is the only available command currently. It can retrive +information from a jolokia endpoint. + +The syntax of this command is + +``` +get <-a attributes...> <-o operations...> +``` + +It takes a `path` argument, a `name` argument, an optional `-a`(attribute) option and an optional +`-o` (operation) option. + +The value of path is a string representing a target mbean from which you want to get information. +It takes the form [target endpoint]/[component]. The `target endpoint` in `interactive` mode allows +you to specify which broker you want to retrieve information from. If absent it takes the current broker +cached by the cli. In non-interactive mode that [target endpoint] can be empty if `-e` option is given, +or it is the target remote endpoint name prefix by a `@` char. For example `@broker1/` + +The `component` part is the type of the mbean. Currently the supported mbean types are + +- `queue` +- `address` +- `acceptor` +- `cluster-connection` + +The argument is the mbean name. + +The value of `-a` option is a list of attribute names (space or comma separated) to read from the target mbean. +If the value is a `*` it will read all the attributes of the target mbean. + +The value of `-o` option is a list of operation names (space or comma separated) to read from the target mbean. +If the value is a `*` it will read all the operations of the target mbean. When retrieving operation informations +the `name` part of the component if optional because operations are defined on the component type rather than +a specific mbean.?? + +examples: + +`get /` - get the broker mbean information + +`get /*` - get all mbeans registered with the broker mbean + +`get / -a *` - read all the attributes of the broker mbean + +`get / -a * -o *` - read information of all attributes and operations of the broker mbean + +`get queue` (or `get /queue`) - list all the queue mbeans information + +`get acceptor acceptor0 -a *` - read all attributes of acceptor named `acceptor0` + +`get queue TEST -a MessageCount RoutingType` - read `MessageCount` and `RoutingType` of queue `TEST` + +`get queue -o xxx` - read information of operation xxx of queue TEST + +### The `run` command + +The `run` command is used to invoke operations on a jolokia endpoint. + +The syntax of this command is + +``` +run +``` + +It takes a `path` argument, a `name` argument and optional `-a`(attribute) option + +### Commands exclusive to Interactive mode + +There are several commands that are only available to interactive mode. + +#### The `add` command + +Add a jolokia endpoint to the cli cache. Syntax: + +``` +add -u -p +``` + +example: + +``` +add ex-aao-ssl https://ex-aao-ssl-wconsj-0-svc-rte-default.apps-crc.testing -u user -p password +``` + +#### The `switch` command + +To switch current endpoint to another. Syntax: + +``` +switch +``` + +example: + +``` +switch broker0 +``` + +#### The `list` command + +To list all the jolokia endpoints cached in cli and managed on the api-server. Syntax: + +``` +list +``` + +With the cached enpoints in this mode, user can run a command against a cached jolokia endpoint. +For example: + +`get ex-aao-ssl/queue DLQ -a MessageCount` + +will read `MessageCount` attribute of queue DLQ on endpoint `ex-aao-ssl` in the cached endpoint list. diff --git a/package.json b/package.json index 4f33466..e4055ea 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "activemq-artemis-jolokia-api-server", - "version": "0.1.0", - "private": true, + "version": "0.1.2", + "private": false, "homepage": "https://github.com/artemiscloud/activemq-artemis-jolokia-api-server#readme", "bugs": { "url": "https://github.com/artemiscloud/activemq-artemis-jolokia-api-server/issues" @@ -11,7 +11,7 @@ "url": "git+ssh://git@github.com:artemiscloud/activemq-activemq-artemis-jolokia-api-server.git" }, "scripts": { - "build": "yarn clean && tsc -p tsconfig.json && yarn copy-config", + "build": "yarn clean && tsc -p tsconfig.json && yarn copy-config && chmod +x dist/cli/index.js", "build-api-doc": "yarn openapi-to-md src/config/openapi.yml api.md && yarn pretty-quick", "clean": "rm -rf dist", "copy-config": "cp -r src/config dist/config", @@ -22,7 +22,9 @@ "test": "NODE_TLS_REJECT_UNAUTHORIZED=0 TZ=UTC jest --runInBand", "test:coverage": "yarn run test --watch=false --coverage", "test:generate-output": "yarn test -- --json --outputFile=.jest-test-results.json", - "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'" + "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'", + "apigen": "apigen-ts src/config/openapi.yml src/cli/api-client.ts", + "start-cli": "node --no-warnings ./dist/cli/index.js" }, "lint-staged": { "*.{js,ts,tsx}": [ @@ -30,10 +32,16 @@ "eslint --fix" ] }, + "dependencies": { + "dotenv": "^16.4.5", + "commander": "^12.1.0", + "figlet": "^1.7.0" + }, "devDependencies": { "@types/base-64": "^1.0.2", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/figlet": "^1.5.8", "@types/jest": "27.5.2", "@types/js-yaml": "4.0.5", "@types/jsonwebtoken": "^9.0.6", @@ -42,11 +50,11 @@ "@types/webpack": "5.28.1", "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", + "apigen-ts": "^0.2.0", "base-64": "^1.0.0", "chromatic": "6.10.1", "copy-webpack-plugin": "11.0.0", "cors": "^2.8.5", - "dotenv": "^16.4.5", "eslint": "^8.10.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", @@ -75,6 +83,13 @@ "typescript": "^4.7.4", "yaml": "^2.4.5" }, + "bin": { + "jolokia-api-server-cli": "./dist/cli/index.js" + }, + "files": [ + "./dist" + ], "readme": "README.md", - "_id": "activemq-artemis-jolokia-api-server@0.1.0" + "_id": "activemq-artemis-jolokia-api-server@0.1.0", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/cli/api-client.ts b/src/cli/api-client.ts new file mode 100644 index 0000000..e59f475 --- /dev/null +++ b/src/cli/api-client.ts @@ -0,0 +1,455 @@ +// Auto-generated by https://github.com/vladkens/apigen-ts +// Source: src/config/openapi.yml + +export interface ApigenConfig { + baseUrl: string; + headers: Record; +} + +export interface ApigenRequest extends Omit { + search?: Record; + body?: unknown; +} + +export class ApiClient { + Config: ApigenConfig; + + constructor(config?: Partial) { + this.Config = { baseUrl: '/', headers: {}, ...config }; + } + + async ParseError(rep: Response) { + try { + return await rep.json(); + } catch (e) { + throw rep; + } + } + + PrepareFetchUrl(path: string): URL { + let base = this.Config.baseUrl; + if ('location' in globalThis && (base === '' || base.startsWith('/'))) { + const { location } = globalThis as unknown as { + location: { origin: string }; + }; + base = `${location.origin}${base.endsWith('/') ? base : `/${base}`}`; + } + + return new URL(path, base); + } + + async Fetch( + method: string, + path: string, + opts: ApigenRequest = {}, + ): Promise { + const url = this.PrepareFetchUrl(path); + + for (const [k, v] of Object.entries(opts?.search ?? {})) { + url.searchParams.append( + k, + Array.isArray(v) ? v.join(',') : (v as string), + ); + } + + const headers = new Headers({ ...this.Config.headers, ...opts.headers }); + const ct = headers.get('content-type') ?? 'application/json'; + + let body: FormData | URLSearchParams | string | undefined = undefined; + + if ( + ct === 'multipart/form-data' || + ct === 'application/x-www-form-urlencoded' + ) { + headers.delete('content-type'); + body = + ct === 'multipart/form-data' ? new FormData() : new URLSearchParams(); + for (const [k, v] of Object.entries( + opts.body as Record, + )) { + body.append(k, v); + } + } + + if (ct === 'application/json' && typeof opts.body !== 'string') { + headers.set('content-type', 'application/json'); + body = JSON.stringify(opts.body); + } + + const credentials = opts.credentials ?? 'include'; + const rep = await fetch(url.toString(), { + method, + ...opts, + headers, + body, + credentials, + }); + if (!rep.ok) throw await this.ParseError(rep); + + const rs = await rep.text(); + try { + return JSON.parse(rs) as T; + } catch (e) { + return rs as unknown as T; + } + } + + security = { + serverLogin: (body: { userName: string; password: string }) => { + return this.Fetch('post', '/server/login', { body }); + }, + + serverLogout: (body: EmptyBody) => { + return this.Fetch('post', '/server/logout', { + body, + }); + }, + + login: (body: { + brokerName: string; + userName: string; + password: string; + jolokiaHost: string; + scheme: string; + port: string; + }) => { + return this.Fetch('post', '/jolokia/login', { body }); + }, + }; + + admin = { + listEndpoints: () => { + return this.Fetch('get', '/server/admin/listEndpoints', {}); + }, + }; + + jolokia = { + getBrokers: (search: { targetEndpoint?: string }) => { + return this.Fetch('get', '/brokers', { search }); + }, + + getBrokerDetails: (search: { targetEndpoint?: string }) => { + return this.Fetch('get', '/brokerDetails', { search }); + }, + + readBrokerAttributes: (search: { + names?: string[]; + targetEndpoint?: string; + }) => { + return this.Fetch('get', '/readBrokerAttributes', { + search, + }); + }, + + readAddressAttributes: (search: { + name?: string; + attrs?: string[]; + targetEndpoint?: string; + }) => { + return this.Fetch('get', '/readAddressAttributes', { + search, + }); + }, + + readQueueAttributes: (search: { + name?: string; + address?: string; + 'routing-type'?: string; + attrs?: string[]; + targetEndpoint?: string; + }) => { + return this.Fetch('get', '/readQueueAttributes', { + search, + }); + }, + + readAcceptorAttributes: (search: { + name?: string; + attrs?: string[]; + targetEndpoint?: string; + }) => { + return this.Fetch( + 'get', + '/readAcceptorAttributes', + { search }, + ); + }, + + readClusterConnectionAttributes: (search: { + name?: string; + attrs?: string[]; + targetEndpoint?: string; + }) => { + return this.Fetch( + 'get', + '/readClusterConnectionAttributes', + { search }, + ); + }, + + execClusterConnectionOperation: ( + body: OperationRef, + search: { + name?: string; + targetEndpoint?: string; + }, + ) => { + return this.Fetch( + 'post', + '/execClusterConnectionOperation', + { search, body }, + ); + }, + + checkCredentials: () => { + return this.Fetch('get', '/checkCredentials', {}); + }, + + execBrokerOperation: ( + body: OperationRef, + search: { + targetEndpoint?: string; + }, + ) => { + return this.Fetch('post', '/execBrokerOperation', { + search, + body, + }); + }, + + getBrokerComponents: (search: { targetEndpoint?: string }) => { + return this.Fetch('get', '/brokerComponents', { search }); + }, + + getAddresses: (search: { targetEndpoint?: string }) => { + return this.Fetch('get', '/addresses', { search }); + }, + + getQueues: (search: { address?: string; targetEndpoint?: string }) => { + return this.Fetch('get', '/queues', { search }); + }, + + getQueueDetails: (search: { + addressName?: string; + name?: string; + routingType?: string; + targetEndpoint?: string; + }) => { + return this.Fetch('get', '/queueDetails', { search }); + }, + + getAddressDetails: (search: { name?: string; targetEndpoint?: string }) => { + return this.Fetch('get', '/addressDetails', { search }); + }, + + getAcceptors: (search: { targetEndpoint?: string }) => { + return this.Fetch('get', '/acceptors', { search }); + }, + + getAcceptorDetails: (search: { + name?: string; + targetEndpoint?: string; + }) => { + return this.Fetch('get', '/acceptorDetails', { + search, + }); + }, + + getClusterConnections: (search: { targetEndpoint?: string }) => { + return this.Fetch('get', '/clusterConnections', { + search, + }); + }, + + getClusterConnectionDetails: (search: { + name?: string; + targetEndpoint?: string; + }) => { + return this.Fetch('get', '/clusterConnectionDetails', { + search, + }); + }, + }; + + development = { + apiInfo: () => { + return this.Fetch('get', '/api-info', {}); + }, + }; +} + +export type Acceptor = { + name: string; + broker: Broker; +}; + +export type Address = { + name: string; + broker: Broker; +}; + +export type ApiResponse = { + message?: { + security?: { + enabled?: boolean; + }; + info?: { + name?: string; + description?: string; + version?: string; + }; + paths?: { + post?: string[]; + get?: string[]; + }; + }; + status?: 'successful'; + 'jolokia-session-id'?: string; +}; + +export type Argument = { + name: string; + type: JavaTypes; + desc: string; +}; + +export type Attr = { + desc: string; + rw: boolean; + type: JavaTypes; +}; + +export type Broker = { + name: string; +}; + +export type ClusterConnection = { + name: string; + broker: Broker; +}; + +export type ComponentAttribute = { + request: { + mbean: string; + attribute?: string; + type: string; + }; + value?: unknown; + error_type?: string; + error?: string; + timestamp?: number; + status: number; +}; + +export type ComponentDetails = { + op: object; + attr: object; + class: string; + desc: string; +}; + +export type DummyResponse = { + message: 'ok'; + status: 'successful'; +}; + +export type EmptyBody = object | null; + +export type Endpoint = { + name: string; + url?: string; +}; + +export type ExecResult = { + request: { + mbean: string; + arguments: string[]; + type: string; + operation: string; + }; + value?: unknown; + error_type?: string; + error?: string; + timestamp?: number; + status: number; +}; + +export type FailureResponse = { + status: 'failed' | 'error'; + message: string; +}; + +export enum JavaTypes { + _Ljava_lang_Object_ = '[Ljava.lang.Object;', + _Ljava_lang_String_ = '[Ljava.lang.String;', + _Ljava_util_Map_ = '[Ljava.util.Map;', + _Ljavax_management_openmbean_CompositeData_ = '[Ljavax.management.openmbean.CompositeData;', + Object = 'Object', + Boolean = 'boolean', + Double = 'double', + Int = 'int', + Java_lang_Boolean = 'java.lang.Boolean', + Java_lang_Integer = 'java.lang.Integer', + Java_lang_Long = 'java.lang.Long', + Java_lang_Object = 'java.lang.Object', + Java_lang_String = 'java.lang.String', + Java_util_Map = 'java.util.Map', + Long = 'long', + Void = 'void', +} + +export type LoginResponse = { + message: string; + status: string; + 'jolokia-session-id': string; +}; + +export type OperationArgument = { + type: JavaTypes; + value: string; +}; + +export type OperationRef = { + signature: { + name: string; + args: OperationArgument[]; + }; +}; + +export type OperationResult = { + request: { + mbean: string; + arguments: string[]; + type: string; + operation: string; + }; + value: string; + timestamp: number; + status: number; +}; + +export type Queue = { + name: string; + 'routing-type': string; + address?: Address; + broker: Broker; +}; + +export type ServerLoginResponse = { + message: string; + status: string; + bearerToken: string; +}; + +export type ServerLogoutResponse = { + message: string; + status: string; +}; + +export type Signature = { + ret?: JavaTypes; + desc: string; + args: Argument[]; +}; + +export type Signatures = Signature[]; diff --git a/src/cli/cli.ts b/src/cli/cli.ts new file mode 100644 index 0000000..f1b2073 --- /dev/null +++ b/src/cli/cli.ts @@ -0,0 +1,132 @@ +import figlet from 'figlet'; +import readline from 'readline'; +import { stdin, stdout } from 'process'; + +import { + CommandContext, + InteractiveCommandContext, + printError, +} from './context'; +import { ServerAccess } from './server-access'; +import { Command, OptionValues } from 'commander'; + +export class Cli { + static start = ( + serverAccess: ServerAccess, + options: OptionValues, + program: Command, + ) => { + let userName: string; + let password: string; + let shouldLogin = false; + + if (options.user) { + if (!options.password) { + printError('Error: no password'); + process.exit(1); + } + userName = options.user; + password = options.password; + shouldLogin = true; + } else { + if (process.env.SERVER_USER_NAME) { + if (!process.env.SERVER_PASSWORD) { + printError('Error: no password'); + process.exit(1); + } + userName = process.env.SERVER_USER_NAME; + password = process.env.SERVER_PASSWORD; + shouldLogin = true; + } + } + + if (shouldLogin) { + serverAccess + .loginServer(userName, password) + .then((res) => { + if (res.bearerToken) { + serverAccess.updateBearerToken(res.bearerToken); + } + serverAccess.setLoginUser(userName); + if (res.status !== 'succeed') { + printError('Failed to login server', res); + process.exit(1); + } + Cli.internalStart(serverAccess, options, program); + }) + .catch((err) => { + printError('Failed to login server', err); + process.exit(1); + }); + } else { + Cli.internalStart(serverAccess, options, program); + } + }; + + static internalStart = ( + serverAccess: ServerAccess, + options: OptionValues, + program: Command, + ) => { + if (options.interactive) { + const endpointMap = new Map(); + const commandContext = new InteractiveCommandContext( + serverAccess, + endpointMap, + ); + + const rl = readline.createInterface({ + input: stdin, + output: stdout, + }); + program.exitOverride(); //avoid exit on error + + const runMain = async () => { + rl.question(commandContext.getPrompt(), function (command) { + if (command === 'exit') { + return rl.close(); + } + commandContext + .processSingleCommand(command) + .then(() => { + runMain(); + }) + .catch((e) => { + printError('error processing command', e); + runMain(); + }); + }); + }; + console.log(figlet.textSync('Api Server Cli')); + runMain(); + } else { + const commandContext = new CommandContext( + serverAccess, + program.opts().endpoint, + null, + ); + + commandContext + .login() + .then((value) => { + if (value === 0) { + commandContext + .processCommand(program.args) + .then((result) => { + process.exit(result); + }) + .catch((e) => { + printError('failed to run command', e); + program.help(); + }); + } else { + program.help(); + } + }) + .catch((err) => { + printError('failed to run command', err); + program.help(); + }); + } + }; +} diff --git a/src/cli/context.ts b/src/cli/context.ts new file mode 100644 index 0000000..1a2c003 --- /dev/null +++ b/src/cli/context.ts @@ -0,0 +1,918 @@ +import { Command } from 'commander'; +import { ServerAccess } from './server-access'; + +export class JolokiaEndpoint { + isRemote = (): boolean => { + return false; + }; + getUrl = (): string => { + return ''; + }; + + getBrokerName = (): string => { + return ''; + }; + + setBrokerName = (name: string): void => { + throw new Error('Method not implemented.'); + }; +} + +export class RemoteJolokiaEndpoint extends JolokiaEndpoint { + endpointName: string; + + constructor(endpointName: string) { + super(); + this.endpointName = endpointName; + } + + isRemote = (): boolean => { + return true; + }; + + getBrokerName = (): string => { + return this.endpointName; + }; + + setBrokerName = (name: string) => { + this.endpointName = name; + }; +} + +export class LocalJolokiaEndpoint extends JolokiaEndpoint { + brokerName: string; + userName: string; + password: string; + jolokiaHost: string; + scheme: string; + port: string; + accessToken: string; + url: string; + + constructor( + endpointName: string, + userName: string, + password: string, + jolokiaHost: string, + scheme: string, + port: string, + accessToken: string, + ) { + super(); + this.brokerName = endpointName; + this.userName = userName; + this.password = password; + this.jolokiaHost = jolokiaHost; + this.scheme = scheme; + this.port = port; + this.accessToken = accessToken; + } + + getBrokerName = (): string => { + return this.brokerName; + }; + + setBrokerName = (name: string) => { + this.brokerName = name; + }; + + getUrl = () => { + return this.scheme + '://' + this.jolokiaHost + ':' + this.port; + }; +} + +const replaceErrors = (key: any, value: any) => { + if (key === 'details') { + if (value instanceof Error) { + const error = {}; + + Object.getOwnPropertyNames(value).forEach(function (propName) { + error[propName] = value[propName]; + }); + + return error; + } + if (value instanceof Response) { + return { status: value.status, statusText: value.statusText }; + } + } + + return value; +}; + +export const printResult = (result: object) => { + console.log(JSON.stringify(result, null, 2)); +}; + +export const printError = (message: string, detail?: object | string) => { + console.error( + JSON.stringify( + { + message: 'Error: ' + message, + details: detail ? detail : '', + }, + replaceErrors, + 2, + ), + ); +}; + +export class CommandContext { + apiClient: ServerAccess; + currentEndpoint: JolokiaEndpoint; + + constructor( + serverAccess: ServerAccess, + endpointUrl: string, + endpoint: JolokiaEndpoint | null, + ) { + this.apiClient = serverAccess; + + if (endpointUrl) { + const url = new URL(endpointUrl); + this.currentEndpoint = new LocalJolokiaEndpoint( + 'current', + url.username, + url.password, + url.hostname, + url.protocol.substring(0, url.protocol.length - 1), + this.getActualPort(url), + '', + ); + } else { + this.currentEndpoint = endpoint as JolokiaEndpoint; + } + } + + getActualPort(url: URL): string { + return url.port === '' + ? url.protocol === 'http:' + ? '80' + : '443' + : url.port; + } + + // this login is used to login a jolokia endpoint + async login(): Promise { + const current = this.currentEndpoint as LocalJolokiaEndpoint; + if (!current || current.accessToken !== '') { + return 0; + } + const result = await this.apiClient.login(current); + if (result.status === 'success') { + const accessToken = result['jolokia-session-id']; + this.apiClient.updateClientHeader('jolokia-session-id', accessToken); + current.accessToken = accessToken; + return 0; + } + return 1; + } + + async processCommand(args: string[]): Promise { + let retValue = 0; + let resolvedArgs = args; + if (args.length === 1) { + // the command is quoted + resolvedArgs = args[0].trim().split(' '); + } + + switch (resolvedArgs[0]) { + case 'get': { + const getCmd = this.newGetCmd(); + try { + await getCmd.parseAsync(resolvedArgs, { from: 'electron' }); + } catch (e) { + printError('failed to execute get command', e); + retValue = 1; + } + break; + } + default: + printError('unknown command', args); + retValue = 1; + break; + } + return retValue; + } + + parseGetPath = async ( + path: string, + callback: (targetType: string, remoteEndpoint: string) => Promise, + ): Promise => { + //for non-interactive mode if + // path = '/' : to get all components of the target broker + // path = '/' : to get all components of + // path = '' : same as '/' + let targetType: string; + let targetEndpoint: string = null; + if (path === '/') { + const currentTarget = this.currentEndpoint?.getBrokerName(); + if (currentTarget) { + path = currentTarget + path; + } + } + if (path.startsWith('@') && !path.includes('/')) { + path = path + '/'; + } + const pathElements = path.split('/'); + if (pathElements.length === 1) { + targetType = pathElements[0]; + } else if (pathElements.length === 2) { + targetType = pathElements[1]; + if (pathElements[0].startsWith('@')) { + targetEndpoint = pathElements[0].substring(1); + } + } else { + throw 'Invalid target expression: ' + path; + } + await callback(targetType, targetEndpoint); + }; + + newGetCmd(): Command { + const getCmd = new Command('get') + .description('get information from a endpoint') + .argument( + '', + 'path of the component with format [[@]endpointName/componentType] where @ means a remote target', + ) + .argument('[compName]', 'name of the component', '') + .option( + '-a, --attributes ', + 'get attributes from component', + ) + .option( + '-o, --operations ', + 'get operations info from component', + ) + .exitOverride() + .showHelpAfterError() + .action(async (path, compName, options, cmd): Promise => { + await this.parseGetPath(path, async (targetType, remoteEndpoint) => { + if (compName === '') { + // read all comps of type + if (targetType === '') { + // '/' get broker info + if ( + options.attributes?.length > 0 || + options.operations?.length > 0 + ) { + if (options.attributes?.length > 0) { + await this.getComponentAttributes( + remoteEndpoint, + 'broker', + '', + options.attributes[0] === '*' ? null : options.attributes, + ); + } + if (options.operations?.length > 0) { + await this.getComponentOperations( + remoteEndpoint, + 'broker', + '', + options.operations[0] === '*' ? null : options.operations, + ); + } + } else { + await this.getComponent(remoteEndpoint, 'broker', ''); + } + } else if (targetType === '*') { + // '/*' to get all components + if (options.attributes?.length > 0) { + throw Error('cannot specify attributes for all components'); + } else { + await this.getAllComponents(remoteEndpoint, ''); + } + } else { + // '/type' read all comps of type + if (options.attributes?.length > 0) { + throw 'need a component name to get attributes of'; + } + await this.getAllComponents(remoteEndpoint, targetType); + } + } else { + if (options.attributes?.length > 0) { + // '/type or type -a ...' read one comp's attributes + await this.getComponentAttributes( + remoteEndpoint, + targetType, + compName, + options.attributes[0] === '*' ? null : options.attributes, + ); + } else { + //nothing specified, just return type info + await this.getComponent(remoteEndpoint, targetType, compName); + } + } + }); + }); + return getCmd; + } + + async getComponent( + remoteEndpoint: string, + targetType: string, + compName: string, + ): Promise { + switch (targetType) { + case 'broker': + return await this.getBroker(remoteEndpoint); + case 'queue': + case 'queues': + return await this.getQueue(remoteEndpoint, compName); + case 'address': + case 'addresses': + return await this.getAddress(remoteEndpoint, compName); + case 'acceptor': + case 'acceptors': + return await this.getAcceptor(remoteEndpoint, compName); + default: + printError('component type not supported', targetType); + return 1; + } + } + + async getAllBrokerComponents(remoteTarget: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getBrokerComponents(remoteTarget); + printResult(result); + } catch (ex) { + printError('failed to get broker components', ex); + retValue = 1; + } + return retValue; + } + + async getAllQueueComponents(remoteTarget: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getQueues(remoteTarget); + printResult(result); + } catch (ex) { + printError( + 'failed to get queues at ' + remoteTarget ? remoteTarget : 'current', + ex, + ); + retValue = 1; + } + return retValue; + } + + async getAllAddresses(remoteTarget: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getAddresses(remoteTarget); + printResult(result); + } catch (ex) { + printError('failed to get addresses', ex); + retValue = 1; + } + return retValue; + } + + async getAllAcceptors(remoteTarget: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getAcceptors(remoteTarget); + printResult(result); + } catch (ex) { + printError('failed to get acceptors', ex); + retValue = 1; + } + return retValue; + } + + async getAllClusterConnections(remoteTarget: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getClusterConnections(remoteTarget); + printResult(result); + } catch (ex) { + printError('failed to get cluster connections', ex); + retValue = 1; + } + return retValue; + } + + async getAllComponents( + remoteEndpoint: string, + targetType: string, + ): Promise { + switch (targetType) { + case '': + return await this.getAllBrokerComponents(remoteEndpoint); + case 'queue': + case 'queues': + return await this.getAllQueueComponents(remoteEndpoint); + case 'address': + case 'addresses': + return await this.getAllAddresses(remoteEndpoint); + case 'acceptor': + case 'acceptors': + return await this.getAllAcceptors(remoteEndpoint); + case 'cluster-connection': + case 'cluster-connections': + return await this.getAllClusterConnections(remoteEndpoint); + case 'bridge': + case 'bridges': + printError('not implemented!'); + return 1; + case 'broadcast-group': + case 'broadcast-groups': + printError('not implemented!'); + return 1; + default: + printError('component type not supported', targetType); + return 1; + } + } + + async getQueue(remoteEndpoint: string, compName: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getQueues(remoteEndpoint); + const queues = result.filter((q) => q.name === compName); + printResult(queues); + } catch (ex) { + printError('failed to get queues', ex); + retValue = 1; + } + return retValue; + } + + async getAddress(remoteTarget: string, compName: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getAddresses(remoteTarget); + const addresses = result.filter((a) => a.name === compName); + printResult(addresses); + } catch (ex) { + printError('failed to get addresses', ex); + retValue = 1; + } + return retValue; + } + + async getAcceptor(remoteTarget: string, compName: string): Promise { + let retValue = 0; + try { + const result = await this.apiClient.getAcceptors(remoteTarget); + const acceptors = result.filter((a) => a.name === compName); + printResult(acceptors); + } catch (ex) { + printError('failed to get acceptors', ex); + retValue = 1; + } + return retValue; + } + + async getBrokerOperations( + remoteTarget: string, + operations: string[], + ): Promise { + let retValue = 0; + const opts = operations === null ? {} : { names: operations }; + try { + const values = await this.apiClient.readBrokerOperations(remoteTarget); + + let opSet: Set = null; + if (opts.names) { + opSet = new Set(); + opts.names.forEach((n) => { + // deal with commas + const names = n.split(','); + names.forEach((m) => { + if (m !== '') { + opSet.add(m); + } + }); + }); + } + + const result = new Array(); + for (const prop in values.op) { + if ( + Object.prototype.hasOwnProperty.call(values.op, prop) && + (opSet === null || opSet.has(prop)) + ) { + result.push({ [prop]: values.op[prop] }); + } + } + printResult(result); + } catch (e) { + printError('failed to read operationss', e); + retValue = 1; + } + return retValue; + } + + async getBrokerAttributes( + remoteTarget: string, + attributes: string[], + ): Promise { + let retValue = 0; + const opts = attributes === null ? {} : { names: attributes }; + try { + const values = await this.apiClient.readBrokerAttributes( + remoteTarget, + opts, + ); + printResult(values); + } catch (e) { + printError('failed to read attributes', e); + retValue = 1; + } + return retValue; + } + + async getQueueAttributes( + remoteTarget: string, + compName: string, + attributes: string[], + ): Promise { + let retValue = 0; + const result = await this.apiClient.getQueues(remoteTarget); + const queues = result.filter((q) => q.name === compName); + for (let i = 0; i < queues.length; i++) { + const q = queues[i]; + const opts = + attributes === null + ? { + name: compName, + address: q.address?.name, + 'routing-type': q['routing-type'], + } + : { + name: compName, + address: q.address?.name, + 'routing-type': q['routing-type'], + attrs: attributes, + }; + + try { + const values = await this.apiClient.readQueueAttributes( + remoteTarget, + opts, + ); + printResult(values); + } catch (e) { + printError('failed to read queue attributes', e); + retValue = 1; + break; + } + } + return retValue; + } + + async getAddressAttributes( + remoteTarget: string, + compName: string, + attributes: string[], + ): Promise { + let retValue = 0; + const opts = + attributes === null + ? { name: compName } + : { name: compName, attrs: attributes }; + try { + const values = await this.apiClient.readAddressAttributes( + remoteTarget, + opts, + ); + printResult(values); + } catch (e) { + printError('failed to read address attributes', e); + retValue = 1; + } + return retValue; + } + + async getAcceptorAttributes( + remoteTarget: string, + compName: string, + attributes: string[], + ): Promise { + let retValue = 0; + const opts = + attributes === null + ? { name: compName } + : { name: compName, attrs: attributes }; + try { + const values = await this.apiClient.readAcceptorAttributes( + remoteTarget, + opts, + ); + printResult(values); + } catch (e) { + printError('failed to read acceptor attributes', e); + retValue = 1; + } + return retValue; + } + async getClusterConnectionAttributes( + remoteTarget: string, + compName: string, + attributes: string[], + ): Promise { + let retValue = 0; + const opts = + attributes === null + ? { name: compName } + : { name: compName, attrs: attributes }; + try { + const values = await this.apiClient.readClusterConnectionAttributes( + remoteTarget, + opts, + ); + printResult(values); + } catch (e) { + printError('failed to read cluster connection attributes', e); + retValue = 1; + } + return retValue; + } + + async getComponentAttributes( + remoteEndpoint: string, + targetType: string, + compName: string, + attributes: string[], + ): Promise { + switch (targetType) { + case 'broker': + return await this.getBrokerAttributes(remoteEndpoint, attributes); + case 'queue': + case 'queues': + return await this.getQueueAttributes( + remoteEndpoint, + compName, + attributes, + ); + case 'address': + case 'addresses': + return await this.getAddressAttributes( + remoteEndpoint, + compName, + attributes, + ); + case 'acceptor': + case 'acceptors': + return await this.getAcceptorAttributes( + remoteEndpoint, + compName, + attributes, + ); + case 'cluster-connection': + case 'cluster-connections': + return await this.getClusterConnectionAttributes( + remoteEndpoint, + compName, + attributes, + ); + default: + printError('Error: component type not supported', targetType); + return 1; + } + } + + async getComponentOperations( + remoteEndpoint: string, + targetType: string, + compName: string, + operations: string[], + ): Promise { + switch (targetType) { + case 'broker': + return await this.getBrokerOperations(remoteEndpoint, operations); + case 'queue': + case 'queues': + case 'address': + case 'addresses': + case 'acceptor': + case 'acceptors': + case 'cluster-connection': + case 'cluster-connections': + default: + printError('Error: component type not supported', targetType); + return 1; + } + } + + async getBroker(remoteEndpoint: string): Promise { + let retValue = 0; + try { + const values = await this.apiClient.getBrokers(remoteEndpoint); + printResult(values); + } catch (ex) { + printError('failed to get brokers', ex); + retValue = 1; + } + return retValue; + } +} + +export class InteractiveCommandContext extends CommandContext { + readonly endpoints: Map; + + constructor( + serverAccess: ServerAccess, + endpointMap: Map, + ) { + super(serverAccess, '', null); + this.endpoints = endpointMap; + } + + getPrompt(): string { + const currentUser = this.apiClient.currentUser ?? undefined; + if (this.currentEndpoint) { + if (currentUser) { + return currentUser + ':' + this.currentEndpoint.getBrokerName() + '> '; + } + return this.currentEndpoint.getBrokerName() + '> '; + } + if (currentUser) { + return currentUser + '> '; + } + return '> '; + } + + hasEndpoint(endpointName: string): boolean { + return this.endpoints.has(endpointName); + } + + newAddCmd(): Command { + const addCmd = new Command('add') + .argument('', 'name of the endpoint') + .argument('', 'the endpoint url') + .option('-u, --user [userName]', 'the user name', 'user') + .option('-p, --password [password]', 'the password', 'password') + .exitOverride() + .showHelpAfterError() + .description( + 'add an jolokia endpoint, example: add mybroker0 http://localhost:8161', + ) + .action(async (endpointName, endpointUrl, options) => { + const url = new URL(endpointUrl); + if (this.hasEndpoint(endpointName)) { + printError('endpoint already exists!'); + } + + const newEndpoint = new LocalJolokiaEndpoint( + endpointName, + options.user, + options.password, + url.hostname, + url.protocol.substring(0, url.protocol.length - 1), + this.getActualPort(url), + '', + ); + const context = new CommandContext(this.apiClient, '', newEndpoint); + try { + await context.login(); + context.currentEndpoint.setBrokerName(endpointName); + this.endpoints.set(endpointName, context); + this.switchContext(context); + } catch (ex) { + printError('failed to login', ex); + } + }); + + return addCmd; + } + + addEndpoint = async (args: string[]): Promise => { + let retValue = 0; + const addCmd = this.newAddCmd(); + try { + await addCmd.parseAsync(args, { from: 'electron' }).catch(() => { + //commander would print the error message + retValue = 1; + }); + } catch (ex) { + printError('failed to execute add command', ex); + retValue = 1; + } + return retValue; + }; + + getEndpoint = (endpointName: string): CommandContext | undefined => { + return this.endpoints.get(endpointName); + }; + + listJolokiaEndpoints = async (): Promise => { + const endpointList = new Array(); + this.endpoints.forEach((context, key) => { + endpointList.push(key + '(local): ' + context.currentEndpoint.getUrl()); + }); + + const remoteEndpoints = await this.apiClient.listEndpoints(); + remoteEndpoints.forEach((e) => { + endpointList.push('@' + e.name + ': ' + e.url); + }); + printResult(endpointList); + + return 0; + }; + + switchContext(target: CommandContext) { + this.apiClient = target.apiClient; + this.currentEndpoint = target.currentEndpoint; + } + + newSwitchCmd(): Command { + const switchCmd = new Command('switch') + .argument('') + .description('switch to a jolokia endpoint') + .exitOverride() + .action(async (endpointName) => { + if (endpointName.startsWith('@')) { + this.currentEndpoint = new RemoteJolokiaEndpoint(endpointName); + } else { + if (!this.hasEndpoint(endpointName)) { + printError('no such endpoint', endpointName); + } else { + const target = this.getEndpoint(endpointName) as CommandContext; + this.switchContext(target); + } + } + }); + return switchCmd; + } + + async switchJolokiaEndpoint(args: string[]): Promise { + let retValue = 0; + const switchCmd = this.newSwitchCmd(); + try { + switchCmd.parse(args, { from: 'electron' }); + } catch (ex) { + printError('failed to execute switch command', ex); + retValue = 1; + } + return retValue; + } + + // command path is in form: + // [[@]endpointName]/[componentType] + // if @ is present it means endpointName is targeted at api server + // if @ is not present it means a local endpoint + // if endpointName part is absent at all it means current local endpoint + // componentType is the target component of a broker (queues, address, etc) + // if componentType is absent it means all components of the broker + // if path is / it gets the mbean info of the current local broker. + getContextForGetCmd(path: string): CommandContext { + if (!path) { + return this; + } + + const isRemoteTarget = + path.startsWith('@') || this.currentEndpoint?.isRemote(); + + if (!isRemoteTarget) { + if (this.endpoints.size === 0) { + throw Error('there is no endpoint for command'); + } + + const elements = path.split('/'); + if (elements.length === 2 && elements[0] !== '') { + if (this.hasEndpoint(elements[0])) { + if (elements[0] === this.currentEndpoint?.getBrokerName()) { + return this; + } else { + return this.getEndpoint(elements[0]) as CommandContext; + } + } else { + throw Error('target endpoint not exist: ' + elements[0]); + } + } + } + return this; + } + + async processSingleCommand(cmd: string): Promise { + const args = cmd.trim().split(' '); + switch (args[0]) { + case '': + return 0; + case 'add': + return await this.addEndpoint(args); + case 'list': + return await this.listJolokiaEndpoints(); + case 'switch': + return this.switchJolokiaEndpoint(args); + case 'get': { + let context: CommandContext; + try { + context = this.getContextForGetCmd(args[1]); + } catch (ex) { + printError('failed to get context', ex); + return 1; + } + return await context.processCommand([cmd.trim()]); + } + default: { + printError('unknown command'); + return 1; + } + } + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..86c2b08 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,59 @@ +#! /usr/bin/env -S node --no-warnings + +import { Command } from 'commander'; +import dotenv from 'dotenv'; + +import { ServerAccess } from './server-access'; +import { printError } from './context'; +import { Cli } from './cli'; + +dotenv.config({ path: '.cli.env' }); + +if (process.env['NODE_TLS_REJECT_UNAUTHORIZED'] !== '0') { + console.log('Warning: TLS Certificate check is disabled.'); + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; +} + +const program = new Command(); +program + .version('1.0.0') + .description('CLI tool for ActiveMQ Artemis Jolokia API Server') + .argument('[command]', 'the command to be executed') + .option( + '-l, --url [api-server-url]', + 'the url of api server', + 'https://localhost:9443', + ) + .option('-i, --interactive', 'run in interactive mode', false) + .option('-e, --endpoint [endpoint]', 'target jolokia endpoint url') + .option( + '-u, --user [userName]', + 'user name to log in to the api server if security is enabled', + false, + ) + .option( + '-p, --password [password]', + 'user password to log in to the api server', + false, + ) + .parse(process.argv); + +const cliOpts = program.opts(); + +const apiServerUrl = cliOpts.url; + +const serverAccess = new ServerAccess(apiServerUrl); + +serverAccess + .checkApiServer() + .then((result) => { + if (!result) { + printError('The api server is not available', apiServerUrl); + process.exit(1); + } + Cli.start(serverAccess, cliOpts, program); + }) + .catch((e) => { + printError('Error checking api server: ' + apiServerUrl, e); + process.exit(1); + }); diff --git a/src/cli/server-access.ts b/src/cli/server-access.ts new file mode 100644 index 0000000..20c1f41 --- /dev/null +++ b/src/cli/server-access.ts @@ -0,0 +1,175 @@ +import { ApiClient } from './api-client'; +import { LocalJolokiaEndpoint } from './context'; + +class JolokiaClient extends ApiClient { + PrepareFetchUrl(path: string) { + return new URL(`${this.Config.baseUrl}/${path}`.replace(/\/{2,}/g, '/')); + } +} + +export class ServerAccess { + apiClient: JolokiaClient; + currentUser: string; + + constructor(apiServerUrl: string) { + this.apiClient = new JolokiaClient({ + baseUrl: apiServerUrl + '/api/v1/', + }); + } + + setLoginUser(userName: string) { + this.currentUser = userName; + } + + login = async (currentEndpoint: LocalJolokiaEndpoint) => { + return this.apiClient.security.login(currentEndpoint); + }; + + checkApiServer = async (): Promise => { + return this.apiClient.development + .apiInfo() + .then((value) => { + if (value.status === 'successful') { + return true; + } + return false; + }) + .catch(() => { + return false; + }); + }; + + updateClientHeader = (name: string, accessToken: string) => { + this.apiClient.Config.headers = { + ...this.apiClient.Config.headers, + [name]: accessToken, + }; + }; + + updateBearerToken(bearerToken: string) { + this.apiClient.Config.headers = { + ...this.apiClient.Config.headers, + Authorization: 'Bearer ' + bearerToken, + }; + } + + loginServer = async (userName: string, password: string) => { + return this.apiClient.security.serverLogin({ userName, password }); + }; + + getTargetOpts = (remoteTarget: string) => { + return remoteTarget ? { targetEndpoint: remoteTarget } : {}; + }; + + getBrokerComponents = async (remoteTarget: string) => { + return this.apiClient.jolokia.getBrokerComponents( + this.getTargetOpts(remoteTarget), + ); + }; + + getQueues = async (remoteTarget: string) => { + return this.apiClient.jolokia.getQueues(this.getTargetOpts(remoteTarget)); + }; + + getAddresses = async (remoteTarget: string) => { + return this.apiClient.jolokia.getAddresses( + this.getTargetOpts(remoteTarget), + ); + }; + + getAcceptors = async (remoteTarget: string) => { + return this.apiClient.jolokia.getAcceptors( + this.getTargetOpts(remoteTarget), + ); + }; + + getClusterConnections = async (remoteTarget: string) => { + return this.apiClient.jolokia.getClusterConnections( + this.getTargetOpts(remoteTarget), + ); + }; + + readBrokerAttributes = async ( + remoteTarget: string, + opts: { names?: undefined } | { names: string[] }, + ) => { + return this.apiClient.jolokia.readBrokerAttributes({ + ...opts, + ...this.getTargetOpts(remoteTarget), + }); + }; + + readBrokerOperations = async (remoteTarget: string) => { + return this.apiClient.jolokia.getBrokerDetails({ + ...this.getTargetOpts(remoteTarget), + }); + }; + + readQueueAttributes = async ( + remoteTarget: string, + opts: + | { + name: string; + address: string; + 'routing-type': string; + attrs?: undefined; + } + | { + name: string; + address: string; + 'routing-type': string; + attrs: string[]; + }, + ) => { + return this.apiClient.jolokia.readQueueAttributes({ + ...opts, + ...this.getTargetOpts(remoteTarget), + }); + }; + + readAddressAttributes = async ( + remoteTarget: string, + opts: + | { name: string; attrs?: undefined } + | { name: string; attrs: string[] }, + ) => { + return this.apiClient.jolokia.readAddressAttributes({ + ...opts, + ...this.getTargetOpts(remoteTarget), + }); + }; + + readAcceptorAttributes = async ( + remoteTarget: string, + opts: + | { name: string; attrs?: undefined } + | { name: string; attrs: string[] }, + ) => { + return this.apiClient.jolokia.readAcceptorAttributes({ + ...opts, + ...this.getTargetOpts(remoteTarget), + }); + }; + + readClusterConnectionAttributes = async ( + remoteTarget: string, + opts: + | { name: string; attrs?: undefined } + | { name: string; attrs: string[] }, + ) => { + return this.apiClient.jolokia.readClusterConnectionAttributes({ + ...opts, + ...this.getTargetOpts(remoteTarget), + }); + }; + + getBrokers = async (remoteEndpoint: string) => { + return this.apiClient.jolokia.getBrokers( + this.getTargetOpts(remoteEndpoint), + ); + }; + + listEndpoints = async () => { + return this.apiClient.admin.listEndpoints(); + }; +} diff --git a/src/config/openapi.yml b/src/config/openapi.yml index 0824a10..da92a39 100644 --- a/src/config/openapi.yml +++ b/src/config/openapi.yml @@ -93,8 +93,92 @@ tags: description: jolokia API - name: development description: for development purposes + - name: admin + description: for management operations + +security: + - bearerAuth: [] # use the same name as above paths: + /server/login: + post: + summary: Api to log in to the api server. + description: > + This api is used to login to the api server. + tags: + - security + operationId: serverLogin + security: [] # no security for login + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + userName: + type: string + password: + type: string + required: + - userName + - password + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ServerLoginResponse' + 401: + description: Invalid credentials + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + 500: + description: Internal server error + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + /server/logout: + post: + summary: Api to log out + description: > + This api is used to logout the current session. + tags: + - security + operationId: serverLogout + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyBody' + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ServerLogoutResponse' + 401: + description: Invalid credentials + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + 500: + description: Internal server error + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' /jolokia/login: post: summary: The login api @@ -112,6 +196,7 @@ paths: named `jolokia-session-id`. The src will validate the token before processing a request is and rejects the request if the token is not valid. + security: [] # no security for login tags: - security operationId: login @@ -168,6 +253,39 @@ paths: schema: type: object $ref: '#/components/schemas/FailureResponse' + /server/admin/listEndpoints: + get: + summary: List endpoints managed by the api-server + description: > + ** List broker jolokia endpoints ** + The return value is a list of endpoints currently + managed by the api server. + tags: + - admin + operationId: listEndpoints + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Endpoint' + 401: + description: Invalid credentials + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' + 500: + description: Internal server error + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/FailureResponse' /brokers: get: summary: retrieve the broker mbean @@ -183,7 +301,17 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: header + name: endpoint-name + schema: + type: string + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -223,7 +351,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -263,7 +396,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: names description: attribute names separated by commas. If not speified read all attributes. required: false @@ -274,6 +407,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -315,7 +453,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the address name schema: @@ -332,6 +470,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -373,7 +516,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the queue name schema: @@ -402,6 +545,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -443,7 +591,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the queue name schema: @@ -460,6 +608,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -501,7 +654,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name description: the cluster connection name schema: @@ -518,6 +671,11 @@ paths: type: string style: form explode: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -559,12 +717,17 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - in: query name: name schema: type: string required: true + - in: query + name: targetEndpoint + schema: + type: string + required: false requestBody: required: true content: @@ -594,7 +757,6 @@ paths: schema: type: object $ref: '#/components/schemas/FailureResponse' - /checkCredentials: get: summary: Check the validity of the credentials @@ -606,7 +768,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false responses: 200: description: Success @@ -647,7 +809,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false requestBody: required: true content: @@ -693,7 +860,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -732,7 +904,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -771,13 +948,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: address required: false in: query schema: type: string description: If given only list the queues on this address + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -818,7 +1000,7 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: addressName required: false in: query @@ -837,6 +1019,11 @@ paths: schema: type: string description: the routing type of the queue (anycast or multicast) + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -875,13 +1062,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name required: true in: query schema: type: string description: the address name + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -918,7 +1110,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -959,13 +1156,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name required: true in: query schema: type: string description: the acceptor name + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -1002,7 +1204,12 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -1043,13 +1250,18 @@ paths: name: jolokia-session-id schema: type: string - required: true + required: false - name: name required: true in: query schema: type: string description: the cluster connection name + - in: query + name: targetEndpoint + schema: + type: string + required: false responses: 200: description: Success @@ -1082,6 +1294,7 @@ paths: tags: - development operationId: apiInfo + security: [] #no security for api-info responses: 200: description: Success @@ -1171,6 +1384,11 @@ components: message: type: object properties: + security: + type: object + properties: + enabled: + type: boolean info: type: object properties: @@ -1197,6 +1415,30 @@ components: jolokia-session-id: type: string description: The jwt token + ServerLogoutResponse: + type: object + required: + - status + - message + properties: + message: + type: string + status: + type: string + ServerLoginResponse: + type: object + required: + - status + - message + - bearerToken + properties: + message: + type: string + status: + type: string + bearerToken: + type: string + description: The jwt token LoginResponse: type: object required: @@ -1263,6 +1505,15 @@ components: properties: name: type: string + Endpoint: + type: object + required: + - name + properties: + name: + type: string + url: + type: string FailureResponse: type: object required: @@ -1417,3 +1668,11 @@ components: type: number status: type: number + EmptyBody: + type: object + nullable: true + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/tsconfig.json b/tsconfig.json index 77ea373..f9fc804 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "baseUrl": ".", "target": "es2020", - "lib": [ - "es2020" - ], + "lib": ["es2020"], "module": "CommonJS", "moduleResolution": "node", "outDir": "./dist", @@ -16,6 +14,6 @@ "@app/*": ["src/*"] } }, - "include": ["./src/*", "./env"], + "include": ["./src/**/*", "./env"], "exclude": ["node_modules", "dist"] } diff --git a/yarn.lock b/yarn.lock index 08040ae..b5e0822 100644 --- a/yarn.lock +++ b/yarn.lock @@ -650,6 +650,38 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@redocly/ajv@^8.11.2": + version "8.11.2" + resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.11.2.tgz#46e1bf321ec0ac1e0fd31dea41a3d1fcbdcda0b5" + integrity sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js-replace "^1.0.1" + +"@redocly/config@^0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.10.1.tgz#c7bcbab6cb3b82236c2f5c87aa44924a652d8e80" + integrity sha512-H3LnKVGzOaxskwJu8pmJYwBOWjP61qOK7TuTrbafqArDVckE06fhA6l0nO4KvBbjLPjy1Al7UnlxOu23V4Nl0w== + +"@redocly/openapi-core@^1.22.1": + version "1.24.0" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.24.0.tgz#0d234b62a81ebbba070a348b4654a60f878eb1a8" + integrity sha512-BFCwRTwkAHeYd8/IggOo0ZyseLJHzSTVGnVRaZCG0rcH3+d+N6qcjYIQRuNrjHlIFCsVFL7/ugLWFL5irODt7g== + dependencies: + "@redocly/ajv" "^8.11.2" + "@redocly/config" "^0.10.1" + colorette "^1.2.0" + https-proxy-agent "^7.0.4" + js-levenshtein "^1.1.6" + js-yaml "^4.1.0" + lodash.isequal "^4.5.0" + minimatch "^5.0.1" + node-fetch "^2.6.1" + pluralize "^8.0.0" + yaml-ast-parser "0.0.43" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -795,6 +827,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/figlet@^1.5.8": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@types/figlet/-/figlet-1.5.8.tgz#96b8186c7e2a388b4f8d09ee3276cba2af88bb0b" + integrity sha512-G22AUvy4Tl95XLE7jmUM8s8mKcoz+Hr+Xm9W90gJsppJq9f9tHvOGkrpn4gRX0q/cLtBdNkWtWCKDg2UDZoZvQ== + "@types/graceful-fs@^4.1.2": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" @@ -851,6 +888,18 @@ dependencies: "@types/node" "*" +"@types/lodash-es@^4.17.12": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" + integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== + "@types/mime@^1": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" @@ -925,6 +974,14 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/swagger2openapi@^7.0.4": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@types/swagger2openapi/-/swagger2openapi-7.0.4.tgz#56723c4184c067a70f2cd47f7607e5330770dded" + integrity sha512-ffMqzciTDihOKH4Q//9Ond1yb5JP1P5FC/aFPsLK4blea1Fwk2aYctiNCkAh5etDYFswFXS+5LV/vuGkf+PU6A== + dependencies: + "@types/node" "*" + openapi-types "^12.1.0" + "@types/webpack-env@^1.17.0": version "1.18.5" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.5.tgz#eccda0b04fe024bed505881e2e532f9c119169bf" @@ -1238,6 +1295,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -1361,6 +1425,19 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +apigen-ts@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/apigen-ts/-/apigen-ts-0.2.0.tgz#fe195418adb70a4e32bcb8146aa1bab3f0065525" + integrity sha512-9R3rQSq2i3qAUq5/fzQLiKlwo3MbQyyZNAQp0dFIoPmeOZl2wkrGDLmCfnhArNUTwShI0WBwh5jqDvbIu9Q1oA== + dependencies: + "@redocly/openapi-core" "^1.22.1" + "@types/lodash-es" "^4.17.12" + "@types/swagger2openapi" "^7.0.4" + array-utils-ts "^0.1.2" + cleye "^1.3.2" + lodash-es "^4.17.21" + swagger2openapi "^7.0.8" + append-field@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" @@ -1432,6 +1509,11 @@ array-unique@^0.2.1: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" integrity sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg== +array-utils-ts@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/array-utils-ts/-/array-utils-ts-0.1.2.tgz#232a416b0e6794b64880372e297d9702cf8877ef" + integrity sha512-AVp/ybvqELxWd7ZtSC9HGwPPv4FOoWlJWtOaQY1lZuPKmRmJKXA80f+CAyMByH6yCF7H5wDupA67c7N0SGTiTQ== + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -1838,6 +1920,14 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cleye@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/cleye/-/cleye-1.3.2.tgz#3c013c816f604d446a92e1457debdb360985f7e9" + integrity sha512-MngIC2izcCz07iRKr3Pe8Z6ZBv4zbKFl/YnQEN/aMHis6PpH+MxI2e6n0bMUAmSVlMoAyQkdBCSTbfDmtcSovQ== + dependencies: + terminal-columns "^1.4.1" + type-flag "^3.0.0" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -1931,6 +2021,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + colorette@^2.0.19: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" @@ -1948,6 +2043,11 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2813,6 +2913,11 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +figlet@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.7.0.tgz#46903a04603fd19c3e380358418bb2703587a72e" + integrity sha512-gO8l3wvqo0V7wEFLXPbkX83b7MVjRrk1oRLfYlZXol8nEpb/ON9pcKLI4qpBv5YtOTfrINtqb7b40iYY2FTWFg== + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -3314,6 +3419,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.4: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -4176,6 +4289,11 @@ js-base64@^2.1.9: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4447,6 +4565,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -4467,6 +4590,11 @@ lodash.isboolean@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -5054,6 +5182,11 @@ openapi-to-md@^1.0.24: swagger2openapi "*" yaml "*" +openapi-types@^12.1.0: + 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.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -6523,7 +6656,7 @@ swagger-routes-express@^3.3.2: dependencies: semver "^7.3.7" -swagger2openapi@*: +swagger2openapi@*, swagger2openapi@^7.0.8: version "7.0.8" resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-7.0.8.tgz#12c88d5de776cb1cbba758994930f40ad0afac59" integrity sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g== @@ -6594,6 +6727,11 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +terminal-columns@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/terminal-columns/-/terminal-columns-1.4.1.tgz#f669dd878afaad0c404d4b7aa01855fb6f24f1a4" + integrity sha512-IKVL/itiMy947XWVv4IHV7a0KQXvKjj4ptbi7Ew9MPMcOLzkiQeyx3Gyvh62hKrfJ0RZc4M1nbhzjNM39Kyujw== + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -6805,6 +6943,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/type-flag/-/type-flag-3.0.0.tgz#2caef2f20f2c71e960fe1d3b6f57bae8c8246459" + integrity sha512-3YaYwMseXCAhBB14RXW5cRQfJQlEknS6i4C8fCfeUdS3ihG9EdccdR9kt3vP73ZdeTGmPb4bZtkDn5XMIn1DLA== + type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -6944,6 +7087,11 @@ update-notifier@^2.2.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" +uri-js-replace@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uri-js-replace/-/uri-js-replace-1.0.1.tgz#c285bb352b701c9dfdaeffc4da5be77f936c9048" + integrity sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g== + uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -7258,6 +7406,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml-ast-parser@0.0.43: + version "0.0.43" + resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" + integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== + yaml@*, yaml@^2.1.3, yaml@^2.4.5: version "2.4.5" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e"