Skip to content

Commit 404f1c3

Browse files
authored
fix(amplify-provider-awscloudformation): support multiprofile delete (#1353)
Amplify CLI used to configure credential at the AWS SDK root level. With multi environment support, while deleting envs the delete request run in parallel. Since there could only be on profile set at the SDK level, this caused deletion to fail when env used different profiles. Moved the credetianls per service instance to support deletion of multi env fix #978
1 parent 8f03a37 commit 404f1c3

File tree

14 files changed

+218
-136
lines changed

14 files changed

+218
-136
lines changed

packages/amplify-provider-awscloudformation/lib/configuration-manager.js

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,11 @@ function persistLocalEnvConfig(context) {
424424
}
425425

426426
function getCurrentConfig(context) {
427+
const { envName } = context.amplify.getEnvInfo();
428+
return getConfigForEnv(context, envName);
429+
}
430+
431+
function getConfigForEnv(context, envName) {
427432
const projectConfigInfo = {
428433
configLevel: 'general',
429434
config: {},
@@ -433,7 +438,6 @@ function getCurrentConfig(context) {
433438

434439
if (fs.existsSync(configInfoFilePath)) {
435440
try {
436-
const { envName } = context.amplify.getEnvInfo();
437441
const configInfo = context.amplify.readJsonFile(configInfoFilePath, 'utf8')[envName];
438442

439443
if (configInfo && configInfo.configLevel !== 'general') {
@@ -458,7 +462,6 @@ function getCurrentConfig(context) {
458462
}
459463
return projectConfigInfo;
460464
}
461-
462465
function updateProjectConfig(context) {
463466
removeProjectConfig(context);
464467
persistLocalEnvConfig(context);
@@ -485,19 +488,33 @@ function removeProjectConfig(context) {
485488
}
486489
}
487490

488-
async function loadConfiguration(context, awsClient) {
489-
const projectConfigInfo = getCurrentConfig(context);
491+
async function loadConfiguration(context) {
492+
const { envName } = context.amplify.getEnvInfo();
493+
const config = await loadConfigurationForEnv(context, envName);
494+
return config;
495+
}
496+
function loadConfigFromPath(context, profilePath) {
497+
if (fs.existsSync(profilePath)) {
498+
const config = context.amplify.readJsonFile(profilePath);
499+
if (config.accessKeyId && config.secretAccessKey && config.region) {
500+
return config;
501+
}
502+
}
503+
throw Error(`Invalid config ${profilePath}`);
504+
}
505+
506+
async function loadConfigurationForEnv(context, env) {
507+
const projectConfigInfo = getConfigForEnv(context, env);
490508
if (projectConfigInfo.configLevel === 'project') {
491509
const { config } = projectConfigInfo;
510+
let awsConfig;
492511
if (config.useProfile) {
493-
const awsConfig = await systemConfigManager.getProfiledAwsConfig(context, config.profileName);
494-
awsClient.config.update(awsConfig);
512+
awsConfig = await systemConfigManager.getProfiledAwsConfig(context, config.profileName);
495513
} else {
496-
awsClient.config.loadFromPath(config.awsConfigFilePath);
514+
awsConfig = loadConfigFromPath(context, config.awsConfigFilePath);
497515
}
516+
return awsConfig;
498517
}
499-
500-
return awsClient;
501518
}
502519

503520
async function resetCache(context) {
@@ -617,4 +634,5 @@ module.exports = {
617634
loadConfiguration,
618635
resetCache,
619636
resolveRegion,
637+
loadConfigurationForEnv,
620638
};

packages/amplify-provider-awscloudformation/lib/delete-env.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
const Cloudformation = require('../src/aws-utils/aws-cfn');
2-
2+
const { loadConfigurationForEnv } = require('./configuration-manager');
33

44
async function run(context, envName) {
5-
return new Cloudformation(context)
6-
.then(cfnItem => cfnItem.deleteResourceStack(envName));
5+
const credentials = await loadConfigurationForEnv(context, envName);
6+
const cfn = await new Cloudformation(context, null, credentials);
7+
await cfn.deleteResourceStack(envName);
78
}
89

910
module.exports = {

packages/amplify-provider-awscloudformation/lib/initializer.js

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const systemConfigManager = require('./system-config-manager');
1212
async function run(context) {
1313
await configurationManager.init(context);
1414
if (!context.exeInfo || (context.exeInfo.isNewEnv)) {
15-
const awscfn = await getConfiguredAwsCfnClient(context);
1615
const initTemplateFilePath = path.join(__dirname, 'rootStackTemplate.json');
1716
const timeStamp = `${moment().format('YYYYMMDDHHmmss')}`;
1817
const { envName = '' } = context.exeInfo.localEnvInfo;
@@ -42,7 +41,8 @@ async function run(context) {
4241

4342
const spinner = ora();
4443
spinner.start('Initializing project in the cloud...');
45-
return new Cloudformation(context, awscfn, 'init')
44+
const awsConfig = await getConfiguredAwsCfnClient(context);
45+
return new Cloudformation(context, 'init', awsConfig)
4646
.then(cfnItem => cfnItem.createResourceStack(params))
4747
.then((waitData) => {
4848
processStackCreationData(context, waitData);
@@ -58,21 +58,18 @@ async function run(context) {
5858

5959
async function getConfiguredAwsCfnClient(context) {
6060
const { awsConfigInfo } = context.exeInfo;
61-
process.env.AWS_SDK_LOAD_CONFIG = true;
62-
const aws = require('aws-sdk');
63-
let awsconfig;
61+
let awsConfig;
6462
if (awsConfigInfo.config.useProfile) {
65-
awsconfig =
63+
awsConfig =
6664
await systemConfigManager.getProfiledAwsConfig(context, awsConfigInfo.config.profileName);
6765
} else {
68-
awsconfig = {
66+
awsConfig = {
6967
accessKeyId: awsConfigInfo.config.accessKeyId,
7068
secretAccessKey: awsConfigInfo.config.secretAccessKey,
7169
region: awsConfigInfo.config.region,
7270
};
7371
}
74-
aws.config.update(awsconfig);
75-
return aws;
72+
return awsConfig;
7673
}
7774

7875
function processStackCreationData(context, stackDescriptiondata) {

packages/amplify-provider-awscloudformation/lib/system-config-manager.js

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function setProfile(awsConfig, profileName) {
4747
}
4848
});
4949
if (!isConfigSet) {
50-
const keyName = (profileName === 'default') ? 'default' : `profile ${profileName}`;
50+
const keyName = profileName === 'default' ? 'default' : `profile ${profileName}`;
5151
config[keyName] = {
5252
region: awsConfig.region,
5353
};
@@ -62,8 +62,7 @@ async function getProfiledAwsConfig(context, profileName, isRoleSourceProfile) {
6262
const profileConfig = getProfileConfig(profileName);
6363
if (profileConfig) {
6464
if (!isRoleSourceProfile && profileConfig.role_arn) {
65-
const roleCredentials =
66-
await getRoleCredentials(context, profileName, profileConfig);
65+
const roleCredentials = await getRoleCredentials(context, profileName, profileConfig);
6766
delete profileConfig.role_arn;
6867
delete profileConfig.source_profile;
6968
awsConfig = {
@@ -89,11 +88,9 @@ async function getRoleCredentials(context, profileName, profileConfig) {
8988
let roleCredentials = getCachedRoleCredentials(context, profileConfig.role_arn, roleSessionName);
9089

9190
if (!roleCredentials) {
92-
if (profileConfig.source_profile) {
93-
const sourceProfileAwsConfig =
94-
await getProfiledAwsConfig(context, profileConfig.source_profile, true);
95-
aws.config.update(sourceProfileAwsConfig);
96-
}
91+
const sourceProfileAwsConfig = profileConfig.source_profile
92+
? await getProfiledAwsConfig(context, profileConfig.source_profile, true)
93+
: {};
9794
let mfaTokenCode;
9895
if (profileConfig.mfa_serial) {
9996
context.print.info(`Profile ${profileName} is configured to assume role`);
@@ -103,15 +100,17 @@ async function getRoleCredentials(context, profileName, profileConfig) {
103100
mfaTokenCode = await getMfaTokenCode();
104101
}
105102

106-
const sts = new aws.STS();
107-
const roleData = await sts.assumeRole({
108-
RoleArn: profileConfig.role_arn,
109-
RoleSessionName: roleSessionName,
110-
DurationSeconds: profileConfig.duration_seconds,
111-
ExternalId: profileConfig.external_id,
112-
SerialNumber: profileConfig.mfa_serial,
113-
TokenCode: mfaTokenCode,
114-
}).promise();
103+
const sts = new aws.STS(sourceProfileAwsConfig);
104+
const roleData = await sts
105+
.assumeRole({
106+
RoleArn: profileConfig.role_arn,
107+
RoleSessionName: roleSessionName,
108+
DurationSeconds: profileConfig.duration_seconds,
109+
ExternalId: profileConfig.external_id,
110+
SerialNumber: profileConfig.mfa_serial,
111+
TokenCode: mfaTokenCode,
112+
})
113+
.promise();
115114

116115
roleCredentials = {
117116
accessKeyId: roleData.Credentials.AccessKeyId,
@@ -132,7 +131,7 @@ async function getMfaTokenCode() {
132131
name: 'tokenCode',
133132
message: 'Enter the MFA token code:',
134133
validate: (value) => {
135-
let isValid = (value.length === 6);
134+
let isValid = value.length === 6;
136135
if (!isValid) {
137136
return 'Must have length equal to 6';
138137
}
@@ -176,7 +175,8 @@ function validateCachedCredentials(roleCredentials) {
176175
let isValid = false;
177176

178177
if (roleCredentials) {
179-
isValid = !isCredentialsExpired(roleCredentials) &&
178+
isValid =
179+
!isCredentialsExpired(roleCredentials) &&
180180
roleCredentials.accessKeyId &&
181181
roleCredentials.secretAccessKey &&
182182
roleCredentials.sessionToken;
@@ -192,7 +192,7 @@ function isCredentialsExpired(roleCredentials) {
192192
const TOTAL_MILLISECONDS_IN_ONE_MINUTE = 1000 * 60;
193193
const now = new Date();
194194
const expirationDate = new Date(roleCredentials.expiration);
195-
isExpired = (expirationDate - now) < TOTAL_MILLISECONDS_IN_ONE_MINUTE;
195+
isExpired = expirationDate - now < TOTAL_MILLISECONDS_IN_ONE_MINUTE;
196196
}
197197

198198
return isExpired;
@@ -222,8 +222,10 @@ async function resetCache(context, profileName) {
222222
}
223223

224224
function getCacheFilePath(context) {
225-
const sharedConfigDirPath =
226-
path.join(context.amplify.pathManager.getHomeDotAmplifyDirPath(), constants.Label);
225+
const sharedConfigDirPath = path.join(
226+
context.amplify.pathManager.getHomeDotAmplifyDirPath(),
227+
constants.Label,
228+
);
227229
fs.ensureDirSync(sharedConfigDirPath);
228230
return path.join(sharedConfigDirPath, constants.CacheFileName);
229231
}

packages/amplify-provider-awscloudformation/src/aws-utils/aws-apigw.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
const aws = require('./aws.js');
2+
const configurationManager = require('../../lib/configuration-manager');
23

34
class APIGateway {
4-
constructor(context) {
5-
return aws.configureWithCreds(context).then((awsItem) => {
5+
constructor(context, options = {}) {
6+
return (async () => {
7+
let cred = {};
8+
try {
9+
cred = await configurationManager.loadConfiguration(context);
10+
} catch (e) {
11+
// nothing
12+
}
613
this.context = context;
7-
this.apigw = new awsItem.APIGateway();
14+
this.apigw = new aws.APIGateway({ ...cred, ...options });
815
return this;
9-
});
16+
})();
1017
}
1118
}
1219

packages/amplify-provider-awscloudformation/src/aws-utils/aws-appsync.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
const aws = require('./aws.js');
2+
const configurationManager = require('../../lib/configuration-manager');
23

34
class AppSync {
45
constructor(context, options = {}) {
5-
return aws.configureWithCreds(context).then((awsItem) => {
6+
return (async () => {
7+
let cred = {};
8+
try {
9+
cred = await configurationManager.loadConfiguration(context);
10+
} catch (e) {
11+
// could not load the creds
12+
}
13+
614
this.context = context;
7-
this.appSync = new awsItem.AppSync(options);
15+
this.appSync = new aws.AppSync({ ...cred, ...options });
816
return this;
9-
});
17+
})();
1018
}
1119
}
1220

packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const aws = require('./aws.js');
99
const S3 = require('./aws-s3');
1010
const providerName = require('../../lib/constants').ProviderName;
1111
const { formUserAgentParam } = require('./user-agent');
12+
const configurationManager = require('../../lib/configuration-manager');
1213

1314
const CFN_MAX_CONCURRENT_REQUEST = 5;
1415
const CFN_POLL_TIME = 5 * 1000; // 5 secs wait to check if new stacks are created by root stack
@@ -22,28 +23,31 @@ const CFN_SUCCESS_STATUS = [
2223

2324
const CNF_ERROR_STATUS = ['CREATE_FAILED', 'DELETE_FAILED', 'UPDATE_FAILED'];
2425
class CloudFormation {
25-
constructor(context, awsClientWithCreds, userAgentAction) {
26-
const initializeAwsClient = awsClientWithCreds
27-
? Promise.resolve(awsClientWithCreds)
28-
: aws.configureWithCreds(context);
29-
30-
let userAgentParam;
31-
if (userAgentAction) {
32-
userAgentParam = formUserAgentParam(context, userAgentAction);
33-
}
34-
35-
this.pollQueue = new BottleNeck({ minTime: 100, maxConcurrent: CFN_MAX_CONCURRENT_REQUEST });
36-
this.pollQueueStacks = [];
37-
this.stackEvents = [];
26+
constructor(context, userAgentAction, options = {}) {
27+
return (async () => {
28+
let userAgentParam;
29+
if (userAgentAction) {
30+
userAgentParam = formUserAgentParam(context, userAgentAction);
31+
}
3832

39-
return initializeAwsClient.then((awsItem) => {
33+
this.pollQueue = new BottleNeck({ minTime: 100, maxConcurrent: CFN_MAX_CONCURRENT_REQUEST });
34+
this.pollQueueStacks = [];
35+
this.stackEvents = [];
36+
let cred;
37+
try {
38+
cred = await configurationManager.loadConfiguration(context);
39+
} catch (e) {
40+
// no credential. New project
41+
}
42+
const userAgentOption = {};
4043
if (userAgentAction) {
41-
awsItem.config.update({ customUserAgent: userAgentParam });
44+
userAgentOption.customUserAgent = userAgentParam;
4245
}
43-
this.cfn = new awsItem.CloudFormation();
46+
47+
this.cfn = new aws.CloudFormation({ ...cred, ...options, ...userAgentOption });
4448
this.context = context;
4549
return this;
46-
});
50+
})();
4751
}
4852

4953
createResourceStack(cfnParentStackParams) {
@@ -112,7 +116,8 @@ class CloudFormation {
112116
.filter(stack => stack.ResourceType !== 'AWS::CloudFormation::Stack')
113117
.map((event) => {
114118
const err = [];
115-
const resourceName = event.PhysicalResourceId.replace(envRegExp, '') || event.LogicalResourceId;
119+
const resourceName =
120+
event.PhysicalResourceId.replace(envRegExp, '') || event.LogicalResourceId;
116121
const cfnURL = getCFNConsoleLink(event, this.cfn);
117122
err.push(`${chalk.bold('Resource Name:')} ${resourceName} (${event.ResourceType})`);
118123
err.push(`${chalk.bold('Event Type:')} ${getStatusToErrorMsg(event.ResourceStatus)}`);
@@ -129,7 +134,6 @@ class CloudFormation {
129134
this.pollForEvents = setInterval(() => this.addToPollQueue(stackName, 3), CFN_POLL_TIME);
130135
}
131136

132-
133137
pollStack(stackName) {
134138
return this.getStackEvents(stackName)
135139
.then((stackEvents) => {
@@ -465,7 +469,8 @@ function getStatusToErrorMsg(status) {
465469
}
466470

467471
function getCFNConsoleLink(event, cfn) {
468-
if (event.ResourceStatus === 'CREATE_FAILED') { // Stacks get deleted and don't have perm link
472+
if (event.ResourceStatus === 'CREATE_FAILED') {
473+
// Stacks get deleted and don't have perm link
469474
return null;
470475
}
471476
const arn = event.StackId;

packages/amplify-provider-awscloudformation/src/aws-utils/aws-cognito.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
const aws = require('./aws.js');
2+
const configurationManager = require('../../lib/configuration-manager');
23

34
class Cognito {
4-
constructor(context) {
5-
return aws.configureWithCreds(context)
6-
.then((awsItem) => {
7-
this.context = context;
8-
this.cognito = new awsItem.CognitoIdentityServiceProvider();
9-
return this;
10-
});
5+
constructor(context, options = {}) {
6+
return (async () => {
7+
let cred = {};
8+
try {
9+
cred = await configurationManager.loadConfiguration(context);
10+
} catch (e) {
11+
// could not load cred
12+
}
13+
this.context = context;
14+
this.cognito = new aws.CognitoIdentityServiceProvider({ ...cred, ...options });
15+
return this;
16+
})();
1117
}
1218
}
1319

0 commit comments

Comments
 (0)