Skip to content

Commit c1e3f12

Browse files
committed
sandbox polling
1 parent 9853001 commit c1e3f12

1 file changed

Lines changed: 124 additions & 1 deletion

File tree

packages/b2c-cli/src/commands/ods/create.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {t} from '../../i18n/index.js';
66

77
type SandboxModel = OdsComponents['schemas']['SandboxModel'];
88
type SandboxResourceProfile = OdsComponents['schemas']['SandboxResourceProfile'];
9+
type SandboxState = OdsComponents['schemas']['SandboxState'];
10+
11+
/** States that indicate sandbox creation has completed (success or failure) */
12+
const TERMINAL_STATES = new Set<SandboxState>(['deleted', 'failed', 'started']);
913

1014
/**
1115
* Command to create a new on-demand sandbox.
@@ -20,6 +24,8 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
2024
'<%= config.bin %> <%= command.id %> --realm abcd --ttl 48',
2125
'<%= config.bin %> <%= command.id %> --realm abcd --profile large',
2226
'<%= config.bin %> <%= command.id %> --realm abcd --auto-scheduled',
27+
'<%= config.bin %> <%= command.id %> --realm abcd --wait',
28+
'<%= config.bin %> <%= command.id %> --realm abcd --wait --poll-interval 15',
2329
'<%= config.bin %> <%= command.id %> --realm abcd --json',
2430
];
2531

@@ -42,13 +48,31 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
4248
description: 'Enable automatic start/stop scheduling',
4349
default: false,
4450
}),
51+
wait: Flags.boolean({
52+
char: 'w',
53+
description: 'Wait for the sandbox to reach started or failed state before returning',
54+
default: false,
55+
}),
56+
'poll-interval': Flags.integer({
57+
description: 'Polling interval in seconds when using --wait',
58+
default: 10,
59+
dependsOn: ['wait'],
60+
}),
61+
timeout: Flags.integer({
62+
description: 'Maximum time to wait in seconds when using --wait (0 for no timeout)',
63+
default: 600,
64+
dependsOn: ['wait'],
65+
}),
4566
};
4667

4768
async run(): Promise<SandboxModel> {
4869
const realm = this.flags.realm;
4970
const profile = this.flags.profile as SandboxResourceProfile;
5071
const ttl = this.flags.ttl;
5172
const autoScheduled = this.flags['auto-scheduled'];
73+
const wait = this.flags.wait;
74+
const pollInterval = this.flags['poll-interval'];
75+
const timeout = this.flags.timeout;
5276

5377
this.log(t('commands.ods.create.creating', 'Creating sandbox in realm {{realm}}...', {realm}));
5478
this.log(t('commands.ods.create.profile', 'Profile: {{profile}}', {profile}));
@@ -74,11 +98,16 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
7498
);
7599
}
76100

77-
const sandbox = result.data.data;
101+
let sandbox = result.data.data;
78102

79103
this.log('');
80104
this.log(t('commands.ods.create.success', 'Sandbox created successfully!'));
81105

106+
if (wait && sandbox.id) {
107+
this.log('');
108+
sandbox = await this.waitForSandbox(sandbox.id, pollInterval, timeout);
109+
}
110+
82111
if (this.jsonEnabled()) {
83112
return sandbox;
84113
}
@@ -115,4 +144,98 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
115144

116145
ux.stdout(ui.toString());
117146
}
147+
148+
/**
149+
* Sleep for a given number of milliseconds.
150+
*/
151+
private async sleep(ms: number): Promise<void> {
152+
await new Promise((resolve) => {
153+
setTimeout(resolve, ms);
154+
});
155+
}
156+
157+
/**
158+
* Polls for sandbox status until it reaches a terminal state.
159+
* @param sandboxId - The sandbox ID to poll
160+
* @param pollIntervalSeconds - Interval between polls in seconds
161+
* @param timeoutSeconds - Maximum time to wait (0 for no timeout)
162+
* @returns The final sandbox state
163+
*/
164+
private async waitForSandbox(
165+
sandboxId: string,
166+
pollIntervalSeconds: number,
167+
timeoutSeconds: number,
168+
): Promise<SandboxModel> {
169+
const startTime = Date.now();
170+
const pollIntervalMs = pollIntervalSeconds * 1000;
171+
const timeoutMs = timeoutSeconds * 1000;
172+
let lastState: SandboxState | undefined;
173+
174+
this.log(t('commands.ods.create.waiting', 'Waiting for sandbox to be ready...'));
175+
176+
while (true) {
177+
// Check for timeout
178+
if (timeoutSeconds > 0 && Date.now() - startTime > timeoutMs) {
179+
this.error(
180+
t('commands.ods.create.timeout', 'Timeout waiting for sandbox after {{seconds}} seconds', {
181+
seconds: String(timeoutSeconds),
182+
}),
183+
);
184+
}
185+
186+
// eslint-disable-next-line no-await-in-loop
187+
const result = await this.odsClient.GET('/sandboxes/{sandboxId}', {
188+
params: {
189+
path: {sandboxId},
190+
},
191+
});
192+
193+
if (!result.data?.data) {
194+
this.error(
195+
t('commands.ods.create.pollError', 'Failed to fetch sandbox status: {{message}}', {
196+
message: result.response?.statusText || 'Unknown error',
197+
}),
198+
);
199+
}
200+
201+
const sandbox = result.data.data;
202+
const currentState = sandbox.state as SandboxState;
203+
204+
// Log state changes
205+
if (currentState !== lastState) {
206+
const elapsed = Math.round((Date.now() - startTime) / 1000);
207+
this.log(
208+
t('commands.ods.create.stateChange', '[{{elapsed}}s] State: {{state}}', {
209+
elapsed: String(elapsed),
210+
state: currentState || 'unknown',
211+
}),
212+
);
213+
lastState = currentState;
214+
}
215+
216+
// Check for terminal states
217+
if (currentState && TERMINAL_STATES.has(currentState)) {
218+
switch (currentState) {
219+
case 'deleted': {
220+
this.error(t('commands.ods.create.deleted', 'Sandbox was deleted'));
221+
break;
222+
}
223+
case 'failed': {
224+
this.error(t('commands.ods.create.failed', 'Sandbox creation failed'));
225+
break;
226+
}
227+
case 'started': {
228+
this.log('');
229+
this.log(t('commands.ods.create.ready', 'Sandbox is now ready!'));
230+
break;
231+
}
232+
}
233+
return sandbox;
234+
}
235+
236+
// Wait before next poll
237+
// eslint-disable-next-line no-await-in-loop
238+
await this.sleep(pollIntervalMs);
239+
}
240+
}
118241
}

0 commit comments

Comments
 (0)