Skip to content

Instantly share code, notes, and snippets.

@revmischa
Created August 18, 2022 18:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save revmischa/03316a0c238a5d05badf601769b0299e to your computer and use it in GitHub Desktop.
Save revmischa/03316a0c238a5d05badf601769b0299e to your computer and use it in GitHub Desktop.
Get cloudformation exports or outputs at runtime
/**
* CloudFormation exports. Unique per-region.
*/
export enum StackExports {
GREMLIN_PORT = "GremlinPort",
GREMLIN_ENDPOINT = "GremlinEndpoint"
}
/**
* CloudFormation stack outputs. Unique per-stack.
* Output names are based on CDK node hierarchy + random suffix. Matches by substring.
*/
export enum StackOutput {
MigrationScriptArn = "MigrationScriptArn",
CognitoWebClientId = "CognitoWebClientId",
UserPoolId = "CognitoUserPoolId",
CacheAddress = "CacheCacheAddress",
ImportQueueUrl = "ImportQueueUrl",
}
let _stackOutputs: Output[]
// cloudformation export lookup
// exports are unique for a region and not qualified by stack as stack outputs are
let _cachedExports: Record<string, string>
export const getStackExport = async (exportName: StackExports): Promise<string | undefined> => {
if (_cachedExports) return _cachedExports[exportName]
const client = new CloudFormationClient({})
const exports = (await client.send(new ListExportsCommand({}))).Exports
if (!exports?.length) console.warn("Failed to fetch AWS stack exports. Are you using the right profile?")
_cachedExports = exports ? Object.fromEntries(exports.map((e) => [e.Name, e.Value])) : {}
return _cachedExports[exportName]
}
// like above but explodes if not found
export const requireStackExport = async (stackExport: StackExports): Promise<string> => {
const val = await getStackExport(stackExport)
if (!val) throw new Error(`Didn't find stack export ${stackExport} but is required`)
return val
}
// get name of CloudFormation stack
export const getStackPrefix = (): string => "XX" // should be something like SST_APP-SST_STAGE from env vars
// stack name and resourcePrefix are the same
export const getResourcePrefix = getStackPrefix
// retrieve a stack output value
// throws an exception if none found
export const getStackOutput = async ({
cloudFormationClient,
output,
stackPrefix,
}: {
cloudFormationClient?: CloudFormationClient
output: StackOutput
stackPrefix?: string
}): Promise<string> => {
if (_stackOutputs) return lookupOutput(_stackOutputs, output, stackPrefix)
if (!cloudFormationClient) cloudFormationClient = new CloudFormationClient({})
// get our stacks
if (!stackPrefix) stackPrefix = getStackPrefix()
const allStacks = await getAllStacks(cloudFormationClient, stackPrefix)
// get all stack outputs
const stackOutputs = allStacks.flatMap((s) => s.Outputs).filter((o): o is Output => !!o)
// search
return lookupOutput(stackOutputs, output, stackPrefix)
}
/**
* Describe all stacks that are a part of our app.
*/
const getAllStacks = async (cloudFormationClient: CloudFormationClient, stackPrefix: string): Promise<Stack[]> => {
// describe all stacks
const descAllStacksPaginator = paginateDescribeStacks({ client: cloudFormationClient, pageSize: 100 }, {})
const omittedStackStatusList: StackStatus[] = [
StackStatus.DELETE_COMPLETE,
StackStatus.DELETE_FAILED,
StackStatus.DELETE_IN_PROGRESS,
StackStatus.CREATE_FAILED,
]
// find stacks that belong to our app
const appStacks: Stack[] = []
for await (const page of descAllStacksPaginator) {
const relatedStacks = page.Stacks?.filter((stack) => stack.StackName?.startsWith(stackPrefix)).filter(
({ StackStatus }) => StackStatus && !omittedStackStatusList.includes(StackStatus as StackStatus)
)
if (!relatedStacks?.length) continue
appStacks.push(...relatedStacks)
}
return appStacks
}
// expect to find name in stackOutputs
// raise exception if not found
const lookupOutput = (stackOutputs: Output[] | undefined, name: StackOutput, stackPrefix?: string): string => {
const value = findOutputIn(stackOutputs, name)
if (value === undefined)
throw new Error(
`Failed to find stack output ${name} using stack prefix '${stackPrefix}' in all outputs: ${stackOutputs?.map(
(o) => o.OutputKey
)}`
)
return value
}
const findOutputIn = (stackOutputs: Output[] | undefined, name: StackOutput): string | undefined => {
// look for an output that begins with this name
return stackOutputs?.find((o) => o.OutputKey?.includes(name))?.OutputValue
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment