From 9a2e26d3d9364212ba5b8c6060b7a7376e04bdfc Mon Sep 17 00:00:00 2001 From: Nicolas Froidure Date: Mon, 21 Aug 2023 07:53:00 +0200 Subject: [PATCH] feat(deploy): add the terraform deployment --- packages/whook-example/README.md | 46 ++ packages/whook-example/package.json | 7 +- packages/whook-example/src/cli.test.ts | 9 +- .../src/commands/terraformValues.ts | 372 ++++++++++++++++ .../src/config/staging/config.ts | 9 + .../src/config/staging/overrides.ts | 11 + packages/whook-example/src/index.test.ts | 46 +- packages/whook-example/src/openAPISchema.d.ts | 23 +- .../terraform/.terraform.lock.hcl | 76 ++++ .../whook-example/terraform/api_gateway.tf | 79 ++++ .../whook-example/terraform/api_lambdas.tf | 13 + .../whook-example/terraform/api_responses.tf | 399 ++++++++++++++++++ .../terraform/consumer_lambdas.tf | 49 +++ .../whook-example/terraform/cron_lambdas.tf | 61 +++ packages/whook-example/terraform/lambdas.tf | 104 +++++ packages/whook-example/terraform/main.tf | 5 + packages/whook-example/terraform/outputs.tf | 11 + packages/whook-example/terraform/variables.tf | 4 + packages/whook-example/terraform/versions.tf | 28 ++ 19 files changed, 1304 insertions(+), 48 deletions(-) create mode 100644 packages/whook-example/src/commands/terraformValues.ts create mode 100644 packages/whook-example/src/config/staging/config.ts create mode 100644 packages/whook-example/src/config/staging/overrides.ts create mode 100755 packages/whook-example/terraform/.terraform.lock.hcl create mode 100644 packages/whook-example/terraform/api_gateway.tf create mode 100644 packages/whook-example/terraform/api_lambdas.tf create mode 100644 packages/whook-example/terraform/api_responses.tf create mode 100644 packages/whook-example/terraform/consumer_lambdas.tf create mode 100644 packages/whook-example/terraform/cron_lambdas.tf create mode 100644 packages/whook-example/terraform/lambdas.tf create mode 100644 packages/whook-example/terraform/main.tf create mode 100644 packages/whook-example/terraform/outputs.tf create mode 100644 packages/whook-example/terraform/variables.tf create mode 100644 packages/whook-example/terraform/versions.tf diff --git a/packages/whook-example/README.md b/packages/whook-example/README.md index 7bb100fc..d9613922 100644 --- a/packages/whook-example/README.md +++ b/packages/whook-example/README.md @@ -88,6 +88,52 @@ APP_ENV=staging bin/lambdas_zip.sh APP_ENV=staging bin/lambdas_push.sh ``` +### Deploy with Terraform + +Otherwise, here is a full step by step setup for you. + +First install Terraform: + +```sh +wget https://releases.hashicorp.com/terraform/1.2.2/terraform_1.2.2_linux_amd64.zip +mkdir .bin +unzip -d .bin terraform_1.2.2_linux_amd64.zip +rm terraform_1.2.2_linux_amd64.zip +``` + +Then initialize the Terraform configuration: + +```sh +cd ./terraform +../.bin/terraform init; +``` + +Create a new workspace for each `APP_ENV`: + +```sh +../.bin/terraform workspace new staging +``` + +Plan the deployment: + +```sh +../.bin/terraform plan -out=terraform.plan -var "node_env=${NODE_ENV}" +``` + +Apply changes: + +```sh +../.bin/terraform apply -var "node_env=${NODE_ENV}" terraform.plan +``` + +Finally retrieve the API URL and add and enjoy! + +```sh +../.bin/terraform output api_url +curl "$(.bin/terraform output api_url)staging/v3/ping" +# {"pong":"pong"} +``` + Generate API types: ```sh diff --git a/packages/whook-example/package.json b/packages/whook-example/package.json index e1b04773..38fcf6aa 100644 --- a/packages/whook-example/package.json +++ b/packages/whook-example/package.json @@ -15,7 +15,12 @@ "distFiles": "'dist/**/*.js'", "ignore": [ "dist", - "builds" + "builds", + ".bin", + ".terraform", + "*.plan", + "*.tfstate.d", + "*.credentials.json" ], "bundleFiles": [ "bin", diff --git a/packages/whook-example/src/cli.test.ts b/packages/whook-example/src/cli.test.ts index ccd7478c..dbd8dc68 100644 --- a/packages/whook-example/src/cli.test.ts +++ b/packages/whook-example/src/cli.test.ts @@ -9,9 +9,9 @@ describe('commands should work', () => { ); expect({ - stdout: stdout.replace(/( |"|')([^ ]+)\/whook\//g, ' /whook/'), - stderr: stderr.replace(/( |"|')([^ ]+)\/whook\//g, ' /whook/'), - }).toMatchInlineSnapshot(` + stdout: stdout.replace(/( |"|')([^ ]+)\/whook\//g, ' /whook/'), + stderr: stderr.replace(/( |"|')([^ ]+)\/whook\//g, ' /whook/') +}).toMatchInlineSnapshot(` { "stderr": "⚡ - Loading configurations from /whook/packages/whook-example/dist/config/local/config.js". 🤖 - Initializing the \`$autoload\` service. @@ -19,8 +19,9 @@ On air 🚀🌕 ", "stdout": " -# Provided by "@whook/example": 1 commands +# Provided by "@whook/example": 2 commands - printEnv: A command printing every env values +- terraformValues: A command printing lambdas informations for Terraform # Provided by "@whook/whook": 8 commands diff --git a/packages/whook-example/src/commands/terraformValues.ts b/packages/whook-example/src/commands/terraformValues.ts new file mode 100644 index 00000000..33f141dc --- /dev/null +++ b/packages/whook-example/src/commands/terraformValues.ts @@ -0,0 +1,372 @@ +import { extra, autoService } from 'knifecycle'; +import { readArgs, identity } from '@whook/whook'; +import { getOpenAPIOperations } from '@whook/http-router'; +import { YError } from 'yerror'; +import { exec } from 'child_process'; +import crypto from 'crypto'; +import type { + WhookCommandArgs, + WhookCommandDefinition, + WhookConfig, + WhookAPIHandlerDefinition, +} from '@whook/whook'; +import type { ExecException } from 'child_process'; +import type { LogService } from 'common-services'; +import type { AppEnvVars } from 'application-services'; +import type { OpenAPIV3_1 } from 'openapi-types'; +import type { WhookAWSLambdaBaseCronConfiguration } from '../config/common/config.js'; + +export const definition: WhookCommandDefinition = { + description: 'A command printing lambdas informations for Terraform', + example: `whook terraformValues --type paths`, + arguments: { + type: 'object', + additionalProperties: false, + required: ['type'], + properties: { + type: { + description: 'Type of values to return', + type: 'string', + enum: ['globals', 'envvars', 'paths', 'lambdas', 'lambda', 'schedules'], + }, + pretty: { + description: 'Pretty print JSON values', + type: 'boolean', + }, + lambdaName: { + description: 'Name of the lambda', + type: 'string', + }, + pathsIndex: { + description: 'Index of the paths to retrieve', + type: 'number', + }, + lambdaType: { + description: 'Types of the lambdas to return', + type: 'string', + }, + }, + }, +}; + +export default extra(definition, autoService(initTerraformValuesCommand)); + +async function initTerraformValuesCommand({ + API, + CONFIG, + BASE_PATH, + ENV, + PROXYED_ENV_VARS, + log, + args, + execAsync = _execAsync, +}: { + API: OpenAPIV3_1.Document; + CONFIG: WhookConfig; + BASE_PATH: string; + ENV: AppEnvVars; + PROXYED_ENV_VARS: string[]; + log: LogService; + args: WhookCommandArgs; + execAsync: typeof _execAsync; +}) { + return async (): Promise => { + const { + namedArguments: { type, pretty, lambdaName, pathsIndex, lambdaType }, + } = readArgs<{ + type: string; + pretty: boolean; + lambdaName: string; + pathsIndex: number; + lambdaType: string; + }>(definition.arguments, args); + const allOperations = + await getOpenAPIOperations< + WhookAPIHandlerDefinition['operation']['x-whook'] + >(API); + const configurations = allOperations.map((operation) => { + const whookConfiguration = + operation['x-whook'] || + ({ + type: 'http', + } as WhookAPIHandlerDefinition['operation']['x-whook']); + const configuration = { + type: whookConfiguration?.type || 'http', + timeout: (whookConfiguration?.timeout || 10).toString(), + memory: (whookConfiguration?.memory || 128).toString(), + contentHandling: 'CONVERT_TO_TEXT', + description: operation.summary || '', + enabled: whookConfiguration?.disabled ? 'false' : 'true', + operationId: operation.operationId, + sourceOperationId: operation.operationId, + suffix: whookConfiguration?.suffix || '', + ...Object.keys(whookConfiguration || {}).reduce( + (accConfigurations, key) => ({ + ...accConfigurations, + [key]: ( + ( + whookConfiguration as NonNullable< + WhookAPIHandlerDefinition['operation']['x-whook'] + > + )[key] as string + ).toString(), + }), + {}, + ), + schedules: ( + (whookConfiguration as WhookAWSLambdaBaseCronConfiguration) + .schedules || [] + ).map(({ rule, body, enabled }) => ({ + rule: fixAWSSchedule(rule), + body, + enabled, + })), + }; + const qualifiedOperationId = + (configuration.sourceOperationId || operation.operationId) + + (configuration.suffix || ''); + + return { + qualifiedOperationId, + method: operation.method.toUpperCase(), + path: BASE_PATH + operation.path, + resourceName: buildPartName( + (BASE_PATH + operation.path).split('/').filter(identity), + ), + ...configuration, + }; + }); + + if (type === 'globals') { + const commitHash = await execAsync(`git rev-parse HEAD`); + const commitMessage = ( + await execAsync(`git rev-list --format=%B --max-count=1 HEAD`) + ).split('\n')[1]; + const openapi = JSON.stringify( + { + ...API, + servers: [], + paths: configurations + .filter(({ type }) => !type || type === 'http') + .reduce((currentPaths, configuration) => { + return { + ...currentPaths, + [configuration.path]: { + ...(currentPaths[configuration.path] || {}), + [configuration.method.toLowerCase()]: { + ...((API.paths?.[configuration.path] || {})[ + configuration.method.toLowerCase() + ] || {}), + operationId: configuration.qualifiedOperationId, + responses: {}, + ['x-amazon-apigateway-integration']: { + uri: `\${${configuration.qualifiedOperationId}}`, + httpMethod: 'POST', + contentHandling: configuration.contentHandling, + timeoutInMillis: + parseInt(configuration.timeout, 10) * 1000, + type: 'aws_proxy', + }, + }, + }, + }; + }, {}), + }, + null, + pretty ? 2 : 0, + ); + + const openapiHash = crypto + .createHash('md5') + .update(JSON.stringify(API)) + .digest('hex'); + const infos = { + apiDomain: new URL(CONFIG.baseURL || '').hostname, + commitHash, + commitMessage, + openapi, + openapiHash, + }; + log('info', JSON.stringify(infos, null, pretty ? 2 : 0)); + return; + } + + if (type === 'envvars') { + const env = PROXYED_ENV_VARS.reduce( + (finalEnv, key) => ({ ...finalEnv, [key]: ENV[key] }), + {}, + ); + log('info', JSON.stringify(env, null, pretty ? 2 : 0)); + return; + } + + if (type === 'lambdas') { + const lambdas = configurations + .filter((configuration) => + lambdaType ? configuration.type === lambdaType : true, + ) + .reduce( + (accLambdas, configuration) => ({ + ...accLambdas, + [configuration.qualifiedOperationId]: + configuration.resourceName + + '|' + + (configuration.path.split('/').filter(identity).length - 1) + + '|' + + configuration.timeout + + '|' + + configuration.memory, + }), + {}, + ); + + log('info', JSON.stringify(lambdas, null, pretty ? 2 : 0)); + return; + } + + if (type === 'paths') { + if (typeof pathsIndex !== 'number') { + throw new YError('E_PATHS_INDEX_REQUIRED'); + } + const allPathParts = configurations.reduce( + (accAllPathParts, configuration) => { + const parts = configuration.path.split('/').filter(identity); + + return parts.reduce((accPathParts, part, index) => { + const partName = buildPartName(parts.slice(0, index + 1)); + const parentName = + 0 < index ? buildPartName(parts.slice(0, index)) : '__root'; + + return [ + ...accPathParts, + { + partName, + parentName, + part, + index, + }, + ]; + }, accAllPathParts); + }, + [] as { + partName: string; + parentName: string; + part: string; + index: number; + }[], + ); + const allPathPartsForIndex = (allPathParts || []) + .filter(({ index }) => pathsIndex === index) + .reduce( + (accAllPathPartsForIndex, { partName, parentName, part }) => ({ + ...accAllPathPartsForIndex, + [partName]: parentName + '|' + part, + }), + {}, + ); + + log('info', JSON.stringify(allPathPartsForIndex, null, pretty ? 2 : 0)); + return; + } + + if (type === 'schedules') { + const schedules = configurations + .filter((configuration) => configuration.type === 'cron') + .reduce((accSchedules, configuration) => { + let scheduleIndex = 0; + + return { + ...accSchedules, + ...configuration.schedules.reduce( + (accOpSchedules, schedule) => ({ + ...accOpSchedules, + [`${ + configuration.qualifiedOperationId + }schedule${scheduleIndex++}`]: + configuration.qualifiedOperationId + + '|' + + schedule.rule + + '|' + + schedule.enabled + + '|' + + JSON.stringify(schedule.body || {}), + }), + {}, + ), + }; + }, {}); + + log('info', `${JSON.stringify(schedules, null, pretty ? 2 : 0)}`); + return; + } + + if (!lambdaName) { + throw new YError('E_LAMBDA_NAME_REQUIRED'); + } + + const lambdaConfiguration = configurations.find( + ({ qualifiedOperationId }) => qualifiedOperationId === lambdaName, + ); + + log( + 'info', + `${JSON.stringify( + { + ...lambdaConfiguration, + schedules: undefined, + }, + null, + pretty ? 2 : 0, + )}`, + ); + }; +} + +function buildPartName(parts: string[]): string { + return parts + .map((aPart, anIndex) => { + const realPartName = aPart.replace(/[{}]/g, ''); + + return anIndex + ? realPartName[0].toUpperCase() + realPartName.slice(1) + : realPartName; + }) + .join(''); +} + +function fixAWSSchedule(schedule: string): string { + if (typeof schedule === 'undefined') { + return ''; + } + + // The last wildcard is for years. + // This is a non-standard AWS addition... + // Also, we have to put a `?` in either + // day(month) or day(week) to fit AWS + // way of building cron tabs... + const fields = schedule.split(' '); + + if ('*' === fields[4]) { + fields[4] = '?'; + } else if ('*' === fields[2]) { + fields[2] = '?'; + } else { + throw new YError('E_BAD_AWS_SCHEDULE', schedule); + } + return `cron(${fields.concat('*').join(' ')})`; +} + +async function _execAsync(command: string): Promise { + return await new Promise((resolve, reject) => { + exec( + command, + (err: ExecException | null, stdout: string, stderr: string) => { + if (err) { + reject(YError.wrap(err, 'E_EXEC_FAILURE', stderr)); + return; + } + resolve(stdout.trim()); + }, + ); + }); +} diff --git a/packages/whook-example/src/config/staging/config.ts b/packages/whook-example/src/config/staging/config.ts new file mode 100644 index 00000000..780033b9 --- /dev/null +++ b/packages/whook-example/src/config/staging/config.ts @@ -0,0 +1,9 @@ +import COMMON_CONFIG from '../common/config.js'; +import type { AppConfig } from 'application-services'; + +const CONFIG: AppConfig = { + ...COMMON_CONFIG, + HOST: 'staging.api.example.com', +}; + +export default CONFIG; diff --git a/packages/whook-example/src/config/staging/overrides.ts b/packages/whook-example/src/config/staging/overrides.ts new file mode 100644 index 00000000..fcf69c35 --- /dev/null +++ b/packages/whook-example/src/config/staging/overrides.ts @@ -0,0 +1,11 @@ +import { type Overrides } from 'knifecycle'; + +// This allows you to map service names depending on +// the targeted environment +export const OVERRIDES: Overrides = { + // You could debug logs by substituting the normal log + // service by another named `debugLog`. + // log: 'debugLog', +}; + +export default OVERRIDES; diff --git a/packages/whook-example/src/index.test.ts b/packages/whook-example/src/index.test.ts index 9e83f80b..084dd068 100644 --- a/packages/whook-example/src/index.test.ts +++ b/packages/whook-example/src/index.test.ts @@ -100,9 +100,9 @@ describe('runProcess', () => { it('should work', async () => { expect(logger.output.mock.calls.length).toEqual(0); expect({ - debugCalls: logger.debug.mock.calls.map(filterPaths).sort(sortLogs), - logErrorCalls: logger.error.mock.calls.map(filterPaths).sort(sortLogs), - }).toMatchInlineSnapshot(` + debugCalls: logger.debug.mock.calls.map(filterPaths).sort(sortLogs), + logErrorCalls: logger.error.mock.calls.map(filterPaths).sort(sortLogs) +}).toMatchInlineSnapshot(` { "debugCalls": [ [ @@ -217,58 +217,55 @@ describe('runProcess', () => { "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/BUFFER_LIMIT.js'", - ], - [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/DECODERS.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/DEFAULT_ERROR_CODE.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/ENCODERS.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/HTTP_SERVER_OPTIONS.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/IGNORED_FILES_PREFIXES.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/IGNORED_FILES_SUFFIXES.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/JWT_SECRET_ENV_NAME.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/MAX_CLEAR_RATIO.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/PARSERS.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/PROCESS_NAME.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/REDUCED_FILES_SUFFIXES.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/SHIELD_CHAR.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/SIGNALS.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/STRINGIFYERS.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/TIMEOUT.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/uniqueId.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ - "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-cors/dist/services/BUFFER_LIMIT.js'", + "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", ], [ "Error: ENOENT: no such file or directory, access 'file:///home/whoiam/projects/whook/packages/whook-example/src/index.test.ts:78:59)", @@ -1074,9 +1071,6 @@ describe('runProcess', () => { [ "🚫 - File doesn't exist at "file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/PARSERS.js".", ], - [ - "🚫 - File doesn't exist at "file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/PROCESS_NAME.js".", - ], [ "🚫 - File doesn't exist at "file:///home/whoiam/projects/whook/packages/whook-aws-lambda/dist/services/REDUCED_FILES_SUFFIXES.js".", ], diff --git a/packages/whook-example/src/openAPISchema.d.ts b/packages/whook-example/src/openAPISchema.d.ts index d6964fe5..b4a80029 100644 --- a/packages/whook-example/src/openAPISchema.d.ts +++ b/packages/whook-example/src/openAPISchema.d.ts @@ -116,14 +116,14 @@ declare namespace Components { readonly headers?: { readonly [name: string]: unknown; }; - readonly body?: NonNullable; + readonly body?: unknown; }; export type handleMinutesResponse200 = { readonly status: S; readonly headers?: { readonly [name: string]: unknown; }; - readonly body?: NonNullable; + readonly body?: unknown; }; export type Diagnostic = { readonly status: S; @@ -169,29 +169,18 @@ declare namespace Components { }; } export namespace Schemas { -<<<<<<< HEAD export type TimeSchema = { currentDate?: string; }; + export type ExampleSchema = { + foo?: string; + bar?: string; + }; export type Echo = { echo: string; }; export type ResponsesDiagnosticBody0 = { transactions: { -======= - export type TimeSchema = NonNullable<{ - currentDate?: NonNullable; - }>; - export type ExampleSchema = NonNullable<{ - foo?: NonNullable; - bar?: NonNullable; - }>; - export type Echo = NonNullable<{ - echo: NonNullable; - }>; - export type ResponsesDiagnosticBody0 = NonNullable<{ - transactions: NonNullable<{ ->>>>>>> 22d682f5 (feat(@whook/example): add AWS build to @whook/example) [pattern: string]: unknown; }; }; diff --git a/packages/whook-example/terraform/.terraform.lock.hcl b/packages/whook-example/terraform/.terraform.lock.hcl new file mode 100755 index 00000000..89776b44 --- /dev/null +++ b/packages/whook-example/terraform/.terraform.lock.hcl @@ -0,0 +1,76 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/archive" { + version = "2.1.0" + constraints = "~> 2.1" + hashes = [ + "h1:f3WXKM/FBu5EMY6j2BGt982hzVMNicrxTyEAz5EsrOU=", + "zh:033279ecbf60f565303222e9a6d26b50fdebe43aa1c6e8f565f09bb64d67c3fd", + "zh:0af998e42eb421c92e87202df5bfee436b3cfe553214394f08d786c72a9e3f70", + "zh:1183b661c692f38409a61eefb5d412167c246fcd9e49d4d61d6d910012d206ba", + "zh:5febb66f4a8207117f71dcd460fb9c81d3afb7b600b5e598cf517cf6e27cf4b2", + "zh:66135ce46d29d0ccf0e3b6a119423754ca334dbf4266bc989cce5b0b667b5fde", + "zh:6b9dc1a4f0a680bb650a7191784927f99675a8c8dd3c155ba821185f630db604", + "zh:91e249482c016ecf6bf8b83849964005cd2d0b4396688419cd1752809b46b23e", + "zh:a6a2e5f2f010c511e66174cb84ea18899e8bcfc1354c4b9fed972fdb131ffffc", + "zh:bb1f6abc76552a883732caff897ff7b07a91977a9b4bb97915f6aac54116bb65", + "zh:f05a9a63607f85719fde705f58d82ee16fa67f9158a5c3424c0216507631eddf", + "zh:fc603a05a06814387ffa4a054d1baee8ea6b5ab32c53cb73e90a5bf9a2616777", + ] +} + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.30.0" + constraints = "~> 3.30" + hashes = [ + "h1:z9kdXY2A/+dIZrPy9hNlg/B5I/AuETQsp0jz9EgprIQ=", + "zh:01f562a6a31fe46a8ca74804f360e3452b26f71abc549ce1f0ab5a8af2484cdf", + "zh:25bacc5ed725051f0ab1f7d575e45c901e5b8e1d50da4156a31dda92b2b7e481", + "zh:349b79979d9169db614d8ebd1bc2e0caeb7a38dc816e261b8b2b4b5204615519", + "zh:5e41446acc54c6fc15e82c3fa14b72174b30eba81e0711ede297e5620c55a628", + "zh:68ad98f6d612bdc35a65d48950abc8e75c69decb49db28258ce8eeb5458586b7", + "zh:704603d65e8bac17d203b57c2db142c3134a91076e1b4a31c40f75eb3257dde8", + "zh:a362c700032b2db047d16007d52f28b3f216d32671b6b355d23bdaa082c66a4b", + "zh:bd197797b41268de3c93cad02b7c655dc0c4d8661abb37544ca049e6b1eccae6", + "zh:deb12ef0e3396a71d485977ddc14b695775f7937097ebf2b2f53ed348a4365e7", + "zh:ec8a7d0f02738f290107d39bf401d68ddce82a95cd9d998003f7e04b3a196411", + "zh:ffcc43b6c5e7f26c55e2a8c539d7370fca8042722400a3e06bdce4240bd7088a", + ] +} + +provider "registry.terraform.io/hashicorp/external" { + version = "2.1.0" + hashes = [ + "h1:wbtDfLeawmv6xVT1W0w0fctRCb4ABlaD3JTxwb1jXag=", + "zh:0d83ffb72fbd08986378204a7373d8c43b127049096eaf2765bfdd6b00ad9853", + "zh:7577d6edc67b1e8c2cf62fe6501192df1231d74125d90e51d570d586d95269c5", + "zh:9c669ded5d5affa4b2544952c4b6588dfed55260147d24ced02dca3a2829f328", + "zh:a404d46f2831f90633947ab5d57e19dbfe35b3704104ba6ec80bcf50b058acfd", + "zh:ae1caea1c936d459ceadf287bb5c5bd67b5e2a7819df6f5c4114b7305df7f822", + "zh:afb4f805477694a4b9dde86b268d2c0821711c8aab1c6088f5f992228c4c06fb", + "zh:b993b4a1de8a462643e78f4786789e44ce5064b332fee1cb0d6250ed085561b8", + "zh:c84b2c13fa3ea2c0aa7291243006d560ce480a5591294b9001ce3742fc9c5791", + "zh:c8966f69b7eccccb771704fd5335923692eccc9e0e90cb95d14538fe2e92a3b8", + "zh:d5fe68850d449b811e633a300b114d0617df6d450305e8251643b4d143dc855b", + "zh:ddebfd1e674ba336df09b1f27bbaa0e036c25b7a7087dc8081443f6e5954028b", + ] +} + +provider "registry.terraform.io/hashicorp/template" { + version = "2.2.0" + constraints = "~> 2.2.0" + hashes = [ + "h1:94qn780bi1qjrbC3uQtjJh3Wkfwd5+tTtJHOb7KTg9w=", + "zh:01702196f0a0492ec07917db7aaa595843d8f171dc195f4c988d2ffca2a06386", + "zh:09aae3da826ba3d7df69efeb25d146a1de0d03e951d35019a0f80e4f58c89b53", + "zh:09ba83c0625b6fe0a954da6fbd0c355ac0b7f07f86c91a2a97849140fea49603", + "zh:0e3a6c8e16f17f19010accd0844187d524580d9fdb0731f675ffcf4afba03d16", + "zh:45f2c594b6f2f34ea663704cc72048b212fe7d16fb4cfd959365fa997228a776", + "zh:77ea3e5a0446784d77114b5e851c970a3dde1e08fa6de38210b8385d7605d451", + "zh:8a154388f3708e3df5a69122a23bdfaf760a523788a5081976b3d5616f7d30ae", + "zh:992843002f2db5a11e626b3fc23dc0c87ad3729b3b3cff08e32ffb3df97edbde", + "zh:ad906f4cebd3ec5e43d5cd6dc8f4c5c9cc3b33d2243c89c5fc18f97f7277b51d", + "zh:c979425ddb256511137ecd093e23283234da0154b7fa8b21c2687182d9aea8b2", + ] +} diff --git a/packages/whook-example/terraform/api_gateway.tf b/packages/whook-example/terraform/api_gateway.tf new file mode 100644 index 00000000..a5f4a576 --- /dev/null +++ b/packages/whook-example/terraform/api_gateway.tf @@ -0,0 +1,79 @@ +# imports the globals +data "external" "globals" { + program = ["env", "APP_ENV=${terraform.workspace}", "NODE_ENV=${var.node_env}", "npx", "whook", "terraformValues", "--type='globals'"] + working_dir = ".." +} + +resource "aws_internet_gateway" "api_gateway" { + tags = { + Name = "Whook API ${terraform.workspace}" + } +} + +data "template_file" "template_file" { + template = data.external.globals.result["openapi"] + + vars = zipmap( + keys(data.external.api_lambdas.result), + [for key in keys(data.external.api_lambdas.result) : aws_lambda_function.lambda_function[key].invoke_arn] + ) +} + + +resource "aws_api_gateway_rest_api" "api_gateway_rest_api" { + name = "Whook API (${terraform.workspace})" + description = "An Whook API on the AWS Lambda platform" + minimum_compression_size = 1024 + binary_media_types = [ + "image/png", + "image/*", + "image/apng", + "image/webp", + "application/pdf" + ] + body = data.template_file.template_file.rendered + endpoint_configuration { + types = ["REGIONAL"] + } + depends_on = [aws_lambda_function.lambda_function] +} + +resource "aws_api_gateway_deployment" "api_gateway_deployment" { + rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id + # This has to be left empty to let api_gateway_stage + # manage the stage options + stage_name = "" + stage_description = "Deployment checksums ${data.external.globals.result["openapiHash"]}-${data.external.globals.result["commitHash"]}" + description = data.external.globals.result["commitMessage"] + depends_on = [aws_api_gateway_rest_api.api_gateway_rest_api] + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_api_gateway_stage" "api_gateway_stage" { + stage_name = terraform.workspace + rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id + deployment_id = aws_api_gateway_deployment.api_gateway_deployment.id + cache_cluster_enabled = true + cache_cluster_size = 0.5 + xray_tracing_enabled = true +} + +# You may want to add a custom domain that way +# resource "aws_api_gateway_domain_name" "api_gateway_domain_name" { +# domain_name = data.external.globals.result["domain"] +# regional_certificate_arn = aws_acm_certificate.acm_certificate.arn +# endpoint_configuration { +# types = ["REGIONAL"] +# } +# security_policy = "TLS_1_0" +# } + +# resource "aws_api_gateway_base_path_mapping" "api_gateway_base_path_mapping" { +# api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id +# stage_name = aws_api_gateway_stage.api_gateway_stage.stage_name +# domain_name = aws_api_gateway_domain_name.api_gateway_domain_name.domain_name +# base_path = "" +# } diff --git a/packages/whook-example/terraform/api_lambdas.tf b/packages/whook-example/terraform/api_lambdas.tf new file mode 100644 index 00000000..327bc942 --- /dev/null +++ b/packages/whook-example/terraform/api_lambdas.tf @@ -0,0 +1,13 @@ +# imports the cron lambda list +data "external" "api_lambdas" { + program = ["env", "APP_ENV=${terraform.workspace}", "NODE_ENV=${var.node_env}", "npx", "whook", "terraformValues", "--type='lambdas'", "--lambdaType='http'"] + working_dir = ".." +} + +resource "aws_lambda_permission" "lambdas" { + for_each = data.external.api_lambdas.result + statement_id = "AllowExecutionFromAPIGateway" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.lambda_function[each.key].arn + principal = "apigateway.amazonaws.com" +} diff --git a/packages/whook-example/terraform/api_responses.tf b/packages/whook-example/terraform/api_responses.tf new file mode 100644 index 00000000..82fd33ab --- /dev/null +++ b/packages/whook-example/terraform/api_responses.tf @@ -0,0 +1,399 @@ +# The purpose of this file is to make all AWS +# errors look like the Whook's ones + +resource "aws_api_gateway_gateway_response" "access_denied" { + rest_api_id = aws_api_gateway_rest_api.api_gateway_rest_api.id + status_code = "403" + response_type = "ACCESS_DENIED" + response_templates = { + "application/json" = <, + "id": , + "detail-type": , + "detail": , + "source": , + "account": , + "time":