Skip to content

Commit 48b2b85

Browse files
committed
feat: clone command
1 parent 90f7298 commit 48b2b85

File tree

2 files changed

+195
-0
lines changed

2 files changed

+195
-0
lines changed

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",
4+
"examples": [
5+
"$ sfdx force:org:clone -t sandbox MySandbox",
6+
"$ sfdx force:org:clone -f config/enterprise-scratch-def.json -s",
7+
"$ sfdx force:org:clone -f config/enterprise-scratch-def.json -a MyAlias"
8+
],
9+
"flags": {
10+
"type": "type of org to create",
11+
"wait": "number of minutes to wait while polling for status",
12+
"setdefaultusername": "set the created org as the default username",
13+
"setalias": "alias for the created org",
14+
"definitionfile": "path to an org definition file"
15+
},
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+
}

src/commands/force/org/clone.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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 } 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 SANDBOXDEF_SRC_SANDBOXNAME = 'SourceSandboxName';
37+
38+
public static readonly flagsConfig: FlagsConfig = {
39+
type: flags.enum({
40+
char: 't',
41+
description: messages.getMessage('type'),
42+
longDescription: messages.getMessage('type'),
43+
required: true,
44+
options: ['sandbox'],
45+
}),
46+
definitionfile: flags.filepath({
47+
char: 'f',
48+
description: messages.getMessage('definitionfile'),
49+
longDescription: messages.getMessage('definitionfile'),
50+
}),
51+
setdefaultusername: flags.boolean({
52+
char: 's',
53+
description: messages.getMessage('setdefaultusername'),
54+
longDescription: messages.getMessage('setdefaultusername'),
55+
}),
56+
setalias: flags.string({
57+
char: 'a',
58+
description: messages.getMessage('setalias'),
59+
longDescription: messages.getMessage('setalias'),
60+
}),
61+
wait: flags.minutes({
62+
char: 'w',
63+
description: messages.getMessage('flags.wait'),
64+
min: Duration.minutes(2),
65+
default: Duration.minutes(6),
66+
}),
67+
};
68+
69+
protected readonly lifecycleEventNames = ['postorgcreate'];
70+
71+
public async run(): Promise<unknown> {
72+
const lifecycle = Lifecycle.getInstance();
73+
if (this.flags.type === OrgTypes.Sandbox) {
74+
// eslint-disable-next-line @typescript-eslint/require-await
75+
lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: SandboxProcessObject) => {
76+
// Keep all console output in the command
77+
this.ux.log(messages.getMessage('commandSuccess', [results.Id, results.SandboxName]));
78+
});
79+
80+
// eslint-disable-next-line @typescript-eslint/require-await
81+
lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => {
82+
SandboxReporter.sandboxProgress(results);
83+
});
84+
85+
lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => {
86+
SandboxReporter.logSandboxProcessResult(results);
87+
if (results.sandboxRes && results.sandboxRes.authUserName) {
88+
if (this.flags.setalias) {
89+
const alias = await Aliases.create({});
90+
const result = alias.set(this.flags.setalias, results.sandboxRes.authUserName);
91+
this.logger.debug('Set Alias: %s result: %s', this.flags.setalias, result);
92+
}
93+
if (this.flags.setdefaultusername) {
94+
const globalConfig: Config = this.configAggregator.getGlobalConfig();
95+
globalConfig.set(Config.DEFAULT_USERNAME, results.sandboxRes.authUserName);
96+
const result = await globalConfig.write();
97+
this.logger.debug('Set defaultUsername: %s result: %s', this.flags.setdefaultusername, result);
98+
}
99+
}
100+
});
101+
102+
const { sandboxReq, srcSandboxName } = this.createSandboxRequest();
103+
104+
this.logger.debug('Calling clone with SandboxRequest: %s and SandboxName: %s ', sandboxReq, srcSandboxName);
105+
const wait = this.flags.wait as Duration;
106+
return this.org.cloneSandbox(sandboxReq, srcSandboxName, { wait });
107+
} else {
108+
throw SfdxError.create(
109+
new SfdxErrorConfig('@salesforce/plugin-org', 'clone', 'commandOrganizationTypeNotSupport', [
110+
OrgTypes.Sandbox,
111+
]).addAction('commandOrganizationTypeNotSupportAction', [OrgTypes.Sandbox])
112+
);
113+
}
114+
}
115+
116+
private createSandboxRequest(): { sandboxReq: SandboxRequest; srcSandboxName: string } {
117+
this.logger.debug('Clone started with args %s ', this.flags);
118+
this.logger.debug('Clone Varargs: %s ', this.varargs);
119+
let sandboxDefFileContents = this.readJsonDefFile();
120+
let capitalizedVarArgs = {};
121+
122+
if (sandboxDefFileContents) {
123+
sandboxDefFileContents = this.lowerToUpper(sandboxDefFileContents);
124+
}
125+
if (this.varargs) {
126+
capitalizedVarArgs = this.lowerToUpper(this.varargs);
127+
}
128+
129+
// varargs override file input
130+
const sandboxReq: SandboxRequest = { SandboxName: undefined, ...sandboxDefFileContents, ...capitalizedVarArgs };
131+
132+
this.logger.debug('SandboxRequest after merging DefFile and Varargs: %s ', sandboxReq);
133+
134+
// try to find the source sandbox name either from the definition file or the commandline arg
135+
// NOTE the name and the case "SourceSandboxName" must match exactly
136+
const srcSandboxName = sandboxReq[OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME] as string;
137+
if (srcSandboxName) {
138+
// we have to delete this property from the sandboxRequest object,
139+
// because sandboxRequest object represent the POST request to create SandboxInfo bpo,
140+
// sandboxInfo does not have a column named SourceSandboxName, this field will be converted to sourceId in the clone call below
141+
delete sandboxReq[OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME];
142+
} else {
143+
// error - we need SourceSandboxName to know which sandbox to clone from
144+
throw SfdxError.create(
145+
new SfdxErrorConfig('@salesforce/plugin-org', 'clone', 'missingSourceSandboxName', [
146+
OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME,
147+
]).addAction('missingSourceSandboxNameAction', [OrgCloneCommand.SANDBOXDEF_SRC_SANDBOXNAME])
148+
);
149+
}
150+
return { sandboxReq, srcSandboxName };
151+
}
152+
153+
private lowerToUpper(object: Record<string, unknown>): Record<string, unknown> {
154+
// the API has keys defined in capital camel case, while the definition schema has them as lower camel case
155+
// we need to convert lower camel case to upper before merging options so they will override properly
156+
Object.keys(object).map((key) => {
157+
const upperCase = key.charAt(0).toUpperCase();
158+
if (key.charAt(0) !== upperCase) {
159+
const capitalKey = upperCase + key.slice(1);
160+
object[capitalKey] = object[key];
161+
delete object[key];
162+
}
163+
});
164+
return object;
165+
}
166+
167+
private readJsonDefFile(): Record<string, unknown> {
168+
// the -f option
169+
if (this.flags.definitionfile) {
170+
this.logger.debug('Reading JSON DefFile %s ', this.flags.definitionfile);
171+
return JSON.parse(fs.readFileSync(this.flags.definitionfile, 'utf-8')) as Record<string, unknown>;
172+
}
173+
}
174+
}

0 commit comments

Comments
 (0)