Skip to content

Commit

Permalink
Merge pull request #58 from lifeomic/proxyLambdaFunction
Browse files Browse the repository at this point in the history
Use local lambda handler file
  • Loading branch information
DavidTanner authored Oct 8, 2022
2 parents 78f81e9 + 7512178 commit 4bbeaec
Show file tree
Hide file tree
Showing 15 changed files with 343 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr-branch-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: yarn
- run: yarn install
- run: yarn test --testTimeout=120000 --maxWorkers=50%
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: yarn
- run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
- run: yarn install --frozen-lockfile
Expand Down
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ $ npm install -g @lifeomic/alpha-cli
```bash
$ alpha --help
Options:
--help Show help [boolean]
-H, --header Pass custom header line to server
-X, --request Specify the request method to use
--data-binary Send binary data
--proxy Run a local http proxy that passes requests to alpha
--proxy-port The port to run the http proxy on
-V, --version Show the version number and quit [boolean]
--sign Sign requests using aws SignatureV4 [boolean]
--role Use STS to assume a role when signing
--help Show help [boolean]
-H, --header Pass custom header line to server [string]
--lambda-handler A javascript/typescript lambda handler to send requests
to [string]
--env-file File to load as environment variables when importing
lambda [string]
-X, --request Specify the request method to use
--data-binary Send binary data
--proxy-port port to proxy requests on [number] [default: 9000]
--proxy http proxy requests to alpha [boolean] [default: false]
--sign Sign requests with AWS SignatureV4
[boolean] [default: false]
--role Role to assume when signing [string]
--validate-status Validate the HTTP response code and fail if not 2XX
[boolean] [default: false]
-V, --version Show the version number and quit [boolean]

$ alpha lambda://user-service/users/jagoda | jq
{
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
},
"devDependencies": {
"@aws-sdk/types": "^3.110.0",
"@koa/router": "^12.0.0",
"@lifeomic/eslint-config-standards": "^3.0.0",
"@lifeomic/jest-config": "^1.1.3",
"@lifeomic/typescript-config": "^1.0.3",
"@swc/core": "^1.2.207",
"@swc/jest": "^0.2.21",
"@types/glob": "^7.2.0",
"@types/jest": "^28.1.3",
"@types/js-yaml": "^4.0.5",
"@types/koa": "^2.13.4",
"@types/koa-bodyparser": "^4.3.7",
"@types/koa__router": "^12.0.0",
"@types/node": "^16",
"aws-sdk-client-mock": "^1.0.0",
"conventional-changelog-conventionalcommits": "^4.6.3",
Expand All @@ -41,15 +44,19 @@
"koa": "^2.13.4",
"koa-bodyparser": "^4.3.0",
"semantic-release": "^19.0.2",
"serverless-http": "^3.0.2",
"ts-jest": "^28.0.5",
"ts-node": "^10.8.1",
"typescript": "^4.7.4"
"typescript": "^4.7.4",
"ulid": "^2.3.0"
},
"dependencies": {
"@aws-sdk/client-sts": "^3.121.0",
"@lifeomic/alpha": "^5.1.0",
"axios": "^0.27.2",
"dotenv": "^16.0.2",
"glob": "^8.0.3",
"js-yaml": "^4.1.0",
"yargs": "^17.5.1"
},
"publishConfig": {
Expand Down
3 changes: 3 additions & 0 deletions src/alphaProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const alphaProxy = (baseConfig: AlphaCliConfig) => {
...req.headers as Record<string, string>,
},
};
if (requestConfig.lambda) {
requestConfig.url = url;
}

let data = '';

Expand Down
16 changes: 11 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ValidationError } from './ValidationError';
import { alphaProxy } from './alphaProxy';
import { AlphaCliConfig } from './types';
import { callAlpha } from './utils';
import { AddressInfo } from 'net';

const config: AlphaCliConfig = {
transformResponse: [(data) => data],
Expand All @@ -23,17 +24,22 @@ const run = async () => {

const args = await yargs.parse();

const skipRequest = plugins.some((execute) => execute(config, args));
const results: any[] = [];
for (const plugin of plugins) {
results.push(await plugin(config, args));
}

const skipRequest = results.some((next) => next);

const {
proxied,
proxyPort,
} = config;

if (proxied) {
alphaProxy(config).on('listening', () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
console.log(`Proxy is listening on port ${proxyPort!}; Press any key to quit;`);
const srv = alphaProxy(config);
srv.on('listening', () => {
const { address, port } = srv.address() as AddressInfo;
console.log(`Proxy is listening on port ${address}:${port}; Press any key to quit;`);
});

// These are only relevant in a terminal, not in tests
Expand Down
50 changes: 50 additions & 0 deletions src/plugins/lambda-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AlphaCliArguments, AlphaCliConfig } from '../types';
import { Argv } from 'yargs';
import { promises as fs } from 'fs';
import path from 'path';
import { parse } from 'dotenv';
import { load } from 'js-yaml';

const loadEnvFile = async (envFile: string) => {
const ext = path.extname(envFile).toLowerCase();
const contents = await fs.readFile(envFile, 'utf-8');
let newEnv: Record<string, any> = {};
if (ext === '.env') {
newEnv = parse(contents);
} else if (ext === '.json') {
newEnv = JSON.parse(contents);
} else if (['.yaml', '.yml'].includes(ext)) {
newEnv = load(contents) as Record<string, any>;
} else {
throw new Error(`Unable to load ${envFile}, unrecognized extension ${ext}`);
}
Object.assign(process.env, newEnv);
};

export default (yargs: Argv) => {
yargs
.option('lambda-handler', {
type: 'string',
describe: 'A javascript/typescript lambda handler to send requests to',
})
.option('env-file', {
type: 'string',
describe: 'File to load as environment variables when importing lambda',
});

return async (
config: AlphaCliConfig,
{
'lambda-handler': lambdaHandler,
'env-file': envFile,
}: AlphaCliArguments,
) => {
if (lambdaHandler) {
if (envFile) {
await loadEnvFile(envFile);
}
const exported = await import(lambdaHandler);
config.lambda = exported.handler || exported.default.handler;
}
};
};
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface AlphaCliArguments {
'data-binary'?: any;
proxy?: boolean;
'proxy-port'?: number;
'lambda-handler'?: string;
'env-file'?: string;
'validate-status'?: boolean;
version?: boolean;
sign?: boolean;
Expand Down
32 changes: 32 additions & 0 deletions test/lambdaHandlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import path from 'path';
import { getPort, runCommand, spawnProxy } from './utils';
import { ulid } from 'ulid';

describe.each([
'exportApp',
'exportHandler',
])('can send requests to %s', (handlerFile) => {
const param: string = ulid();
const filePath = path.join(__dirname, 'lambdaHandlers', `${handlerFile}.ts`);

test('will send request to exported handler', async () => {
const { stdout, stderr } = await runCommand('--lambda-handler', filePath, `/echo/${param}`);
expect(stderr).toBeFalsy();
expect(stdout).toBe(param);
});

test('will proxy requests to the exported handler', async () => {
const proxyPort = await getPort();
const process = await spawnProxy('--proxy', '--proxy-port', proxyPort, '--lambda-handler', filePath);

try {
const { stdout, stderr } = await runCommand(`http://localhost:${proxyPort}/echo/${param}`);

expect(stdout).toBe(param);
expect(stderr).toBeFalsy();
} finally {
process.kill();
}
});
});

21 changes: 21 additions & 0 deletions test/lambdaHandlers/exportApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Koa from 'koa';
import Router from '@koa/router';
import serverless from 'serverless-http';

const app = new Koa() as Koa & { handler: ReturnType<typeof serverless>; };

const router = new Router();

router.get('/echo/:param', (ctx) => {
ctx.body = ctx.params.param;
ctx.status = 200;
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
app.use(router.routes());
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
app.use(router.allowedMethods());

app.handler = serverless(app);

export default app;
19 changes: 19 additions & 0 deletions test/lambdaHandlers/exportHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Koa from 'koa';
import Router from '@koa/router';
import serverless from 'serverless-http';

const app = new Koa();

const router = new Router();

router.get('/echo/:param', (ctx) => {
ctx.body = ctx.params.param;
ctx.status = 200;
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
app.use(router.routes());
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
app.use(router.allowedMethods());

export const handler = serverless(app);
91 changes: 91 additions & 0 deletions test/plugins/lambda-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import path from 'path';
import yargs from 'yargs';
import { ulid } from 'ulid';
import { dump } from 'js-yaml';
import { promises as fs } from 'fs';

import lambdaHandlerPlugin from '../../src/plugins/lambda-handler';
import { AlphaCliArguments, AlphaCliConfig } from '../../src/types';

const buildDir = path.join(__dirname, '..', 'build');
const lambdaHandler = path.join(__dirname, '..', 'lambdaHandlers', 'exportHandler.ts');
const pluginFunc = lambdaHandlerPlugin(yargs);

beforeAll(async () => {
await fs.mkdir(buildDir, { recursive: true });
});

test.each([
'exportHandler.ts',
'exportApp.ts',
])('%s will load the lambda-handler function', async (lambdaHandlerFile) => {
const config: AlphaCliConfig = {
responsePostProcessors: [],
};
const cliArgs: AlphaCliArguments = {
_: [''],
'lambda-handler': path.join(__dirname, '..', 'lambdaHandlers', lambdaHandlerFile),
};
await expect(pluginFunc(config, cliArgs)).resolves.toBeUndefined();
expect(config).toHaveProperty('lambda', expect.any(Function));
});

test('will throw exception on unknown extension', async () => {
const ext = `.${ulid()}`;
const envFile = path.join(buildDir, `${ulid()}${ext.toUpperCase()}`);
await fs.writeFile(envFile, '', 'utf-8');

const config: AlphaCliConfig = {
responsePostProcessors: [],
};
const cliArgs: AlphaCliArguments = {
_: [''],
'lambda-handler': lambdaHandler,
'env-file': envFile,
};
try {
await expect(pluginFunc(config, cliArgs)).rejects.toThrowError(`Unable to load ${envFile}, unrecognized extension ${ext.toLowerCase()}`);
} finally {
await fs.rm(envFile, { force: true });
}
});

describe.each([
'json',
'yaml',
'yml',
'env',
])('will load %s into the process.env', (ext) => {
const envFile = path.join(buildDir, `${ulid()}.${ext}`);
const envVars = {
[ulid()]: ulid(),
};
beforeAll(async () => {
let envString = '';
if (ext === 'json') {
envString = JSON.stringify(envVars);
} else if (ext === 'env') {
envString = Object.entries(envVars).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join('\n');
} else if (['yaml', 'yml'].includes(ext)) {
envString = dump(envVars);
}

await fs.writeFile(envFile, envString, 'utf-8');
});
afterAll(async () => {
await fs.rm(envFile, { force: true });
});

test('will load the env file', async () => {
const config: AlphaCliConfig = {
responsePostProcessors: [],
};
const cliArgs: AlphaCliArguments = {
_: [''],
'lambda-handler': lambdaHandler,
'env-file': envFile,
};
await expect(pluginFunc(config, cliArgs)).resolves.toBeUndefined();
expect(process.env).toStrictEqual(expect.objectContaining(envVars));
});
});
8 changes: 4 additions & 4 deletions test/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test('The --proxy flag starts a proxy to send commands to alpha', async () => {
const process = await spawnProxy('--proxy', '--proxy-port', proxyPort, context.url);

try {
const { stdout, stderr } = await runCommand('-H', 'Test-Header: header value', `http://127.0.0.1:${proxyPort}/headerTest`);
const { stdout, stderr } = await runCommand('-H', 'Test-Header: header value', `http://localhost:${proxyPort}/headerTest`);
const headers = JSON.parse(stdout) as Record<string, string>;

expect(Object.keys(headers).sort()).toEqual(['accept', 'connection', 'host', 'test-header', 'user-agent']);
Expand All @@ -59,7 +59,7 @@ test('The proxy passes data', async () => {
'--data-binary', '{"message":"hello"}',
'--header', 'Content-Type: text/plain',
'--request', 'POST',
`http://127.0.0.1:${proxyPort}/dataTest`,
`http://localhost:${proxyPort}/dataTest`,
);

expect(stdout).toBe('{"message":"hello"}');
Expand All @@ -78,7 +78,7 @@ test('The proxy handles errors', async () => {
'--data-binary', '{"message":"hello"}',
'--header', 'Content-Type: text/plain',
'--request', 'POST',
`http://127.0.0.1:${proxyPort}/derp`,
`http://localhost:${proxyPort}/derp`,
);
expect(stdout).toMatch(/Error: connect/);
} finally {
Expand All @@ -89,7 +89,7 @@ test('The proxy handles errors', async () => {
test('The proxy ends if the user presses a key', async () => {
const proxyPort = await getPort();

const process = await spawnProxy('--proxy', '--proxy-port', proxyPort, `http://127.0.0.1:${proxyPort}/headerTest`);
const process = await spawnProxy('--proxy', '--proxy-port', proxyPort, `http://localhost:${proxyPort}/headerTest`);
try {
process.stdin.write('q\n');
await new Promise((resolve) => {
Expand Down
Loading

0 comments on commit 4bbeaec

Please sign in to comment.