Repository: serverless/serverless (JavaScript)
Date: 2018-04-07 13:27:14+00:00
Task type: Bug Fix

Issue:
Different validation behavior between `serverless deploy` & `serverless deploy function`
# This is a `Bug Report`

## Description

* What went wrong?
I'm facing the validation issue when running `serverless deploy function --function facebook-events`
**BUT** everything is working as expected when I'm running `serverless deploy`

* What did you expect should have happened?
The same validation rules should be applied.

* What was the config you used?

```yml
...
functions:
  facebook-albums:
    handler: facebook-albums/index.handler
    package:
      include: ['facebook-albums/**']
    environment:
      API_TOKEN: ${ssm:facebookApiToken}

  facebook-events:
    handler: facebook-events/index.handler
    package:
      include: ['facebook-events/**']
    environment:
      COUNT: 6 # <== ISSUE 
      API_TOKEN: ${ssm:facebookApiToken}
...
```

* What stacktrace or error message from your provider did you see?

`Expected params.Environment.Variables['COUNT'] to be a string`

## Additional Data

* ***Serverless Framework Version you're using***: 1.26.1
* ***Operating System***: macOS 10.13.4
* ***Node.js Version***: v6.11.4



Issue discussion:
@ddimitrioglo Thanks for opening the issue.

This shows that the validation in `serverless deploy` is broken. Per AWS definition the environment variables have to contain strings only (or CF refernces in the CF template which resolve to strings on CF deployment time). So your validation error here is correct, but in `serverless deploy` it is wrong.

Solution patch:
diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md
index 08de22e0e44..691b88e6f03 100644
--- a/docs/providers/aws/guide/functions.md
+++ b/docs/providers/aws/guide/functions.md
@@ -254,7 +254,7 @@ In order for other services such as Kinesis streams to be made available, a NAT
 
 ## Environment Variables
 
-You can add environment variable configuration to a specific function in `serverless.yml` by adding an `environment` object property in the function configuration. This object should contain a key/value collection of strings:
+You can add environment variable configuration to a specific function in `serverless.yml` by adding an `environment` object property in the function configuration. This object should contain a key-value pairs of string to string:
 
 ```yml
 # serverless.yml
diff --git a/lib/plugins/aws/deployFunction/index.js b/lib/plugins/aws/deployFunction/index.js
index 3b590b866b6..2106b2b9e6a 100644
--- a/lib/plugins/aws/deployFunction/index.js
+++ b/lib/plugins/aws/deployFunction/index.js
@@ -164,6 +164,10 @@ class AwsDeployFunction {
             const errorMessage = 'Invalid characters in environment variable';
             throw new this.serverless.classes.Error(errorMessage);
           }
+          if (!_.isString(params.Environment.Variables[key])) {
+            const errorMessage = `Environment variable ${key} must contain strings`;
+            throw new this.serverless.classes.Error(errorMessage);
+          }
         });
       }
     }
diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js
index d880d0f1115..561db40f7c8 100644
--- a/lib/plugins/aws/package/compile/functions/index.js
+++ b/lib/plugins/aws/package/compile/functions/index.js
@@ -249,6 +249,13 @@ class AwsCompileFunctions {
             invalidEnvVar = `Invalid characters in environment variable ${key}`;
             return false;   // break loop with lodash
           }
+          const value = newFunction.Properties.Environment.Variables[key];
+          const isCFRef = _.isObject(value) &&
+            !_.some(value, (v, k) => k !== 'Ref' && !_.startsWith(k, 'Fn::'));
+          if (!isCFRef && !_.isString(value)) {
+            invalidEnvVar = `Environment variable ${key} must contain string`;
+            return false;
+          }
         }
       );
 


Test patch:
diff --git a/lib/plugins/aws/deployFunction/index.test.js b/lib/plugins/aws/deployFunction/index.test.js
index 3a2bf18583e..526607c92d1 100644
--- a/lib/plugins/aws/deployFunction/index.test.js
+++ b/lib/plugins/aws/deployFunction/index.test.js
@@ -353,6 +353,20 @@ describe('AwsDeployFunction', () => {
       expect(() => awsDeployFunction.updateFunctionConfiguration()).to.throw(Error);
     });
 
+    it('should fail when using non-string values as environment variables', () => {
+      options.functionObj = {
+        name: 'first',
+        description: 'change',
+        environment: {
+          COUNTER: 6,
+        },
+      };
+
+      awsDeployFunction.options = options;
+
+      expect(() => awsDeployFunction.updateFunctionConfiguration()).to.throw(Error);
+    });
+
     it('should inherit provider-level config', () => {
       options.functionObj = {
         name: 'first',
diff --git a/lib/plugins/aws/package/compile/functions/index.test.js b/lib/plugins/aws/package/compile/functions/index.test.js
index d170bdeb217..243aa854db3 100644
--- a/lib/plugins/aws/package/compile/functions/index.test.js
+++ b/lib/plugins/aws/package/compile/functions/index.test.js
@@ -1353,6 +1353,53 @@ describe('AwsCompileFunctions', () => {
       return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error);
     });
 
+    it('should throw an error if environment variable is not a string', () => {
+      awsCompileFunctions.serverless.service.functions = {
+        func: {
+          handler: 'func.function.handler',
+          name: 'new-service-dev-func',
+          environment: {
+            counter: 18,
+          },
+        },
+      };
+
+      return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error);
+    });
+
+    it('should accept an environment variable with CF ref and functions', () => {
+      awsCompileFunctions.serverless.service.functions = {
+        func: {
+          handler: 'func.function.handler',
+          name: 'new-service-dev-func',
+          environment: {
+            counter: {
+              Ref: 'TestVariable',
+            },
+            list: {
+              'Fn::Join:': [', ', ['a', 'b', 'c']],
+            },
+          },
+        },
+      };
+
+      return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled
+        .then(() => {
+          awsCompileFunctions.serverless.service.functions = {
+            func: {
+              handler: 'func.function.handler',
+              name: 'new-service-dev-func',
+              environment: {
+                counter: {
+                  NotRef: 'TestVariable',
+                },
+              },
+            },
+          };
+          return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error);
+        });
+    });
+
     it('should consider function based config when creating a function resource', () => {
       const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
       const s3FileName = awsCompileFunctions.serverless.service.package.artifact
diff --git a/lib/plugins/plugin/install/install.test.js b/lib/plugins/plugin/install/install.test.js
index 396dd603a16..ee0602db4de 100644
--- a/lib/plugins/plugin/install/install.test.js
+++ b/lib/plugins/plugin/install/install.test.js
@@ -253,14 +253,16 @@ describe('PluginInstall', () => {
       serverless.utils
         .writeFileSync(packageJsonFilePath, packageJson);
 
-      return expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => {
-        expect(consoleLogStub.called).to.equal(true);
-        expect(npmInstallStub.calledWithExactly(
-          'npm install --save-dev serverless-plugin-1@latest',
-          { stdio: 'ignore' }
-        )).to.equal(true);
-        expect(serverlessErrorStub.calledOnce).to.equal(false);
-      });
+      return expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() =>
+        Promise.all([
+          expect(consoleLogStub.called).to.equal(true),
+          expect(npmInstallStub.calledWithExactly(
+            'npm install --save-dev serverless-plugin-1@latest',
+            { stdio: 'ignore' }
+          )).to.equal(true),
+          expect(serverlessErrorStub.calledOnce).to.equal(false),
+        ])
+      );
     });
 
     it('should generate a package.json file in the service directory if not present',