diff --git a/package-lock.json b/package-lock.json index 062c5408a..5846cf81f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "jest-extended": "^4.0.2", "lerna": "^7.4.2", "ts-jest": "^29.1.1", - "typescript": "^4.9.5" + "typescript": "^5.2.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -14377,15 +14377,15 @@ "dev": true }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -26315,9 +26315,9 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==" }, "uglify-js": { "version": "3.17.4", diff --git a/package.json b/package.json index 0580af9da..6e68b1d64 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "jest-extended": "^4.0.2", "lerna": "^7.4.2", "ts-jest": "^29.1.1", - "typescript": "^4.9.5" + "typescript": "^5.2.2" }, "dependencies": { "@pristine-ts/auth0": "file:packages/auth0", diff --git a/packages/aws/src/clients/cloudformation.client.ts b/packages/aws/src/clients/cloudformation.client.ts index 5d87b9d5d..2fe3a6d54 100644 --- a/packages/aws/src/clients/cloudformation.client.ts +++ b/packages/aws/src/clients/cloudformation.client.ts @@ -8,11 +8,30 @@ import { CreateStackCommand, CreateStackCommandInput, CreateStackCommandOutput, DeleteStackCommand, DeleteStackCommandInput, DeleteStackCommandOutput, + CreateChangeSetCommand, + CreateChangeSetCommandInput, + CreateChangeSetCommandOutput, + DeleteChangeSetCommand, + DeleteChangeSetCommandInput, + DeleteChangeSetCommandOutput, + DescribeChangeSetCommand, + DescribeChangeSetCommandInput, + DescribeChangeSetCommandOutput, + ExecuteChangeSetCommand, + ExecuteChangeSetCommandInput, + ExecuteChangeSetCommandOutput, + ListChangeSetsCommand, + ListChangeSetsCommandInput, + ListChangeSetsCommandOutput, DescribeStacksCommand, DescribeStacksCommandOutput, + StackStatus, ChangeSetStatus, + Parameter, + Capability, Stack, UpdateStackCommand, UpdateStackCommandInput, UpdateStackCommandOutput } from "@aws-sdk/client-cloudformation"; import {CloudformationClientInterface} from "../interfaces/cloudformation-client.interface"; +import {v4 as uuid} from "uuid"; /** * The client to use to interact with AWS Cloudformation. It is a wrapper around the CloudformationClient of @aws-sdk/client-cloudformation. @@ -151,4 +170,163 @@ export class CloudformationClient implements CloudformationClientInterface { throw e; } } + + /** + * Creates a Change Set. + * @param input The input to create a change set. + */ + async createChangeSet(input: CreateChangeSetCommandInput): Promise { + this.logHandler.debug("CLOUDFORMATION CLIENT - Create Change Set", {input}, AwsModuleKeyname); + const command = new CreateChangeSetCommand(input) + try { + return await this.getClient().send(command); + } catch (e) { + this.logHandler.error("Error creating change set in cloudformation", {error: e}, AwsModuleKeyname); + throw e; + } + } + + /** + * Deletes a Change Set. + * @param input The input to delete a change set. + */ + async deleteChangeSet(input: DeleteChangeSetCommandInput): Promise { + this.logHandler.debug("CLOUDFORMATION CLIENT - delete Change Set", {input}, AwsModuleKeyname); + const command = new DeleteChangeSetCommand(input) + try { + return await this.getClient().send(command); + } catch (e) { + this.logHandler.error("Error deleting change set in cloudformation", {error: e}, AwsModuleKeyname); + throw e; + } + } + + /** + * Describes a Change Set. + * @param input The input to describe a change set. + */ + async describeChangeSet(input: DescribeChangeSetCommandInput): Promise { + this.logHandler.debug("CLOUDFORMATION CLIENT - Describe Change Set", {input}, AwsModuleKeyname); + const command = new DescribeChangeSetCommand(input) + try { + return await this.getClient().send(command); + } catch (e) { + this.logHandler.error("Error describing change set in cloudformation", {error: e}, AwsModuleKeyname); + throw e; + } + } + + /** + * Executes a Change Set. + * @param input The input to execute a change set. + */ + async executeChangeSet(input: ExecuteChangeSetCommandInput): Promise { + this.logHandler.debug("CLOUDFORMATION CLIENT - Execute Change Set", {input}, AwsModuleKeyname); + const command = new ExecuteChangeSetCommand(input) + try { + return await this.getClient().send(command); + } catch (e) { + this.logHandler.error("Error executing change set in cloudformation", {error: e}, AwsModuleKeyname); + throw e; + } + } + + /** + * Lists a Change Set. + * @param input The input to list a change set. + */ + async listChangeSets(input: ListChangeSetsCommandInput): Promise { + this.logHandler.debug("CLOUDFORMATION CLIENT - List Change Sets", {input}, AwsModuleKeyname); + const command = new ListChangeSetsCommand(input) + try { + return await this.getClient().send(command); + } catch (e) { + this.logHandler.error("Error listing change sets in cloudformation", {error: e}, AwsModuleKeyname); + throw e; + } + } + + /** + * This method encapsulates the deployment of a Stack, whether the Stack already exists or not. It uses ChangeSets to do so. + * It monitors the status of the stack and returns the status when the status is a final state. + * @param stackName + * @param cloudformationTemplateS3Url + * @param stackParameters + * @param capabilities + * @param statusCallback + */ + async deployStack(stackName: string, cloudformationTemplateS3Url: string, stackParameters: {[key in string]:string}, capabilities: Capability[], statusCallback?: (status: ChangeSetStatus, changeSetName: string) => void): Promise { + const parameters: Parameter[] = []; + + for(const key in stackParameters) { + if(stackParameters.hasOwnProperty(key) === false) { + continue; + } + parameters.push({ + ParameterKey: key, + ParameterValue: stackParameters[key], + }); + } + + const changeSetName = uuid(); + + await this.createChangeSet( + { + StackName: stackName, + TemplateURL: cloudformationTemplateS3Url, + Parameters: parameters, + Capabilities: capabilities, + ChangeSetName: changeSetName, + } + ); + + // Check if there are actual changes in the ChangeSet. + const describeChangeSetCommandOutput = await this.describeChangeSet({ + StackName: stackName, + ChangeSetName: changeSetName, + }) + + if(describeChangeSetCommandOutput?.Changes?.length === 0) { + return "NO_CHANGES_TO_PERFORM"; + } + + // Execute the changes and start monitoring + await this.executeChangeSet({ + StackName: stackName, + ChangeSetName: changeSetName, + }) + + while(true) { + const response = await this.describeChangeSet({ + StackName: stackName, + ChangeSetName: changeSetName, + }); + + const status = response.Status; + + if(status === undefined) { + await new Promise(resolve => setTimeout(resolve, 5000)); + continue; + } + + switch (response.Status) { + case "CREATE_IN_PROGRESS": + case "CREATE_PENDING": + case "DELETE_IN_PROGRESS": + case "DELETE_PENDING": + if(statusCallback) { + statusCallback(status, changeSetName); + } + continue; + + case "CREATE_COMPLETE": + case "DELETE_COMPLETE": + case "DELETE_FAILED": + case "FAILED": + return response.Status; + } + + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } } diff --git a/packages/aws/src/interfaces/cloudformation-client.interface.ts b/packages/aws/src/interfaces/cloudformation-client.interface.ts index 834e1db02..ece7a4083 100644 --- a/packages/aws/src/interfaces/cloudformation-client.interface.ts +++ b/packages/aws/src/interfaces/cloudformation-client.interface.ts @@ -1,13 +1,34 @@ -import { S3PresignedOperationTypeEnum } from "../enums/s3-presigned-operation-type.enum"; +import {S3PresignedOperationTypeEnum} from "../enums/s3-presigned-operation-type.enum"; import {GetObjectCommandOutput, S3Client as AWSS3Client, S3ClientConfig} from "@aws-sdk/client-s3"; -import {CloudFormationClient as AWSCloudformationClient} from "@aws-sdk/client-cloudformation/dist-types/CloudFormationClient"; +import { + CloudFormationClient as AWSCloudformationClient +} from "@aws-sdk/client-cloudformation/dist-types/CloudFormationClient"; import { CreateStackCommandInput, CreateStackCommandOutput, DeleteStackCommandInput, DeleteStackCommandOutput, - Stack, UpdateStackCommandInput, UpdateStackCommandOutput + Stack, UpdateStackCommandInput, UpdateStackCommandOutput, + CreateChangeSetCommand, + CreateChangeSetCommandInput, + CreateChangeSetCommandOutput, + DeleteChangeSetCommand, + DeleteChangeSetCommandInput, + DeleteChangeSetCommandOutput, + DescribeChangeSetCommand, + DescribeChangeSetCommandInput, + DescribeChangeSetCommandOutput, + ExecuteChangeSetCommand, + ExecuteChangeSetCommandInput, + ExecuteChangeSetCommandOutput, + ListChangeSetsCommand, + ListChangeSetsCommandInput, + ListChangeSetsCommandOutput, + DescribeStacksCommand, + DescribeStacksCommandOutput, + StackStatus, Capability, ChangeSetStatus, } from "@aws-sdk/client-cloudformation"; import {CloudFormationClientConfig} from "@aws-sdk/client-cloudformation/dist-types/ts3.4"; +import {AwsModuleKeyname} from "../aws.module.keyname"; /** * The CloudformationClient Interface defines the methods that a Cloudformation client must implement. @@ -53,4 +74,45 @@ export interface CloudformationClientInterface { * @param input The input to delete the new stack. */ deleteStack(input: DeleteStackCommandInput): Promise; + + /** + * Creates a Change Set. + * @param input The input to create a change set. + */ + createChangeSet(input: CreateChangeSetCommandInput): Promise; + + /** + * Deletes a Change Set. + * @param input The input to delete a change set. + */ + deleteChangeSet(input: DeleteChangeSetCommandInput): Promise; + + /** + * Describes a Change Set. + * @param input The input to describe a change set. + */ + describeChangeSet(input: DescribeChangeSetCommandInput): Promise; + + /** + * Executes a Change Set. + * @param input The input to execute a change set. + */ + executeChangeSet(input: ExecuteChangeSetCommandInput): Promise; + + /** + * Lists a Change Set. + * @param input The input to list a change set. + */ + listChangeSets(input: ListChangeSetsCommandInput): Promise; + + /** + * This method encapsulates the deployment of a Stack, whether the Stack already exists or not. It uses ChangeSets to do so. + * It monitors the status of the stack and returns the status when the status is a final state. + * @param stackName + * @param cloudformationTemplateS3Url + * @param stackParameters + * @param capabilities + * @param statusCallback + */ + deployStack(stackName: string, cloudformationTemplateS3Url: string, stackParameters: {[key in string]:string}, capabilities: Capability[], statusCallback?: (status: ChangeSetStatus, changeSetName: string) => void): Promise; }