UNPKG

@aws-amplify/cli-internal

Version:
412 lines • 20.6 kB
"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