From c338b58da26efaaa08b4f8ba7c002feda4089ec1 Mon Sep 17 00:00:00 2001 From: Grady Rogers Date: Fri, 12 Mar 2021 10:15:00 -0500 Subject: [PATCH 1/5] Allow function.image.command[0] to define the handler name --- lib/validate.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/validate.js b/lib/validate.js index 8209ec8e2..d95048445 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -17,6 +17,23 @@ const preferredExtensions = [ '.js', '.ts', '.jsx', '.tsx' ]; module.exports = { validate() { + const getHandlerFileAndFunctionName = functionDefinition => { + const { handler: handlerProp, image: imageProp } = functionDefinition; + + if (handlerProp) { + return handlerProp; + } + + if (!imageProp || !imageProp.command || !imageProp.command.length < 1) { + const docsLink = 'https://www.serverless.com/blog/container-support-for-lambda'; + throw new this.serverless.classes.Error( + `Either function.handler or function.image must be defined. Pass the handler name (i.e. 'index.handler') as the value for function.image.command[0]. For help see: ${docsLink}` + ); + } + + return imageProp.command[0]; + }; + const getHandlerFile = handler => { // Check if handler is a well-formed path based handler. const handlerEntry = /(.*)\..*?$/.exec(handler); @@ -59,7 +76,7 @@ module.exports = { }; const getEntryForFunction = (name, serverlessFunction) => { - const handler = serverlessFunction.handler; + const handler = getHandlerFileAndFunctionName(serverlessFunction); const handlerFile = getHandlerFile(handler); if (!handlerFile) { @@ -212,7 +229,7 @@ module.exports = { }), funcName => { const func = this.serverless.service.getFunction(funcName); - const handler = func.handler; + const handler = getHandlerFileAndFunctionName(func); const handlerFile = path.relative('.', getHandlerFile(handler)); return { handlerFile, From 3024633b01a43b585425ec2cc8ec77097558feae Mon Sep 17 00:00:00 2001 From: Grady Rogers Date: Fri, 12 Mar 2021 10:26:13 -0500 Subject: [PATCH 2/5] add testing for image.command[0] handler --- lib/validate.js | 2 +- tests/validate.test.js | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/validate.js b/lib/validate.js index d95048445..b5cdae2c7 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -24,7 +24,7 @@ module.exports = { return handlerProp; } - if (!imageProp || !imageProp.command || !imageProp.command.length < 1) { + if (!imageProp || !imageProp.command || imageProp.command.length < 1) { const docsLink = 'https://www.serverless.com/blog/container-support-for-lambda'; throw new this.serverless.classes.Error( `Either function.handler or function.image must be defined. Pass the handler name (i.e. 'index.handler') as the value for function.image.command[0]. For help see: ${docsLink}` diff --git a/tests/validate.test.js b/tests/validate.test.js index ee4210b99..1c5217be8 100644 --- a/tests/validate.test.js +++ b/tests/validate.test.js @@ -605,6 +605,24 @@ describe('validate', () => { } ], runtime: 'provided' + }, + func4: { + artifact: 'artifact-func4.zip', + events: [ + { + http: { + method: 'POST', + path: 'func4path' + } + }, + { + nonhttp: 'non-http' + } + ], + image: { + name: 'custom-image', + command: ['module4.func1handler'] + } } }; @@ -626,11 +644,12 @@ describe('validate', () => { const lib = require('../lib/index'); const expectedLibEntries = { module1: './module1.js', - module2: './module2.js' + module2: './module2.js', + module4: './module4.js' }; expect(lib.entries).to.deep.equal(expectedLibEntries); - expect(globSyncStub).to.have.callCount(2); + expect(globSyncStub).to.have.callCount(3); expect(serverless.cli.log).to.not.have.been.called; return null; }); From 53b2bba867f37bf299f193fa45bb408d80008858 Mon Sep 17 00:00:00 2001 From: Grady Rogers Date: Fri, 12 Mar 2021 11:23:39 -0500 Subject: [PATCH 3/5] documentation --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 8d08df3ad..ebda8bc87 100644 --- a/README.md +++ b/README.md @@ -551,6 +551,28 @@ custom: ``` Will run each webpack build one at a time which helps reduce memory usage and in some cases impoves overall build performance. +### Support for Docker Images as Custom Runtimes +AWS Lambda and `serverless` started supporting the use of Docker images as custom runtimes in 2021. See the [serverless documentation](https://www.serverless.com/blog/container-support-for-lambda) for details on how to configure a `serverless.yml` to use these features. + +** NOTE: You must provide an override for the Image `CMD` property in your function definitions.** +See [Dockerfile documentation](https://docs.docker.com/engine/reference/builder/#cmd) for more information about the native Docker `CMD` property. + +In the following example `entrypoint` is inherited from the shared Docker image, while `command` is provided as an override for each function: +```yaml +# serverless.yml +functions: + myFunction1: + image: + name: public.ecr.aws/lambda/nodejs:12 + command: + - app.handler1 + myFunction2: + image: + name: public.ecr.aws/lambda/nodejs:12 + command: + - app.handler2 +``` + ## Usage ### Automatic bundling From 2b3aba49e3b69817e427b9ea59199f015062246d Mon Sep 17 00:00:00 2001 From: Grady Rogers Date: Fri, 12 Mar 2021 11:39:53 -0500 Subject: [PATCH 4/5] documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebda8bc87..a36a130ec 100644 --- a/README.md +++ b/README.md @@ -554,7 +554,7 @@ Will run each webpack build one at a time which helps reduce memory usage and in ### Support for Docker Images as Custom Runtimes AWS Lambda and `serverless` started supporting the use of Docker images as custom runtimes in 2021. See the [serverless documentation](https://www.serverless.com/blog/container-support-for-lambda) for details on how to configure a `serverless.yml` to use these features. -** NOTE: You must provide an override for the Image `CMD` property in your function definitions.** +**NOTE: You must provide an override for the Image `CMD` property in your function definitions.** See [Dockerfile documentation](https://docs.docker.com/engine/reference/builder/#cmd) for more information about the native Docker `CMD` property. In the following example `entrypoint` is inherited from the shared Docker image, while `command` is provided as an override for each function: From dda85dbf1655090ca745d66316bb0c1c093a354a Mon Sep 17 00:00:00 2001 From: Grady Rogers Date: Mon, 22 Mar 2021 12:35:33 -0400 Subject: [PATCH 5/5] added additional test coverage --- tests/validate.test.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/validate.test.js b/tests/validate.test.js index 1c5217be8..78e7cb274 100644 --- a/tests/validate.test.js +++ b/tests/validate.test.js @@ -480,6 +480,20 @@ describe('validate', () => { } } ] + }, + dockerfunc: { + image: { + name: 'some-image-uri', + command: ['com.serverless.Handler'] + }, + events: [ + { + http: { + method: 'POST', + path: 'mydockerfuncpath' + } + } + ] } }; @@ -517,6 +531,7 @@ describe('validate', () => { return expect(module.validate()).to.be.fulfilled.then(() => { const lib = require('../lib/index'); const expectedLibEntries = { + 'com.serverless': './com.serverless.js', module1: './module1.js', module2: './module2.js', 'handlers/func3/module2': './handlers/func3/module2.js', @@ -524,7 +539,7 @@ describe('validate', () => { }; expect(lib.entries).to.deep.equal(expectedLibEntries); - expect(globSyncStub).to.have.callCount(4); + expect(globSyncStub).to.have.callCount(5); expect(serverless.cli.log).to.not.have.been.called; return null; }); @@ -772,6 +787,15 @@ describe('validate', () => { key: 'handlers/module2/func3/module2', value: './handlers/module2/func3/module2.js' } + }, + { + handlerFile: 'com.serverless', + funcName: 'dockerfunc', + func: testFunctionsConfig.dockerfunc, + entry: { + key: 'com.serverless', + value: './com.serverless.js' + } } ]); return null; @@ -783,11 +807,12 @@ describe('validate', () => { module.serverless.service.functions = testFunctionsConfig; globSyncStub.callsFake(filename => [_.replace(filename, '*', 'js')]); return expect(module.validate()).to.be.fulfilled.then(() => { - expect(module.webpackConfig).to.have.lengthOf(4); + expect(module.webpackConfig).to.have.lengthOf(5); expect(module.webpackConfig[0].output.path).to.equal(path.join('output', 'func1')); expect(module.webpackConfig[1].output.path).to.equal(path.join('output', 'func2')); expect(module.webpackConfig[2].output.path).to.equal(path.join('output', 'func3')); expect(module.webpackConfig[3].output.path).to.equal(path.join('output', 'func4')); + expect(module.webpackConfig[4].output.path).to.equal(path.join('output', 'dockerfunc')); return null; }); @@ -808,19 +833,22 @@ describe('validate', () => { module.serverless.service.functions = testFunctionsConfig; globSyncStub.callsFake(filename => [_.replace(filename, '*', 'js')]); return expect(module.validate()).to.be.fulfilled.then(() => { - expect(module.webpackConfig).to.have.lengthOf(4); + expect(module.webpackConfig).to.have.lengthOf(5); expect(module.webpackConfig[0].devtool).to.equal('source-map'); expect(module.webpackConfig[1].devtool).to.equal('source-map'); expect(module.webpackConfig[2].devtool).to.equal('source-map'); expect(module.webpackConfig[3].devtool).to.equal('source-map'); + expect(module.webpackConfig[4].devtool).to.equal('source-map'); expect(module.webpackConfig[0].context).to.equal('some context'); expect(module.webpackConfig[1].context).to.equal('some context'); expect(module.webpackConfig[2].context).to.equal('some context'); expect(module.webpackConfig[3].context).to.equal('some context'); + expect(module.webpackConfig[4].context).to.equal('some context'); expect(module.webpackConfig[0].output.libraryTarget).to.equal('commonjs'); expect(module.webpackConfig[1].output.libraryTarget).to.equal('commonjs'); expect(module.webpackConfig[2].output.libraryTarget).to.equal('commonjs'); expect(module.webpackConfig[3].output.libraryTarget).to.equal('commonjs'); + expect(module.webpackConfig[4].output.libraryTarget).to.equal('commonjs'); return null; }); });