Repository: serverless-heaven/serverless-webpack
Pull request #723 (2021-03-12 16:06:55)

Add Serverless Container Support
<!--
1. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the PR
2. Please follow the template, otherwise we'll have to ask you to update it and it will take longer until your PR is merged
-->

## What did you implement:

Closes https://github.com/serverless-heaven/serverless-webpack/issues/698

<!--
Briefly describe the feature if no issue exists for this PR. If possible only
submit PRs for existing issues. If the PR is trivial (like doc changes or simple
code fixes) it can be submitted without a related issue, but as soon as it adds
or changes functionality, a related issue should be present.
-->

## How did you implement it:

<!--
If this is a nontrivial change please briefly describe your implementation so its easy for us to understand and review your code.
-->

I added another function to `validate.js` to get the actual handler name from the function definition, now that `serverless` rolled out `image` for custom runtimes. 

Review the following documentation: serverless documentation: https://www.serverless.com/blog/container-support-for-lambda. The only gap between feature support defined in the above article and this commit, is the ability to inherit the docker image's default `command` property. For example, `serverless-webpack` can't read the `CMD` property for the following Dockerfile:
```bash
# Dockerfile
FROM public.ecr.aws/lambda/nodejs:12
CMD "index.handler"
```
However you can simply set the `function.image.command[0]` property in `serverless.yml` as an override. I assume this would be the desired approach for most use cases, when sharing a docker image for all functions within the service. 
```YAML
# serverless.yml
service:
  ...
provider:
  ...
functions:
  myFunction1:
    image:
      name: public.ecr.aws/lambda/nodejs:12
      command:
        - 'index.handler1'
  myFunction2:
    image:
      name: public.ecr.aws/lambda/nodejs:12
      command:
        - 'index.handler2'
```

## How can we verify it:
<!--
Add any applicable config, commands, screenshots or other resources
to make it easy for us to verify this works. The easier you make it for us
to review a PR, the faster we can review and merge it.

Examples:
* Step by step description, how to verify
* Screenshots - Showing the difference between your output and the master
* Other - Anything else that comes to mind to help us evaluate
-->
* I added to the existing unit tests to check for a function definition with no `handler` property, and has `image.command` instead. Run `npm run test` to quickly validate test coverage:

![Screen Shot 2021-03-12 at 10 31 13 AM](https://user-images.githubusercontent.com/14983357/110961702-146edf00-831e-11eb-9014-9b95381692d3.png)

* On my branch I went into `serverless-webpack` and did a `yalc publish` to locally mock the `npm` package.
* I created a new serverless docker project with 

<img width="654" alt="Screen Shot 2021-03-12 at 10 47 11 AM" src="https://user-images.githubusercontent.com/14983357/110963630-4f721200-8320-11eb-8bc7-f906b7354a13.png">

* I opened the new project in my editor; I then ran the following:

```bash
npm init
npm install
npm install webpack serverless-webpack bestzip --dev
yalc link serverless-webpack # do a local install of my serverless-webpack branch
``` 

* I added a `plugins` section with `serverless-webpack`:

![Screen Shot 2021-03-12 at 10 52 08 AM](https://user-images.githubusercontent.com/14983357/110964361-1d14e480-8321-11eb-98e5-afc93473fe05.png)


* I added a `command` array to the existing `image` property for the function called `hello`:

![Screen Shot 2021-03-12 at 10 53 42 AM](https://user-images.githubusercontent.com/14983357/110964468-387fef80-8321-11eb-823a-e0911ec1cec9.png)

Note `app.js` contains a function called `handler`.
* I added the following to a new file called `webpack.config.js`
```bash
const path = require('path');
const slsw = require('serverless-webpack');

module.exports = {
  entry: slsw.lib.entries,
  target: 'node',
  optimization: {
    minimize: false,
  },
  performance: {
    hints: false,
  },
  output: {
    libraryTarget: 'commonjs2',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
    sourceMapFilename: '[file].map',
  },
};
```
* I then ran `serverless package`

![Screen Shot 2021-03-12 at 11 03 08 AM](https://user-images.githubusercontent.com/14983357/110965788-89441800-8322-11eb-8e5e-89bef1e51bc7.png)

* I then ran `serverless deploy`

![Screen Shot 2021-03-12 at 11 05 54 AM](https://user-images.githubusercontent.com/14983357/110966169-0079ac00-8323-11eb-9e0d-302936e02a6b.png)

![Screen Shot 2021-03-12 at 11 06 00 AM](https://user-images.githubusercontent.com/14983357/110966200-053e6000-8323-11eb-90e6-4f5af0b8eef1.png)


## Todos:

- [x] Write tests
- [x] Write documentation
- [x] Fix linting errors
- [x] Make sure code coverage hasn't dropped
- [x] Provide verification config / commands / resources
- [x] Enable "Allow edits from maintainers" for this PR
- [x] Update the messages below

***Is this ready for review?:*** YES
***Is it a breaking change?:*** NO



Patch:
diff --git a/README.md b/README.md
index 9cc1bf2..b16302f 100644
--- a/README.md
+++ b/README.md
@@ -567,6 +567,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
diff --git a/lib/validate.js b/lib/validate.js
index cae04ed..588722b 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) {
@@ -213,7 +230,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,


Test patch:
diff --git a/tests/validate.test.js b/tests/validate.test.js
index cf49bf3..35ba2ec 100644
--- a/tests/validate.test.js
+++ b/tests/validate.test.js
@@ -504,6 +504,20 @@ describe('validate', () => {
               }
             }
           ]
+        },
+        dockerfunc: {
+          image: {
+            name: 'some-image-uri',
+            command: ['com.serverless.Handler']
+          },
+          events: [
+            {
+              http: {
+                method: 'POST',
+                path: 'mydockerfuncpath'
+              }
+            }
+          ]
         }
       };
 
@@ -541,6 +555,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',
@@ -548,7 +563,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;
         });
@@ -629,6 +644,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']
+            }
           }
         };
 
@@ -650,11 +683,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;
         });
@@ -777,6 +811,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;
@@ -788,11 +831,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;
           });
@@ -813,19 +857,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;
           });
         });