Skip to content

Commit

Permalink
feat(@whook/example): add Google Cloud Functions build
Browse files Browse the repository at this point in the history
  • Loading branch information
nfroidure committed Mar 30, 2020
1 parent cd36ba4 commit 8a17249
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 7 deletions.
51 changes: 51 additions & 0 deletions packages/whook-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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
```

[//]: # (::contents:end)

# Authors
Expand Down
2 changes: 2 additions & 0 deletions packages/whook-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,15 @@
"@whook/authorization": "^3.1.3",
"@whook/cli": "^3.1.3",
"@whook/cors": "^3.1.3",
"@whook/gcp-functions": "^3.1.3",
"@whook/http-router": "^3.1.3",
"@whook/http-transaction": "^3.1.3",
"@whook/swagger-ui": "^3.1.3",
"@whook/whook": "^3.1.3",
"common-services": "^7.0.0",
"ecstatic": "^4.1.2",
"http-auth-utils": "^2.3.0",
"js-yaml": "^3.13.1",
"knifecycle": "^9.0.0",
"strict-qs": "^6.0.2",
"yerror": "^5.0.0",
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 { LogService } from 'common-services';
import { ENVService, identity } from '@whook/whook';
import { readArgs, WhookCommandArgs, WhookCommandDefinition } from '@whook/cli';
import { OpenAPIV3 } from 'openapi-types';
import { getOpenAPIOperations } from '@whook/http-router';
import YError from 'yerror';
import { exec } from 'child_process';
import crypto from 'crypto';
import yaml from 'js-yaml';

export type WhookGoogleFunctionsBaseBuildConfiguration = {
private?: boolean;
memory?: number;
timeout?: number;
suffix?: string;
sourceOperationId?: string;
};
export type WhookGoogleFunctionsBuildConfiguration = {
type: 'http';
} & WhookGoogleFunctionsBaseBuildConfiguration;

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: OpenAPIV3.Document;
BASE_PATH: string;
log: LogService;
args: WhookCommandArgs;
execAsync: typeof _execAsync;
}) {
return async () => {
const { type, pretty, functionName, pathsIndex, functionType } = readArgs(
definition.arguments,
args,
) as {
type: string;
pretty: boolean;
functionName: string;
pathsIndex: number;
functionType: string;
};
const operations = getOpenAPIOperations(API);
const configurations = operations.map(operation => {
const whookConfiguration = (operation['x-whook'] || {
type: 'http',
}) as WhookGoogleFunctionsBuildConfiguration;
const configuration = {
type: 'http',
timeout: '10',
memory: '128',
description: operation.summary || '',
enabled: 'true',
sourceOperationId: operation.operationId,
suffix: '',
...Object.keys(whookConfiguration).reduce(
(accConfigurations, key) => ({
...accConfigurations,
[key]: whookConfiguration[key].toString(),
}),
{},
),
};
const qualifiedOperationId =
(configuration.sourceOperationId || operation.operationId) +
(configuration.suffix || '');

return {
qualifiedOperationId,
method: operation.method.toUpperCase(),
path: operation.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) => {
return {
...accPaths,
[configuration.path]: {
...(accPaths[configuration.path] || {}),
[configuration.method.toLowerCase()]: {
summary: configuration.description || '',
operationId: configuration.qualifiedOperationId,
'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)}`,
);
};
}

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<string> {
return await new Promise((resolve, reject) => {
exec(command, (err: Error, stdout: string, stderr: string) => {
if (err) {
reject(YError.wrap(err, 'E_EXEC_FAILURE', stderr));
return;
}
resolve(stdout.trim());
});
});
}
21 changes: 14 additions & 7 deletions packages/whook-example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
} from '@whook/whook';
import initHTTPRouter from '@whook/http-router';
import wrapHTTPRouterWithSwaggerUI from '@whook/swagger-ui';
import YError from 'yerror';
import {
runBuild as runBaseBuild,
prepareBuildEnvironment as prepareBaseBuildEnvironment,
} from '@whook/gcp-functions';

// Per convention a Whook server main file must export
// the following 3 functions to be composable:
Expand Down Expand Up @@ -72,7 +75,13 @@ export async function prepareEnvironment(
$.register(constant('TRANSACTIONS', {}));

// Setup your own whook plugins or avoid whook defaults by leaving it empty
$.register(constant('WHOOK_PLUGINS', ['@whook/cli', '@whook/whook']));
$.register(
constant('WHOOK_PLUGINS', [
'@whook/cli',
'@whook/whook',
'@whook/gcp-functions',
]),
);

return $;
}
Expand All @@ -85,10 +94,8 @@ export async function prepareEnvironment(
export async function runBuild(
innerPrepareEnvironment = prepareBuildEnvironment,
): Promise<void> {
throw new YError('E_NO_BUILD_IMPLEMENTED');

// Usually, here you call the installed build
// return runBaseBuild(innerPrepareEnvironment);
return runBaseBuild(innerPrepareEnvironment);
}

// The `prepareBuildEnvironment` create the build
Expand All @@ -99,7 +106,7 @@ export async function prepareBuildEnvironment(
$ = await prepareEnvironment($);

// Usually, here you call the installed build env
// $ = await prepareBaseBuildEnvironment($);
$ = await prepareBaseBuildEnvironment($);

// The build often need to know were initializer
// can be found to create a static build and
Expand All @@ -111,7 +118,7 @@ export async function prepareBuildEnvironment(
obfuscator: require.resolve(
'@whook/http-transaction/dist/services/obfuscator',
),
log: require.resolve('common-services/dist/log'),
log: require.resolve('@whook/gcp-functions/dist/services/log'),
time: require.resolve('common-services/dist/time'),
delay: require.resolve('common-services/dist/delay'),
}),
Expand Down
Loading

0 comments on commit 8a17249

Please sign in to comment.