From e9b2cb6ccb350c0747ffa4c9798619963d6c2a37 Mon Sep 17 00:00:00 2001 From: Howard Gao Date: Tue, 24 Sep 2024 11:14:25 +0800 Subject: [PATCH] [#10] adding support for cluster-connection component access --- api.md | 332 +++++++++++++++++++++++++++-- src/api/apiutil/artemis_jolokia.ts | 246 +++++++++++++-------- src/api/controllers/api_impl.ts | 148 +++++++++++++ src/api/controllers/security.ts | 2 + src/config/openapi.yml | 206 ++++++++++++++++++ src/utils/server.test.ts | 199 +++++++++++++++++ 6 files changed, 1028 insertions(+), 105 deletions(-) diff --git a/api.md b/api.md index ec1eb76..ff484a4 100644 --- a/api.md +++ b/api.md @@ -81,25 +81,29 @@ If necessary update the code that is using the hooks to comply with your changes ## Path Table -| Method | Path | Description | -| ------ | ----------------------------------------------------- | ------------------------------------- | -| POST | [/jolokia/login](#postjolokialogin) | The login api | -| GET | [/brokers](#getbrokers) | retrieve the broker mbean | -| GET | [/brokerDetails](#getbrokerdetails) | broker details | -| GET | [/readBrokerAttributes](#getreadbrokerattributes) | read broker attributes | -| GET | [/readAddressAttributes](#getreadaddressattributes) | read address attributes | -| GET | [/readQueueAttributes](#getreadqueueattributes) | read queue attributes | -| GET | [/readAcceptorAttributes](#getreadacceptorattributes) | read acceptor attributes | -| GET | [/checkCredentials](#getcheckcredentials) | Check the validity of the credentials | -| POST | [/execBrokerOperation](#postexecbrokeroperation) | execute a broker operation | -| GET | [/brokerComponents](#getbrokercomponents) | list all mbeans | -| GET | [/addresses](#getaddresses) | retrieve all addresses on broker | -| GET | [/queues](#getqueues) | list queues | -| GET | [/queueDetails](#getqueuedetails) | retrieve queue details | -| GET | [/addressDetails](#getaddressdetails) | retrieve address details | -| GET | [/acceptors](#getacceptors) | list acceptors | -| GET | [/acceptorDetails](#getacceptordetails) | retrieve acceptor details | -| GET | [/api-info](#getapi-info) | the api info | +| Method | Path | Description | +| ------ | ----------------------------------------------------------------------- | -------------------------------------- | +| POST | [/jolokia/login](#postjolokialogin) | The login api | +| GET | [/brokers](#getbrokers) | retrieve the broker mbean | +| GET | [/brokerDetails](#getbrokerdetails) | broker details | +| GET | [/readBrokerAttributes](#getreadbrokerattributes) | read broker attributes | +| GET | [/readAddressAttributes](#getreadaddressattributes) | read address attributes | +| GET | [/readQueueAttributes](#getreadqueueattributes) | read queue attributes | +| GET | [/readAcceptorAttributes](#getreadacceptorattributes) | read acceptor attributes | +| GET | [/readClusterConnectionAttributes](#getreadclusterconnectionattributes) | read cluster connection attributes | +| POST | [/execClusterConnectionOperation](#postexecclusterconnectionoperation) | execute a cluster connection operation | +| GET | [/checkCredentials](#getcheckcredentials) | Check the validity of the credentials | +| POST | [/execBrokerOperation](#postexecbrokeroperation) | execute a broker operation | +| GET | [/brokerComponents](#getbrokercomponents) | list all mbeans | +| GET | [/addresses](#getaddresses) | retrieve all addresses on broker | +| GET | [/queues](#getqueues) | list queues | +| GET | [/queueDetails](#getqueuedetails) | retrieve queue details | +| GET | [/addressDetails](#getaddressdetails) | retrieve address details | +| GET | [/acceptors](#getacceptors) | list acceptors | +| GET | [/acceptorDetails](#getacceptordetails) | retrieve acceptor details | +| GET | [/clusterConnections](#getclusterconnections) | list cluster connections | +| GET | [/clusterConnectionDetails](#getclusterconnectiondetails) | retrieve cluster connection details | +| GET | [/api-info](#getapi-info) | the api info | ## Reference Table @@ -113,6 +117,7 @@ If necessary update the code that is using the hooks to comply with your changes | LoginResponse | [#/components/schemas/LoginResponse](#componentsschemasloginresponse) | | | Address | [#/components/schemas/Address](#componentsschemasaddress) | | | Acceptor | [#/components/schemas/Acceptor](#componentsschemasacceptor) | | +| ClusterConnection | [#/components/schemas/ClusterConnection](#componentsschemasclusterconnection) | | | Queue | [#/components/schemas/Queue](#componentsschemasqueue) | | | Broker | [#/components/schemas/Broker](#componentsschemasbroker) | | | FailureResponse | [#/components/schemas/FailureResponse](#componentsschemasfailureresponse) | | @@ -604,6 +609,164 @@ jolokia-session-id: string --- +### [GET]/readClusterConnectionAttributes + +- Summary + read cluster connection attributes + +- Description + **Read values of cluster connection attributes** + The return value is a json array that contains + values of requested attributes of the cluster connection's mbean. + **Note**: to read multiple attributes, set it to **attrs** parameter + separated by commas, e.g. `NodeID, Topology`. + +#### Parameters(Query) + +```ts +name: string; +``` + +```ts +attrs?: string[] +``` + +#### Headers + +```ts +jolokia-session-id: string +``` + +#### Responses + +- 200 Success + +`application/json` + +```ts +{ + request: { + mbean: string + attribute?: string + type: string + } + error_type?: string + error?: string + timestamp?: number + status: number +}[] +``` + +- 401 Invalid credentials + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +- 500 Internal server error + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +--- + +### [POST]/execClusterConnectionOperation + +- Summary + execute a cluster connection operation + +- Description + **Invoke an operation of the cluster connection mbean** + It receives a POST request where the body + should have the operation signature and its args. + The return value is a one element json array that contains + return values of invoked operation along with the request info. + +#### Parameters(Query) + +```ts +name: string; +``` + +#### Headers + +```ts +jolokia-session-id: string +``` + +#### RequestBody + +- application/json + +```ts +{ + // The method signature + signature: { + name: string + args: { + type: enum[[Ljava.lang.Object;, [Ljava.lang.String;, [Ljava.util.Map;, [Ljavax.management.openmbean.CompositeData;, Object, boolean, double, int, java.lang.Boolean, java.lang.Integer, java.lang.Long, java.lang.Object, java.lang.String, java.util.Map, long, void] + value: string + }[] + } +} +``` + +#### Responses + +- 200 Success + +`application/json` + +```ts +{ + request: { + mbean: string + arguments?: string[] + type: string + operation: string + } + error_type?: string + error?: string + timestamp?: number + status: number +}[] +``` + +- 401 Invalid credentials + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +- 500 Internal server error + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +--- + ### [GET]/checkCredentials - Summary @@ -1163,6 +1326,126 @@ jolokia-session-id: string --- +### [GET]/clusterConnections + +- Summary + list cluster connections + +- Description + **Get all cluster connections in a broker** + It retrieves and returns a list of all cluster connection mbeans + +#### Headers + +```ts +jolokia-session-id: string +``` + +#### Responses + +- 200 Success + +`application/json` + +```ts +{ + name: string; + broker: { + name: string; + } +} +[]; +``` + +- 401 Invalid credentials + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +- 500 Internal server error + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +--- + +### [GET]/clusterConnectionDetails + +- Summary + retrieve cluster connection details + +- Description + **Get details of a connection cluster** + The return value is a json object that contains + description of all the operations and attributes of a `cluster connection` mbean. + It is defined in [ClusterConnectionControl.java](https://github.com/apache/activemq-artemis/blob/2.33.0/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ClusterConnectionControl.java) + +#### Parameters(Query) + +```ts +// the cluster connection name +name: string; +``` + +#### Headers + +```ts +jolokia-session-id: string +``` + +#### Responses + +- 200 Success + +`application/json` + +```ts +{ + op: { + } + attr: { + } + class: string + desc: string +} +``` + +- 401 Invalid credentials + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +- 500 Internal server error + +`application/json` + +```ts +{ + status: enum[failed, error] + message: string +} +``` + +--- + ### [GET]/api-info - Summary @@ -1303,6 +1586,17 @@ jolokia-session-id: string } ``` +### #/components/schemas/ClusterConnection + +```ts +{ + name: string; + broker: { + name: string; + } +} +``` + ### #/components/schemas/Queue ```ts diff --git a/src/api/apiutil/artemis_jolokia.ts b/src/api/apiutil/artemis_jolokia.ts index ae37c97..1162938 100644 --- a/src/api/apiutil/artemis_jolokia.ts +++ b/src/api/apiutil/artemis_jolokia.ts @@ -13,6 +13,10 @@ const acceptorComponentsSearchPattern = 'org.apache.activemq.artemis:broker="BROKER_NAME",component=acceptors,name=*'; const queueComponentsSearchPattern = 'org.apache.activemq.artemis:broker=*,component=addresses,address="ADDRESS_NAME",subcomponent=queues,*'; +// search cluster connections +const clusterConnectionComponentsSearchPattern = + 'org.apache.activemq.artemis:broker="BROKER_NAME",component=cluster-connections,name=*'; + // list a Queue's operations and attributes const queueDetailsListPattern = 'org.apache.activemq.artemis:address="ADDRESS_NAME",broker="BROKER_NAME",component=addresses,queue="QUEUE_NAME",routing-type="ROUTING_TYPE"/subcomponent=queues'; @@ -20,6 +24,8 @@ const addressDetailsListPattern = 'org.apache.activemq.artemis:address="ADDRESS_NAME",broker="BROKER_NAME"/component=addresses'; const acceptorDetailsListPattern = 'org.apache.activemq.artemis:name="ACCEPTOR_NAME",broker="BROKER_NAME"/component=acceptors'; +const clusterConnectionDetailsListPattern = + 'org.apache.activemq.artemis:name="CLUSTER_CONNECTION_NAME",broker="BROKER_NAME"/component=cluster-connections'; const brokerDetailsListPattern = 'org.apache.activemq.artemis/broker="BROKER_NAME"'; @@ -32,14 +38,16 @@ const acceptorComponentPattern = 'org.apache.activemq.artemis:broker="BROKER_NAME",component=acceptors,name="ACCEPTOR_NAME"'; const queueComponentPattern = 'org.apache.activemq.artemis:address="ADDRESS_NAME",broker="BROKER_NAME",component=addresses,queue="QUEUE_NAME",routing-type="ROUTING_TYPE",subcomponent=queues'; - +const clusterConnectionComponentPattern = + 'org.apache.activemq.artemis:broker="BROKER_NAME",component=cluster-connections,name="CLUSTER_CONNECTION_NAME"'; export class ArtemisJolokia { - username: string; - password: string; - protocol: string; - port: string; - hostName: string; + readonly username: string; + readonly password: string; + readonly protocol: string; + readonly port: string; + readonly hostName: string; brokerName: string; + readonly baseUrl: string; static readonly BROKER = 'broker'; static readonly BROKER_DETAILS = 'broker-details'; @@ -50,6 +58,8 @@ export class ArtemisJolokia { static readonly QUEUE_DETAILS = 'queue-details'; static readonly ADDRESS_DETAILS = 'address-details'; static readonly ACCEPTOR_DETAILS = 'acceptor-details'; + static readonly CLUSTER_CONNECTION_DETAILS = 'cluster-connection-details'; + static readonly CLUSTER_CONNECTION = 'cluster-connection'; componentMap = new Map([ [ArtemisJolokia.BROKER, brokerSearchPattern], @@ -57,6 +67,10 @@ export class ArtemisJolokia { [ArtemisJolokia.ADDRESS, addressComponentsSearchPattern], [ArtemisJolokia.QUEUE, queueComponentsSearchPattern], [ArtemisJolokia.ACCEPTOR, acceptorComponentsSearchPattern], + [ + ArtemisJolokia.CLUSTER_CONNECTION, + clusterConnectionComponentsSearchPattern, + ], ]); componentDetailsMap = new Map([ @@ -64,6 +78,10 @@ export class ArtemisJolokia { [ArtemisJolokia.QUEUE_DETAILS, queueDetailsListPattern], [ArtemisJolokia.ADDRESS_DETAILS, addressDetailsListPattern], [ArtemisJolokia.ACCEPTOR_DETAILS, acceptorDetailsListPattern], + [ + ArtemisJolokia.CLUSTER_CONNECTION_DETAILS, + clusterConnectionDetailsListPattern, + ], ]); componentNameMap = new Map([ @@ -71,6 +89,7 @@ export class ArtemisJolokia { [ArtemisJolokia.ADDRESS, addressComponentPattern], [ArtemisJolokia.ACCEPTOR, acceptorComponentPattern], [ArtemisJolokia.QUEUE, queueComponentPattern], + [ArtemisJolokia.CLUSTER_CONNECTION, clusterConnectionComponentPattern], ]); constructor( @@ -86,6 +105,13 @@ export class ArtemisJolokia { this.port = port; this.hostName = hostName; this.brokerName = ''; + this.baseUrl = + this.protocol + + '://' + + this.hostName + + ':' + + this.port + + '/console/jolokia/'; } getAuthHeaders = (): fetch.Headers => { @@ -115,14 +141,7 @@ export class ArtemisJolokia { searchPattern = searchPattern?.replace('BROKER_NAME', this.brokerName); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/search/' + - searchPattern; + const url = this.baseUrl + 'search/' + searchPattern; const reply = await fetch(url, { method: 'GET', @@ -146,14 +165,7 @@ export class ArtemisJolokia { searchPattern = searchPattern?.replace('BROKER_NAME', this.brokerName); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/list/' + - searchPattern; + const url = this.baseUrl + 'list/' + searchPattern; const reply = await fetch(url, { method: 'GET', @@ -195,14 +207,7 @@ export class ArtemisJolokia { } searchPattern = searchPattern?.replace('BROKER_NAME', this.brokerName); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/list/' + - searchPattern; + const url = this.baseUrl + 'list/' + searchPattern; const reply = await fetch(url, { method: 'GET', @@ -244,14 +249,49 @@ export class ArtemisJolokia { } searchPattern = searchPattern?.replace('BROKER_NAME', this.brokerName); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/list/' + - searchPattern; + const url = this.baseUrl + 'list/' + searchPattern; + + const reply = await fetch(url, { + method: 'GET', + headers: headers, + }) + .then((response) => { + if (response.ok) { + return response.text(); + } + throw response; + }) + .then((message) => { + const resp: JolokiaListResponseType = JSON.parse(message); + if (resp.status !== 200) { + throw resp.error; + } + return resp.value; + }) + .catch((err) => { + throw err; + }); + + return reply; + }; + + getClusterConnectionDetails = async ( + params?: Map, + ): Promise => { + const headers = this.getAuthHeaders(); + + let searchPattern = this.componentDetailsMap.get( + ArtemisJolokia.CLUSTER_CONNECTION_DETAILS, + ); + + if (typeof params !== 'undefined') { + for (const [key, value] of params) { + searchPattern = searchPattern?.replace(key, value); + } + } + searchPattern = searchPattern?.replace('BROKER_NAME', this.brokerName); + + const url = this.baseUrl + 'list/' + searchPattern; const reply = await fetch(url, { method: 'GET', @@ -282,15 +322,8 @@ export class ArtemisJolokia { ): Promise => { const headers = this.getAuthHeaders(); headers.set('Content-Type', 'application/json'); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/'; - const reply = await fetch(url, { + const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForAttributes( @@ -322,18 +355,11 @@ export class ArtemisJolokia { ): Promise => { const headers = this.getAuthHeaders(); headers.set('Content-Type', 'application/json'); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/'; const param = new Map(); param.set('ADDRESS_NAME', addressName); - const reply = await fetch(url, { + const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForAttributes( @@ -359,25 +385,90 @@ export class ArtemisJolokia { return reply; }; + readClusterConnectionAttributes = async ( + clusterConnectionName: string, + clusterConnectionAttrNames: string[], + ): Promise => { + const headers = this.getAuthHeaders(); + headers.set('Content-Type', 'application/json'); + + const param = new Map(); + param.set('CLUSTER_CONNECTION_NAME', clusterConnectionName); + + const reply = await fetch(this.baseUrl, { + method: 'POST', + headers: headers, + body: this.getPostBodyForAttributes( + ArtemisJolokia.CLUSTER_CONNECTION, + param, + clusterConnectionAttrNames, + ), + }) + .then((response) => { + if (response.ok) { + return response.text(); + } + throw response; + }) //directly use json()? + .then((message) => { + const resp: JolokiaReadResponse[] = JSON.parse(message); + return resp; + }) + .catch((err) => { + throw err; + }); + + return reply; + }; + + execClusterConnectionOperation = async ( + param: Map, + signature: string, + args: string[], + ): Promise => { + const headers = this.getAuthHeaders(); + headers.set('Content-Type', 'application/json'); + + const reply = await fetch(this.baseUrl, { + method: 'POST', + headers: headers, + body: this.getPostBodyForOperation( + ArtemisJolokia.CLUSTER_CONNECTION, + param, + signature, + args, + ), + }) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + throw response; + } + }) + .then((jsonObj) => { + return jsonObj as JolokiaExecResponse; + }) + .catch((err) => { + throw err; + }); + + return reply; + }; + execBrokerOperation = async ( signature: string, args: string[], ): Promise => { const headers = this.getAuthHeaders(); headers.set('Content-Type', 'application/json'); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/'; - const reply = await fetch(url, { + const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForOperation( ArtemisJolokia.BROKER, + new Map(), signature, args, ), @@ -415,14 +506,7 @@ export class ArtemisJolokia { } searchPattern = searchPattern?.replace('BROKER_NAME', this.brokerName); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/list/' + - searchPattern; + const url = this.baseUrl + 'list/' + searchPattern; const reply = await fetch(url, { method: 'GET', @@ -456,20 +540,13 @@ export class ArtemisJolokia { ): Promise => { const headers = this.getAuthHeaders(); headers.set('Content-Type', 'application/json'); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/'; const param = new Map(); param.set('QUEUE_NAME', queueName); param.set('ROUTING_TYPE', routingType); param.set('ADDRESS_NAME', addressName); - const reply = await fetch(url, { + const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForAttributes( @@ -500,18 +577,11 @@ export class ArtemisJolokia { ): Promise => { const headers = this.getAuthHeaders(); headers.set('Content-Type', 'application/json'); - const url = - this.protocol + - '://' + - this.hostName + - ':' + - this.port + - '/console/jolokia/'; const param = new Map(); param.set('ACCEPTOR_NAME', acceptorName); - const reply = await fetch(url, { + const reply = await fetch(this.baseUrl, { method: 'POST', headers: headers, body: this.getPostBodyForAttributes( @@ -581,6 +651,7 @@ export class ArtemisJolokia { getPostBodyForOperation = ( component: string, + params: Map, signature: string, args?: string[], ): string => { @@ -590,6 +661,9 @@ export class ArtemisJolokia { throw 'undefined bean'; } bean = bean.replace('BROKER_NAME', this.brokerName); + params.forEach((value, key) => { + bean = bean.replace(key, value); + }); bodyItems.push({ type: 'exec', diff --git a/src/api/controllers/api_impl.ts b/src/api/controllers/api_impl.ts index 5aa5c6b..efc6968 100644 --- a/src/api/controllers/api_impl.ts +++ b/src/api/controllers/api_impl.ts @@ -51,6 +51,154 @@ export const getBrokers = (_: express.Request, res: express.Response): void => { } }; +export const getClusterConnections = ( + _: express.Request, + res: express.Response, +): void => { + try { + const jolokia = res.locals.jolokia; + + const comps = jolokia.getComponents(ArtemisJolokia.CLUSTER_CONNECTION); + + comps + .then((result: any[]) => { + res.json( + result.map((entry: string) => { + const props = parseProps(entry); + const clusterConnection = { + name: props.get('name'), + broker: { + name: props.get(BROKER), + }, + }; + return clusterConnection; + }), + ); + }) + .catch((error: any) => { + console.log(error); + }); + } catch (err) { + console.log(err); + res.status(500).json({ + status: 'error', + message: 'server error: ' + JSON.stringify(err), + }); + } +}; + +export const readClusterConnectionAttributes = ( + req: express.Request, + res: express.Response, +): void => { + try { + const jolokia = res.locals.jolokia; + const clusterConnectionName = req.query.name as string; + const clusterConnectionAttrNames = req.query.attrs as string[]; + + const attributes = jolokia.readClusterConnectionAttributes( + clusterConnectionName, + clusterConnectionAttrNames, + ); + attributes + .then((result: JolokiaReadResponse[]) => { + res.json(result); + }) + .catch((error: any) => { + console.log(error); + res.status(500).json({ status: 'error', message: 'server error' }); + }); + } catch (err) { + console.log(err); + res.status(500).json({ + status: 'error', + message: 'server error: ' + JSON.stringify(err), + }); + } +}; + +export const getClusterConnectionDetails = ( + req: express.Request, + res: express.Response, +): void => { + try { + const jolokia = res.locals.jolokia; + + const clusterConnectionName = req.query.name; + + const param = new Map(); + param.set('CLUSTER_CONNECTION_NAME', clusterConnectionName); + + const compDetails = jolokia.getClusterConnectionDetails(param); + + compDetails + .then((result: JolokiaObjectDetailsType) => { + Object.entries(result.op).forEach(([key, value]) => { + if (!Array.isArray(value)) { + result.op[key] = [value]; + } + }); + res.json(result); + }) + .catch((error: any) => { + console.log(error); + res.status(500).json({ status: 'error', message: 'server error' }); + }); + } catch (err) { + console.log(err); + res.status(500).json({ + status: 'error', + message: 'server error: ' + JSON.stringify(err), + }); + } +}; + +export const execClusterConnectionOperation = ( + req: express.Request, + res: express.Response, +): void => { + try { + const jolokia = res.locals.jolokia; + + const operationRef = req.body as OperationRef; + const strArgs: string[] = []; + let strSignature = operationRef.signature.name + '('; + operationRef.signature.args.forEach((arg, item, array) => { + strSignature = strSignature + arg.type; + if (item < array.length - 1) { + strSignature = strSignature + ','; + } + strArgs.push(arg.value); + }); + strSignature = strSignature + ')'; + + const clusterConnectionName = req.query.name; + + const param = new Map(); + param.set('CLUSTER_CONNECTION_NAME', clusterConnectionName); + + const resp = jolokia.execClusterConnectionOperation( + param, + strSignature, + strArgs, + ); + resp + .then((result: JolokiaExecResponse) => { + res.json(result); + }) + .catch((error: any) => { + console.log(error); + res.status(500).json({ status: 'error', message: 'server error' }); + }); + } catch (err) { + console.log(err); + res.status(500).json({ + status: 'error', + message: 'server error: ' + JSON.stringify(err), + }); + } +}; + export const getAcceptors = ( _: express.Request, res: express.Response, diff --git a/src/api/controllers/security.ts b/src/api/controllers/security.ts index 4691990..7c74207 100644 --- a/src/api/controllers/security.ts +++ b/src/api/controllers/security.ts @@ -155,6 +155,7 @@ export const VerifyLogin = async ( res: express.Response, next: any, ) => { + console.log('verify login for request:', req.path); try { if (ignoreAuth(req.path)) { console.log('ignore path', req.path); @@ -166,6 +167,7 @@ export const VerifyLogin = async ( console.log('no auth'); res.sendStatus(401); } else { + console.log('verifying header', authHeader); jwt.verify( authHeader, getSecretToken(), diff --git a/src/config/openapi.yml b/src/config/openapi.yml index 515e6f8..0824a10 100644 --- a/src/config/openapi.yml +++ b/src/config/openapi.yml @@ -483,6 +483,118 @@ paths: schema: type: object $ref: '#/components/schemas/FailureResponse' + /readClusterConnectionAttributes: + get: + summary: read cluster connection attributes + description: > + **Read values of cluster connection attributes** + The return value is a json array that contains + values of requested attributes of the cluster connection's mbean. + + **Note**: to read multiple attributes, set it to **attrs** parameter + separated by commas, e.g. `NodeID, Topology`. + tags: + - jolokia + operationId: readClusterConnectionAttributes + parameters: + - in: header + name: jolokia-session-id + schema: + type: string + required: true + - name: name + description: the cluster connection name + schema: + type: string + required: true + in: query + - name: attrs + description: attribute names separated by commas. If not speified read all attributes. + required: false + in: query + schema: + type: array + items: + type: string + style: form + explode: false + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ComponentAttribute' + 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' + /execClusterConnectionOperation: + post: + summary: execute a cluster connection operation + description: > + **Invoke an operation of the cluster connection mbean** + + It receives a POST request where the body + should have the operation signature and its args. + The return value is a one element json array that contains + return values of invoked operation along with the request info. + tags: + - jolokia + operationId: execClusterConnectionOperation + parameters: + - in: header + name: jolokia-session-id + schema: + type: string + required: true + - in: query + name: name + schema: + type: string + required: true + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OperationRef' + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ExecResult' + 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' + /checkCredentials: get: summary: Check the validity of the credentials @@ -875,6 +987,90 @@ paths: schema: type: object $ref: '#/components/schemas/FailureResponse' + /clusterConnections: + get: + summary: list cluster connections + description: > + **Get all cluster connections in a broker** + + It retrieves and returns a list of all cluster connection mbeans + tags: + - jolokia + operationId: getClusterConnections + parameters: + - in: header + name: jolokia-session-id + schema: + type: string + required: true + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ClusterConnection' + 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' + /clusterConnectionDetails: + get: + summary: retrieve cluster connection details + description: > + **Get details of a connection cluster** + The return value is a json object that contains + description of all the operations and attributes of a `cluster connection` mbean. + + It is defined in [ClusterConnectionControl.java](https://github.com/apache/activemq-artemis/blob/2.33.0/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/ClusterConnectionControl.java) + tags: + - jolokia + operationId: getClusterConnectionDetails + parameters: + - in: header + name: jolokia-session-id + schema: + type: string + required: true + - name: name + required: true + in: query + schema: + type: string + description: the cluster connection name + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ComponentDetails' + 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' /api-info: get: summary: the api info @@ -1035,6 +1231,16 @@ components: type: string broker: $ref: '#/components/schemas/Broker' + ClusterConnection: + type: object + required: + - name + - broker + properties: + name: + type: string + broker: + $ref: '#/components/schemas/Broker' Queue: type: object required: diff --git a/src/utils/server.test.ts b/src/utils/server.test.ts index 213a193..dfedfa4 100644 --- a/src/utils/server.test.ts +++ b/src/utils/server.test.ts @@ -855,4 +855,203 @@ describe('test api server apis', () => { const value = await resp.json(); expect(JSON.stringify(value)).toEqual(JSON.stringify(result)); }); + + it('test get cluster connections', async () => { + const expectedApiResult = [ + { + name: 'my-cluster', + broker: { + name: 'amq-broker', + }, + }, + ]; + + const jolokiaResult = [ + 'org.apache.activemq.artemis:broker="amq-broker",component=cluster-connections,name="my-cluster"', + ]; + + const jolokiaResp = { + request: {}, + value: jolokiaResult, + timestamp: 1714703745, + status: 200, + }; + + mockJolokia + .get( + apiUrlPrefix + + '/search/org.apache.activemq.artemis:broker=%22amq-broker%22,component=cluster-connections,name=*', + ) + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doGet('/clusterConnections', authToken); + expect(resp.ok).toBeTruthy(); + + const value = await resp.json(); + expect(value.length).toEqual(expectedApiResult.length); + for (let i = 0; i < value.length; i++) { + expect(value[i]).toEqual(expectedApiResult[i]); + } + }); + + it('test clusterConnectionDetails', async () => { + const apiResult = { + op: { + stop: [ + { + args: [], + ret: 'void', + desc: 'stop this component', + }, + ], + }, + attr: { + Address: { + rw: false, + type: 'java.lang.String', + desc: 'address used by this cluster connection', + }, + }, + class: + 'org.apache.activemq.artemis.core.management.impl.ClusterConnectionControlImpl', + desc: 'Information on the management interface of the MBean', + }; + + const jolokiaResult = { + op: { + stop: { + args: [], + ret: 'void', + desc: 'stop this component', + }, + }, + attr: { + Address: { + rw: false, + type: 'java.lang.String', + desc: 'address used by this cluster connection', + }, + }, + class: + 'org.apache.activemq.artemis.core.management.impl.ClusterConnectionControlImpl', + desc: 'Information on the management interface of the MBean', + }; + const jolokiaResp = { + request: {}, + value: jolokiaResult, + timestamp: 1714703745, + status: 200, + }; + mockJolokia + .get( + apiUrlPrefix + + '/list/org.apache.activemq.artemis:name=%22my-cluster%22,broker=%22amq-broker%22/component=cluster-connections', + ) + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doGet( + '/clusterConnectionDetails?name=my-cluster', + authToken, + ); + expect(resp.ok).toBeTruthy(); + + const value = await resp.json(); + expect(JSON.stringify(value)).toEqual(JSON.stringify(apiResult)); + }); + + it('test readClusterConnectionAttributes', async () => { + const jolokiaResp = [ + { + request: { + mbean: + 'org.apache.activemq.artemis:broker="amq-broker",component=cluster-connections,name="my-cluster"', + attribute: 'MessageLoadBalancingType', + type: 'read', + }, + value: 'ON_DEMAND', + timestamp: 1716368499, + status: 200, + }, + ]; + + mockJolokia + .post(apiUrlPrefix + '/', (body) => { + if ( + body.length === 1 && + body[0].type === 'read' && + body[0].mbean === + 'org.apache.activemq.artemis:broker="amq-broker",component=cluster-connections,name="my-cluster"' && + body[0].attribute === 'MessageLoadBalancingType' + ) { + return true; + } + return false; + }) + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doGet( + '/readClusterConnectionAttributes?name=my-cluster&attrs=MessageLoadBalancingType', + authToken, + ); + expect(resp.ok).toBeTruthy(); + + const value = await resp.json(); + expect(JSON.stringify(value)).toEqual(JSON.stringify(jolokiaResp)); + }); + + it('test execClusterConnectionOperation', async () => { + const jolokiaResp = [ + { + request: { + mbean: + 'org.apache.activemq.artemis:broker="amq-broker",component=cluster-connections,name="my-cluster"', + arguments: ['d71746e8-769b-11ef-8fc9-201e881aa455'], + type: 'exec', + operation: 'getBridgeMetrics(java.lang.String)', + }, + value: { + messagesAcknowledged: 0, + messagesPendingAcknowledgement: 0, + }, + timestamp: 1716385483, + status: 200, + }, + ]; + + mockJolokia + .post(apiUrlPrefix + '/', (body) => { + if ( + body.length === 1 && + body[0].type === 'exec' && + body[0].mbean === + 'org.apache.activemq.artemis:broker="amq-broker",component=cluster-connections,name="my-cluster"' && + body[0].operation === 'getBridgeMetrics(java.lang.String)' && + body[0].arguments[0] === 'd71746e8-769b-11ef-8fc9-201e881aa455' + ) { + return true; + } + return false; + }) + .reply(200, JSON.stringify(jolokiaResp)); + + const resp = await doPost( + '/execClusterConnectionOperation?name=my-cluster', + JSON.stringify({ + signature: { + name: 'getBridgeMetrics', + args: [ + { + type: 'java.lang.String', + value: 'd71746e8-769b-11ef-8fc9-201e881aa455', + }, + ], + }, + }), + authToken, + ); + expect(resp.ok).toBeTruthy(); + + const value = await resp.json(); + expect(JSON.stringify(value)).toEqual(JSON.stringify(jolokiaResp)); + }); });