packages/aws-cdk/lib/commands/migrate.ts (623 lines of code) (raw):

/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-var-requires */ import * as fs from 'fs'; import * as path from 'path'; import type { ForReading } from '@aws-cdk/cli-plugin-contract'; import type { Environment } from '@aws-cdk/cx-api'; import { UNKNOWN_ACCOUNT, UNKNOWN_REGION } from '@aws-cdk/cx-api'; import type { DescribeGeneratedTemplateCommandOutput, DescribeResourceScanCommandOutput, GetGeneratedTemplateCommandOutput, ListResourceScanResourcesCommandInput, ResourceDefinition, ResourceDetail, ResourceIdentifierSummary, ResourceScanSummary, ScannedResource, ScannedResourceIdentifier, } from '@aws-sdk/client-cloudformation'; import * as cdk_from_cfn from 'cdk-from-cfn'; import * as chalk from 'chalk'; import { cliInit } from './init'; import { ToolkitError } from '../../../@aws-cdk/toolkit-lib/lib/api'; import { info } from '../../lib/logging'; import type { ICloudFormationClient, SdkProvider } from '../api/aws-auth'; import { CloudFormationStack } from '../api/cloudformation'; import { zipDirectory } from '../util'; const camelCase = require('camelcase'); const decamelize = require('decamelize'); /** The list of languages supported by the built-in noctilucent binary. */ const MIGRATE_SUPPORTED_LANGUAGES: readonly string[] = cdk_from_cfn.supported_languages(); /** * Generates a CDK app from a yaml or json template. * * @param stackName The name to assign to the stack in the generated app * @param stack The yaml or json template for the stack * @param language The language to generate the CDK app in * @param outputPath The path at which to generate the CDK app */ export async function generateCdkApp( stackName: string, stack: string, language: string, outputPath?: string, compress?: boolean, ): Promise<void> { const resolvedOutputPath = path.join(outputPath ?? process.cwd(), stackName); const formattedStackName = decamelize(stackName); try { fs.rmSync(resolvedOutputPath, { recursive: true, force: true }); fs.mkdirSync(resolvedOutputPath, { recursive: true }); const generateOnly = compress; await cliInit({ type: 'app', language, canUseNetwork: true, generateOnly, workDir: resolvedOutputPath, stackName, migrate: true, }); let stackFileName: string; switch (language) { case 'typescript': stackFileName = `${resolvedOutputPath}/lib/${formattedStackName}-stack.ts`; break; case 'java': stackFileName = `${resolvedOutputPath}/src/main/java/com/myorg/${camelCase(formattedStackName, { pascalCase: true })}Stack.java`; break; case 'python': stackFileName = `${resolvedOutputPath}/${formattedStackName.replace(/-/g, '_')}/${formattedStackName.replace(/-/g, '_')}_stack.py`; break; case 'csharp': stackFileName = `${resolvedOutputPath}/src/${camelCase(formattedStackName, { pascalCase: true })}/${camelCase(formattedStackName, { pascalCase: true })}Stack.cs`; break; case 'go': stackFileName = `${resolvedOutputPath}/${formattedStackName}.go`; break; default: throw new ToolkitError( `${language} is not supported by CDK Migrate. Please choose from: ${MIGRATE_SUPPORTED_LANGUAGES.join(', ')}`, ); } fs.writeFileSync(stackFileName, stack); if (compress) { await zipDirectory(resolvedOutputPath, `${resolvedOutputPath}.zip`); fs.rmSync(resolvedOutputPath, { recursive: true, force: true }); } } catch (error) { fs.rmSync(resolvedOutputPath, { recursive: true, force: true }); throw error; } } /** * Generates a CDK stack file. * @param template The template to translate into a CDK stack * @param stackName The name to assign to the stack * @param language The language to generate the stack in * @returns A string representation of a CDK stack file */ export function generateStack(template: string, stackName: string, language: string) { const formattedStackName = `${camelCase(decamelize(stackName), { pascalCase: true })}Stack`; try { return cdk_from_cfn.transmute(template, language, formattedStackName); } catch (e) { throw new ToolkitError(`${formattedStackName} could not be generated because ${(e as Error).message}`); } } /** * Reads and returns a stack template from a local path. * * @param inputPath The location of the template * @returns A string representation of the template if present, otherwise undefined */ export function readFromPath(inputPath: string): string { let readFile: string; try { readFile = fs.readFileSync(inputPath, 'utf8'); } catch (e) { throw new ToolkitError(`'${inputPath}' is not a valid path.`); } if (readFile == '') { throw new ToolkitError(`Cloudformation template filepath: '${inputPath}' is an empty file.`); } return readFile; } /** * Reads and returns a stack template from a deployed CloudFormation stack. * * @param stackName The name of the stack * @param sdkProvider The sdk provider for making CloudFormation calls * @param environment The account and region where the stack is deployed * @returns A string representation of the template if present, otherwise undefined */ export async function readFromStack( stackName: string, sdkProvider: SdkProvider, environment: Environment, ): Promise<string | undefined> { const cloudFormation = (await sdkProvider.forEnvironment(environment, 0 satisfies ForReading)).sdk.cloudFormation(); const stack = await CloudFormationStack.lookup(cloudFormation, stackName, true); if (stack.stackStatus.isDeploySuccess || stack.stackStatus.isRollbackSuccess) { return JSON.stringify(await stack.template()); } else { throw new ToolkitError( `Stack '${stackName}' in account ${environment.account} and region ${environment.region} has a status of '${stack.stackStatus.name}' due to '${stack.stackStatus.reason}'. The stack cannot be migrated until it is in a healthy state.`, ); } } /** * Takes in a stack name and account and region and returns a generated cloudformation template using the cloudformation * template generator. * * @param GenerateTemplateOptions An object containing the stack name, filters, sdkProvider, environment, and newScan flag * @returns a generated cloudformation template */ export async function generateTemplate(options: GenerateTemplateOptions): Promise<GenerateTemplateOutput> { const cfn = new CfnTemplateGeneratorProvider(await buildCfnClient(options.sdkProvider, options.environment)); const scanId = await findLastSuccessfulScan(cfn, options); // if a customer accidentally ctrl-c's out of the command and runs it again, this will continue the progress bar where it left off const curScan = await cfn.describeResourceScan(scanId); if (curScan.Status == ScanStatus.IN_PROGRESS) { info('Resource scan in progress. Please wait, this can take 10 minutes or longer.'); await scanProgressBar(scanId, cfn); } displayTimeDiff(new Date(), new Date(curScan.StartTime!)); let resources: ScannedResource[] = await cfn.listResourceScanResources(scanId!, options.filters); info('finding related resources.'); let relatedResources = await cfn.getResourceScanRelatedResources(scanId!, resources); info(`Found ${relatedResources.length} resources.`); info('Generating CFN template from scanned resources.'); const templateArn = (await cfn.createGeneratedTemplate(options.stackName, relatedResources)).GeneratedTemplateId!; let generatedTemplate = await cfn.describeGeneratedTemplate(templateArn); info('Please wait, template creation in progress. This may take a couple minutes.'); while (generatedTemplate.Status !== ScanStatus.COMPLETE && generatedTemplate.Status !== ScanStatus.FAILED) { await printDots(`[${generatedTemplate.Status}] Template Creation in Progress`, 400); generatedTemplate = await cfn.describeGeneratedTemplate(templateArn); } info(''); info('Template successfully generated!'); return buildGenertedTemplateOutput( generatedTemplate, (await cfn.getGeneratedTemplate(templateArn)).TemplateBody!, templateArn, ); } async function findLastSuccessfulScan( cfn: CfnTemplateGeneratorProvider, options: GenerateTemplateOptions, ): Promise<string> { let resourceScanSummaries: ResourceScanSummary[] | undefined = []; const clientRequestToken = `cdk-migrate-${options.environment.account}-${options.environment.region}`; if (options.fromScan === FromScan.NEW) { info(`Starting new scan for account ${options.environment.account} in region ${options.environment.region}`); try { await cfn.startResourceScan(clientRequestToken); resourceScanSummaries = (await cfn.listResourceScans()).ResourceScanSummaries; } catch (e) { // continuing here because if the scan fails on a new-scan it is very likely because there is either already a scan in progress // or the customer hit a rate limit. In either case we want to continue with the most recent scan. // If this happens to fail for a credential error then that will be caught immediately after anyway. info(`Scan failed to start due to error '${(e as Error).message}', defaulting to latest scan.`); } } else { resourceScanSummaries = (await cfn.listResourceScans()).ResourceScanSummaries; await cfn.checkForResourceScan(resourceScanSummaries, options, clientRequestToken); } // get the latest scan, which we know will exist resourceScanSummaries = (await cfn.listResourceScans()).ResourceScanSummaries; let scanId: string | undefined = resourceScanSummaries![0].ResourceScanId; // find the most recent scan that isn't in a failed state in case we didn't start a new one for (const summary of resourceScanSummaries!) { if (summary.Status !== ScanStatus.FAILED) { scanId = summary.ResourceScanId!; break; } } return scanId!; } /** * Takes a string of filters in the format of key1=value1,key2=value2 and returns a map of the filters. * * @param filters a string of filters in the format of key1=value1,key2=value2 * @returns a map of the filters */ function parseFilters(filters: string): { [key in FilterType]: string | undefined; } { if (!filters) { return { 'resource-identifier': undefined, 'resource-type-prefix': undefined, 'tag-key': undefined, 'tag-value': undefined, }; } const filterShorthands: { [key: string]: FilterType } = { 'identifier': FilterType.RESOURCE_IDENTIFIER, 'id': FilterType.RESOURCE_IDENTIFIER, 'type': FilterType.RESOURCE_TYPE_PREFIX, 'type-prefix': FilterType.RESOURCE_TYPE_PREFIX, }; const filterList = filters.split(','); let filterMap: { [key in FilterType]: string | undefined } = { [FilterType.RESOURCE_IDENTIFIER]: undefined, [FilterType.RESOURCE_TYPE_PREFIX]: undefined, [FilterType.TAG_KEY]: undefined, [FilterType.TAG_VALUE]: undefined, }; for (const fil of filterList) { const filter = fil.split('='); let filterKey = filter[0]; const filterValue = filter[1]; // if the key is a shorthand, replace it with the full name if (filterKey in filterShorthands) { filterKey = filterShorthands[filterKey]; } if (Object.values(FilterType).includes(filterKey as any)) { filterMap[filterKey as keyof typeof filterMap] = filterValue; } else { throw new ToolkitError(`Invalid filter: ${filterKey}`); } } return filterMap; } /** * Takes a list of any type and breaks it up into chunks of a specified size. * * @param list The list to break up * @param chunkSize The size of each chunk * @returns A list of lists of the specified size */ export function chunks(list: any[], chunkSize: number): any[][] { const chunkedList: any[][] = []; for (let i = 0; i < list.length; i += chunkSize) { chunkedList.push(list.slice(i, i + chunkSize)); } return chunkedList; } /** * Sets the account and region for making CloudFormation calls. * @param account The account to use * @param region The region to use * @returns The environment object */ export function setEnvironment(account?: string, region?: string): Environment { return { account: account ?? UNKNOWN_ACCOUNT, region: region ?? UNKNOWN_REGION, name: 'cdk-migrate-env', }; } /** * Enum for the source options for the template */ export enum TemplateSourceOptions { PATH = 'path', STACK = 'stack', SCAN = 'scan', } /** * An object representing the source of a template. */ type TemplateSource = | { source: TemplateSourceOptions.SCAN } | { source: TemplateSourceOptions.PATH; templatePath: string } | { source: TemplateSourceOptions.STACK; stackName: string }; /** * Enum for the status of a resource scan */ export enum ScanStatus { IN_PROGRESS = 'IN_PROGRESS', COMPLETE = 'COMPLETE', FAILED = 'FAILED', } export enum FilterType { RESOURCE_IDENTIFIER = 'resource-identifier', RESOURCE_TYPE_PREFIX = 'resource-type-prefix', TAG_KEY = 'tag-key', TAG_VALUE = 'tag-value', } /** * Validates that exactly one source option has been provided. * @param fromPath The content of the flag `--from-path` * @param fromStack the content of the flag `--from-stack` */ export function parseSourceOptions(fromPath?: string, fromStack?: boolean, stackName?: string): TemplateSource { if (fromPath && fromStack) { throw new ToolkitError('Only one of `--from-path` or `--from-stack` may be provided.'); } if (!stackName) { throw new ToolkitError('`--stack-name` is a required field.'); } if (!fromPath && !fromStack) { return { source: TemplateSourceOptions.SCAN }; } if (fromPath) { return { source: TemplateSourceOptions.PATH, templatePath: fromPath }; } return { source: TemplateSourceOptions.STACK, stackName: stackName! }; } /** * Takes a set of resources and removes any with the managedbystack flag set to true. * * @param resourceList the list of resources provided by the list scanned resources calls * @returns a list of resources not managed by cfn stacks */ function excludeManaged(resourceList: ScannedResource[]): ScannedResourceIdentifier[] { return resourceList .filter((r) => !r.ManagedByStack) .map((r) => ({ ResourceType: r.ResourceType!, ResourceIdentifier: r.ResourceIdentifier!, })); } /** * Transforms a list of resources into a list of resource identifiers by removing the ManagedByStack flag. * Setting the value of the field to undefined effectively removes it from the object. * * @param resourceList the list of resources provided by the list scanned resources calls * @returns a list of ScannedResourceIdentifier[] */ function resourceIdentifiers(resourceList: ScannedResource[]): ScannedResourceIdentifier[] { const identifiers: ScannedResourceIdentifier[] = []; resourceList.forEach((r) => { const identifier: ScannedResourceIdentifier = { ResourceType: r.ResourceType!, ResourceIdentifier: r.ResourceIdentifier!, }; identifiers.push(identifier); }); return identifiers; } /** * Takes a scan id and maintains a progress bar to display the progress of a scan to the user. * * @param scanId A string representing the scan id * @param cloudFormation The CloudFormation sdk client to use */ export async function scanProgressBar(scanId: string, cfn: CfnTemplateGeneratorProvider) { let curProgress = 0.5; // we know it's in progress initially since we wouldn't have gotten here if it wasn't let curScan: DescribeResourceScanCommandOutput = { Status: ScanStatus.IN_PROGRESS, $metadata: {}, }; while (curScan.Status == ScanStatus.IN_PROGRESS) { curScan = await cfn.describeResourceScan(scanId); curProgress = curScan.PercentageCompleted ?? curProgress; printBar(30, curProgress); await new Promise((resolve) => setTimeout(resolve, 2000)); } info(''); info('✅ Scan Complete!'); } /** * Prints a progress bar to the console. To be used in a while loop to show progress of a long running task. * The progress bar deletes the current line on the console and rewrites it with the progress amount. * * @param width The width of the progress bar * @param progress The current progress to display as a percentage of 100 */ export function printBar(width: number, progress: number) { if (!process.env.MIGRATE_INTEG_TEST) { const FULL_BLOCK = '█'; const PARTIAL_BLOCK = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']; const fraction = Math.min(progress / 100, 1); const innerWidth = Math.max(1, width - 2); const chars = innerWidth * fraction; const remainder = chars - Math.floor(chars); const fullChars = FULL_BLOCK.repeat(Math.floor(chars)); const partialChar = PARTIAL_BLOCK[Math.floor(remainder * PARTIAL_BLOCK.length)]; const filler = '·'.repeat(innerWidth - Math.floor(chars) - (partialChar ? 1 : 0)); const color = chalk.green; rewriteLine('[' + color(fullChars + partialChar) + filler + `] (${progress}%)`); } } /** * Prints a message to the console with a series periods appended to it. To be used in a while loop to show progress of a long running task. * The message deletes the current line and rewrites it several times to display 1-3 periods to show the user that the task is still running. * * @param message The message to display * @param timeoutx4 The amount of time to wait before printing the next period */ export async function printDots(message: string, timeoutx4: number) { if (!process.env.MIGRATE_INTEG_TEST) { rewriteLine(message + ' .'); await new Promise((resolve) => setTimeout(resolve, timeoutx4)); rewriteLine(message + ' ..'); await new Promise((resolve) => setTimeout(resolve, timeoutx4)); rewriteLine(message + ' ...'); await new Promise((resolve) => setTimeout(resolve, timeoutx4)); rewriteLine(message); await new Promise((resolve) => setTimeout(resolve, timeoutx4)); } } /** * Rewrites the current line on the console and writes a new message to it. * This is a helper funciton for printDots and printBar. * * @param message The message to display */ export function rewriteLine(message: string) { process.stdout.clearLine(0); process.stdout.cursorTo(0); process.stdout.write(message); } /** * Prints the time difference between two dates in days, hours, and minutes. * * @param time1 The first date to compare * @param time2 The second date to compare */ export function displayTimeDiff(time1: Date, time2: Date): void { const diff = Math.abs(time1.getTime() - time2.getTime()); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); info(`Using the latest successful scan which is ${days} days, ${hours} hours, and ${minutes} minutes old.`); } /** * Writes a migrate.json file to the output directory. * * @param outputPath The path to write the migrate.json file to * @param stackName The name of the stack * @param generatedOutput The output of the template generator */ export function writeMigrateJsonFile( outputPath: string | undefined, stackName: string, migrateJson: MigrateJsonFormat, ) { const outputToJson = { '//': 'This file is generated by cdk migrate. It will be automatically deleted after the first successful deployment of this app to the environment of the original resources.', 'Source': migrateJson.source, 'Resources': migrateJson.resources, }; fs.writeFileSync( `${path.join(outputPath ?? process.cwd(), stackName)}/migrate.json`, JSON.stringify(outputToJson, null, 2), ); } /** * Takes a string representing the from-scan flag and returns a FromScan enum value. * * @param scanType A string representing the from-scan flag * @returns A FromScan enum value */ export function getMigrateScanType(scanType: string) { switch (scanType) { case 'new': return FromScan.NEW; case 'most-recent': return FromScan.MOST_RECENT; case '': return FromScan.DEFAULT; case undefined: return FromScan.DEFAULT; default: throw new ToolkitError(`Unknown scan type: ${scanType}`); } } /** * Takes a generatedTemplateOutput objct and returns a boolean representing whether there are any warnings on any rescources. * * @param generatedTemplateOutput A GenerateTemplateOutput object * @returns A boolean representing whether there are any warnings on any rescources */ export function isThereAWarning(generatedTemplateOutput: GenerateTemplateOutput) { if (generatedTemplateOutput.resources) { for (const resource of generatedTemplateOutput.resources) { if (resource.Warnings && resource.Warnings.length > 0) { return true; } } } return false; } /** * Builds the GenerateTemplateOutput object from the DescribeGeneratedTemplateOutput and the template body. * * @param generatedTemplateSummary The output of the describe generated template call * @param templateBody The body of the generated template * @returns A GenerateTemplateOutput object */ export function buildGenertedTemplateOutput( generatedTemplateSummary: DescribeGeneratedTemplateCommandOutput, templateBody: string, source: string, ): GenerateTemplateOutput { const resources: ResourceDetail[] | undefined = generatedTemplateSummary.Resources; const migrateJson: MigrateJsonFormat = { templateBody: templateBody, source: source, resources: generatedTemplateSummary.Resources!.map((r) => ({ ResourceType: r.ResourceType!, LogicalResourceId: r.LogicalResourceId!, ResourceIdentifier: r.ResourceIdentifier!, })), }; const templateId = generatedTemplateSummary.GeneratedTemplateId!; return { migrateJson: migrateJson, resources: resources, templateId: templateId, }; } /** * Builds a CloudFormation sdk client for making requests with the CFN template generator. * * @param sdkProvider The sdk provider for making CloudFormation calls * @param environment The account and region where the stack is deployed * @returns A CloudFormation sdk client */ export async function buildCfnClient(sdkProvider: SdkProvider, environment: Environment) { const sdk = (await sdkProvider.forEnvironment(environment, 0 satisfies ForReading)).sdk; sdk.appendCustomUserAgent('cdk-migrate'); return sdk.cloudFormation(); } /** * Appends a list of warnings to a readme file. * * @param filepath The path to the readme file * @param resources A list of resources to append warnings for */ export function appendWarningsToReadme(filepath: string, resources: ResourceDetail[]) { const readme = fs.readFileSync(filepath, 'utf8'); const lines = readme.split('\n'); const index = lines.findIndex((line) => line.trim() === 'Enjoy!'); let linesToAdd = ['\n## Warnings']; linesToAdd.push('### Write-only properties'); linesToAdd.push( "Write-only properties are resource property values that can be written to but can't be read by AWS CloudFormation or CDK Migrate. For more information, see [IaC generator and write-only properties](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/generate-IaC-write-only-properties.html).", ); linesToAdd.push('\n'); linesToAdd.push( 'Write-only properties discovered during migration are organized here by resource ID and categorized by write-only property type. Resolve write-only properties by providing property values in your CDK app. For guidance, see [Resolve write-only properties](https://docs.aws.amazon.com/cdk/v2/guide/migrate.html#migrate-resources-writeonly).', ); for (const resource of resources) { if (resource.Warnings && resource.Warnings.length > 0) { linesToAdd.push(`### ${resource.LogicalResourceId}`); for (const warning of resource.Warnings) { linesToAdd.push(`- **${warning.Type}**: `); for (const property of warning.Properties!) { linesToAdd.push(` - ${property.PropertyPath}: ${property.Description}`); } } } } lines.splice(index, 0, ...linesToAdd); fs.writeFileSync(filepath, lines.join('\n')); } /** * takes a list of resources and returns a list of unique resources based on the resource type and logical resource id. * * @param resources A list of resources to deduplicate * @returns A list of unique resources */ function deduplicateResources(resources: ResourceDetail[]) { let uniqueResources: { [key: string]: ResourceDetail } = {}; for (const resource of resources) { const key = Object.keys(resource.ResourceIdentifier!)[0]; // Creating our unique identifier using the resource type, the key, and the value of the resource identifier // The resource identifier is a combination of a key value pair defined by a resource's schema, and the resource type of the resource. const uniqueIdentifer = `${resource.ResourceType}:${key}:${resource.ResourceIdentifier![key]}`; uniqueResources[uniqueIdentifer] = resource; } return Object.values(uniqueResources); } /** * Class for making CloudFormation template generator calls */ export class CfnTemplateGeneratorProvider { private cfn: ICloudFormationClient; constructor(cfn: ICloudFormationClient) { this.cfn = cfn; } async checkForResourceScan( resourceScanSummaries: ResourceScanSummary[] | undefined, options: GenerateTemplateOptions, clientRequestToken: string, ) { if (!resourceScanSummaries || resourceScanSummaries.length === 0) { if (options.fromScan === FromScan.MOST_RECENT) { throw new ToolkitError( 'No scans found. Please either start a new scan with the `--from-scan` new or do not specify a `--from-scan` option.', ); } else { info('No scans found. Initiating a new resource scan.'); await this.startResourceScan(clientRequestToken); } } } /** * Retrieves a tokenized list of resources and their associated scan. If a token is present the function * will loop through all pages and combine them into a single list of ScannedRelatedResources * * @param scanId scan id for the to list resources for * @param resources A list of resources to find related resources for */ async getResourceScanRelatedResources( scanId: string, resources: ScannedResource[], ): Promise<ScannedResourceIdentifier[]> { let relatedResourceList = resources; // break the list of resources into chunks of 100 to avoid hitting the 100 resource limit for (const chunk of chunks(resources, 100)) { // get the first page of related resources const res = await this.cfn.listResourceScanRelatedResources({ ResourceScanId: scanId, Resources: chunk, }); // add the first page to the list relatedResourceList.push(...(res.RelatedResources ?? [])); let nextToken = res.NextToken; // if there are more pages, cycle through them and add them to the list before moving on to the next chunk while (nextToken) { const nextRelatedResources = await this.cfn.listResourceScanRelatedResources({ ResourceScanId: scanId, Resources: resourceIdentifiers(resources), NextToken: nextToken, }); nextToken = nextRelatedResources.NextToken; relatedResourceList.push(...(nextRelatedResources.RelatedResources ?? [])); } } relatedResourceList = deduplicateResources(relatedResourceList); // prune the managedbystack flag off of them again. return process.env.MIGRATE_INTEG_TEST ? resourceIdentifiers(relatedResourceList) : resourceIdentifiers(excludeManaged(relatedResourceList)); } /** * Kicks off a scan of a customers account, returning the scan id. A scan can take * 10 minutes or longer to complete. However this will return a scan id as soon as * the scan has begun. * * @returns A string representing the scan id */ async startResourceScan(requestToken: string) { return ( await this.cfn.startResourceScan({ ClientRequestToken: requestToken, }) ).ResourceScanId; } /** * Gets the most recent scans a customer has completed * * @returns a list of resource scan summaries */ async listResourceScans() { return this.cfn.listResourceScans(); } /** * Retrieves a tokenized list of resources from a resource scan. If a token is present, this function * will loop through all pages and combine them into a single list of ScannedResource[]. * Additionally will apply any filters provided by the customer. * * @param scanId scan id for the to list resources for * @param filters a string of filters in the format of key1=value1,key2=value2 * @returns a combined list of all resources from the scan */ async listResourceScanResources(scanId: string, filters: string[] = []): Promise<ScannedResourceIdentifier[]> { let resourceList: ScannedResource[] = []; let resourceScanInputs: ListResourceScanResourcesCommandInput; if (filters.length > 0) { info('Applying filters to resource scan.'); for (const filter of filters) { const filterList = parseFilters(filter); resourceScanInputs = { ResourceScanId: scanId, ResourceIdentifier: filterList[FilterType.RESOURCE_IDENTIFIER], ResourceTypePrefix: filterList[FilterType.RESOURCE_TYPE_PREFIX], TagKey: filterList[FilterType.TAG_KEY], TagValue: filterList[FilterType.TAG_VALUE], }; const resources = await this.cfn.listResourceScanResources(resourceScanInputs); resourceList = resourceList.concat(resources.Resources ?? []); let nextToken = resources.NextToken; // cycle through the pages adding all resources to the list until we run out of pages while (nextToken) { resourceScanInputs.NextToken = nextToken; const nextResources = await this.cfn.listResourceScanResources(resourceScanInputs); nextToken = nextResources.NextToken; resourceList = resourceList!.concat(nextResources.Resources ?? []); } } } else { info('No filters provided. Retrieving all resources from scan.'); resourceScanInputs = { ResourceScanId: scanId, }; const resources = await this.cfn.listResourceScanResources(resourceScanInputs); resourceList = resourceList!.concat(resources.Resources ?? []); let nextToken = resources.NextToken; // cycle through the pages adding all resources to the list until we run out of pages while (nextToken) { resourceScanInputs.NextToken = nextToken; const nextResources = await this.cfn.listResourceScanResources(resourceScanInputs); nextToken = nextResources.NextToken; resourceList = resourceList!.concat(nextResources.Resources ?? []); } } if (resourceList.length === 0) { throw new ToolkitError(`No resources found with filters ${filters.join(' ')}. Please try again with different filters.`); } resourceList = deduplicateResources(resourceList); return process.env.MIGRATE_INTEG_TEST ? resourceIdentifiers(resourceList) : resourceIdentifiers(excludeManaged(resourceList)); } /** * Retrieves information about a resource scan. * * @param scanId scan id for the to list resources for * @returns information about the scan */ async describeResourceScan(scanId: string): Promise<DescribeResourceScanCommandOutput> { return this.cfn.describeResourceScan({ ResourceScanId: scanId, }); } /** * Describes the current status of the template being generated. * * @param templateId A string representing the template id * @returns DescribeGeneratedTemplateOutput an object containing the template status and results */ async describeGeneratedTemplate(templateId: string): Promise<DescribeGeneratedTemplateCommandOutput> { const generatedTemplate = await this.cfn.describeGeneratedTemplate({ GeneratedTemplateName: templateId, }); if (generatedTemplate.Status == ScanStatus.FAILED) { throw new ToolkitError(generatedTemplate.StatusReason!); } return generatedTemplate; } /** * Retrieves a completed generated cloudformation template from the template generator. * * @param templateId A string representing the template id * @param cloudFormation The CloudFormation sdk client to use * @returns DescribeGeneratedTemplateOutput an object containing the template status and body */ async getGeneratedTemplate(templateId: string): Promise<GetGeneratedTemplateCommandOutput> { return this.cfn.getGeneratedTemplate({ GeneratedTemplateName: templateId, }); } /** * Kicks off a template generation for a set of resources. * * @param stackName The name of the stack * @param resources A list of resources to generate the template from * @returns CreateGeneratedTemplateOutput an object containing the template arn to query on later */ async createGeneratedTemplate(stackName: string, resources: ResourceDefinition[]) { const createTemplateOutput = await this.cfn.createGeneratedTemplate({ Resources: resources, GeneratedTemplateName: stackName, }); if (createTemplateOutput.GeneratedTemplateId === undefined) { throw new ToolkitError('CreateGeneratedTemplate failed to return an Arn.'); } return createTemplateOutput; } /** * Deletes a generated template from the template generator. * * @param templateArn The arn of the template to delete * @returns A promise that resolves when the template has been deleted */ async deleteGeneratedTemplate(templateArn: string): Promise<void> { await this.cfn.deleteGeneratedTemplate({ GeneratedTemplateName: templateArn, }); } } /** * The possible ways to choose a scan to generate a CDK application from */ export enum FromScan { /** * Initiate a new resource scan to build the CDK application from. */ NEW, /** * Use the last successful scan to build the CDK application from. Will fail if no scan is found. */ MOST_RECENT, /** * Starts a scan if none exists, otherwise uses the most recent successful scan to build the CDK application from. */ DEFAULT, } /** * Interface for the options object passed to the generateTemplate function * * @param stackName The name of the stack * @param filters A list of filters to apply to the scan * @param fromScan An enum value specifying whether a new scan should be started or the most recent successful scan should be used * @param sdkProvider The sdk provider for making CloudFormation calls * @param environment The account and region where the stack is deployed */ export interface GenerateTemplateOptions { stackName: string; filters?: string[]; fromScan?: FromScan; sdkProvider: SdkProvider; environment: Environment; } /** * Interface for the output of the generateTemplate function * * @param migrateJson The generated Migrate.json file * @param resources The generated template */ export interface GenerateTemplateOutput { migrateJson: MigrateJsonFormat; resources?: ResourceDetail[]; templateId?: string; } /** * Interface defining the format of the generated Migrate.json file * * @param TemplateBody The generated template * @param Source The source of the template * @param Resources A list of resources that were used to generate the template */ export interface MigrateJsonFormat { templateBody: string; source: string; resources?: GeneratedResourceImportIdentifier[]; } /** * Interface representing the format of a resource identifier required for resource import * * @param ResourceType The type of resource * @param LogicalResourceId The logical id of the resource * @param ResourceIdentifier The resource identifier of the resource */ export interface GeneratedResourceImportIdentifier { // cdk deploy expects the migrate.json resource identifiers to be PascalCase, not camelCase. ResourceType: string; LogicalResourceId: string; ResourceIdentifier: ResourceIdentifierSummary; }