Repository: serverless/serverless (JavaScript)
Date: 2019-08-30 10:52:36+00:00
Task type: Bug Fix

Issue:
Serverless complains about "Rest API id could not be resolved."
# This is a Bug Report

## Description

- What went wrong?
After updating serverless to version 1.51.0 the deployment process never succeeds. Deploy command does the job partially. I can see in the logs that Stack update has finished and it prints out the endpoints and the functions, but then immediately fires an error that looks like this:

```txt
Serverless Error ---------------------------------------
 
  Rest API id could not be resolved.
  This might be caused by a custom API Gateway configuration.
  
  In given setup stage specific options such as `tracing`, `logs` and `tags` are not supported.
  
  Please update your configuration (or open up an issue if you feel that there's a way to support your setup).
 
  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com
 
  Your Environment Information ---------------------------
     Operating System:          darwin
     Node Version:              10.16.0
     Framework Version:         1.51.0
     Plugin Version:            1.3.9
     SDK Version:               2.1.0
```

- What did you expect should have happened?
The deployment should pass without any issues like it was with the previous version (1.50.1)

- What was the config you used?
     Operating System:          darwin
     Node Version:              10.16.0
     Framework Version:         1.51.0
     Plugin Version:            1.3.9
     SDK Version:               2.1.0

- What stacktrace or error message from your provider did you see?
```txt
Serverless Error ---------------------------------------
 
  ServerlessError: Rest API id could not be resolved.
  This might be caused by a custom API Gateway configuration.
  
  In given setup stage specific options such as `tracing`, `logs` and `tags` are not supported.
  
  Please update your configuration (or open up an issue if you feel that there's a way to support your setup).
      at resolveAccountId.call.then.then (/Users/g/Sites/app/node_modules/serverless/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js:66:17)
  From previous event:
      at AwsCompileApigEvents.updateStage (/Users/g/Sites/app/node_modules/serverless/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js:44:8)
      at Object.after:deploy:deploy [as hook] (/Users/g/Sites/app/node_modules/serverless/lib/plugins/aws/package/compile/events/apiGateway/index.js:85:28)
      at BbPromise.reduce (/Users/g/Sites/app/node_modules/serverless/lib/classes/PluginManager.js:464:55)
  From previous event:
      at PluginManager.invoke (/Users/g/Sites/app/node_modules/serverless/lib/classes/PluginManager.js:464:22)
      at PluginManager.run (/Users/g/Sites/app/node_modules/serverless/lib/classes/PluginManager.js:496:17)
      at variables.populateService.then (/Users/g/Sites/app/node_modules/serverless/lib/Serverless.js:116:33)
      at runCallback (timers.js:705:18)
      at tryOnImmediate (timers.js:676:5)
      at processImmediate (timers.js:658:5)
      at process.topLevelDomainCallback (domain.js:126:23)
  From previous event:
      at Serverless.run (/Users/g/Sites/app/node_modules/serverless/lib/Serverless.js:103:74)
      at serverless.init.then (/Users/g/Sites/app/node_modules/serverless/bin/serverless.js:60:28)
      at /Users/g/Sites/app/node_modules/graceful-fs/graceful-fs.js:136:16
      at /Users/g/Sites/app/node_modules/graceful-fs/graceful-fs.js:57:14
      at FSReqWrap.args [as oncomplete] (fs.js:140:20)
  From previous event:
      at initializeErrorReporter.then (/Users/g/Sites/app/node_modules/serverless/bin/serverless.js:60:6)
      at runCallback (timers.js:705:18)
      at tryOnImmediate (timers.js:676:5)
      at processImmediate (timers.js:658:5)
      at process.topLevelDomainCallback (domain.js:126:23)
  From previous event:
      at Object.<anonymous> (/Users/g/Sites/app/node_modules/serverless/bin/serverless.js:46:39)
      at Module._compile (internal/modules/cjs/loader.js:776:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
      at Module.load (internal/modules/cjs/loader.js:653:32)
      at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
      at Function.Module._load (internal/modules/cjs/loader.js:585:3)
      at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
      at findNodeScript.then.existing (/Users/g/.nodenv/versions/10.16.0/lib/node_modules/npm/node_modules/libnpx/index.js:268:14)
```


Issue discussion:
@gevorggalstyan can you share your `serverless.yml` (at least part related to API Gateway configuration) ?
Sorry, accidentally  closed
@medikoo I think I found the exact lines that cause this

```yaml
provider:
  logs:
    restApi: true
```
 We had these lines based on https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/
What do you have at `provider` and `apiGateway` config? Is there a chance you can share all config, with sensitive parts masked out?
Does this help?
```yaml
frameworkVersion: ">=1.1.0 <2.0.0"
provider:
  name: aws
  runtime: nodejs10.x
  region: >-
    ${opt:region, self:custom.secrets.DEFAULT_REGION,
    self:provider.environment.DEFAULT_REGION}
  stage: "${opt:stage, self:custom.defaults.stage}"
  profile: "${self:custom.profiles.${self:provider.stage}}"
  timeout: 30 # The default is 6 seconds. Note: API Gateway current maximum is 30 seconds
  memorySize: 1024
  apiGateway:
    minimumCompressionSize: 1024
  tracing:
    apiGateway: true
    lambda: true
  logs:
    restApi: true

cors: &cors
  origin: "*"
  headers:
    - Content-Type
    - X-Amz-Date
    - Authorization
    - X-Api-Key
    - X-Amz-Security-Token
    - X-Amz-User-Agent
    - X-Cache-Control
  allowCredentials: false
  maxAge: 86400
  # Caches on browser and proxy for 1 hour and doesn't allow proxy to serve out of date content
  cacheControl: "max-age=3600, s-maxage=3600, proxy-revalidate"

functions:
  koa:
    handler: sls/koa-sls-wrapper.ts
    memorySize: 2048
    events:
      # These would allow any request to be handled by koa
      # which we do not want to allow but keeping these here as a reference
      # - http: any /
      # - http: "any {proxy+}"

      # region Public API endpoints
      - http:
          cors: *cors
          method: any
          path: "migrations/{id}"
      # endregion
```
@gevorggalstyan unfortunately with above example I wasn't able to reproduce the error. Here's a sample project I've prepared, and it deploys without issues: https://gist.github.com/medikoo/9a7091308770885b2314fc0c197cd9d9

This error appears when you rely on options which are not perfectly supported in CloudFormation, therefore we apply them via SDK calls (which happens after deployment of CloudFormation stack).

And for that calls we need to be able to resolve Rest API id programmatically. This resolution takes place here: https://github.com/serverless/serverless/blob/91ae8bcc17d3ab7874507c8192375b4a28627592/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js#L88-L96

Are you maybe configuring an additional custom `AWS::ApiGateway::Resource` in `resources.Resources` section of `serverless.yml`?

@medikoo Our application has been deployed with previous versions successfully and even with version 1.50.0. The issue has only started with version 1.51.0. I can see in that version commits that there have been several changes related to logs at least. There is nothing else left in the configs, just a long list of -http events on the same function.

Can you please try deploying with version 1.50.0 and then redeploying with 1.51.0?

I am going to try to do the same thing with your gist code.
Update: I have tried deploying the stack in your gist with 1.50.0 and then with 1.51.0 and did not hit the issue.

Going to redeploy the app from ground-up, to check if it works that way.
@medikoo I found the exact commit on the master that is causing the issues for us:

https://github.com/serverless/serverless/commit/dda0c71048f90efe8d1c5b2c774dbbf6023b86b4

I think the problem is in the assumption that AWS::ApiGateway::Resource will/must always be in the main stack. In fact, it can be in pretty much any related nested stack. After the changes, the code tries to find it in the main stack's resources and fails.

Before this change, the code that was initializing the `apiName` was pretty much making a correct assumption.

```js
const apiName = provider.apiName || `${this.options.stage}-${this.state.service.service}`;
```

This is almost always correct if you do not define a custom rest API id.

After the change, this assumption has been removed entirely and the code looks like this

```js
if (!apiGatewayResource) {
      resolve(null);
      return;
}

const apiName = apiGatewayResource.Properties.Name;
```

So basically if we use any form of resource splitting and the API gateway resource happens to be in any stack other than the main one, the code will exit.

Do you think we could change the code to look something like this?

```js
    // Keeping the code for reference, it should be deleted
    // if (!apiGatewayResource) {
    //   resolve(null);
    //   return;
    // }
    const apiName = apiGatewayResource ? apiGatewayResource.Properties.Name : provider.apiName || `${this.options.stage}-${this.state.service.service}`;
```

I think this will be a good compromise and give more flexibility to the users of the framework.
> I think the problem is in the assumption that AWS::ApiGateway::Resource will/must always be in the main stack. In fact, it can be in pretty much any related nested stack.

Indeed I didn't took into account that users may rely on nested stacks. Good find!

> Do you think we could change the code to look something like this?

Yes, definitely we can do that. Do you want to open a PR?




Solution patch:
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js
index e8ebe3cbaf3..7c50415c073 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.js
@@ -106,19 +106,17 @@ function resolveRestApiId() {
     const apiGatewayResource = resolveApiGatewayResource(
       this.serverless.service.provider.compiledCloudFormationTemplate.Resources
     );
-    if (!apiGatewayResource) {
-      resolve(null);
-      return;
-    }
-    const apiName = apiGatewayResource.Properties.Name;
-    const resolvefromAws = position =>
+    const apiName = apiGatewayResource
+      ? apiGatewayResource.Properties.Name
+      : provider.apiName || `${this.options.stage}-${this.state.service.service}`;
+    const resolveFromAws = position =>
       this.provider.request('APIGateway', 'getRestApis', { position, limit: 500 }).then(result => {
         const restApi = result.items.find(api => api.name === apiName);
         if (restApi) return restApi.id;
-        if (result.position) return resolvefromAws(result.position);
+        if (result.position) return resolveFromAws(result.position);
         return null;
       });
-    resolve(resolvefromAws());
+    resolve(resolveFromAws());
   }).then(restApiId => {
     this.apiGatewayRestApiId = restApiId;
   });


Test patch:
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js
index 79a70911198..3b16c4e9520 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js
@@ -59,11 +59,14 @@ describe('#updateStage()', () => {
         position: undefined,
       })
       .resolves({
-        items: [{ name: 'dev-my-service', id: 'someRestApiId' }],
+        items: [
+          { name: 'dev-my-service', id: 'devRestApiId' },
+          { name: 'prod-my-service', id: 'prodRestApiId' },
+        ],
       });
     providerRequestStub
       .withArgs('APIGateway', 'getStage', {
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       })
       .resolves({
@@ -71,12 +74,27 @@ describe('#updateStage()', () => {
           old: 'tag',
         },
       });
+    providerRequestStub
+      .withArgs('APIGateway', 'getStage', {
+        restApiId: 'prodRestApiId',
+        stageName: 'prod',
+      })
+      .resolves({
+        tags: {
+          old: 'tag',
+        },
+      });
 
     providerRequestStub
       .withArgs('CloudWatchLogs', 'deleteLogGroup', {
         logGroupName: '/aws/api-gateway/my-service-dev',
       })
       .resolves();
+    providerRequestStub
+      .withArgs('CloudWatchLogs', 'deleteLogGroup', {
+        logGroupName: '/aws/api-gateway/my-service-prod',
+      })
+      .resolves();
   });
 
   afterEach(() => {
@@ -129,20 +147,20 @@ describe('#updateStage()', () => {
       expect(providerRequestStub.args[1][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[1][1]).to.equal('getStage');
       expect(providerRequestStub.args[1][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       });
       expect(providerRequestStub.args[2][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[2][1]).to.equal('updateStage');
       expect(providerRequestStub.args[2][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
         patchOperations,
       });
       expect(providerRequestStub.args[3][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[3][1]).to.equal('tagResource');
       expect(providerRequestStub.args[3][2]).to.deep.equal({
-        resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/someRestApiId/stages/dev',
+        resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/devRestApiId/stages/dev',
         tags: {
           'Containing Space': 'bar',
           'bar': 'high-priority',
@@ -152,7 +170,7 @@ describe('#updateStage()', () => {
       expect(providerRequestStub.args[4][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[4][1]).to.equal('untagResource');
       expect(providerRequestStub.args[4][2]).to.deep.equal({
-        resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/someRestApiId/stages/dev',
+        resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/devRestApiId/stages/dev',
         tagKeys: ['old'],
       });
     });
@@ -180,13 +198,13 @@ describe('#updateStage()', () => {
       expect(providerRequestStub.args[1][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[1][1]).to.equal('getStage');
       expect(providerRequestStub.args[1][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       });
       expect(providerRequestStub.args[2][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[2][1]).to.equal('updateStage');
       expect(providerRequestStub.args[2][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
         patchOperations,
       });
@@ -201,14 +219,14 @@ describe('#updateStage()', () => {
   it('should create a new stage and proceed as usual if none can be found', () => {
     providerRequestStub
       .withArgs('APIGateway', 'getStage', {
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       })
       .rejects();
 
     providerRequestStub
       .withArgs('APIGateway', 'getDeployments', {
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         limit: 500,
       })
       .resolves({
@@ -218,7 +236,7 @@ describe('#updateStage()', () => {
     providerRequestStub
       .withArgs('APIGateway', 'createStage', {
         deploymentId: 'someDeploymentId',
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       })
       .resolves();
@@ -241,26 +259,26 @@ describe('#updateStage()', () => {
       expect(providerRequestStub.args[1][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[1][1]).to.equal('getStage');
       expect(providerRequestStub.args[1][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       });
       expect(providerRequestStub.args[2][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[2][1]).to.equal('getDeployments');
       expect(providerRequestStub.args[2][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         limit: 500,
       });
       expect(providerRequestStub.args[3][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[3][1]).to.equal('createStage');
       expect(providerRequestStub.args[3][2]).to.deep.equal({
         deploymentId: 'someDeploymentId',
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       });
       expect(providerRequestStub.args[4][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[4][1]).to.equal('updateStage');
       expect(providerRequestStub.args[4][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
         patchOperations,
       });
@@ -321,7 +339,16 @@ describe('#updateStage()', () => {
     resources.CustomApiGatewayRestApi = resources.ApiGatewayRestApi;
     delete resources.ApiGatewayRestApi;
     return updateStage.call(context).then(() => {
-      expect(context.apiGatewayRestApiId).to.equal('someRestApiId');
+      expect(context.apiGatewayRestApiId).to.equal('devRestApiId');
+    });
+  });
+
+  it('should resolve with a default api name if the AWS::ApiGateway::Resource is not present', () => {
+    const resources = context.serverless.service.provider.compiledCloudFormationTemplate.Resources;
+    delete resources.ApiGatewayRestApi;
+    options.stage = 'prod';
+    return updateStage.call(context).then(() => {
+      expect(context.apiGatewayRestApiId).to.equal('prodRestApiId');
     });
   });
 
@@ -341,11 +368,11 @@ describe('#updateStage()', () => {
         position: 'foobarfoo1',
       })
       .resolves({
-        items: [{ name: 'dev-my-service', id: 'someRestApiId' }],
+        items: [{ name: 'dev-my-service', id: 'devRestApiId' }],
       });
 
     return updateStage.call(context).then(() => {
-      expect(context.apiGatewayRestApiId).to.equal('someRestApiId');
+      expect(context.apiGatewayRestApiId).to.equal('devRestApiId');
     });
   });
 
@@ -397,20 +424,20 @@ describe('#updateStage()', () => {
       expect(providerRequestStub.args[1][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[1][1]).to.equal('getStage');
       expect(providerRequestStub.args[1][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
       });
       expect(providerRequestStub.args[2][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[2][1]).to.equal('updateStage');
       expect(providerRequestStub.args[2][2]).to.deep.equal({
-        restApiId: 'someRestApiId',
+        restApiId: 'devRestApiId',
         stageName: 'dev',
         patchOperations,
       });
       expect(providerRequestStub.args[3][0]).to.equal('APIGateway');
       expect(providerRequestStub.args[3][1]).to.equal('untagResource');
       expect(providerRequestStub.args[3][2]).to.deep.equal({
-        resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/someRestApiId/stages/dev',
+        resourceArn: 'arn:aws:apigateway:us-east-1::/restapis/devRestApiId/stages/dev',
         tagKeys: ['old'],
       });
     });