-
Notifications
You must be signed in to change notification settings - Fork 126
feat: Add support for invoke local #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
pgrzesik
merged 5 commits into
serverless:master
from
CorentinDoue:feat/add-invoke-local
Jun 1, 2021
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
13d823d
refactor: extract provider.getRuntime
CorentinDoue 5c93797
refactor: extract validateEventsProperty
CorentinDoue 037a998
feat: implement invokeLocal for node events
CorentinDoue ff3c294
refactor: extract provider.getConfiguredEnvironment
CorentinDoue 4ee7b24
feat: load environment variables in local process.env
CorentinDoue File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
'use strict'; | ||
|
||
const validate = require('../shared/validate'); | ||
const setDefaults = require('../shared/utils'); | ||
const getDataAndContext = require('./lib/getDataAndContext'); | ||
const nodeJs = require('./lib/nodeJs'); | ||
|
||
class GoogleInvokeLocal { | ||
constructor(serverless, options) { | ||
this.serverless = serverless; | ||
this.options = options; | ||
|
||
this.provider = this.serverless.getProvider('google'); | ||
|
||
Object.assign(this, validate, setDefaults, getDataAndContext, nodeJs); | ||
|
||
this.hooks = { | ||
'initialize': () => { | ||
this.options = this.serverless.processedInput.options; | ||
}, | ||
'before:invoke:local:invoke': async () => { | ||
await this.validate(); | ||
await this.setDefaults(); | ||
await this.getDataAndContext(); | ||
}, | ||
'invoke:local:invoke': async () => this.invokeLocal(), | ||
}; | ||
} | ||
|
||
async invokeLocal() { | ||
const functionObj = this.serverless.service.getFunction(this.options.function); | ||
this.validateEventsProperty(functionObj, this.options.function, ['event']); // Only event is currently supported | ||
|
||
const runtime = this.provider.getRuntime(functionObj); | ||
if (!runtime.startsWith('nodejs')) { | ||
throw new Error(`Local invocation with runtime ${runtime} is not supported`); | ||
} | ||
return this.invokeLocalNodeJs(functionObj, this.options.data, this.options.context); | ||
} | ||
} | ||
|
||
module.exports = GoogleInvokeLocal; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
'use strict'; | ||
|
||
const sinon = require('sinon'); | ||
|
||
const GoogleProvider = require('../provider/googleProvider'); | ||
const GoogleInvokeLocal = require('./googleInvokeLocal'); | ||
const Serverless = require('../test/serverless'); | ||
|
||
describe('GoogleInvokeLocal', () => { | ||
let serverless; | ||
const functionName = 'myFunction'; | ||
const rawOptions = { | ||
f: functionName, | ||
}; | ||
const processedOptions = { | ||
function: functionName, | ||
}; | ||
let googleInvokeLocal; | ||
|
||
beforeAll(() => { | ||
serverless = new Serverless(); | ||
serverless.setProvider('google', new GoogleProvider(serverless)); | ||
googleInvokeLocal = new GoogleInvokeLocal(serverless, rawOptions); | ||
serverless.processedInput.options = processedOptions; | ||
}); | ||
|
||
describe('#constructor()', () => { | ||
it('should set the serverless instance', () => { | ||
expect(googleInvokeLocal.serverless).toEqual(serverless); | ||
}); | ||
|
||
it('should set the raw options if provided', () => { | ||
expect(googleInvokeLocal.options).toEqual(rawOptions); | ||
}); | ||
|
||
it('should make the provider accessible', () => { | ||
expect(googleInvokeLocal.provider).toBeInstanceOf(GoogleProvider); | ||
}); | ||
|
||
it.each` | ||
method | ||
${'validate'} | ||
${'setDefaults'} | ||
${'getDataAndContext'} | ||
${'invokeLocalNodeJs'} | ||
${'loadFileInOption'} | ||
${'validateEventsProperty'} | ||
${'addEnvironmentVariablesToProcessEnv'} | ||
`('should declare $method method', ({ method }) => { | ||
expect(googleInvokeLocal[method]).toBeDefined(); | ||
}); | ||
|
||
describe('hooks', () => { | ||
let validateStub; | ||
let setDefaultsStub; | ||
let getDataAndContextStub; | ||
let invokeLocalStub; | ||
|
||
beforeAll(() => { | ||
validateStub = sinon.stub(googleInvokeLocal, 'validate').resolves(); | ||
setDefaultsStub = sinon.stub(googleInvokeLocal, 'setDefaults').resolves(); | ||
getDataAndContextStub = sinon.stub(googleInvokeLocal, 'getDataAndContext').resolves(); | ||
invokeLocalStub = sinon.stub(googleInvokeLocal, 'invokeLocal').resolves(); | ||
}); | ||
|
||
afterEach(() => { | ||
googleInvokeLocal.validate.resetHistory(); | ||
googleInvokeLocal.setDefaults.resetHistory(); | ||
googleInvokeLocal.getDataAndContext.resetHistory(); | ||
googleInvokeLocal.invokeLocal.resetHistory(); | ||
}); | ||
|
||
afterAll(() => { | ||
googleInvokeLocal.validate.restore(); | ||
googleInvokeLocal.setDefaults.restore(); | ||
googleInvokeLocal.getDataAndContext.restore(); | ||
googleInvokeLocal.invokeLocal.restore(); | ||
}); | ||
|
||
it.each` | ||
hook | ||
${'initialize'} | ||
${'before:invoke:local:invoke'} | ||
${'invoke:local:invoke'} | ||
`('should declare $hook hook', ({ hook }) => { | ||
expect(googleInvokeLocal.hooks[hook]).toBeDefined(); | ||
}); | ||
|
||
describe('initialize hook', () => { | ||
it('should override raw options with processed options', () => { | ||
googleInvokeLocal.hooks.initialize(); | ||
expect(googleInvokeLocal.options).toEqual(processedOptions); | ||
}); | ||
}); | ||
|
||
describe('before:invoke:local:invoke hook', () => { | ||
it('should validate the configuration', async () => { | ||
await googleInvokeLocal.hooks['before:invoke:local:invoke'](); | ||
expect(validateStub.calledOnce).toEqual(true); | ||
}); | ||
|
||
it('should set the defaults values', async () => { | ||
await googleInvokeLocal.hooks['before:invoke:local:invoke'](); | ||
expect(setDefaultsStub.calledOnce).toEqual(true); | ||
}); | ||
|
||
it('should resolve the data and the context of the invocation', async () => { | ||
await googleInvokeLocal.hooks['before:invoke:local:invoke'](); | ||
expect(getDataAndContextStub.calledOnce).toEqual(true); | ||
}); | ||
}); | ||
|
||
describe('invoke:local:invoke hook', () => { | ||
it('should invoke the function locally', () => { | ||
googleInvokeLocal.hooks['invoke:local:invoke'](); | ||
expect(invokeLocalStub.calledOnce).toEqual(true); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('#invokeLocal()', () => { | ||
const functionObj = Symbol('functionObj'); | ||
const data = Symbol('data'); | ||
const context = Symbol('context'); | ||
const runtime = 'nodejs14'; | ||
let getFunctionStub; | ||
let validateEventsPropertyStub; | ||
let getRuntimeStub; | ||
let invokeLocalNodeJsStub; | ||
|
||
beforeAll(() => { | ||
googleInvokeLocal.options = { | ||
...processedOptions, // invokeLocal is called after the initialize hook which override the options | ||
data, // data and context are populated by getDataAndContext in before:invoke:local:invoke hook | ||
context, | ||
}; | ||
getFunctionStub = sinon.stub(serverless.service, 'getFunction').returns(functionObj); | ||
validateEventsPropertyStub = sinon | ||
.stub(googleInvokeLocal, 'validateEventsProperty') | ||
.returns(); | ||
getRuntimeStub = sinon.stub(googleInvokeLocal.provider, 'getRuntime').returns(runtime); | ||
|
||
invokeLocalNodeJsStub = sinon.stub(googleInvokeLocal, 'invokeLocalNodeJs').resolves(); | ||
}); | ||
|
||
afterEach(() => { | ||
serverless.service.getFunction.resetHistory(); | ||
googleInvokeLocal.validateEventsProperty.resetHistory(); | ||
googleInvokeLocal.provider.getRuntime.resetHistory(); | ||
googleInvokeLocal.invokeLocalNodeJs.resetHistory(); | ||
}); | ||
|
||
afterAll(() => { | ||
serverless.service.getFunction.restore(); | ||
googleInvokeLocal.validateEventsProperty.restore(); | ||
googleInvokeLocal.provider.getRuntime.restore(); | ||
googleInvokeLocal.invokeLocalNodeJs.restore(); | ||
}); | ||
|
||
it('should get the function configuration', async () => { | ||
await googleInvokeLocal.invokeLocal(); | ||
expect(getFunctionStub.calledOnceWith(functionName)).toEqual(true); | ||
}); | ||
|
||
it('should validate the function configuration', async () => { | ||
await googleInvokeLocal.invokeLocal(); | ||
expect( | ||
validateEventsPropertyStub.calledOnceWith(functionObj, functionName, ['event']) | ||
).toEqual(true); | ||
}); | ||
|
||
it('should get the runtime', async () => { | ||
await googleInvokeLocal.invokeLocal(); | ||
expect(getRuntimeStub.calledOnceWith(functionObj)).toEqual(true); | ||
}); | ||
|
||
it('should invoke locally the function with node js', async () => { | ||
await googleInvokeLocal.invokeLocal(); | ||
expect(invokeLocalNodeJsStub.calledOnceWith(functionObj, data, context)).toEqual(true); | ||
}); | ||
|
||
it('should throw if the runtime is not node js', async () => { | ||
getRuntimeStub.returns('python3'); | ||
await expect(googleInvokeLocal.invokeLocal()).rejects.toThrow( | ||
'Local invocation with runtime python3 is not supported' | ||
); | ||
}); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
'use strict'; | ||
|
||
const path = require('path'); | ||
const fs = require('fs'); | ||
const stdin = require('get-stdin'); | ||
|
||
module.exports = { | ||
async loadFileInOption(filePath, optionKey) { | ||
const absolutePath = path.isAbsolute(filePath) | ||
? filePath | ||
: path.join(this.serverless.serviceDir, filePath); | ||
|
||
if (!fs.existsSync(absolutePath)) { | ||
throw new Error(`The file you provided does not exist: ${absolutePath}`); | ||
} | ||
if (absolutePath.endsWith('.js')) { | ||
// to support js - export as an input data | ||
this.options[optionKey] = require(absolutePath); | ||
return; | ||
} | ||
this.options[optionKey] = await this.serverless.utils.readFile(absolutePath); | ||
}, | ||
|
||
async getDataAndContext() { | ||
// unless asked to preserve raw input, attempt to parse any provided objects | ||
if (!this.options.raw) { | ||
if (this.options.data) { | ||
try { | ||
this.options.data = JSON.parse(this.options.data); | ||
} catch (exception) { | ||
// do nothing if it's a simple string or object already | ||
} | ||
} | ||
if (this.options.context) { | ||
try { | ||
this.options.context = JSON.parse(this.options.context); | ||
} catch (exception) { | ||
// do nothing if it's a simple string or object already | ||
} | ||
} | ||
} | ||
|
||
if (!this.options.data) { | ||
if (this.options.path) { | ||
await this.loadFileInOption(this.options.path, 'data'); | ||
} else { | ||
try { | ||
this.options.data = await stdin(); | ||
} catch (e) { | ||
// continue if no stdin was provided | ||
} | ||
} | ||
} | ||
|
||
if (!this.options.context && this.options.contextPath) { | ||
await this.loadFileInOption(this.options.contextPath, 'context'); | ||
} | ||
}, | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
'use strict'; | ||
|
||
const sinon = require('sinon'); | ||
|
||
const GoogleProvider = require('../../provider/googleProvider'); | ||
const GoogleInvokeLocal = require('../googleInvokeLocal'); | ||
const Serverless = require('../../test/serverless'); | ||
|
||
jest.mock('get-stdin'); | ||
|
||
describe('getDataAndContext', () => { | ||
let serverless; | ||
let googleInvokeLocal; | ||
let loadFileInOptionStub; | ||
|
||
beforeEach(() => { | ||
serverless = new Serverless(); | ||
serverless.setProvider('google', new GoogleProvider(serverless)); | ||
googleInvokeLocal = new GoogleInvokeLocal(serverless, {}); | ||
loadFileInOptionStub = sinon.stub(googleInvokeLocal, 'loadFileInOption').resolves(); | ||
}); | ||
|
||
afterEach(() => { | ||
googleInvokeLocal.loadFileInOption.restore(); | ||
}); | ||
|
||
describe.each` | ||
key | pathKey | ||
${'data'} | ${'path'} | ||
${'context'} | ${'contextPath'} | ||
`('$key', ({ key, pathKey }) => { | ||
it('should keep the raw value if the value exist and there is the raw option', async () => { | ||
const rawValue = Symbol('rawValue'); | ||
googleInvokeLocal.options[key] = rawValue; | ||
googleInvokeLocal.options.raw = true; | ||
await googleInvokeLocal.getDataAndContext(); | ||
expect(googleInvokeLocal.options[key]).toEqual(rawValue); | ||
}); | ||
|
||
it('should keep the raw value if the value exist and is not a valid JSON', async () => { | ||
const rawValue = 'rawValue'; | ||
googleInvokeLocal.options[key] = rawValue; | ||
await googleInvokeLocal.getDataAndContext(); | ||
expect(googleInvokeLocal.options[key]).toEqual(rawValue); | ||
}); | ||
|
||
it('should parse the raw value if the value exist and is a stringified JSON', async () => { | ||
googleInvokeLocal.options[key] = '{"attribute":"value"}'; | ||
await googleInvokeLocal.getDataAndContext(); | ||
expect(googleInvokeLocal.options[key]).toEqual({ attribute: 'value' }); | ||
}); | ||
|
||
it('should load the file from the provided path if it exists', async () => { | ||
const path = 'path'; | ||
googleInvokeLocal.options[pathKey] = path; | ||
await googleInvokeLocal.getDataAndContext(); | ||
expect(loadFileInOptionStub.calledOnceWith(path, key)).toEqual(true); | ||
}); | ||
|
||
it('should not load the file from the provided path if the key already exists', async () => { | ||
const rawValue = Symbol('rawValue'); | ||
googleInvokeLocal.options[key] = rawValue; | ||
googleInvokeLocal.options[pathKey] = 'path'; | ||
|
||
await googleInvokeLocal.getDataAndContext(); | ||
|
||
expect(loadFileInOptionStub.notCalled).toEqual(true); | ||
expect(googleInvokeLocal.options[key]).toEqual(rawValue); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it depend on the
parsing
functionality ofreadFile
here? If not, then I would suggest to not useutils.readFile
and use nativefs
to read itThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to make it as iso as possible with the AWS implementation and it loads the files with this util: https://github.com/serverless/serverless/blob/d5e2baf714958c5718610659887f485f9bd161e4/lib/plugins/aws/invokeLocal/index.js#L62