Skip to content

Commit

Permalink
feat: Suport --context and --contextPath at invoke command (#8652)
Browse files Browse the repository at this point in the history
  • Loading branch information
lewgordon authored Dec 23, 2020
1 parent f9c8677 commit ff253e3
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 20 deletions.
16 changes: 16 additions & 0 deletions docs/providers/aws/cli-reference/invoke.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ serverless invoke [local] --function functionName
- `--data` or `-d` String data to be passed as an event to your function. By default data is read from standard input.
- `--raw` Pass data as a raw string even if it is JSON. If not set, JSON data are parsed and passed as an object.
- `--path` or `-p` The path to a json file with input data to be passed to the invoked function. This path is relative to the root directory of the service.
- `--contextPath`, The path to a json file holding input context to be passed to the invoked function. This path is relative to the root directory of the service.
- `--context` String data to be passed as a context to your function. Same like with `--data`, context included in `--contextPath` will overwrite the context you passed with `--context` flag.
- `--type` or `-t` The type of invocation. Either `RequestResponse`, `Event` or `DryRun`. Default is `RequestResponse`.
- `--log` or `-l` If set to `true` and invocation type is `RequestResponse`, it will output logging data of the invocation. Default is `false`.

Expand Down Expand Up @@ -73,6 +75,20 @@ output the result of the invocation in your terminal.
serverless invoke --function functionName --stage dev --region us-east-1 --data "hello world"
```

#### Function invocation with custom context

```bash
serverless invoke --function functionName --stage dev --region us-east-1 --context "hello world"
```

#### Function invocation with context passing

```bash
serverless invoke --function functionName --stage dev --region us-east-1 --contextPath lib/context.json
```

This example will pass the json context in the `lib/context.json` file (relative to the root of the service) while invoking the specified/deployed function.

#### Function invocation with data from standard input

```bash
Expand Down
39 changes: 32 additions & 7 deletions lib/plugins/aws/invoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ class AwsInvoke {
};
}

async validateFile(key) {
const absolutePath = path.resolve(this.serverless.config.servicePath, this.options[key]);
try {
return await this.serverless.utils.readFile(absolutePath);
} catch (err) {
if (err.code === 'ENOENT') {
throw new this.serverless.classes.Error(
'The file you provided does not exist.',
'FILE_NOT_FOUND'
);
}
throw err;
}
}

async extendedValidate() {
this.validate();
// validate function exists in service
Expand All @@ -30,13 +45,7 @@ class AwsInvoke {

if (!this.options.data) {
if (this.options.path) {
const absolutePath = path.isAbsolute(this.options.path)
? this.options.path
: path.join(this.serverless.config.servicePath, this.options.path);
if (!this.serverless.utils.fileExistsSync(absolutePath)) {
throw new this.serverless.classes.Error('The file you provided does not exist.');
}
this.options.data = this.serverless.utils.readFileSync(absolutePath);
this.options.data = await this.validateFile('path');
} else {
try {
this.options.data = await stdin();
Expand All @@ -46,13 +55,25 @@ class AwsInvoke {
}
}

if (!this.options.context && this.options.contextPath) {
this.options.context = await this.validateFile('contextPath');
}

try {
if (!this.options.raw) {
this.options.data = JSON.parse(this.options.data);
}
} catch (exception) {
// do nothing if it's a simple string or object already
}

try {
if (!this.options.raw && this.options.context) {
this.options.context = JSON.parse(this.options.context);
}
} catch (exception) {
// do nothing if it's a simple string or object already
}
}

async invoke() {
Expand All @@ -70,6 +91,10 @@ class AwsInvoke {
Payload: Buffer.from(JSON.stringify(this.options.data || {})),
};

if (this.options.context) {
params.ClientContext = Buffer.from(JSON.stringify(this.options.context)).toString('base64');
}

if (this.options.qualifier) {
params.Qualifier = this.options.qualifier;
}
Expand Down
6 changes: 6 additions & 0 deletions lib/plugins/invoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ class Invoke {
raw: {
usage: 'Flag to pass input data as a raw string',
},
context: {
usage: 'Context of the service',
},
contextPath: {
usage: 'Path to JSON or YAML file holding context data',
},
},
commands: {
local: {
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/invocation/context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "testProp": "testValue" }
137 changes: 124 additions & 13 deletions test/unit/lib/plugins/aws/invoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ const AwsProvider = require('../../../../../lib/plugins/aws/provider');
const Serverless = require('../../../../../lib/Serverless');
const { getTmpDirPath } = require('../../../../utils/fs');
const runServerless = require('../../../../utils/run-serverless');
const { ServerlessError } = require('../../../../../lib/classes/Error');

chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));

const expect = chai.expect;

Expand Down Expand Up @@ -300,8 +302,8 @@ describe('AwsInvoke', () => {
});
});

describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
describe('Common', () => {
describe('test/unit/lib/plugins/aws/invoke.test.js', () => {
describe.skip('Common', () => {
before(async () => {
await runServerless({
fixture: 'invocation',
Expand Down Expand Up @@ -331,7 +333,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
});
});

it('TODO: should accept no data', async () => {
xit('TODO: should accept no data', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback'],
Expand All @@ -343,7 +345,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L107-L113
});

it('TODO: should support plain string data', async () => {
xit('TODO: should support plain string data', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--data', 'inputData'],
Expand All @@ -355,7 +357,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L115-L121
});

it('TODO: should should not attempt to parse data with raw option', async () => {
xit('TODO: should should not attempt to parse data with raw option', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--data', '{"inputKey":"inputValue"}', '--raw'],
Expand All @@ -367,7 +369,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L131-L138
});

it('TODO: should support JSON file path as data', async () => {
xit('TODO: should support JSON file path as data', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--path', 'payload.json'],
Expand All @@ -379,7 +381,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L140-L154
});

it('TODO: should support absolute file path as data', async () => {
xit('TODO: should support absolute file path as data', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: [
Expand All @@ -396,7 +398,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L156-L168
});

it('TODO: should support YAML file path as data', async () => {
xit('TODO: should support YAML file path as data', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--path', 'payload.yaml'],
Expand All @@ -408,7 +410,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L170-L185
});

it('TODO: should throw error if data file path does not exist', async () => {
xit('TODO: should throw error if data file path does not exist', async () => {
await expect(
runServerless({
fixture: 'invocation',
Expand All @@ -419,7 +421,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L192-L199
});

it('TODO: should throw error if function is not provided', async () => {
xit('TODO: should throw error if function is not provided', async () => {
await expect(
runServerless({
fixture: 'invocation',
Expand All @@ -430,7 +432,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L102-L105
});

it('TODO: should support --type option', async () => {
xit('TODO: should support --type option', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--type', 'Event'],
Expand All @@ -442,7 +444,7 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L253-L267
});

it('TODO: should support --qualifier option', async () => {
xit('TODO: should support --qualifier option', async () => {
await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--qualifier', 'foo'],
Expand All @@ -454,7 +456,116 @@ describe.skip('test/unit/lib/plugins/aws/invoke.test.js', () => {
// https://github.com/serverless/serverless/blob/537fcac7597f0c6efbae7a5fc984270a78a2a53a/test/unit/lib/plugins/aws/invoke.test.js#L269-L287
});

it('TODO: should fail the process for failed invocations', async () => {
it('should support `--context` param', async () => {
const lambdaInvokeStub = sinon.stub();

const result = await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--context', 'somecontext'],
awsRequestStubMap: {
Lambda: {
invoke: args => {
lambdaInvokeStub.returns('payload');
return lambdaInvokeStub(args);
},
},
},
});
expect(lambdaInvokeStub).to.have.been.calledOnce;
expect(lambdaInvokeStub.args[0][0]).to.deep.equal({
ClientContext: 'InNvbWVjb250ZXh0Ig==', // "somecontext"
FunctionName: result.serverless.service.getFunction('callback').name,
InvocationType: 'RequestResponse',
LogType: 'None',
Payload: Buffer.from('{}'),
});
});

it('should support `--context` param with `--raw` param', async () => {
const lambdaInvokeStub = sinon.stub();

const result = await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--raw', '--context', '{"ctx": "somecontext"}'],
awsRequestStubMap: {
Lambda: {
invoke: args => {
lambdaInvokeStub.returns('payload');
return lambdaInvokeStub(args);
},
},
},
});
expect(lambdaInvokeStub).to.have.been.calledOnce;
expect(lambdaInvokeStub.args[0][0]).to.deep.equal({
ClientContext: 'IntcImN0eFwiOiBcInNvbWVjb250ZXh0XCJ9Ig==', // "{\"ctx\": \"somecontext\"}"
FunctionName: result.serverless.service.getFunction('callback').name,
InvocationType: 'RequestResponse',
LogType: 'None',
Payload: Buffer.from('{}'),
});
});

it('should support `--contextPath` param', async () => {
const lambdaInvokeStub = sinon.stub();
const contextDataFile = path.join(
__dirname,
'..',
'..',
'..',
'..',
'fixtures',
'invocation',
'context.json'
);

const result = await runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--contextPath', contextDataFile],
awsRequestStubMap: {
Lambda: {
invoke: args => {
lambdaInvokeStub.returns('payload');
return lambdaInvokeStub(args);
},
},
},
});
expect(lambdaInvokeStub).to.have.been.calledOnce;
expect(lambdaInvokeStub.args[0][0]).to.deep.equal({
ClientContext: 'eyJ0ZXN0UHJvcCI6InRlc3RWYWx1ZSJ9', // {"testProp":"testValue"}
FunctionName: result.serverless.service.getFunction('callback').name,
InvocationType: 'RequestResponse',
LogType: 'None',
Payload: Buffer.from('{}'),
});
});

it('should throw error on invoke with contextPath if file not exists', async () => {
const lambdaInvokeStub = sinon.stub();

const contextDataFile = path.join(getTmpDirPath(), 'context.json');

await expect(
runServerless({
fixture: 'invocation',
cliArgs: ['invoke', '--function', 'callback', '--contextPath', contextDataFile],
awsRequestStubMap: {
Lambda: {
invoke: args => {
lambdaInvokeStub.returns('payload');
return lambdaInvokeStub(args);
},
},
},
})
)
.to.be.eventually.rejectedWith(ServerlessError)
.and.have.property('code', 'FILE_NOT_FOUND');
expect(lambdaInvokeStub).to.have.been.callCount(0);
});

xit('TODO: should fail the process for failed invocations', async () => {
await expect(
runServerless({
fixture: 'invocation',
Expand Down

0 comments on commit ff253e3

Please sign in to comment.