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() {