-
Notifications
You must be signed in to change notification settings - Fork 1
New: [AEA-6028] - Added shared code for CDK and Proxygen deployments #432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MatthewPopat-NHS
wants to merge
13
commits into
main
Choose a base branch
from
AEA-6028
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
280324f
Added shared code for CDK and Proxygen deployments
MatthewPopat-NHS 547d5ed
Added missed files
MatthewPopat-NHS 75b99da
Merge remote-tracking branch 'origin/main' into AEA-6028
MatthewPopat-NHS 77e31b8
Sonar fixes
MatthewPopat-NHS 434dccc
Added stack name calculation for stateless stacks
MatthewPopat-NHS c8b303d
Merge remote-tracking branch 'origin/main' into AEA-6028
MatthewPopat-NHS 37e1827
Fixed sonar issue
MatthewPopat-NHS 60e7a33
Added support for additional security schemes and hidden paths in spe…
MatthewPopat-NHS de588aa
Fixed image scanning script
MatthewPopat-NHS 683398d
Merge branch 'main' into AEA-6028
MatthewPopat-NHS daad389
Split out fixSpec from deployApi
MatthewPopat-NHS 5bee63c
Merge branch 'AEA-6028' of https://github.com/NHSDigital/eps-cdk-util…
MatthewPopat-NHS a399ea2
Added option to disable blue green to deployApi
MatthewPopat-NHS File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { | ||
| App, | ||
| Aspects, | ||
| Tags, | ||
| StackProps | ||
| } from "aws-cdk-lib" | ||
| import {AwsSolutionsChecks} from "cdk-nag" | ||
| import {getConfigFromEnvVar, getBooleanConfigFromEnvVar, calculateVersionedStackName} from "../config" | ||
|
|
||
| export interface StandardStackProps extends StackProps { | ||
| readonly stackName: string | ||
| readonly version: string | ||
| readonly commitId: string | ||
| readonly isPullRequest: boolean | ||
| } | ||
|
|
||
| export function createApp( | ||
| appName: string, | ||
| repoName: string, | ||
| driftDetectionGroup: string, | ||
| isStateless: boolean = true, | ||
| region: string = "eu-west-2" | ||
| ): {app: App, props: StandardStackProps} { | ||
| let stackName = getConfigFromEnvVar("stackName") | ||
| const versionNumber = getConfigFromEnvVar("versionNumber") | ||
| const commitId = getConfigFromEnvVar("commitId") | ||
| const isPullRequest = getBooleanConfigFromEnvVar("isPullRequest") | ||
| let cfnDriftDetectionGroup = driftDetectionGroup | ||
| if (isPullRequest) { | ||
| cfnDriftDetectionGroup += "-pull-request" | ||
| } | ||
|
|
||
| const app = new App() | ||
|
|
||
| Aspects.of(app).add(new AwsSolutionsChecks({verbose: true})) | ||
|
|
||
| Tags.of(app).add("version", versionNumber) | ||
| Tags.of(app).add("commit", commitId) | ||
| Tags.of(app).add("stackName", stackName) | ||
| Tags.of(app).add("cdkApp", appName) | ||
| Tags.of(app).add("repo", repoName) | ||
| Tags.of(app).add("cfnDriftDetectionGroup", cfnDriftDetectionGroup) | ||
|
|
||
| if (isStateless && !isPullRequest) { | ||
| stackName = calculateVersionedStackName(stackName, versionNumber) | ||
| } | ||
|
|
||
| return { | ||
| app, | ||
| props: { | ||
| env: { | ||
| region | ||
| }, | ||
| stackName, | ||
| version: versionNumber, | ||
| commitId, | ||
| isPullRequest | ||
| } | ||
| } | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import {CloudFormationClient, ListExportsCommand, DescribeStacksCommand} from "@aws-sdk/client-cloudformation" | ||
| import {S3Client, HeadObjectCommand} from "@aws-sdk/client-s3" | ||
|
|
||
| export function getConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): string { | ||
| const value = process.env[prefix + varName] | ||
| if (!value) { | ||
| throw new Error(`Environment variable ${prefix}${varName} is not set`) | ||
| } | ||
| return value | ||
| } | ||
|
|
||
| export function getBooleanConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): boolean { | ||
| const value = getConfigFromEnvVar(varName, prefix) | ||
| return value.toLowerCase() === "true" | ||
| } | ||
|
|
||
| export function getNumberConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): number { | ||
| const value = getConfigFromEnvVar(varName, prefix) | ||
| return Number(value) | ||
| } | ||
|
|
||
| export async function getTrustStoreVersion(trustStoreFile: string, region: string = "eu-west-2"): Promise<string> { | ||
| const cfnClient = new CloudFormationClient({region}) | ||
| const s3Client = new S3Client({region}) | ||
| const describeStacksCommand = new DescribeStacksCommand({StackName: "account-resources"}) | ||
| const response = await cfnClient.send(describeStacksCommand) | ||
| const trustStoreBucketArn = response.Stacks![0].Outputs! | ||
| .find(output => output.OutputKey === "TrustStoreBucket")!.OutputValue | ||
| const bucketName = trustStoreBucketArn!.split(":")[5] | ||
| const headObjectCommand = new HeadObjectCommand({Bucket: bucketName, Key: trustStoreFile}) | ||
| const headObjectResponse = await s3Client.send(headObjectCommand) | ||
| return headObjectResponse.VersionId! | ||
| } | ||
|
|
||
| export async function getCloudFormationExports(region: string = "eu-west-2"): Promise<Record<string, string>> { | ||
| const cfnClient = new CloudFormationClient({region}) | ||
| const listExportsCommand = new ListExportsCommand({}) | ||
| const exports: Record<string, string> = {} | ||
| let nextToken: string | undefined = undefined | ||
|
|
||
| do { | ||
| const response = await cfnClient.send(listExportsCommand) | ||
| response.Exports?.forEach((exp) => { | ||
| if (exp.Name && exp.Value) { | ||
| exports[exp.Name] = exp.Value | ||
| } | ||
| }) | ||
| nextToken = response.NextToken | ||
| listExportsCommand.input.NextToken = nextToken | ||
| } while (nextToken) | ||
|
|
||
| return exports | ||
| } | ||
|
|
||
| export function getCFConfigValue(exports: Record<string, string>, exportName: string): string { | ||
| const value = exports[exportName] | ||
| if (!value) { | ||
| throw new Error(`CloudFormation export ${exportName} not found`) | ||
| } | ||
| return value | ||
| } | ||
|
|
||
| export function getBooleanCFConfigValue(exports: Record<string, string>, exportName: string): boolean { | ||
| const value = getCFConfigValue(exports, exportName) | ||
| return value.toLowerCase() === "true" | ||
| } | ||
|
|
||
| export function calculateVersionedStackName(baseStackName: string, version: string): string { | ||
| return `${baseStackName}-${version.replaceAll(".", "-")}` | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,6 @@ | ||
| // Export all constructs | ||
| export * from "./constructs/TypescriptLambdaFunction.js" | ||
| export * from "./apps/createApp.js" | ||
| export * from "./config/index.js" | ||
| export * from "./specifications/writeSchemas.js" | ||
| export * from "./specifications/deployApi.js" |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import {LambdaClient, InvokeCommand} from "@aws-sdk/client-lambda" | ||
| import {getCFConfigValue, getCloudFormationExports} from "../config" | ||
| import {fixSpec} from "./fixSpec" | ||
|
|
||
| export type ApiConfig = { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| spec: any | ||
| apiName: string | ||
| version: string | ||
| apigeeEnvironment: string | ||
| isPullRequest: boolean | ||
| awsEnvironment: string | ||
| stackName: string | ||
| mtlsSecretName: string | ||
| clientCertExportName: string | ||
| clientPrivateKeyExportName: string | ||
| proxygenPrivateKeyExportName: string | ||
| proxygenKid: string | ||
| hiddenPaths: Array<string> | ||
| } | ||
|
|
||
| const lambda = new LambdaClient({}) | ||
|
|
||
| async function invokeLambda( | ||
| dryRun: boolean, | ||
| functionName: string, | ||
| payload: unknown | ||
| ): Promise<void> { | ||
| if (dryRun) { | ||
| console.log(`Would invoke lambda ${functionName}`) | ||
| return | ||
| } | ||
| const invokeResult = await lambda.send(new InvokeCommand({ | ||
| FunctionName: functionName, | ||
| Payload: Buffer.from(JSON.stringify(payload)) | ||
| })) | ||
| const responsePayload = Buffer.from(invokeResult.Payload!).toString() | ||
| if (invokeResult.FunctionError) { | ||
| throw new Error(`Error calling lambda ${functionName}: ${responsePayload}`) | ||
| } | ||
| console.log(`Lambda ${functionName} invoked successfully. Response:`, responsePayload) | ||
| } | ||
|
|
||
| export async function deployApi( | ||
MatthewPopat-NHS marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| spec, | ||
| apiName, | ||
| version, | ||
| apigeeEnvironment, | ||
| isPullRequest, | ||
| awsEnvironment, | ||
| stackName, | ||
| mtlsSecretName, | ||
| clientCertExportName, | ||
| clientPrivateKeyExportName, | ||
| proxygenPrivateKeyExportName, | ||
| proxygenKid, | ||
| hiddenPaths | ||
| }: ApiConfig, | ||
| blueGreen: boolean = true, | ||
| dryRun: boolean = false | ||
| ): Promise<void> { | ||
| const instance = fixSpec({ | ||
| spec, | ||
| apiName, | ||
| version, | ||
| apigeeEnvironment, | ||
| isPullRequest, | ||
| awsEnvironment, | ||
| stackName, | ||
| mtlsSecretName, | ||
| blueGreen | ||
| }) | ||
|
|
||
| const exports = await getCloudFormationExports() | ||
| const clientCertArn = getCFConfigValue(exports, `account-resources:${clientCertExportName}`) | ||
| const clientPrivateKeyArn = getCFConfigValue(exports, `account-resources:${clientPrivateKeyExportName}`) | ||
| const proxygenPrivateKeyArn = getCFConfigValue(exports, `account-resources:${proxygenPrivateKeyExportName}`) | ||
|
|
||
| let put_secret_lambda = "lambda-resources-ProxygenPTLMTLSSecretPut" | ||
| let instance_put_lambda = "lambda-resources-ProxygenPTLInstancePut" | ||
| let spec_publish_lambda = "lambda-resources-ProxygenPTLSpecPublish" | ||
| if (/^(int|sandbox|prod)$/.test(apigeeEnvironment)) { | ||
| put_secret_lambda = "lambda-resources-ProxygenProdMTLSSecretPut" | ||
| instance_put_lambda = "lambda-resources-ProxygenProdInstancePut" | ||
| spec_publish_lambda = "lambda-resources-ProxygenProdSpecPublish" | ||
| } | ||
|
|
||
| if (!isPullRequest) { | ||
| console.log("Store the secret used for mutual TLS to AWS using Proxygen proxy lambda") | ||
| await invokeLambda( | ||
| dryRun, | ||
| put_secret_lambda, | ||
| { | ||
| apiName, | ||
| environment: apigeeEnvironment, | ||
| secretName: mtlsSecretName, | ||
| secretKeyName: clientPrivateKeyArn, | ||
| secretCertName: clientCertArn, | ||
| kid: proxygenKid, | ||
| proxygenSecretName: proxygenPrivateKeyArn | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| console.log("Deploy the API instance using Proxygen proxy lambda") | ||
| await invokeLambda( | ||
| dryRun, | ||
| instance_put_lambda, | ||
| { | ||
| apiName, | ||
| environment: apigeeEnvironment, | ||
| specDefinition: spec, | ||
| instance, | ||
| kid: proxygenKid, | ||
| proxygenSecretName: proxygenPrivateKeyArn | ||
| } | ||
| ) | ||
|
|
||
| let spec_publish_env | ||
| if (apigeeEnvironment === "int") { | ||
| console.log("Deploy the API spec to prod catalogue as it is int environment") | ||
| spec.servers = [ {url: `https://sandbox.api.service.nhs.uk/${instance}`} ] | ||
| spec_publish_env = "prod" | ||
| } else if (apigeeEnvironment === "internal-dev" && !isPullRequest) { | ||
| console.log("Deploy the API spec to uat catalogue as it is internal-dev environment") | ||
| spec.servers = [ {url: `https://internal-dev-sandbox.api.service.nhs.uk/${instance}`} ] | ||
| spec_publish_env = "uat" | ||
| } | ||
| if (spec_publish_env) { | ||
| for (const path of hiddenPaths) { | ||
| delete spec.paths[path] | ||
| } | ||
| await invokeLambda( | ||
| dryRun, | ||
| spec_publish_lambda, | ||
| { | ||
| apiName, | ||
| environment: spec_publish_env, | ||
| specDefinition: spec, | ||
| instance, | ||
| kid: proxygenKid, | ||
| proxygenSecretName: proxygenPrivateKeyArn | ||
| } | ||
| ) | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.