import OrchestratorLogger from '../../services/core/orchestrator-logger'; import * as core from '@actions/core'; import { CloudFormation, CreateStackCommand, // eslint-disable-next-line import/named CreateStackCommandInput, DescribeStacksCommand, // eslint-disable-next-line import/named DescribeStacksCommandInput, ListStacksCommand, // eslint-disable-next-line import/named Parameter, UpdateStackCommand, // eslint-disable-next-line import/named UpdateStackCommandInput, waitUntilStackCreateComplete, waitUntilStackUpdateComplete, } from '@aws-sdk/client-cloudformation'; import { BaseStackFormation } from './cloud-formations/base-stack-formation'; import crypto from 'node:crypto'; const DEFAULT_STACK_WAIT_TIME_SECONDS = 600; function getStackWaitTime(): number { const overrideValue = Number(process.env.ORCHESTRATOR_AWS_STACK_WAIT_TIME ?? ''); if (!Number.isNaN(overrideValue) && overrideValue > 0) { return overrideValue; } return DEFAULT_STACK_WAIT_TIME_SECONDS; } export class AWSBaseStack { constructor(baseStackName: string) { this.baseStackName = baseStackName; } private baseStackName: string; async setupBaseStack(CF: CloudFormation) { const baseStackName = this.baseStackName; const stackWaitTimeSeconds = getStackWaitTime(); const baseStack = BaseStackFormation.formation; // Cloud Formation Input const describeStackInput: DescribeStacksCommandInput = { StackName: baseStackName, }; const parametersWithoutHash: Parameter[] = [{ ParameterKey: 'EnvironmentName', ParameterValue: baseStackName }]; const parametersHash = crypto .createHash('md5') .update(baseStack + JSON.stringify(parametersWithoutHash)) .digest('hex'); const parameters: Parameter[] = [ ...parametersWithoutHash, ...[{ ParameterKey: 'Version', ParameterValue: parametersHash }], ]; const updateInput: UpdateStackCommandInput = { StackName: baseStackName, TemplateBody: baseStack, Parameters: parameters, Capabilities: ['CAPABILITY_IAM'], }; const createStackInput: CreateStackCommandInput = { StackName: baseStackName, TemplateBody: baseStack, Parameters: parameters, Capabilities: ['CAPABILITY_IAM'], }; const stacks = await CF.send( new ListStacksCommand({ StackStatusFilter: [ 'CREATE_IN_PROGRESS', 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE', 'CREATE_COMPLETE', 'ROLLBACK_COMPLETE', ], }), ); const stackNames = stacks.StackSummaries?.map((x) => x.StackName) || []; const stackExists: boolean = stackNames.includes(baseStackName); const describeStack = async () => { return await CF.send(new DescribeStacksCommand(describeStackInput)); }; try { if (!stackExists) { OrchestratorLogger.log(`${baseStackName} stack does not exist (${JSON.stringify(stackNames)})`); let created = false; try { await CF.send(new CreateStackCommand(createStackInput)); created = true; } catch (error: any) { const message = `${error?.name ?? ''} ${error?.message ?? ''}`; if (message.includes('AlreadyExistsException')) { OrchestratorLogger.log(`Base stack already exists, continuing with describe`); } else { throw error; } } if (created) { OrchestratorLogger.log(`created stack (version: ${parametersHash})`); } } const CFState = await describeStack(); let stack = CFState.Stacks?.[0]; if (!stack) { throw new Error(`Base stack doesn't exist, even after creation, stackExists check: ${stackExists}`); } const stackVersion = stack.Parameters?.find((x) => x.ParameterKey === 'Version')?.ParameterValue; if (stack.StackStatus === 'CREATE_IN_PROGRESS') { OrchestratorLogger.log( `Waiting up to ${stackWaitTimeSeconds}s for '${baseStackName}' CloudFormation creation to finish`, ); await waitUntilStackCreateComplete( { client: CF, maxWaitTime: stackWaitTimeSeconds, }, describeStackInput, ); } if (stackExists) { OrchestratorLogger.log(`Base stack exists (version: ${stackVersion}, local version: ${parametersHash})`); if (parametersHash !== stackVersion) { OrchestratorLogger.log(`Attempting update of base stack`); try { await CF.send(new UpdateStackCommand(updateInput)); } catch (error: any) { if (error['message'].includes('No updates are to be performed')) { OrchestratorLogger.log(`No updates are to be performed`); } else { OrchestratorLogger.log(`Update Failed (Stack name: ${baseStackName})`); OrchestratorLogger.log(error['message']); } OrchestratorLogger.log(`Continuing...`); } } else { OrchestratorLogger.log(`No update required`); } stack = (await describeStack()).Stacks?.[0]; if (!stack) { throw new Error( `Base stack doesn't exist, even after updating and creation, stackExists check: ${stackExists}`, ); } if (stack.StackStatus === 'UPDATE_IN_PROGRESS') { OrchestratorLogger.log( `Waiting up to ${stackWaitTimeSeconds}s for '${baseStackName}' CloudFormation update to finish`, ); await waitUntilStackUpdateComplete( { client: CF, maxWaitTime: stackWaitTimeSeconds, }, describeStackInput, ); } } OrchestratorLogger.log('base stack is now ready'); } catch (error) { core.error(JSON.stringify(await describeStack(), undefined, 4)); throw error; } } }