Skip to content

Commit 5f5d7b4

Browse files
authored
Merge pull request #306 from salesforcecli/bm/W-10774438
New command org:clone
2 parents 503be42 + 2106306 commit 5f5d7b4

File tree

10 files changed

+528
-154
lines changed

10 files changed

+528
-154
lines changed

command-snapshot.json

Lines changed: 71 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,72 @@
11
[
2-
{
3-
"command": "force:org:beta:create",
4-
"plugin": "@salesforce/plugin-org",
5-
"flags": [
6-
"apiversion",
7-
"clientid",
8-
"definitionfile",
9-
"durationdays",
10-
"json",
11-
"loglevel",
12-
"noancestors",
13-
"nonamespace",
14-
"retry",
15-
"setalias",
16-
"setdefaultusername",
17-
"targetdevhubusername",
18-
"targetusername",
19-
"type",
20-
"wait"
21-
]
22-
},
23-
{
24-
"command": "force:org:delete",
25-
"plugin": "@salesforce/plugin-org",
26-
"flags": [
27-
"apiversion",
28-
"json",
29-
"loglevel",
30-
"noprompt",
31-
"targetdevhubusername",
32-
"targetusername"
33-
]
34-
},
35-
{
36-
"command": "force:org:display",
37-
"plugin": "@salesforce/plugin-org",
38-
"flags": [
39-
"apiversion",
40-
"json",
41-
"loglevel",
42-
"targetusername",
43-
"verbose"
44-
]
45-
},
46-
{
47-
"command": "force:org:list",
48-
"plugin": "@salesforce/plugin-org",
49-
"flags": [
50-
"all",
51-
"clean",
52-
"json",
53-
"loglevel",
54-
"noprompt",
55-
"skipconnectionstatus",
56-
"verbose"
57-
]
58-
},
59-
{
60-
"command": "force:org:open",
61-
"plugin": "@salesforce/plugin-org",
62-
"flags": [
63-
"apiversion",
64-
"browser",
65-
"json",
66-
"loglevel",
67-
"path",
68-
"targetusername",
69-
"urlonly"
70-
]
71-
},
72-
{
73-
"command": "force:org:status",
74-
"plugin": "@salesforce/plugin-org",
75-
"flags": [
76-
"apiversion",
77-
"json",
78-
"loglevel",
79-
"sandboxname",
80-
"setalias",
81-
"setdefaultusername",
82-
"targetusername",
83-
"wait"
84-
]
85-
}
86-
]
2+
{
3+
"command": "force:org:beta:create",
4+
"plugin": "@salesforce/plugin-org",
5+
"flags": [
6+
"apiversion",
7+
"clientid",
8+
"definitionfile",
9+
"durationdays",
10+
"json",
11+
"loglevel",
12+
"noancestors",
13+
"nonamespace",
14+
"retry",
15+
"setalias",
16+
"setdefaultusername",
17+
"targetdevhubusername",
18+
"targetusername",
19+
"type",
20+
"wait"
21+
]
22+
},
23+
{
24+
"command": "force:org:delete",
25+
"plugin": "@salesforce/plugin-org",
26+
"flags": ["apiversion", "json", "loglevel", "noprompt", "targetdevhubusername", "targetusername"]
27+
},
28+
{
29+
"command": "force:org:display",
30+
"plugin": "@salesforce/plugin-org",
31+
"flags": ["apiversion", "json", "loglevel", "targetusername", "verbose"]
32+
},
33+
{
34+
"command": "force:org:list",
35+
"plugin": "@salesforce/plugin-org",
36+
"flags": ["all", "clean", "json", "loglevel", "noprompt", "skipconnectionstatus", "verbose"]
37+
},
38+
{
39+
"command": "force:org:open",
40+
"plugin": "@salesforce/plugin-org",
41+
"flags": ["apiversion", "browser", "json", "loglevel", "path", "targetusername", "urlonly"]
42+
},
43+
{
44+
"command": "force:org:status",
45+
"plugin": "@salesforce/plugin-org",
46+
"flags": [
47+
"apiversion",
48+
"json",
49+
"loglevel",
50+
"sandboxname",
51+
"setalias",
52+
"setdefaultusername",
53+
"targetusername",
54+
"wait"
55+
]
56+
},
57+
{
58+
"command": "force:org:clone",
59+
"plugin": "@salesforce/plugin-org",
60+
"flags": [
61+
"apiversion",
62+
"json",
63+
"loglevel",
64+
"type",
65+
"definitionfile",
66+
"setalias",
67+
"setdefaultusername",
68+
"targetusername",
69+
"wait"
70+
]
71+
}
72+
]

messages/clone.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"success": "The sandbox org cloning process %s is in progress. Run \"sfdx force:org:status -n %s\" to check for status. If the org is ready, checking the status also logs the requesting user in to the sandbox org and authorizes the org for use with Salesforce CLI.",
3+
"description": "clone a sandbox org\nThere are two ways to clone a sandbox: either specify a sandbox definition file or provide key=value pairs at the command line. Key-value pairs at the command-line override their equivalent sandbox definition file values. In either case, you must specify both the \"SandboxName\" and \"SourceSandboxName\" options. \n\nSet the --targetusername (-u) parameter to a production org with sandbox licenses. The --type (-t) parameter is required and must be set to \"sandbox\".",
4+
"examples": [
5+
"$ sfdx force:org:clone -t sandbox -f config/dev-sandbox-def.json -u prodOrg -a MyDevSandbox",
6+
"$ sfdx force:org:clone -t sandbox SandboxName=DevSbx1 SourceSandboxName=Sbx2Clone -u prodOrg -a MyDevSandbox"
7+
],
8+
"flags": {
9+
"type": "type of org to create",
10+
"wait": "number of minutes to wait while polling for status",
11+
"setdefaultusername": "set the cloned org as your default",
12+
"setalias": "alias for the cloned org",
13+
"definitionfile": "path to the sandbox definition file"
14+
},
15+
"commandSuccess": "The sandbox org cloning process %s is in progress. Run \"sfdx force:org:status -n %s\" to check for status. If the org is ready, checking the status also logs the requesting user in to the sandbox org and authorizes the org for use with Salesforce CLI.",
16+
"missingLicenseType": "The sandbox license type is required, but you didn't provide a value. Specify the license type in the sandbox definition file with the \"licenseType\" option, or specify the option as a name-value pair at the command-line. See https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_sandbox_definition.htm for more information.",
17+
"commandOrganizationTypeNotSupport": "The only supported org type is: %s",
18+
"commandOrganizationTypeNotSupportAction": "force:org:clone -t %s",
19+
"missingSourceSandboxName": "Specify a value for %s in a definition file or on the command line.",
20+
"missingSourceSandboxNameAction": "To indicate which sandbox org you want to clone, specify %s in a definition file or as a command line argument."
21+
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@
5050
"typescript": "^4.4.4"
5151
},
5252
"resolutions": {
53-
"@salesforce/core": "^2.36.3"
53+
"@salesforce/core": "^2.37.1"
5454
},
5555
"config": {
5656
"commitizen": {
5757
"path": "cz-conventional-changelog"
5858
}
5959
},
6060
"engines": {
61-
"node": ">=12.0.0"
61+
"node": ">=14.0.0"
6262
},
6363
"files": [
6464
"/lib",

src/commands/force/org/clone.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright (c) 2021, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { EOL } from 'os';
9+
import * as fs from 'fs';
10+
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
11+
import {
12+
SfdxError,
13+
SfdxErrorConfig,
14+
Config,
15+
Lifecycle,
16+
Messages,
17+
OrgTypes,
18+
Aliases,
19+
SandboxEvents,
20+
SandboxRequest,
21+
StatusEvent,
22+
ResultEvent,
23+
SandboxProcessObject,
24+
} from '@salesforce/core';
25+
import { Duration, upperFirst } from '@salesforce/kit';
26+
import { SandboxReporter } from '../../../shared/sandboxReporter';
27+
28+
Messages.importMessagesDirectory(__dirname);
29+
const messages = Messages.loadMessages('@salesforce/plugin-org', 'clone');
30+
31+
export class OrgCloneCommand extends SfdxCommand {
32+
public static readonly examples = messages.getMessage('examples').split(EOL);
33+
public static readonly description = messages.getMessage('description');
34+
public static readonly requiresProject = false;
35+
public static readonly requiresUsername = true;
36+
public static readonly varargs = true;
37+
public static readonly SANDBOXDEF_SRC_SANDBOXNAME = 'SourceSandboxName';
38+
39+
public static readonly flagsConfig: FlagsConfig = {
40+
type: flags.enum({
41+
char: 't',
42+
description: messages.getMessage('flags.type'),
43+
required: true,
44+
options: ['sandbox'],
45+
}),
46+
definitionfile: flags.filepath({
47+
char: 'f',
48+
description: messages.getMessage('flags.definitionfile'),
49+
}),
50+
setdefaultusername: flags.boolean({
51+
char: 's',
52+
description: messages.getMessage('flags.setdefaultusername'),
53+
}),
54+
setalias: flags.string({
55+
char: 'a',
56+
description: messages.getMessage('flags.setalias'),
57+
}),
58+
wait: flags.minutes({
59+
char: 'w',
60+
description: messages.getMessage('flags.wait'),
61+
min: Duration.minutes(2),
62+
default: Duration.minutes(6),
63+
}),
64+
};
65+
66+
public async run(): Promise<unknown> {
67+
const lifecycle = Lifecycle.getInstance();
68+
if (this.flags.type === OrgTypes.Sandbox) {
69+
// eslint-disable-next-line @typescript-eslint/require-await
70+
lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: SandboxProcessObject) => {
71+
// Keep all console output in the command
72+
this.ux.log(messages.getMessage('commandSuccess', [results.Id, results.SandboxName]));
73+
});
74+
75+
// eslint-disable-next-line @typescript-eslint/require-await
76+
lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => {
77+
this.ux.log(SandboxReporter.sandboxProgress(results));
78+
});
79+
80+
lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => {
81+
const { sandboxReadyForUse, data } = SandboxReporter.logSandboxProcessResult(results);
82+
this.ux.log(sandboxReadyForUse);
83+
this.ux.styledHeader('Sandbox Org Cloning Status');
84+
this.ux.table(data, {
85+
columns: [
86+
{ key: 'key', label: 'Name' },
87+
{ key: 'value', label: 'Value' },
88+
],
89+
});
90+
91+
if (results?.sandboxRes?.authUserName) {
92+
if (this.flags.setalias) {
93+
const alias = await Aliases.create({});
94+
const result = alias.set(this.flags.setalias, results.sandboxRes.authUserName);
95+
this.logger.debug('Set Alias: %s result: %s', this.flags.setalias, result);
96+
}
97+
if (this.flags.setdefaultusername) {
98+
const globalConfig: Config = this.configAggregator.getGlobalConfig();
99+
globalConfig.set(Config.DEFAULT_USERNAME, results.sandboxRes.authUserName);
100+
const result = await globalConfig.write();
101+
this.logger.debug('Set defaultUsername: %s result: %s', this.flags.setdefaultusername, result);
102+
}
103+
}
104+
});
105+
106+
const { sandboxReq, srcSandboxName } = this.createSandboxRequest();
107+
108+
this.logger.debug('Calling clone with SandboxRequest: %s and SandboxName: %s ', sandboxReq, srcSandboxName);
109+
const wait = this.flags.wait as Duration;
110+
return this.org.cloneSandbox(sandboxReq, srcSandboxName, { wait });
111+
} else {
112+
throw SfdxError.create(
113+
new SfdxErrorConfig('@salesforce/plugin-org', 'clone', 'commandOrganizationTypeNotSupport', [
114+
OrgTypes.Sandbox,
115+
]).addAction('commandOrganizationTypeNotSupportAction', [OrgTypes.Sandbox])
116+
);
117+
}
118+
}
119+
120+
private createSandboxRequest(): { sandboxReq: SandboxRequest; srcSandboxName: string } {
121+
this.logger.debug('Clone started with args %s ', this.flags);
122+
this.logger.debug('Clone Varargs: %s ', this.varargs);
123+
let sandboxDefFileContents = this.readJsonDefFile();
124+
let capitalizedVarArgs = {};
125+
126+
if (sandboxDefFileContents) {
127+
sandboxDefFileContents = this.lowerToUpper(sandboxDefFileContents);
128+
}
129+
if (this.varargs) {
130+
capitalizedVarArgs = this.lowerToUpper(this.varargs);
131+
}
132+
133+
// varargs override file input
134+
const sandboxReq: SandboxRequest = { SandboxName: undefined, ...sandboxDefFileContents, ...capitalizedVarArgs };
135+
136+
this.logger.debug('SandboxRequest after merging DefFile and Varargs: %s ', sandboxReq);
137+
138+
// try to find the source sandbox name either from the definition file or the commandline arg
139+
// NOTE the name and the case "SourceSandboxName" must match exactly
140+
const srcSandboxName = sandboxReq[OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME] as string;
141+
if (srcSandboxName) {
142+
// we have to delete this property from the sandboxRequest object,
143+
// because sandboxRequest object represent the POST request to create SandboxInfo bpo,
144+
// sandboxInfo does not have a column named SourceSandboxName, this field will be converted to sourceId in the clone call below
145+
delete sandboxReq[OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME];
146+
} else {
147+
// error - we need SourceSandboxName to know which sandbox to clone from
148+
throw SfdxError.create(
149+
new SfdxErrorConfig('@salesforce/plugin-org', 'clone', 'missingSourceSandboxName', [
150+
OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME,
151+
]).addAction('missingSourceSandboxNameAction', [OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME])
152+
);
153+
}
154+
return { sandboxReq, srcSandboxName };
155+
}
156+
157+
private lowerToUpper(object: Record<string, unknown>): Record<string, unknown> {
158+
// the API has keys defined in capital camel case, while the definition schema has them as lower camel case
159+
// we need to convert lower camel case to upper before merging options so they will override properly
160+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [upperFirst(key), value]));
161+
}
162+
163+
private readJsonDefFile(): Record<string, unknown> {
164+
// the -f option
165+
if (this.flags.definitionfile) {
166+
this.logger.debug('Reading JSON DefFile %s ', this.flags.definitionfile);
167+
return JSON.parse(fs.readFileSync(this.flags.definitionfile, 'utf-8')) as Record<string, unknown>;
168+
}
169+
}
170+
}

src/commands/force/org/list.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { EOL } from 'os';
99
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
1010
import { AuthInfo, ConfigAggregator, ConfigInfo, Connection, Org, SfdxError, Messages } from '@salesforce/core';
1111
import { sortBy } from '@salesforce/kit';
12-
import { Table } from 'cli-ux/lib';
1312
import { OrgListUtil, identifyActiveOrgByStatus } from '../../../shared/orgListUtil';
1413
import { getStyledObject } from '../../../shared/orgHighlighter';
1514
import { ExtendedAuthFields } from '../../../shared/orgTypes';
@@ -160,7 +159,7 @@ export class OrgListCommand extends SfdxCommand {
160159
}
161160
}
162161

163-
private getScratchOrgColumnData(): Array<Partial<Table.TableColumn>> {
162+
private getScratchOrgColumnData(): unknown[] {
164163
// default columns for the scratch org list
165164
let scratchOrgColumns = [
166165
{ key: 'defaultMarker', label: '' },

0 commit comments

Comments
 (0)