Repository: serverless/serverless (JavaScript)
Date: 2019-01-13 13:28:48+00:00
Task type: Bug Fix

Issue:
AWS Cross Region Request incorrectly cached during variables
# This is a Bug Report

## Description

* What went wrong?
Cross Region requests for cloudformation output values are not retrieved when stackNames are identical.  Both variables are ending up with the same values. 
* What did you expect should have happened?
Retrieved output values would correctly return each regions values. 
* What was the config you used?
 ${cf.us-east-1:netsystem.VPCID} (Retrieves VPCID in us-east-1)
 ${cf.us-east-2:netsystem.VPCID} (Retrieves VPCID in us-east 2)


Similar or dependent issues:
https://github.com/serverless/serverless/issues/5606#issue-391682210
However, this deals with credential clashing. Data retrieved is a slightly separate issue. 

## Additional Data

I believe this is an error because 
https://github.com/serverless/serverless/blob/3bfcd0ae206434879720c603a2b5d0066ad50993/lib/classes/Variables.js#L656-L664
sets useCache to true. But the region is not considered during cache key generation. 

https://github.com/serverless/serverless/blob/c0fcdbb32ccf6ff8601582e159d33c252ea5a75a/lib/plugins/aws/provider/awsProvider.js#L195-L201
And 
https://github.com/serverless/serverless/blob/c0fcdbb32ccf6ff8601582e159d33c252ea5a75a/lib/plugins/aws/provider/awsProvider.js#L237

I'd think that changing the paramsHash to include the options (and thus the region) would correct this issue. This would in theory generate similar cache key collisions if other request are made cross regions with identical parameters.  I believe that ${cf.region} is currently the only cross region code (and thus impact by this issues but have not fully reviewed/tested}

* ***Serverless Framework Version you're using***: 1.36.0
* ***Operating System***: Amzn Linux 2
* ***Stack Trace***: None
* ***Provider Error messages***: None



Issue discussion:

Solution patch:
diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js
index cb68ddcd898..099007f004b 100644
--- a/lib/plugins/aws/provider/awsProvider.js
+++ b/lib/plugins/aws/provider/awsProvider.js
@@ -223,6 +223,7 @@ class AwsProvider {
    * @param {Object} params - Parameters
    * @param {Object} [options] - Options to modify the request behavior
    * @prop [options.useCache] - Utilize cache to retrieve results
+   * @prop [options.region] - Specify when to request to different region
    */
   request(service, method, params, options) {
     const that = this;
@@ -230,7 +231,10 @@ class AwsProvider {
     // Make sure options is an object (honors wrong calls of request)
     const requestOptions = _.isObject(options) ? options : {};
     const shouldCache = _.get(requestOptions, 'useCache', false);
-    const paramsHash = objectHash.sha1(params);
+    const paramsWithRegion = _.merge({}, params, {
+      region: _.get(options, 'region'),
+    });
+    const paramsHash = objectHash.sha1(paramsWithRegion);
     const MAX_TRIES = 4;
     const persistentRequest = (f) => new BbPromise((resolve, reject) => {
       const doCall = (numTry) => {


Test patch:
diff --git a/lib/plugins/aws/provider/awsProvider.test.js b/lib/plugins/aws/provider/awsProvider.test.js
index d3ce0ee9227..b39f46119fd 100644
--- a/lib/plugins/aws/provider/awsProvider.test.js
+++ b/lib/plugins/aws/provider/awsProvider.test.js
@@ -565,6 +565,50 @@ describe('AwsProvider', () => {
           });
       });
 
+      it('should request if same service, method and params but different region in option', () => {
+        const expectedResult = { called: true };
+        const sendStub = sinon.stub().yields(null, { called: true });
+        const requestSpy = sinon.spy(awsProvider, 'request');
+        class FakeCF {
+          constructor(credentials) {
+            this.credentials = credentials;
+          }
+
+          describeStacks() {
+            return {
+              send: sendStub,
+            };
+          }
+        }
+        awsProvider.sdk = {
+          CloudFormation: FakeCF,
+        };
+        const executeRequestWithRegion = (region) => awsProvider.request(
+          'CloudFormation',
+          'describeStacks',
+          { StackName: 'same-stack' },
+          {
+            useCache: true,
+            region,
+          }
+        );
+        const requests = [];
+        requests.push(BbPromise.try(() => executeRequestWithRegion('us-east-1')));
+        requests.push(BbPromise.try(() => executeRequestWithRegion('ap-northeast-1')));
+
+        return BbPromise.all(requests)
+          .then(results => {
+            expect(_.size(results, 2));
+            _.forEach(results, result => {
+              expect(result).to.deep.equal(expectedResult);
+            });
+            return expect(sendStub.callCount).to.equal(2);
+          })
+          .finally(() => {
+            requestSpy.restore();
+          });
+      });
+
       it('should resolve to the same response with mutiple parallel requests', () => {
         const expectedResult = { called: true };
         const sendStub = sinon.stub().yields(null, { called: true });