@aws-amplify/cli-internal
Version:
Amplify CLI
313 lines • 15.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectTemplateDrift = void 0;
const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core");
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
const client_cloudformation_2 = require("@aws-sdk/client-cloudformation");
const fs_extra_1 = __importDefault(require("fs-extra"));
const path = __importStar(require("path"));
function extractStackNameFromArn(stackArn) {
const resource = (0, amplify_cli_core_1.parseArn)(stackArn).resource;
return resource.split('/')[1];
}
function extractChangeSetNameFromArn(changeSetArn) {
const resource = (0, amplify_cli_core_1.parseArn)(changeSetArn).resource;
return resource.split('/')[1];
}
function isRecoverableFailure(reason) {
if (!reason)
return false;
if (reason.includes('EarlyValidation'))
return true;
if (reason.includes('Template format error'))
return true;
return false;
}
function hasNestedChangeSetFailure(changeSet) {
var _a, _b;
return ((_b = (_a = changeSet.Changes) === null || _a === void 0 ? void 0 : _a.some((c) => { var _a, _b; return ((_a = c.ResourceChange) === null || _a === void 0 ? void 0 : _a.ResourceType) === 'AWS::CloudFormation::Stack' && ((_b = c.ResourceChange) === null || _b === void 0 ? void 0 : _b.ChangeSetId); })) !== null && _b !== void 0 ? _b : false);
}
const CHANGESET_PREFIX = 'amplify-drift-detection-';
async function cleanupOldDriftChangesets(cfn, stackName, print) {
var _a;
try {
const toDelete = [];
for await (const page of (0, client_cloudformation_2.paginateListChangeSets)({ client: cfn }, { StackName: stackName })) {
for (const cs of page.Summaries || []) {
if ((_a = cs.ChangeSetName) === null || _a === void 0 ? void 0 : _a.startsWith(CHANGESET_PREFIX)) {
toDelete.push(cs.ChangeSetName);
}
}
}
await Promise.allSettled(toDelete.map((name) => {
print.debug(`Deleting old drift changeset: ${name}`);
return cfn.send(new client_cloudformation_1.DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: name }));
}));
}
catch (error) {
print.debug(`Failed to clean up old drift changesets: ${error.message}`);
}
}
async function detectTemplateDrift(stackName, print, cfn) {
var _a;
try {
const currentCloudBackendPath = amplify_cli_core_1.pathManager.getCurrentCloudBackendDirPath();
print.debug(`Checking for #current-cloud-backend at: ${currentCloudBackendPath}`);
if (!fs_extra_1.default.existsSync(currentCloudBackendPath)) {
return {
changes: [],
skipped: true,
skipReason: 'No #current-cloud-backend found. Run "amplify pull" first.',
};
}
const templatePath = path.join(currentCloudBackendPath, 'awscloudformation', 'build', 'root-cloudformation-stack.json');
print.debug(`Reading cached template from: ${templatePath}`);
if (!fs_extra_1.default.existsSync(templatePath)) {
return {
changes: [],
skipped: true,
skipReason: 'No cached CloudFormation template found',
};
}
const template = await fs_extra_1.default.readJson(templatePath);
print.debug(`Fetching stack parameters from CloudFormation for: ${stackName}`);
const stackDescription = await cfn.send(new client_cloudformation_1.DescribeStacksCommand({
StackName: stackName,
}));
if (!stackDescription.Stacks || stackDescription.Stacks.length === 0) {
return {
changes: [],
skipped: true,
skipReason: `Stack ${stackName} not found in CloudFormation`,
};
}
const parameters = stackDescription.Stacks[0].Parameters || [];
print.debug(`Using ${parameters.length} parameters from deployed stack`);
await cleanupOldDriftChangesets(cfn, stackName, print);
const changeSetName = `${CHANGESET_PREFIX}${Date.now()}`;
print.debug(`Creating changeset: ${changeSetName}`);
await cfn.send(new client_cloudformation_1.CreateChangeSetCommand({
StackName: stackName,
ChangeSetName: changeSetName,
TemplateBody: JSON.stringify(template),
Parameters: parameters,
Capabilities: ['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'],
ChangeSetType: 'UPDATE',
IncludeNestedStacks: true,
}));
try {
await (0, client_cloudformation_1.waitUntilChangeSetCreateComplete)({
client: cfn,
maxWaitTime: 300,
}, {
StackName: stackName,
ChangeSetName: changeSetName,
});
}
catch (waitError) {
print.debug(`Changeset waiter failed, will check status...`);
}
const changeSet = await cfn.send(new client_cloudformation_1.DescribeChangeSetCommand({
StackName: stackName,
ChangeSetName: changeSetName,
}));
if (changeSet.Status === 'FAILED' && ((_a = changeSet.StatusReason) === null || _a === void 0 ? void 0 : _a.includes("didn't contain changes"))) {
print.debug('✓ Changeset status: No changes detected (no drift)');
await cfn
.send(new client_cloudformation_1.DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName }))
.catch((e) => print.debug(`Failed to delete changeset: ${e.message}`));
return {
changes: [],
skipped: false,
};
}
if (changeSet.Status === 'FAILED') {
print.warn(`Root changeset FAILED: ${changeSet.StatusReason}`);
}
print.debug(`CloudFormation ChangeSet: ${stackName}`);
print.debug(`Status: ${changeSet.Status}`);
print.debug(`IncludeNestedStacks: ${changeSet.IncludeNestedStacks}`);
if (changeSet.StatusReason) {
print.debug(`StatusReason: ${changeSet.StatusReason}`);
}
if (changeSet.Changes && changeSet.Changes.length > 0) {
print.debug(`Changes: ${changeSet.Changes.length}`);
for (const change of changeSet.Changes) {
if (change.ResourceChange) {
const rc = change.ResourceChange;
print.debug(` ${rc.LogicalResourceId} (${rc.ResourceType}) - ${rc.Action}`);
}
}
}
else {
print.debug('Changes: 0');
}
const result = await analyzeChangeSet(cfn, changeSet, print, true);
return { ...result, changeSetId: changeSet.ChangeSetId };
}
catch (error) {
return {
changes: [],
skipped: true,
skipReason: `Error during template drift detection: ${error.message}`,
};
}
}
exports.detectTemplateDrift = detectTemplateDrift;
async function analyzeChangeSet(cfn, changeSet, print, isRoot = false) {
var _a, _b, _c, _d;
const result = {
changes: [],
skipped: false,
};
const skippedStacks = [];
const terminalStatuses = ['CREATE_COMPLETE', 'FAILED'];
if (!terminalStatuses.includes((_a = changeSet.Status) !== null && _a !== void 0 ? _a : '')) {
print.warn(`Changeset in non-terminal status: ${changeSet.Status}`);
return {
changes: [],
skipped: true,
skipReason: `Changeset in non-terminal status: ${changeSet.Status}`,
};
}
if (changeSet.Status === 'FAILED') {
if (((_b = changeSet.StatusReason) === null || _b === void 0 ? void 0 : _b.includes("didn't contain changes")) || ((_c = changeSet.StatusReason) === null || _c === void 0 ? void 0 : _c.includes('No updates'))) {
print.debug(`ChangeSet has no updates: ${changeSet.StatusReason}`);
return result;
}
if (isRecoverableFailure(changeSet.StatusReason)) {
if (!changeSet.Changes || changeSet.Changes.length === 0) {
print.warn(`Recoverable failure but no Changes populated: ${changeSet.StatusReason}`);
return {
changes: [],
skipped: true,
skipReason: `Changeset failed with no usable changes: ${changeSet.StatusReason}`,
};
}
print.warn(`Nested changeset FAILED (recoverable): ${changeSet.StatusReason}`);
}
else if (isRoot && hasNestedChangeSetFailure(changeSet)) {
if (!changeSet.Changes || changeSet.Changes.length === 0) {
print.warn(`Root changeset FAILED with nested failure but no Changes to analyze: ${changeSet.StatusReason}`);
return {
changes: [],
skipped: true,
skipReason: `Changeset failed with no usable changes: ${changeSet.StatusReason || 'Unknown reason'}`,
};
}
print.debug(`Root changeset FAILED due to nested failure — falling through to analyze nested changesets`);
}
else {
print.warn(`ChangeSet failed with unexpected reason: ${changeSet.StatusReason || 'No reason provided'}`);
return {
changes: [],
skipped: true,
skipReason: `Changeset failed: ${changeSet.StatusReason || 'Unknown reason'}`,
};
}
}
if (!changeSet.Changes || changeSet.Changes.length === 0) {
print.debug('ChangeSet has no changes');
return result;
}
print.debug(`Analyzing ${changeSet.Changes.length} changes from changeset`);
for (const change of changeSet.Changes) {
if (change.Type !== 'Resource' || !change.ResourceChange) {
continue;
}
const rc = change.ResourceChange;
const changeInfo = { ...rc };
if (rc.ResourceType === 'AWS::CloudFormation::Stack' && rc.ChangeSetId && rc.PhysicalResourceId) {
try {
const stackName = extractStackNameFromArn(rc.PhysicalResourceId);
const changeSetName = extractChangeSetNameFromArn(rc.ChangeSetId);
print.debug(`Fetching nested changeset: ${stackName}`);
print.debug(`ChangeSet: ${changeSetName}`);
try {
await (0, client_cloudformation_1.waitUntilChangeSetCreateComplete)({ client: cfn, maxWaitTime: 120 }, { StackName: stackName, ChangeSetName: changeSetName });
}
catch (waitError) {
print.debug(`Nested changeset waiter for ${stackName} finished with: ${waitError.message}`);
}
const nestedChangeSet = await cfn.send(new client_cloudformation_1.DescribeChangeSetCommand({
StackName: stackName,
ChangeSetName: changeSetName,
}));
if (!terminalStatuses.includes((_d = nestedChangeSet.Status) !== null && _d !== void 0 ? _d : '')) {
print.warn(`Nested changeset ${stackName} did not reach terminal status (${nestedChangeSet.Status})`);
skippedStacks.push(stackName);
result.changes.push(changeInfo);
continue;
}
if (nestedChangeSet.Changes && nestedChangeSet.Changes.length > 0) {
print.debug(`Nested Stack: ${stackName}`);
print.debug(`Nested Changes: ${nestedChangeSet.Changes.length}`);
for (const nestedChange of nestedChangeSet.Changes) {
if (nestedChange.ResourceChange) {
const nrc = nestedChange.ResourceChange;
print.debug(` ${nrc.LogicalResourceId} (${nrc.ResourceType}) - ${nrc.Action}`);
if (nrc.ResourceType === 'AWS::CloudFormation::Stack' && nrc.ChangeSetId) {
print.debug(` Has nested changeset (3rd level or deeper)`);
}
}
}
}
const nestedResult = await analyzeChangeSet(cfn, nestedChangeSet, print);
if (nestedResult.skipped) {
print.warn(`⚠ Nested stack ${stackName} analysis was skipped: ${nestedResult.skipReason}`);
skippedStacks.push(stackName);
}
if (nestedResult.incompleteStacks) {
skippedStacks.push(...nestedResult.incompleteStacks);
}
if (nestedResult.changes && nestedResult.changes.length > 0) {
changeInfo.nestedChanges = nestedResult.changes;
print.debug(`Processed ${nestedResult.changes.length} nested changes`);
}
}
catch (error) {
print.warn(`⚠ Could not fetch nested changeset for ${rc.LogicalResourceId}: ${error.message}`);
print.debug(`Stack ARN: ${rc.PhysicalResourceId}`);
print.debug(`ChangeSet ID: ${rc.ChangeSetId}`);
try {
skippedStacks.push(extractStackNameFromArn(rc.PhysicalResourceId));
}
catch (_e) {
skippedStacks.push(rc.LogicalResourceId || 'unknown');
}
}
}
result.changes.push(changeInfo);
}
if (skippedStacks.length > 0) {
return { ...result, incompleteStacks: skippedStacks };
}
return result;
}
//# sourceMappingURL=detect-template-drift.js.map