Skip to content

Commit

Permalink
feat(deploy): add the terraform deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
nfroidure committed Feb 14, 2025
1 parent 6e035b8 commit cec6ad8
Show file tree
Hide file tree
Showing 8 changed files with 453 additions and 9 deletions.
16 changes: 8 additions & 8 deletions packages/whook-example/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ Per convention a Whook server build file must export
The `runBuild` function is intended to build the
project.

[See in context](./src/build.ts#L15-L19)
[See in context](./src/build.ts#L17-L21)



Expand All @@ -264,7 +264,7 @@ The `runBuild` function is intended to build the
The `prepareBuildEnvironment` create the build
environment

[See in context](./src/build.ts#L27-L31)
[See in context](./src/build.ts#L29-L33)



Expand All @@ -284,15 +284,15 @@ The `src/config/common/config.ts` one allows to add common

Whook provides several types you may extend here.

[See in context](./src/whook.d.ts#L26-L29)
[See in context](./src/whook.d.ts#L31-L34)



#### 2.1.1. AppEnvVars

The process environment can be typed by extending this type.

[See in context](./src/whook.d.ts#L32-L35)
[See in context](./src/whook.d.ts#L37-L40)



Expand All @@ -301,7 +301,7 @@ The process environment can be typed by extending this type.
The configuration is typed so that you are sure you cannot
produce a bad configuration for your API.

[See in context](./src/whook.d.ts#L43-L47)
[See in context](./src/whook.d.ts#L48-L52)



Expand All @@ -311,15 +311,15 @@ Here we export a custom API handler config type in order
to allow using the various plugins installed that deal
with the handlers.

[See in context](./src/whook.d.ts#L59-L64)
[See in context](./src/whook.d.ts#L67-L72)



#### 2.1.3. WhookMain

Here we export a main config to type AppEnv.

[See in context](./src/whook.d.ts#L70-L73)
[See in context](./src/whook.d.ts#L79-L82)



Expand Down Expand Up @@ -519,7 +519,7 @@ Here we export a custom API handler config type in order
to allow using the various plugins installed that deal
with the handlers.

[See in context](./src/whook.d.ts#L81-L86)
[See in context](./src/whook.d.ts#L90-L95)



Expand Down
51 changes: 51 additions & 0 deletions packages/whook-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,57 @@ Debug `knifecycle` internals (dependency injection issues):
DEBUG=knifecycle npm run dev
```

## Deploying with Google Cloud Functions

Create a project and save its credentials to `.credentials.json`.

Then install Terraform:
```sh
wget https://releases.hashicorp.com/terraform/0.12.24/terraform_0.12.24_linux_amd64.zip
mkdir .bin
unzip -d .bin terraform_0.12.24_linux_amd64.zip
rm terraform_0.12.24_linux_amd64.zip
```

Then initialize the Terraform configuration:
```sh
.bin/terraform init ./terraform;
```

Create a new workspace:
```sh
.bin/terraform workspace new staging
```

Build the functions:
```sh
NODE_ENV=staging npm run build
```

Build the Whook commands Terraform depends on:
```sh
npm run compile
```

Plan the deployment:
```sh
.bin/terraform plan -var="project_id=my-project-1664" \
-out=terraform.plan terraform
```

Apply changes:
```sh
# parallelism may be necessary to avoid hitting
# timeouts with a slow connection
.bin/terraform apply -parallelism=1 terraform.plan
```

Finally retrieve the API URL and enjoy!
```sh
.bin/terraform -var="project_id=my-project-1664" \
output api_url
```

## Testing the GCP Functions

```sh
Expand Down
3 changes: 2 additions & 1 deletion packages/whook-example/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 functions informations for Terraform
# Provided by "@whook/whook": 8 commands
Expand Down
241 changes: 241 additions & 0 deletions packages/whook-example/src/commands/terraformValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { extra, autoService } from 'knifecycle';
import { readArgs, WhookAPIHandlerConfig } from '@whook/whook';
import { YError } from 'yerror';
import { exec } from 'child_process';
import crypto from 'crypto';
import yaml from 'js-yaml';
import { type ExecException } from 'child_process';
import { type LogService } from 'common-services';
import {
type WhookCommandArgs,
type WhookCommandDefinition,
type WhookAPIHandlerDefinition,
type WhookOpenAPI,
} from '@whook/whook';
import { pathItemToOperationMap } from 'ya-open-api-types';

export const definition: WhookCommandDefinition = {
description: 'A command printing functions 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', 'paths', 'functions', 'function'],
},
pretty: {
description: 'Pretty print JSON values',
type: 'boolean',
},
functionName: {
description: 'Name of the function',
type: 'string',
},
pathsIndex: {
description: 'Index of the paths to retrieve',
type: 'number',
},
functionType: {
description: 'Types of the functions to return',
type: 'string',
},
},
},
};

export default extra(definition, autoService(initTerraformValuesCommand));

async function initTerraformValuesCommand({
API,
BASE_PATH,
log,
args,
execAsync = _execAsync,
}: {
API: WhookOpenAPI;
BASE_PATH: string;
log: LogService;
args: WhookCommandArgs;
execAsync: typeof _execAsync;
}) {
return async () => {
const {
namedArguments: { type, pretty, functionName, functionType },
} = readArgs<{
type: string;
pretty: boolean;
functionName: string;
functionType: string;
}>(definition.arguments, args);
const definitions: WhookAPIHandlerDefinition[] = [];

for (const [path, pathItem] of Object.entries(API.paths || {})) {
for (const [method, operation] of Object.entries(
pathItemToOperationMap(pathItem || {}),
)) {
definitions.push({
path,
method,
operation,
config: { type: 'http', ...((operation['x-whook'] as object) || {}) },
} as unknown as WhookAPIHandlerDefinition);
}
}

const configurations = definitions.map((definition) => {
const whookConfiguration = (definition.config || {
type: 'http',
}) as WhookAPIHandlerConfig;
const configuration = {
type: 'http',
timeout: '10',
memory: '128',
description: definition.operation.summary || '',
enabled: 'true',
sourceOperationId: definition.operation.operationId,
suffix: '',
...Object.keys(whookConfiguration || {}).reduce(
(accConfigurations, key) => ({
...accConfigurations,
[key]: (
(
whookConfiguration as NonNullable<
WhookAPIHandlerDefinition['operation']['x-whook']
>
)[key] as string
).toString(),
}),
{},
),
};
const qualifiedOperationId =
(configuration.sourceOperationId || definition.operation.operationId) +
(configuration.suffix || '');

return {
qualifiedOperationId,
method: definition.method.toUpperCase(),
path: definition.path,
...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 openapi2 = yaml.safeDump({
swagger: '2.0',
info: {
title: API.info.title,
description: API.info.description,
version: API.info.version,
},
host: '${infos_host}',
basePath: BASE_PATH,
schemes: ['https'],
produces: ['application/json'],
paths: configurations.reduce((accPaths, configuration) => {
const definition = definitions.find(
({ operation }) =>
operation.operationId === configuration.sourceOperationId,
);

return {
...accPaths,
[configuration.path]: {
...(accPaths[configuration.path] || {}),
[configuration.method.toLowerCase()]: {
summary: configuration.description || '',
operationId: configuration.qualifiedOperationId,
...((definition?.operation?.parameters || []).length
? {
parameters:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((definition?.operation?.parameters || []) as any).map(
({ in: theIn, name, required }) => ({
in: theIn,
name,
type: 'string',
required: required || false,
}),
),
}
: undefined),
'x-google-backend': {
address: `\${function_${configuration.qualifiedOperationId}}`,
},
responses: {
'200': { description: 'x', schema: { type: 'string' } },
},
},
},
};
}, {}),
});
const openapiHash = crypto
.createHash('md5')
.update(JSON.stringify(API))
.digest('hex');
const infos = {
commitHash,
commitMessage,
openapi2,
openapiHash,
};
log('info', JSON.stringify(infos));
return;
}

if (type === 'functions') {
const functions = configurations
.filter((configuration) =>
functionType ? configuration.type === functionType : true,
)
.reduce(
(accLambdas, configuration) => ({
...accLambdas,
[configuration.qualifiedOperationId]:
configuration.qualifiedOperationId,
}),
{},
);

log('info', `${JSON.stringify(functions, null, pretty ? 2 : 0)}`);
return;
}

if (!functionName) {
throw new YError('E_FUNCTION_NAME_REQUIRED');
}

const functionConfiguration = configurations.find(
({ qualifiedOperationId }) => qualifiedOperationId === functionName,
);

log(
'info',
`${JSON.stringify(functionConfiguration, null, pretty ? 2 : 0)}`,
);
};
}

async function _execAsync(command: string): Promise<string> {
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());
},
);
});
}
Loading

0 comments on commit cec6ad8

Please sign in to comment.