Skip to content

Commit b84a75e

Browse files
New command to check status of broker operation.
1 parent c3caf2c commit b84a75e

File tree

7 files changed

+266
-2
lines changed

7 files changed

+266
-2
lines changed

package-lock.json

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"@oclif/core": "^4",
1212
"@oclif/plugin-help": "^6",
1313
"@oclif/plugin-plugins": "^5",
14+
"@types/cli-progress": "^3.11.6",
1415
"axios": "^1.9.0",
16+
"cli-progress": "^3.12.0",
1517
"table": "^6.9.0"
1618
},
1719
"devDependencies": {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Flags } from '@oclif/core'
2+
import cliProgress from 'cli-progress'
3+
4+
import { ScCommand } from '../../../sc-command.js'
5+
import { EventBrokerListApiResponse, OperationData, OperationResponse } from '../../../types/broker.js'
6+
import { ScConnection } from '../../../util/sc-connection.js'
7+
import { camelCaseToTitleCase, renderKeyValueTable, sleep } from '../../../util/internal.js'
8+
9+
export default class MissionctrlBrokerOpstatus extends ScCommand<typeof MissionctrlBrokerOpstatus> {
10+
static override args = {}
11+
static override description = `Get the status of an operation that being performed on an event broker service.
12+
To get the operation, you provide identifier of the operation and the identifier of the event broker service.
13+
14+
Token Permissions: [ mission_control:access or services:get or services:get:self or services:view or services:view:self ]`
15+
static override examples = [
16+
'<%= config.bin %> <%= command.id %>',
17+
]
18+
static override flags = {
19+
'broker-id': Flags.string({
20+
char: 'b',
21+
description: 'Id of the event broker service.',
22+
exactlyOne: ['broker-id', 'name'],
23+
}),
24+
name: Flags.string({
25+
char: 'n',
26+
description: 'Name of the event broker service.',
27+
exactlyOne: ['broker-id', 'name'],
28+
}),
29+
'operation-id': Flags.string({
30+
char: 'o',
31+
description: 'The identifier of the operation being performed on the event broker service.'
32+
}),
33+
'show-progress': Flags.boolean({
34+
char: 'p',
35+
description: 'Displays a status bar of the in-progress operation. The command will wait for completion of each step of the operation.'
36+
}),
37+
'wait-ms': Flags.integer({
38+
char: 'w',
39+
description: 'The milliseconds to wait between API call in when showing progress. Default is 5000 ms.'
40+
}),
41+
}
42+
43+
public async run(): Promise<OperationData> {
44+
const { flags } = await this.parse(MissionctrlBrokerOpstatus)
45+
46+
const name = flags.name ?? ''
47+
let brokerId = flags['broker-id'] ?? ''
48+
const operationId = flags['operation-id'] ?? ''
49+
const showProgress = flags['show-progress'] ?? false
50+
const waitMs = flags['wait-ms'] ?? 5000
51+
52+
const conn = new ScConnection()
53+
54+
// API url
55+
let apiUrl: string = `/missionControl/eventBrokerServices`
56+
57+
// If broker name provided, retrieve the broker service id first
58+
// then retrieve the operation status using the id
59+
if (name) {
60+
// API call to get broker by name
61+
apiUrl += `?customAttributes=name=="${name}"`
62+
const resp = await conn.get<EventBrokerListApiResponse>(apiUrl)
63+
// TODO FUTURE: show status of multiple brokers operations that match the name
64+
if (resp.data.length > 1) {
65+
this.error(`Multiple broker services found with: ${name}. Exactly one broker service must match the provided name.`)
66+
} else {
67+
brokerId = resp.data[0]?.id
68+
}
69+
}
70+
71+
// API call to retrieve status of the broker operation
72+
apiUrl = `/missionControl/eventBrokerServices/${brokerId}/operations/${operationId}`
73+
if (showProgress) {
74+
apiUrl += '?expand=progressLogs'
75+
}
76+
let resp = await conn.get<OperationResponse>(apiUrl)
77+
this.print(resp.data)
78+
79+
// Display progress bar for each step included in the progress logs
80+
// Enable progress bar if set
81+
if (showProgress && resp.data.progressLogs && resp.data.progressLogs.length > 0) {
82+
let progressLogs = resp.data.progressLogs
83+
let numSteps = progressLogs.length
84+
85+
// Create a new progress bar instance and use shades_classic theme
86+
const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
87+
88+
// start the progress bar with a total value of size of the steps and start value of 0
89+
progressBar.start(numSteps, 0);
90+
91+
// Update the progress with the steps completed
92+
let completedNumSteps = 0
93+
while (completedNumSteps < numSteps) {
94+
// Wait before making the next API call
95+
await sleep(waitMs)
96+
// Make another API call to get the lastest progress
97+
resp = await conn.get<OperationResponse>(apiUrl)
98+
if (resp.data.progressLogs) {
99+
progressLogs = resp.data.progressLogs
100+
for (const progressLog of progressLogs) {
101+
if (progressLog.step === 'success') {
102+
completedNumSteps++
103+
}
104+
}
105+
// Update progress bar
106+
progressBar.update(completedNumSteps)
107+
} else {
108+
break
109+
}
110+
}
111+
112+
// stop the progress bar
113+
progressBar.stop()
114+
}
115+
116+
return resp.data
117+
}
118+
119+
private print(environment: OperationData): void {
120+
const tableRows = [
121+
['Key', 'Value'],
122+
...Object.entries(environment).map(([key, value]) => [camelCaseToTitleCase(key), value]),
123+
]
124+
this.log()
125+
this.log(renderKeyValueTable(tableRows))
126+
}
127+
}

src/types/broker.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,32 @@ export interface EventBrokerServiceDetail {
6161
ownedBy?: string
6262
serviceClassId?: string
6363
type?: string
64+
}
65+
66+
/**
67+
* For Broker Operation Status
68+
*/
69+
export interface OperationResponse {
70+
data: OperationData;
71+
}
72+
73+
export interface OperationData {
74+
completedTime: string; // ISO timestamp
75+
createdBy: string;
76+
createdTime: string; // ISO timestamp
77+
id: string;
78+
operationType: string;
79+
progressLogs?: ProgressLog[];
80+
resourceId: string;
81+
resourceType: string;
82+
status: string;
83+
type: "operation";
84+
}
85+
86+
export interface ProgressLog {
87+
message: string;
88+
status: string;
89+
step: string;
90+
stepId: string;
91+
timestamp: string; // ISO timestamp
6492
}

src/util/internal.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,13 @@ export function renderKeyValueTable<T>(
4747
}
4848
const tableStr = table(data, tableConfig)
4949
return tableStr
50+
}
51+
52+
/**
53+
* Sleep function.
54+
* @param ms Num of milliseconds to wait
55+
* @returns
56+
*/
57+
export function sleep(ms: number): Promise<void> {
58+
return new Promise(resolve => setTimeout(resolve, ms));
5059
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { runCommand } from '@oclif/test'
2+
import { expect } from 'chai'
3+
import * as sinon from 'sinon'
4+
5+
import { camelCaseToTitleCase, renderKeyValueTable } from '../../../../src/util/internal.js'
6+
import { ScConnection } from '../../../../src/util/sc-connection.js'
7+
import { createTestOperationResponse, setEnvVariables } from '../../../util/test-utils.js'
8+
9+
describe('missionctrl:broker:opstatus', () => {
10+
setEnvVariables()
11+
const brokerName: string = 'MyTestBroker'
12+
const brokerId: string = 'MyTestBrokerId'
13+
const operationId: string = 'MyOperationId'
14+
let scConnStub: sinon.SinonStub
15+
16+
beforeEach(() => {
17+
scConnStub = sinon.stub(ScConnection.prototype, 'get')
18+
})
19+
20+
afterEach(() => {
21+
scConnStub.restore()
22+
})
23+
24+
it('runs missionctrl:broker:opstatus cmd', async () => {
25+
const { stdout } = await runCommand('missionctrl:broker:opstatus')
26+
expect(stdout).to.contain('')
27+
})
28+
29+
it(`runs missionctrl:broker:opstatus -b ${brokerId}`, async () => {
30+
// Arrange
31+
let opsResponse = createTestOperationResponse(brokerId, 5, operationId, 'in-progress')
32+
scConnStub.returns(Promise.resolve(opsResponse))
33+
34+
const tableRows = [
35+
['Key', 'Value'],
36+
...Object.entries(opsResponse.data).map(([key, value]) => [camelCaseToTitleCase(key), value]),
37+
]
38+
39+
const { stdout } = await runCommand(`missionctrl:broker:opstatus -b ${brokerId}`)
40+
expect(stdout).to.contain(renderKeyValueTable(tableRows))
41+
})
42+
})

test/util/test-utils.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ProgressLog, OperationResponse } from "../../src/types/broker"
2+
13
export function setEnvVariables(): void {
24
process.env.SC_ACCESS_TOKEN = 'TEST'
35
}
@@ -32,4 +34,37 @@ export function aBroker(brokerId: string, brokerName: string,) {
3234
status: '',
3335
type: '',
3436
}
37+
}
38+
39+
export function createTestOperationResponse(brokerId: string, numSteps: number, operationId: string, status: string): OperationResponse {
40+
const opsResp: OperationResponse = {
41+
data: {
42+
completedTime: '2025-08-02T16:29:34Z',
43+
createdBy: '67tr8tku4l',
44+
createdTime: '2025-08-02T16:26:40Z',
45+
id: operationId,
46+
operationType: 'createService',
47+
progressLogs: createTestProgressLogs(numSteps, status),
48+
resourceId: brokerId,
49+
resourceType: 'service',
50+
status: status,
51+
type: "operation"
52+
}
53+
}
54+
return opsResp
55+
}
56+
57+
export function createTestProgressLogs(numSteps: number, status: string): ProgressLog[] {
58+
let progressLogs: ProgressLog[] = []
59+
for (let i = 0; i < numSteps; i++) {
60+
const progressLog: ProgressLog = {
61+
message: 'This is a decription for the step',
62+
status,
63+
step: `This is step number ${i}`,
64+
stepId: `${i}`,
65+
timestamp: '025-08-02T16:26:41.272Z',
66+
}
67+
progressLogs.push(progressLog)
68+
}
69+
return progressLogs
3570
}

0 commit comments

Comments
 (0)