Skip to content

Instantly share code, notes, and snippets.

@stoyan-scava
Created February 16, 2021 15:56
Show Gist options
  • Save stoyan-scava/46cfc265cea52ba2b82553731f0f4bb9 to your computer and use it in GitHub Desktop.
Save stoyan-scava/46cfc265cea52ba2b82553731f0f4bb9 to your computer and use it in GitHub Desktop.
Stack Manager - Wrapper of AWS CloudFormation calls. To be used in CI scripts for resource look-up.
import {CloudFormation} from "aws-sdk";
import {
ListStackResourcesOutput,
ListStacksOutput,
Stack,
StackResourceSummary,
StackStatus,
StackSummary
} from "aws-sdk/clients/cloudformation";
/**
* The minimal configuration required to deploy a stack
*/
export interface StackEnvironment {
account: string,
region: string,
stage: string
}
/**
* Manage Cloudformation Stacks and their resources.
* Wrapper of CloudFormation calls
*/
export class StackManager {
/**
* Stacks with these statuses actually exist
* @private
*/
private readonly existingStackStatuses = [
"CREATE_IN_PROGRESS",
"CREATE_COMPLETE",
"ROLLBACK_IN_PROGRESS",
"ROLLBACK_FAILED",
"ROLLBACK_COMPLETE",
"DELETE_IN_PROGRESS",
"DELETE_FAILED",
"UPDATE_IN_PROGRESS",
"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
"UPDATE_COMPLETE",
"UPDATE_ROLLBACK_IN_PROGRESS",
"UPDATE_ROLLBACK_FAILED",
"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
"UPDATE_ROLLBACK_COMPLETE",
"REVIEW_IN_PROGRESS",
"IMPORT_IN_PROGRESS",
"IMPORT_COMPLETE",
"IMPORT_ROLLBACK_IN_PROGRESS",
"IMPORT_ROLLBACK_FAILED",
"IMPORT_ROLLBACK_COMPLETE"
]
// Stacks with these statuses does not exist
// private readonly nonExistingStackStatuses = [
// "CREATE_FAILED",
// "DELETE_COMPLETE"
// ]
/**
* Resources with these statuses actually exist
* @private
*/
private readonly existingResourceStatuses = [
"CREATE_IN_PROGRESS",
"CREATE_COMPLETE",
"DELETE_IN_PROGRESS",
"DELETE_FAILED",
"DELETE_SKIPPED",
"UPDATE_IN_PROGRESS",
"UPDATE_FAILED",
"UPDATE_COMPLETE",
"IMPORT_FAILED",
"IMPORT_COMPLETE",
"IMPORT_IN_PROGRESS",
"IMPORT_ROLLBACK_IN_PROGRESS",
"IMPORT_ROLLBACK_FAILED",
"IMPORT_ROLLBACK_COMPLETE"
]
/**
* Resources with these statuses does not exist
* @private
*/
private readonly nonExistingResourceStatuses = [
"CREATE_FAILED",
"DELETE_COMPLETE"
]
constructor() {
}
async listStacksInEnvironment(env: StackEnvironment, statusFilter?: StackStatus[]): Promise<StackSummary[]> {
const cf = new CloudFormation({
region: env.region
})
const StackStatusFilter = statusFilter ?? this.existingStackStatuses;
let stackSummaries: StackSummary[] = [];
let listStacksOutput: ListStacksOutput;
let NextToken: string | undefined = undefined;
do {
listStacksOutput = await cf.listStacks({NextToken, StackStatusFilter}).promise();
if (listStacksOutput.StackSummaries !== undefined) stackSummaries = [
...stackSummaries,
...listStacksOutput.StackSummaries
];
NextToken = listStacksOutput.NextToken
} while (NextToken !== undefined)
// return stackSummaries
return stackSummaries.filter((summary: StackSummary) => {
return (summary.StackId) ? this.stackIsInEnv(summary.StackId, env) : false
})
}
stackIsInEnv(stackId: string, env: StackEnvironment): boolean {
return (
(this.getRegionFromArn(stackId) === env.region) &&
(this.getAccountIdFromArn(stackId) === env.account) &&
(this.getStackNameFromArn(stackId).startsWith(env.stage))
)
}
// WIP
// async getStackByNameAndEnvironment(stackName: string, env: StackEnvironment): Promise<Stack> {
// const cf = new CloudFormation({region: env.region});
// const describeStacksOutput: DescribeStacksOutput = await cf.describeStacks({
// StackName: stackName,
// NextToken: undefined
// }).promise()
// describeStacksOutput.Stacks?.
// }
getPartitionFromArn(arn: string): string {
return arn.split(":")[1]
}
getServiceFromArn(arn: string): string {
return arn.split(":")[2]
}
getRegionFromArn(arn: string): string {
return arn.split(":")[3]
}
getAccountIdFromArn(arn: string): string {
return arn.split(":")[4]
}
getResourceIdFromArn(arn: string): string {
return arn.split(":")[5]
}
getStackNameFromArn(arn: string): string {
const resourceId = this.getResourceIdFromArn(arn);
return resourceId.split("/")[1]
}
async listResourcesInStack(stackId: string): Promise<StackResourceSummary[]> {
const region = this.getRegionFromArn(stackId);
const cf = new CloudFormation({region})
const StackName = this.getStackNameFromArn(stackId)
let stackResourceSummaries: StackResourceSummary[] = [];
let listStackResourcesOutput: ListStackResourcesOutput;
let NextToken: string | undefined = undefined;
do {
listStackResourcesOutput = await cf.listStackResources({NextToken, StackName}).promise();
if (listStackResourcesOutput.StackResourceSummaries !== undefined) stackResourceSummaries = [
...stackResourceSummaries,
...listStackResourcesOutput.StackResourceSummaries
];
console.log(`${listStackResourcesOutput.StackResourceSummaries?.length} new resources`);
console.log(`${stackResourceSummaries.length} total resources`)
console.log(`NextToken: ${!!listStackResourcesOutput.NextToken}`);
NextToken = listStackResourcesOutput.NextToken
} while (NextToken !== undefined)
// Filter out resources that failed to be created or are deleted already
return stackResourceSummaries.filter((resource: StackResourceSummary) => {
// Note: another possible check is if the resource has PhysicalResourceId (if not then the resource does not exist)
return (!this.nonExistingResourceStatuses.includes(resource.ResourceStatus))
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment