From 361a8f0ea70ab87732dde9e007deaa955b57545d Mon Sep 17 00:00:00 2001 From: Paul Richardson Date: Sat, 5 Oct 2024 17:06:05 +0100 Subject: [PATCH] fix: Upgrades management-api use of jolokia * managed-pod.ts * Simplifies with removal of jolokia-simple API to just use fetch only * jolokia-response-utils.ts * Updates on type API changes from jolokia --- docker/gateway/src/jolokia-agent/globals.ts | 25 ++-- .../src/jolokia-agent/jolokia-agent.ts | 2 +- docker/gateway/src/jolokia-agent/rbac.test.ts | 2 +- docker/gateway/src/jolokia-agent/rbac.ts | 14 ++- .../src/jolokia-response-utils.ts | 22 ++-- packages/management-api/src/managed-pod.ts | 113 +++++++++--------- 6 files changed, 94 insertions(+), 84 deletions(-) diff --git a/docker/gateway/src/jolokia-agent/globals.ts b/docker/gateway/src/jolokia-agent/globals.ts index 6d33ae52..709012b9 100644 --- a/docker/gateway/src/jolokia-agent/globals.ts +++ b/docker/gateway/src/jolokia-agent/globals.ts @@ -1,6 +1,5 @@ import { Request as ExpressRequest, Response as ExpressResponse } from 'express-serve-static-core' -import { MBeanInfo, MBeanInfoError, MBeanAttribute, MBeanOperation, Request as MBeanRequest } from 'jolokia.js' -import 'jolokia.js/simple' +import { MBeanInfo, MBeanInfoError, MBeanAttribute, MBeanOperation, JolokiaRequest as MBeanRequest } from 'jolokia.js' import { GatewayOptions } from 'src/globals' export interface BulkValue { @@ -51,7 +50,7 @@ export interface OptimisedMBeanInfo extends Omit { } interface OperationDefined { - op: MBeanOperation + op: Record } interface AttributeDefined { @@ -156,7 +155,7 @@ export function isMBeanOperation(obj: unknown): obj is MBeanOperation { export function hasMBeanOperation(obj: unknown): obj is OperationDefined { if (!obj) return false - return isMBeanOperation((obj as OperationDefined).op) && (obj as OperationDefined)?.op !== undefined + return (obj as OperationDefined).op !== undefined } export function hasMBeanAttribute(obj: unknown): obj is AttributeDefined { @@ -165,6 +164,16 @@ export function hasMBeanAttribute(obj: unknown): obj is AttributeDefined { return (obj as AttributeDefined)?.attr !== undefined } +export function isMBeanAttribute(obj: unknown): obj is MBeanAttribute { + if (!obj) return false + + return ( + (obj as MBeanAttribute).desc !== undefined && + (obj as MBeanAttribute).type !== undefined && + (obj as MBeanAttribute).rw !== undefined + ) +} + export function isArgumentExecRequest(obj: unknown): obj is ExecMBeanRequest { if (!obj) return false @@ -177,14 +186,14 @@ export function hasArguments(obj: unknown): obj is ArgumentRequest { return isArgumentExecRequest(obj) && (obj as ArgumentRequest).arguments !== undefined } -export function isMBeanInfo(obj: MBeanInfo | MBeanInfoError): obj is MBeanInfo { - if (!obj) return false +export function isMBeanInfo(obj: string | MBeanInfo | MBeanInfoError): obj is MBeanInfo { + if (!obj || typeof obj === 'string') return false return (obj as MBeanInfo).desc !== undefined } -export function isMBeanInfoError(obj: MBeanInfo | MBeanInfoError): obj is MBeanInfoError { - if (!obj) return false +export function isMBeanInfoError(obj: string | MBeanInfo | MBeanInfoError): obj is MBeanInfoError { + if (!obj || typeof obj === 'string') return false return (obj as MBeanInfoError).error !== undefined } diff --git a/docker/gateway/src/jolokia-agent/jolokia-agent.ts b/docker/gateway/src/jolokia-agent/jolokia-agent.ts index acce4b6d..7d45a637 100644 --- a/docker/gateway/src/jolokia-agent/jolokia-agent.ts +++ b/docker/gateway/src/jolokia-agent/jolokia-agent.ts @@ -2,7 +2,7 @@ import yaml from 'yaml' import { Request as ExpressRequest, Response as ExpressResponse } from 'express-serve-static-core' import { jwtDecode } from 'jwt-decode' import * as fs from 'fs' -import { Request as MBeanRequest } from 'jolokia.js' +import { JolokiaRequest as MBeanRequest } from 'jolokia.js' import { logger } from '../logger' import { GatewayOptions } from '../globals' import { isObject, isError } from '../utils' diff --git a/docker/gateway/src/jolokia-agent/rbac.test.ts b/docker/gateway/src/jolokia-agent/rbac.test.ts index f314e99a..fe995c14 100644 --- a/docker/gateway/src/jolokia-agent/rbac.test.ts +++ b/docker/gateway/src/jolokia-agent/rbac.test.ts @@ -159,7 +159,7 @@ describe('intercept', function () { listMBeans, ) expect(result.intercepted).toBe(true) - expect(hasMBeanOperation(result.response?.value)).toBe(false) + expect(hasMBeanOperation(result.response?.value)).toBe(true) }) it('should intercept optimised list MBeans requests', function () { diff --git a/docker/gateway/src/jolokia-agent/rbac.ts b/docker/gateway/src/jolokia-agent/rbac.ts index 67904e3a..bbdfe7e7 100644 --- a/docker/gateway/src/jolokia-agent/rbac.ts +++ b/docker/gateway/src/jolokia-agent/rbac.ts @@ -1,12 +1,11 @@ import { - Request as MBeanRequest, + JolokiaRequest as MBeanRequest, JmxDomains, MBeanInfo, MBeanInfoError, MBeanOperation, MBeanOperationArgument, } from 'jolokia.js' -import 'jolokia.js/simple' import { BulkValue, Intercepted, @@ -19,6 +18,7 @@ import { hasMBeanAttribute, hasMBeanOperation, isArgumentExecRequest, + isMBeanAttribute, isMBeanDefinedRequest, isMBeanInfoError, isOptimisedMBeanInfo, @@ -154,10 +154,12 @@ export function intercept(request: MBeanRequest, role: string, mbeans: JmxDomain if (hasMBeanAttribute(info)) { // Check attributes - const res = Object.entries(info.attr || []).find( - attr => - canInvokeGetter(mbean, attr[0], attr[1].type, role) || canInvokeSetter(mbean, attr[0], attr[1].type, role), - ) + const res = Object.entries(info.attr || []).find(([key, attr]) => { + return ( + isMBeanAttribute(attr) && + (canInvokeGetter(mbean, key, attr.type, role) || canInvokeSetter(mbean, key, attr.type, role)) + ) + }) return intercepted(typeof res !== 'undefined') } diff --git a/packages/management-api/src/jolokia-response-utils.ts b/packages/management-api/src/jolokia-response-utils.ts index 94eee1ea..ee0693f4 100644 --- a/packages/management-api/src/jolokia-response-utils.ts +++ b/packages/management-api/src/jolokia-response-utils.ts @@ -1,7 +1,7 @@ import { - Response as JolokiaResponse, - ErrorResponse as JolokiaErrorResponse, - VersionResponse as JolokiaVersionResponse, + JolokiaErrorResponse, + VersionResponseValue as JolokiaVersionResponseValue, + JolokiaSuccessResponse, } from 'jolokia.js' export type ParseResult = { hasError: false; parsed: T } | { hasError: true; error: string } @@ -11,7 +11,7 @@ function isObject(value: unknown): value is object { return value != null && (type === 'object' || type === 'function') } -export function isJolokiaResponseType(o: unknown): o is JolokiaResponse { +export function isJolokiaResponseSuccessType(o: unknown): o is JolokiaSuccessResponse { return isObject(o) && 'status' in o && 'timestamp' in o && 'value' in o } @@ -19,20 +19,22 @@ export function isJolokiaResponseErrorType(o: unknown): o is JolokiaErrorRespons return isObject(o) && 'error_type' in o && 'error' in o } -export function isJolokiaVersionResponseType(o: unknown): o is JolokiaVersionResponse { +export function isJolokiaVersionResponseType(o: unknown): o is JolokiaVersionResponseValue { return isObject(o) && 'protocol' in o && 'agent' in o && 'info' in o } -export function jolokiaResponseParse(text: string): ParseResult { +export async function jolokiaResponseParse( + response: Response, +): Promise> { try { - const parsed = JSON.parse(text) + const parsed = await response.json() if (isJolokiaResponseErrorType(parsed)) { const errorResponse: JolokiaErrorResponse = parsed as JolokiaErrorResponse return { error: errorResponse.error, hasError: true } - } else if (isJolokiaResponseType(parsed)) { - const response: JolokiaResponse = parsed as JolokiaResponse - return { parsed: response, hasError: false } + } else if (isJolokiaResponseSuccessType(parsed)) { + const parsedResponse: JolokiaSuccessResponse = parsed as JolokiaSuccessResponse + return { parsed: parsedResponse, hasError: false } } else { return { error: 'Unrecognised jolokia response', hasError: true } } diff --git a/packages/management-api/src/managed-pod.ts b/packages/management-api/src/managed-pod.ts index c06a96b8..15b650bd 100644 --- a/packages/management-api/src/managed-pod.ts +++ b/packages/management-api/src/managed-pod.ts @@ -1,11 +1,9 @@ -import Jolokia, { - BaseRequestOptions, - Response as JolokiaResponse, - VersionResponse as JolokiaVersionResponse, +import { + JolokiaErrorResponse, + JolokiaSuccessResponse, + VersionResponseValue as JolokiaVersionResponseValue, } from 'jolokia.js' -import 'jolokia.js/simple' -import $ from 'jquery' -import { log } from './globals' +import { eventService } from '@hawtio/react' import jsonpath from 'jsonpath' import { k8Api, @@ -16,15 +14,8 @@ import { PodSpec, JOLOKIA_PORT_QUERY, } from '@hawtio/online-kubernetes-api' +import { log } from './globals' import { ParseResult, isJolokiaVersionResponseType, jolokiaResponseParse } from './jolokia-response-utils' -import { eventService } from '@hawtio/react' - -const DEFAULT_JOLOKIA_OPTIONS: BaseRequestOptions = { - method: 'post', - mimeType: 'application/json', - canonicalNaming: false, - ignoreErrors: true, -} as const export type Management = { status: { @@ -56,7 +47,6 @@ export class ManagedPod { readonly jolokiaPort: number readonly jolokiaPath: string - readonly jolokia: Jolokia private _management: Management = { status: { @@ -78,7 +68,6 @@ export class ManagedPod { constructor(public kubePod: KubePod) { this.jolokiaPort = this.extractPort(kubePod) this.jolokiaPath = ManagedPod.getJolokiaPath(kubePod, this.jolokiaPort) || '' - this.jolokia = this.createJolokia() } static getAnnotation(pod: KubePod, name: string, defaultValue: string): string { @@ -116,17 +105,6 @@ export class ManagedPod { return ports[0].containerPort || ManagedPod.DEFAULT_JOLOKIA_PORT } - private createJolokia() { - if (!this.jolokiaPath || this.jolokiaPath.length === 0) { - throw new Error(`Failed to find jolokia path for pod ${this.kubePod.metadata?.uid}`) - } - - const options = { ...DEFAULT_JOLOKIA_OPTIONS } - options.url = this.jolokiaPath - - return new Jolokia(options) - } - get kind(): string | undefined { return this.kubePod.kind } @@ -199,56 +177,75 @@ export class ManagedPod { async probeJolokiaUrl(): Promise { return new Promise((resolve, reject) => { - $.ajax({ - url: `${this.jolokiaPath}version`, - method: 'GET', - dataType: 'text', - }) - .done((data: string, textStatus: string, xhr: JQueryXHR) => { - if (xhr.status !== 200) { - this.setManagementError(xhr.status, textStatus) + const path = `${this.jolokiaPath}version` + fetch(path) + .then(async (response: Response) => { + if (!response.ok) { + log.debug('Using URL:', path, 'assuming it could be an agent but got return code:', response.status) + this.setManagementError(response.status, response.statusText) reject(this.mgmtError) return } - const result: ParseResult = jolokiaResponseParse(data) - if (result.hasError) { - this.setManagementError(500, result.error) - reject(this.mgmtError) - return - } - - const jsonResponse: JolokiaResponse = result.parsed - if (isJolokiaVersionResponseType(jsonResponse.value)) { - const versionResponse = jsonResponse.value as JolokiaVersionResponse + try { + const result: ParseResult = + await jolokiaResponseParse(response) + if (result.hasError) { + this.setManagementError(500, result.error) + reject(this.mgmtError) + return + } + + const jsonResponse: JolokiaSuccessResponse = result.parsed as JolokiaSuccessResponse + if (!isJolokiaVersionResponseType(jsonResponse.value)) { + this.setManagementError(500, 'Detected jolokia but cannot determine agent or version') + reject(this.mgmtError) + return + } + + const versionResponse = jsonResponse.value as JolokiaVersionResponseValue log.debug('Found jolokia agent at:', this.jolokiaPath, 'details:', versionResponse.agent) resolve(this.jolokiaPath) - } else { - this.setManagementError(500, 'Detected jolokia but cannot determine agent or version') + } catch (e) { + // Parse error should mean redirect to html + const msg = `Jolokia Connect Error - ${e ?? response.statusText}` + this.setManagementError(response.status, msg) reject(this.mgmtError) } }) - .fail((xhr: JQueryXHR, _: string, error: string) => { - const msg = `Jolokia Connect Error - ${error ?? xhr.statusText}` - this.setManagementError(xhr.status, msg) + .catch(error => { + this.setManagementError(error.status, error.error) reject(this.mgmtError) }) }) } search(successCb: () => void, failCb: (error: Error) => void) { - this.jolokia.search('org.apache.camel:context=*,type=routes,*', { + const body = { + type: 'search', + mbean: 'org.apache.camel:context=*,type=routes,*', + } + + fetch(`${this.jolokiaPath}?ignoreErrors=true&canonicalNaming=false&mimeType=application/json`, { method: 'post', - success: (routes: string[]) => { + body: JSON.stringify(body), + }) + .then(async (response: Response) => { + if (!response.ok) { + return Promise.reject(response) + } + + const data = await response.json() + const routes = data.value as string[] + this._management.status.error = undefined this._management.camel.routes_count = routes.length successCb() - }, - error: error => { + }) + .catch(error => { this.setManagementError(error.status, error.error) - failCb(this.mgmtError as Error) - }, - }) + failCb(error) + }) } errorNotify() {