Skip to content

Commit

Permalink
Merge pull request #624 from magieno/add-change-sets-cloudformation
Browse files Browse the repository at this point in the history
- Updated how the stack is monitored.
  • Loading branch information
etiennenoel authored Nov 28, 2023
2 parents c258433 + c11bf4d commit 4e2cbab
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 14 deletions.
83 changes: 70 additions & 13 deletions packages/aws/src/clients/cloudformation.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,21 @@ import {
ExecuteChangeSetCommand,
ExecuteChangeSetCommandInput,
ExecuteChangeSetCommandOutput,
ExecutionStatus,
ListChangeSetsCommand,
ListChangeSetsCommandInput,
ListChangeSetsCommandOutput,
OnStackFailure,
Parameter,
Stack,
StackStatus,
UpdateStackCommand,
UpdateStackCommandInput,
UpdateStackCommandOutput,
OnStackFailure,
} from "@aws-sdk/client-cloudformation";
import {CloudformationClientInterface} from "../interfaces/cloudformation-client.interface";
import {v4 as uuid} from "uuid";
import {CloudformationDeploymentStatusEnum} from "../enums/cloudformation-deployment-status.enum";
import {NotFoundHttpError} from "@pristine-ts/networking";

/**
* The client to use to interact with AWS Cloudformation. It is a wrapper around the CloudformationClient of @aws-sdk/client-cloudformation.
Expand Down Expand Up @@ -288,7 +289,7 @@ export class CloudformationClient implements CloudformationClientInterface {
* @param capabilities
* @param statusCallback
*/
async deployStack(stackName: string, cloudformationTemplateS3Url: string, stackParameters: { [key in string]: string }, capabilities: Capability[], statusCallback?: (status: CloudformationDeploymentStatusEnum, changeSetName: string) => void): Promise<CloudformationDeploymentStatusEnum> {
async deployStack(stackName: string, cloudformationTemplateS3Url: string, stackParameters: { [key in string]: string }, capabilities: Capability[], statusCallback?: (status: CloudformationDeploymentStatusEnum, changeSetName?: string) => void): Promise<CloudformationDeploymentStatusEnum> {
const parameters: Parameter[] = [];

for (const key in stackParameters) {
Expand Down Expand Up @@ -326,7 +327,7 @@ export class CloudformationClient implements CloudformationClientInterface {

this.logHandler.debug("After calling createChangeSet", {stack, response, changeSetName, stackName}, AwsModuleKeyname)

const status = await this.monitorChangeSet(stackName, changeSetName, "changeSet");
const status = await this.monitorChangeSet(stackName, changeSetName);

switch (status) {
case CloudformationDeploymentStatusEnum.Failed:
Expand All @@ -344,10 +345,10 @@ export class CloudformationClient implements CloudformationClientInterface {
ChangeSetName: changeSetName,
})

return this.monitorChangeSet(stackName, changeSetName, "execution", statusCallback);
return this.monitorStack(stackName, statusCallback);
}

private async monitorChangeSet(stackName: string, changeSetName: string, monitoringType: "changeSet" | "execution", statusCallback?: (status: CloudformationDeploymentStatusEnum, changeSetName: string) => void): Promise<CloudformationDeploymentStatusEnum> {
private async monitorChangeSet(stackName: string, changeSetName: string, statusCallback?: (status: CloudformationDeploymentStatusEnum, changeSetName: string) => void): Promise<CloudformationDeploymentStatusEnum> {
while (true) {
const response = await this.describeChangeSet({
StackName: stackName,
Expand All @@ -356,16 +357,14 @@ export class CloudformationClient implements CloudformationClientInterface {

this.logHandler.debug("Describe ChangeSet result.", {response}, AwsModuleKeyname)

const status = monitoringType == "changeSet" ? response.Status : response.ExecutionStatus;
const status = response.Status;

if (status === undefined) {
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}

switch (status) {
case ExecutionStatus.AVAILABLE:
case ExecutionStatus.EXECUTE_IN_PROGRESS:
case ChangeSetStatus.CREATE_IN_PROGRESS:
case ChangeSetStatus.CREATE_PENDING:
case ChangeSetStatus.DELETE_IN_PROGRESS:
Expand All @@ -375,7 +374,6 @@ export class CloudformationClient implements CloudformationClientInterface {
}
break;

case ExecutionStatus.EXECUTE_COMPLETE:
case ChangeSetStatus.CREATE_COMPLETE:
case ChangeSetStatus.DELETE_COMPLETE:
return CloudformationDeploymentStatusEnum.Completed;
Expand All @@ -384,14 +382,73 @@ export class CloudformationClient implements CloudformationClientInterface {
if(response.StatusReason == "The submitted information didn't contain changes. Submit different information to create a change set.") {
return CloudformationDeploymentStatusEnum.NoChangesToPerform;
}
case ExecutionStatus.EXECUTE_FAILED:
case ExecutionStatus.OBSOLETE:
case ExecutionStatus.UNAVAILABLE:
case ChangeSetStatus.DELETE_FAILED:
this.logHandler.error("Error with the ChangeSet.", {response}, AwsModuleKeyname)
return CloudformationDeploymentStatusEnum.Failed;
}

await new Promise(resolve => setTimeout(resolve, 5000));
}
}
private async monitorStack(stackName: string, statusCallback?: (status: CloudformationDeploymentStatusEnum) => void): Promise<CloudformationDeploymentStatusEnum> {
while (true) {
const response = await this.getStackDescription(stackName);

this.logHandler.debug("Stack Description result.", {response}, AwsModuleKeyname)

if(response === undefined) {
const message = `Stack '${stackName}' wasn't found.`;

this.logHandler.error(message, {stackName, response})
throw new NotFoundHttpError(message);
}

const status = response.StackStatus;

if (status === undefined) {
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}

switch (status) {
// In progress states
case StackStatus.CREATE_IN_PROGRESS:
case StackStatus.DELETE_IN_PROGRESS:
case StackStatus.IMPORT_IN_PROGRESS:
case StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS:
case StackStatus.UPDATE_IN_PROGRESS:
case StackStatus.REVIEW_IN_PROGRESS:
if (statusCallback) {
statusCallback(CloudformationDeploymentStatusEnum.InProgress);
}
break

// Failure states
case StackStatus.IMPORT_ROLLBACK_IN_PROGRESS: // A rollback means that it will eventually fail. Let's fail now instead.
case StackStatus.UPDATE_ROLLBACK_IN_PROGRESS: // A rollback means that it will eventually fail. Let's fail now instead.
case StackStatus.ROLLBACK_IN_PROGRESS: // A rollback means that it will eventually fail. Let's fail now instead.
case StackStatus.CREATE_FAILED:
case StackStatus.DELETE_COMPLETE:
case StackStatus.DELETE_FAILED:
case StackStatus.IMPORT_ROLLBACK_COMPLETE:
case StackStatus.IMPORT_ROLLBACK_FAILED:
case StackStatus.ROLLBACK_COMPLETE:
case StackStatus.ROLLBACK_FAILED:
case StackStatus.UPDATE_FAILED:

case StackStatus.UPDATE_ROLLBACK_COMPLETE:
case StackStatus.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS:
case StackStatus.UPDATE_ROLLBACK_FAILED:
this.logHandler.error(`Invalid status '${status}' for stack '${stackName}'.`, {response, stackName});
return CloudformationDeploymentStatusEnum.Failed;

// Success states
case StackStatus.CREATE_COMPLETE:
case StackStatus.IMPORT_COMPLETE:
case StackStatus.UPDATE_COMPLETE:
return CloudformationDeploymentStatusEnum.Completed;
}

await new Promise(resolve => setTimeout(resolve, 5000));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,5 @@ export interface CloudformationClientInterface {
* @param capabilities
* @param statusCallback
*/
deployStack(stackName: string, cloudformationTemplateS3Url: string, stackParameters: {[key in string]:string}, capabilities: Capability[], statusCallback?: (status: CloudformationDeploymentStatusEnum, changeSetName: string) => void): Promise<CloudformationDeploymentStatusEnum>;
deployStack(stackName: string, cloudformationTemplateS3Url: string, stackParameters: {[key in string]:string}, capabilities: Capability[], statusCallback?: (status: CloudformationDeploymentStatusEnum, changeSetName?: string) => void): Promise<CloudformationDeploymentStatusEnum>;
}

0 comments on commit 4e2cbab

Please sign in to comment.