diff --git a/packages/aws-cdk/lib/api/aws-auth/_env.ts b/packages/aws-cdk/lib/api/aws-auth/_env.ts deleted file mode 100644 index a5df37a182412..0000000000000 --- a/packages/aws-cdk/lib/api/aws-auth/_env.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * This file exists to expose and centralize some features that the files in this module expect from the surrounding - * CLI. - * - * The calls will be forwarded to whatever logging system is the "official" logging system for this CLI. - * - * Centralizing in this way makes it easy to copy/paste this directory out and have a single place to - * break dependencies and replace these functions. - */ - -export { debug, warning, trace } from '../../logging'; - -import { cdkCacheDir } from '../../util/directories'; - -export function accountCacheDir() { - return cdkCacheDir(); -} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/account-cache.ts b/packages/aws-cdk/lib/api/aws-auth/account-cache.ts index ec90ce48ec4d5..543dbf15d1188 100644 --- a/packages/aws-cdk/lib/api/aws-auth/account-cache.ts +++ b/packages/aws-cdk/lib/api/aws-auth/account-cache.ts @@ -1,7 +1,8 @@ import * as path from 'path'; import * as fs from 'fs-extra'; -import { accountCacheDir, debug } from './_env'; import { Account } from './sdk-provider'; +import { debug } from '../../logging'; +import { cdkCacheDir } from '../../util/directories'; /** * Disk cache which maps access key IDs to account IDs. @@ -21,7 +22,7 @@ export class AccountAccessKeyCache { * @param filePath Path to the cache file */ constructor(filePath?: string) { - this.cacheFile = filePath || path.join(accountCacheDir(), 'accounts_partitions.json'); + this.cacheFile = filePath || path.join(cdkCacheDir(), 'accounts_partitions.json'); } /** @@ -67,7 +68,7 @@ export class AccountAccessKeyCache { // nuke cache if it's too big. if (Object.keys(map).length >= AccountAccessKeyCache.MAX_ENTRIES) { - map = { }; + map = {}; } map[accessKeyId] = account; @@ -80,10 +81,14 @@ export class AccountAccessKeyCache { } catch (e: any) { // File doesn't exist or is not readable. This is a cache, // pretend we successfully loaded an empty map. - if (e.code === 'ENOENT' || e.code === 'EACCES') { return {}; } + if (e.name === 'ENOENT' || e.name === 'EACCES') { + return {}; + } // File is not JSON, could be corrupted because of concurrent writes. // Again, an empty cache is fine. - if (e instanceof SyntaxError) { return {}; } + if (e instanceof SyntaxError) { + return {}; + } throw e; } } @@ -95,7 +100,9 @@ export class AccountAccessKeyCache { } catch (e: any) { // File doesn't exist or file/dir isn't writable. This is a cache, // if we can't write it then too bad. - if (e.code === 'ENOENT' || e.code === 'EACCES' || e.code === 'EROFS') { return; } + if (e.name === 'ENOENT' || e.name === 'EACCES' || e.name === 'EROFS') { + return; + } throw e; } } diff --git a/packages/aws-cdk/lib/api/aws-auth/aws-sdk-inifile.ts b/packages/aws-cdk/lib/api/aws-auth/aws-sdk-inifile.ts deleted file mode 100644 index 7ff3a840a6cbc..0000000000000 --- a/packages/aws-cdk/lib/api/aws-auth/aws-sdk-inifile.ts +++ /dev/null @@ -1,175 +0,0 @@ -import * as AWS from 'aws-sdk'; - -/** - * Hack-fix - * - * There are a number of issues in the upstream version of SharedIniFileCredentials - * that need fixing: - * - * 1. The upstream aws-sdk does not support the 'credential_source' option. Meaning credentials - * for assume-role cannot be fetched using EC2/ESC metadata. - * - * 2. The upstream aws-sdk does not support SSO profiles as the source of RoleProfiles, - * because it will always use the `SharedIniFileCredentials` provider to load - * source credentials, but in order to support SSO profiles you must use a - * separate class (`SsoCredentials). - */ -export class PatchedSharedIniFileCredentials extends AWS.SharedIniFileCredentials { - declare private profile: string; - declare private filename: string; - declare private disableAssumeRole: boolean; - declare private options: Record; - declare private roleArn: string; - declare private httpOptions?: AWS.HTTPOptions; - declare private tokenCodeFn?: (mfaSerial: string, callback: (err?: Error, token?: string) => void) => void; - - public loadRoleProfile( - creds: Record>, - roleProfile: Record, - callback: (err?: Error, data?: any) => void) { - - // Need to duplicate the whole implementation here -- the function is long and has been written in - // such a way that there are no small monkey patches possible. - - if (this.disableAssumeRole) { - throw (AWS as any).util.error( - new Error('Role assumption profiles are disabled. ' + - 'Failed to load profile ' + this.profile + - ' from ' + creds.filename), - { code: 'SharedIniFileCredentialsProviderFailure' }, - ); - } - - var self = this; - var roleArn = roleProfile.role_arn; - var roleSessionName = roleProfile.role_session_name; - var externalId = roleProfile.external_id; - var mfaSerial = roleProfile.mfa_serial; - var sourceProfile = roleProfile.source_profile; - var credentialSource = roleProfile.credential_source; - - if (!!sourceProfile === !!credentialSource) { - throw (AWS as any).util.error( - new Error(`When using 'role_arn' in profile ('${this.profile}'), you must also configure exactly one of 'source_profile' or 'credential_source'`), - { code: 'SharedIniFileCredentialsProviderFailure' }, - ); - } - - // Confirmed this against AWS CLI behavior -- the region must be in the assumED profile, - // otherwise `us-east-1`. From the upstream comment in `aws-sdk-js`: - // -------- comment from aws-sdk-js ------------------- - // Experimentation shows that the AWS CLI (tested at version 1.18.136) - // ignores the following potential sources of a region for the purposes of - // this AssumeRole call: - // - // - The [default] profile - // - The AWS_REGION environment variable - // - // Ignoring the [default] profile for the purposes of AssumeRole is arguably - // a bug in the CLI since it does use the [default] region for service - // calls... but right now we're matching behavior of the other tool. - // ------------------------------------------------- - - const region = roleProfile?.region ?? 'us-east-1'; - - const stsCreds = sourceProfile ? this.sourceProfileCredentials(sourceProfile, creds) : this.credentialSourceCredentials(credentialSource); - - this.roleArn = roleArn; - var sts = new AWS.STS({ - credentials: stsCreds, - region, - httpOptions: this.httpOptions, - }); - - var roleParams: AWS.STS.AssumeRoleRequest = { - RoleArn: roleArn, - RoleSessionName: roleSessionName || 'aws-sdk-js-' + Date.now(), - }; - - if (externalId) { - roleParams.ExternalId = externalId; - } - - if (mfaSerial && self.tokenCodeFn) { - roleParams.SerialNumber = mfaSerial; - self.tokenCodeFn(mfaSerial, function(err, token) { - if (err) { - var message; - if (err instanceof Error) { - message = err.message; - } else { - message = err; - } - callback( - (AWS as any).util.error( - new Error('Error fetching MFA token: ' + message), - { code: 'SharedIniFileCredentialsProviderFailure' }, - )); - return; - } - - roleParams.TokenCode = token; - sts.assumeRole(roleParams, callback); - }); - return; - } - sts.assumeRole(roleParams, callback); - } - - private sourceProfileCredentials(sourceProfile: string, profiles: Record>) { - var sourceProfileExistanceTest = profiles[sourceProfile]; - - if (typeof sourceProfileExistanceTest !== 'object') { - throw (AWS as any).util.error( - new Error('source_profile ' + sourceProfile + ' using profile ' - + this.profile + ' does not exist'), - { code: 'SharedIniFileCredentialsProviderFailure' }, - ); - } - - // We need to do a manual check here if the source profile (providing the - // credentials for the AssumeRole) is an SSO profile. That's because - // `SharedIniFileCredentials` itself doesn't support providing credentials from - // arbitrary profiles, only for StaticCredentials and AssumeRole type - // profiles; if it's an SSO profile you need to instantiate a special - // Credential Provider for that. - // - // --- - // - // An SSO profile can be configured in 2 ways (put all the info in the profile - // section, or put half of it in an `[sso-session]` block), but in both cases - // the primary profile block must have the `sso_account_id` key - if (sourceProfileExistanceTest.sso_account_id) { - return new AWS.SsoCredentials({ profile: sourceProfile }); - } - - return new PatchedSharedIniFileCredentials( - (AWS as any).util.merge(this.options || {}, { - profile: sourceProfile, - preferStaticCredentials: true, - }), - ); - - } - - // the aws-sdk for js does not support 'credential_source' (https://github.com/aws/aws-sdk-js/issues/1916) - // so unfortunately we need to implement this ourselves. - private credentialSourceCredentials(sourceCredential: string) { - // see https://docs.aws.amazon.com/credref/latest/refdocs/setting-global-credential_source.html - switch (sourceCredential) { - case 'Environment': { - return new AWS.EnvironmentCredentials('AWS'); - } - case 'Ec2InstanceMetadata': { - return new AWS.EC2MetadataCredentials(); - } - case 'EcsContainer': { - return new AWS.ECSCredentials(); - } - default: { - throw new Error(`credential_source ${sourceCredential} in profile ${this.profile} is unsupported. choose one of [Environment, Ec2InstanceMetadata, EcsContainer]`); - } - } - - } -} diff --git a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts index de434f2f95957..58bb2138ef9ba 100644 --- a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts +++ b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts @@ -1,13 +1,15 @@ -import * as child_process from 'child_process'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import * as AWS from 'aws-sdk'; -import * as fs from 'fs-extra'; +import { createCredentialChain, fromEnv, fromIni, fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import { MetadataService } from '@aws-sdk/ec2-metadata-service'; +import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; +import { loadSharedConfigFiles } from '@smithy/shared-ini-file-loader'; +import { AwsCredentialIdentityProvider, Logger } from '@smithy/types'; import * as promptly from 'promptly'; -import { debug } from './_env'; -import { PatchedSharedIniFileCredentials } from './aws-sdk-inifile'; -import { SharedIniFile } from './sdk_ini_file'; +import type { SdkHttpOptions } from './sdk-provider'; +import { readIfPossible } from './util'; +import { debug } from '../../logging'; + +const DEFAULT_CONNECTION_TIMEOUT = 10000; +const DEFAULT_TIMEOUT = 300000; /** * Behaviors to match AWS CLI @@ -20,134 +22,119 @@ import { SharedIniFile } from './sdk_ini_file'; export class AwsCliCompatible { /** * Build an AWS CLI-compatible credential chain provider - * - * This is similar to the default credential provider chain created by the SDK - * except: - * - * 1. Accepts profile argument in the constructor (the SDK must have it prepopulated - * in the environment). - * 2. Conditionally checks EC2 credentials, because checking for EC2 - * credentials on a non-EC2 machine may lead to long delays (in the best case) - * or an exception (in the worst case). - * 3. Respects $AWS_SHARED_CREDENTIALS_FILE. - * 4. Respects $AWS_DEFAULT_PROFILE in addition to $AWS_PROFILE. */ - public static async credentialChain(options: CredentialChainOptions = {}) { - // Force reading the `config` file if it exists by setting the appropriate - // environment variable. - await forceSdkToReadConfigIfPresent(); - - // To match AWS CLI behavior, if a profile is explicitly given using --profile, - // we use that to the exclusion of everything else (note: this does not apply - // to AWS_PROFILE, environment credentials still take precedence over AWS_PROFILE) + public static async credentialChainBuilder( + options: CredentialChainOptions = {}, + ): Promise { + /** + * The previous implementation matched AWS CLI behavior: + * + * If a profile is explicitly set using `--profile`, + * we use that to the exclusion of everything else. + * + * Note: this does not apply to AWS_PROFILE, + * environment credentials still take precedence over AWS_PROFILE + */ if (options.profile) { - return new AWS.CredentialProviderChain(iniFileCredentialFactories(options.profile, options.httpOptions)); - } - - const implicitProfile = process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; - - const sources = [ - () => new AWS.EnvironmentCredentials('AWS'), - () => new AWS.EnvironmentCredentials('AMAZON'), - ...iniFileCredentialFactories(implicitProfile, options.httpOptions), - ]; - - if (options.containerCreds ?? hasEcsCredentials()) { - sources.push(() => new AWS.ECSCredentials()); - } else if (hasWebIdentityCredentials()) { - // else if: we have found WebIdentityCredentials as provided by EKS ServiceAccounts - sources.push(() => new AWS.TokenFileWebIdentityCredentials()); - } else if (options.ec2instance ?? await isEc2Instance()) { - // else if: don't get EC2 creds if we should have gotten ECS or EKS creds - // ECS and EKS instances also run on EC2 boxes but the creds represent something different. - // Same behavior as upstream code. - sources.push(() => new AWS.EC2MetadataCredentials()); + return fromIni({ + profile: options.profile, + ignoreCache: true, + mfaCodeProvider: tokenCodeFn, + clientConfig: { + requestHandler: AwsCliCompatible.requestHandlerBuilder(options.httpOptions), + customUserAgent: 'aws-cdk', + logger: options.logger, + }, + logger: options.logger, + }); } - return new AWS.CredentialProviderChain(sources); + const profile = options.profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE; + + /** + * Env AWS - EnvironmentCredentials with string AWS + * Env Amazon - EnvironmentCredentials with string AMAZON + * Profile Credentials - PatchedSharedIniFileCredentials with implicit profile, credentials file, http options, and token fn + * SSO with implicit profile only + * SharedIniFileCredentials with implicit profile and preferStaticCredentials true (profile with source_profile) + * Shared Credential file that points to Environment Credentials with AWS prefix + * Shared Credential file that points to EC2 Metadata + * Shared Credential file that points to ECS Credentials + * SSO Credentials - SsoCredentials with implicit profile and http options + * ProcessCredentials with implicit profile + * ECS Credentials - ECSCredentials with no input OR Web Identity - TokenFileWebIdentityCredentials with no input OR EC2 Metadata - EC2MetadataCredentials with no input + * + * These translate to: + * fromEnv() + * fromSSO()/fromIni() + * fromProcess() + * fromContainerMetadata() + * fromTokenFile() + * fromInstanceMetadata() + */ + const nodeProviderChain = fromNodeProviderChain({ + profile: profile, + clientConfig: { + requestHandler: AwsCliCompatible.requestHandlerBuilder(options.httpOptions), + customUserAgent: 'aws-cdk', + logger: options.logger, + }, + logger: options.logger, + ignoreCache: true, + }); - function profileCredentials(profileName: string) { - return new PatchedSharedIniFileCredentials({ - profile: profileName, - filename: credentialsFileName(), - httpOptions: options.httpOptions, - tokenCodeFn, - }); - } + return shouldPrioritizeEnv() + ? createCredentialChain(fromEnv(), nodeProviderChain).expireAfter(60 * 60_000) + : nodeProviderChain; + } - function iniFileCredentialFactories(theProfile: string, theHttpOptions?: AWS.HTTPOptions) { - return [ - () => profileCredentials(theProfile), - () => new AWS.SsoCredentials({ - profile: theProfile, - httpOptions: theHttpOptions, - }), - () => new AWS.ProcessCredentials({ profile: theProfile }), - ]; - } + public static requestHandlerBuilder(options: SdkHttpOptions = {}): NodeHttpHandlerOptions { + const config: NodeHttpHandlerOptions = { + connectionTimeout: DEFAULT_CONNECTION_TIMEOUT, + requestTimeout: DEFAULT_TIMEOUT, + httpsAgent: { + ca: tryGetCACert(options.caBundlePath), + localAddress: options.proxyAddress, + }, + httpAgent: { + localAddress: options.proxyAddress, + }, + }; + return config; } /** - * Return the default region in a CLI-compatible way + * Attempts to get the region from a number of sources and falls back to us-east-1 if no region can be found, + * as is done in the AWS CLI. * - * Mostly copied from node_loader.js, but with the following differences to make it - * AWS CLI compatible: + * The order of priority is the following: * - * 1. Takes a profile name as an argument (instead of forcing it to be taken from $AWS_PROFILE). - * This requires having made a copy of the SDK's `SharedIniFile` (the original - * does not take an argument). - * 2. $AWS_DEFAULT_PROFILE and $AWS_DEFAULT_REGION are also respected. - * - * Lambda and CodeBuild set the $AWS_REGION variable. + * 1. Environment variables specifying region, with both an AWS prefix and AMAZON prefix + * to maintain backwards compatibility, and without `DEFAULT` in the name because + * Lambda and CodeBuild set the $AWS_REGION variable. + * 2. Regions listed in the Shared Ini Files - First checking for the profile provided + * and then checking for the default profile. + * 3. IMDS instance identity region from the Metadata Service. + * 4. us-east-1 */ - public static async region(options: RegionOptions = {}): Promise { - const profile = options.profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; - - // Defaults inside constructor - const toCheck = [ - { filename: credentialsFileName(), profile }, - { isConfig: true, filename: configFileName(), profile }, - { isConfig: true, filename: configFileName(), profile: 'default' }, - ]; - - let region = process.env.AWS_REGION || process.env.AMAZON_REGION || - process.env.AWS_DEFAULT_REGION || process.env.AMAZON_DEFAULT_REGION; - - while (!region && toCheck.length > 0) { - const opts = toCheck.shift()!; - if (await fs.pathExists(opts.filename)) { - const configFile = new SharedIniFile(opts); - const section = await configFile.getProfile(opts.profile); - region = section?.region; - } - } - - if (!region && (options.ec2instance ?? await isEc2Instance())) { - debug('Looking up AWS region in the EC2 Instance Metadata Service (IMDS).'); - const imdsOptions = { - httpOptions: { timeout: 1000, connectTimeout: 1000 }, maxRetries: 2, - }; - const metadataService = new AWS.MetadataService(imdsOptions); - - let token; - try { - token = await getImdsV2Token(metadataService); - } catch (e) { - debug(`No IMDSv2 token: ${e}`); - } - - try { - region = await getRegionFromImds(metadataService, token); - debug(`AWS region from IMDS: ${region}`); - } catch (e) { - debug(`Unable to retrieve AWS region from IMDS: ${e}`); - } - } + public static async region(maybeProfile?: string): Promise { + const defaultRegion = 'us-east-1'; + const profile = maybeProfile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; + + const region = + process.env.AWS_REGION || + process.env.AMAZON_REGION || + process.env.AWS_DEFAULT_REGION || + process.env.AMAZON_DEFAULT_REGION || + (await getRegionFromIni(profile)) || + (await regionFromMetadataService()); if (!region) { const usedProfile = !profile ? '' : ` (profile: "${profile}")`; - region = 'us-east-1'; // This is what the AWS CLI does - debug(`Unable to determine AWS region from environment or AWS configuration${usedProfile}, defaulting to '${region}'`); + debug( + `Unable to determine AWS region from environment or AWS configuration${usedProfile}, defaulting to '${defaultRegion}'`, + ); + return defaultRegion; } return region; @@ -155,177 +142,85 @@ export class AwsCliCompatible { } /** - * Return whether it looks like we'll have ECS credentials available + * Looks up the region of the provided profile. If no region is present, + * it will attempt to lookup the default region. + * @param profile The profile to use to lookup the region + * @returns The region for the profile or default profile, if present. Otherwise returns undefined. */ -function hasEcsCredentials(): boolean { - return (AWS.ECSCredentials.prototype as any).isConfiguredForEcsCredentials(); +async function getRegionFromIni(profile: string): Promise { + const sharedFiles = await loadSharedConfigFiles({ ignoreCache: true }); + return sharedFiles?.configFile?.[profile]?.region || sharedFiles?.configFile?.default?.region; } -/** - * Return whether it looks like we'll have WebIdentityCredentials (that's what EKS uses) available - * No check like hasEcsCredentials available, so have to implement our own. - * @see https://github.com/aws/aws-sdk-js/blob/3ccfd94da07234ae87037f55c138392f38b6881d/lib/credentials/token_file_web_identity_credentials.js#L59 - */ -function hasWebIdentityCredentials(): boolean { - return Boolean(process.env.AWS_ROLE_ARN && process.env.AWS_WEB_IDENTITY_TOKEN_FILE); -} - -/** - * Return whether we're on an EC2 instance - */ -async function isEc2Instance() { - if (isEc2InstanceCache === undefined) { - debug("Determining if we're on an EC2 instance."); - let instance = false; - if (process.platform === 'win32') { - // https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/identify_ec2_instances.html - try { - const result = await util.promisify(child_process.exec)('wmic path win32_computersystemproduct get uuid', { encoding: 'utf-8' }); - // output looks like - // UUID - // EC2AE145-D1DC-13B2-94ED-01234ABCDEF - const lines = result.stdout.toString().split('\n'); - instance = lines.some(x => matchesRegex(/^ec2/i, x)); - } catch (e: any) { - // Modern machines may not have wmic.exe installed. No reason to fail, just assume it's not an EC2 instance. - debug(`Checking using WMIC failed, assuming NOT an EC2 instance: ${e.message} (pass --ec2creds to force)`); - instance = false; - } - } else { - // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html - const files: Array<[string, RegExp]> = [ - // This recognizes the Xen hypervisor based instances (pre-5th gen) - ['/sys/hypervisor/uuid', /^ec2/i], - - // This recognizes the new Hypervisor (5th-gen instances and higher) - // Can't use the advertised file '/sys/devices/virtual/dmi/id/product_uuid' because it requires root to read. - // Instead, sys_vendor contains something like 'Amazon EC2'. - ['/sys/devices/virtual/dmi/id/sys_vendor', /ec2/i], - ]; - for (const [file, re] of files) { - if (matchesRegex(re, readIfPossible(file))) { - instance = true; - break; - } - } - } - debug(instance ? 'Looks like an EC2 instance.' : 'Does not look like an EC2 instance.'); - isEc2InstanceCache = instance; +function tryGetCACert(bundlePath?: string) { + const path = bundlePath || caBundlePathFromEnvironment(); + if (path) { + debug('Using CA bundle path: %s', bundlePath); + return readIfPossible(path); } - return isEc2InstanceCache; -} - -let isEc2InstanceCache: boolean | undefined = undefined; - -/** - * Attempts to get a Instance Metadata Service V2 token - */ -async function getImdsV2Token(metadataService: AWS.MetadataService): Promise { - debug('Attempting to retrieve an IMDSv2 token.'); - return new Promise((resolve, reject) => { - metadataService.request( - '/latest/api/token', - { - method: 'PUT', - headers: { 'x-aws-ec2-metadata-token-ttl-seconds': '60' }, - }, - (err: AWS.AWSError, token: string | undefined) => { - if (err) { - reject(err); - } else if (!token) { - reject(new Error('IMDS did not return a token.')); - } else { - resolve(token); - } - }); - }); + return undefined; } /** - * Attempts to get the region from the Instance Metadata Service + * Find and return a CA certificate bundle path to be passed into the SDK. */ -async function getRegionFromImds(metadataService: AWS.MetadataService, token: string | undefined): Promise { - debug('Retrieving the AWS region from the IMDS.'); - let options: { method?: string | undefined; headers?: { [key: string]: string } | undefined } = {}; - if (token) { - options = { headers: { 'x-aws-ec2-metadata-token': token } }; +function caBundlePathFromEnvironment(): string | undefined { + if (process.env.aws_ca_bundle) { + return process.env.aws_ca_bundle; } - return new Promise((resolve, reject) => { - metadataService.request( - '/latest/dynamic/instance-identity/document', - options, - (err: AWS.AWSError, instanceIdentityDocument: string | undefined) => { - if (err) { - reject(err); - } else if (!instanceIdentityDocument) { - reject(new Error('IMDS did not return an Instance Identity Document.')); - } else { - try { - resolve(JSON.parse(instanceIdentityDocument).region); - } catch (e) { - reject(e); - } - } - }); - }); -} - -function homeDir() { - return process.env.HOME || process.env.USERPROFILE - || (process.env.HOMEPATH ? ((process.env.HOMEDRIVE || 'C:/') + process.env.HOMEPATH) : null) || os.homedir(); -} - -function credentialsFileName() { - return process.env.AWS_SHARED_CREDENTIALS_FILE || path.join(homeDir(), '.aws', 'credentials'); -} - -function configFileName() { - return process.env.AWS_CONFIG_FILE || path.join(homeDir(), '.aws', 'config'); + if (process.env.AWS_CA_BUNDLE) { + return process.env.AWS_CA_BUNDLE; + } + return undefined; } /** - * Force the JS SDK to honor the ~/.aws/config file (and various settings therein) - * - * For example, there is just *NO* way to do AssumeRole credentials as long as AWS_SDK_LOAD_CONFIG is not set, - * or read credentials from that file. + * We used to support both AWS and AMAZON prefixes for these environment variables. * - * The SDK crashes if the variable is set but the file does not exist, so conditionally set it. + * Adding this for backward compatibility. */ -async function forceSdkToReadConfigIfPresent() { - if (await fs.pathExists(configFileName())) { - process.env.AWS_SDK_LOAD_CONFIG = '1'; +function shouldPrioritizeEnv() { + const id = process.env.AWS_ACCESS_KEY_ID || process.env.AMAZON_ACCESS_KEY_ID; + const key = process.env.AWS_SECRET_ACCESS_KEY || process.env.AMAZON_SECRET_ACCESS_KEY; + process.env.AWS_SESSION_TOKEN = process.env.AWS_SESSION_TOKEN || process.env.AMAZON_SESSION_TOKEN; + + if (!!id && !!key) { + process.env.AWS_ACCESS_KEY_ID = id; + process.env.AWS_SECRET_ACCESS_KEY = key; + return true; } -} -function matchesRegex(re: RegExp, s: string | undefined) { - return s !== undefined && re.exec(s) !== null; + return false; } /** - * Read a file if it exists, or return undefined + * The MetadataService class will attempt to fetch the instance identity document from + * IMDSv2 first, and then will attempt v1 as a fallback. * - * Not async because it is used in the constructor + * If this fails, we will use us-east-1 as the region so no error should be thrown. + * @returns The region for the instance identity */ -function readIfPossible(filename: string): string | undefined { +async function regionFromMetadataService() { + debug('Looking up AWS region in the EC2 Instance Metadata Service (IMDS).'); try { - if (!fs.pathExistsSync(filename)) { return undefined; } - return fs.readFileSync(filename, { encoding: 'utf-8' }); - } catch (e: any) { - debug(e); - return undefined; + const metadataService = new MetadataService({ + httpOptions: { + timeout: 1000, + }, + }); + + await metadataService.fetchMetadataToken(); + const document = await metadataService.request('/latest/dynamic/instance-identity/document', {}); + return JSON.parse(document).region; + } catch (e) { + debug(`Unable to retrieve AWS region from IMDS: ${e}`); } } export interface CredentialChainOptions { readonly profile?: string; - readonly ec2instance?: boolean; - readonly containerCreds?: boolean; - readonly httpOptions?: AWS.HTTPOptions; -} - -export interface RegionOptions { - readonly profile?: string; - readonly ec2instance?: boolean; + readonly httpOptions?: SdkHttpOptions; + readonly logger?: Logger; } /** @@ -333,7 +228,7 @@ export interface RegionOptions { * * Result is send to callback function for SDK to authorize the request */ -async function tokenCodeFn(serialArn: string, cb: (err?: Error, token?: string) => void): Promise { +async function tokenCodeFn(serialArn: string): Promise { debug('Require MFA token for serial ARN', serialArn); try { const token: string = await promptly.prompt(`MFA token for ${serialArn}: `, { @@ -341,9 +236,11 @@ async function tokenCodeFn(serialArn: string, cb: (err?: Error, token?: string) default: '', }); debug('Successfully got MFA token from user'); - cb(undefined, token); + return token; } catch (err: any) { debug('Failed to get MFA token', err); - cb(err); + const e = new Error(`Error fetching MFA token: ${err.message ?? err}`); + e.name = 'SharedIniFileCredentialsProviderFailure'; + throw e; } } diff --git a/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts index 640e8cf4b462f..983377e6512ea 100644 --- a/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts +++ b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts @@ -1,7 +1,6 @@ -import { debug } from './_env'; -import { Mode } from './credentials'; -import { warning } from '../../logging'; -import { CredentialProviderSource, PluginHost } from '../plugin'; +import type { AwsCredentialIdentity } from '@smithy/types'; +import { debug, warning } from '../../logging'; +import { CredentialProviderSource, Mode, PluginHost } from '../plugin'; /** * Cache for credential providers. @@ -16,7 +15,7 @@ import { CredentialProviderSource, PluginHost } from '../plugin'; * for the given account. */ export class CredentialPlugins { - private readonly cache: {[key: string]: PluginCredentials | undefined} = {}; + private readonly cache: { [key: string]: PluginCredentials | undefined } = {}; public async fetchCredentialsFor(awsAccountId: string, mode: Mode): Promise { const key = `${awsAccountId}-${mode}`; @@ -27,7 +26,7 @@ export class CredentialPlugins { } public get availablePluginNames(): string[] { - return PluginHost.instance.credentialProviderSources.map(s => s.name); + return PluginHost.instance.credentialProviderSources.map((s) => s.name); } private async lookupCredentials(awsAccountId: string, mode: Mode): Promise { @@ -56,13 +55,17 @@ export class CredentialPlugins { warning(`Uncaught exception in ${source.name}: ${e.message}`); canProvide = false; } - if (!canProvide) { continue; } + if (!canProvide) { + continue; + } debug(`Using ${source.name} credentials for account ${awsAccountId}`); const providerOrCreds = await source.getProvider(awsAccountId, mode); // Backwards compatibility: if the plugin returns a ProviderChain, resolve that chain. // Otherwise it must have returned credentials. - const credentials = (providerOrCreds as any).resolvePromise ? await (providerOrCreds as any).resolvePromise() : providerOrCreds; + const credentials = (providerOrCreds as any).resolvePromise + ? await (providerOrCreds as any).resolvePromise() + : providerOrCreds; return { credentials, pluginName: source.name }; } @@ -71,6 +74,6 @@ export class CredentialPlugins { } export interface PluginCredentials { - readonly credentials: AWS.Credentials; + readonly credentials: AwsCredentialIdentity; readonly pluginName: string; } diff --git a/packages/aws-cdk/lib/api/aws-auth/credentials.ts b/packages/aws-cdk/lib/api/aws-auth/credentials.ts deleted file mode 100644 index b93cd43550a5c..0000000000000 --- a/packages/aws-cdk/lib/api/aws-auth/credentials.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Re-export this here because it used to be here and I don't want -// to change imports too much. -export { Mode } from '../plugin'; \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/index.ts b/packages/aws-cdk/lib/api/aws-auth/index.ts index cade9b2eada26..a913c20f425c4 100644 --- a/packages/aws-cdk/lib/api/aws-auth/index.ts +++ b/packages/aws-cdk/lib/api/aws-auth/index.ts @@ -1,3 +1,2 @@ export * from './sdk'; -export * from './sdk-provider'; -export * from './credentials'; \ No newline at end of file +export * from './sdk-provider'; \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 6d8d341d7a8c5..196b192ed4040 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -1,29 +1,19 @@ import * as os from 'os'; -import * as path from 'path'; -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import * as cxapi from '@aws-cdk/cx-api'; -import * as AWS from 'aws-sdk'; -import type { ConfigurationOptions } from 'aws-sdk/lib/config-base'; -import * as fs from 'fs-extra'; -import { debug, warning } from './_env'; +import { ContextLookupRoleOptions } from '@aws-cdk/cloud-assembly-schema'; +import { Environment, EnvironmentUtils, UNKNOWN_ACCOUNT, UNKNOWN_REGION } from '@aws-cdk/cx-api'; +import { AssumeRoleCommandInput } from '@aws-sdk/client-sts'; +import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; +import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; +import { AwsCredentialIdentity, AwsCredentialIdentityProvider, Logger } from '@smithy/types'; import { AwsCliCompatible } from './awscli-compatible'; import { cached } from './cached'; import { CredentialPlugins } from './credential-plugins'; -import { Mode } from './credentials'; -import { ISDK, SDK, isUnrecoverableAwsError } from './sdk'; -import { rootDir } from '../../util/directories'; +import { SDK } from './sdk'; +import { debug, warning } from '../../logging'; import { traceMethods } from '../../util/tracing'; +import { Mode } from '../plugin'; -// Partial because `RoleSessionName` is required in STS, but we have a default value for it. -export type AssumeRoleAdditionalOptions = Partial< -// cloud-assembly-schema validates that `ExternalId` and `RoleArn` are not configured -Omit ->; - -// Some configuration that can only be achieved by setting -// environment variables. -process.env.AWS_STS_REGIONAL_ENDPOINTS = 'regional'; -process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = '1'; +export type AssumeRoleAdditionalOptions = Partial>; /** * Options for the default SDK provider @@ -37,23 +27,14 @@ export interface SdkProviderOptions { readonly profile?: string; /** - * Whether we should check for EC2 credentials - * - * @default - Autodetect - */ - readonly ec2creds?: boolean; - - /** - * Whether we should check for container credentials - * - * @default - Autodetect + * HTTP options for SDK */ - readonly containerCreds?: boolean; + readonly httpOptions?: SdkHttpOptions; /** - * HTTP options for SDK + * The logger for sdk calls. */ - readonly httpOptions?: SdkHttpOptions; + readonly logger?: Logger; } /** @@ -73,13 +54,6 @@ export interface SdkHttpOptions { * @default No certificate bundle */ readonly caBundlePath?: string; - - /** - * The custom user agent to use. - * - * @default - / - */ - readonly userAgent?: string; } const CACHED_ACCOUNT = Symbol('cached_account'); @@ -101,7 +75,7 @@ export interface SdkForEnvironment { /** * The SDK for the given environment */ - readonly sdk: ISDK; + readonly sdk: SDK; /** * Whether or not the assume role was successful. @@ -144,32 +118,27 @@ export class SdkProvider { * class `AwsCliCompatible` for the details. */ public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions = {}) { - const sdkOptions = parseHttpOptions(options.httpOptions ?? {}); - - const chain = await AwsCliCompatible.credentialChain({ - profile: options.profile, - ec2instance: options.ec2creds, - containerCreds: options.containerCreds, - httpOptions: sdkOptions.httpOptions, - }); - const region = await AwsCliCompatible.region({ + const credentialProvider = await AwsCliCompatible.credentialChainBuilder({ profile: options.profile, - ec2instance: options.ec2creds, + httpOptions: options.httpOptions, + logger: options.logger, }); - return new SdkProvider(chain, region, sdkOptions); + const region = await AwsCliCompatible.region(options.profile); + const requestHandler = AwsCliCompatible.requestHandlerBuilder(options.httpOptions); + return new SdkProvider(credentialProvider, region, requestHandler); } private readonly plugins = new CredentialPlugins(); public constructor( - private readonly defaultChain: AWS.CredentialProviderChain, + private readonly defaultCredentialProvider: AwsCredentialIdentityProvider, /** * Default region */ public readonly defaultRegion: string, - private readonly sdkOptions: ConfigurationOptions = {}) { - } + private readonly requestHandler: NodeHttpHandlerOptions = {}, + ) {} /** * Return an SDK which can do operations in the given environment @@ -177,7 +146,7 @@ export class SdkProvider { * The `environment` parameter is resolved first (see `resolveEnvironment()`). */ public async forEnvironment( - environment: cxapi.Environment, + environment: Environment, mode: Mode, options?: CredentialsOptions, quiet = false, @@ -187,32 +156,39 @@ export class SdkProvider { const baseCreds = await this.obtainBaseCredentials(env.account, mode); // At this point, we need at least SOME credentials - if (baseCreds.source === 'none') { throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); } + if (baseCreds.source === 'none') { + throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); + } // Simple case is if we don't need to "assumeRole" here. If so, we must now have credentials for the right // account. if (options?.assumeRoleArn === undefined) { - if (baseCreds.source === 'incorrectDefault') { throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); } + if (baseCreds.source === 'incorrectDefault') { + throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); + } // Our current credentials must be valid and not expired. Confirm that before we get into doing // actual CloudFormation calls, which might take a long time to hang. - const sdk = new SDK(baseCreds.credentials, env.region, this.sdkOptions); + const sdk = new SDK(baseCreds.credentials, env.region, this.requestHandler); await sdk.validateCredentials(); return { sdk, didAssumeRole: false }; } // We will proceed to AssumeRole using whatever we've been given. - const sdk = await this.withAssumedRole(baseCreds, options.assumeRoleArn, - options.assumeRoleExternalId, options.assumeRoleAdditionalOptions, env.region); + const sdk = await this.withAssumedRole( + baseCreds, + options.assumeRoleArn, + options.assumeRoleExternalId, + options.assumeRoleAdditionalOptions, + env.region, + ); - // Exercise the AssumeRoleCredentialsProvider we've gotten at least once so - // we can determine whether the AssumeRole call succeeds or not. try { - await sdk.forceCredentialRetrieval(); + // Force retrieval here return { sdk, didAssumeRole: true }; - } catch (e: any) { - if (isUnrecoverableAwsError(e)) { - throw e; + } catch (err: any) { + if (err.name === 'ExpiredToken') { + throw err; } // AssumeRole failed. Proceed and warn *if and only if* the baseCredentials were already for the right account @@ -220,13 +196,18 @@ export class SdkProvider { // feed the CLI credentials which are sufficient by themselves. Prefer to assume the correct role if we can, // but if we can't then let's just try with available credentials anyway. if (baseCreds.source === 'correctDefault' || baseCreds.source === 'plugin') { - debug(e.message); + debug(err.message); const logger = quiet ? debug : warning; - logger(`${fmtObtainedCredentials(baseCreds)} could not be used to assume '${options.assumeRoleArn}', but are for the right account. Proceeding anyway.`); - return { sdk: new SDK(baseCreds.credentials, env.region, this.sdkOptions), didAssumeRole: false }; + logger( + `${fmtObtainedCredentials(baseCreds)} could not be used to assume '${options.assumeRoleArn}', but are for the right account. Proceeding anyway.`, + ); + return { + sdk: new SDK(baseCreds.credentials, env.region, this.requestHandler), + didAssumeRole: false, + }; } - throw e; + throw err; } } @@ -235,11 +216,13 @@ export class SdkProvider { * * Returns `undefined` if there are no base credentials. */ - public async baseCredentialsPartition(environment: cxapi.Environment, mode: Mode): Promise { + public async baseCredentialsPartition(environment: Environment, mode: Mode): Promise { const env = await this.resolveEnvironment(environment); const baseCreds = await this.obtainBaseCredentials(env.account, mode); - if (baseCreds.source === 'none') { return undefined; } - return (await new SDK(baseCreds.credentials, env.region, this.sdkOptions).currentAccount()).partition; + if (baseCreds.source === 'none') { + return undefined; + } + return (await new SDK(baseCreds.credentials, env.region, this.requestHandler).currentAccount()).partition; } /** @@ -252,18 +235,20 @@ export class SdkProvider { * It is an error if `UNKNOWN_ACCOUNT` is used but the user hasn't configured * any SDK credentials. */ - public async resolveEnvironment(env: cxapi.Environment): Promise { - const region = env.region !== cxapi.UNKNOWN_REGION ? env.region : this.defaultRegion; - const account = env.account !== cxapi.UNKNOWN_ACCOUNT ? env.account : (await this.defaultAccount())?.accountId; + public async resolveEnvironment(env: Environment): Promise { + const region = env.region !== UNKNOWN_REGION ? env.region : this.defaultRegion; + const account = env.account !== UNKNOWN_ACCOUNT ? env.account : (await this.defaultAccount())?.accountId; if (!account) { - throw new Error('Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment'); + throw new Error( + 'Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment', + ); } return { region, account, - name: cxapi.EnvironmentUtils.format(account, region), + name: EnvironmentUtils.format(account, region), }; } @@ -280,27 +265,28 @@ export class SdkProvider { * * Uses a cache to avoid STS calls if we don't need 'em. */ - public defaultAccount(): Promise { + public async defaultAccount(): Promise { return cached(this, CACHED_ACCOUNT, async () => { try { - const creds = await this.defaultCredentials(); - - const accessKeyId = creds.accessKeyId; + const credentials = await this.defaultCredentials(); + const accessKeyId = credentials.accessKeyId; if (!accessKeyId) { throw new Error('Unable to resolve AWS credentials (setup with "aws configure")'); } - return await new SDK(creds, this.defaultRegion, this.sdkOptions).currentAccount(); + return await new SDK(credentials, this.defaultRegion, this.requestHandler).currentAccount(); } catch (e: any) { // Treat 'ExpiredToken' specially. This is a common situation that people may find themselves in, and // they are complaining about if we fail 'cdk synth' on them. We loudly complain in order to show that // the current situation is probably undesirable, but we don't fail. - if (e.code === 'ExpiredToken') { - warning('There are expired AWS credentials in your environment. The CDK app will synth without current account information.'); + if (e.name === 'ExpiredToken') { + warning( + 'There are expired AWS credentials in your environment. The CDK app will synth without current account information.', + ); return undefined; } - debug(`Unable to determine the default AWS account (${e.code}): ${e.message}`); + debug(`Unable to determine the default AWS account (${e.name}): ${e.message}`); return undefined; } }); @@ -319,7 +305,10 @@ export class SdkProvider { // First try 'current' credentials const defaultAccountId = (await this.defaultAccount())?.accountId; if (defaultAccountId === accountId) { - return { source: 'correctDefault', credentials: await this.defaultCredentials() }; + return { + source: 'correctDefault', + credentials: await this.defaultCredentials(), + }; } // Then try the plugins @@ -348,10 +337,10 @@ export class SdkProvider { /** * Resolve the default chain to the first set of credentials that is available */ - private defaultCredentials(): Promise { - return cached(this, CACHED_DEFAULT_CREDENTIALS, () => { + private async defaultCredentials(): Promise { + return cached(this, CACHED_DEFAULT_CREDENTIALS, async () => { debug('Resolving default credentials'); - return this.defaultChain.resolvePromise(); + return this.defaultCredentialProvider(); }); } @@ -363,34 +352,53 @@ export class SdkProvider { * otherwise it will be the current credentials. */ private async withAssumedRole( - masterCredentials: Exclude, + mainCredentials: Exclude, roleArn: string, - externalId: string | undefined, - additionalOptions: AssumeRoleAdditionalOptions | undefined, - region: string | undefined) { + externalId?: string, + additionalOptions?: AssumeRoleAdditionalOptions, + region?: string, + ): Promise { debug(`Assuming role '${roleArn}'.`); region = region ?? this.defaultRegion; - const creds = new AWS.ChainableTemporaryCredentials({ - params: { - RoleArn: roleArn, - ExternalId: externalId, - RoleSessionName: `aws-cdk-${safeUsername()}`, - TransitiveTagKeys: additionalOptions?.Tags - ? additionalOptions.Tags.map((t) => t.Key) - : undefined, - ...(additionalOptions ?? {}), - }, - stsConfig: { - region, - ...this.sdkOptions, - }, - masterCredentials: masterCredentials.credentials, - }); - return new SDK(creds, region, this.sdkOptions, { - assumeRoleCredentialsSourceDescription: fmtObtainedCredentials(masterCredentials), - }); + const sourceDescription = fmtObtainedCredentials(mainCredentials); + + try { + const credentials = await fromTemporaryCredentials({ + masterCredentials: mainCredentials.credentials, + params: { + RoleArn: roleArn, + ExternalId: externalId, + RoleSessionName: `aws-cdk-${safeUsername()}`, + ...additionalOptions, + TransitiveTagKeys: additionalOptions?.Tags ? additionalOptions.Tags.map((t) => t.Key!) : undefined, + }, + clientConfig: { + region, + ...this.requestHandler, + }, + })(); + + return new SDK(credentials, region, this.requestHandler, { + assumeRoleCredentialsSourceDescription: fmtObtainedCredentials(mainCredentials), + }); + } catch (err: any) { + if (err.name === 'ExpiredToken') { + throw err; + } + + debug(`Assuming role failed: ${err.message}`); + throw new Error( + [ + 'Could not assume role in target account', + ...(sourceDescription ? [`using ${sourceDescription}`] : []), + err.message, + ". Please make sure that this role exists in the account. If it doesn't exist, (re)-bootstrap the environment " + + "with the right '--trust', using the latest version of the CDK CLI.", + ].join(' '), + ); + } } } @@ -412,93 +420,6 @@ export interface Account { readonly partition: string; } -const DEFAULT_CONNECTION_TIMEOUT = 10000; -const DEFAULT_TIMEOUT = 300000; - -/** - * Get HTTP options for the SDK - * - * Read from user input or environment variables. - * - * Returns a complete `ConfigurationOptions` object because that's where - * `customUserAgent` lives, but `httpOptions` is the most important attribute. - */ -function parseHttpOptions(options: SdkHttpOptions) { - const config: ConfigurationOptions = {}; - config.httpOptions = {}; - - config.httpOptions.connectTimeout = DEFAULT_CONNECTION_TIMEOUT; - config.httpOptions.timeout = DEFAULT_TIMEOUT; - - let userAgent = options.userAgent; - if (userAgent == null) { - userAgent = defaultCliUserAgent(); - } - config.customUserAgent = userAgent; - - const caBundlePath = options.caBundlePath || caBundlePathFromEnvironment(); - if (caBundlePath) { - debug('Using CA bundle path: %s', caBundlePath); - (config.httpOptions as any).ca = readIfPossible(caBundlePath); - } - - if (options.proxyAddress) { - debug('Proxy server from command-line arguments: %s', options.proxyAddress); - } - - // Configure the proxy agent. By default, this will use HTTPS?_PROXY and - // NO_PROXY environment variables to determine which proxy to use for each - // request. - // - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { ProxyAgent } = require('proxy-agent'); - config.httpOptions.agent = new ProxyAgent(options.proxyAddress); - - return config; -} - -/** - * Find the package.json from the main toolkit. - * - * If we can't read it for some reason, try to do something reasonable anyway. - * Fall back to argv[1], or a standard string if that is undefined for some reason. - */ -export function defaultCliUserAgent() { - const root = rootDir(false); - const pkg = JSON.parse((root ? readIfPossible(path.join(root, 'package.json')) : undefined) ?? '{}'); - const name = pkg.name ?? path.basename(process.argv[1] ?? 'cdk-cli'); - const version = pkg.version ?? ''; - return `${name}/${version}`; -} - -/** - * Find and return a CA certificate bundle path to be passed into the SDK. - */ -function caBundlePathFromEnvironment(): string | undefined { - if (process.env.aws_ca_bundle) { - return process.env.aws_ca_bundle; - } - if (process.env.AWS_CA_BUNDLE) { - return process.env.AWS_CA_BUNDLE; - } - return undefined; -} - -/** - * Read a file if it exists, or return undefined - * - * Not async because it is used in the constructor - */ -function readIfPossible(filename: string): string | undefined { - try { - if (!fs.pathExistsSync(filename)) { return undefined; } - return fs.readFileSync(filename, { encoding: 'utf-8' }); - } catch (e: any) { - debug(e); - return undefined; - } -} - /** * Return the username with characters invalid for a RoleSessionName removed * @@ -536,9 +457,14 @@ export interface CredentialsOptions { * Result of obtaining base credentials */ type ObtainBaseCredentialsResult = - { source: 'correctDefault'; credentials: AWS.Credentials } - | { source: 'plugin'; pluginName: string; credentials: AWS.Credentials } - | { source: 'incorrectDefault'; credentials: AWS.Credentials; accountId: string; unusedPlugins: string[] } + | { source: 'correctDefault'; credentials: AwsCredentialIdentity } + | { source: 'plugin'; pluginName: string; credentials: AwsCredentialIdentity } + | { + source: 'incorrectDefault'; + credentials: AwsCredentialIdentity; + accountId: string; + unusedPlugins: string[]; + } | { source: 'none'; unusedPlugins: string[] }; /** @@ -549,7 +475,12 @@ type ObtainBaseCredentialsResult = * - No credentials are available at all * - Default credentials are for the wrong account */ -function fmtObtainCredentialsError(targetAccountId: string, obtainResult: ObtainBaseCredentialsResult & { source: 'none' | 'incorrectDefault' }): string { +function fmtObtainCredentialsError( + targetAccountId: string, + obtainResult: ObtainBaseCredentialsResult & { + source: 'none' | 'incorrectDefault'; + }, +): string { const msg = [`Need to perform AWS calls for account ${targetAccountId}`]; switch (obtainResult.source) { case 'incorrectDefault': @@ -573,8 +504,7 @@ function fmtObtainCredentialsError(targetAccountId: string, obtainResult: Obtain * - Default credentials for the wrong account * - Credentials returned from a plugin */ -function fmtObtainedCredentials( - obtainResult: Exclude): string { +function fmtObtainedCredentials(obtainResult: Exclude): string { switch (obtainResult.source) { case 'correctDefault': return 'current credentials'; @@ -597,8 +527,7 @@ function fmtObtainedCredentials( * Instantiate an SDK for context providers. This function ensures that all * lookup assume role options are used when context providers perform lookups. */ -export async function initContextProviderSdk(aws: SdkProvider, options: cxschema.ContextLookupRoleOptions): Promise { - +export async function initContextProviderSdk(aws: SdkProvider, options: ContextLookupRoleOptions): Promise { const account = options.account; const region = options.region; @@ -608,5 +537,5 @@ export async function initContextProviderSdk(aws: SdkProvider, options: cxschema assumeRoleAdditionalOptions: options.assumeRoleAdditionalOptions, }; - return (await aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading, creds)).sdk; -} \ No newline at end of file + return (await aws.forEnvironment(EnvironmentUtils.make(account, region), Mode.ForReading, creds)).sdk; +} diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 6a78965620c01..381d057e343e3 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -1,74 +1,297 @@ -import * as AWS from 'aws-sdk'; -import type { ConfigurationOptions } from 'aws-sdk/lib/config-base'; -import { debug, trace } from './_env'; +import { + GetSchemaCreationStatusCommand, + type GetSchemaCreationStatusCommandInput, + type GetSchemaCreationStatusCommandOutput, + type ListFunctionsCommandInput, + type StartSchemaCreationCommandInput, + type StartSchemaCreationCommandOutput, + type UpdateApiKeyCommandInput, + type UpdateApiKeyCommandOutput, + type UpdateFunctionCommandInput, + type UpdateFunctionCommandOutput, + type UpdateResolverCommandInput, + type UpdateResolverCommandOutput, + StartSchemaCreationCommand, + UpdateApiKeyCommand, + UpdateFunctionCommand, + UpdateResolverCommand, + AppSyncClient, + paginateListFunctions, + FunctionConfiguration, +} from '@aws-sdk/client-appsync'; +import { + CloudFormationClient, + CreateChangeSetCommand, + CreateStackCommand, + type CreateChangeSetCommandInput, + type CreateChangeSetCommandOutput, + type CreateStackCommandInput, + type CreateStackCommandOutput, + type DeleteChangeSetCommandInput, + type DeleteChangeSetCommandOutput, + type DeleteStackCommandInput, + type DeleteStackCommandOutput, + type DescribeChangeSetCommandInput, + type DescribeChangeSetCommandOutput, + type DescribeStackEventsCommandInput, + type DescribeStacksCommandInput, + type DescribeStacksCommandOutput, + type ExecuteChangeSetCommandInput, + type ExecuteChangeSetCommandOutput, + type GetTemplateCommandInput, + type GetTemplateCommandOutput, + type ListExportsCommandInput, + type ListExportsCommandOutput, + type ListStackResourcesCommandInput, + type UpdateStackCommandInput, + type UpdateStackCommandOutput, + type UpdateTerminationProtectionCommandInput, + type UpdateTerminationProtectionCommandOutput, + DeleteChangeSetCommand, + DeleteStackCommand, + DescribeChangeSetCommand, + DescribeStackResourcesCommand, + DescribeStacksCommand, + ExecuteChangeSetCommand, + GetTemplateCommand, + ListExportsCommand, + UpdateStackCommand, + UpdateTerminationProtectionCommand, + type GetTemplateSummaryCommandInput, + type GetTemplateSummaryCommandOutput, + GetTemplateSummaryCommand, + type ListResourceScanResourcesCommandInput, + type ListResourceScanResourcesCommandOutput, + ListResourceScanResourcesCommand, + type DescribeResourceScanCommandInput, + type DescribeResourceScanCommandOutput, + DescribeResourceScanCommand, + DescribeGeneratedTemplateCommand, + type DescribeGeneratedTemplateCommandOutput, + type DescribeGeneratedTemplateCommandInput, + GetGeneratedTemplateCommand, + type GetGeneratedTemplateCommandOutput, + type GetGeneratedTemplateCommandInput, + CreateGeneratedTemplateCommand, + type CreateGeneratedTemplateCommandOutput, + type CreateGeneratedTemplateCommandInput, + DeleteGeneratedTemplateCommand, + type DeleteGeneratedTemplateCommandInput, + type DeleteGeneratedTemplateCommandOutput, + ListResourceScanRelatedResourcesCommand, + type ListResourceScanRelatedResourcesCommandInput, + type ListResourceScanRelatedResourcesCommandOutput, + StartResourceScanCommand, + type StartResourceScanCommandInput, + type StartResourceScanCommandOutput, + ListResourceScansCommand, + type ListResourceScansCommandInput, + type ListResourceScansCommandOutput, + paginateDescribeStackEvents, + paginateListStackResources, + StackEvent, + StackResourceSummary, + RollbackStackCommandInput, + RollbackStackCommandOutput, + RollbackStackCommand, + ContinueUpdateRollbackCommandInput, + ContinueUpdateRollbackCommandOutput, + ContinueUpdateRollbackCommand, +} from '@aws-sdk/client-cloudformation'; +import { + CloudWatchLogsClient, + DescribeLogGroupsCommand, + FilterLogEventsCommandInput, + type DescribeLogGroupsCommandInput, + type DescribeLogGroupsCommandOutput, + FilterLogEventsCommandOutput, + FilterLogEventsCommand, +} from '@aws-sdk/client-cloudwatch-logs'; +import { + CodeBuildClient, + type UpdateProjectCommandInput, + type UpdateProjectCommandOutput, + UpdateProjectCommand, +} from '@aws-sdk/client-codebuild'; +import { + EC2Client, + type DescribeAvailabilityZonesCommandInput, + type DescribeAvailabilityZonesCommandOutput, + type DescribeImagesCommandInput, + type DescribeImagesCommandOutput, + type DescribeInstancesCommandInput, + type DescribeInstancesCommandOutput, + type DescribeRouteTablesCommandInput, + type DescribeRouteTablesCommandOutput, + type DescribeSecurityGroupsCommandInput, + type DescribeSecurityGroupsCommandOutput, + type DescribeSubnetsCommandInput, + type DescribeSubnetsCommandOutput, + type DescribeVpcEndpointServicesCommandInput, + type DescribeVpcEndpointServicesCommandOutput, + type DescribeVpcsCommandInput, + type DescribeVpcsCommandOutput, + type DescribeVpnGatewaysCommandInput, + type DescribeVpnGatewaysCommandOutput, + DescribeAvailabilityZonesCommand, + DescribeImagesCommand, + DescribeInstancesCommand, + DescribeRouteTablesCommand, + DescribeSecurityGroupsCommand, + DescribeSubnetsCommand, + DescribeVpcEndpointServicesCommand, + DescribeVpcsCommand, + DescribeVpnGatewaysCommand, +} from '@aws-sdk/client-ec2'; +import { + type CreateRepositoryCommandInput, + type CreateRepositoryCommandOutput, + type DescribeImagesCommandInput as ECRDescribeImagesCommandInput, + type DescribeImagesCommandOutput as ECRDescribeImagesCommandOutput, + DescribeImagesCommand as ECRDescribeImagesCommand, + type DescribeRepositoriesCommandInput, + type DescribeRepositoriesCommandOutput, + type PutImageScanningConfigurationCommandInput, + type PutImageScanningConfigurationCommandOutput, + type GetAuthorizationTokenCommandInput, + type GetAuthorizationTokenCommandOutput, + ECRClient, + CreateRepositoryCommand, + DescribeRepositoriesCommand, + GetAuthorizationTokenCommand, + PutImageScanningConfigurationCommand, +} from '@aws-sdk/client-ecr'; +import { + ECSClient, + ListClustersCommand, + RegisterTaskDefinitionCommandInput, + type ListClustersCommandInput, + type ListClustersCommandOutput, + type RegisterTaskDefinitionCommandOutput, + RegisterTaskDefinitionCommand, + type UpdateServiceCommandInput, + type UpdateServiceCommandOutput, + UpdateServiceCommand, + DescribeServicesCommandInput, + waitUntilServicesStable, +} from '@aws-sdk/client-ecs'; +import { + ElasticLoadBalancingV2Client, + type DescribeListenersCommandInput, + type DescribeListenersCommandOutput, + type DescribeLoadBalancersCommandInput, + type DescribeLoadBalancersCommandOutput, + type DescribeTagsCommandInput, + type DescribeTagsCommandOutput, + DescribeListenersCommand, + DescribeLoadBalancersCommand, + DescribeTagsCommand, + paginateDescribeListeners, + paginateDescribeLoadBalancers, + LoadBalancer, + Listener, +} from '@aws-sdk/client-elastic-load-balancing-v2'; +import { + IAMClient, + type CreatePolicyCommandInput, + type CreatePolicyCommandOutput, + type GetPolicyCommandInput, + type GetPolicyCommandOutput, + type GetRoleCommandInput, + type GetRoleCommandOutput, + CreatePolicyCommand, + GetPolicyCommand, + GetRoleCommand, +} from '@aws-sdk/client-iam'; +import { + KMSClient, + type DescribeKeyCommandInput, + type DescribeKeyCommandOutput, + type ListAliasesCommandInput, + type ListAliasesCommandOutput, + DescribeKeyCommand, + ListAliasesCommand, +} from '@aws-sdk/client-kms'; +import { + LambdaClient, + type InvokeCommandInput, + type InvokeCommandOutput, + InvokeCommand, + type UpdateFunctionCodeCommandInput, + type UpdateFunctionCodeCommandOutput, + type UpdateFunctionConfigurationCommandInput, + type UpdateFunctionConfigurationCommandOutput, + UpdateFunctionConfigurationCommand, + UpdateFunctionCodeCommand, + type PublishVersionCommandInput, + type PublishVersionCommandOutput, + PublishVersionCommand, + type UpdateAliasCommandInput, + type UpdateAliasCommandOutput, + UpdateAliasCommand, + waitUntilFunctionUpdated, +} from '@aws-sdk/client-lambda'; +import { + Route53Client, + type GetHostedZoneCommandInput, + type GetHostedZoneCommandOutput, + type ListHostedZonesByNameCommandInput, + type ListHostedZonesByNameCommandOutput, + type ListHostedZonesCommandInput, + type ListHostedZonesCommandOutput, + GetHostedZoneCommand, + ListHostedZonesCommand, + ListHostedZonesByNameCommand, +} from '@aws-sdk/client-route-53'; +import { + type CompleteMultipartUploadCommandOutput, + type GetBucketEncryptionCommandInput, + type GetBucketEncryptionCommandOutput, + GetBucketLocationCommand, + type GetBucketLocationCommandInput, + type GetBucketLocationCommandOutput, + type GetObjectCommandInput, + type GetObjectCommandOutput, + ListObjectsV2Command, + type ListObjectsV2CommandInput, + type ListObjectsV2CommandOutput, + type PutObjectCommandInput, + S3Client, + GetBucketEncryptionCommand, + GetObjectCommand, +} from '@aws-sdk/client-s3'; +import { + SecretsManagerClient, + type GetSecretValueCommandInput, + type GetSecretValueCommandOutput, + GetSecretValueCommand, +} from '@aws-sdk/client-secrets-manager'; +import { + SFNClient, + UpdateStateMachineCommandInput, + UpdateStateMachineCommandOutput, + UpdateStateMachineCommand, +} from '@aws-sdk/client-sfn'; +import { + SSMClient, + type GetParameterCommandInput, + type GetParameterCommandOutput, + GetParameterCommand, +} from '@aws-sdk/client-ssm'; +import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; +import { Upload } from '@aws-sdk/lib-storage'; +import { getEndpointFromInstructions } from '@smithy/middleware-endpoint'; +import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; +import { AwsCredentialIdentity, Logger } from '@smithy/types'; +import { ConfiguredRetryStrategy } from '@smithy/util-retry'; +import { WaiterResult } from '@smithy/util-waiter'; import { AccountAccessKeyCache } from './account-cache'; import { cached } from './cached'; import { Account } from './sdk-provider'; +import { defaultCliUserAgent } from './user-agent'; +import { debug } from '../../logging'; import { traceMethods } from '../../util/tracing'; -// We need to map regions to domain suffixes, and the SDK already has a function to do this. -// It's not part of the public API, but it's also unlikely to go away. -// -// Reuse that function, and add a safety check, so we don't accidentally break if they ever -// refactor that away. - -/* eslint-disable @typescript-eslint/no-require-imports */ -const regionUtil = require('aws-sdk/lib/region_config'); -require('aws-sdk/lib/maintenance_mode_message').suppress = true; -/* eslint-enable @typescript-eslint/no-require-imports */ - -if (!regionUtil.getEndpointSuffix) { - throw new Error('This version of AWS SDK for JS does not have the \'getEndpointSuffix\' function!'); -} - -export interface ISDK { - /** - * The region this SDK has been instantiated for - * - * (As distinct from the `defaultRegion()` on SdkProvider which - * represents the region configured in the default config). - */ - readonly currentRegion: string; - - /** - * The Account this SDK has been instantiated for - * - * (As distinct from the `defaultAccount()` on SdkProvider which - * represents the account available by using default credentials). - */ - currentAccount(): Promise; - - getEndpointSuffix(region: string): string; - - /** - * Appends the given string as the extra information to put into the User-Agent header for any requests invoked by this SDK. - * If the string is 'undefined', this method has no effect. - */ - appendCustomUserAgent(userAgentData?: string): void; - - /** - * Removes the given string from the extra User-Agent header data used for requests invoked by this SDK. - */ - removeCustomUserAgent(userAgentData: string): void; - - lambda(): AWS.Lambda; - cloudFormation(): AWS.CloudFormation; - ec2(): AWS.EC2; - iam(): AWS.IAM; - ssm(): AWS.SSM; - s3(): AWS.S3; - route53(): AWS.Route53; - ecr(): AWS.ECR; - ecs(): AWS.ECS; - elbv2(): AWS.ELBv2; - secretsManager(): AWS.SecretsManager; - kms(): AWS.KMS; - stepFunctions(): AWS.StepFunctions; - codeBuild(): AWS.CodeBuild; - cloudWatchLogs(): AWS.CloudWatchLogs; - appsync(): AWS.AppSync; -} - /** * Additional SDK configuration options */ @@ -81,34 +304,186 @@ export interface SdkOptions { readonly assumeRoleCredentialsSourceDescription?: string; } +// TODO: still some cleanup here. Make the pagination functions do all the work here instead of in individual packages. +// Also add async/await. Does that actually matter in this context? Find out and update accordingly. + +// Also add notes to the PR about why you imported everything individually and used 'type' so reviewers don't have to ask. + +export interface ConfigurationOptions { + region: string; + credentials: AwsCredentialIdentity; + requestHandler: NodeHttpHandlerOptions; + retryStrategy: ConfiguredRetryStrategy; + customUserAgent: string; + logger?: Logger; +} + +export interface IAppSyncClient { + getSchemaCreationStatus(input: GetSchemaCreationStatusCommandInput): Promise; + startSchemaCreation(input: StartSchemaCreationCommandInput): Promise; + updateApiKey(input: UpdateApiKeyCommandInput): Promise; + updateFunction(input: UpdateFunctionCommandInput): Promise; + updateResolver(input: UpdateResolverCommandInput): Promise; + // Pagination functions + listFunctions(input: ListFunctionsCommandInput): Promise; +} + +export interface ICloudFormationClient { + continueUpdateRollback(input: ContinueUpdateRollbackCommandInput): Promise; + createChangeSet(input: CreateChangeSetCommandInput): Promise; + createGeneratedTemplate(input: CreateGeneratedTemplateCommandInput): Promise; + createStack(input: CreateStackCommandInput): Promise; + deleteChangeSet(input: DeleteChangeSetCommandInput): Promise; + deleteGeneratedTemplate(input: DeleteGeneratedTemplateCommandInput): Promise; + deleteStack(input: DeleteStackCommandInput): Promise; + describeChangeSet(input: DescribeChangeSetCommandInput): Promise; + describeGeneratedTemplate( + input: DescribeGeneratedTemplateCommandInput, + ): Promise; + describeResourceScan(input: DescribeResourceScanCommandInput): Promise; + describeStacks(input: DescribeStacksCommandInput): Promise; + executeChangeSet(input: ExecuteChangeSetCommandInput): Promise; + getGeneratedTemplate(input: GetGeneratedTemplateCommandInput): Promise; + getTemplate(input: GetTemplateCommandInput): Promise; + getTemplateSummary(input: GetTemplateSummaryCommandInput): Promise; + listExports(input: ListExportsCommandInput): Promise; + listResourceScanRelatedResources( + input: ListResourceScanRelatedResourcesCommandInput, + ): Promise; + listResourceScanResources( + input: ListResourceScanResourcesCommandInput, + ): Promise; + listResourceScans(input?: ListResourceScansCommandInput): Promise; + rollbackStack(input: RollbackStackCommandInput): Promise; + startResourceScan(input: StartResourceScanCommandInput): Promise; + updateStack(input: UpdateStackCommandInput): Promise; + updateTerminationProtection( + input: UpdateTerminationProtectionCommandInput, + ): Promise; + // Pagination functions + describeStackEvents(input: DescribeStackEventsCommandInput): Promise; + listStackResources(input: ListStackResourcesCommandInput): Promise; +} + +export interface ICloudWatchLogsClient { + describeLogGroups(input: DescribeLogGroupsCommandInput): Promise; + filterLogEvents(input: FilterLogEventsCommandInput): Promise; +} + +export interface ICodeBuildClient { + updateProject(input: UpdateProjectCommandInput): Promise; +} +export interface IEC2Client { + describeAvailabilityZones( + input: DescribeAvailabilityZonesCommandInput, + ): Promise; + describeImages(input: DescribeImagesCommandInput): Promise; + describeInstances(input: DescribeInstancesCommandInput): Promise; + describeRouteTables(input: DescribeRouteTablesCommandInput): Promise; + describeSecurityGroups(input: DescribeSecurityGroupsCommandInput): Promise; + describeSubnets(input: DescribeSubnetsCommandInput): Promise; + describeVpcEndpointServices( + input: DescribeVpcEndpointServicesCommandInput, + ): Promise; + describeVpcs(input: DescribeVpcsCommandInput): Promise; + describeVpnGateways(input: DescribeVpnGatewaysCommandInput): Promise; +} + +export interface IECRClient { + createRepository(input: CreateRepositoryCommandInput): Promise; + describeImages(input: ECRDescribeImagesCommandInput): Promise; + describeRepositories(input: DescribeRepositoriesCommandInput): Promise; + getAuthorizationToken(input: GetAuthorizationTokenCommandInput): Promise; + putImageScanningConfiguration( + input: PutImageScanningConfigurationCommandInput, + ): Promise; +} + +export interface IECSClient { + listClusters(input: ListClustersCommandInput): Promise; + registerTaskDefinition(input: RegisterTaskDefinitionCommandInput): Promise; + updateService(input: UpdateServiceCommandInput): Promise; + // Waiters + waitUntilServicesStable(input: DescribeServicesCommandInput): Promise; +} + +export interface IElasticLoadBalancingV2Client { + describeListeners(input: DescribeListenersCommandInput): Promise; + describeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; + describeTags(input: DescribeTagsCommandInput): Promise; + // Pagination + paginateDescribeListeners(input: DescribeListenersCommandInput): Promise; + paginateDescribeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; +} + +export interface IIAMClient { + createPolicy(input: CreatePolicyCommandInput): Promise; + getPolicy(input: GetPolicyCommandInput): Promise; + getRole(input: GetRoleCommandInput): Promise; +} + +export interface IKMSClient { + describeKey(input: DescribeKeyCommandInput): Promise; + listAliases(input: ListAliasesCommandInput): Promise; +} + +export interface ILambdaClient { + invokeCommand(input: InvokeCommandInput): Promise; + publishVersion(input: PublishVersionCommandInput): Promise; + updateAlias(input: UpdateAliasCommandInput): Promise; + updateFunctionCode(input: UpdateFunctionCodeCommandInput): Promise; + updateFunctionConfiguration( + input: UpdateFunctionConfigurationCommandInput, + ): Promise; + // Waiters + waitUntilFunctionUpdated(delaySeconds: number, input: UpdateFunctionConfigurationCommandInput): Promise; +} + +export interface IRoute53Client { + getHostedZone(input: GetHostedZoneCommandInput): Promise; + listHostedZones(input: ListHostedZonesCommandInput): Promise; + listHostedZonesByName(input: ListHostedZonesByNameCommandInput): Promise; +} + +export interface IS3Client { + getBucketEncryption(input: GetBucketEncryptionCommandInput): Promise; + getBucketLocation(input: GetBucketLocationCommandInput): Promise; + getObject(input: GetObjectCommandInput): Promise; + listObjectsV2(input: ListObjectsV2CommandInput): Promise; + upload(input: PutObjectCommandInput): Promise; +} + +export interface ISecretsManagerClient { + getSecretValue(input: GetSecretValueCommandInput): Promise; +} + +export interface ISSMClient { + getParameter(input: GetParameterCommandInput): Promise; +} + +export interface IStepFunctionsClient { + // listStateMachines(input: ListStateMachinesCommandInput): Promise; + updateStateMachine(input: UpdateStateMachineCommandInput): Promise; +} + /** * Base functionality of SDK without credential fetching */ @traceMethods -export class SDK implements ISDK { +export class SDK { private static readonly accountCache = new AccountAccessKeyCache(); public readonly currentRegion: string; - private readonly config: ConfigurationOptions; - - /** - * Default retry options for SDK clients. - */ - private readonly retryOptions = { maxRetries: 6, retryDelayOptions: { base: 300 } }; - - /** - * The more generous retry policy for CloudFormation, which has a 1 TPM limit on certain APIs, - * which are abundantly used for deployment tracking, ... - * - * So we're allowing way more retries, but waiting a bit more. - */ - private readonly cloudFormationRetryOptions = { maxRetries: 10, retryDelayOptions: { base: 1_000 } }; + public readonly config: ConfigurationOptions; /** * STS is used to check credential validity, don't do too many retries. */ - private readonly stsRetryOptions = { maxRetries: 3, retryDelayOptions: { base: 100 } }; + private readonly stsRetryOptions = { + maxRetries: 3, + retryDelayOptions: { base: 100 }, + }; /** * Whether we have proof that the credentials have not expired @@ -120,17 +495,17 @@ export class SDK implements ISDK { private _credentialsValidated = false; constructor( - private readonly _credentials: AWS.Credentials, + private readonly _credentials: AwsCredentialIdentity, region: string, - httpOptions: ConfigurationOptions = {}, - private readonly sdkOptions: SdkOptions = {}) { - + requestHandler: NodeHttpHandlerOptions, + private readonly sdkOptions: SdkOptions = {}, + ) { this.config = { - ...httpOptions, - ...this.retryOptions, - credentials: _credentials, region, - logger: { log: (...messages) => messages.forEach(m => trace('%s', m)) }, + credentials: _credentials, + requestHandler, + retryStrategy: new ConfiguredRetryStrategy(7, (attempt) => attempt ** 300), + customUserAgent: defaultCliUserAgent(), }; this.currentRegion = region; } @@ -141,141 +516,387 @@ export class SDK implements ISDK { } const currentCustomUserAgent = this.config.customUserAgent; - this.config.customUserAgent = currentCustomUserAgent - ? `${currentCustomUserAgent} ${userAgentData}` - : userAgentData; + this.config.customUserAgent = currentCustomUserAgent ? `${currentCustomUserAgent} ${userAgentData}` : userAgentData; } public removeCustomUserAgent(userAgentData: string): void { this.config.customUserAgent = this.config.customUserAgent?.replace(userAgentData, ''); } - public lambda(): AWS.Lambda { - return this.wrapServiceErrorHandling(new AWS.Lambda(this.config)); + public appsync(): IAppSyncClient { + const client = new AppSyncClient(this.config); + return { + getSchemaCreationStatus: ( + input: GetSchemaCreationStatusCommandInput, + ): Promise => client.send(new GetSchemaCreationStatusCommand(input)), + startSchemaCreation: (input: StartSchemaCreationCommandInput): Promise => + client.send(new StartSchemaCreationCommand(input)), + updateApiKey: (input: UpdateApiKeyCommandInput): Promise => + client.send(new UpdateApiKeyCommand(input)), + updateFunction: (input: UpdateFunctionCommandInput): Promise => + client.send(new UpdateFunctionCommand(input)), + updateResolver: (input: UpdateResolverCommandInput): Promise => + client.send(new UpdateResolverCommand(input)), + + // Pagination Functions + listFunctions: async (input: ListFunctionsCommandInput): Promise => { + const functions = Array(); + const paginator = paginateListFunctions({ client }, input); + for await (const page of paginator) { + functions.push(...(page.functions || [])); + } + return functions; + }, + }; } - public cloudFormation(): AWS.CloudFormation { - return this.wrapServiceErrorHandling(new AWS.CloudFormation({ + public cloudFormation(): ICloudFormationClient { + const client = new CloudFormationClient({ ...this.config, - ...this.cloudFormationRetryOptions, - })); + retryStrategy: new ConfiguredRetryStrategy(11, (attempt: number) => attempt ** 1000), + }); + return { + continueUpdateRollback: async ( + input: ContinueUpdateRollbackCommandInput, + ): Promise => client.send(new ContinueUpdateRollbackCommand(input)), + createChangeSet: (input: CreateChangeSetCommandInput): Promise => + client.send(new CreateChangeSetCommand(input)), + createGeneratedTemplate: ( + input: CreateGeneratedTemplateCommandInput, + ): Promise => client.send(new CreateGeneratedTemplateCommand(input)), + createStack: (input: CreateStackCommandInput): Promise => + client.send(new CreateStackCommand(input)), + deleteChangeSet: (input: DeleteChangeSetCommandInput): Promise => + client.send(new DeleteChangeSetCommand(input)), + deleteGeneratedTemplate: ( + input: DeleteGeneratedTemplateCommandInput, + ): Promise => client.send(new DeleteGeneratedTemplateCommand(input)), + deleteStack: (input: DeleteStackCommandInput): Promise => + client.send(new DeleteStackCommand(input)), + describeChangeSet: (input: DescribeChangeSetCommandInput): Promise => + client.send(new DescribeChangeSetCommand(input)), + describeGeneratedTemplate: ( + input: DescribeGeneratedTemplateCommandInput, + ): Promise => client.send(new DescribeGeneratedTemplateCommand(input)), + describeResourceScan: (input: DescribeResourceScanCommandInput): Promise => + client.send(new DescribeResourceScanCommand(input)), + describeStacks: (input: DescribeStacksCommandInput): Promise => + client.send(new DescribeStacksCommand(input)), + executeChangeSet: (input: ExecuteChangeSetCommandInput): Promise => + client.send(new ExecuteChangeSetCommand(input)), + getGeneratedTemplate: (input: GetGeneratedTemplateCommandInput): Promise => + client.send(new GetGeneratedTemplateCommand(input)), + getTemplate: (input: GetTemplateCommandInput): Promise => + client.send(new GetTemplateCommand(input)), + getTemplateSummary: (input: GetTemplateSummaryCommandInput): Promise => + client.send(new GetTemplateSummaryCommand(input)), + listExports: (input: ListExportsCommandInput): Promise => + client.send(new ListExportsCommand(input)), + listResourceScanRelatedResources: ( + input: ListResourceScanRelatedResourcesCommandInput, + ): Promise => + client.send(new ListResourceScanRelatedResourcesCommand(input)), + listResourceScanResources: ( + input: ListResourceScanResourcesCommandInput, + ): Promise => client.send(new ListResourceScanResourcesCommand(input)), + listResourceScans: (input: ListResourceScansCommandInput): Promise => + client.send(new ListResourceScansCommand(input)), + rollbackStack: (input: RollbackStackCommandInput): Promise => + client.send(new RollbackStackCommand(input)), + startResourceScan: (input: StartResourceScanCommandInput): Promise => + client.send(new StartResourceScanCommand(input)), + updateStack: (input: UpdateStackCommandInput): Promise => + client.send(new UpdateStackCommand(input)), + updateTerminationProtection: ( + input: UpdateTerminationProtectionCommandInput, + ): Promise => + client.send(new UpdateTerminationProtectionCommand(input)), + describeStackEvents: async (input: DescribeStackEventsCommandInput): Promise => { + const stackEvents = Array(); + const paginator = paginateDescribeStackEvents({ client }, input); + for await (const page of paginator) { + stackEvents.push(...(page?.StackEvents || [])); + } + return stackEvents; + }, + listStackResources: async (input: ListStackResourcesCommandInput): Promise => { + const stackResources = Array(); + const paginator = paginateListStackResources({ client }, input); + for await (const page of paginator) { + stackResources.push(...(page?.StackResourceSummaries || [])); + } + return stackResources; + }, + }; } - public ec2(): AWS.EC2 { - return this.wrapServiceErrorHandling(new AWS.EC2(this.config)); + public cloudWatchLogs(): ICloudWatchLogsClient { + const client = new CloudWatchLogsClient(this.config); + return { + describeLogGroups: (input: DescribeLogGroupsCommandInput): Promise => + client.send(new DescribeLogGroupsCommand(input)), + filterLogEvents: (input: FilterLogEventsCommandInput): Promise => + client.send(new FilterLogEventsCommand(input)), + }; } - public iam(): AWS.IAM { - return this.wrapServiceErrorHandling(new AWS.IAM(this.config)); + public codeBuild(): ICodeBuildClient { + const client = this.wrapServiceErrorHandling(new CodeBuildClient(this.config)); + return { + updateProject: (input: UpdateProjectCommandInput): Promise => + client.send(new UpdateProjectCommand(input)), + }; } - public ssm(): AWS.SSM { - return this.wrapServiceErrorHandling(new AWS.SSM(this.config)); + public ec2(): IEC2Client { + const client = this.wrapServiceErrorHandling(new EC2Client(this.config)); + return { + describeAvailabilityZones: ( + input: DescribeAvailabilityZonesCommandInput, + ): Promise => client.send(new DescribeAvailabilityZonesCommand(input)), + describeImages: (input: DescribeImagesCommandInput): Promise => + client.send(new DescribeImagesCommand(input)), + describeInstances: (input: DescribeInstancesCommandInput): Promise => + client.send(new DescribeInstancesCommand(input)), + describeRouteTables: (input: DescribeRouteTablesCommandInput): Promise => + client.send(new DescribeRouteTablesCommand(input)), + describeSecurityGroups: ( + input: DescribeSecurityGroupsCommandInput, + ): Promise => client.send(new DescribeSecurityGroupsCommand(input)), + describeSubnets: (input: DescribeSubnetsCommandInput): Promise => + client.send(new DescribeSubnetsCommand(input)), + describeVpcEndpointServices: ( + input: DescribeVpcEndpointServicesCommandInput, + ): Promise => + client.send(new DescribeVpcEndpointServicesCommand(input)), + describeVpcs: (input: DescribeVpcsCommandInput): Promise => + client.send(new DescribeVpcsCommand(input)), + describeVpnGateways: (input: DescribeVpnGatewaysCommandInput): Promise => + client.send(new DescribeVpnGatewaysCommand(input)), + }; } - public s3(): AWS.S3 { - return this.wrapServiceErrorHandling(new AWS.S3(this.config)); + public ecr(): IECRClient { + const client = this.wrapServiceErrorHandling(new ECRClient(this.config)); + return { + createRepository: (input: CreateRepositoryCommandInput): Promise => + client.send(new CreateRepositoryCommand(input)), + describeImages: (input: ECRDescribeImagesCommandInput): Promise => + client.send(new ECRDescribeImagesCommand(input)), + describeRepositories: (input: DescribeRepositoriesCommandInput): Promise => + client.send(new DescribeRepositoriesCommand(input)), + getAuthorizationToken: (input: GetAuthorizationTokenCommandInput): Promise => + client.send(new GetAuthorizationTokenCommand(input)), + putImageScanningConfiguration: ( + input: PutImageScanningConfigurationCommandInput, + ): Promise => + client.send(new PutImageScanningConfigurationCommand(input)), + }; } - public route53(): AWS.Route53 { - return this.wrapServiceErrorHandling(new AWS.Route53(this.config)); + public ecs(): IECSClient { + const client = this.wrapServiceErrorHandling(new ECSClient(this.config)); + return { + listClusters: (input: ListClustersCommandInput): Promise => + client.send(new ListClustersCommand(input)), + registerTaskDefinition: ( + input: RegisterTaskDefinitionCommandInput, + ): Promise => client.send(new RegisterTaskDefinitionCommand(input)), + updateService: (input: UpdateServiceCommandInput): Promise => + client.send(new UpdateServiceCommand(input)), + // Waiters + waitUntilServicesStable: (input: DescribeServicesCommandInput): Promise => { + return waitUntilServicesStable( + { + client, + maxWaitTime: 600, + minDelay: 6, + maxDelay: 6, + }, + input, + ); + }, + }; } - public ecr(): AWS.ECR { - return this.wrapServiceErrorHandling(new AWS.ECR(this.config)); + public elbv2(): IElasticLoadBalancingV2Client { + const client = this.wrapServiceErrorHandling(new ElasticLoadBalancingV2Client(this.config)); + return { + describeListeners: (input: DescribeListenersCommandInput): Promise => + client.send(new DescribeListenersCommand(input)), + describeLoadBalancers: (input: DescribeLoadBalancersCommandInput): Promise => + client.send(new DescribeLoadBalancersCommand(input)), + describeTags: (input: DescribeTagsCommandInput): Promise => + client.send(new DescribeTagsCommand(input)), + // Pagination Functions + paginateDescribeListeners: async (input: DescribeListenersCommandInput): Promise => { + const listeners = Array(); + const paginator = paginateDescribeListeners({ client }, input); + for await (const page of paginator) { + listeners.push(...(page?.Listeners || [])); + } + return listeners; + }, + paginateDescribeLoadBalancers: async (input: DescribeLoadBalancersCommandInput): Promise => { + const loadBalancers = Array(); + const paginator = paginateDescribeLoadBalancers({ client }, input); + for await (const page of paginator) { + loadBalancers.push(...(page?.LoadBalancers || [])); + } + return loadBalancers; + }, + }; } - public ecs(): AWS.ECS { - return this.wrapServiceErrorHandling(new AWS.ECS(this.config)); + public iam(): IIAMClient { + const client = this.wrapServiceErrorHandling(new IAMClient(this.config)); + return { + createPolicy: (input: CreatePolicyCommandInput): Promise => + client.send(new CreatePolicyCommand(input)), + getPolicy: (input: GetPolicyCommandInput): Promise => + client.send(new GetPolicyCommand(input)), + getRole: (input: GetRoleCommandInput): Promise => client.send(new GetRoleCommand(input)), + }; } - public elbv2(): AWS.ELBv2 { - return this.wrapServiceErrorHandling(new AWS.ELBv2(this.config)); + public kms(): IKMSClient { + const client = this.wrapServiceErrorHandling(new KMSClient(this.config)); + return { + describeKey: (input: DescribeKeyCommandInput): Promise => + client.send(new DescribeKeyCommand(input)), + listAliases: (input: ListAliasesCommandInput): Promise => + client.send(new ListAliasesCommand(input)), + }; } - public secretsManager(): AWS.SecretsManager { - return this.wrapServiceErrorHandling(new AWS.SecretsManager(this.config)); + public lambda(): ILambdaClient { + const client = this.wrapServiceErrorHandling(new LambdaClient(this.config)); + return { + invokeCommand: (input: InvokeCommandInput): Promise => client.send(new InvokeCommand(input)), + publishVersion: (input: PublishVersionCommandInput): Promise => + client.send(new PublishVersionCommand(input)), + updateAlias: (input: UpdateAliasCommandInput): Promise => + client.send(new UpdateAliasCommand(input)), + updateFunctionCode: (input: UpdateFunctionCodeCommandInput): Promise => + client.send(new UpdateFunctionCodeCommand(input)), + updateFunctionConfiguration: ( + input: UpdateFunctionConfigurationCommandInput, + ): Promise => + client.send(new UpdateFunctionConfigurationCommand(input)), + // Waiters + waitUntilFunctionUpdated: ( + delaySeconds: number, + input: UpdateFunctionConfigurationCommandInput, + ): Promise => { + return waitUntilFunctionUpdated( + { + client, + maxDelay: delaySeconds, + minDelay: delaySeconds, + maxWaitTime: delaySeconds * 60, + }, + input, + ); + }, + }; } - public kms(): AWS.KMS { - return this.wrapServiceErrorHandling(new AWS.KMS(this.config)); + public route53(): IRoute53Client { + const client = this.wrapServiceErrorHandling(new Route53Client(this.config)); + return { + getHostedZone: (input: GetHostedZoneCommandInput): Promise => + client.send(new GetHostedZoneCommand(input)), + listHostedZones: (input: ListHostedZonesCommandInput): Promise => + client.send(new ListHostedZonesCommand(input)), + listHostedZonesByName: (input: ListHostedZonesByNameCommandInput): Promise => + client.send(new ListHostedZonesByNameCommand(input)), + }; } - public stepFunctions(): AWS.StepFunctions { - return this.wrapServiceErrorHandling(new AWS.StepFunctions(this.config)); - } + public s3(): IS3Client { + const client = this.wrapServiceErrorHandling(new S3Client(this.config)); + return { + getBucketEncryption: (input: GetBucketEncryptionCommandInput): Promise => + client.send(new GetBucketEncryptionCommand(input)), + getBucketLocation: (input: GetBucketLocationCommandInput): Promise => + client.send(new GetBucketLocationCommand(input)), + getObject: (input: GetObjectCommandInput): Promise => + client.send(new GetObjectCommand(input)), + listObjectsV2: (input: ListObjectsV2CommandInput): Promise => + client.send(new ListObjectsV2Command(input)), + upload: (input: PutObjectCommandInput): Promise => { + try { + const upload = new Upload({ + client, + params: input, + }); - public codeBuild(): AWS.CodeBuild { - return this.wrapServiceErrorHandling(new AWS.CodeBuild(this.config)); + return upload.done(); + } catch (e: any) { + throw new Error(`Upload failed: ${e.message}`); + } + }, + }; } - public cloudWatchLogs(): AWS.CloudWatchLogs { - return this.wrapServiceErrorHandling(new AWS.CloudWatchLogs(this.config)); + public secretsManager(): ISecretsManagerClient { + const client = this.wrapServiceErrorHandling(new SecretsManagerClient(this.config)); + return { + getSecretValue: (input: GetSecretValueCommandInput): Promise => + client.send(new GetSecretValueCommand(input)), + }; } - public appsync(): AWS.AppSync { - return this.wrapServiceErrorHandling(new AWS.AppSync(this.config)); + public ssm(): ISSMClient { + const client = this.wrapServiceErrorHandling(new SSMClient(this.config)); + return { + getParameter: (input: GetParameterCommandInput): Promise => + client.send(new GetParameterCommand(input)), + }; } - public async currentAccount(): Promise { - // Get/refresh if necessary before we can access `accessKeyId` - await this.forceCredentialRetrieval(); - - return cached(this, CURRENT_ACCOUNT_KEY, () => SDK.accountCache.fetch(this._credentials.accessKeyId, async () => { - // if we don't have one, resolve from STS and store in cache. - debug('Looking up default account ID from STS'); - const result = await new AWS.STS({ ...this.config, ...this.stsRetryOptions }).getCallerIdentity().promise(); - const accountId = result.Account; - const partition = result.Arn!.split(':')[1]; - if (!accountId) { - throw new Error('STS didn\'t return an account ID'); - } - debug('Default account ID:', accountId); - - // Save another STS call later if this one already succeeded - this._credentialsValidated = true; - return { accountId, partition }; - })); + public stepFunctions(): IStepFunctionsClient { + const client = this.wrapServiceErrorHandling(new SFNClient(this.config)); + return { + updateStateMachine: (input: UpdateStateMachineCommandInput): Promise => + client.send(new UpdateStateMachineCommand(input)), + }; } /** - * Return the current credentials - * - * Don't use -- only used to write tests around assuming roles. + * The AWS SDK v3 requires a client config and a command in order to get an endpoint for + * any given service. */ - public async currentCredentials(): Promise { - await this.forceCredentialRetrieval(); - return this._credentials; + public async getUrlSuffix(region: string): Promise { + const cfn = new CloudFormationClient({ region }); + const endpoint = await getEndpointFromInstructions({}, DescribeStackResourcesCommand, { ...cfn.config }); + return endpoint.url.hostname.split(`${region}.`).pop()!; } - /** - * Force retrieval of the current credentials - * - * Relevant if the current credentials are AssumeRole credentials -- do the actual - * lookup, and translate any error into a useful error message (taking into - * account credential provenance). - */ - public async forceCredentialRetrieval() { - try { - await this._credentials.getPromise(); - } catch (e: any) { - if (isUnrecoverableAwsError(e)) { - throw e; - } + public async currentAccount(): Promise { + return cached(this, CURRENT_ACCOUNT_KEY, () => + SDK.accountCache.fetch(this._credentials.accessKeyId, async () => { + // if we don't have one, resolve from STS and store in cache. + debug('Looking up default account ID from STS'); + const client = new STSClient({ + ...this.config, + ...this.stsRetryOptions, + }); + const command = new GetCallerIdentityCommand({}); + const result = await client.send(command); + debug(result.Account!, result.Arn, result.UserId); + const accountId = result.Account; + const partition = result.Arn!.split(':')[1]; + if (!accountId) { + throw new Error("STS didn't return an account ID"); + } + debug('Default account ID:', accountId); - // Only reason this would fail is if it was an AssumRole. Otherwise, - // reading from an INI file or reading env variables is unlikely to fail. - debug(`Assuming role failed: ${e.message}`); - throw new Error([ - 'Could not assume role in target account', - ...this.sdkOptions.assumeRoleCredentialsSourceDescription - ? [`using ${this.sdkOptions.assumeRoleCredentialsSourceDescription}`] - : [], - e.message, - '. Please make sure that this role exists in the account. If it doesn\'t exist, (re)-bootstrap the environment ' + - 'with the right \'--trust\', using the latest version of the CDK CLI.', - ].join(' ')); - } + // Save another STS call later if this one already succeeded + this._credentialsValidated = true; + return { accountId, partition }; + }), + ); } /** @@ -286,14 +907,11 @@ export class SDK implements ISDK { return; } - await new AWS.STS({ ...this.config, ...this.stsRetryOptions }).getCallerIdentity().promise(); + const client = new STSClient({ ...this.config, ...this.stsRetryOptions }); + await client.send(new GetCallerIdentityCommand({})); this._credentialsValidated = true; } - public getEndpointSuffix(region: string): string { - return regionUtil.getEndpointSuffix(region); - } - /** * Return a wrapping object for the underlying service object * @@ -328,28 +946,34 @@ export class SDK implements ISDK { // - Anything that's not a function. // - 'constructor', s3.upload() will use this to do some magic and we need the underlying constructor. // - Any method that's not on the service class (do not intercept 'makeRequest' and other helpers). - if (prop === 'constructor' || !classObject.hasOwnProperty(prop) || !isFunction(real)) { return real; } + if (prop === 'constructor' || !classObject.hasOwnProperty(prop) || !isFunction(real)) { + return real; + } // NOTE: This must be a function() and not an () => { // because I need 'this' to be dynamically bound and not statically bound. // If your linter complains don't listen to it! - return function(this: any) { + return function (this: any) { // Call the underlying function. If it returns an object with a promise() // method on it, wrap that 'promise' method. const args = [].slice.call(arguments, 0); const response = real.apply(this, args); // Don't intercept unless the return value is an object with a '.promise()' method. - if (typeof response !== 'object' || !response) { return response; } - if (!('promise' in response)) { return response; } + if (typeof response !== 'object' || !response) { + return response; + } + if (!('promise' in response)) { + return response; + } // Return an object with the promise method replaced with a wrapper which will // do additional things to errors. return Object.assign(Object.create(response), { promise() { - return response.promise().catch((e: Error & { code?: string }) => { + return response.catch((e: Error & { code?: string }) => { e = self.makeDetailedException(e); - debug(`Call failed: ${prop}(${JSON.stringify(args[0])}) => ${e.message} (code=${e.code})`); + debug(`Call failed: ${prop}(${JSON.stringify(args[0])}) => ${e.message} (code=${e.name})`); return Promise.reject(e); // Re-'throw' the new error }); }, @@ -384,10 +1008,10 @@ export class SDK implements ISDK { if (e.message === 'Could not load credentials from ChainableTemporaryCredentials') { e.message = [ 'Could not assume role in target account', - ...this.sdkOptions.assumeRoleCredentialsSourceDescription + ...(this.sdkOptions.assumeRoleCredentialsSourceDescription ? [`using ${this.sdkOptions.assumeRoleCredentialsSourceDescription}`] - : [], - '(did you bootstrap the environment with the right \'--trust\'s?)', + : []), + "(did you bootstrap the environment with the right '--trust's?)", ].join(' '); } @@ -415,10 +1039,3 @@ function allChainedExceptionMessages(e: Error | undefined) { } return ret.join(': '); } - -/** - * Return whether an error should not be recovered from - */ -export function isUnrecoverableAwsError(e: Error) { - return (e as any).code === 'ExpiredToken'; -} diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk_ini_file.ts b/packages/aws-cdk/lib/api/aws-auth/sdk_ini_file.ts deleted file mode 100644 index 91c50e87c0a97..0000000000000 --- a/packages/aws-cdk/lib/api/aws-auth/sdk_ini_file.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * A reimplementation of JS AWS SDK's SharedIniFile class - * - * We need that class to parse the ~/.aws/config file to determine the correct - * region at runtime, but unfortunately it is private upstream. - */ - -import * as os from 'os'; -import * as path from 'path'; -import * as AWS from 'aws-sdk'; -import * as fs from 'fs-extra'; - -export interface SharedIniFileOptions { - isConfig?: boolean; - filename?: string; -} - -export class SharedIniFile { - private readonly isConfig: boolean; - private readonly filename: string; - private parsedContents?: { [key: string]: { [key: string]: string } }; - - constructor(options?: SharedIniFileOptions) { - options = options || {}; - this.isConfig = options.isConfig === true; - this.filename = options.filename || this.getDefaultFilepath(); - } - - public async getProfile(profile: string) { - await this.ensureFileLoaded(); - - const profileIndex = profile !== (AWS as any).util.defaultProfile && this.isConfig ? - 'profile ' + profile : profile; - - return this.parsedContents![profileIndex]; - } - - private getDefaultFilepath(): string { - return path.join( - os.homedir(), - '.aws', - this.isConfig ? 'config' : 'credentials', - ); - } - - private async ensureFileLoaded() { - if (this.parsedContents) { - return; - } - - if (!await fs.pathExists(this.filename)) { - this.parsedContents = {}; - return; - } - - const contents: string = (await fs.readFile(this.filename)).toString(); - this.parsedContents = (AWS as any).util.ini.parse(contents); - } -} diff --git a/packages/aws-cdk/lib/api/aws-auth/user-agent.ts b/packages/aws-cdk/lib/api/aws-auth/user-agent.ts new file mode 100644 index 0000000000000..98e2f716d57d1 --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/user-agent.ts @@ -0,0 +1,17 @@ +import * as path from 'path'; +import { readIfPossible } from './util'; +import { rootDir } from '../../util/directories'; + +/** + * Find the package.json from the main toolkit. + * + * If we can't read it for some reason, try to do something reasonable anyway. + * Fall back to argv[1], or a standard string if that is undefined for some reason. + */ +export function defaultCliUserAgent() { + const root = rootDir(false); + const pkg = JSON.parse((root ? readIfPossible(path.join(root, 'package.json')) : undefined) ?? '{}'); + const name = pkg.name ?? path.basename(process.argv[1] ?? 'cdk-cli'); + const version = pkg.version ?? ''; + return `${name}/${version}`; +} diff --git a/packages/aws-cdk/lib/api/aws-auth/util.ts b/packages/aws-cdk/lib/api/aws-auth/util.ts new file mode 100644 index 0000000000000..b5c6f57b7032b --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/util.ts @@ -0,0 +1,19 @@ +import * as fs from 'fs-extra'; +import { debug } from '../../logging'; + +/** + * Read a file if it exists, or return undefined + * + * Not async because it is used in the constructor + */ +export function readIfPossible(filename: string): string | undefined { + try { + if (!fs.pathExistsSync(filename)) { + return undefined; + } + return fs.readFileSync(filename, { encoding: 'utf-8' }); + } catch (e: any) { + debug(e); + return undefined; + } +} diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index f3041fd3864ec..eaf7e47537679 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -1,27 +1,26 @@ import { info } from 'console'; import * as path from 'path'; import * as cxapi from '@aws-cdk/cx-api'; -import { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props'; +import type { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props'; import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap'; import { legacyBootstrapTemplate } from './legacy-template'; import { warning } from '../../logging'; import { loadStructuredFile, serializeStructure } from '../../serialize'; import { rootDir } from '../../util/directories'; -import { ISDK, Mode, SdkProvider } from '../aws-auth'; -import { DeployStackResult } from '../deploy-stack'; +import type { SDK, SdkProvider } from '../aws-auth'; +import type { DeployStackResult } from '../deploy-stack'; +import { Mode } from '../plugin'; -/* eslint-disable max-len */ - -export type BootstrapSource = - { source: 'legacy' } - | { source: 'default' } - | { source: 'custom'; templateFile: string }; +export type BootstrapSource = { source: 'legacy' } | { source: 'default' } | { source: 'custom'; templateFile: string }; export class Bootstrapper { - constructor(private readonly source: BootstrapSource) { - } + constructor(private readonly source: BootstrapSource) {} - public bootstrapEnvironment(environment: cxapi.Environment, sdkProvider: SdkProvider, options: BootstrapEnvironmentOptions = {}): Promise { + public bootstrapEnvironment( + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions = {}, + ): Promise { switch (this.source.source) { case 'legacy': return this.legacyBootstrap(environment, sdkProvider, options); @@ -41,7 +40,11 @@ export class Bootstrapper { * Deploy legacy bootstrap stack * */ - private async legacyBootstrap(environment: cxapi.Environment, sdkProvider: SdkProvider, options: BootstrapEnvironmentOptions = {}): Promise { + private async legacyBootstrap( + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions = {}, + ): Promise { const params = options.parameters ?? {}; if (params.trustedAccounts?.length) { @@ -58,10 +61,14 @@ export class Bootstrapper { } const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName); - return current.update(await this.loadTemplate(params), {}, { - ...options, - terminationProtection: options.terminationProtection ?? current.terminationProtection, - }); + return current.update( + await this.loadTemplate(params), + {}, + { + ...options, + terminationProtection: options.terminationProtection ?? current.terminationProtection, + }, + ); } /** @@ -71,8 +78,8 @@ export class Bootstrapper { private async modernBootstrap( environment: cxapi.Environment, sdkProvider: SdkProvider, - options: BootstrapEnvironmentOptions = {}): Promise { - + options: BootstrapEnvironmentOptions = {}, + ): Promise { const params = options.parameters ?? {}; const bootstrapTemplate = await this.loadTemplate(); @@ -81,7 +88,9 @@ export class Bootstrapper { const partition = await current.partition(); if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) { - throw new Error('You cannot pass \'--bootstrap-kms-key-id\' and \'--bootstrap-customer-key\' together. Specify one or the other'); + throw new Error( + "You cannot pass '--bootstrap-kms-key-id' and '--bootstrap-customer-key' together. Specify one or the other", + ); } // If people re-bootstrap, existing parameter values are reused so that people don't accidentally change the configuration @@ -95,10 +104,14 @@ export class Bootstrapper { const trustedAccounts = params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts); info(`Trusted accounts for deployment: ${trustedAccounts.length > 0 ? trustedAccounts.join(', ') : '(none)'}`); - const trustedAccountsForLookup = params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup); - info(`Trusted accounts for lookup: ${trustedAccountsForLookup.length > 0 ? trustedAccountsForLookup.join(', ') : '(none)'}`); + const trustedAccountsForLookup = + params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup); + info( + `Trusted accounts for lookup: ${trustedAccountsForLookup.length > 0 ? trustedAccountsForLookup.join(', ') : '(none)'}`, + ); - const cloudFormationExecutionPolicies = params.cloudFormationExecutionPolicies ?? splitCfnArray(current.parameters.CloudFormationExecutionPolicies); + const cloudFormationExecutionPolicies = + params.cloudFormationExecutionPolicies ?? splitCfnArray(current.parameters.CloudFormationExecutionPolicies); if (trustedAccounts.length === 0 && cloudFormationExecutionPolicies.length === 0) { // For self-trust it's okay to default to AdministratorAccess, and it improves the usability of bootstrapping a lot. // @@ -114,9 +127,13 @@ export class Bootstrapper { // Would leave AdministratorAccess policies with a trust relationship, without the user explicitly // approving the trust policy. const implicitPolicy = `arn:${partition}:iam::aws:policy/AdministratorAccess`; - warning(`Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`); + warning( + `Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`, + ); } else if (cloudFormationExecutionPolicies.length === 0) { - throw new Error(`Please pass \'--cloudformation-execution-policies\' when using \'--trust\' to specify deployment permissions. Try a managed policy of the form \'arn:${partition}:iam::aws:policy/\'.`); + throw new Error( + `Please pass \'--cloudformation-execution-policies\' when using \'--trust\' to specify deployment permissions. Try a managed policy of the form \'arn:${partition}:iam::aws:policy/\'.`, + ); } else { // Remind people what the current settings are info(`Execution policies: ${cloudFormationExecutionPolicies.join(', ')}`); @@ -129,21 +146,27 @@ export class Bootstrapper { // * undefined if we already had a value in place (reusing what we had) // * '-' if this is the first time we're deploying this stack (or upgrading from old to new bootstrap) const currentKmsKeyId = current.parameters.FileAssetsBucketKmsKeyId; - const kmsKeyId = params.kmsKeyId ?? - (params.createCustomerMasterKey === true ? CREATE_NEW_KEY : - params.createCustomerMasterKey === false || currentKmsKeyId === undefined ? USE_AWS_MANAGED_KEY : undefined); + const kmsKeyId = + params.kmsKeyId ?? + (params.createCustomerMasterKey === true + ? CREATE_NEW_KEY + : params.createCustomerMasterKey === false || currentKmsKeyId === undefined + ? USE_AWS_MANAGED_KEY + : undefined); /* A permissions boundary can be provided via: - * - the flag indicating the example one should be used - * - the name indicating the custom permissions boundary to be used - * Re-bootstrapping will NOT be blocked by either tightening or relaxing the permissions' boundary. - */ + * - the flag indicating the example one should be used + * - the name indicating the custom permissions boundary to be used + * Re-bootstrapping will NOT be blocked by either tightening or relaxing the permissions' boundary. + */ // InputPermissionsBoundary is an `any` type and if it is not defined it // appears as an empty string ''. We need to force it to evaluate an empty string // as undefined const currentPermissionsBoundary: string | undefined = current.parameters.InputPermissionsBoundary || undefined; - const inputPolicyName = params.examplePermissionsBoundary ? CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY : params.customPermissionsBoundary; + const inputPolicyName = params.examplePermissionsBoundary + ? CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY + : params.customPermissionsBoundary; let policyName: string | undefined; if (inputPolicyName) { // If the example policy is not already in place, it must be created. @@ -170,27 +193,37 @@ export class Bootstrapper { TrustedAccountsForLookup: trustedAccountsForLookup.join(','), CloudFormationExecutionPolicies: cloudFormationExecutionPolicies.join(','), Qualifier: params.qualifier, - PublicAccessBlockConfiguration: params.publicAccessBlockConfiguration || params.publicAccessBlockConfiguration === undefined ? 'true' : 'false', + PublicAccessBlockConfiguration: + params.publicAccessBlockConfiguration || params.publicAccessBlockConfiguration === undefined + ? 'true' + : 'false', InputPermissionsBoundary: policyName, - }, { + }, + { ...options, terminationProtection: options.terminationProtection ?? current.terminationProtection, - }); + }, + ); } private async getPolicyName( environment: cxapi.Environment, - sdk: ISDK, + sdk: SDK, permissionsBoundary: string, partition: string, - params: BootstrappingParameters): Promise { - + params: BootstrappingParameters, + ): Promise { if (permissionsBoundary !== CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY) { this.validatePolicyName(permissionsBoundary); return Promise.resolve(permissionsBoundary); } // if no Qualifier is supplied, resort to the default one - const arn = await this.getExamplePermissionsBoundary(params.qualifier ?? 'hnb659fds', partition, environment.account, sdk); + const arn = await this.getExamplePermissionsBoundary( + params.qualifier ?? 'hnb659fds', + partition, + environment.account, + sdk, + ); const policyName = arn.split('/').pop(); if (!policyName) { throw new Error('Could not retrieve the example permission boundary!'); @@ -198,14 +231,19 @@ export class Bootstrapper { return Promise.resolve(policyName); } - private async getExamplePermissionsBoundary(qualifier: string, partition: string, account: string, sdk: ISDK): Promise { + private async getExamplePermissionsBoundary( + qualifier: string, + partition: string, + account: string, + sdk: SDK, + ): Promise { const iam = sdk.iam(); let policyName = `cdk-${qualifier}-permissions-boundary`; const arn = `arn:${partition}:iam::${account}:policy/${policyName}`; try { - let getPolicyResp = await iam.getPolicy({ PolicyArn: arn }).promise(); + let getPolicyResp = await iam.getPolicy({ PolicyArn: arn }); if (getPolicyResp.Policy) { return arn; } @@ -255,10 +293,7 @@ export class Bootstrapper { Sid: 'DenyPermBoundaryIAMPolicyAlteration', }, { - Action: [ - 'iam:DeleteUserPermissionsBoundary', - 'iam:DeleteRolePermissionsBoundary', - ], + Action: ['iam:DeleteUserPermissionsBoundary', 'iam:DeleteRolePermissionsBoundary'], Resource: '*', Effect: 'Deny', Sid: 'DenyRemovalOfPermBoundaryFromAnyUserOrRole', @@ -269,7 +304,7 @@ export class Bootstrapper { PolicyName: policyName, PolicyDocument: JSON.stringify(policyDoc), }; - const createPolicyResponse = await iam.createPolicy(request).promise(); + const createPolicyResponse = await iam.createPolicy(request); if (createPolicyResponse.Policy?.Arn) { return createPolicyResponse.Policy.Arn; } else { @@ -291,8 +326,8 @@ export class Bootstrapper { private async customBootstrap( environment: cxapi.Environment, sdkProvider: SdkProvider, - options: BootstrapEnvironmentOptions = {}): Promise { - + options: BootstrapEnvironmentOptions = {}, + ): Promise { // Look at the template, decide whether it's most likely a legacy or modern bootstrap // template, and use the right bootstrapper for that. const version = bootstrapVersionFromTemplate(await this.loadTemplate()); @@ -335,6 +370,8 @@ const CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY = 'CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY'; * An empty string is the empty array (instead of `['']`). */ function splitCfnArray(xs: string | undefined): string[] { - if (xs === '' || xs === undefined) { return []; } + if (xs === '' || xs === undefined) { + return []; + } return xs.split(','); } diff --git a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts index 501122697eab5..5be78b63d480d 100644 --- a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts +++ b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts @@ -1,13 +1,20 @@ import * as os from 'os'; import * as path from 'path'; -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import * as cxapi from '@aws-cdk/cx-api'; +import { ArtifactType } from '@aws-cdk/cloud-assembly-schema'; +import { CloudAssemblyBuilder, Environment, EnvironmentUtils } from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; -import { BOOTSTRAP_VERSION_OUTPUT, BootstrapEnvironmentOptions, BOOTSTRAP_VERSION_RESOURCE, BOOTSTRAP_VARIANT_PARAMETER, DEFAULT_BOOTSTRAP_VARIANT } from './bootstrap-props'; +import { + BOOTSTRAP_VERSION_OUTPUT, + BootstrapEnvironmentOptions, + BOOTSTRAP_VERSION_RESOURCE, + BOOTSTRAP_VARIANT_PARAMETER, + DEFAULT_BOOTSTRAP_VARIANT, +} from './bootstrap-props'; import * as logging from '../../logging'; -import { Mode, SdkProvider, ISDK } from '../aws-auth'; -import { deployStack, DeployStackResult } from '../deploy-stack'; +import type { SdkProvider, SDK } from '../aws-auth'; +import { deployStack, type DeployStackResult } from '../deploy-stack'; import { NoBootstrapStackEnvironmentResources } from '../environment-resources'; +import { Mode } from '../plugin'; import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; /** @@ -25,7 +32,7 @@ import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; * current bootstrap stack and doing something intelligent). */ export class BootstrapStack { - public static async lookup(sdkProvider: SdkProvider, environment: cxapi.Environment, toolkitStackName?: string) { + public static async lookup(sdkProvider: SdkProvider, environment: Environment, toolkitStackName?: string) { toolkitStackName = toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment); @@ -38,11 +45,11 @@ export class BootstrapStack { protected constructor( private readonly sdkProvider: SdkProvider, - private readonly sdk: ISDK, - private readonly resolvedEnvironment: cxapi.Environment, + private readonly sdk: SDK, + private readonly resolvedEnvironment: Environment, private readonly toolkitStackName: string, - private readonly currentToolkitInfo: ToolkitInfo) { - } + private readonly currentToolkitInfo: ToolkitInfo, + ) {} public get parameters(): Record { return this.currentToolkitInfo.found ? this.currentToolkitInfo.bootstrapStack.parameters : {}; @@ -76,7 +83,9 @@ export class BootstrapStack { const currentVariant = this.currentToolkitInfo.variant; const newVariant = bootstrapVariantFromTemplate(template); if (currentVariant !== newVariant) { - logging.warning(`Bootstrap stack already exists, containing '${currentVariant}'. Not overwriting it with a template containing '${newVariant}' (use --force if you intend to overwrite)`); + logging.warning( + `Bootstrap stack already exists, containing '${currentVariant}'. Not overwriting it with a template containing '${newVariant}' (use --force if you intend to overwrite)`, + ); return abortResponse; } @@ -84,24 +93,28 @@ export class BootstrapStack { const newVersion = bootstrapVersionFromTemplate(template); const currentVersion = this.currentToolkitInfo.version; if (newVersion < currentVersion) { - logging.warning(`Bootstrap stack already at version ${currentVersion}. Not downgrading it to version ${newVersion} (use --force if you intend to downgrade)`); + logging.warning( + `Bootstrap stack already at version ${currentVersion}. Not downgrading it to version ${newVersion} (use --force if you intend to downgrade)`, + ); if (newVersion === 0) { // A downgrade with 0 as target version means we probably have a new-style bootstrap in the account, // and an old-style bootstrap as current target, which means the user probably forgot to put this flag in. - logging.warning('(Did you set the \'@aws-cdk/core:newStyleStackSynthesis\' feature flag in cdk.json?)'); + logging.warning("(Did you set the '@aws-cdk/core:newStyleStackSynthesis' feature flag in cdk.json?)"); } return abortResponse; } } const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-bootstrap')); - const builder = new cxapi.CloudAssemblyBuilder(outdir); + const builder = new CloudAssemblyBuilder(outdir); const templateFile = `${this.toolkitStackName}.template.json`; - await fs.writeJson(path.join(builder.outdir, templateFile), template, { spaces: 2 }); + await fs.writeJson(path.join(builder.outdir, templateFile), template, { + spaces: 2, + }); builder.addArtifact(this.toolkitStackName, { - type: cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK, - environment: cxapi.EnvironmentUtils.format(this.resolvedEnvironment.account, this.resolvedEnvironment.region), + type: ArtifactType.AWS_CLOUDFORMATION_STACK, + environment: EnvironmentUtils.format(this.resolvedEnvironment.account, this.resolvedEnvironment.region), properties: { templateFile, terminationProtection: options.terminationProtection ?? false, @@ -134,7 +147,9 @@ export function bootstrapVersionFromTemplate(template: any): number { ]; for (const vs of versionSources) { - if (typeof vs === 'number') { return vs; } + if (typeof vs === 'number') { + return vs; + } if (typeof vs === 'string' && !isNaN(parseInt(vs, 10))) { return parseInt(vs, 10); } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index b8b44a60bc556..ccc2dbf749fab 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -1,21 +1,34 @@ import * as cxapi from '@aws-cdk/cx-api'; -import type { CloudFormation } from 'aws-sdk'; +import type { + CreateChangeSetCommandInput, + CreateStackCommandInput, + DescribeChangeSetCommandOutput, + ExecuteChangeSetCommandInput, + UpdateStackCommandInput, + Tag, +} from '@aws-sdk/client-cloudformation'; import * as chalk from 'chalk'; import * as uuid from 'uuid'; -import { ISDK, SdkProvider } from './aws-auth'; -import { EnvironmentResources } from './environment-resources'; +import type { SDK, SdkProvider, ICloudFormationClient } from './aws-auth'; +import type { EnvironmentResources } from './environment-resources'; import { CfnEvaluationException } from './evaluate-cloudformation-template'; import { HotswapMode, ICON } from './hotswap/common'; import { tryHotswapDeployment } from './hotswap-deployments'; import { addMetadataAssetsToManifest } from '../assets'; -import { Tag } from '../cdk-toolkit'; import { debug, print, warning } from '../logging'; import { - changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet, - waitForStackDeploy, waitForStackDelete, ParameterValues, ParameterChanges, ResourcesToImport, + changeSetHasNoChanges, + CloudFormationStack, + TemplateParameters, + waitForChangeSet, + waitForStackDeploy, + waitForStackDelete, + ParameterValues, + ParameterChanges, + ResourcesToImport, } from './util/cloudformation'; -import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; -import { TemplateBodyParameter, makeBodyParameter } from './util/template-body-parameter'; +import { StackActivityMonitor, type StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; +import { type TemplateBodyParameter, makeBodyParameter } from './util/template-body-parameter'; import { AssetManifestBuilder } from '../util/asset-manifest-builder'; import { publishAssets } from '../util/asset-publishing'; @@ -45,7 +58,7 @@ export interface DeployStackOptions { * Should have been initialized with the correct role with which * stack operations should be performed. */ - readonly sdk: ISDK; + readonly sdk: SDK; /** * SDK provider (seeded with default credentials) @@ -200,10 +213,7 @@ export interface DeployStackOptions { readonly assetParallelism?: boolean; } -export type DeploymentMethod = - | DirectDeploymentMethod - | ChangeSetDeploymentMethod - ; +export type DeploymentMethod = DirectDeploymentMethod | ChangeSetDeploymentMethod; export interface DirectDeploymentMethod { readonly method: 'direct'; @@ -237,11 +247,15 @@ export async function deployStack(options: DeployStackOptions): Promise; + private readonly cfn: ICloudFormationClient; private readonly stackName: string; private readonly update: boolean; private readonly verb: string; @@ -356,7 +396,9 @@ class FullCloudFormationDeployment { } public async performDeployment(): Promise { - const deploymentMethod = this.options.deploymentMethod ?? { method: 'change-set' }; + const deploymentMethod = this.options.deploymentMethod ?? { + method: 'change-set', + }; if (deploymentMethod.method === 'direct' && this.options.resourcesToImport) { throw new Error('Importing resources requires a changeset deployment'); @@ -381,25 +423,41 @@ class FullCloudFormationDeployment { debug('No changes are to be performed on %s.', this.stackName); if (execute) { debug('Deleting empty change set %s', changeSetDescription.ChangeSetId); - await this.cfn.deleteChangeSet({ StackName: this.stackName, ChangeSetName: changeSetName }).promise(); + await this.cfn.deleteChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + }); } if (this.options.force) { - warning([ - 'You used the --force flag, but CloudFormation reported that the deployment would not make any changes.', - 'According to CloudFormation, all resources are already up-to-date with the state in your CDK app.', - '', - 'You cannot use the --force flag to get rid of changes you made in the console. Try using', - 'CloudFormation drift detection instead: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html', - ].join('\n')); + warning( + [ + 'You used the --force flag, but CloudFormation reported that the deployment would not make any changes.', + 'According to CloudFormation, all resources are already up-to-date with the state in your CDK app.', + '', + 'You cannot use the --force flag to get rid of changes you made in the console. Try using', + 'CloudFormation drift detection instead: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html', + ].join('\n'), + ); } - return { noOp: true, outputs: this.cloudFormationStack.outputs, stackArn: changeSetDescription.StackId! }; + return { + noOp: true, + outputs: this.cloudFormationStack.outputs, + stackArn: changeSetDescription.StackId!, + }; } if (!execute) { - print('Changeset %s created and waiting in review for manual execution (--no-execute)', changeSetDescription.ChangeSetId); - return { noOp: false, outputs: this.cloudFormationStack.outputs, stackArn: changeSetDescription.StackId! }; + print( + 'Changeset %s created and waiting in review for manual execution (--no-execute)', + changeSetDescription.ChangeSetId, + ); + return { + noOp: false, + outputs: this.cloudFormationStack.outputs, + stackArn: changeSetDescription.StackId!, + }; } return this.executeChangeSet(changeSetDescription); @@ -418,14 +476,16 @@ class FullCloudFormationDeployment { Description: `CDK Changeset for execution ${this.uuid}`, ClientToken: `create${this.uuid}`, ...this.commonPrepareOptions(), - }).promise(); + }); debug('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id); // Fetching all pages if we'll execute, so we can have the correct change count when monitoring. - return waitForChangeSet(this.cfn, this.stackName, changeSetName, { fetchAll: willExecute }); + return waitForChangeSet(this.cfn, this.stackName, changeSetName, { + fetchAll: willExecute, + }); } - private async executeChangeSet(changeSet: CloudFormation.DescribeChangeSetOutput): Promise { + private async executeChangeSet(changeSet: DescribeChangeSetCommandOutput): Promise { debug('Initiating execution of changeset %s on stack %s', changeSet.ChangeSetId, this.stackName); await this.cfn.executeChangeSet({ @@ -433,9 +493,13 @@ class FullCloudFormationDeployment { ChangeSetName: changeSet.ChangeSetName!, ClientRequestToken: `exec${this.uuid}`, ...this.commonExecuteOptions(), - }).promise(); + }); - debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', changeSet.ChangeSetId, this.stackName); + debug( + 'Execution of changeset %s on stack %s has started; waiting for the update to complete...', + changeSet.ChangeSetId, + this.stackName, + ); // +1 for the extra event emitted from updates. const changeSetLength: number = (changeSet.Changes ?? []).length + (this.update ? 1 : 0); @@ -447,7 +511,10 @@ class FullCloudFormationDeployment { // Delete any existing change sets generated by CDK since change set names must be unique. // The delete request is successful as long as the stack exists (even if the change set does not exist). debug(`Removing existing change set with name ${changeSetName} if it exists`); - await this.cfn.deleteChangeSet({ StackName: this.stackName, ChangeSetName: changeSetName }).promise(); + await this.cfn.deleteChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + }); } } @@ -455,11 +522,16 @@ class FullCloudFormationDeployment { // Update termination protection only if it has changed. const terminationProtection = this.stackArtifact.terminationProtection ?? false; if (!!this.cloudFormationStack.terminationProtection !== terminationProtection) { - debug('Updating termination protection from %s to %s for stack %s', this.cloudFormationStack.terminationProtection, terminationProtection, this.stackName); + debug( + 'Updating termination protection from %s to %s for stack %s', + this.cloudFormationStack.terminationProtection, + terminationProtection, + this.stackName, + ); await this.cfn.updateTerminationProtection({ StackName: this.stackName, EnableTerminationProtection: terminationProtection, - }).promise(); + }); debug('Termination protection updated to %s for stack %s', terminationProtection, this.stackName); } } @@ -478,11 +550,15 @@ class FullCloudFormationDeployment { ClientRequestToken: `update${this.uuid}`, ...this.commonPrepareOptions(), ...this.commonExecuteOptions(), - }).promise(); + }); } catch (err: any) { if (err.message === 'No updates are to be performed.') { debug('No updates are to be performed for stack %s', this.stackName); - return { noOp: true, outputs: this.cloudFormationStack.outputs, stackArn: this.cloudFormationStack.stackId }; + return { + noOp: true, + outputs: this.cloudFormationStack.outputs, + stackArn: this.cloudFormationStack.stackId, + }; } throw err; } @@ -495,29 +571,33 @@ class FullCloudFormationDeployment { await this.cfn.createStack({ StackName: this.stackName, ClientRequestToken: `create${this.uuid}`, - ...terminationProtection ? { EnableTerminationProtection: true } : undefined, + ...(terminationProtection ? { EnableTerminationProtection: true } : undefined), ...this.commonPrepareOptions(), ...this.commonExecuteOptions(), - }).promise(); + }); return this.monitorDeployment(startTime, undefined); } } private async monitorDeployment(startTime: Date, expectedChanges: number | undefined): Promise { - const monitor = this.options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(this.cfn, this.stackName, this.stackArtifact, { - resourcesTotal: expectedChanges, - progress: this.options.progress, - changeSetCreationTime: startTime, - ci: this.options.ci, - }).start(); + const monitor = this.options.quiet + ? undefined + : StackActivityMonitor.withDefaultPrinter(this.cfn, this.stackName, this.stackArtifact, { + resourcesTotal: expectedChanges, + progress: this.options.progress, + changeSetCreationTime: startTime, + ci: this.options.ci, + }).start(); let finalState = this.cloudFormationStack; try { const successStack = await waitForStackDeploy(this.cfn, this.stackName); // This shouldn't really happen, but catch it anyway. You never know. - if (!successStack) { throw new Error('Stack deploy failed (the stack disappeared while we were deploying it)'); } + if (!successStack) { + throw new Error('Stack deploy failed (the stack disappeared while we were deploying it)'); + } finalState = successStack; } catch (e: any) { throw new Error(suffixWithErrors(e.message, monitor?.errors)); @@ -525,13 +605,17 @@ class FullCloudFormationDeployment { await monitor?.stop(); } debug('Stack %s has completed updating', this.stackName); - return { noOp: false, outputs: finalState.outputs, stackArn: finalState.stackId }; + return { + noOp: false, + outputs: finalState.outputs, + stackArn: finalState.stackId, + }; } /** * Return the options that are shared between CreateStack, UpdateStack and CreateChangeSet */ - private commonPrepareOptions(): Partial> { + private commonPrepareOptions(): Partial> { return { Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], NotificationARNs: this.options.notificationArns, @@ -549,12 +633,12 @@ class FullCloudFormationDeployment { * Be careful not to add in keys for options that aren't used, as the features may not have been * deployed everywhere yet. */ - private commonExecuteOptions(): Partial> { + private commonExecuteOptions(): Partial> { const shouldDisableRollback = this.options.rollback === false; return { StackName: this.stackName, - ...shouldDisableRollback ? { DisableRollback: true } : undefined, + ...(shouldDisableRollback ? { DisableRollback: true } : undefined), }; } } @@ -565,7 +649,7 @@ export interface DestroyStackOptions { */ stack: cxapi.CloudFormationStackArtifact; - sdk: ISDK; + sdk: SDK; roleArn?: string; deployName?: string; quiet?: boolean; @@ -580,12 +664,14 @@ export async function destroyStack(options: DestroyStackOptions) { if (!currentStack.exists) { return; } - const monitor = options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, { - ci: options.ci, - }).start(); + const monitor = options.quiet + ? undefined + : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, { + ci: options.ci, + }).start(); try { - await cfn.deleteStack({ StackName: deployName, RoleARN: options.roleArn }).promise(); + await cfn.deleteStack({ StackName: deployName, RoleARN: options.roleArn }); const destroyedStack = await waitForStackDelete(cfn, deployName); if (destroyedStack && destroyedStack.stackStatus.name !== 'DELETE_COMPLETE') { throw new Error(`Failed to destroy ${deployName}: ${destroyedStack.stackStatus}`); @@ -593,7 +679,9 @@ export async function destroyStack(options: DestroyStackOptions) { } catch (e: any) { throw new Error(suffixWithErrors(e.message, monitor?.errors)); } finally { - if (monitor) { await monitor.stop(); } + if (monitor) { + await monitor.stop(); + } } } @@ -609,8 +697,8 @@ export async function destroyStack(options: DestroyStackOptions) { async function canSkipDeploy( deployStackOptions: DeployStackOptions, cloudFormationStack: CloudFormationStack, - parameterChanges: ParameterChanges): Promise { - + parameterChanges: ParameterChanges, +): Promise { const deployName = deployStackOptions.deployName || deployStackOptions.stack.stackName; debug(`${deployName}: checking if we can skip deploy`); @@ -621,7 +709,10 @@ async function canSkipDeploy( } // Creating changeset only (default true), never skip - if (deployStackOptions.deploymentMethod?.method === 'change-set' && deployStackOptions.deploymentMethod.execute === false) { + if ( + deployStackOptions.deploymentMethod?.method === 'change-set' && + deployStackOptions.deploymentMethod.execute === false + ) { debug(`${deployName}: --no-execute, always creating change set`); return false; } @@ -685,7 +776,7 @@ function compareTags(a: Tag[], b: Tag[]): boolean { } for (const aTag of a) { - const bTag = b.find(tag => tag.Key === aTag.Key); + const bTag = b.find((tag) => tag.Key === aTag.Key); if (!bTag || bTag.Value !== aTag.Value) { return false; @@ -696,11 +787,9 @@ function compareTags(a: Tag[], b: Tag[]): boolean { } function suffixWithErrors(msg: string, errors?: string[]) { - return errors && errors.length > 0 - ? `${msg}: ${errors.join(', ')}` - : msg; + return errors && errors.length > 0 ? `${msg}: ${errors.join(', ')}` : msg; } function arrayEquals(a: any[], b: any[]): boolean { - return a.every(item => b.includes(item)) && b.every(item => a.includes(item)); + return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item)); } diff --git a/packages/aws-cdk/lib/api/deployments.ts b/packages/aws-cdk/lib/api/deployments.ts index d5b6f8a63e987..80639b4c9cbec 100644 --- a/packages/aws-cdk/lib/api/deployments.ts +++ b/packages/aws-cdk/lib/api/deployments.ts @@ -3,23 +3,40 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as cdk_assets from 'cdk-assets'; import { AssetManifest, IManifestEntry } from 'cdk-assets'; import * as chalk from 'chalk'; -import { Tag } from '../cdk-toolkit'; -import { debug, warning, error } from '../logging'; -import { Mode } from './aws-auth/credentials'; -import { ISDK } from './aws-auth/sdk'; -import { CredentialsOptions, SdkForEnvironment, SdkProvider } from './aws-auth/sdk-provider'; -import { deployStack, DeployStackResult, destroyStack, DeploymentMethod } from './deploy-stack'; -import { EnvironmentResources, EnvironmentResourcesRegistry } from './environment-resources'; +import type { SDK } from './aws-auth/sdk'; +import type { CredentialsOptions, SdkForEnvironment, SdkProvider } from './aws-auth/sdk-provider'; +import { deployStack, type DeployStackResult, destroyStack, type DeploymentMethod } from './deploy-stack'; +import { type EnvironmentResources, EnvironmentResourcesRegistry } from './environment-resources'; import { HotswapMode } from './hotswap/common'; -import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate, RootTemplateWithNestedStacks } from './nested-stack-helpers'; -import { CloudFormationStack, Template, ResourcesToImport, ResourceIdentifierSummaries, stabilizeStack } from './util/cloudformation'; +import { + loadCurrentTemplateWithNestedStacks, + loadCurrentTemplate, + type RootTemplateWithNestedStacks, +} from './nested-stack-helpers'; +import { Mode } from './plugin'; +import type { Tag } from '../cdk-toolkit'; +import { debug, warning, error } from '../logging'; +import { + CloudFormationStack, + type Template, + type ResourcesToImport, + type ResourceIdentifierSummaries, + stabilizeStack, +} from './util/cloudformation'; import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; import { StackEventPoller } from './util/cloudformation/stack-event-poller'; import { RollbackChoice } from './util/cloudformation/stack-status'; import { replaceEnvPlaceholders } from './util/placeholders'; import { makeBodyParameter } from './util/template-body-parameter'; import { AssetManifestBuilder } from '../util/asset-manifest-builder'; -import { buildAssets, publishAssets, BuildAssetsOptions, PublishAssetsOptions, PublishingAws, EVENT_TO_LOGGER } from '../util/asset-publishing'; +import { + buildAssets, + publishAssets, + type BuildAssetsOptions, + type PublishAssetsOptions, + PublishingAws, + EVENT_TO_LOGGER, +} from '../util/asset-publishing'; const BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK = 23; @@ -31,7 +48,7 @@ export interface PreparedSdkWithLookupRoleForEnvironment { /** * The SDK for the given environment */ - readonly sdk: ISDK; + readonly sdk: SDK; /** * The resolved environment for the stack @@ -360,7 +377,7 @@ export interface PreparedSdkForEnvironment { /** * The SDK for the given environment */ - readonly stackSdk: ISDK; + readonly stackSdk: SDK; /** * The resolved environment for the stack @@ -423,7 +440,11 @@ export class Deployments { debug(`Retrieving template summary for stack ${stackArtifact.displayName}.`); // Currently, needs to use `deploy-role` since it may need to read templates in the staging // bucket which have been encrypted with a KMS key (and lookup-role may not read encrypted things) - const { stackSdk, resolvedEnvironment, envResources } = await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading); + const { stackSdk, resolvedEnvironment, envResources } = await this.prepareSdkFor( + stackArtifact, + undefined, + Mode.ForReading, + ); const cfn = stackSdk.cloudFormation(); // Upload the template, if necessary, before passing it to CFN @@ -432,9 +453,9 @@ export class Deployments { resolvedEnvironment, new AssetManifestBuilder(), envResources, - stackSdk); + ); - const response = await cfn.getTemplateSummary(cfnParam).promise(); + const response = await cfn.getTemplateSummary(cfnParam); if (!response.ResourceIdentifierSummaries) { debug('GetTemplateSummary API call did not return "ResourceIdentifierSummaries"'); } @@ -445,7 +466,9 @@ export class Deployments { let deploymentMethod = options.deploymentMethod; if (options.changeSetName || options.execute !== undefined) { if (deploymentMethod) { - throw new Error('You cannot supply both \'deploymentMethod\' and \'changeSetName/execute\'. Supply one or the other.'); + throw new Error( + "You cannot supply both 'deploymentMethod' and 'changeSetName/execute'. Supply one or the other.", + ); } deploymentMethod = { method: 'change-set', @@ -454,19 +477,19 @@ export class Deployments { }; } - const { - stackSdk, - resolvedEnvironment, - cloudFormationRoleArn, - envResources, - } = await this.prepareSdkFor(options.stack, options.roleArn, Mode.ForWriting); + const { stackSdk, resolvedEnvironment, cloudFormationRoleArn, envResources } = await this.prepareSdkFor( + options.stack, + options.roleArn, + Mode.ForWriting, + ); // Do a verification of the bootstrap stack version await this.validateBootstrapStackVersion( options.stack.stackName, options.stack.requiresBootstrapStackVersion, options.stack.bootstrapStackVersionSsmParameter, - envResources); + envResources, + ); return deployStack({ stack: options.stack, @@ -514,7 +537,8 @@ export class Deployments { options.stack.stackName, BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK, options.stack.bootstrapStackVersionSsmParameter, - envResources); + envResources, + ); } const cfn = stackSdk.cloudFormation(); @@ -538,7 +562,7 @@ export class Deployments { ClientRequestToken: randomUUID(), // Enabling this is just the better overall default, the only reason it isn't the upstream default is backwards compatibility RetainExceptOnCreate: true, - }).promise(); + }); break; case RollbackChoice.CONTINUE_UPDATE_ROLLBACK: @@ -552,33 +576,35 @@ export class Deployments { }); await poller.poll(); resourcesToSkip = poller.resourceErrors - .filter(r => !r.isStackEvent && r.parentStackLogicalIds.length === 0) - .map(r => r.event.LogicalResourceId ?? ''); + .filter((r) => !r.isStackEvent && r.parentStackLogicalIds.length === 0) + .map((r) => r.event.LogicalResourceId ?? ''); } - const skipDescription = resourcesToSkip.length > 0 - ? ` (orphaning: ${resourcesToSkip.join(', ')})` - : ''; + const skipDescription = resourcesToSkip.length > 0 ? ` (orphaning: ${resourcesToSkip.join(', ')})` : ''; warning(`Continuing rollback of stack ${deployName}${skipDescription}`); await cfn.continueUpdateRollback({ StackName: deployName, ClientRequestToken: randomUUID(), RoleARN: cloudFormationRoleArn, ResourcesToSkip: resourcesToSkip, - }).promise(); + }); break; case RollbackChoice.ROLLBACK_FAILED: - warning(`Stack ${deployName} failed creation and rollback. This state cannot be rolled back. You can recreate this stack by running 'cdk deploy'.`); + warning( + `Stack ${deployName} failed creation and rollback. This state cannot be rolled back. You can recreate this stack by running 'cdk deploy'.`, + ); return { notInRollbackableState: true }; default: throw new Error(`Unexpected rollback choice: ${cloudFormationStack.stackStatus.rollbackChoice}`); } - const monitor = options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, { - ci: options.ci, - }).start(); + const monitor = options.quiet + ? undefined + : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, { + ci: options.ci, + }).start(); let stackErrorMessage: string | undefined = undefined; let finalStackState = cloudFormationStack; @@ -586,7 +612,9 @@ export class Deployments { const successStack = await stabilizeStack(cfn, deployName); // This shouldn't really happen, but catch it anyway. You never know. - if (!successStack) { throw new Error('Stack deploy failed (the stack disappeared while we were rolling it back)'); } + if (!successStack) { + throw new Error('Stack deploy failed (the stack disappeared while we were rolling it back)'); + } finalStackState = successStack; const errors = monitor?.errors?.join(', '); @@ -609,13 +637,21 @@ export class Deployments { continue; } - throw new Error(`${stackErrorMessage} (fix problem and retry, or orphan these resources using --orphan or --force)`);; + throw new Error( + `${stackErrorMessage} (fix problem and retry, or orphan these resources using --orphan or --force)`, + ); } - throw new Error('Rollback did not finish after a large number of iterations; stopping because it looks like we\'re not making progress anymore. You can retry if rollback was progressing as expected.'); + throw new Error( + "Rollback did not finish after a large number of iterations; stopping because it looks like we're not making progress anymore. You can retry if rollback was progressing as expected.", + ); } public async destroyStack(options: DestroyStackOptions): Promise { - const { stackSdk, cloudFormationRoleArn: roleArn } = await this.prepareSdkFor(options.stack, options.roleArn, Mode.ForWriting); + const { stackSdk, cloudFormationRoleArn: roleArn } = await this.prepareSdkFor( + options.stack, + options.roleArn, + Mode.ForWriting, + ); return destroyStack({ sdk: stackSdk, @@ -634,15 +670,22 @@ export class Deployments { } else { stackSdk = (await this.prepareSdkFor(options.stack, undefined, Mode.ForReading)).stackSdk; } - const stack = await CloudFormationStack.lookup(stackSdk.cloudFormation(), options.deployName ?? options.stack.stackName); + const stack = await CloudFormationStack.lookup( + stackSdk.cloudFormation(), + options.deployName ?? options.stack.stackName, + ); return stack.exists; } - public async prepareSdkWithDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise { + public async prepareSdkWithDeployRole( + stackArtifact: cxapi.CloudFormationStackArtifact, + ): Promise { return this.prepareSdkFor(stackArtifact, undefined, Mode.ForWriting); } - private async prepareSdkWithLookupOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise { + private async prepareSdkWithLookupOrDeployRole( + stackArtifact: cxapi.CloudFormationStackArtifact, + ): Promise { // try to assume the lookup role try { const result = await this.prepareSdkWithLookupRoleFor(stackArtifact); @@ -653,7 +696,7 @@ export class Deployments { envResources: result.envResources, }; } - } catch { } + } catch {} // fall back to the deploy role return this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading); } @@ -679,54 +722,64 @@ export class Deployments { const resolvedEnvironment = await this.resolveEnvironment(stack); // Substitute any placeholders with information about the current environment - const arns = await replaceEnvPlaceholders({ - assumeRoleArn: stack.assumeRoleArn, + const arns = await replaceEnvPlaceholders( + { + assumeRoleArn: stack.assumeRoleArn, - // Use the override if given, otherwise use the field from the stack - cloudFormationRoleArn: roleArn ?? stack.cloudFormationExecutionRoleArn, - }, resolvedEnvironment, this.sdkProvider); + // Use the override if given, otherwise use the field from the stack + cloudFormationRoleArn: roleArn ?? stack.cloudFormationExecutionRoleArn, + }, + resolvedEnvironment, + this.sdkProvider, + ); - const stackSdk = await this.cachedSdkForEnvironment(resolvedEnvironment, mode, { - assumeRoleArn: arns.assumeRoleArn, - assumeRoleExternalId: stack.assumeRoleExternalId, - assumeRoleAdditionalOptions: stack.assumeRoleAdditionalOptions, - }); + const stackSdk = ( + await this.cachedSdkForEnvironment(resolvedEnvironment, mode, { + assumeRoleArn: arns.assumeRoleArn, + assumeRoleExternalId: stack.assumeRoleExternalId, + assumeRoleAdditionalOptions: stack.assumeRoleAdditionalOptions, + }) + ).sdk; return { - stackSdk: stackSdk.sdk, + stackSdk: stackSdk, resolvedEnvironment, cloudFormationRoleArn: arns.cloudFormationRoleArn, - envResources: this.environmentResources.for(resolvedEnvironment, stackSdk.sdk), + envResources: this.environmentResources.for(resolvedEnvironment, stackSdk), }; } /** - * Try to use the bootstrap lookupRole. There are two scenarios that are handled here - * 1. The lookup role may not exist (it was added in bootstrap stack version 7) - * 2. The lookup role may not have the correct permissions (ReadOnlyAccess was added in - * bootstrap stack version 8) - * - * In the case of 1 (lookup role doesn't exist) `forEnvironment` will either: - * 1. Return the default credentials if the default credentials are for the stack account - * 2. Throw an error if the default credentials are not for the stack account. - * - * If we successfully assume the lookup role we then proceed to 2 and check whether the bootstrap - * stack version is valid. If it is not we throw an error which should be handled in the calling - * function (and fallback to use a different role, etc) - * - * If we do not successfully assume the lookup role, but do get back the default credentials - * then return those and note that we are returning the default credentials. The calling - * function can then decide to use them or fallback to another role. - */ + * Try to use the bootstrap lookupRole. There are two scenarios that are handled here + * 1. The lookup role may not exist (it was added in bootstrap stack version 7) + * 2. The lookup role may not have the correct permissions (ReadOnlyAccess was added in + * bootstrap stack version 8) + * + * In the case of 1 (lookup role doesn't exist) `forEnvironment` will either: + * 1. Return the default credentials if the default credentials are for the stack account + * 2. Throw an error if the default credentials are not for the stack account. + * + * If we successfully assume the lookup role we then proceed to 2 and check whether the bootstrap + * stack version is valid. If it is not we throw an error which should be handled in the calling + * function (and fallback to use a different role, etc) + * + * If we do not successfully assume the lookup role, but do get back the default credentials + * then return those and note that we are returning the default credentials. The calling + * function can then decide to use them or fallback to another role. + */ public async prepareSdkWithLookupRoleFor( stack: cxapi.CloudFormationStackArtifact, ): Promise { const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment); // Substitute any placeholders with information about the current environment - const arns = await replaceEnvPlaceholders({ - lookupRoleArn: stack.lookupRole?.arn, - }, resolvedEnvironment, this.sdkProvider); + const arns = await replaceEnvPlaceholders( + { + lookupRoleArn: stack.lookupRole?.arn, + }, + resolvedEnvironment, + this.sdkProvider, + ); // try to assume the lookup role const warningMessage = `Could not assume ${arns.lookupRoleArn}, proceeding anyway.`; @@ -742,14 +795,22 @@ export class Deployments { const envResources = this.environmentResources.for(resolvedEnvironment, stackSdk.sdk); // if we succeed in assuming the lookup role, make sure we have the correct bootstrap stack version - if (stackSdk.didAssumeRole && stack.lookupRole?.bootstrapStackVersionSsmParameter && stack.lookupRole.requiresBootstrapStackVersion) { + if ( + stackSdk.didAssumeRole && + stack.lookupRole?.bootstrapStackVersionSsmParameter && + stack.lookupRole.requiresBootstrapStackVersion + ) { const version = await envResources.versionFromSsmParameter(stack.lookupRole.bootstrapStackVersionSsmParameter); if (version < stack.lookupRole.requiresBootstrapStackVersion) { - throw new Error(`Bootstrap stack version '${stack.lookupRole.requiresBootstrapStackVersion}' is required, found version '${version}'. To get rid of this error, please upgrade to bootstrap version >= ${stack.lookupRole.requiresBootstrapStackVersion}`); + throw new Error( + `Bootstrap stack version '${stack.lookupRole.requiresBootstrapStackVersion}' is required, found version '${version}'. To get rid of this error, please upgrade to bootstrap version >= ${stack.lookupRole.requiresBootstrapStackVersion}`, + ); } } else if (!stackSdk.didAssumeRole) { const lookUpRoleExists = stack.lookupRole ? true : false; - warning(`Lookup role ${ lookUpRoleExists ? 'exists but' : 'does not exist, hence'} was not assumed. Proceeding with default credentials.`); + warning( + `Lookup role ${lookUpRoleExists ? 'exists but' : 'does not exist, hence'} was not assumed. Proceeding with default credentials.`, + ); } return { ...stackSdk, resolvedEnvironment, envResources }; } catch (e: any) { @@ -764,8 +825,7 @@ export class Deployments { if (e instanceof Error && e.message.includes('Bootstrap stack version')) { error(e.message); } - - throw (e); + throw e; } } @@ -776,7 +836,8 @@ export class Deployments { options.stack.stackName, asset.requiresBootstrapStackVersion, asset.bootstrapStackVersionSsmParameter, - envResources); + envResources, + ); const manifest = AssetManifest.fromFile(asset.file); @@ -806,15 +867,24 @@ export class Deployments { /** * Build a single asset from an asset manifest */ - // eslint-disable-next-line max-len - public async buildSingleAsset(assetArtifact: cxapi.AssetManifestArtifact, assetManifest: AssetManifest, asset: IManifestEntry, options: BuildStackAssetsOptions) { - const { resolvedEnvironment, envResources } = await this.prepareSdkFor(options.stack, options.roleArn, Mode.ForWriting); + public async buildSingleAsset( + assetArtifact: cxapi.AssetManifestArtifact, + assetManifest: AssetManifest, + asset: IManifestEntry, + options: BuildStackAssetsOptions, + ) { + const { resolvedEnvironment, envResources } = await this.prepareSdkFor( + options.stack, + options.roleArn, + Mode.ForWriting, + ); await this.validateBootstrapStackVersion( options.stack.stackName, assetArtifact.requiresBootstrapStackVersion, assetArtifact.bootstrapStackVersionSsmParameter, - envResources); + envResources, + ); const publisher = this.cachedPublisher(assetManifest, resolvedEnvironment, options.stackName); await publisher.buildEntry(asset); @@ -827,7 +897,11 @@ export class Deployments { * Publish a single asset from an asset manifest */ // eslint-disable-next-line max-len - public async publishSingleAsset(assetManifest: AssetManifest, asset: IManifestEntry, options: PublishStackAssetsOptions) { + public async publishSingleAsset( + assetManifest: AssetManifest, + asset: IManifestEntry, + options: PublishStackAssetsOptions, + ) { const { resolvedEnvironment: stackEnv } = await this.prepareSdkFor(options.stack, options.roleArn, Mode.ForWriting); // No need to validate anymore, we already did that during build @@ -841,7 +915,11 @@ export class Deployments { /** * Return whether a single asset has been published already */ - public async isSingleAssetPublished(assetManifest: AssetManifest, asset: IManifestEntry, options: PublishStackAssetsOptions) { + public async isSingleAssetPublished( + assetManifest: AssetManifest, + asset: IManifestEntry, + options: PublishStackAssetsOptions, + ) { const { resolvedEnvironment: stackEnv } = await this.prepareSdkFor(options.stack, options.roleArn, Mode.ForWriting); const publisher = this.cachedPublisher(assetManifest, stackEnv, options.stackName); return publisher.isEntryPublished(asset); @@ -856,8 +934,8 @@ export class Deployments { stackName: string, requiresBootstrapStackVersion: number | undefined, bootstrapStackVersionSsmParameter: string | undefined, - envResources: EnvironmentResources) { - + envResources: EnvironmentResources, + ) { try { await envResources.validateVersion(requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter); } catch (e: any) { @@ -865,11 +943,7 @@ export class Deployments { } } - private async cachedSdkForEnvironment( - environment: cxapi.Environment, - mode: Mode, - options?: CredentialsOptions, - ) { + private async cachedSdkForEnvironment(environment: cxapi.Environment, mode: Mode, options?: CredentialsOptions) { const cacheKeyElements = [ environment.account, environment.region, @@ -911,8 +985,10 @@ export class Deployments { * Asset progress that doesn't do anything with percentages (currently) */ class ParallelSafeAssetProgress implements cdk_assets.IPublishProgressListener { - constructor(private readonly prefix: string, private readonly quiet: boolean) { - } + constructor( + private readonly prefix: string, + private readonly quiet: boolean, + ) {} public onPublishEvent(type: cdk_assets.EventType, event: cdk_assets.IPublishProgress): void { const handler = this.quiet && type !== 'fail' ? debug : EVENT_TO_LOGGER[type]; @@ -920,14 +996,6 @@ class ParallelSafeAssetProgress implements cdk_assets.IPublishProgressListener { } } -/** - * @deprecated Use 'Deployments' instead - */ -export class CloudFormationDeployments extends Deployments { -} - function suffixWithErrors(msg: string, errors?: string[]) { - return errors && errors.length > 0 - ? `${msg}: ${errors.join(', ')}` - : msg; -} \ No newline at end of file + return errors && errors.length > 0 ? `${msg}: ${errors.join(', ')}` : msg; +} diff --git a/packages/aws-cdk/lib/api/environment-resources.ts b/packages/aws-cdk/lib/api/environment-resources.ts index 937e813ad8f95..0c6aa7f7476ee 100644 --- a/packages/aws-cdk/lib/api/environment-resources.ts +++ b/packages/aws-cdk/lib/api/environment-resources.ts @@ -1,6 +1,6 @@ -import * as cxapi from '@aws-cdk/cx-api'; -import { ISDK } from './aws-auth'; -import { EcrRepositoryInfo, ToolkitInfo } from './toolkit-info'; +import type { Environment } from '@aws-cdk/cx-api'; +import type { SDK } from './aws-auth'; +import { type EcrRepositoryInfo, ToolkitInfo } from './toolkit-info'; import { debug, warning } from '../logging'; import { Notices } from '../notices'; @@ -16,10 +16,9 @@ import { Notices } from '../notices'; export class EnvironmentResourcesRegistry { private readonly cache = new Map(); - constructor(private readonly toolkitStackName?: string) { - } + constructor(private readonly toolkitStackName?: string) {} - public for(resolvedEnvironment: cxapi.Environment, sdk: ISDK) { + public for(resolvedEnvironment: Environment, sdk: SDK) { const key = `${resolvedEnvironment.account}:${resolvedEnvironment.region}`; let envCache = this.cache.get(key); if (!envCache) { @@ -44,8 +43,8 @@ export class EnvironmentResourcesRegistry { */ export class EnvironmentResources { constructor( - public readonly environment: cxapi.Environment, - private readonly sdk: ISDK, + public readonly environment: Environment, + private readonly sdk: SDK, private readonly cache: EnvironmentCache, private readonly toolkitStackName?: string, ) {} @@ -81,7 +80,9 @@ export class EnvironmentResources { doValidate(await this.versionFromSsmParameter(ssmParameterName), this.environment); return; } catch (e: any) { - if (e.code !== 'AccessDeniedException') { throw e; } + if (e.name !== 'AccessDeniedException') { + throw e; + } // This is a fallback! The bootstrap template that goes along with this change introduces // a new 'ssm:GetParameter' permission, but when run using the previous bootstrap template we @@ -92,12 +93,16 @@ export class EnvironmentResources { // so let it fail as it would if we didn't have this fallback. const bootstrapStack = await this.lookupToolkit(); if (bootstrapStack.found && bootstrapStack.version < BOOTSTRAP_TEMPLATE_VERSION_INTRODUCING_GETPARAMETER) { - warning(`Could not read SSM parameter ${ssmParameterName}: ${e.message}, falling back to version from ${bootstrapStack}`); + warning( + `Could not read SSM parameter ${ssmParameterName}: ${e.message}, falling back to version from ${bootstrapStack}`, + ); doValidate(bootstrapStack.version, this.environment); return; } - throw new Error(`This CDK deployment requires bootstrap stack version '${expectedVersion}', but during the confirmation via SSM parameter ${ssmParameterName} the following error occurred: ${e}`); + throw new Error( + `This CDK deployment requires bootstrap stack version '${expectedVersion}', but during the confirmation via SSM parameter ${ssmParameterName} the following error occurred: ${e}`, + ); } } @@ -105,7 +110,7 @@ export class EnvironmentResources { const bootstrapStack = await this.lookupToolkit(); doValidate(bootstrapStack.version, this.environment); - function doValidate(version: number, environment: cxapi.Environment) { + function doValidate(version: number, environment: Environment) { const notices = Notices.get(); if (notices) { // if `Notices` hasn't been initialized there is probably a good @@ -113,7 +118,9 @@ export class EnvironmentResources { notices.addBootstrappedEnvironment({ bootstrapStackVersion: version, environment }); } if (defExpectedVersion > version) { - throw new Error(`This CDK deployment requires bootstrap stack version '${expectedVersion}', found '${version}'. Please run 'cdk bootstrap'.`); + throw new Error( + `This CDK deployment requires bootstrap stack version '${expectedVersion}', found '${version}'. Please run 'cdk bootstrap'.`, + ); } } } @@ -123,12 +130,14 @@ export class EnvironmentResources { */ public async versionFromSsmParameter(parameterName: string): Promise { const existing = this.cache.ssmParameters.get(parameterName); - if (existing !== undefined) { return existing; } + if (existing !== undefined) { + return existing; + } const ssm = this.sdk.ssm(); try { - const result = await ssm.getParameter({ Name: parameterName }).promise(); + const result = await ssm.getParameter({ Name: parameterName }); const asNumber = parseInt(`${result.Parameter?.Value}`, 10); if (isNaN(asNumber)) { @@ -138,8 +147,10 @@ export class EnvironmentResources { this.cache.ssmParameters.set(parameterName, asNumber); return asNumber; } catch (e: any) { - if (e.code === 'ParameterNotFound') { - throw new Error(`SSM parameter ${parameterName} not found. Has the environment been bootstrapped? Please run \'cdk bootstrap\' (see https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html)`); + if (e.name === 'ParameterNotFound') { + throw new Error( + `SSM parameter ${parameterName} not found. Has the environment been bootstrapped? Please run \'cdk bootstrap\' (see https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html)`, + ); } throw e; } @@ -154,19 +165,26 @@ export class EnvironmentResources { // check if repo already exists try { debug(`${repositoryName}: checking if ECR repository already exists`); - const describeResponse = await ecr.describeRepositories({ repositoryNames: [repositoryName] }).promise(); + const describeResponse = await ecr.describeRepositories({ + repositoryNames: [repositoryName], + }); const existingRepositoryUri = describeResponse.repositories![0]?.repositoryUri; if (existingRepositoryUri) { return { repositoryUri: existingRepositoryUri }; } } catch (e: any) { - if (e.code !== 'RepositoryNotFoundException') { throw e; } + if (e.name !== 'RepositoryNotFoundException') { + throw e; + } } // create the repo (tag it so it will be easier to garbage collect in the future) debug(`${repositoryName}: creating ECR repository`); const assetTag = { Key: 'awscdk:asset', Value: 'true' }; - const response = await ecr.createRepository({ repositoryName, tags: [assetTag] }).promise(); + const response = await ecr.createRepository({ + repositoryName, + tags: [assetTag], + }); const repositoryUri = response.repository?.repositoryUri; if (!repositoryUri) { throw new Error(`CreateRepository did not return a repository URI for ${repositoryUri}`); @@ -174,14 +192,17 @@ export class EnvironmentResources { // configure image scanning on push (helps in identifying software vulnerabilities, no additional charge) debug(`${repositoryName}: enable image scanning`); - await ecr.putImageScanningConfiguration({ repositoryName, imageScanningConfiguration: { scanOnPush: true } }).promise(); + await ecr.putImageScanningConfiguration({ + repositoryName, + imageScanningConfiguration: { scanOnPush: true }, + }); return { repositoryUri }; } } export class NoBootstrapStackEnvironmentResources extends EnvironmentResources { - constructor(environment: cxapi.Environment, sdk: ISDK) { + constructor(environment: Environment, sdk: SDK) { super(environment, sdk, emptyCache()); } @@ -189,7 +210,9 @@ export class NoBootstrapStackEnvironmentResources extends EnvironmentResources { * Look up the toolkit for a given environment, using a given SDK */ public async lookupToolkit(): Promise { - throw new Error('Trying to perform an operation that requires a bootstrap stack; you should not see this error, this is a bug in the CDK CLI.'); + throw new Error( + 'Trying to perform an operation that requires a bootstrap stack; you should not see this error, this is a bug in the CDK CLI.', + ); } } diff --git a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts index bf89af5ac6021..75c96c4eb6ad3 100644 --- a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts +++ b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts @@ -1,52 +1,41 @@ -import * as AWS from 'aws-sdk'; -import { PromiseResult } from 'aws-sdk/lib/request'; -import { ISDK } from './aws-auth'; -import { NestedStackTemplates } from './nested-stack-helpers'; +import type { Export, ListExportsCommandOutput, StackResourceSummary } from '@aws-sdk/client-cloudformation'; +import type { SDK } from './aws-auth'; +import type { NestedStackTemplates } from './nested-stack-helpers'; export interface ListStackResources { - listStackResources(): Promise; + listStackResources(): Promise; } export class LazyListStackResources implements ListStackResources { - private stackResources: Promise | undefined; + private stackResources: Promise | undefined; - constructor(private readonly sdk: ISDK, private readonly stackName: string) { - } + constructor( + private readonly sdk: SDK, + private readonly stackName: string, + ) {} - public async listStackResources(): Promise { + public async listStackResources(): Promise { if (this.stackResources === undefined) { - this.stackResources = this.getStackResources(undefined); + this.stackResources = this.sdk.cloudFormation().listStackResources({ + StackName: this.stackName, + }); } return this.stackResources; } - - private async getStackResources(nextToken: string | undefined): Promise { - const ret = new Array(); - return this.sdk.cloudFormation().listStackResources({ - StackName: this.stackName, - NextToken: nextToken, - }).promise().then(async stackResourcesResponse => { - ret.push(...(stackResourcesResponse.StackResourceSummaries ?? [])); - if (stackResourcesResponse.NextToken) { - ret.push(...await this.getStackResources(stackResourcesResponse.NextToken)); - } - return ret; - }); - } } export interface LookupExport { - lookupExport(name: string): Promise; + lookupExport(name: string): Promise; } -export class LookupExportError extends Error { } +export class LookupExportError extends Error {} export class LazyLookupExport implements LookupExport { - private cachedExports: { [name: string]: AWS.CloudFormation.Export } = {} + private cachedExports: { [name: string]: Export } = {}; - constructor(private readonly sdk: ISDK) { } + constructor(private readonly sdk: SDK) {} - async lookupExport(name: string): Promise { + async lookupExport(name: string): Promise { if (this.cachedExports[name]) { return this.cachedExports[name]; } @@ -60,19 +49,16 @@ export class LazyLookupExport implements LookupExport { if (cfnExport.Name === name) { return cfnExport; } - } return undefined; // export not found } - private async * listExports() { + // TODO: Paginate + private async *listExports() { let nextToken: string | undefined = undefined; while (true) { - const response: PromiseResult = await this.sdk.cloudFormation().listExports({ - NextToken: nextToken, - }).promise(); - + const response: ListExportsCommandOutput = await this.sdk.cloudFormation().listExports({ NextToken: nextToken }); for (const cfnExport of response.Exports ?? []) { yield cfnExport; } @@ -85,7 +71,7 @@ export class LazyLookupExport implements LookupExport { } } -export class CfnEvaluationException extends Error { } +export class CfnEvaluationException extends Error {} export interface ResourceDefinition { readonly LogicalId: string; @@ -100,9 +86,10 @@ export interface EvaluateCloudFormationTemplateProps { readonly account: string; readonly region: string; readonly partition: string; - readonly urlSuffix: (region: string) => string; - readonly sdk: ISDK; - readonly nestedStacks?: { [nestedStackLogicalId: string]: NestedStackTemplates }; + readonly sdk: SDK; + readonly nestedStacks?: { + [nestedStackLogicalId: string]: NestedStackTemplates; + }; } export class EvaluateCloudFormationTemplate { @@ -112,9 +99,10 @@ export class EvaluateCloudFormationTemplate { private readonly account: string; private readonly region: string; private readonly partition: string; - private readonly urlSuffix: (region: string) => string; - private readonly sdk: ISDK; - private readonly nestedStacks: { [nestedStackLogicalId: string]: NestedStackTemplates }; + private readonly sdk: SDK; + private readonly nestedStacks: { + [nestedStackLogicalId: string]: NestedStackTemplates; + }; private readonly stackResources: ListStackResources; private readonly lookupExport: LookupExport; @@ -132,7 +120,6 @@ export class EvaluateCloudFormationTemplate { this.account = props.account; this.region = props.region; this.partition = props.partition; - this.urlSuffix = props.urlSuffix; this.sdk = props.sdk; // We need names of nested stack so we can evaluate cross stack references @@ -161,13 +148,15 @@ export class EvaluateCloudFormationTemplate { account: this.account, region: this.region, partition: this.partition, - urlSuffix: this.urlSuffix, sdk: this.sdk, nestedStacks: this.nestedStacks, }); } - public async establishResourcePhysicalName(logicalId: string, physicalNameInCfnTemplate: any): Promise { + public async establishResourcePhysicalName( + logicalId: string, + physicalNameInCfnTemplate: any, + ): Promise { if (physicalNameInCfnTemplate != null) { try { return await this.evaluateCfnExpression(physicalNameInCfnTemplate); @@ -184,12 +173,12 @@ export class EvaluateCloudFormationTemplate { public async findPhysicalNameFor(logicalId: string): Promise { const stackResources = await this.stackResources.listStackResources(); - return stackResources.find(sr => sr.LogicalResourceId === logicalId)?.PhysicalResourceId; + return stackResources.find((sr) => sr.LogicalResourceId === logicalId)?.PhysicalResourceId; } public async findLogicalIdForPhysicalName(physicalName: string): Promise { const stackResources = await this.stackResources.listStackResources(); - return stackResources.find(sr => sr.PhysicalResourceId === physicalName)?.LogicalResourceId; + return stackResources.find((sr) => sr.PhysicalResourceId === physicalName)?.LogicalResourceId; } public findReferencesTo(logicalId: string): Array { @@ -242,7 +231,7 @@ export class EvaluateCloudFormationTemplate { return evaluatedArgs[index]; } - async 'Ref'(logicalId: string): Promise { + async Ref(logicalId: string): Promise { const refTarget = await self.findRefTarget(logicalId); if (refTarget) { return refTarget; @@ -257,23 +246,21 @@ export class EvaluateCloudFormationTemplate { if (attrValue) { return attrValue; } else { - throw new CfnEvaluationException(`Attribute '${attributeName}' of resource '${logicalId}' could not be found for evaluation`); + throw new CfnEvaluationException( + `Attribute '${attributeName}' of resource '${logicalId}' could not be found for evaluation`, + ); } } async 'Fn::Sub'(template: string, explicitPlaceholders?: { [variable: string]: string }): Promise { - const placeholders = explicitPlaceholders - ? await self.evaluateCfnExpression(explicitPlaceholders) - : {}; + const placeholders = explicitPlaceholders ? await self.evaluateCfnExpression(explicitPlaceholders) : {}; - return asyncGlobalReplace(template, /\${([^}]*)}/g, key => { + return asyncGlobalReplace(template, /\${([^}]*)}/g, (key) => { if (key in placeholders) { return placeholders[key]; } else { const splitKey = key.split('.'); - return splitKey.length === 1 - ? this.Ref(key) - : this['Fn::GetAtt'](splitKey[0], splitKey.slice(1).join('.')); + return splitKey.length === 1 ? this.Ref(key) : this['Fn::GetAtt'](splitKey[0], splitKey.slice(1).join('.')); } }); } @@ -297,7 +284,7 @@ export class EvaluateCloudFormationTemplate { if (Array.isArray(cfnExpression)) { // Small arrays in practice // eslint-disable-next-line @aws-cdk/promiseall-no-unbounded-parallelism - return Promise.all(cfnExpression.map(expr => this.evaluateCfnExpression(expr))); + return Promise.all(cfnExpression.map((expr) => this.evaluateCfnExpression(expr))); } if (typeof cfnExpression === 'object') { @@ -330,11 +317,11 @@ export class EvaluateCloudFormationTemplate { } if (Array.isArray(templateElement)) { - return templateElement.some(el => this.references(logicalId, el)); + return templateElement.some((el) => this.references(logicalId, el)); } if (typeof templateElement === 'object') { - return Object.values(templateElement).some(el => this.references(logicalId, el)); + return Object.values(templateElement).some((el) => this.references(logicalId, el)); } return false; @@ -355,7 +342,7 @@ export class EvaluateCloudFormationTemplate { // first, check to see if the Ref is a Parameter who's value we have if (logicalId === 'AWS::URLSuffix') { if (!this.cachedUrlSuffix) { - this.cachedUrlSuffix = this.urlSuffix(this.region); + this.cachedUrlSuffix = await this.sdk.getUrlSuffix(this.region); } return this.cachedUrlSuffix; @@ -378,7 +365,6 @@ export class EvaluateCloudFormationTemplate { } private async findGetAttTarget(logicalId: string, attribute?: string): Promise { - // Handle case where the attribute is referencing a stack output (used in nested stacks to share parameters) // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-cloudformation.html#w2ab1c17c23c19b5 if (logicalId === 'Outputs' && attribute) { @@ -386,7 +372,7 @@ export class EvaluateCloudFormationTemplate { } const stackResources = await this.stackResources.listStackResources(); - const foundResource = stackResources.find(sr => sr.LogicalResourceId === logicalId); + const foundResource = stackResources.find((sr) => sr.LogicalResourceId === logicalId); if (!foundResource) { return undefined; } @@ -400,30 +386,39 @@ export class EvaluateCloudFormationTemplate { const evaluateCfnTemplate = await this.createNestedEvaluateCloudFormationTemplate( dependantStack.physicalName, dependantStack.generatedTemplate, - dependantStack.generatedTemplate.Parameters!); + dependantStack.generatedTemplate.Parameters!, + ); // Split Outputs. into 'Outputs' and '' and recursively call evaluate - return evaluateCfnTemplate.evaluateCfnExpression({ 'Fn::GetAtt': attribute.split(/\.(.*)/s) }); + return evaluateCfnTemplate.evaluateCfnExpression({ + 'Fn::GetAtt': attribute.split(/\.(.*)/s), + }); } // now, we need to format the appropriate identifier depending on the resource type, // and the requested attribute name return this.formatResourceAttribute(foundResource, attribute); } - private findNestedStack(logicalId: string, nestedStacks: { - [nestedStackLogicalId: string]: NestedStackTemplates; - }): NestedStackTemplates | undefined { + private findNestedStack( + logicalId: string, + nestedStacks: { + [nestedStackLogicalId: string]: NestedStackTemplates; + }, + ): NestedStackTemplates | undefined { for (const nestedStackLogicalId of Object.keys(nestedStacks)) { if (nestedStackLogicalId === logicalId) { return nestedStacks[nestedStackLogicalId]; } - const checkInNestedChildStacks = this.findNestedStack(logicalId, nestedStacks[nestedStackLogicalId].nestedStackTemplates); + const checkInNestedChildStacks = this.findNestedStack( + logicalId, + nestedStacks[nestedStackLogicalId].nestedStackTemplates, + ); if (checkInNestedChildStacks) return checkInNestedChildStacks; } return undefined; } - private formatResourceAttribute(resource: AWS.CloudFormation.StackResourceSummary, attribute: string | undefined): string | undefined { + private formatResourceAttribute(resource: StackResourceSummary, attribute: string | undefined): string | undefined { const physicalId = resource.PhysicalResourceId; // no attribute means Ref expression, for which we use the physical ID directly @@ -431,15 +426,19 @@ export class EvaluateCloudFormationTemplate { return physicalId; } - const resourceTypeFormats = RESOURCE_TYPE_ATTRIBUTES_FORMATS[resource.ResourceType]; + const resourceTypeFormats = RESOURCE_TYPE_ATTRIBUTES_FORMATS[resource.ResourceType!]; if (!resourceTypeFormats) { - throw new CfnEvaluationException(`We don't support attributes of the '${resource.ResourceType}' resource. This is a CDK limitation. ` + - 'Please report it at https://github.com/aws/aws-cdk/issues/new/choose'); + throw new CfnEvaluationException( + `We don't support attributes of the '${resource.ResourceType}' resource. This is a CDK limitation. ` + + 'Please report it at https://github.com/aws/aws-cdk/issues/new/choose', + ); } const attributeFmtFunc = resourceTypeFormats[attribute]; if (!attributeFmtFunc) { - throw new CfnEvaluationException(`We don't support the '${attribute}' attribute of the '${resource.ResourceType}' resource. This is a CDK limitation. ` + - 'Please report it at https://github.com/aws/aws-cdk/issues/new/choose'); + throw new CfnEvaluationException( + `We don't support the '${attribute}' attribute of the '${resource.ResourceType}' resource. This is a CDK limitation. ` + + 'Please report it at https://github.com/aws/aws-cdk/issues/new/choose', + ); } const service = this.getServiceOfResource(resource); const resourceTypeArnPart = this.getResourceTypeArnPartOfResource(resource); @@ -453,17 +452,17 @@ export class EvaluateCloudFormationTemplate { }); } - private getServiceOfResource(resource: AWS.CloudFormation.StackResourceSummary): string { - return resource.ResourceType.split('::')[1].toLowerCase(); + private getServiceOfResource(resource: StackResourceSummary): string { + return resource.ResourceType!.split('::')[1].toLowerCase(); } - private getResourceTypeArnPartOfResource(resource: AWS.CloudFormation.StackResourceSummary): string { - const resourceType = resource.ResourceType; + private getResourceTypeArnPartOfResource(resource: StackResourceSummary): string { + const resourceType = resource.ResourceType!; const specialCaseResourceType = RESOURCE_TYPE_SPECIAL_NAMES[resourceType]?.resourceType; return specialCaseResourceType ? specialCaseResourceType - // this is the default case - : resourceType.split('::')[2].toLowerCase(); + : // this is the default case + resourceType.split('::')[2].toLowerCase(); } } @@ -485,13 +484,17 @@ interface ArnParts { * However, some resource types break this simple convention, and we need to special-case them. * This map is for storing those cases. */ -const RESOURCE_TYPE_SPECIAL_NAMES: { [type: string]: { resourceType: string } } = { +const RESOURCE_TYPE_SPECIAL_NAMES: { + [type: string]: { resourceType: string }; +} = { 'AWS::Events::EventBus': { resourceType: 'event-bus', }, }; -const RESOURCE_TYPE_ATTRIBUTES_FORMATS: { [type: string]: { [attribute: string]: (parts: ArnParts) => string } } = { +const RESOURCE_TYPE_ATTRIBUTES_FORMATS: { + [type: string]: { [attribute: string]: (parts: ArnParts) => string }; +} = { 'AWS::IAM::Role': { Arn: iamArnFmt }, 'AWS::IAM::User': { Arn: iamArnFmt }, 'AWS::IAM::Group': { Arn: iamArnFmt }, @@ -500,11 +503,13 @@ const RESOURCE_TYPE_ATTRIBUTES_FORMATS: { [type: string]: { [attribute: string]: 'AWS::Events::EventBus': { Arn: stdSlashResourceArnFmt, // the name attribute of the EventBus is the same as the Ref - Name: parts => parts.resourceName, + Name: (parts) => parts.resourceName, }, 'AWS::DynamoDB::Table': { Arn: stdSlashResourceArnFmt }, 'AWS::AppSync::GraphQLApi': { ApiId: appsyncGraphQlApiApiIdFmt }, - 'AWS::AppSync::FunctionConfiguration': { FunctionId: appsyncGraphQlFunctionIDFmt }, + 'AWS::AppSync::FunctionConfiguration': { + FunctionId: appsyncGraphQlFunctionIDFmt, + }, 'AWS::AppSync::DataSource': { Name: appsyncGraphQlDataSourceNameFmt }, 'AWS::KMS::Key': { Arn: stdSlashResourceArnFmt }, }; @@ -550,13 +555,17 @@ interface Intrinsic { } async function asyncGlobalReplace(str: string, regex: RegExp, cb: (x: string) => Promise): Promise { - if (!regex.global) { throw new Error('Regex must be created with /g flag'); } + if (!regex.global) { + throw new Error('Regex must be created with /g flag'); + } const ret = new Array(); let start = 0; while (true) { const match = regex.exec(str); - if (!match) { break; } + if (!match) { + break; + } ret.push(str.substring(start, match.index)); ret.push(await cb(match[1])); diff --git a/packages/aws-cdk/lib/api/hotswap-deployments.ts b/packages/aws-cdk/lib/api/hotswap-deployments.ts index 8aed36f033fc3..dc3d3657b8066 100644 --- a/packages/aws-cdk/lib/api/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap-deployments.ts @@ -1,18 +1,32 @@ import * as cfn_diff from '@aws-cdk/cloudformation-diff'; import * as cxapi from '@aws-cdk/cx-api'; import * as chalk from 'chalk'; -import { ISDK, Mode, SdkProvider } from './aws-auth'; -import { DeployStackResult } from './deploy-stack'; +import type { SDK, SdkProvider } from './aws-auth'; +import type { DeployStackResult } from './deploy-stack'; import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template'; import { print } from '../logging'; import { isHotswappableAppSyncChange } from './hotswap/appsync-mapping-templates'; import { isHotswappableCodeBuildProjectChange } from './hotswap/code-build-projects'; -import { ICON, ChangeHotswapResult, HotswapMode, HotswappableChange, NonHotswappableChange, HotswappableChangeCandidate, ClassifiedResourceChanges, reportNonHotswappableChange, reportNonHotswappableResource } from './hotswap/common'; +import { + ICON, + ChangeHotswapResult, + HotswapMode, + HotswappableChange, + NonHotswappableChange, + HotswappableChangeCandidate, + ClassifiedResourceChanges, + reportNonHotswappableChange, + reportNonHotswappableResource, +} from './hotswap/common'; import { isHotswappableEcsServiceChange } from './hotswap/ecs-services'; import { isHotswappableLambdaFunctionChange } from './hotswap/lambda-functions'; -import { skipChangeForS3DeployCustomResourcePolicy, isHotswappableS3BucketDeploymentChange } from './hotswap/s3-bucket-deployments'; +import { + skipChangeForS3DeployCustomResourcePolicy, + isHotswappableS3BucketDeploymentChange, +} from './hotswap/s3-bucket-deployments'; import { isHotswappableStateMachineChange } from './hotswap/stepfunctions-state-machines'; import { NestedStackTemplates, loadCurrentTemplateWithNestedStacks } from './nested-stack-helpers'; +import { Mode } from './plugin'; import { CloudFormationStack } from './util/cloudformation'; // Must use a require() otherwise esbuild complains about calling a namespace @@ -20,7 +34,9 @@ import { CloudFormationStack } from './util/cloudformation'; const pLimit: typeof import('p-limit') = require('p-limit'); type HotswapDetector = ( - logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate + logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ) => Promise; const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = { @@ -40,7 +56,9 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = { 'AWS::StepFunctions::StateMachine': isHotswappableStateMachineChange, 'Custom::CDKBucketDeployment': isHotswappableS3BucketDeploymentChange, 'AWS::IAM::Policy': async ( - logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise => { // If the policy is for a S3BucketDeploymentChange, we can ignore the change if (await skipChangeForS3DeployCustomResourcePolicy(logicalId, change, evaluateCfnTemplate)) { @@ -60,8 +78,10 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = { * returns `undefined`. */ export async function tryHotswapDeployment( - sdkProvider: SdkProvider, assetParams: { [key: string]: string }, - cloudFormationStack: CloudFormationStack, stackArtifact: cxapi.CloudFormationStackArtifact, + sdkProvider: SdkProvider, + assetParams: { [key: string]: string }, + cloudFormationStack: CloudFormationStack, + stackArtifact: cxapi.CloudFormationStackArtifact, hotswapMode: HotswapMode, ): Promise { // resolve the environment, so we can substitute things like AWS::Region in CFN expressions @@ -79,14 +99,16 @@ export async function tryHotswapDeployment( account: resolvedEnv.account, region: resolvedEnv.region, partition: (await sdk.currentAccount()).partition, - urlSuffix: (region) => sdk.getEndpointSuffix(region), sdk, nestedStacks: currentTemplate.nestedStacks, }); const stackChanges = cfn_diff.fullDiff(currentTemplate.deployedRootTemplate, stackArtifact.template); const { hotswappableChanges, nonHotswappableChanges } = await classifyResourceChanges( - stackChanges, evaluateCfnTemplate, sdk, currentTemplate.nestedStacks, + stackChanges, + evaluateCfnTemplate, + sdk, + currentTemplate.nestedStacks, ); logNonHotswappableChanges(nonHotswappableChanges, hotswapMode); @@ -101,7 +123,11 @@ export async function tryHotswapDeployment( // apply the short-circuitable changes await applyAllHotswappableChanges(sdk, hotswappableChanges); - return { noOp: hotswappableChanges.length === 0, stackArn: cloudFormationStack.stackId, outputs: cloudFormationStack.outputs }; + return { + noOp: hotswappableChanges.length === 0, + stackArn: cloudFormationStack.stackId, + outputs: cloudFormationStack.outputs, + }; } /** @@ -111,7 +137,7 @@ export async function tryHotswapDeployment( async function classifyResourceChanges( stackChanges: cfn_diff.TemplateDiff, evaluateCfnTemplate: EvaluateCloudFormationTemplate, - sdk: ISDK, + sdk: SDK, nestedStackNames: { [nestedStackName: string]: NestedStackTemplates }, ): Promise { const resourceDifferences = getStackResourceDifferences(stackChanges); @@ -130,8 +156,17 @@ async function classifyResourceChanges( } // gather the results of the detector functions for (const [logicalId, change] of Object.entries(resourceDifferences)) { - if (change.newValue?.Type === 'AWS::CloudFormation::Stack' && change.oldValue?.Type === 'AWS::CloudFormation::Stack') { - const nestedHotswappableResources = await findNestedHotswappableChanges(logicalId, change, nestedStackNames, evaluateCfnTemplate, sdk); + if ( + change.newValue?.Type === 'AWS::CloudFormation::Stack' && + change.oldValue?.Type === 'AWS::CloudFormation::Stack' + ) { + const nestedHotswappableResources = await findNestedHotswappableChanges( + logicalId, + change, + nestedStackNames, + evaluateCfnTemplate, + sdk, + ); hotswappableResources.push(...nestedHotswappableResources.hotswappableChanges); nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappableChanges); @@ -151,9 +186,16 @@ async function classifyResourceChanges( const resourceType: string = hotswappableChangeCandidate.newValue.Type; if (resourceType in RESOURCE_DETECTORS) { // run detector functions lazily to prevent unhandled promise rejections - promises.push(() => RESOURCE_DETECTORS[resourceType](logicalId, hotswappableChangeCandidate, evaluateCfnTemplate)); + promises.push(() => + RESOURCE_DETECTORS[resourceType](logicalId, hotswappableChangeCandidate, evaluateCfnTemplate), + ); } else { - reportNonHotswappableChange(nonHotswappableResources, hotswappableChangeCandidate, undefined, 'This resource type is not supported for hotswap deployments'); + reportNonHotswappableChange( + nonHotswappableResources, + hotswappableChangeCandidate, + undefined, + 'This resource type is not supported for hotswap deployments', + ); } } @@ -168,9 +210,9 @@ async function classifyResourceChanges( for (const resourceDetectionResults of changesDetectionResults) { for (const propertyResult of resourceDetectionResults) { - propertyResult.hotswappable ? - hotswappableResources.push(propertyResult) : - nonHotswappableResources.push(propertyResult); + propertyResult.hotswappable + ? hotswappableResources.push(propertyResult) + : nonHotswappableResources.push(propertyResult); } } @@ -185,12 +227,14 @@ async function classifyResourceChanges( * * @param stackChanges the collection of all changes to a given Stack */ -function getStackResourceDifferences(stackChanges: cfn_diff.TemplateDiff): { [logicalId: string]: cfn_diff.ResourceDifference } { +function getStackResourceDifferences(stackChanges: cfn_diff.TemplateDiff): { + [logicalId: string]: cfn_diff.ResourceDifference; +} { // we need to collapse logical ID rename changes into one change, // as they are represented in stackChanges as a pair of two changes: one addition and one removal const allResourceChanges: { [logId: string]: cfn_diff.ResourceDifference } = stackChanges.resources.changes; - const allRemovalChanges = filterDict(allResourceChanges, resChange => resChange.isRemoval); - const allNonRemovalChanges = filterDict(allResourceChanges, resChange => !resChange.isRemoval); + const allRemovalChanges = filterDict(allResourceChanges, (resChange) => resChange.isRemoval); + const allNonRemovalChanges = filterDict(allResourceChanges, (resChange) => !resChange.isRemoval); for (const [logId, nonRemovalChange] of Object.entries(allNonRemovalChanges)) { if (nonRemovalChange.isAddition) { const addChange = nonRemovalChange; @@ -218,12 +262,15 @@ function getStackResourceDifferences(stackChanges: cfn_diff.TemplateDiff): { [lo /** Filters an object with string keys based on whether the callback returns 'true' for the given value in the object. */ function filterDict(dict: { [key: string]: T }, func: (t: T) => boolean): { [key: string]: T } { - return Object.entries(dict).reduce((acc, [key, t]) => { - if (func(t)) { - acc[key] = t; - } - return acc; - }, {} as { [key: string]: T }); + return Object.entries(dict).reduce( + (acc, [key, t]) => { + if (func(t)) { + acc[key] = t; + } + return acc; + }, + {} as { [key: string]: T }, + ); } /** Finds any hotswappable changes in all nested stacks. */ @@ -232,38 +279,53 @@ async function findNestedHotswappableChanges( change: cfn_diff.ResourceDifference, nestedStackTemplates: { [nestedStackName: string]: NestedStackTemplates }, evaluateCfnTemplate: EvaluateCloudFormationTemplate, - sdk: ISDK, + sdk: SDK, ): Promise { const nestedStack = nestedStackTemplates[logicalId]; if (!nestedStack.physicalName) { return { hotswappableChanges: [], - nonHotswappableChanges: [{ - hotswappable: false, - logicalId, - reason: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`, - rejectedChanges: [], - resourceType: 'AWS::CloudFormation::Stack', - }], + nonHotswappableChanges: [ + { + hotswappable: false, + logicalId, + reason: `physical name for AWS::CloudFormation::Stack '${logicalId}' could not be found in CloudFormation, so this is a newly created nested stack and cannot be hotswapped`, + rejectedChanges: [], + resourceType: 'AWS::CloudFormation::Stack', + }, + ], }; } const evaluateNestedCfnTemplate = await evaluateCfnTemplate.createNestedEvaluateCloudFormationTemplate( - nestedStack.physicalName, nestedStack.generatedTemplate, change.newValue?.Properties?.Parameters, + nestedStack.physicalName, + nestedStack.generatedTemplate, + change.newValue?.Properties?.Parameters, ); const nestedDiff = cfn_diff.fullDiff( - nestedStackTemplates[logicalId].deployedTemplate, nestedStackTemplates[logicalId].generatedTemplate, + nestedStackTemplates[logicalId].deployedTemplate, + nestedStackTemplates[logicalId].generatedTemplate, ); - return classifyResourceChanges(nestedDiff, evaluateNestedCfnTemplate, sdk, nestedStackTemplates[logicalId].nestedStackTemplates); + return classifyResourceChanges( + nestedDiff, + evaluateNestedCfnTemplate, + sdk, + nestedStackTemplates[logicalId].nestedStackTemplates, + ); } /** Returns 'true' if a pair of changes is for the same resource. */ -function changesAreForSameResource(oldChange: cfn_diff.ResourceDifference, newChange: cfn_diff.ResourceDifference): boolean { - return oldChange.oldResourceType === newChange.newResourceType && +function changesAreForSameResource( + oldChange: cfn_diff.ResourceDifference, + newChange: cfn_diff.ResourceDifference, +): boolean { + return ( + oldChange.oldResourceType === newChange.newResourceType && // this isn't great, but I don't want to bring in something like underscore just for this comparison - JSON.stringify(oldChange.oldProperties) === JSON.stringify(newChange.newProperties); + JSON.stringify(oldChange.oldProperties) === JSON.stringify(newChange.newProperties) + ); } function makeRenameDifference( @@ -291,7 +353,8 @@ function makeRenameDifference( * Returns a `NonHotswappableChange` if the change is not hotswappable */ function isCandidateForHotswapping( - change: cfn_diff.ResourceDifference, logicalId: string, + change: cfn_diff.ResourceDifference, + logicalId: string, ): HotswappableChange | NonHotswappableChange | HotswappableChangeCandidate { // a resource has been removed OR a resource has been added; we can't short-circuit that change if (!change.oldValue) { @@ -331,7 +394,7 @@ function isCandidateForHotswapping( }; } -async function applyAllHotswappableChanges(sdk: ISDK, hotswappableChanges: HotswappableChange[]): Promise { +async function applyAllHotswappableChanges(sdk: SDK, hotswappableChanges: HotswappableChange[]): Promise { if (hotswappableChanges.length > 0) { print(`\n${ICON} hotswapping resources:`); } @@ -342,7 +405,7 @@ async function applyAllHotswappableChanges(sdk: ISDK, hotswappableChanges: Hotsw }))); } -async function applyHotswappableChange(sdk: ISDK, hotswapOperation: HotswappableChange): Promise { +async function applyHotswappableChange(sdk: SDK, hotswapOperation: HotswappableChange): Promise { // note the type of service that was successfully hotswapped in the User-Agent const customUserAgent = `cdk-hotswap/success-${hotswapOperation.service}`; sdk.appendCustomUserAgent(customUserAgent); @@ -381,15 +444,32 @@ function logNonHotswappableChanges(nonHotswappableChanges: NonHotswappableChange } } if (hotswapMode === HotswapMode.HOTSWAP_ONLY) { - print('\n%s %s', chalk.red('⚠️'), chalk.red('The following non-hotswappable changes were found. To reconcile these using CloudFormation, specify --hotswap-fallback')); + print( + '\n%s %s', + chalk.red('⚠️'), + chalk.red( + 'The following non-hotswappable changes were found. To reconcile these using CloudFormation, specify --hotswap-fallback', + ), + ); } else { print('\n%s %s', chalk.red('⚠️'), chalk.red('The following non-hotswappable changes were found:')); } for (const change of nonHotswappableChanges) { - change.rejectedChanges.length > 0 ? - print(' logicalID: %s, type: %s, rejected changes: %s, reason: %s', chalk.bold(change.logicalId), chalk.bold(change.resourceType), chalk.bold(change.rejectedChanges), chalk.red(change.reason)) : - print(' logicalID: %s, type: %s, reason: %s', chalk.bold(change.logicalId), chalk.bold(change.resourceType), chalk.red(change.reason)); + change.rejectedChanges.length > 0 + ? print( + ' logicalID: %s, type: %s, rejected changes: %s, reason: %s', + chalk.bold(change.logicalId), + chalk.bold(change.resourceType), + chalk.bold(change.rejectedChanges), + chalk.red(change.reason), + ) + : print( + ' logicalID: %s, type: %s, reason: %s', + chalk.bold(change.logicalId), + chalk.bold(change.resourceType), + chalk.red(change.reason), + ); } print(''); // newline diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index 91dcba4461f78..15af669c03c35 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -1,11 +1,22 @@ -import { GetSchemaCreationStatusRequest, GetSchemaCreationStatusResponse, ListFunctionsResponse, FunctionConfiguration } from 'aws-sdk/clients/appsync'; -import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; -import { ISDK } from '../aws-auth'; +import type { + GetSchemaCreationStatusCommandOutput, + GetSchemaCreationStatusCommandInput, +} from '@aws-sdk/client-appsync'; +import { + type ChangeHotswapResult, + classifyChanges, + type HotswappableChangeCandidate, + lowerCaseFirstCharacter, + transformObjectKeys, +} from './common'; +import type { SDK } from '../aws-auth'; -import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; +import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableAppSyncChange( - logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { const isResolver = change.newValue.Type === 'AWS::AppSync::Resolver'; const isFunction = change.newValue.Type === 'AWS::AppSync::FunctionConfiguration'; @@ -33,7 +44,10 @@ export async function isHotswappableAppSyncChange( const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); if (namesOfHotswappableChanges.length > 0) { let physicalName: string | undefined = undefined; - const arn = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, isFunction ? change.newValue.Properties?.Name : undefined); + const arn = await evaluateCfnTemplate.establishResourcePhysicalName( + logicalId, + isFunction ? change.newValue.Properties?.Name : undefined, + ); if (isResolver) { const arnParts = arn?.split('/'); physicalName = arnParts ? `${arnParts[3]}.${arnParts[5]}` : undefined; @@ -46,7 +60,7 @@ export async function isHotswappableAppSyncChange( propsChanged: namesOfHotswappableChanges, service: 'appsync', resourceNames: [`${change.newValue.Type} '${physicalName}'`], - apply: async (sdk: ISDK) => { + apply: async (sdk: SDK) => { if (!physicalName) { return; } @@ -68,26 +82,31 @@ export async function isHotswappableAppSyncChange( // resolve s3 location files as SDK doesn't take in s3 location but inline code if (sdkRequestObject.requestMappingTemplateS3Location) { - sdkRequestObject.requestMappingTemplate = (await fetchFileFromS3(sdkRequestObject.requestMappingTemplateS3Location, sdk))?.toString('utf8'); + sdkRequestObject.requestMappingTemplate = await fetchFileFromS3( + sdkRequestObject.requestMappingTemplateS3Location, + sdk, + ); delete sdkRequestObject.requestMappingTemplateS3Location; } if (sdkRequestObject.responseMappingTemplateS3Location) { - sdkRequestObject.responseMappingTemplate = (await fetchFileFromS3(sdkRequestObject.responseMappingTemplateS3Location, sdk))?.toString('utf8'); + sdkRequestObject.responseMappingTemplate = await fetchFileFromS3( + sdkRequestObject.responseMappingTemplateS3Location, + sdk, + ); delete sdkRequestObject.responseMappingTemplateS3Location; } if (sdkRequestObject.definitionS3Location) { - sdkRequestObject.definition = (await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk))?.toString('utf8'); + sdkRequestObject.definition = await fetchFileFromS3(sdkRequestObject.definitionS3Location, sdk); delete sdkRequestObject.definitionS3Location; } if (sdkRequestObject.codeS3Location) { - sdkRequestObject.code = (await fetchFileFromS3(sdkRequestObject.codeS3Location, sdk))?.toString('utf8'); + sdkRequestObject.code = await fetchFileFromS3(sdkRequestObject.codeS3Location, sdk); delete sdkRequestObject.codeS3Location; } if (isResolver) { - await sdk.appsync().updateResolver(sdkRequestObject).promise(); + await sdk.appsync().updateResolver(sdkRequestObject); } else if (isFunction) { - // Function version is only applicable when using VTL and mapping templates // Runtime only applicable when using code (JS mapping templates) if (sdkRequestObject.code) { @@ -96,26 +115,37 @@ export async function isHotswappableAppSyncChange( delete sdkRequestObject.runtime; } - const functions = await getAppSyncFunctions(sdk, sdkRequestObject.apiId); - const { functionId } = functions?.find(fn => fn.name === physicalName) ?? {}; + const functions = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }); + const { functionId } = functions.find((fn) => fn.name === physicalName) ?? {}; // Updating multiple functions at the same time or along with graphql schema results in `ConcurrentModificationException` await simpleRetry( - () => sdk.appsync().updateFunction({ ...sdkRequestObject, functionId: functionId! }).promise(), + () => + sdk.appsync().updateFunction({ + ...sdkRequestObject, + functionId: functionId, + }), 5, - 'ConcurrentModificationException'); + 'ConcurrentModificationException', + ); } else if (isGraphQLSchema) { - let schemaCreationResponse: GetSchemaCreationStatusResponse = await sdk.appsync().startSchemaCreation(sdkRequestObject).promise(); - while (schemaCreationResponse.status && ['PROCESSING', 'DELETING'].some(status => status === schemaCreationResponse.status)) { + let schemaCreationResponse: GetSchemaCreationStatusCommandOutput = await sdk + .appsync() + .startSchemaCreation(sdkRequestObject); + while ( + schemaCreationResponse.status && + ['PROCESSING', 'DELETING'].some((status) => status === schemaCreationResponse.status) + ) { await sleep(1000); // poll every second - const getSchemaCreationStatusRequest: GetSchemaCreationStatusRequest = { + const getSchemaCreationStatusRequest: GetSchemaCreationStatusCommandInput = { apiId: sdkRequestObject.apiId, }; - schemaCreationResponse = await sdk.appsync().getSchemaCreationStatus(getSchemaCreationStatusRequest).promise(); + schemaCreationResponse = await sdk.appsync().getSchemaCreationStatus(getSchemaCreationStatusRequest); } if (schemaCreationResponse.status === 'FAILED') { throw new Error(schemaCreationResponse.details); } - } else { //isApiKey + } else { + //isApiKey if (!sdkRequestObject.id) { // ApiKeyId is optional in CFN but required in SDK. Grab the KeyId from physicalArn if not available as part of CFN template const arnParts = physicalName?.split('/'); @@ -123,7 +153,7 @@ export async function isHotswappableAppSyncChange( sdkRequestObject.id = arnParts[3]; } } - await sdk.appsync().updateApiKey(sdkRequestObject).promise(); + await sdk.appsync().updateApiKey(sdkRequestObject); } }, }); @@ -132,11 +162,11 @@ export async function isHotswappableAppSyncChange( return ret; } -async function fetchFileFromS3(s3Url: string, sdk: ISDK) { +async function fetchFileFromS3(s3Url: string, sdk: SDK) { const s3PathParts = s3Url.split('/'); const s3Bucket = s3PathParts[2]; // first two are "s3:" and "" due to s3:// const s3Key = s3PathParts.splice(3).join('/'); // after removing first three we reconstruct the key - return (await sdk.s3().getObject({ Bucket: s3Bucket, Key: s3Key }).promise()).Body; + return (await sdk.s3().getObject({ Bucket: s3Bucket, Key: s3Key })).Body?.transformToString(); } async function simpleRetry(fn: () => Promise, numOfRetries: number, errorCodeToRetry: string) { @@ -153,20 +183,5 @@ async function simpleRetry(fn: () => Promise, numOfRetries: number, errorCo } async function sleep(ms: number) { - return new Promise(ok => setTimeout(ok, ms)); -} - -/** - * Get all functions for a given AppSync API by iterating through the paginated list of functions - */ -async function getAppSyncFunctions(sdk: ISDK, apiId: string, nextToken?: string): Promise { - const ret = new Array(); - return sdk.appsync().listFunctions({ apiId, nextToken }).promise() - .then(async (listFunctionsResponse: ListFunctionsResponse) => { - ret.push(...(listFunctionsResponse.functions ?? [])); - if (listFunctionsResponse.nextToken) { - ret.push(...await getAppSyncFunctions(sdk, apiId, listFunctionsResponse.nextToken)); - } - return ret; - }); + return new Promise((ok) => setTimeout(ok, ms)); } diff --git a/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts b/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts index 4d3e54cd15de3..0b2e57e6fbadb 100644 --- a/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts +++ b/packages/aws-cdk/lib/api/hotswap/code-build-projects.ts @@ -1,10 +1,18 @@ -import * as AWS from 'aws-sdk'; -import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, transformObjectKeys } from './common'; -import { ISDK } from '../aws-auth'; -import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; +import type { UpdateProjectCommandInput } from '@aws-sdk/client-codebuild'; +import { + type ChangeHotswapResult, + classifyChanges, + type HotswappableChangeCandidate, + lowerCaseFirstCharacter, + transformObjectKeys, +} from './common'; +import type { SDK } from '../aws-auth'; +import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableCodeBuildProjectChange( - logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { if (change.newValue.Type !== 'AWS::CodeBuild::Project') { return []; @@ -15,17 +23,20 @@ export async function isHotswappableCodeBuildProjectChange( const classifiedChanges = classifyChanges(change, ['Source', 'Environment', 'SourceVersion']); classifiedChanges.reportNonHotswappablePropertyChanges(ret); if (classifiedChanges.namesOfHotswappableProps.length > 0) { - const updateProjectInput: AWS.CodeBuild.UpdateProjectInput = { + const updateProjectInput: UpdateProjectCommandInput = { name: '', }; - const projectName = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, change.newValue.Properties?.Name); + const projectName = await evaluateCfnTemplate.establishResourcePhysicalName( + logicalId, + change.newValue.Properties?.Name, + ); ret.push({ hotswappable: true, resourceType: change.newValue.Type, propsChanged: classifiedChanges.namesOfHotswappableProps, service: 'codebuild', resourceNames: [`CodeBuild Project '${projectName}'`], - apply: async (sdk: ISDK) => { + apply: async (sdk: SDK) => { if (!projectName) { return; } @@ -52,7 +63,7 @@ export async function isHotswappableCodeBuildProjectChange( } } - await sdk.codeBuild().updateProject(updateProjectInput).promise(); + await sdk.codeBuild().updateProject(updateProjectInput); }, }); } diff --git a/packages/aws-cdk/lib/api/hotswap/common.ts b/packages/aws-cdk/lib/api/hotswap/common.ts index eb4b787d04e92..6046b1c34883a 100644 --- a/packages/aws-cdk/lib/api/hotswap/common.ts +++ b/packages/aws-cdk/lib/api/hotswap/common.ts @@ -1,5 +1,5 @@ -import * as cfn_diff from '@aws-cdk/cloudformation-diff'; -import { ISDK } from '../aws-auth'; +import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff'; +import type { SDK } from '../aws-auth'; export const ICON = '✨'; @@ -18,7 +18,7 @@ export interface HotswappableChange { */ readonly resourceNames: string[]; - readonly apply: (sdk: ISDK) => Promise; + readonly apply: (sdk: SDK) => Promise; } export interface NonHotswappableChange { @@ -76,19 +76,19 @@ export class HotswappableChangeCandidate { /** * The value the resource is being updated from */ - public readonly oldValue: cfn_diff.Resource; + public readonly oldValue: Resource; /** * The value the resource is being updated to */ - public readonly newValue: cfn_diff.Resource; + public readonly newValue: Resource; /** * The changes made to the resource properties */ public readonly propertyUpdates: PropDiffs; - public constructor(logicalId: string, oldValue: cfn_diff.Resource, newValue: cfn_diff.Resource, propertyUpdates: PropDiffs) { + public constructor(logicalId: string, oldValue: Resource, newValue: Resource, propertyUpdates: PropDiffs) { this.logicalId = logicalId; this.oldValue = oldValue; this.newValue = newValue; @@ -96,7 +96,7 @@ export class HotswappableChangeCandidate { } } -type Exclude = { [key: string]: Exclude | true } +type Exclude = { [key: string]: Exclude | true }; /** * This function transforms all keys (recursively) in the provided `val` object. @@ -135,16 +135,16 @@ export function lowerCaseFirstCharacter(str: string): string { return str.length > 0 ? `${str[0].toLowerCase()}${str.slice(1)}` : str; } -export type PropDiffs = Record>; +export type PropDiffs = Record>; export class ClassifiedChanges { public constructor( public readonly change: HotswappableChangeCandidate, public readonly hotswappableProps: PropDiffs, public readonly nonHotswappableProps: PropDiffs, - ) { } + ) {} - public reportNonHotswappablePropertyChanges(ret: ChangeHotswapResult):void { + public reportNonHotswappablePropertyChanges(ret: ChangeHotswapResult): void { const nonHotswappablePropNames = Object.keys(this.nonHotswappableProps); if (nonHotswappablePropNames.length > 0) { const tagOnlyChange = nonHotswappablePropNames.length === 1 && nonHotswappablePropNames[0] === 'Tags'; @@ -152,7 +152,9 @@ export class ClassifiedChanges { ret, this.change, this.nonHotswappableProps, - tagOnlyChange ? 'Tags are not hotswappable' : `resource properties '${nonHotswappablePropNames}' are not hotswappable on this resource type`, + tagOnlyChange + ? 'Tags are not hotswappable' + : `resource properties '${nonHotswappablePropNames}' are not hotswappable on this resource type`, ); } } @@ -162,10 +164,7 @@ export class ClassifiedChanges { } } -export function classifyChanges( - xs: HotswappableChangeCandidate, - hotswappablePropNames: string[], -): ClassifiedChanges { +export function classifyChanges(xs: HotswappableChangeCandidate, hotswappablePropNames: string[]): ClassifiedChanges { const hotswappableProps: PropDiffs = {}; const nonHotswappableProps: PropDiffs = {}; @@ -205,11 +204,13 @@ export function reportNonHotswappableResource( change: HotswappableChangeCandidate, reason?: string, ): ChangeHotswapResult { - return [{ - hotswappable: false, - rejectedChanges: Object.keys(change.propertyUpdates), - logicalId: change.logicalId, - resourceType: change.newValue.Type, - reason, - }]; + return [ + { + hotswappable: false, + rejectedChanges: Object.keys(change.propertyUpdates), + logicalId: change.logicalId, + resourceType: change.newValue.Type, + reason, + }, + ]; } diff --git a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts index c3e306fb0b878..5e4b367354945 100644 --- a/packages/aws-cdk/lib/api/hotswap/ecs-services.ts +++ b/packages/aws-cdk/lib/api/hotswap/ecs-services.ts @@ -1,10 +1,18 @@ -import * as AWS from 'aws-sdk'; -import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys } from './common'; -import { ISDK } from '../aws-auth'; -import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; +import { + type ChangeHotswapResult, + classifyChanges, + type HotswappableChangeCandidate, + lowerCaseFirstCharacter, + reportNonHotswappableChange, + transformObjectKeys, +} from './common'; +import type { SDK } from '../aws-auth'; +import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableEcsServiceChange( - logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { // the only resource change we can evaluate here is an ECS TaskDefinition if (change.newValue.Type !== 'AWS::ECS::TaskDefinition') { @@ -21,7 +29,9 @@ export async function isHotswappableEcsServiceChange( // find all ECS Services that reference the TaskDefinition that changed const resourcesReferencingTaskDef = evaluateCfnTemplate.findReferencesTo(logicalId); - const ecsServiceResourcesReferencingTaskDef = resourcesReferencingTaskDef.filter(r => r.Type === 'AWS::ECS::Service'); + const ecsServiceResourcesReferencingTaskDef = resourcesReferencingTaskDef.filter( + (r) => r.Type === 'AWS::ECS::Service', + ); const ecsServicesReferencingTaskDef = new Array(); for (const ecsServiceResource of ecsServiceResourcesReferencingTaskDef) { const serviceArn = await evaluateCfnTemplate.findPhysicalNameFor(ecsServiceResource.LogicalId); @@ -33,12 +43,18 @@ export async function isHotswappableEcsServiceChange( // if there are no resources referencing the TaskDefinition, // hotswap is not possible in FALL_BACK mode reportNonHotswappableChange(ret, change, undefined, 'No ECS services reference the changed task definition', false); - } if (resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) { + } + if (resourcesReferencingTaskDef.length > ecsServicesReferencingTaskDef.length) { // if something besides an ECS Service is referencing the TaskDefinition, // hotswap is not possible in FALL_BACK mode - const nonEcsServiceTaskDefRefs = resourcesReferencingTaskDef.filter(r => r.Type !== 'AWS::ECS::Service'); + const nonEcsServiceTaskDefRefs = resourcesReferencingTaskDef.filter((r) => r.Type !== 'AWS::ECS::Service'); for (const taskRef of nonEcsServiceTaskDefRefs) { - reportNonHotswappableChange(ret, change, undefined, `A resource '${taskRef.LogicalId}' with Type '${taskRef.Type}' that is not an ECS Service was found referencing the changed TaskDefinition '${logicalId}'`); + reportNonHotswappableChange( + ret, + change, + undefined, + `A resource '${taskRef.LogicalId}' with Type '${taskRef.Type}' that is not an ECS Service was found referencing the changed TaskDefinition '${logicalId}'`, + ); } } @@ -52,9 +68,9 @@ export async function isHotswappableEcsServiceChange( service: 'ecs-service', resourceNames: [ `ECS Task Definition '${await taskDefinitionResource.Family}'`, - ...ecsServicesReferencingTaskDef.map(ecsService => `ECS Service '${ecsService.serviceArn.split('/')[2]}'`), + ...ecsServicesReferencingTaskDef.map((ecsService) => `ECS Service '${ecsService.serviceArn.split('/')[2]}'`), ], - apply: async (sdk: ISDK) => { + apply: async (sdk: SDK) => { // Step 1 - update the changed TaskDefinition, creating a new TaskDefinition Revision // we need to lowercase the evaluated TaskDef from CloudFormation, // as the AWS SDK uses lowercase property names for these @@ -80,101 +96,31 @@ export async function isHotswappableEcsServiceChange( }, }, }); - const registerTaskDefResponse = await sdk.ecs().registerTaskDefinition(lowercasedTaskDef).promise(); + const registerTaskDefResponse = await sdk.ecs().registerTaskDefinition(lowercasedTaskDef); const taskDefRevArn = registerTaskDefResponse.taskDefinition?.taskDefinitionArn; // Step 2 - update the services using that TaskDefinition to point to the new TaskDefinition Revision - const servicePerClusterUpdates: { [cluster: string]: Array<{ promise: Promise; ecsService: EcsService }> } = {}; - for (const ecsService of ecsServicesReferencingTaskDef) { - const clusterName = ecsService.serviceArn.split('/')[1]; - - const existingClusterPromises = servicePerClusterUpdates[clusterName]; - let clusterPromises: Array<{ promise: Promise; ecsService: EcsService }>; - if (existingClusterPromises) { - clusterPromises = existingClusterPromises; - } else { - clusterPromises = []; - servicePerClusterUpdates[clusterName] = clusterPromises; - } - // Forcing New Deployment and setting Minimum Healthy Percent to 0. - // As CDK HotSwap is development only, this seems the most efficient way to ensure all tasks are replaced immediately, regardless of original amount. - clusterPromises.push({ - promise: sdk.ecs().updateService({ - service: ecsService.serviceArn, + // Forcing New Deployment and setting Minimum Healthy Percent to 0. + // As CDK HotSwap is development only, this seems the most efficient way to ensure all tasks are replaced immediately, regardless of original amount + await Promise.all( + ecsServicesReferencingTaskDef.map(async (service) => { + const cluster = service.serviceArn.split('/')[1]; + const update = await sdk.ecs().updateService({ + service: service.serviceArn, taskDefinition: taskDefRevArn, - cluster: clusterName, + cluster, forceNewDeployment: true, deploymentConfiguration: { minimumHealthyPercent: 0, }, - }).promise(), - ecsService: ecsService, - }); - } - // Limited set of updates per cluster - // eslint-disable-next-line @aws-cdk/promiseall-no-unbounded-parallelism - await Promise.all(Object.values(servicePerClusterUpdates) - .map(clusterUpdates => { - // Limited set of updates per cluster - // eslint-disable-next-line @aws-cdk/promiseall-no-unbounded-parallelism - return Promise.all(clusterUpdates.map(serviceUpdate => serviceUpdate.promise)); + }); + + await sdk.ecs().waitUntilServicesStable({ + cluster: update.service?.clusterArn, + services: [service.serviceArn], + }); }), ); - - // Step 3 - wait for the service deployments triggered in Step 2 to finish - // configure a custom Waiter - (sdk.ecs() as any).api.waiters.deploymentCompleted = { - name: 'DeploymentCompleted', - operation: 'describeServices', - delay: 6, - maxAttempts: 100, - acceptors: [ - { - matcher: 'pathAny', - argument: 'failures[].reason', - expected: 'MISSING', - state: 'failure', - }, - { - matcher: 'pathAny', - argument: 'services[].status', - expected: 'DRAINING', - state: 'failure', - }, - { - matcher: 'pathAny', - argument: 'services[].status', - expected: 'INACTIVE', - state: 'failure', - }, - - // failure if any services report a deployment with status FAILED - { - matcher: 'path', - argument: "length(services[].deployments[? rolloutState == 'FAILED'][]) > `0`", - expected: true, - state: 'failure', - }, - - // wait for all services to report only a single deployment - { - matcher: 'path', - argument: 'length(services[? length(deployments) > `1`]) == `0`', - expected: true, - state: 'success', - }, - ], - }; - // create a custom Waiter that uses the deploymentCompleted configuration added above - const deploymentWaiter = new (AWS as any).ResourceWaiter(sdk.ecs(), 'deploymentCompleted'); - // wait for all of the waiters to finish - // eslint-disable-next-line @aws-cdk/promiseall-no-unbounded-parallelism - await Promise.all(Object.entries(servicePerClusterUpdates).map(([clusterName, serviceUpdates]) => { - return deploymentWaiter.wait({ - cluster: clusterName, - services: serviceUpdates.map(serviceUpdate => serviceUpdate.ecsService.serviceArn), - }).promise(); - })); }, }); } @@ -187,14 +133,19 @@ interface EcsService { } async function prepareTaskDefinitionChange( - evaluateCfnTemplate: EvaluateCloudFormationTemplate, logicalId: string, change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, + logicalId: string, + change: HotswappableChangeCandidate, ) { const taskDefinitionResource: { [name: string]: any } = { ...change.oldValue.Properties, ContainerDefinitions: change.newValue.Properties?.ContainerDefinitions, }; // first, let's get the name of the family - const familyNameOrArn = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, taskDefinitionResource?.Family); + const familyNameOrArn = await evaluateCfnTemplate.establishResourcePhysicalName( + logicalId, + taskDefinitionResource?.Family, + ); if (!familyNameOrArn) { // if the Family property has not been provided, and we can't find it in the current Stack, // this means hotswapping is not possible @@ -203,18 +154,19 @@ async function prepareTaskDefinitionChange( // the physical name of the Task Definition in CloudFormation includes its current revision number at the end, // remove it if needed const familyNameOrArnParts = familyNameOrArn.split(':'); - const family = familyNameOrArnParts.length > 1 - // familyNameOrArn is actually an ARN, of the format 'arn:aws:ecs:region:account:task-definition/:' + const family = + familyNameOrArnParts.length > 1 + ? // familyNameOrArn is actually an ARN, of the format 'arn:aws:ecs:region:account:task-definition/:' // so, take the 6th element, at index 5, and split it on '/' - ? familyNameOrArnParts[5].split('/')[1] - // otherwise, familyNameOrArn is just the simple name evaluated from the CloudFormation template - : familyNameOrArn; + familyNameOrArnParts[5].split('/')[1] + : // otherwise, familyNameOrArn is just the simple name evaluated from the CloudFormation template + familyNameOrArn; // then, let's evaluate the body of the remainder of the TaskDef (without the Family property) return { - ...await evaluateCfnTemplate.evaluateCfnExpression({ + ...(await evaluateCfnTemplate.evaluateCfnExpression({ ...(taskDefinitionResource ?? {}), Family: undefined, - }), + })), Family: family, }; } diff --git a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts index 6df07dd5880d9..8c38941c198f2 100644 --- a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts +++ b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts @@ -1,30 +1,34 @@ import { Writable } from 'stream'; -import * as AWS from 'aws-sdk'; -import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, PropDiffs } from './common'; +import { type FunctionConfiguration, type UpdateFunctionConfigurationCommandInput } from '@aws-sdk/client-lambda'; +import { type ChangeHotswapResult, classifyChanges, type HotswappableChangeCandidate, PropDiffs } from './common'; import { flatMap } from '../../util'; -import { ISDK } from '../aws-auth'; -import { CfnEvaluationException, EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; +import type { ILambdaClient, SDK } from '../aws-auth'; +import { CfnEvaluationException, type EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; // namespace object imports won't work in the bundle for function exports // eslint-disable-next-line @typescript-eslint/no-require-imports const archiver = require('archiver'); export async function isHotswappableLambdaFunctionChange( - logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { // if the change is for a Lambda Version, // ignore it by returning an empty hotswap operation - // we will publish a new version when we get to hotswapping the actual Function this Version points to, below // (Versions can't be changed in CloudFormation anyway, they're immutable) if (change.newValue.Type === 'AWS::Lambda::Version') { - return [{ - hotswappable: true, - resourceType: 'AWS::Lambda::Version', - resourceNames: [], - propsChanged: [], - service: 'lambda', - apply: async (_sdk: ISDK) => {}, - }]; + return [ + { + hotswappable: true, + resourceType: 'AWS::Lambda::Version', + resourceNames: [], + propsChanged: [], + service: 'lambda', + apply: async (_sdk: SDK) => {}, + }, + ]; } // we handle Aliases specially too @@ -40,7 +44,10 @@ export async function isHotswappableLambdaFunctionChange( const classifiedChanges = classifyChanges(change, ['Code', 'Environment', 'Description']); classifiedChanges.reportNonHotswappablePropertyChanges(ret); - const functionName = await evaluateCfnTemplate.establishResourcePhysicalName(logicalId, change.newValue.Properties?.FunctionName); + const functionName = await evaluateCfnTemplate.establishResourcePhysicalName( + logicalId, + change.newValue.Properties?.FunctionName, + ); const namesOfHotswappableChanges = Object.keys(classifiedChanges.hotswappableProps); if (namesOfHotswappableChanges.length > 0) { ret.push({ @@ -51,13 +58,19 @@ export async function isHotswappableLambdaFunctionChange( resourceNames: [ `Lambda Function '${functionName}'`, // add Version here if we're publishing a new one - ...await renderVersions(logicalId, evaluateCfnTemplate, [`Lambda Version for Function '${functionName}'`]), + ...(await renderVersions(logicalId, evaluateCfnTemplate, [`Lambda Version for Function '${functionName}'`])), // add any Aliases that we are hotswapping here - ...await renderAliases(logicalId, evaluateCfnTemplate, async (alias) => `Lambda Alias '${alias}' for Function '${functionName}'`), + ...(await renderAliases( + logicalId, + evaluateCfnTemplate, + async (alias) => `Lambda Alias '${alias}' for Function '${functionName}'`, + )), ], - apply: async (sdk: ISDK) => { + apply: async (sdk: SDK) => { const lambdaCodeChange = await evaluateLambdaFunctionProps( - classifiedChanges.hotswappableProps, change.newValue.Properties?.Runtime, evaluateCfnTemplate, + classifiedChanges.hotswappableProps, + change.newValue.Properties?.Runtime, + evaluateCfnTemplate, ); if (lambdaCodeChange === undefined) { return; @@ -80,13 +93,13 @@ export async function isHotswappableLambdaFunctionChange( ImageUri: lambdaCodeChange.code.imageUri, ZipFile: lambdaCodeChange.code.functionCodeZip, S3ObjectVersion: lambdaCodeChange.code.s3ObjectVersion, - }).promise(); + }); await waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda, functionName); } if (lambdaCodeChange.configurations !== undefined) { - const updateRequest: AWS.Lambda.UpdateFunctionConfigurationRequest = { + const updateRequest: UpdateFunctionConfigurationCommandInput = { FunctionName: functionName, }; if (lambdaCodeChange.configurations.description !== undefined) { @@ -95,7 +108,7 @@ export async function isHotswappableLambdaFunctionChange( if (lambdaCodeChange.configurations.environment !== undefined) { updateRequest.Environment = lambdaCodeChange.configurations.environment; } - const updateFunctionCodeResponse = await lambda.updateFunctionConfiguration(updateRequest).promise(); + const updateFunctionCodeResponse = await lambda.updateFunctionConfiguration(updateRequest); await waitForLambdasPropertiesUpdateToFinish(updateFunctionCodeResponse, lambda, functionName); } @@ -103,17 +116,19 @@ export async function isHotswappableLambdaFunctionChange( if (versionsReferencingFunction.length > 0) { const publishVersionPromise = lambda.publishVersion({ FunctionName: functionName, - }).promise(); + }); if (aliasesNames.length > 0) { // we need to wait for the Version to finish publishing const versionUpdate = await publishVersionPromise; for (const alias of aliasesNames) { - operations.push(lambda.updateAlias({ - FunctionName: functionName, - Name: alias, - FunctionVersion: versionUpdate.Version, - }).promise()); + operations.push( + lambda.updateAlias({ + FunctionName: functionName, + Name: alias, + FunctionVersion: versionUpdate.Version, + }), + ); } } else { operations.push(publishVersionPromise); @@ -148,7 +163,7 @@ function classifyAliasChanges(change: HotswappableChangeCandidate): ChangeHotswa propsChanged: [], service: 'lambda', resourceNames: [], - apply: async (_sdk: ISDK) => {}, + apply: async (_sdk: SDK) => {}, }); } @@ -161,7 +176,9 @@ function classifyAliasChanges(change: HotswappableChangeCandidate): ChangeHotswa * Returns `undefined` if the change is not hotswappable. */ async function evaluateLambdaFunctionProps( - hotswappablePropChanges: PropDiffs, runtime: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + hotswappablePropChanges: PropDiffs, + runtime: string, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { /* * At first glance, we would want to initialize these using the "previous" values (change.oldValue), @@ -228,7 +245,9 @@ async function evaluateLambdaFunctionProps( break; default: // we will never get here, but just in case we do throw an error - throw new Error ('while apply()ing, found a property that cannot be hotswapped. Please report this at github.com/aws/aws-cdk/issues/new/choose'); + throw new Error( + 'while apply()ing, found a property that cannot be hotswapped. Please report this at github.com/aws/aws-cdk/issues/new/choose', + ); } } @@ -291,52 +310,29 @@ function zipString(fileName: string, rawString: string): Promise { } /** - * After a Lambda Function is updated, it cannot be updated again until the - * `State=Active` and the `LastUpdateStatus=Successful`. - * - * Depending on the configuration of the Lambda Function this could happen relatively quickly - * or very slowly. For example, Zip based functions _not_ in a VPC can take ~1 second whereas VPC - * or Container functions can take ~25 seconds (and 'idle' VPC functions can take minutes). - */ + * After a Lambda Function is updated, it cannot be updated again until the + * `State=Active` and the `LastUpdateStatus=Successful`. + * + * Depending on the configuration of the Lambda Function this could happen relatively quickly + * or very slowly. For example, Zip based functions _not_ in a VPC can take ~1 second whereas VPC + * or Container functions can take ~25 seconds (and 'idle' VPC functions can take minutes). + */ async function waitForLambdasPropertiesUpdateToFinish( - currentFunctionConfiguration: AWS.Lambda.FunctionConfiguration, lambda: AWS.Lambda, functionName: string, + currentFunctionConfiguration: FunctionConfiguration, + lambda: ILambdaClient, + functionName: string, ): Promise { - const functionIsInVpcOrUsesDockerForCode = currentFunctionConfiguration.VpcConfig?.VpcId || - currentFunctionConfiguration.PackageType === 'Image'; + const functionIsInVpcOrUsesDockerForCode = + currentFunctionConfiguration.VpcConfig?.VpcId || currentFunctionConfiguration.PackageType === 'Image'; // if the function is deployed in a VPC or if it is a container image function // then the update will take much longer and we can wait longer between checks // otherwise, the update will be quick, so a 1-second delay is fine const delaySeconds = functionIsInVpcOrUsesDockerForCode ? 5 : 1; - // configure a custom waiter to wait for the function update to complete - (lambda as any).api.waiters.updateFunctionPropertiesToFinish = { - name: 'UpdateFunctionPropertiesToFinish', - operation: 'getFunction', - // equates to 1 minute for zip function not in a VPC and - // 5 minutes for container functions or function in a VPC - maxAttempts: 60, - delay: delaySeconds, - acceptors: [ - { - matcher: 'path', - argument: "Configuration.LastUpdateStatus == 'Successful' && Configuration.State == 'Active'", - expected: true, - state: 'success', - }, - { - matcher: 'path', - argument: 'Configuration.LastUpdateStatus', - expected: 'Failed', - state: 'failure', - }, - ], - }; - - const updateFunctionPropertiesWaiter = new (AWS as any).ResourceWaiter(lambda, 'updateFunctionPropertiesToFinish'); - await updateFunctionPropertiesWaiter.wait({ + await lambda.waitUntilFunctionUpdated(delaySeconds, { FunctionName: functionName, - }).promise(); + }); } /** @@ -352,7 +348,9 @@ function determineCodeFileExtFromRuntime(runtime: string): string { } // Currently inline code only supports Node.js and Python, ignoring other runtimes. // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#aws-properties-lambda-function-code-properties - throw new CfnEvaluationException(`runtime ${runtime} is unsupported, only node.js and python runtimes are currently supported.`); + throw new CfnEvaluationException( + `runtime ${runtime} is unsupported, only node.js and python runtimes are currently supported.`, + ); } /** @@ -361,8 +359,9 @@ function determineCodeFileExtFromRuntime(runtime: string): string { */ async function versionsAndAliases(logicalId: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate) { // find all Lambda Versions that reference this Function - const versionsReferencingFunction = evaluateCfnTemplate.findReferencesTo(logicalId) - .filter(r => r.Type === 'AWS::Lambda::Version'); + const versionsReferencingFunction = evaluateCfnTemplate + .findReferencesTo(logicalId) + .filter((r) => r.Type === 'AWS::Lambda::Version'); // find all Lambda Aliases that reference the above Versions const aliasesReferencingVersions = flatMap(versionsReferencingFunction, v => evaluateCfnTemplate.findReferencesTo(v.LogicalId)); @@ -392,7 +391,11 @@ async function renderAliases( /** * Renders the string used in displaying Version resource names that reference the specified Lambda Function */ -async function renderVersions(logicalId: string, evaluateCfnTemplate: EvaluateCloudFormationTemplate, versionString: string[]): Promise { +async function renderVersions( + logicalId: string, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, + versionString: string[], +): Promise { const versions = (await versionsAndAliases(logicalId, evaluateCfnTemplate)).versionsReferencingFunction; return versions.length > 0 ? versionString : []; diff --git a/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts b/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts index 4ccfa3a5f72d4..1df6eb545a6af 100644 --- a/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap/s3-bucket-deployments.ts @@ -1,6 +1,6 @@ -import { ChangeHotswapResult, HotswappableChangeCandidate } from './common'; -import { ISDK } from '../aws-auth'; -import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; +import type { ChangeHotswapResult, HotswappableChangeCandidate } from './common'; +import type { SDK } from '../aws-auth'; +import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; /** * This means that the value is required to exist by CloudFormation's Custom Resource API (or our S3 Bucket Deployment Lambda's API) @@ -9,7 +9,9 @@ import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-templ export const REQUIRED_BY_CFN = 'required-to-be-present-by-cfn'; export async function isHotswappableS3BucketDeploymentChange( - _logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + _logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { // In old-style synthesis, the policy used by the lambda to copy assets Ref's the assets directly, // meaning that the changes made to the Policy are artifacts that can be safely ignored @@ -31,14 +33,14 @@ export async function isHotswappableS3BucketDeploymentChange( propsChanged: ['*'], service: 'custom-s3-deployment', resourceNames: [`Contents of S3 Bucket '${customResourceProperties.DestinationBucketName}'`], - apply: async (sdk: ISDK) => { + apply: async (sdk: SDK) => { // note that this gives the ARN of the lambda, not the name. This is fine though, the invoke() sdk call will take either const functionName = await evaluateCfnTemplate.evaluateCfnExpression(change.newValue.Properties?.ServiceToken); if (!functionName) { return; } - await sdk.lambda().invoke({ + await sdk.lambda().invokeCommand({ FunctionName: functionName, // Lambda refuses to take a direct JSON object and requires it to be stringify()'d Payload: JSON.stringify({ @@ -50,7 +52,7 @@ export async function isHotswappableS3BucketDeploymentChange( LogicalResourceId: REQUIRED_BY_CFN, ResourceProperties: stringifyObject(customResourceProperties), // JSON.stringify() doesn't turn the actual objects to strings, but the lambda expects strings }), - }).promise(); + }); }, }); @@ -58,7 +60,9 @@ export async function isHotswappableS3BucketDeploymentChange( } export async function skipChangeForS3DeployCustomResourcePolicy( - iamPolicyLogicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + iamPolicyLogicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { if (change.newValue.Type !== 'AWS::IAM::Policy') { return false; @@ -81,21 +85,26 @@ export async function skipChangeForS3DeployCustomResourcePolicy( } // Find all interesting reference to the role - const roleRefs = evaluateCfnTemplate.findReferencesTo(roleLogicalId) + const roleRefs = evaluateCfnTemplate + .findReferencesTo(roleLogicalId) // we are not interested in the reference from the original policy - it always exists - .filter(roleRef => !(roleRef.Type == 'AWS::IAM::Policy' && roleRef.LogicalId === iamPolicyLogicalId)); + .filter((roleRef) => !(roleRef.Type == 'AWS::IAM::Policy' && roleRef.LogicalId === iamPolicyLogicalId)); // Check if the role is only used for S3Deployment // We know this is the case, if S3Deployment -> Lambda -> Role is satisfied for every reference // And we have at least one reference. - const isRoleOnlyForS3Deployment = roleRefs.length >= 1 && roleRefs.every(roleRef => { - if (roleRef.Type === 'AWS::Lambda::Function') { - const lambdaRefs = evaluateCfnTemplate.findReferencesTo(roleRef.LogicalId); - // Every reference must be to the custom resource and at least one reference must be present - return lambdaRefs.length >= 1 && lambdaRefs.every(lambdaRef => lambdaRef.Type === 'Custom::CDKBucketDeployment'); - } - return false; - }); + const isRoleOnlyForS3Deployment = + roleRefs.length >= 1 && + roleRefs.every((roleRef) => { + if (roleRef.Type === 'AWS::Lambda::Function') { + const lambdaRefs = evaluateCfnTemplate.findReferencesTo(roleRef.LogicalId); + // Every reference must be to the custom resource and at least one reference must be present + return ( + lambdaRefs.length >= 1 && lambdaRefs.every((lambdaRef) => lambdaRef.Type === 'Custom::CDKBucketDeployment') + ); + } + return false; + }); // We have determined this role is used for something else, so we can't skip the change if (!isRoleOnlyForS3Deployment) { diff --git a/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts b/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts index dadf403333583..55cfa67bffe9d 100644 --- a/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts +++ b/packages/aws-cdk/lib/api/hotswap/stepfunctions-state-machines.ts @@ -1,9 +1,11 @@ -import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate } from './common'; -import { ISDK } from '../aws-auth'; -import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; +import { type ChangeHotswapResult, classifyChanges, type HotswappableChangeCandidate } from './common'; +import type { SDK } from '../aws-auth'; +import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; export async function isHotswappableStateMachineChange( - logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate, + logicalId: string, + change: HotswappableChangeCandidate, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): Promise { if (change.newValue.Type !== 'AWS::StepFunctions::StateMachine') { return []; @@ -17,7 +19,9 @@ export async function isHotswappableStateMachineChange( const stateMachineNameInCfnTemplate = change.newValue?.Properties?.StateMachineName; const stateMachineArn = stateMachineNameInCfnTemplate ? await evaluateCfnTemplate.evaluateCfnExpression({ - 'Fn::Sub': 'arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:' + stateMachineNameInCfnTemplate, + 'Fn::Sub': + 'arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:' + + stateMachineNameInCfnTemplate, }) : await evaluateCfnTemplate.findPhysicalNameFor(logicalId); ret.push({ @@ -26,7 +30,7 @@ export async function isHotswappableStateMachineChange( propsChanged: namesOfHotswappableChanges, service: 'stepfunctions-service', resourceNames: [`${change.newValue.Type} '${stateMachineArn?.split(':')[6]}'`], - apply: async (sdk: ISDK) => { + apply: async (sdk: SDK) => { if (!stateMachineArn) { return; } @@ -35,7 +39,7 @@ export async function isHotswappableStateMachineChange( await sdk.stepFunctions().updateStateMachine({ stateMachineArn, definition: await evaluateCfnTemplate.evaluateCfnExpression(change.propertyUpdates.DefinitionString.newValue), - }).promise(); + }); }, }); } diff --git a/packages/aws-cdk/lib/api/index.ts b/packages/aws-cdk/lib/api/index.ts index 5671f05837205..ee04643de4a05 100644 --- a/packages/aws-cdk/lib/api/index.ts +++ b/packages/aws-cdk/lib/api/index.ts @@ -1,4 +1,3 @@ -export * from './aws-auth/credentials'; export * from './bootstrap'; export * from './deploy-stack'; export * from './toolkit-info'; diff --git a/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts b/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts index f56ed70d0746b..9bf9a755fd358 100644 --- a/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts +++ b/packages/aws-cdk/lib/api/logs/find-cloudwatch-logs.ts @@ -1,8 +1,9 @@ -import * as cxapi from '@aws-cdk/cx-api'; -import { CloudFormation } from 'aws-sdk'; -import { ISDK, Mode, SdkProvider } from '../aws-auth'; +import type { CloudFormationStackArtifact, Environment } from '@aws-cdk/cx-api'; +import type { StackResourceSummary } from '@aws-sdk/client-cloudformation'; +import type { SDK, SdkProvider } from '../aws-auth'; import { Deployments } from '../deployments'; import { EvaluateCloudFormationTemplate, LazyListStackResources } from '../evaluate-cloudformation-template'; +import { Mode } from '../plugin'; // resource types that have associated CloudWatch Log Groups that should _not_ be monitored const IGNORE_LOGS_RESOURCE_TYPES = ['AWS::EC2::FlowLog', 'AWS::CloudTrail::Trail', 'AWS::CodeBuild::Project']; @@ -16,13 +17,13 @@ export interface FoundLogGroupsResult { * The resolved environment (account/region) that the log * groups are deployed in */ - readonly env: cxapi.Environment; + readonly env: Environment; /** * The SDK that can be used to read events from the CloudWatch * Log Groups in the given environment */ - readonly sdk: ISDK; + readonly sdk: SDK; /** * The names of the relevant CloudWatch Log Groups @@ -33,9 +34,9 @@ export interface FoundLogGroupsResult { export async function findCloudWatchLogGroups( sdkProvider: SdkProvider, - stackArtifact: cxapi.CloudFormationStackArtifact, + stackArtifact: CloudFormationStackArtifact, ): Promise { - let sdk: ISDK; + let sdk: SDK; const resolvedEnv = await sdkProvider.resolveEnvironment(stackArtifact.environment); // try to assume the lookup role and fallback to the default credentials try { @@ -52,7 +53,6 @@ export async function findCloudWatchLogGroups( account: resolvedEnv.account, region: resolvedEnv.region, partition: (await sdk.currentAccount()).partition, - urlSuffix: (region) => sdk.getEndpointSuffix(region), sdk, }); @@ -71,18 +71,18 @@ export async function findCloudWatchLogGroups( * with an ignored resource */ function isReferencedFromIgnoredResource( - logGroupResource: CloudFormation.StackResourceSummary, + logGroupResource: StackResourceSummary, evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): boolean { - const resourcesReferencingLogGroup = evaluateCfnTemplate.findReferencesTo(logGroupResource.LogicalResourceId); - return resourcesReferencingLogGroup.some(reference => { + const resourcesReferencingLogGroup = evaluateCfnTemplate.findReferencesTo(logGroupResource.LogicalResourceId!); + return resourcesReferencingLogGroup.some((reference) => { return IGNORE_LOGS_RESOURCE_TYPES.includes(reference.Type); }); } type CloudWatchLogsResolver = ( - resource: CloudFormation.StackResourceSummary, - evaluateCfnTemplate: EvaluateCloudFormationTemplate + resource: StackResourceSummary, + evaluateCfnTemplate: EvaluateCloudFormationTemplate, ) => string | undefined; const cloudWatchLogsResolvers: Record = { @@ -97,7 +97,7 @@ const cloudWatchLogsResolvers: Record = { // The keys are CFN resource types, and the values are the name of the physical name property of that resource // and the service name that is used in the automatically created CloudWatch log group. 'AWS::Lambda::Function': (resource, evaluateCfnTemplate) => { - const loggingConfig = evaluateCfnTemplate.getResourceProperty(resource.LogicalResourceId, 'LoggingConfig'); + const loggingConfig = evaluateCfnTemplate.getResourceProperty(resource.LogicalResourceId!, 'LoggingConfig'); if (loggingConfig?.LogGroup) { // if LogGroup is a string then use it as the LogGroupName as it is referred by LogGroup.fromLogGroupArn in CDK if (typeof loggingConfig.LogGroup === 'string') { @@ -122,13 +122,13 @@ const cloudWatchLogsResolvers: Record = { * and Log Groups created implicitly (i.e. Lambda Functions) */ function findAllLogGroupNames( - stackResources: CloudFormation.StackResourceSummary[], + stackResources: StackResourceSummary[], evaluateCfnTemplate: EvaluateCloudFormationTemplate, ): string[] { const logGroupNames: string[] = []; for (const resource of stackResources) { - const logGroupResolver = cloudWatchLogsResolvers[resource.ResourceType]; + const logGroupResolver = cloudWatchLogsResolvers[resource.ResourceType!]; if (logGroupResolver) { const logGroupName = logGroupResolver(resource, evaluateCfnTemplate); if (logGroupName) { diff --git a/packages/aws-cdk/lib/api/logs/logs-monitor.ts b/packages/aws-cdk/lib/api/logs/logs-monitor.ts index 446e4bd62033a..a796f90956680 100644 --- a/packages/aws-cdk/lib/api/logs/logs-monitor.ts +++ b/packages/aws-cdk/lib/api/logs/logs-monitor.ts @@ -3,7 +3,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as chalk from 'chalk'; import { print, error } from '../../logging'; import { flatten } from '../../util/arrays'; -import { ISDK } from '../aws-auth'; +import type { SDK } from '../aws-auth'; /** * After reading events from all CloudWatch log groups @@ -43,7 +43,7 @@ interface LogGroupsAccessSettings { /** * The SDK for a given environment (account/region) */ - readonly sdk: ISDK; + readonly sdk: SDK; /** * A map of log groups and associated startTime in a given account. @@ -101,12 +101,15 @@ export class CloudWatchLogEventMonitor { * per env along with the SDK object that has access to read from * that environment. */ - public addLogGroups(env: cxapi.Environment, sdk: ISDK, logGroupNames: string[]): void { + public addLogGroups(env: cxapi.Environment, sdk: SDK, logGroupNames: string[]): void { const awsEnv = `${env.account}:${env.region}`; - const logGroupsStartTimes = logGroupNames.reduce((acc, groupName) => { - acc[groupName] = this.startTime; - return acc; - }, {} as { [logGroupName: string]: number }); + const logGroupsStartTimes = logGroupNames.reduce( + (acc, groupName) => { + acc[groupName] = this.startTime; + return acc; + }, + {} as { [logGroupName: string]: number }, + ); this.envsLogGroupsAccessSettings.set(awsEnv, { sdk, logGroupsStartTimes: { @@ -117,7 +120,7 @@ export class CloudWatchLogEventMonitor { } private scheduleNextTick(sleep: number): void { - setTimeout(() => void(this.tick()), sleep); + setTimeout(() => void this.tick(), sleep); } private async tick(): Promise { @@ -126,7 +129,7 @@ export class CloudWatchLogEventMonitor { } try { const events = flatten(await this.readNewEvents()); - events.forEach(event => { + events.forEach((event) => { this.print(event); }); } catch (e) { @@ -156,10 +159,14 @@ export class CloudWatchLogEventMonitor { * Print out a cloudwatch event */ private print(event: CloudWatchLogEvent): void { - print(util.format('[%s] %s %s', - chalk.blue(event.logGroupName), - chalk.yellow(event.timestamp.toLocaleTimeString()), - event.message.trim())); + print( + util.format( + '[%s] %s %s', + chalk.blue(event.logGroupName), + chalk.yellow(event.timestamp.toLocaleTimeString()), + event.message.trim(), + ), + ); } /** @@ -183,7 +190,7 @@ export class CloudWatchLogEventMonitor { logGroupName: logGroupName, limit: 100, startTime: startTime, - }).promise(); + }); const filteredEvents = response.events ?? []; for (const event of filteredEvents) { @@ -197,7 +204,6 @@ export class CloudWatchLogEventMonitor { if (event.timestamp && endTime < event.timestamp) { endTime = event.timestamp; } - } } // As long as there are _any_ events in the log group `filterLogEvents` will return a nextToken. @@ -215,7 +221,7 @@ export class CloudWatchLogEventMonitor { // with Lambda functions the CloudWatch is not created // until something is logged, so just keep polling until // there is somthing to find - if (e.code === 'ResourceNotFoundException') { + if (e.name === 'ResourceNotFoundException') { return []; } throw e; diff --git a/packages/aws-cdk/lib/api/nested-stack-helpers.ts b/packages/aws-cdk/lib/api/nested-stack-helpers.ts index 3242794aa1039..fcf2677a040b0 100644 --- a/packages/aws-cdk/lib/api/nested-stack-helpers.ts +++ b/packages/aws-cdk/lib/api/nested-stack-helpers.ts @@ -1,27 +1,32 @@ import * as path from 'path'; -import * as cxapi from '@aws-cdk/cx-api'; +import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; -import { ISDK } from './aws-auth'; -import { LazyListStackResources, ListStackResources } from './evaluate-cloudformation-template'; -import { CloudFormationStack, Template } from './util/cloudformation'; +import type { SDK } from './aws-auth'; +import { LazyListStackResources, type ListStackResources } from './evaluate-cloudformation-template'; +import { CloudFormationStack, type Template } from './util/cloudformation'; export interface NestedStackTemplates { readonly physicalName: string | undefined; readonly deployedTemplate: Template; readonly generatedTemplate: Template; - readonly nestedStackTemplates: { [nestedStackLogicalId: string]: NestedStackTemplates}; + readonly nestedStackTemplates: { + [nestedStackLogicalId: string]: NestedStackTemplates; + }; } export interface RootTemplateWithNestedStacks { readonly deployedRootTemplate: Template; - readonly nestedStacks: { [nestedStackLogicalId: string]: NestedStackTemplates }; + readonly nestedStacks: { + [nestedStackLogicalId: string]: NestedStackTemplates; + }; } /** * Reads the currently deployed template and all of its nested stack templates from CloudFormation. */ export async function loadCurrentTemplateWithNestedStacks( - rootStackArtifact: cxapi.CloudFormationStackArtifact, sdk: ISDK, + rootStackArtifact: CloudFormationStackArtifact, + sdk: SDK, retrieveProcessedTemplate: boolean = false, ): Promise { const deployedRootTemplate = await loadCurrentTemplate(rootStackArtifact, sdk, retrieveProcessedTemplate); @@ -41,44 +46,53 @@ export async function loadCurrentTemplateWithNestedStacks( * Returns the currently deployed template from CloudFormation that corresponds to `stackArtifact`. */ export async function loadCurrentTemplate( - stackArtifact: cxapi.CloudFormationStackArtifact, sdk: ISDK, + stackArtifact: CloudFormationStackArtifact, + sdk: SDK, retrieveProcessedTemplate: boolean = false, ): Promise