Created
August 18, 2022 18:22
-
-
Save revmischa/03316a0c238a5d05badf601769b0299e to your computer and use it in GitHub Desktop.
Get cloudformation exports or outputs at runtime
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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