Skip to content

Commit

Permalink
feat(AWS HTTP API): Support attachment to externally created API
Browse files Browse the repository at this point in the history
  • Loading branch information
medikoo committed Feb 26, 2020
1 parent 22364b8 commit f47b340
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 5 deletions.
12 changes: 12 additions & 0 deletions docs/providers/aws/events/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,15 @@ provider:
```

See [AWS HTTP API Logging](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html) documentation for more info on variables that can be used

### Resuing HTTP API in different services

We may attach configured endpoints to HTTP API creted externally. For that provide HTTP API id in provider settings as follows:

```yaml
provider:
httpApi:
id: xxxx # id of externally created HTTP API to which endpoints should be attached.
```

In such case no API and stage resources are created, therefore extending HTTP API with CORS or access logs settings is not supported.
12 changes: 12 additions & 0 deletions lib/plugins/aws/info/getStackInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ module.exports = {
if (result) stackData.outputs = result.Stacks[0].Outputs;
}),
];
if (this.serverless.service.provider.httpApi && this.serverless.service.provider.httpApi.id) {
sdkRequests.push(
this.provider
.request('ApiGatewayV2', 'getApi', { ApiId: this.serverless.service.provider.httpApi.id })
.then(result => {
if (result) stackData.externalHttpApiEndpoint = result.ApiEndpoint;
})
);
}

// Get info from CloudFormation Outputs
return BbPromise.all(sdkRequests).then(() => {
Expand Down Expand Up @@ -89,6 +98,9 @@ module.exports = {
}
});
}
if (stackData.externalHttpApiEndpoint) {
this.gatheredData.info.endpoints.push(`httpApi: ${stackData.externalHttpApiEndpoint}`);
}

return BbPromise.resolve();
});
Expand Down
27 changes: 22 additions & 5 deletions lib/plugins/aws/package/compile/events/httpApi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ class HttpApiEvents {
},
};
}
getApiIdConfig() {
return this.config.id || { Ref: this.provider.naming.getHttpApiLogicalId() };
}
compileApi() {
if (this.config.id) return;
const properties = {
Name: this.provider.naming.getHttpApiName(),
ProtocolType: 'HTTP',
Expand Down Expand Up @@ -76,6 +80,7 @@ class HttpApiEvents {
};
}
compileStage() {
if (this.config.id) return;
const properties = {
ApiId: { Ref: this.provider.naming.getHttpApiLogicalId() },
StageName: '$default',
Expand Down Expand Up @@ -118,7 +123,7 @@ class HttpApiEvents {
] = {
Type: 'AWS::ApiGatewayV2::Authorizer',
Properties: {
ApiId: { Ref: this.provider.naming.getHttpApiLogicalId() },
ApiId: this.getApiIdConfig(),
AuthorizerType: 'JWT',
IdentitySource: [authorizer.identitySource],
JwtConfiguration: {
Expand All @@ -139,7 +144,7 @@ class HttpApiEvents {
] = {
Type: 'AWS::ApiGatewayV2::Route',
Properties: {
ApiId: { Ref: this.provider.naming.getHttpApiLogicalId() },
ApiId: this.getApiIdConfig(),
RouteKey: routeKey === '*' ? '$default' : routeKey,
Target: {
'Fn::Join': [
Expand Down Expand Up @@ -173,13 +178,19 @@ Object.defineProperties(
memoizeeMethods({
resolveConfiguration: d(function() {
const routes = new Map();
this.config = { routes };
const providerConfig = this.serverless.service.provider;
const userConfig = providerConfig.httpApi || {};
this.config = { routes, id: userConfig.id };
let cors = null;
let shouldFillCorsMethods = false;
const userCors = userConfig.cors;
if (userCors) {
if (userConfig.id) {
throw new this.serverless.classes.Error(
'Cannot setup CORS rules for externally confugured HTTP API',
'EXTERNAL_HTTP_API_CORS_CONFIG'
);
}
cors = this.config.cors = {};
if (userConfig.cors === true) {
Object.assign(cors, defaultCors);
Expand Down Expand Up @@ -217,6 +228,12 @@ Object.defineProperties(

const userLogsConfig = providerConfig.logs && providerConfig.logs.httpApi;
if (userLogsConfig) {
if (userConfig.id) {
throw new this.serverless.classes.Error(
'Cannot setup access logs for externally confugured HTTP API',
'EXTERNAL_HTTP_API_LOGS_CONFIG'
);
}
this.config.accessLogFormat =
userLogsConfig.format ||
`{${JSON.stringify({
Expand Down Expand Up @@ -355,7 +372,7 @@ Object.defineProperties(
] = {
Type: 'AWS::ApiGatewayV2::Integration',
Properties: {
ApiId: { Ref: this.provider.naming.getHttpApiLogicalId() },
ApiId: this.getApiIdConfig(),
IntegrationType: 'AWS_PROXY',
IntegrationUri: resolveTargetConfig(routeTargetData),
PayloadFormatVersion: '1.0',
Expand All @@ -382,7 +399,7 @@ Object.defineProperties(
':',
{ Ref: 'AWS::AccountId' },
':',
{ Ref: this.provider.naming.getHttpApiLogicalId() },
this.getApiIdConfig(),
'/*',
],
],
Expand Down
49 changes: 49 additions & 0 deletions lib/plugins/aws/package/compile/events/httpApi/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,53 @@ describe('HttpApiEvents', () => {
expect(stageResourceProps.AccessLogSettings).to.have.property('Format');
});
});

describe('External HTTP API', () => {
let cfResources;
let cfOutputs;
let naming;
const apiId = 'external-api-id';

before(() =>
fixtures.extend('httpApi', { provider: { httpApi: { id: apiId } } }).then(fixturePath =>
runServerless({
cwd: fixturePath,
cliArgs: ['package'],
}).then(serverless => {
({
Resources: cfResources,
Outputs: cfOutputs,
} = serverless.service.provider.compiledCloudFormationTemplate);
naming = serverless.getProvider('aws').naming;
})
)
);

it('Should not configure API resource', () => {
expect(cfResources).to.not.have.property(naming.getHttpApiLogicalId());
});
it('Should not configure stage resource', () => {
expect(cfResources).to.not.have.property(naming.getHttpApiStageLogicalId());
});
it('Should not configure output', () => {
expect(cfOutputs).to.not.have.property('HttpApiUrl');
});
it('Should configure endpoint that attaches to external API', () => {
const routeKey = 'POST /some-post';
const resource = cfResources[naming.getHttpApiRouteLogicalId(routeKey)];
expect(resource.Type).to.equal('AWS::ApiGatewayV2::Route');
expect(resource.Properties.RouteKey).to.equal(routeKey);
expect(resource.Properties.ApiId).to.equal(apiId);
});
it('Should configure endpoint integration', () => {
const resource = cfResources[naming.getHttpApiIntegrationLogicalId('foo')];
expect(resource.Type).to.equal('AWS::ApiGatewayV2::Integration');
expect(resource.Properties.IntegrationType).to.equal('AWS_PROXY');
});
it('Should configure lambda permissions', () => {
const resource = cfResources[naming.getLambdaHttpApiPermissionLogicalId('foo')];
expect(resource.Type).to.equal('AWS::Lambda::Permission');
expect(resource.Properties.Action).to.equal('lambda:InvokeFunction');
});
});
});
24 changes: 24 additions & 0 deletions tests/fixtures/httpApiExport/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
service: service
provider:
name: aws

resources:
Resources:
HttpApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: dev-${self:service}
ProtocolType: HTTP
HttpApiStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId:
Ref: HttpApi
StageName: $default
AutoDeploy: true
Outputs:
HttpApiId:
Value:
Ref: HttpApi
Export:
Name: TestHttpApiExportId
72 changes: 72 additions & 0 deletions tests/integration-all/http-api/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,76 @@ describe('HTTP API Integration Test', function() {
expect(json).to.deep.equal({ method: 'PATCH', path: '/foo' });
});
});

describe('Shared API', () => {
let exportServicePath;

before(async () => {
exportServicePath = getTmpDirPath();
log.debug('service #1 path %s', exportServicePath);

const exportServiceConfig = await createTestService(exportServicePath, {
templateDir: fixtures.map.httpApiExport,
});
log.notice('deploying %s service', exportServiceConfig.service);
await deployService(exportServicePath);
const httpApiId = (
await awsRequest('CloudFormation', 'describeStacks', {
StackName: `${exportServiceConfig.service}-${stage}`,
})
).Stacks[0].Outputs[0].OutputValue;

tmpDirPath = getTmpDirPath();
log.debug('sevice #2 path %s', tmpDirPath);
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: await fixtures.extend('httpApi', {
provider: { httpApi: { id: httpApiId } },
}),
});
serviceName = serverlessConfig.service;
stackName = `${serviceName}-${stage}`;
log.notice('deploying %s service', serviceName);
await deployService(tmpDirPath);
endpoint = (await awsRequest('ApiGatewayV2', 'getApi', { ApiId: httpApiId })).ApiEndpoint;
});

after(async () => {
if (serviceName) {
log.notice('removing service #2');
await removeService(tmpDirPath);
}
log.notice('removing service #1');
await removeService(exportServicePath);
});

it('should expose an accessible POST HTTP endpoint', async () => {
const testEndpoint = `${endpoint}/some-post`;

const response = await fetch(testEndpoint, { method: 'POST' });
const json = await response.json();
expect(json).to.deep.equal({ method: 'POST', path: '/some-post' });
});

it('should expose an accessible paramed GET HTTP endpoint', async () => {
const testEndpoint = `${endpoint}/bar/whatever`;

const response = await fetch(testEndpoint, { method: 'GET' });
const json = await response.json();
expect(json).to.deep.equal({ method: 'GET', path: '/bar/whatever' });
});

it('should return 404 on not supported method', async () => {
const testEndpoint = `${endpoint}/foo`;

const response = await fetch(testEndpoint, { method: 'POST' });
expect(response.status).to.equal(404);
});

it('should return 404 on not configured path', async () => {
const testEndpoint = `${endpoint}/not-configured`;

const response = await fetch(testEndpoint, { method: 'GET' });
expect(response.status).to.equal(404);
});
});
});

0 comments on commit f47b340

Please sign in to comment.