Repository: serverless/serverless (JavaScript)
Date: 2019-02-07 22:58:45+00:00
Task type: Bug Fix

Issue:
Default values don't work in variables within ${file}
<!--
1. If you have a question and not a bug report please ask first at http://forum.serverless.com
2. Please check if an issue already exists. This bug may have already been documented
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Bug Report

## Description

After updating to serverless 1.37 our serverless.yml (almost) silently stopped setting environment to lambdas. The issue ended up being related to default values of variables when including external files.

Here is an example config:

```
service: my-service

custom:
  config: ${file(config/${opt:stage, 'development'}.yml)}

provider:
  name: aws
  stage: ${self:custom.stage}
  environment: ${self:custom.config.environment}
```

The output of `sls print -s production` is:

```
service: my-service
custom:
  config:
    environment:
      IS_PRODUCTION: 'True'
provider:
  stage: production
  name: aws
```

Additionally, serverless shows a warning: `A valid service attribute to satisfy the declaration 'self:custom.config.environment' could not be found.`

If I remove the default value for `opt:stage` and use the following config, the problem disappears:

```
sls print -s production

service: my-service
custom:
  config:
    environment:
      IS_PRODUCTION: 'True'
provider:
  stage: production
  name: aws
  environment: <<< ENVIRONMENT IS PRESENT NOW
     $ref: '$["custom"]["config"]["environment"]'
```

The same config (with the default value) works just fine with `serverless@1.36.3`.


Issue discussion:

Solution patch:
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 6d81ab4bfdc..ef1b4f829b6 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -785,7 +785,7 @@ class Variables {
       index = this.deep.push(variable) - 1;
     }
     const variableContainer = variable.match(this.variableSyntax)[0];
-    const variableString = this.cleanVariable(variableContainer);
+    const variableString = this.cleanVariable(variableContainer).replace(/\s/g, '');
     return variableContainer
       .replace(/\s/g, '')
       .replace(variableString, `deep:${index}`);


Test patch:
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index e6e3dfdb1d4..f49356cc1b5 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -945,6 +945,23 @@ module.exports = {
           return serverless.variables.populateObject(service.custom)
             .should.become(expected);
         });
+        it('should still work with a default file name in double or single quotes', () => {
+          makeTempFile(asyncFileName, asyncContent);
+          service.custom = {
+            val1: '${self:custom.val0}', // eslint-disable-line no-template-curly-in-string
+            val2: '${self:custom.val1}', // eslint-disable-line no-template-curly-in-string
+            val3: `\${file(\${self:custom.nonexistent, "${asyncFileName}"}):str}`,
+            val0: `\${file(\${self:custom.nonexistent, '${asyncFileName}'}):str}`,
+          };
+          const expected = {
+            val1: 'my-async-value-1',
+            val2: 'my-async-value-1',
+            val3: 'my-async-value-1',
+            val0: 'my-async-value-1',
+          };
+          return serverless.variables.populateObject(service.custom)
+            .should.become(expected);
+        });
         it('should populate any given variable only once regardless of ordering or reference count',
           () => {
             makeTempFile(asyncFileName, asyncContent);
@@ -997,8 +1014,8 @@ module.exports = {
           () => {
             const fileName = `./node_modules/@scoped-org/${asyncFileName}`;
             makeTempFile(
-                fileName,
-                asyncContent
+              fileName,
+              asyncContent
             );
             service.custom = {
               val0: `\${file(${fileName}):str}`,
@@ -1007,8 +1024,8 @@ module.exports = {
               val0: 'my-async-value-1',
             };
             return serverless.variables
-                .populateObject(service.custom)
-                .should.become(expected);
+              .populateObject(service.custom)
+              .should.become(expected);
           });
         const selfFileName = 'self.yml';
         const selfContent = `foo: baz