@aws-amplify/cli-internal
Version:
Amplify CLI
211 lines • 10.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AmplifyMigrationRetainStep = void 0;
const plan_1 = require("./_common/plan");
const step_1 = require("./_common/step");
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
const cfn_1 = require("./_common/cfn");
const utils_1 = require("./_common/utils");
const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core");
const drift_formatter_1 = require("../drift/services/drift-formatter");
const chalk_1 = __importDefault(require("chalk"));
class AmplifyMigrationRetainStep extends step_1.AmplifyMigrationStep {
async forward() {
const stackIds = await this.walkStackHierarchy(this.gen1App.rootStackName);
this.logger.info(`Discovered ${stackIds.length} stacks below root`);
const stackToResource = await this.classifyStacks();
const operations = stackIds.map((stackId) => this.buildRetainOperation(stackId, stackToResource.get(stackId)));
return new plan_1.Plan({
operations,
logger: this.logger,
title: 'Execute',
implications: [
'Retain policies will be applied to every resource in every Gen1 stack below the root',
'The root stack is not touched (no changeset is executed on it)',
'When you later delete the root stack, all retained resources will survive as orphaned resources',
],
});
}
rollback() {
throw new amplify_cli_core_1.AmplifyFault('NotImplementedFault', {
message: 'Rollback is not supported for the retain step',
resolution: 'Retain only marks resources with DeletionPolicy: Retain. To undo, manually update the CloudFormation templates.',
});
}
async walkStackHierarchy(rootStackId) {
const result = [];
await this.walkChildren(rootStackId, result);
return result;
}
async walkChildren(stackId, result) {
var _a;
const pages = (0, client_cloudformation_1.paginateListStackResources)({ client: this.gen1App.clients.cloudFormation }, { StackName: stackId });
for await (const page of pages) {
for (const resource of (_a = page.StackResourceSummaries) !== null && _a !== void 0 ? _a : []) {
if (resource.ResourceType === 'AWS::CloudFormation::Stack' && resource.PhysicalResourceId) {
result.push(resource.PhysicalResourceId);
await this.walkChildren(resource.PhysicalResourceId, result);
}
}
}
}
async classifyStacks() {
const stackToResource = new Map();
const discovered = this.gen1App.discover();
if (discovered.length === 0)
return stackToResource;
const rootNestedStacks = await this.listNestedStack(this.gen1App.rootStackName);
for (const resource of discovered) {
switch (resource.key) {
case 'auth:Cognito':
case 'auth:Cognito-UserPool-Groups':
case 'storage:S3':
case 'storage:DynamoDB':
case 'analytics:Kinesis':
case 'function:Lambda':
case 'api:API Gateway':
case 'geo:Map':
case 'geo:PlaceIndex':
case 'custom:customCDK':
case 'geo:GeofenceCollection': {
const stackId = this.findNestedStack(rootNestedStacks, `${resource.category}${resource.resourceName}`);
stackToResource.set(stackId, resource);
break;
}
case 'api:AppSync': {
const apiStackId = this.findNestedStack(rootNestedStacks, `api${resource.resourceName}`);
stackToResource.set(apiStackId, resource);
const apiNestedStacks = await this.listNestedStack(apiStackId);
for (const child of apiNestedStacks) {
if (child.PhysicalResourceId) {
stackToResource.set(child.PhysicalResourceId, resource);
}
}
break;
}
case 'UNKNOWN':
break;
}
}
return stackToResource;
}
buildRetainOperation(stackId, resource) {
const cfn = new cfn_1.Cfn(this.gen1App, this.logger);
const stackName = (0, utils_1.extractStackNameFromId)(stackId);
return {
resource,
describe: async () => [`Set Retain policies on resources in '${stackName}'`],
validate: () => undefined,
execute: async () => {
var _a, _b;
const pushed = resource !== undefined;
if (resource) {
this.logger.push(`${resource.category}/${resource.resourceName} (${resource.service})`);
}
try {
const template = await cfn.fetchTemplate(stackId);
const targetEntries = Object.values(template.Resources).filter((r) => r.Type !== 'AWS::CloudFormation::Stack');
if (targetEntries.length === 0) {
this.logger.info(`${stackName} — no resources to retain`);
return;
}
const needsChange = targetEntries.some((r) => r.DeletionPolicy !== 'Retain' || r.UpdateReplacePolicy !== 'Retain');
if (!needsChange) {
this.logger.info(`${stackName} — no retain changes needed`);
return;
}
for (const r of targetEntries) {
r.DeletionPolicy = 'Retain';
r.UpdateReplacePolicy = 'Retain';
}
const stack = await cfn.describeStack(stackId);
const parameters = ((_a = stack.Parameters) !== null && _a !== void 0 ? _a : []).map((p) => ({
ParameterKey: p.ParameterKey,
UsePreviousValue: true,
}));
this.logger.push(`${stackName} (Create ChangeSet)`);
const changeset = await cfn.createChangeSet({ stackName: stackId, parameters, templateBody: template });
this.logger.pop();
if (!changeset) {
this.logger.info(`${stackName} — no retain changes needed`);
return;
}
if (!this.isAllowedRetainChangeset(changeset)) {
throw new amplify_cli_core_1.AmplifyError('MigrationError', {
message: `Retain changeset for ${stackName} contains unexpected changes`,
resolution: cfn.renderChangeSet(changeset),
});
}
const url = (0, drift_formatter_1.cfnChangesetConsoleUrl)((_b = changeset.ChangeSetId) !== null && _b !== void 0 ? _b : '', changeset.StackId);
if (url) {
this.logger.info(`Changeset URL: ${chalk_1.default.dim(url)}`);
}
await cfn.executeChangeSet({
changeSet: changeset,
templateBody: template,
captureSnapshot: false,
});
}
finally {
if (pushed)
this.logger.pop();
}
},
};
}
isAllowedRetainChangeset(changeSet) {
var _a, _b, _c, _d;
const changes = (_a = changeSet.Changes) !== null && _a !== void 0 ? _a : [];
if (changes.length === 0)
return true;
for (const change of changes) {
const rc = change.ResourceChange;
if (!rc)
return false;
if (rc.Action !== 'Modify')
return false;
if (rc.Replacement === 'True')
return false;
const details = (_b = rc.Details) !== null && _b !== void 0 ? _b : [];
if (details.length === 0)
return false;
const isNestedStackAutomaticReEval = rc.ResourceType === 'AWS::CloudFormation::Stack' &&
details.every((d) => {
var _a, _b;
return ((_a = d.Target) === null || _a === void 0 ? void 0 : _a.Attribute) === 'Properties' &&
((_b = d.Target) === null || _b === void 0 ? void 0 : _b.RequiresRecreation) === 'Never' &&
d.Evaluation === 'Dynamic' &&
d.ChangeSource === 'Automatic';
});
if (isNestedStackAutomaticReEval)
continue;
for (const detail of details) {
const attr = (_c = detail.Target) === null || _c === void 0 ? void 0 : _c.Attribute;
const after = (_d = detail.Target) === null || _d === void 0 ? void 0 : _d.AfterValue;
if (attr !== 'DeletionPolicy' && attr !== 'UpdateReplacePolicy')
return false;
if (after !== 'Retain')
return false;
}
}
return true;
}
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('MigrationError', {
message: `Unable to find nested stack logical id prefix: ${logicalIdPrefix}`,
});
}
return stackId;
}
}
exports.AmplifyMigrationRetainStep = AmplifyMigrationRetainStep;
//# sourceMappingURL=retain.js.map