Skip to content

Commit

Permalink
feat(AWS API Gateway): support provider.apiGateway.stage (#11487)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavanandhukuri committed Oct 31, 2022
1 parent 7a38483 commit 0a49c4f
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 5 deletions.
10 changes: 10 additions & 0 deletions docs/providers/aws/events/apigateway.md
Expand Up @@ -1871,3 +1871,13 @@ provider:
apiGateway:
disableDefaultEndpoint: true
```

## Providing a custom stage name

By default, the API Gateway stage will be same as the serverless stage. This can be overridden by passing stage under the apiGateway configuration under provider.

```yml
provider:
apiGateway:
stage: customStageName
```
Expand Up @@ -172,7 +172,7 @@ async function resolveStage() {
return this.provider
.request('APIGateway', 'getStage', {
restApiId,
stageName: this.options.stage,
stageName: this.provider.getApiGatewayStage(),
})
.then((res) => {
this.apiGatewayStageState = res;
Expand Down Expand Up @@ -215,7 +215,7 @@ async function ensureStage() {
return this.provider.request('APIGateway', 'createStage', {
deploymentId,
restApiId,
stageName: this.options.stage,
stageName: this.provider.getApiGatewayStage(),
});
}

Expand Down Expand Up @@ -257,7 +257,7 @@ function handleLogs() {

if (logs) {
const service = this.state.service.service;
const stage = this.options.stage;
const stage = this.provider.getApiGatewayStage();
const region = this.options.region;
const partition = this.partition;
const logGroupName = `/aws/api-gateway/${service}-${stage}`;
Expand Down Expand Up @@ -367,7 +367,7 @@ function applyUpdates() {
if (patchOperations.length) {
return this.provider.request('APIGateway', 'updateStage', {
restApiId,
stageName: this.options.stage,
stageName: this.provider.getApiGatewayStage(),
patchOperations,
});
}
Expand All @@ -378,7 +378,7 @@ function applyUpdates() {
async function removeAccessLoggingLogGroup() {
const service = this.state.service.service;
const provider = this.state.service.provider;
const stage = this.options.stage;
const stage = this.provider.getApiGatewayStage();
const logGroupName = `/aws/api-gateway/${service}-${stage}`;

let accessLogging = provider.logs && provider.logs.restApi;
Expand Down
6 changes: 6 additions & 0 deletions lib/plugins/aws/provider.js
Expand Up @@ -818,6 +818,7 @@ class AwsProvider {
additionalProperties: false,
},
shouldStartNameWithService: { type: 'boolean' },
stage: { type: 'string' },
usagePlan: {
anyOf: [
apiGatewayUsagePlan,
Expand Down Expand Up @@ -1893,6 +1894,11 @@ class AwsProvider {
return stageSourceValue.value || defaultStage;
}

getApiGatewayStage() {
const apiGatewayConfig = this.serverless.service.provider.apiGateway;
return (apiGatewayConfig && apiGatewayConfig.stage) || this.getStage();
}

/**
* Get API Gateway Rest API ID from serverless config
*/
Expand Down
Expand Up @@ -884,4 +884,170 @@ describe('test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/u
expect(getDeploymentsStub).to.have.been.calledOnce;
expect(getDeploymentsStub.args[0][0].restApiId).to.equal('api-id');
});

it('should use stage defined under apiGateway config if passed', async () => {
const { serviceConfig, servicePath, updateConfig } = await fixtures.setup('api-gateway');
const getDeploymentsStub = sinon.stub().returns({ items: [{ id: 'deployment-id' }] });
const getStageStub = sinon.stub().resolves({});
const createStageStub = sinon.stub();
const updateStageStub = sinon.stub();
const stage = 'dev';

await updateConfig({
provider: {
stage,
apiGateway: {
shouldStartNameWithService: true,
stage: 'customStage',
},
stackTags: { key: 'value' },
logs: { restApi: true },
},
});

await runServerless({
command: 'deploy',
cwd: servicePath,
lastLifecycleHookName: 'after:deploy:deploy',
awsRequestStubMap: {
APIGateway: {
createStage: createStageStub,
getStage: getStageStub,
updateStage: updateStageStub,
getDeployments: getDeploymentsStub,
getRestApis: { items: [{ id: 'api-id', name: `${serviceConfig.service}-${stage}` }] },
tagResource: {},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
describeStackEvents: {
StackEvents: [
{
ResourceStatus: 'UPDATE_COMPLETE',
ResourceType: 'AWS::CloudFormation::Stack',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 'deployment-bucket' },
},
listStackResources: {},
validateTemplate: {},
deleteChangeSet: {},
createChangeSet: {},
executeChangeSet: {},
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'CREATE_COMPLETE',
},
},
S3: {
listObjectsV2: { Contents: [] },
upload: {},
headBucket: {},
},
STS: {
getCallerIdentity: {
ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' },
UserId: 'XXXXXXXXXXXXXXXXXXXXX',
Account: '999999999999',
Arn: 'arn:aws-us-gov:iam::999999999999:user/test',
},
},
},
});

expect(getStageStub.args[0][0].stageName).to.be.equal('customStage');
expect(createStageStub.args[0][0].stageName).to.be.equal('customStage');
expect(updateStageStub.args[0][0].stageName).to.be.equal('customStage');
const accessLogSettingsDestinationArn = updateStageStub.args[0][0].patchOperations.filter(
(op) => op.path === '/accessLogSettings/destinationArn'
)[0].value;
expect(accessLogSettingsDestinationArn)
.to.be.a('string')
.and.satisfy((arn) => arn.endsWith('customStage'));
});

it('should update stage, and remove log group using stage defined under apiGateway config if configured such', async () => {
const { serviceConfig, servicePath, updateConfig } = await fixtures.setup('api-gateway');
const getDeploymentsStub = sinon.stub().returns({ items: [{ id: 'deployment-id' }] });
const updateStageStub = sinon.stub();
const deleteLogGroupStub = sinon.stub();
const stage = 'dev';

await updateConfig({
provider: {
stage,
apiGateway: {
shouldStartNameWithService: true,
stage: 'customStage',
},
stackTags: { key: 'value' },
tracing: { apiGateway: true },
},
});

await runServerless({
command: 'deploy',
cwd: servicePath,
lastLifecycleHookName: 'after:deploy:deploy',
awsRequestStubMap: {
APIGateway: {
updateStage: updateStageStub,
getStage: 'dev',
getDeployments: getDeploymentsStub,
getRestApis: { items: [{ id: 'api-id', name: `${serviceConfig.service}-${stage}` }] },
tagResource: {},
},
CloudFormation: {
describeStacks: { Stacks: [{}] },
describeStackEvents: {
StackEvents: [
{
ResourceStatus: 'UPDATE_COMPLETE',
ResourceType: 'AWS::CloudFormation::Stack',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 'deployment-bucket' },
},
listStackResources: {},
validateTemplate: {},
deleteChangeSet: {},
createChangeSet: {},
executeChangeSet: {},
describeChangeSet: {
ChangeSetName: 'new-service-dev-change-set',
ChangeSetId: 'some-change-set-id',
StackName: 'new-service-dev',
Status: 'CREATE_COMPLETE',
},
},
S3: {
listObjectsV2: { Contents: [] },
upload: {},
headBucket: {},
},
STS: {
getCallerIdentity: {
ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' },
UserId: 'XXXXXXXXXXXXXXXXXXXXX',
Account: '999999999999',
Arn: 'arn:aws-us-gov:iam::999999999999:user/test',
},
},
CloudWatchLogs: {
deleteLogGroup: deleteLogGroupStub,
},
},
});

expect(updateStageStub.args[0][0].stageName).to.be.equal('customStage');
expect(deleteLogGroupStub.args[0][0].logGroupName)
.to.be.a('string')
.and.satisfy((logGroupName) => logGroupName.endsWith('customStage'));
});
});

0 comments on commit 0a49c4f

Please sign in to comment.