Skip to content

Commit 547d5ed

Browse files
Added missed files
1 parent 280324f commit 547d5ed

9 files changed

Lines changed: 1420 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {
2+
App,
3+
Aspects,
4+
Tags,
5+
StackProps
6+
} from "aws-cdk-lib"
7+
import {AwsSolutionsChecks} from "cdk-nag"
8+
import {getConfigFromEnvVar, getBooleanConfigFromEnvVar} from "../config"
9+
10+
export interface StandardStackProps extends StackProps {
11+
readonly stackName: string
12+
readonly version: string
13+
readonly commitId: string
14+
readonly isPullRequest: boolean
15+
}
16+
17+
export function createApp(
18+
appName: string,
19+
repoName: string,
20+
driftDetectionGroup: string,
21+
region: string = "eu-west-2"
22+
): {app: App, props: StandardStackProps} {
23+
const stackName = getConfigFromEnvVar("stackName")
24+
const versionNumber = getConfigFromEnvVar("versionNumber")
25+
const commitId = getConfigFromEnvVar("commitId")
26+
const isPullRequest = getBooleanConfigFromEnvVar("isPullRequest")
27+
let cfnDriftDetectionGroup = driftDetectionGroup
28+
if (isPullRequest) {
29+
cfnDriftDetectionGroup += "-pull-request"
30+
}
31+
32+
const app = new App()
33+
34+
Aspects.of(app).add(new AwsSolutionsChecks({verbose: true}))
35+
36+
Tags.of(app).add("version", versionNumber)
37+
Tags.of(app).add("commit", commitId)
38+
Tags.of(app).add("stackName", stackName)
39+
Tags.of(app).add("cdkApp", appName)
40+
Tags.of(app).add("repo", repoName)
41+
Tags.of(app).add("cfnDriftDetectionGroup", cfnDriftDetectionGroup)
42+
43+
return {
44+
app,
45+
props: {
46+
env: {
47+
region
48+
},
49+
stackName,
50+
version: versionNumber,
51+
commitId,
52+
isPullRequest
53+
}
54+
}
55+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {CloudFormationClient, ListExportsCommand, DescribeStacksCommand} from "@aws-sdk/client-cloudformation"
2+
import {S3Client, HeadObjectCommand} from "@aws-sdk/client-s3"
3+
4+
export function getConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): string {
5+
const value = process.env[prefix + varName]
6+
if (!value) {
7+
throw new Error(`Environment variable ${prefix}${varName} is not set`)
8+
}
9+
return value
10+
}
11+
12+
export function getBooleanConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): boolean {
13+
const value = getConfigFromEnvVar(varName, prefix)
14+
return value.toLowerCase() === "true"
15+
}
16+
17+
export function getNumberConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): number {
18+
const value = getConfigFromEnvVar(varName, prefix)
19+
return Number(value)
20+
}
21+
22+
export async function getTrustStoreVersion(trustStoreFile: string, region: string = "eu-west-2"): Promise<string> {
23+
const cfnClient = new CloudFormationClient({region})
24+
const s3Client = new S3Client({region})
25+
const describeStacksCommand = new DescribeStacksCommand({StackName: "account-resources"})
26+
const response = await cfnClient.send(describeStacksCommand)
27+
const trustStoreBucketArn = response.Stacks![0].Outputs!
28+
.find(output => output.OutputKey === "TrustStoreBucket")!.OutputValue
29+
const bucketName = trustStoreBucketArn!.split(":")[5]
30+
const headObjectCommand = new HeadObjectCommand({Bucket: bucketName, Key: trustStoreFile})
31+
const headObjectResponse = await s3Client.send(headObjectCommand)
32+
return headObjectResponse.VersionId!
33+
}
34+
35+
export async function getCloudFormationExports(region: string = "eu-west-2"): Promise<Record<string, string>> {
36+
const cfnClient = new CloudFormationClient({region})
37+
const listExportsCommand = new ListExportsCommand({})
38+
const exports: Record<string, string> = {}
39+
let nextToken: string | undefined = undefined
40+
41+
do {
42+
const response = await cfnClient.send(listExportsCommand)
43+
response.Exports?.forEach((exp) => {
44+
if (exp.Name && exp.Value) {
45+
exports[exp.Name] = exp.Value
46+
}
47+
})
48+
nextToken = response.NextToken
49+
listExportsCommand.input.NextToken = nextToken
50+
} while (nextToken)
51+
52+
return exports
53+
}
54+
55+
export function getCFConfigValue(exports: Record<string, string>, exportName: string): string {
56+
const value = exports[exportName]
57+
if (!value) {
58+
throw new Error(`CloudFormation export ${exportName} not found`)
59+
}
60+
return value
61+
}
62+
63+
export function getBooleanCFConfigValue(exports: Record<string, string>, exportName: string): boolean {
64+
const value = getCFConfigValue(exports, exportName)
65+
return value.toLowerCase() === "true"
66+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
2+
import {LambdaClient, InvokeCommand} from "@aws-sdk/client-lambda"
3+
import {getCFConfigValue, getCloudFormationExports} from "../config"
4+
5+
export async function deployApi(
6+
specification: string,
7+
apiName: string,
8+
version: string,
9+
apigeeEnvironment: string,
10+
isPullRequest: boolean,
11+
awsEnvironment: string,
12+
stackName: string,
13+
mtlsSecretName: string,
14+
clientCertExportName: string,
15+
clientPrivateKeyExportName: string,
16+
proxygenPrivateKeyExportName: string,
17+
proxygenKid: string,
18+
dryRun: boolean
19+
): Promise<void> {
20+
const lambda = new LambdaClient({})
21+
async function invokeLambda(functionName: string, payload: unknown): Promise<void> {
22+
if (!dryRun) {
23+
const invokeResult = await lambda.send(new InvokeCommand({
24+
FunctionName: functionName,
25+
Payload: Buffer.from(JSON.stringify(payload))
26+
}))
27+
const responsePayload = Buffer.from(invokeResult.Payload!).toString()
28+
if (invokeResult.FunctionError) {
29+
throw new Error(`Error calling lambda ${functionName}: ${responsePayload}`)
30+
}
31+
console.log(`Lambda ${functionName} invoked successfully. Response:`, responsePayload)
32+
} else {
33+
console.log(`Would invoke lambda ${functionName}`)
34+
}
35+
}
36+
37+
let instance = apiName
38+
const spec = JSON.parse(specification)
39+
if (isPullRequest) {
40+
const pr_id = stackName.split("-").pop()
41+
instance = `${apiName}-pr-${pr_id}`
42+
spec.info.title = `[PR-${pr_id}] ${spec.info.title}`
43+
spec["x-nhsd-apim"].monitoring = false
44+
delete spec["x-nhsd-apim"].target.security.secret
45+
} else {
46+
spec["x-nhsd-apim"].target.security.secret = mtlsSecretName
47+
}
48+
spec.info.version = version
49+
spec["x-nhsd-apim"].target.url = `https://${stackName}.${awsEnvironment}.eps.national.nhs.uk`
50+
if (apigeeEnvironment === "prod") {
51+
spec.servers = [ {url: `https://api.service.nhs.uk/${instance}`} ]
52+
spec.components.securitySchemes["nhs-cis2-aal3"] = {
53+
"$ref": "https://proxygen.prod.api.platform.nhs.uk/components/securitySchemes/nhs-cis2-aal3"
54+
}
55+
} else {
56+
spec.servers = [ {url: `https://${apigeeEnvironment}.api.service.nhs.uk/${instance}`} ]
57+
spec.components.securitySchemes["nhs-cis2-aal3"] = {
58+
"$ref": "https://proxygen.ptl.api.platform.nhs.uk/components/securitySchemes/nhs-cis2-aal3"
59+
}
60+
}
61+
if (apigeeEnvironment.includes("sandbox")) {
62+
delete spec["x-nhsd-apim"]["target-attributes"] // Resolve issue with sandbox trying to look up app name
63+
}
64+
65+
const exports = await getCloudFormationExports()
66+
const clientCertArn = getCFConfigValue(exports, `account-resources:${clientCertExportName}`)
67+
const clientPrivateKeyArn = getCFConfigValue(exports, `account-resources:${clientPrivateKeyExportName}`)
68+
const proxygenPrivateKeyArn = getCFConfigValue(exports, `account-resources:${proxygenPrivateKeyExportName}`)
69+
70+
let put_secret_lambda = "lambda-resources-ProxygenPTLMTLSSecretPut"
71+
let instance_put_lambda = "lambda-resources-ProxygenPTLInstancePut"
72+
let spec_publish_lambda = "lambda-resources-ProxygenPTLSpecPublish"
73+
if (/^(int|sandbox|prod)$/.test(apigeeEnvironment)) {
74+
put_secret_lambda = "lambda-resources-ProxygenProdMTLSSecretPut"
75+
instance_put_lambda = "lambda-resources-ProxygenProdInstancePut"
76+
spec_publish_lambda = "lambda-resources-ProxygenProdSpecPublish"
77+
}
78+
79+
// --- Store the secret used for mutual TLS ---
80+
if (!isPullRequest) {
81+
console.log("Store the secret used for mutual TLS to AWS using Proxygen proxy lambda")
82+
await invokeLambda(put_secret_lambda, {
83+
apiName,
84+
environment: apigeeEnvironment,
85+
secretName: mtlsSecretName,
86+
secretKeyName: clientPrivateKeyArn,
87+
secretCertName: clientCertArn,
88+
kid: proxygenKid,
89+
proxygenSecretName: proxygenPrivateKeyArn
90+
})
91+
}
92+
93+
// --- Deploy the API instance ---
94+
console.log("Deploy the API instance using Proxygen proxy lambda")
95+
await invokeLambda(instance_put_lambda, {
96+
apiName,
97+
environment: apigeeEnvironment,
98+
specDefinition: spec,
99+
instance,
100+
kid: proxygenKid,
101+
proxygenSecretName: proxygenPrivateKeyArn
102+
})
103+
104+
// --- Publish the API spec to the catalogue ---
105+
let spec_publish_env
106+
if (apigeeEnvironment === "int") {
107+
console.log("Deploy the API spec to prod catalogue as it is int environment")
108+
spec.servers = [ {url: `https://sandbox.api.service.nhs.uk/${instance}`} ]
109+
spec_publish_env = "prod"
110+
} else if (apigeeEnvironment === "internal-dev" && !isPullRequest) {
111+
console.log("Deploy the API spec to uat catalogue as it is internal-dev environment")
112+
spec.servers = [ {url: `https://internal-dev-sandbox.api.service.nhs.uk/${instance}`} ]
113+
spec_publish_env = "uat"
114+
}
115+
if (spec_publish_env) {
116+
await invokeLambda(spec_publish_lambda, {
117+
apiName,
118+
environment: spec_publish_env,
119+
specDefinition: spec,
120+
instance,
121+
kid: proxygenKid,
122+
proxygenSecretName: proxygenPrivateKeyArn
123+
})
124+
}
125+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import fs from "fs"
2+
import path from "path"
3+
import {JSONSchema} from "json-schema-to-ts"
4+
5+
function isNotJSONSchemaArray(schema: JSONSchema | ReadonlyArray<JSONSchema>): schema is JSONSchema {
6+
return !Array.isArray(schema)
7+
}
8+
9+
function collapseExamples(schema: JSONSchema): JSONSchema {
10+
if (typeof schema !== "object" || schema === null) {
11+
return schema
12+
}
13+
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
const result: any = {...schema}
16+
17+
if (Array.isArray(schema.examples) && schema.examples.length > 0) {
18+
result.example = schema.examples[0]
19+
delete result.examples
20+
}
21+
22+
if (schema.items) {
23+
if (isNotJSONSchemaArray(schema.items)) {
24+
result.items = collapseExamples(schema.items)
25+
} else {
26+
result.items = schema.items.map(collapseExamples)
27+
}
28+
}
29+
30+
if (schema.properties) {
31+
const properties: Record<string, JSONSchema> = {}
32+
for (const key in schema.properties) {
33+
if (Object.prototype.hasOwnProperty.call(schema.properties, key)) {
34+
properties[key] = collapseExamples(schema.properties[key])
35+
}
36+
}
37+
result.properties = properties
38+
}
39+
40+
return result
41+
}
42+
43+
export function writeSchemas(
44+
schemas: Record<string, JSONSchema>,
45+
outputDir: string
46+
): void {
47+
if (!fs.existsSync(outputDir)) {
48+
fs.mkdirSync(outputDir, {recursive: true})
49+
}
50+
for (const name in schemas) {
51+
if (Object.prototype.hasOwnProperty.call(schemas, name)) {
52+
const schema = schemas[name]
53+
const fileName = `${name}.json`
54+
const filePath = path.join(outputDir, fileName)
55+
56+
try {
57+
fs.writeFileSync(filePath, JSON.stringify(collapseExamples(schema), null, 2))
58+
console.log(`Schema ${fileName} written successfully.`)
59+
} catch (error) {
60+
console.error(`Error writing schema ${fileName}:`, error)
61+
}
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)