@aws-amplify/cli-internal
Version:
Amplify CLI
412 lines • 20.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AmplifyMigrationLockStep = void 0;
const step_1 = require("./_common/step");
const plan_1 = require("./_common/plan");
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
const client_amplify_1 = require("@aws-sdk/client-amplify");
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
const utils_1 = require("./_common/utils");
const cfn_1 = require("./_common/cfn");
const resource_types_1 = require("./_common/resource-types");
const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core");
const cli_table3_1 = __importDefault(require("cli-table3"));
const GEN2_MIGRATION_ENVIRONMENT_NAME = 'GEN2_MIGRATION_ENVIRONMENT_NAME';
const DYNAMO_DELETION_PROTECTION_PROPERTY = 'DeletionProtectionEnabled';
const DYNAMO_RESOURCE_TYPE = 'AWS::DynamoDB::Table';
const LOCK_STATEMENT = {
Effect: 'Deny',
Action: 'Update:*',
Principal: '*',
Resource: '*',
};
const isLockStatement = (statement) => statement.Effect === LOCK_STATEMENT.Effect &&
statement.Action === LOCK_STATEMENT.Action &&
statement.Principal === LOCK_STATEMENT.Principal &&
statement.Resource === LOCK_STATEMENT.Resource;
const ALLOW_ALL_POLICY = {
Statement: [
{
Effect: 'Allow',
Action: 'Update:*',
Principal: '*',
Resource: '*',
},
],
};
class AmplifyMigrationLockStep extends step_1.AmplifyMigrationStep {
async forward() {
const operations = [];
operations.push({
describe: async () => [],
validate: () => ({ description: 'Environment Healthy', run: () => this.validateDeploymentStatus() }),
execute: async () => { },
});
operations.push({
describe: async () => [],
validate: () => ({ description: 'Drift', run: () => this.validateDrift() }),
execute: async () => { },
});
operations.push({
validate: () => undefined,
describe: async () => [`Add environment variable '${GEN2_MIGRATION_ENVIRONMENT_NAME}' (value: ${this.gen1App.envName})`],
execute: async () => {
var _a, _b;
const app = await this.gen1App.clients.amplify.send(new client_amplify_1.GetAppCommand({ appId: this.gen1App.appId }));
const environmentVariables = { ...((_b = (_a = app.app) === null || _a === void 0 ? void 0 : _a.environmentVariables) !== null && _b !== void 0 ? _b : {}), [GEN2_MIGRATION_ENVIRONMENT_NAME]: this.gen1App.envName };
await this.gen1App.clients.amplify.send(new client_amplify_1.UpdateAppCommand({ appId: this.gen1App.appId, environmentVariables }));
this.logger.info(`Added '${GEN2_MIGRATION_ENVIRONMENT_NAME}' environment variable (value: ${this.gen1App.envName})`);
},
});
operations.push({
validate: () => undefined,
describe: async () => {
return [`Add lock statement to stack policy on '${this.gen1App.rootStackName}': ${JSON.stringify(LOCK_STATEMENT)}`];
},
execute: async () => {
const existingPolicy = await this.fetchExistingStackPolicy();
const alreadyLocked = existingPolicy.Statement.some(isLockStatement);
if (alreadyLocked) {
this.logger.info(`Lock statement already exists in stack policy on '${this.gen1App.rootStackName}', skipping`);
return;
}
existingPolicy.Statement.push(LOCK_STATEMENT);
const mergedPolicy = JSON.stringify(existingPolicy);
await this.gen1App.clients.cloudFormation.send(new client_cloudformation_1.SetStackPolicyCommand({
StackName: this.gen1App.rootStackName,
StackPolicyBody: mergedPolicy,
}));
this.logger.info(`Successfully added lock statement to stack policy on '${this.gen1App.rootStackName}'`);
},
});
const nestedStacks = await this.listNestedStack(this.gen1App.rootStackName);
for (const resource of this.gen1App.discover()) {
this.logger.push(`${resource.category}/${resource.resourceName} (${resource.service})`);
switch (resource.key) {
case 'api:AppSync': {
const apiStackId = this.findNestedStack(nestedStacks, `${resource.category}${resource.resourceName}`);
const apiNestedStacks = await this.listNestedStack(apiStackId);
for (const tableName of await this.dynamoTableNames()) {
const modelName = tableName.split('-')[0];
this.logger.push(modelName);
const tableStackId = this.findNestedStack(apiNestedStacks, modelName);
operations.push(...(await this.retainResource(resource, tableStackId)));
this.logger.pop();
}
break;
}
case 'auth:Cognito':
case 'auth:Cognito-UserPool-Groups':
case 'storage:S3':
case 'storage:DynamoDB':
case 'custom:customCDK':
case 'analytics:Kinesis': {
const stackId = this.findNestedStack(nestedStacks, `${resource.category}${resource.resourceName}`);
operations.push(...(await this.retainResource(resource, stackId)));
break;
}
case 'api:API Gateway':
case 'geo:Map':
case 'geo:PlaceIndex':
case 'geo:GeofenceCollection':
case 'function:Lambda':
break;
case 'UNKNOWN':
break;
}
this.logger.pop();
}
return new plan_1.Plan({
operations,
logger: this.logger,
title: 'Execute',
implications: [
`You will not be able to run 'amplify push' on '${this.gen1App.appName}/${this.gen1App.envName}'`,
`You will not be able to migrate another environment until migration of '${this.gen1App.appName}/${this.gen1App.envName}' is complete or rolled back`,
],
});
}
async rollback() {
const operations = [];
operations.push({
validate: () => undefined,
describe: async () => [`Remove environment variable '${GEN2_MIGRATION_ENVIRONMENT_NAME}'`],
execute: async () => {
var _a, _b;
const app = await this.gen1App.clients.amplify.send(new client_amplify_1.GetAppCommand({ appId: this.gen1App.appId }));
const environmentVariables = (_b = (_a = app.app) === null || _a === void 0 ? void 0 : _a.environmentVariables) !== null && _b !== void 0 ? _b : {};
delete environmentVariables[GEN2_MIGRATION_ENVIRONMENT_NAME];
await this.gen1App.clients.amplify.send(new client_amplify_1.UpdateAppCommand({ appId: this.gen1App.appId, environmentVariables }));
this.logger.info(`Removed ${GEN2_MIGRATION_ENVIRONMENT_NAME} environment variable`);
},
});
operations.push({
validate: () => undefined,
describe: async () => {
return [`Remove lock statement from stack policy on '${this.gen1App.rootStackName}': ${JSON.stringify(LOCK_STATEMENT)}`];
},
execute: async () => {
const existingPolicy = await this.fetchExistingStackPolicy();
const index = existingPolicy.Statement.findIndex(isLockStatement);
if (index === -1) {
this.logger.info(`Lock statement not found in stack policy on '${this.gen1App.rootStackName}'`);
return;
}
existingPolicy.Statement.splice(index, 1);
const restoredPolicy = existingPolicy.Statement.length > 0 ? JSON.stringify(existingPolicy) : JSON.stringify(ALLOW_ALL_POLICY);
await this.gen1App.clients.cloudFormation.send(new client_cloudformation_1.SetStackPolicyCommand({
StackName: this.gen1App.rootStackName,
StackPolicyBody: restoredPolicy,
}));
this.logger.info(`Successfully removed lock statement from stack policy on '${this.gen1App.rootStackName}': ${JSON.stringify(LOCK_STATEMENT)}`);
},
});
const nestedStacks = await this.listNestedStack(this.gen1App.rootStackName);
for (const resource of this.gen1App.discover()) {
switch (resource.key) {
case 'auth:Cognito':
case 'auth:Cognito-UserPool-Groups': {
const stackId = this.findNestedStack(nestedStacks, `${resource.category}${resource.resourceName}`);
const template = this.gen1App.json(`auth/${resource.resourceName}/build/${resource.resourceName}-cloudformation-template.json`);
operations.push(await this.validateRefactorRollbackStackIntegrity(resource, template, stackId));
break;
}
case 'storage:S3': {
const stackId = this.findNestedStack(nestedStacks, `${resource.category}${resource.resourceName}`);
const template = this.gen1App.json(`storage/${resource.resourceName}/build/cloudformation-template.json`);
operations.push(await this.validateRefactorRollbackStackIntegrity(resource, template, stackId));
break;
}
case 'storage:DynamoDB': {
const stackId = this.findNestedStack(nestedStacks, `${resource.category}${resource.resourceName}`);
const template = this.gen1App.json(`storage/${resource.resourceName}/build/${resource.resourceName}-cloudformation-template.json`);
operations.push(await this.validateRefactorRollbackStackIntegrity(resource, template, stackId));
break;
}
case 'analytics:Kinesis': {
const stackId = this.findNestedStack(nestedStacks, `${resource.category}${resource.resourceName}`);
const template = this.gen1App.json(`analytics/${resource.resourceName}/kinesis-cloudformation-template.json`);
operations.push(await this.validateRefactorRollbackStackIntegrity(resource, template, stackId));
break;
}
case 'custom:customCDK': {
const stackId = this.findNestedStack(nestedStacks, `${resource.category}${resource.resourceName}`);
const template = this.gen1App.json(`custom/${resource.resourceName}/build/${resource.resourceName}-cloudformation-template.json`);
operations.push(await this.validateRefactorRollbackStackIntegrity(resource, template, stackId));
break;
}
case 'api:AppSync':
case 'api:API Gateway':
case 'geo:Map':
case 'geo:PlaceIndex':
case 'geo:GeofenceCollection':
case 'function:Lambda':
case 'UNKNOWN':
break;
}
}
return new plan_1.Plan({
operations,
logger: this.logger,
title: 'Rollback',
implications: [
`You will be able to run 'amplify push' on '${this.gen1App.appName}/${this.gen1App.envName}'`,
`You will be able to start migration on a different environment (lock on '${this.gen1App.appName}/${this.gen1App.envName}' will be released)`,
],
});
}
async validateDeploymentStatus() {
try {
await this.validations.validateDeploymentStatus();
return { valid: true };
}
catch (e) {
return { valid: false, report: e.message };
}
}
async validateDrift() {
try {
await this.validations.validateDrift();
return { valid: true };
}
catch (e) {
return { valid: false, report: e.message };
}
}
async findGraphQLApiId() {
const graphQL = this.gen1App.discover().find((r) => r.category === 'api' && r.service === 'AppSync');
if (!graphQL) {
return undefined;
}
return this.gen1App.resourceMetaOutput(graphQL, 'GraphQLAPIIdOutput');
}
async fetchGraphQLModelTables(graphQLApiId) {
var _a;
const tables = [];
for await (const page of (0, client_dynamodb_1.paginateListTables)({ client: this.gen1App.clients.dynamoDB }, {})) {
for (const tableName of (_a = page.TableNames) !== null && _a !== void 0 ? _a : []) {
if (tableName.includes(`-${graphQLApiId}-${this.gen1App.envName}`)) {
tables.push(tableName);
}
}
}
return tables;
}
async dynamoTableNames() {
if (!this._dynamoTableNames) {
const graphQLApiId = await this.findGraphQLApiId();
if (!graphQLApiId) {
this._dynamoTableNames = [];
}
else {
this._dynamoTableNames = await this.fetchGraphQLModelTables(graphQLApiId);
}
}
return this._dynamoTableNames;
}
async retainResource(appResource, stackId) {
var _a, _b, _c;
const operations = [];
const cfn = new cfn_1.Cfn(this.gen1App, this.logger);
const stackName = (0, utils_1.extractStackNameFromId)(stackId);
const template = await cfn.fetchTemplate(stackId);
for (const [logicalId, resource] of Object.entries(template.Resources)) {
if (this.gen1App.statefulResourceTypes.includes(resource.Type)) {
resource.DeletionPolicy = 'Retain';
resource.UpdateReplacePolicy = 'Retain';
}
if (resource_types_1.AUTH_HOSTED_UI_LOGICAL_IDS_TO_RETAIN.includes(logicalId)) {
resource.DeletionPolicy = 'Retain';
resource.UpdateReplacePolicy = 'Retain';
}
if (resource.Type === DYNAMO_RESOURCE_TYPE) {
resource.Properties[DYNAMO_DELETION_PROTECTION_PROPERTY] = true;
}
}
const describeResponse = await this.gen1App.clients.cloudFormation.send(new client_cloudformation_1.DescribeStacksCommand({ StackName: stackId }));
const parameters = ((_c = (_b = (_a = describeResponse.Stacks) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.Parameters) !== null && _c !== void 0 ? _c : []).map((p) => ({
ParameterKey: p.ParameterKey,
UsePreviousValue: true,
}));
this.logger.push(`${stackName} (Create ChangeSet)`);
const changeSet = await cfn.createChangeSet({
stackName: stackId,
templateBody: template,
parameters,
});
this.logger.pop();
if (!changeSet) {
return [];
}
const report = cfn.renderChangeSet(changeSet);
const valid = this.validateRetainChangeset(changeSet);
operations.push({
resource: appResource,
describe: async () => [
`Set Retain policies on stateful resources and/or enable DynamoDB deletion protection in '${stackName}'\n\n${report}\n`,
],
validate: () => ({
description: `Stack Unchanged: ${stackName}`,
run: async () => ({ valid, report }),
}),
execute: async () => {
await cfn.executeChangeSet({
changeSet: changeSet,
templateBody: template,
resource: appResource,
captureSnapshot: false,
});
},
});
return operations;
}
async validateRefactorRollbackStackIntegrity(resource, localTemplate, stackId) {
const stackName = (0, utils_1.extractStackNameFromId)(stackId);
return {
resource,
validate: () => ({
description: `Stack Integrity: ${stackName}`,
run: async () => {
const cfn = new cfn_1.Cfn(this.gen1App, this.logger);
const deployedTemplate = await cfn.fetchTemplate(stackId);
const missingResources = new cli_table3_1.default({
head: ['Logical ID', 'Type'],
style: { head: [] },
});
for (const logicalId of Object.keys(localTemplate.Resources)) {
const localResource = localTemplate.Resources[logicalId];
if (localResource.Condition) {
continue;
}
if (!deployedTemplate.Resources[logicalId]) {
missingResources.push([logicalId, localResource.Type]);
}
}
return {
valid: missingResources.length === 0,
report: `Following resources are missing. Did you forget to run 'amplify gen2-migration refactor --rollback'?\n\n${missingResources.toString()}`,
};
},
}),
describe: async () => [],
execute: async () => { },
};
}
validateRetainChangeset(changeSet) {
var _a, _b, _c, _d, _e, _f;
const changes = (_a = changeSet.Changes) !== null && _a !== void 0 ? _a : [];
if (changes.length === 0)
return false;
for (const change of changes) {
const rc = change.ResourceChange;
if (!rc || rc.Action !== 'Modify')
return false;
const details = (_b = rc.Details) !== null && _b !== void 0 ? _b : [];
if (details.length === 0)
return false;
for (const detail of details) {
const attr = (_c = detail.Target) === null || _c === void 0 ? void 0 : _c.Attribute;
const name = (_d = detail.Target) === null || _d === void 0 ? void 0 : _d.Name;
const after = (_e = detail.Target) === null || _e === void 0 ? void 0 : _e.AfterValue;
if ((attr === 'DeletionPolicy' || attr === 'UpdateReplacePolicy') && after === 'Retain') {
continue;
}
if (((_f = change.ResourceChange) === null || _f === void 0 ? void 0 : _f.ResourceType) === DYNAMO_RESOURCE_TYPE &&
attr === 'Properties' &&
name === DYNAMO_DELETION_PROTECTION_PROPERTY &&
after === 'true') {
continue;
}
return false;
}
}
return true;
}
async fetchExistingStackPolicy() {
const response = await this.gen1App.clients.cloudFormation.send(new client_cloudformation_1.GetStackPolicyCommand({
StackName: this.gen1App.rootStackName,
}));
if (response.StackPolicyBody) {
return JSON.parse(response.StackPolicyBody);
}
return { Statement: [] };
}
async listNestedStack(rootStack) {
return this.gen1App.aws.listNestedStacks(rootStack);
}
findNestedStack(nestedStacks, logicalIdPrefix) {
var _a;
const stackId = (_a = nestedStacks.find((s) => { var _a; return (_a = s.LogicalResourceId) === null || _a === void 0 ? void 0 : _a.startsWith(logicalIdPrefix); })) === null || _a === void 0 ? void 0 : _a.PhysicalResourceId;
if (!stackId) {
throw new amplify_cli_core_1.AmplifyError('NestedStackNotFoundError', {
message: `Unable to find nested stack logical id prefix: ${logicalIdPrefix}`,
});
}
return stackId;
}
}
exports.AmplifyMigrationLockStep = AmplifyMigrationLockStep;
//# sourceMappingURL=lock.js.map