import { ciMaybeCreateLogger , ciReportEnabled , makeRetryDelay , removeUndef , sleep } from "@adpt/utils" ; import AWS = require ( "aws-sdk" ) ; import { isNumber , xor } from "lodash" ; import should from "should" ; import { inspect } from "util" ; if ( ciReportEnabled ( ) && ! AWS . config . logger ) { const logger = ciMaybeCreateLogger ( "aws-sdk" ) ; if ( logger ) AWS . config . logger = logger ; } const customBackoff = makeRetryDelay ( ) ; AWS . config . update ( { maxRetries : 20 , retryDelayOptions : { customBackoff } , } ) ; interface AwsCredentials { awsAccessKeyId : string ; awsSecretAccessKey : string ; awsRegion : string ; } export const ubuntuAmi = "ami-0411d593eba4708e5" ; export const sshKeyName = "DefaultKeyPair" ; export const defaultSecurityGroup = "http-https-ssh" ; export async function fakeCreds ( ) { return { awsAccessKeyId : "fakeKeyID" , awsSecretAccessKey : "fakeSecret" , awsRegion : "us-west-2" , } ; } export function getAwsClient ( creds ) { return new AWS . CloudFormation ( { region : creds . awsRegion , accessKeyId : creds . awsAccessKeyId , secretAccessKey : creds . awsSecretAccessKey , } ) ; } export function isFailure ( stackStatus ) { return /FAILED/ . test ( stackStatus ) ; } export function isInProgress ( stackStatus ) { return /IN_PROGRESS/ . test ( stackStatus ) ; } export function isTerminal ( stackStatus ) { return ! isInProgress ( stackStatus ) ; } export function isProbablyDeleted ( stackStatus ) { return stackStatus === "DELETE_IN_PROGRESS" || stackStatus === "DELETE_COMPLETE" ; } interface Tagged { Tags ? : AWS . CloudFormation . Tag [ ] ; } function getTag ( obj , tag ) { if ( obj . Tags ) { for ( const t of obj . Tags ) { if ( t . Key === tag ) return t . Value ; } } return undefined ; } function getAdaptDeployId ( stack ) { return getTag ( stack , "adapt:deployID" ) ; } function stacksWithDeployID ( stacks , deployID ) { if ( stacks == null ) return [ ] ; return stacks . filter ( ( s ) => ( getAdaptDeployId ( s ) === deployID ) ) ; } export async function getStacks ( client , deployID ? , stackName ? ) { const resp = stackName ? await client . describeStacks ( { StackName : stackName } ) . promise ( ) : await client . describeStacks ( ) . promise ( ) ; if ( deployID !== undefined ) return stacksWithDeployID ( resp . Stacks , deployID ) ; return resp . Stacks || [ ] ; } export interface DeleteOptions { definite ? : boolean ; pollMs ? : number ; timeoutMs ? : number ; } const deleteDefaults = { definite : true , pollMs : 1000 , timeoutMs : 20 * 1000 , } ; export async function deleteAllStacks ( client , deployID , options ) ; export async function deleteAllStacks ( client , deployID , timeoutMs ? , definite ? ) ; export async function deleteAllStacks ( client , deployID , timeoutOrOptions ? , definite ? ) { const inOpts = timeoutOrOptions === undefined ? { } : isNumber ( timeoutOrOptions ) ? removeUndef ( { timeoutMs : timeoutOrOptions , definite } ) : timeoutOrOptions ; const opts = { ... deleteDefaults , ... inOpts } ; let timeoutMs = opts . timeoutMs ; let stacks = await getStacks ( client , deployID ) ; for ( const s of stacks ) { const name = s . StackId || s . StackName ; await client . deleteStack ( { StackName : name } ) . promise ( ) ; } do { stacks = await getStacks ( client , deployID ) ; if ( ! definite ) stacks = stacks . filter ( ( s ) => ! isProbablyDeleted ( s . StackStatus ) ) ; if ( stacks . length === 0 ) return ; await sleep ( opts . pollMs ) ; timeoutMs -= opts . pollMs ; } while ( timeoutMs > 0 ) ; throw new Error ( `Unable to delete stacks` ) ; } type StatusFilter = ( status : AWS . CloudFormation . StackStatus ) => boolean ; export interface WaitOptions { pollMs ? : number ; timeoutMs ? : number ; terminalOnly ? : boolean ; searchDeleted ? : boolean ; statusFilter ? : StatusFilter ; } const waitDefaults = { pollMs : 2000 , timeoutMs : 30 * 1000 , terminalOnly : true , searchDeleted : false , statusFilter : undefined , } ; export async function waitForStacks ( client , deployID , stackNames , options ? ) { const { pollMs , statusFilter , ... opts } = { ... waitDefaults , ... options } ; let timeoutMs = opts . timeoutMs ; let singleStackName ; if ( opts . searchDeleted ) { if ( stackNames . length !== 1 ) { throw new Error ( `Must specify exactly one stack name when using searchDeleted` ) ; } singleStackName = stackNames [ 0 ] ; } let actual ; do { let stacks = await getStacks ( client , deployID , singleStackName ) ; actual = stacks . map ( ( s ) => [ s . StackName , s . StackStatus ] ) ; if ( statusFilter ) stacks = stacks . filter ( ( s ) => statusFilter ( s . StackStatus ) ) ; if ( opts . terminalOnly ) stacks = stacks . filter ( ( s ) => isTerminal ( s . StackStatus ) ) ; const names = stacks . map ( ( s ) => opts . searchDeleted ? s . StackId : s . StackName ) ; if ( xor ( names , stackNames ) . length === 0 ) return stacks ; await sleep ( pollMs ) ; timeoutMs -= pollMs ; } while ( timeoutMs > 0 ) ; throw new Error ( `Timeout waiting for stacks. Expected: ${ inspect ( stackNames ) } Actual: ${ inspect ( actual ) }` ) ; } export async function checkStackStatus ( stack , expected , logOnFail = false , client ? ) { if ( logOnFail && isFailure ( stack . StackStatus ) && stack . StackStatus !== expected ) { if ( ! client ) throw new Error ( `client cannot be null if logOnFail=true` ) ; const resp = await client . describeStackEvents ( { StackName : stack . StackId || stack . StackName } ) . promise ( ) ; const events = resp . StackEvents ; if ( events ) { const info = events . map ( ( e ) => { let msg = `${ e . ResourceStatus } ${ e . ResourceType }` ; if ( e . ResourceStatusReason ) msg += `: ${ e . ResourceStatusReason }` ; return msg ; } ) ; console . log ( info . join ( "\n" ) ) ; } } should ( stack . StackStatus ) . equal ( expected ) ; }