UNPKG

@aws-amplify/cli-internal

Version:
313 lines 15.1 kB
"use strict"; 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