Skip to content

Commit

Permalink
fix: Upgrades management-api use of jolokia
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
phantomjinx committed Oct 5, 2024
1 parent c2a8bb2 commit 361a8f0
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 84 deletions.
25 changes: 17 additions & 8 deletions docker/gateway/src/jolokia-agent/globals.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -51,7 +50,7 @@ export interface OptimisedMBeanInfo extends Omit<MBeanInfo, 'attr' | 'op'> {
}

interface OperationDefined {
op: MBeanOperation
op: Record<string, MBeanOperation | MBeanOperation[]>
}

interface AttributeDefined {
Expand Down Expand Up @@ -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 {
Expand All @@ -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

Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion docker/gateway/src/jolokia-agent/jolokia-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion docker/gateway/src/jolokia-agent/rbac.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
14 changes: 8 additions & 6 deletions docker/gateway/src/jolokia-agent/rbac.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -19,6 +18,7 @@ import {
hasMBeanAttribute,
hasMBeanOperation,
isArgumentExecRequest,
isMBeanAttribute,
isMBeanDefinedRequest,
isMBeanInfoError,
isOptimisedMBeanInfo,
Expand Down Expand Up @@ -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')
}
Expand Down
22 changes: 12 additions & 10 deletions packages/management-api/src/jolokia-response-utils.ts
Original file line number Diff line number Diff line change
@@ -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<T> = { hasError: false; parsed: T } | { hasError: true; error: string }
Expand All @@ -11,28 +11,30 @@ 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
}

export function isJolokiaResponseErrorType(o: unknown): o is JolokiaErrorResponse {
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<JolokiaResponse> {
export async function jolokiaResponseParse(
response: Response,
): Promise<ParseResult<JolokiaSuccessResponse | JolokiaErrorResponse>> {
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 }
}
Expand Down
113 changes: 55 additions & 58 deletions packages/management-api/src/managed-pod.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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: {
Expand Down Expand Up @@ -56,7 +47,6 @@ export class ManagedPod {

readonly jolokiaPort: number
readonly jolokiaPath: string
readonly jolokia: Jolokia

private _management: Management = {
status: {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -199,56 +177,75 @@ export class ManagedPod {

async probeJolokiaUrl(): Promise<string> {
return new Promise<string>((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<JolokiaResponse> = 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<JolokiaSuccessResponse | JolokiaErrorResponse> =
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() {
Expand Down

0 comments on commit 361a8f0

Please sign in to comment.