Repository: serverless/serverless (JavaScript)
Date: 2019-01-27 08:15:39+00:00
Task type: Bug Fix

Issue:
AWS: Default value is not supported in ${s3} 
# This is a Bug Report

## Description

* What went wrong
   * Serverless throws error instead of using fallback if `${s3}` variables syntax can not retrieve value from AWS. 

* What did you expect should have happened?
   * Fallback value should be used as same as other variables syntax do so.
   * Note: `${ssm}` syntax is already able to use default if value is not found in AWS.

* What was the config you used?

```yml
service: aws-nodejs

provider:
  name: aws
  runtime: nodejs8.10

functions:
  hello:
    handler: handler.hello

custom:
  envVar: ${env:hoge, 'fallback'}
  optVar: ${opt:hoge, 'fallback'}
  selfVar: ${self:custom.xxx, 'fallback'}
  fileVar: ${file(./not-found):nooo, 'fallback'}
  # Note: cf is already reported in issue #4940
  # cfVar: ${cf:no-such-stuck-exists.UNDEFINED_KEY, 'fallback'} 
  s3Var: ${s3:no/such/key, 'fallback'}
  ssmVar: ${ssm:/path/to/service/myParam, 'fallback'}
```

    
* What stacktrace or error message from your provider did you see?

```
$ sls print                                                     
  Serverless Error ---------------------------------------
 
  Error getting value for s3:no/such/key. The specified bucket is not valid.
 
  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com
 
  Your Environment Information -----------------------------
     OS:                     linux
     Node Version:           8.10.0
     Serverless Version:     1.36.3
```
Similar or dependent issues:
* #4940:  Default values for cf references 



Issue discussion:

Solution patch:
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index c65866f3bdc..0d34e0f8884 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -451,14 +451,19 @@ class Variables {
    *  in the given variable strings string.
    */
   overwrite(variableStrings, variableMatch, propertyString) {
+    // A sentinel to rid rejected Promises, so any of resolved value can be used as fallback.
+    const FAIL_TOKEN = {};
     const variableValues = variableStrings.map(variableString =>
-      this.getValueFromSource(variableString, propertyString));
+      this.getValueFromSource(variableString, propertyString)
+        .catch(unused => FAIL_TOKEN)); // eslint-disable-line no-unused-vars
+
     const validValue = value => (
       value !== null &&
       typeof value !== 'undefined' &&
       !(typeof value === 'object' && _.isEmpty(value))
     );
     return BbPromise.all(variableValues)
+      .then(values => values.filter(v => v !== FAIL_TOKEN))
       .then(values => {
         let deepPropertyString = variableMatch;
         let deepProperties = 0;


Test patch:
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index f8af35f59b2..46c3ffa3b01 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -124,6 +124,137 @@ describe('Variables', () => {
           });
       });
   });
+
+  describe('fallback', () => {
+    it('should fallback if ${self} syntax fail to populate but fallback is provided', () => {
+      serverless.variables.service.custom = {
+        settings: '${self:nonExistent, "fallback"}',
+      };
+      return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+        expect(result.custom).to.be.deep.eql({
+          settings: 'fallback',
+        });
+      });
+    });
+
+    it('should fallback if ${opt} syntax fail to populate but fallback is provided', () => {
+      serverless.variables.service.custom = {
+        settings: '${opt:nonExistent, "fallback"}',
+      };
+      return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+        expect(result.custom).to.be.deep.eql({
+          settings: 'fallback',
+        });
+      });
+    });
+
+    it('should fallback if ${env} syntax fail to populate but fallback is provided', () => {
+      serverless.variables.service.custom = {
+        settings: '${env:nonExistent, "fallback"}',
+      };
+      return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+        expect(result.custom).to.be.deep.eql({
+          settings: 'fallback',
+        });
+      });
+    });
+
+    describe('file syntax', () => {
+      it('should fallback if file does not exist but fallback is provided', () => {
+        serverless.variables.service.custom = {
+          settings: '${file(~/config.yml):xyz, "fallback"}',
+        };
+
+        const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(false);
+        const realpathSync = sinon.stub(fse, 'realpathSync').returns(`${os.homedir()}/config.yml`);
+
+        return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+          expect(result.custom).to.be.deep.eql({
+            settings: 'fallback',
+          });
+        }).finally(() => {
+          fileExistsStub.restore();
+          realpathSync.restore();
+        });
+      });
+
+      it('should fallback if file exists but given key not found and fallback is provided', () => {
+        serverless.variables.service.custom = {
+          settings: '${file(~/config.yml):xyz, "fallback"}',
+        };
+
+        const fileExistsStub = sinon.stub(serverless.utils, 'fileExistsSync').returns(true);
+        const realpathSync = sinon.stub(fse, 'realpathSync').returns(`${os.homedir()}/config.yml`);
+        const readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns({
+          test: 1,
+          test2: 'test2',
+        });
+
+        return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+          expect(result.custom).to.be.deep.eql({
+            settings: 'fallback',
+          });
+        }).finally(() => {
+          fileExistsStub.restore();
+          realpathSync.restore();
+          readFileSyncStub.restore();
+        });
+      });
+    });
+
+    describe('aws-specific syntax', () => {
+      let awsProvider;
+      let requestStub;
+      beforeEach(() => {
+        awsProvider = new AwsProvider(serverless, {});
+        requestStub = sinon.stub(awsProvider, 'request', () =>
+          BbPromise.reject(new serverless.classes.Error('Not found.', 400)));
+      });
+      afterEach(() => {
+        requestStub.restore();
+      });
+      it('should fallback if ${s3} syntax fail to populate but fallback is provided', () => {
+        serverless.variables.service.custom = {
+          settings: '${s3:bucket/key, "fallback"}',
+        };
+        return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+          expect(result.custom).to.be.deep.eql({
+            settings: 'fallback',
+          });
+        });
+      });
+
+      it('should fallback if ${cf} syntax fail to populate but fallback is provided', () => {
+        serverless.variables.service.custom = {
+          settings: '${cf:stack.value, "fallback"}',
+        };
+        return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+          expect(result.custom).to.be.deep.eql({
+            settings: 'fallback',
+          });
+        });
+      });
+
+      it('should fallback if ${ssm} syntax fail to populate but fallback is provided', () => {
+        serverless.variables.service.custom = {
+          settings: '${ssm:/path/param, "fallback"}',
+        };
+        return serverless.variables.populateService().should.be.fulfilled.then((result) => {
+          expect(result.custom).to.be.deep.eql({
+            settings: 'fallback',
+          });
+        });
+      });
+
+      it('should throw an error if fallback fails too', () => {
+        serverless.variables.service.custom = {
+          settings: '${s3:bucket/key, ${ssm:/path/param}}',
+        };
+        return serverless.variables.populateService().should.be.rejected;
+      });
+    });
+  });
+
   describe('#prepopulateService', () => {
     // TL;DR: call populateService to test prepopulateService (note addition of 'pre')
     //