Skip to content

Commit 1fa1f0b

Browse files
committed
@W-21319709 adding cloning support
1 parent 362c935 commit 1fa1f0b

File tree

9 files changed

+2400
-1
lines changed

9 files changed

+2400
-1
lines changed

docs/cli/sandbox.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,209 @@ b2c sandbox alias delete zzzv-123 alias-uuid-here --json
647647

648648
---
649649

650+
## Sandbox Cloning
651+
652+
Sandbox cloning commands let you create copies of existing sandboxes with their data and configuration. A clone creates a new sandbox instance with the same data as the source sandbox, allowing you to test changes or create development environments without affecting the original.
653+
654+
Clone commands are available both under the `sandbox` topic and the legacy `ods` aliases:
655+
656+
- `b2c sandbox clone list` (`b2c ods clone:list`)
657+
- `b2c sandbox clone create` (`b2c ods clone:create`)
658+
- `b2c sandbox clone get` (`b2c ods clone:get`)
659+
660+
### Clone ID Format
661+
662+
Clone IDs follow a specific pattern: `realm-instance-timestamp`
663+
664+
- Example: `aaaa-002-1642780893121`
665+
- Pattern: 4-letter realm code, followed by 3-digit instance number, followed by 13-digit timestamp
666+
667+
### b2c sandbox clone list
668+
669+
List all clones for a specific sandbox.
670+
671+
#### Usage
672+
673+
```bash
674+
b2c sandbox clone list <SANDBOXID>
675+
```
676+
677+
#### Arguments
678+
679+
| Argument | Description | Required |
680+
|----------|-------------|----------|
681+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
682+
683+
#### Flags
684+
685+
| Flag | Description |
686+
|------|-------------|
687+
| `--from` | Filter clones created on or after this date (ISO 8601 date format, e.g., `2024-01-01`) |
688+
| `--to` | Filter clones created on or before this date (ISO 8601 date format, e.g., `2024-12-31`) |
689+
| `--status` | Filter clones by status (`Pending`, `InProgress`, `Failed`, `Completed`) |
690+
| `--columns`, `-c` | Columns to display (comma-separated) |
691+
| `--extended`, `-x` | Show all columns |
692+
693+
#### Available Columns
694+
695+
`cloneId`, `status`, `targetInstance`, `targetProfile`, `progressPercentage`, `createdAt`, `createdBy`, `lastUpdated`, `elapsedTimeInSec`, `sourceInstance`, `realm`
696+
697+
**Default columns:** `cloneId`, `status`, `targetInstance`, `progressPercentage`, `createdAt`
698+
699+
#### Examples
700+
701+
```bash
702+
# List all clones for a sandbox
703+
b2c sandbox clone list zzzv-123
704+
705+
# Filter by status
706+
b2c sandbox clone list zzzv-123 --status Completed
707+
708+
# Filter by date range
709+
b2c sandbox clone list zzzv-123 --from 2024-01-01 --to 2024-12-31
710+
711+
# Show all columns
712+
b2c sandbox clone list zzzv-123 --extended
713+
714+
# Custom columns
715+
b2c sandbox clone list zzzv-123 --columns cloneId,status,progressPercentage
716+
717+
# Output as JSON
718+
b2c sandbox clone list zzzv-123 --json
719+
```
720+
721+
#### Output
722+
723+
```
724+
Clone ID Status Target Instance Progress % Created At
725+
────────────────────────────────────────────────────────────────────────────────
726+
aaaa-001-1642780893121 COMPLETED aaaa-001 100% 2/27/2025, 10:00:00 AM
727+
aaaa-002-1642780893122 IN_PROGRESS aaaa-002 75% 2/27/2025, 11:00:00 AM
728+
```
729+
730+
### b2c sandbox clone create
731+
732+
Create a new sandbox clone from an existing sandbox. This creates a complete copy of the source sandbox including all data, configuration, and custom code.
733+
734+
#### Usage
735+
736+
```bash
737+
b2c sandbox clone create <SANDBOXID>
738+
```
739+
740+
#### Arguments
741+
742+
| Argument | Description | Required |
743+
|----------|-------------|----------|
744+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) to clone from | Yes |
745+
746+
#### Flags
747+
748+
| Flag | Description | Default |
749+
|------|-------------|---------|
750+
| `--target-profile` | Resource profile for the cloned sandbox (`medium`, `large`, `xlarge`, `xxlarge`). If not specified, uses the source sandbox's profile. | Source sandbox profile |
751+
| `--ttl` | Time to live in hours (0 or negative = infinite, minimum 24 hours). Values between 1-23 are not allowed. | `24` |
752+
| `--emails` | Comma-separated list of notification email addresses | |
753+
754+
#### Examples
755+
756+
```bash
757+
# Create a clone with same profile as source sandbox
758+
b2c sandbox clone create zzzv-123
759+
760+
# Create a clone with custom TTL (still uses source profile)
761+
b2c sandbox clone create zzzv-123 --ttl 48
762+
763+
# Create a clone with different profile
764+
b2c sandbox clone create zzzv-123 --target-profile large
765+
766+
# Create a clone with large profile and extended TTL
767+
b2c sandbox clone create zzzv-123 --target-profile large --ttl 48
768+
769+
# Create a clone with notification emails
770+
b2c sandbox clone create zzzv-123 --target-profile medium --emails dev@example.com,qa@example.com
771+
772+
# Create a clone with infinite TTL
773+
b2c sandbox clone create zzzv-123 --ttl 0
774+
775+
# Output as JSON
776+
b2c sandbox clone create zzzv-123 --json
777+
```
778+
779+
#### Output
780+
781+
```
782+
✓ Sandbox clone creation started successfully
783+
Clone ID: aaaa-002-1642780893121
784+
785+
To check the clone status, run:
786+
b2c sandbox clone get zzzv-123 aaaa-002-1642780893121
787+
```
788+
789+
#### Notes
790+
791+
- Cloning can take significant time depending on sandbox size and data volume
792+
- If `--target-profile` is not specified, the clone will use the same resource profile as the source sandbox
793+
- The TTL must be 0 or negative (infinite), or 24 hours or greater. Values between 1-23 are rejected
794+
- Notification emails will receive updates about the clone progress
795+
- The clone will be created as a new sandbox instance in the same realm
796+
797+
### b2c sandbox clone get
798+
799+
Retrieve detailed information about a specific sandbox clone, including status, progress, and metadata.
800+
801+
#### Usage
802+
803+
```bash
804+
b2c sandbox clone get <SANDBOXID> <CLONEID>
805+
```
806+
807+
#### Arguments
808+
809+
| Argument | Description | Required |
810+
|----------|-------------|----------|
811+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
812+
| `CLONEID` | Clone ID (e.g., `aaaa-002-1642780893121`) | Yes |
813+
814+
#### Examples
815+
816+
```bash
817+
# Get clone details
818+
b2c sandbox clone get zzzv-123 aaaa-002-1642780893121
819+
820+
# Output as JSON
821+
b2c sandbox clone get zzzv-123 aaaa-002-1642780893121 --json
822+
```
823+
824+
#### Output
825+
826+
Displays essential clone information in a formatted table:
827+
828+
```
829+
Clone Details
830+
──────────────────────────────────────────────────
831+
Clone ID: aaaa-002-1642780893121
832+
Source Instance: aaaa-000
833+
Target Instance: aaaa-002
834+
Realm: aaaa
835+
Progress: 75%
836+
Created At: 2/27/2025, 10:00:00 AM
837+
Created By: user@example.com
838+
```
839+
840+
For detailed information including status, timing, filesystem usage, and other metadata, use the `--json` flag.
841+
842+
#### Clone Status Values
843+
844+
| Status | Description |
845+
|--------|-------------|
846+
| `PENDING` | Clone is queued and waiting to start |
847+
| `IN_PROGRESS` | Clone operation is currently running |
848+
| `COMPLETED` | Clone finished successfully |
849+
| `FAILED` | Clone operation failed |
850+
851+
---
852+
650853
## Realm-Level Commands
651854

652855
Realm commands operate at the **realm** level rather than on an individual sandbox. They are available as both `realm` topic commands and as `sandbox realm` subcommands:
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
import {Args, Flags, Errors} from '@oclif/core';
7+
import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
8+
import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
9+
import {t} from '../../../i18n/index.js';
10+
11+
/**
12+
* Command to create a sandbox clone.
13+
*/
14+
export default class CloneCreate extends OdsCommand<typeof CloneCreate> {
15+
static description = t(
16+
'commands.clone.create.description',
17+
'Create a new sandbox clone from an existing sandbox',
18+
);
19+
20+
static enableJsonFlag = true;
21+
22+
static examples = [
23+
'<%= config.bin %> <%= command.id %> <sandboxId>',
24+
'<%= config.bin %> <%= command.id %> <sandboxId> --target-profile large',
25+
'<%= config.bin %> <%= command.id %> <sandboxId> --ttl 48',
26+
'<%= config.bin %> <%= command.id %> <sandboxId> --target-profile large --ttl 48 --emails dev@example.com,qa@example.com',
27+
];
28+
29+
static args = {
30+
sandboxId: Args.string({
31+
description: 'Sandbox ID (UUID or friendly format like realm-instance) to clone from',
32+
required: true,
33+
}),
34+
};
35+
36+
static flags = {
37+
'target-profile': Flags.string({
38+
description: 'Resource profile for the cloned sandbox (defaults to source sandbox profile)',
39+
required: false,
40+
options: ['medium', 'large', 'xlarge', 'xxlarge'],
41+
}),
42+
emails: Flags.string({
43+
description: 'Comma-separated list of notification email addresses',
44+
required: false,
45+
multiple: true,
46+
}),
47+
ttl: Flags.integer({
48+
description:
49+
'Time to live in hours (0 or negative = infinite, minimum 24 hours). Values between 1-23 are not allowed.',
50+
required: false,
51+
default: 24,
52+
}),
53+
};
54+
55+
async run(): Promise<{cloneId?: string}> {
56+
const {sandboxId: rawSandboxId} = this.args;
57+
const {'target-profile': targetProfileFlag, emails, ttl} = this.flags;
58+
59+
// Validate TTL
60+
if (ttl > 0 && ttl < 24) {
61+
throw new Errors.CLIError(
62+
t(
63+
'commands.clone.create.invalidTTL',
64+
'TTL must be 0 or negative (infinite), or 24 hours or greater. Values between 1-23 are not allowed. Received: {{ttl}}',
65+
{ttl},
66+
),
67+
);
68+
}
69+
70+
// Resolve sandbox ID (handles both UUID and friendly format)
71+
const sandboxId = await this.resolveSandboxId(rawSandboxId);
72+
73+
// Determine target profile - fetch from source sandbox if not provided
74+
let targetProfile = targetProfileFlag;
75+
76+
if (!targetProfile) {
77+
this.log(t('commands.clone.create.fetchingSource', 'Fetching source sandbox profile...'));
78+
79+
const sourceResult = await this.odsClient.GET('/sandboxes/{sandboxId}', {
80+
params: {path: {sandboxId}},
81+
});
82+
83+
if (!sourceResult.data?.data?.resourceProfile) {
84+
this.error(
85+
t(
86+
'commands.clone.create.noSourceProfile',
87+
'Unable to determine source sandbox profile. Please specify --target-profile explicitly.',
88+
),
89+
);
90+
}
91+
92+
targetProfile = sourceResult.data.data.resourceProfile;
93+
94+
if (!this.jsonEnabled()) {
95+
this.log(
96+
t('commands.clone.create.usingSourceProfile', 'Using source sandbox profile: {{profile}}', {
97+
profile: targetProfile,
98+
}),
99+
);
100+
}
101+
}
102+
103+
// Validate that profile is one of the allowed values
104+
const validProfiles = ['medium', 'large', 'xlarge', 'xxlarge'];
105+
if (!validProfiles.includes(targetProfile)) {
106+
this.error(
107+
t(
108+
'commands.clone.create.invalidProfile',
109+
'Invalid target profile "{{profile}}". Must be one of: {{validProfiles}}',
110+
{profile: targetProfile, validProfiles: validProfiles.join(', ')},
111+
),
112+
);
113+
}
114+
115+
this.log(t('commands.clone.create.creating', 'Creating sandbox clone...'));
116+
117+
// Prepare request body
118+
const requestBody: {
119+
targetProfile?: 'medium' | 'large' | 'xlarge' | 'xxlarge';
120+
emails?: string[];
121+
ttl: number;
122+
} = {
123+
targetProfile: targetProfile as 'medium' | 'large' | 'xlarge' | 'xxlarge',
124+
ttl,
125+
};
126+
127+
if (emails && emails.length > 0) {
128+
requestBody.emails = emails.flatMap((email) => email.split(',').map((e) => e.trim()));
129+
}
130+
131+
const result = await this.odsClient.POST('/sandboxes/{sandboxId}/clones', {
132+
params: {
133+
path: {sandboxId},
134+
},
135+
body: requestBody,
136+
});
137+
138+
if (!result.data) {
139+
const message = getApiErrorMessage(result.error, result.response);
140+
this.error(
141+
t('commands.clone.create.error', 'Failed to create sandbox clone: {{message}}', {message}),
142+
);
143+
}
144+
145+
const cloneId = result.data.data?.cloneId;
146+
147+
if (this.jsonEnabled()) {
148+
return {cloneId};
149+
}
150+
151+
this.log(
152+
t(
153+
'commands.clone.create.success',
154+
'✓ Sandbox clone creation started successfully',
155+
),
156+
);
157+
this.log(t('commands.clone.create.cloneId', 'Clone ID: {{cloneId}}', {cloneId}));
158+
this.log(
159+
t(
160+
'commands.clone.create.checkStatus',
161+
'\nTo check the clone status, run:\n <%= config.bin %> ods clone get {{sandboxId}} {{cloneId}}',
162+
{sandboxId, cloneId},
163+
),
164+
);
165+
166+
return {cloneId};
167+
}
168+
}

0 commit comments

Comments
 (0)