|
6 | 6 | import {Flags, ux} from '@oclif/core'; |
7 | 7 | import cliui from 'cliui'; |
8 | 8 | import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli'; |
9 | | -import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk'; |
| 9 | +import { |
| 10 | + getApiErrorMessage, |
| 11 | + SandboxPollingError, |
| 12 | + SandboxPollingTimeoutError, |
| 13 | + SandboxTerminalStateError, |
| 14 | + waitForSandbox, |
| 15 | + type OdsComponents, |
| 16 | +} from '@salesforce/b2c-tooling-sdk'; |
10 | 17 | import {t, withDocs} from '../../i18n/index.js'; |
11 | 18 |
|
12 | 19 | type SandboxModel = OdsComponents['schemas']['SandboxModel']; |
13 | 20 | type SandboxResourceProfile = OdsComponents['schemas']['SandboxResourceProfile']; |
14 | | -type SandboxState = OdsComponents['schemas']['SandboxState']; |
15 | 21 | type OcapiSettings = OdsComponents['schemas']['OcapiSettings']; |
16 | 22 | type WebDavSettings = OdsComponents['schemas']['WebDavSettings']; |
17 | 23 | type SandboxSettings = OdsComponents['schemas']['SandboxSettings']; |
18 | 24 |
|
19 | | -/** States that indicate sandbox creation has completed (success or failure) */ |
20 | | -const TERMINAL_STATES = new Set<SandboxState>(['deleted', 'failed', 'started']); |
21 | | - |
22 | 25 | /** |
23 | 26 | * Default OCAPI resources to grant the client ID access to. |
24 | 27 | * These enable common CI/CD operations like code deployment and job execution. |
@@ -155,8 +158,66 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> { |
155 | 158 | this.logger.info({sandboxId: sandbox.id}, t('commands.ods.create.success', 'Sandbox created successfully')); |
156 | 159 |
|
157 | 160 | if (wait && sandbox.id) { |
| 161 | + this.log(t('commands.ods.create.waiting', 'Waiting for sandbox to get started..')); |
| 162 | + |
| 163 | + try { |
| 164 | + await waitForSandbox(this.odsClient, { |
| 165 | + sandboxId: sandbox.id, |
| 166 | + targetState: 'started', |
| 167 | + pollIntervalSeconds: pollInterval, |
| 168 | + timeoutSeconds: timeout, |
| 169 | + onPoll: ({elapsedSeconds, state}) => { |
| 170 | + this.logger.info( |
| 171 | + {sandboxId: sandbox.id, elapsed: elapsedSeconds, state}, |
| 172 | + `[${elapsedSeconds}s] State: ${state}`, |
| 173 | + ); |
| 174 | + }, |
| 175 | + }); |
| 176 | + } catch (error) { |
| 177 | + if (error instanceof SandboxPollingTimeoutError) { |
| 178 | + this.error( |
| 179 | + t('commands.ods.create.timeout', 'Timeout waiting for sandbox after {{seconds}} seconds', { |
| 180 | + seconds: String(error.timeoutSeconds), |
| 181 | + }), |
| 182 | + ); |
| 183 | + } |
| 184 | + |
| 185 | + if (error instanceof SandboxTerminalStateError) { |
| 186 | + if (error.state === 'deleted') { |
| 187 | + this.error(t('commands.ods.create.deleted', 'Sandbox was deleted')); |
| 188 | + } |
| 189 | + this.error(t('commands.ods.create.failed', 'Sandbox creation failed')); |
| 190 | + } |
| 191 | + |
| 192 | + if (error instanceof SandboxPollingError) { |
| 193 | + this.error( |
| 194 | + t('commands.ods.create.pollError', 'Failed to fetch sandbox status: {{message}}', { |
| 195 | + message: error.message, |
| 196 | + }), |
| 197 | + ); |
| 198 | + } |
| 199 | + |
| 200 | + throw error; |
| 201 | + } |
| 202 | + |
| 203 | + const finalResult = await this.odsClient.GET('/sandboxes/{sandboxId}', { |
| 204 | + params: { |
| 205 | + path: {sandboxId: sandbox.id}, |
| 206 | + }, |
| 207 | + }); |
| 208 | + |
| 209 | + if (!finalResult.data?.data) { |
| 210 | + this.error( |
| 211 | + t('commands.ods.create.pollError', 'Failed to fetch sandbox status: {{message}}', { |
| 212 | + message: finalResult.response?.statusText || 'Unknown error', |
| 213 | + }), |
| 214 | + ); |
| 215 | + } |
| 216 | + |
| 217 | + sandbox = finalResult.data.data; |
| 218 | + |
158 | 219 | this.log(''); |
159 | | - sandbox = await this.waitForSandbox(sandbox.id, pollInterval, timeout); |
| 220 | + this.logger.info({sandboxId: sandbox.id}, t('commands.ods.create.ready', 'Sandbox is now ready')); |
160 | 221 | } |
161 | 222 |
|
162 | 223 | if (this.jsonEnabled()) { |
@@ -226,93 +287,4 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> { |
226 | 287 |
|
227 | 288 | ux.stdout(ui.toString()); |
228 | 289 | } |
229 | | - |
230 | | - /** |
231 | | - * Sleep for a given number of milliseconds. |
232 | | - */ |
233 | | - private async sleep(ms: number): Promise<void> { |
234 | | - await new Promise((resolve) => { |
235 | | - setTimeout(resolve, ms); |
236 | | - }); |
237 | | - } |
238 | | - |
239 | | - /** |
240 | | - * Polls for sandbox status until it reaches a terminal state. |
241 | | - * @param sandboxId - The sandbox ID to poll |
242 | | - * @param pollIntervalSeconds - Interval between polls in seconds |
243 | | - * @param timeoutSeconds - Maximum time to wait (0 for no timeout) |
244 | | - * @returns The final sandbox state |
245 | | - */ |
246 | | - private async waitForSandbox( |
247 | | - sandboxId: string, |
248 | | - pollIntervalSeconds: number, |
249 | | - timeoutSeconds: number, |
250 | | - ): Promise<SandboxModel> { |
251 | | - const startTime = Date.now(); |
252 | | - const pollIntervalMs = pollIntervalSeconds * 1000; |
253 | | - const timeoutMs = timeoutSeconds * 1000; |
254 | | - |
255 | | - this.log(t('commands.ods.create.waiting', 'Waiting for sandbox to be ready...')); |
256 | | - |
257 | | - // Initial delay before first poll to allow the sandbox to be registered in the API |
258 | | - await this.sleep(pollIntervalMs); |
259 | | - |
260 | | - while (true) { |
261 | | - // Check for timeout |
262 | | - if (timeoutSeconds > 0 && Date.now() - startTime > timeoutMs) { |
263 | | - this.error( |
264 | | - t('commands.ods.create.timeout', 'Timeout waiting for sandbox after {{seconds}} seconds', { |
265 | | - seconds: String(timeoutSeconds), |
266 | | - }), |
267 | | - ); |
268 | | - } |
269 | | - |
270 | | - // eslint-disable-next-line no-await-in-loop |
271 | | - const result = await this.odsClient.GET('/sandboxes/{sandboxId}', { |
272 | | - params: { |
273 | | - path: {sandboxId}, |
274 | | - }, |
275 | | - }); |
276 | | - |
277 | | - if (!result.data?.data) { |
278 | | - this.error( |
279 | | - t('commands.ods.create.pollError', 'Failed to fetch sandbox status: {{message}}', { |
280 | | - message: result.response?.statusText || 'Unknown error', |
281 | | - }), |
282 | | - ); |
283 | | - } |
284 | | - |
285 | | - const sandbox = result.data.data; |
286 | | - const currentState = sandbox.state as SandboxState; |
287 | | - |
288 | | - // Log current state on each poll |
289 | | - const elapsed = Math.round((Date.now() - startTime) / 1000); |
290 | | - const state = currentState || 'unknown'; |
291 | | - this.logger.info({sandboxId, elapsed, state}, `[${elapsed}s] State: ${state}`); |
292 | | - |
293 | | - // Check for terminal states |
294 | | - if (currentState && TERMINAL_STATES.has(currentState)) { |
295 | | - switch (currentState) { |
296 | | - case 'deleted': { |
297 | | - this.error(t('commands.ods.create.deleted', 'Sandbox was deleted')); |
298 | | - break; |
299 | | - } |
300 | | - case 'failed': { |
301 | | - this.error(t('commands.ods.create.failed', 'Sandbox creation failed')); |
302 | | - break; |
303 | | - } |
304 | | - case 'started': { |
305 | | - this.log(''); |
306 | | - this.logger.info({sandboxId}, t('commands.ods.create.ready', 'Sandbox is now ready')); |
307 | | - break; |
308 | | - } |
309 | | - } |
310 | | - return sandbox; |
311 | | - } |
312 | | - |
313 | | - // Wait before next poll |
314 | | - // eslint-disable-next-line no-await-in-loop |
315 | | - await this.sleep(pollIntervalMs); |
316 | | - } |
317 | | - } |
318 | 290 | } |
0 commit comments