Skip to content

Commit

Permalink
Merge branch 'release/1.6.0' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Diana Ionita committed Nov 29, 2020
2 parents 21e4916 + 903b9ce commit b3f4979
Show file tree
Hide file tree
Showing 11 changed files with 358 additions and 29 deletions.
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# serverless-api-gateway-caching

![npm](https://img.shields.io/npm/v/serverless-api-gateway-caching.svg)
[![CircleCI](https://circleci.com/gh/DianaIonita/serverless-api-gateway-caching.svg?style=svg)](https://circleci.com/gh/DianaIonita/serverless-api-gateway-caching)
![npm](https://img.shields.io/npm/v/serverless-api-gateway-caching.svg)
[![npm downloads](https://img.shields.io/npm/dt/serverless-api-gateway-caching.svg?style=svg)](https://www.npmjs.com/package/serverless-api-gateway-caching)

## Intro
A plugin for the serverless framework which helps with configuring caching for API Gateway endpoints.
Expand Down Expand Up @@ -282,6 +283,60 @@ custom:
handleUnauthorizedRequests: Ignore
```

## Configuring caching settings for endpoints defined in CloudFormation
You can use this feature to configure caching for endpoints which are defined in CloudFormation and not as serverless functions.
If your `serverless.yml` contains, for example, a [HTTP Proxy](https://www.serverless.com/framework/docs/providers/aws/events/apigateway/#setting-an-http-proxy-on-api-gateway) like this:

```yml
resources:
Resources:
ProxyResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId:
Fn::GetAtt:
- ApiGatewayRestApi # the default Rest API logical ID
- RootResourceId
PathPart: serverless # the endpoint in your API that is set as proxy
RestApiId:
Ref: ApiGatewayRestApi
ProxyMethod:
Type: AWS::ApiGateway::Method
Properties:
ResourceId:
Ref: ProxyResource
RestApiId:
Ref: ApiGatewayRestApi
HttpMethod: GET
AuthorizationType: NONE
MethodResponses:
- StatusCode: 200
Integration:
IntegrationHttpMethod: POST
Type: HTTP
Uri: http://serverless.com # the URL you want to set a proxy to
IntegrationResponses:
- StatusCode: 200
```

Then you can configure caching for it like this:

```yml
plugins:
- serverless-api-gateway-caching
custom:
apiGatewayCaching:
enabled: true
additionalEndpoints:
- method: GET
path: /serverless
caching:
enabled: true # it must be specifically enabled
ttlInSeconds: 1200 # if not set, inherited from global settings
dataEncrypted: true # if not set, inherited from global settings
```

## More Examples

A function with several endpoints:
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-api-gateway-caching",
"version": "1.5.2",
"version": "1.6.0",
"description": "A plugin for the serverless framework which helps with configuring caching for API Gateway endpoints.",
"main": "src/apiGatewayCachingPlugin.js",
"scripts": {
Expand Down
36 changes: 27 additions & 9 deletions src/ApiGatewayCachingSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ class PerKeyInvalidationSettings {
}

class ApiGatewayEndpointCachingSettings {
constructor(customFunctionName, functionName, event, globalSettings) {
this.customFunctionName = customFunctionName;
constructor(functionName, event, globalSettings) {
this.functionName = functionName;

if (typeof (event.http) === 'string') {
Expand Down Expand Up @@ -76,13 +75,24 @@ class ApiGatewayEndpointCachingSettings {
}
}

class ApiGatewayAdditionalEndpointCachingSettings {
constructor(method, path, caching, globalSettings) {
this.method = method;
this.path = path;
this.cachingEnabled = globalSettings.cachingEnabled ? get(caching, 'enabled', false) : false;
this.cacheTtlInSeconds = get(caching, 'ttlInSeconds', globalSettings.cacheTtlInSeconds);
this.dataEncrypted = get(caching, 'dataEncrypted', globalSettings.dataEncrypted);
}
}

class ApiGatewayCachingSettings {
constructor(serverless, options) {
if (!get(serverless, 'service.custom.apiGatewayCaching')) {
return;
}
this.cachingEnabled = serverless.service.custom.apiGatewayCaching.enabled;
this.apiGatewayIsShared = serverless.service.custom.apiGatewayCaching.apiGatewayIsShared;
const cachingSettings = serverless.service.custom.apiGatewayCaching;
this.cachingEnabled = cachingSettings.enabled;
this.apiGatewayIsShared = cachingSettings.apiGatewayIsShared;

if (options) {
this.stage = options.stage || serverless.service.provider.stage;
Expand All @@ -93,18 +103,26 @@ class ApiGatewayCachingSettings {
}

this.endpointSettings = [];
this.additionalEndpointSettings = [];

this.cacheClusterSize = serverless.service.custom.apiGatewayCaching.clusterSize || DEFAULT_CACHE_CLUSTER_SIZE;
this.cacheTtlInSeconds = serverless.service.custom.apiGatewayCaching.ttlInSeconds || DEFAULT_TTL;
this.dataEncrypted = serverless.service.custom.apiGatewayCaching.dataEncrypted || DEFAULT_DATA_ENCRYPTED;
this.cacheClusterSize = cachingSettings.clusterSize || DEFAULT_CACHE_CLUSTER_SIZE;
this.cacheTtlInSeconds = cachingSettings.ttlInSeconds || DEFAULT_TTL;
this.dataEncrypted = cachingSettings.dataEncrypted || DEFAULT_DATA_ENCRYPTED;

const additionalEndpoints = cachingSettings.additionalEndpoints || [];
for (let additionalEndpoint of additionalEndpoints) {
const { method, path, caching } = additionalEndpoint;

this.additionalEndpointSettings.push(new ApiGatewayAdditionalEndpointCachingSettings(method, path, caching, this))
}

this.perKeyInvalidation = new PerKeyInvalidationSettings(serverless.service.custom.apiGatewayCaching);
this.perKeyInvalidation = new PerKeyInvalidationSettings(cachingSettings);

for (let functionName in serverless.service.functions) {
let functionSettings = serverless.service.functions[functionName];
for (let event in functionSettings.events) {
if (isApiGatewayEndpoint(functionSettings.events[event])) {
this.endpointSettings.push(new ApiGatewayEndpointCachingSettings(functionSettings.name, functionName, functionSettings.events[event], this))
this.endpointSettings.push(new ApiGatewayEndpointCachingSettings(functionName, functionSettings.events[event], this))
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/apiGatewayCachingPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ class ApiGatewayCachingPlugin {
enum: ['Ignore', 'IgnoreWithWarning', 'Fail']
}
}
},
additionalEndpoints: {
type: 'array',
items: {
properties: {
method: { type: 'string' },
path: { type: 'string' },
caching: {
properties: {
enabled: { type: 'boolean' },
ttlInSeconds: { type: 'number' },
dataEncrypted: { type: 'boolean' },
}
}
}
}
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions src/stageCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const createPatchForStage = (settings) => {
patch.push({
op: 'replace',
path: '/*/*/caching/dataEncrypted',
value: `${settings.dataEncrypted}`
value: `${settings.dataEncrypted}`
});

patch.push({
Expand All @@ -49,7 +49,7 @@ const createPatchForStage = (settings) => {
value: `${settings.cacheTtlInSeconds}`
});
}

return patch;
}

Expand Down Expand Up @@ -183,7 +183,8 @@ const updateStageCacheSettings = async (settings, serverless) => {

let patchOps = createPatchForStage(settings);

let endpointsWithCachingEnabled = settings.endpointSettings.filter(e => e.cachingEnabled);
let endpointsWithCachingEnabled = settings.endpointSettings.filter(e => e.cachingEnabled)
.concat(settings.additionalEndpointSettings.filter(e => e.cachingEnabled));
if (settings.cachingEnabled && isEmpty(endpointsWithCachingEnabled)) {
serverless.cli.log(`[serverless-api-gateway-caching] [WARNING] API Gateway caching is enabled but none of the endpoints have caching enabled`);
}
Expand All @@ -192,6 +193,13 @@ const updateStageCacheSettings = async (settings, serverless) => {
let endpointPatch = createPatchForEndpoint(endpointSettings, serverless);
patchOps = patchOps.concat(endpointPatch);
}

// TODO handle 'ANY' method, if possible
for (let additionalEndpointSettings of settings.additionalEndpointSettings) {
let endpointPatch = patchForMethod(additionalEndpointSettings.path, additionalEndpointSettings.method, additionalEndpointSettings);
patchOps = patchOps.concat(endpointPatch);
}

let params = {
restApiId,
stageName: settings.stage,
Expand Down
90 changes: 90 additions & 0 deletions test/creating-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,96 @@ describe('Creating settings', () => {
});
});

describe('when there are additional endpoints defined', () => {
describe('and caching is turned off globally', () => {
before(() => {
serverless = given.a_serverless_instance()
.withApiGatewayCachingConfig(false)
.withAdditionalEndpoints([{ method: 'GET', path: '/shelter', caching: { enabled: true } }]);

cacheSettings = createSettingsFor(serverless);
});

it('caching should be disabled for the additional endpoints', () => {
expect(cacheSettings.additionalEndpointSettings[0].cachingEnabled).to.be.false;
});
});

describe('and caching is turned on globally', () => {
before(() => {
serverless = given.a_serverless_instance()
.withApiGatewayCachingConfig(true, '1', 20);
});

describe('and no caching settings are defined for the additional endpoint', () => {
before(() => {
serverless = serverless
.withAdditionalEndpoints([{ method: 'GET', path: '/shelter' }]);

cacheSettings = createSettingsFor(serverless);
});

it('should disable caching for the endpoint', () => {
expect(cacheSettings.additionalEndpointSettings[0].cachingEnabled).to.equal(false);
});
});

describe('and only the fact that caching is enabled is specified on the additional endpoint', () => {
before(() => {
let caching = { enabled: true };
serverless = serverless
.withAdditionalEndpoints([{ method: 'GET', path: '/shelter', caching }]);

cacheSettings = createSettingsFor(serverless);
});

it('should inherit time to live settings from global settings', () => {
expect(cacheSettings.additionalEndpointSettings[0].cacheTtlInSeconds).to.equal(20);
});

it('should inherit data encryption settings from global settings', () => {
expect(cacheSettings.additionalEndpointSettings[0].dataEncrypted).to.equal(false);
});
});

describe('and the time to live is specified', () => {
before(() => {
let caching = { enabled: true, ttlInSeconds: 67 }
serverless = serverless
.withAdditionalEndpoints([{ method: 'GET', path: '/shelter', caching }]);

cacheSettings = createSettingsFor(serverless);
});

it('should set the correct time to live', () => {
expect(cacheSettings.additionalEndpointSettings[0].cacheTtlInSeconds).to.equal(67);
});

it('should inherit data encryption settings from global settings', () => {
expect(cacheSettings.additionalEndpointSettings[0].dataEncrypted).to.equal(false);
});
});

describe('and data encryption is specified', () => {
before(() => {
let caching = { enabled: true, dataEncrypted: true }
serverless = serverless
.withAdditionalEndpoints([{ method: 'GET', path: '/shelter', caching }]);

cacheSettings = createSettingsFor(serverless);
});

it('should set the correct data encryption', () => {
expect(cacheSettings.additionalEndpointSettings[0].dataEncrypted).to.equal(true);
});

it('should inherit time to live settings from global settings', () => {
expect(cacheSettings.additionalEndpointSettings[0].cacheTtlInSeconds).to.equal(20);
});
});
});
});

describe('when there are command line options for the deployment', () => {
let options;
before(() => {
Expand Down
20 changes: 12 additions & 8 deletions test/model/Serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ class Serverless {
return this;
}

withAdditionalEndpoints(additionalEndpoints){
this.service.custom.apiGatewayCaching.additionalEndpoints = additionalEndpoints;
return this;
}

withPredefinedRestApiId(restApiId) {
if (!this.service.provider.apiGateway) {
this.service.provider.apiGateway = {}
Expand Down Expand Up @@ -95,20 +100,19 @@ class Serverless {
},
request: async (awsService, method, properties, stage, region) => {
this._recordedAwsRequests.push({ awsService, method, properties, stage, region });

if (awsService == 'CloudFormation'
&& method == 'describeStacks'
&& properties.StackName == 'serverless-stack-name'
&& stage == settings.stage
&& region == settings.region) {
return {
Stacks: [
{
Outputs: [{
OutputKey: 'RestApiIdForApigCaching',
OutputValue: restApiId
}]
}
]
Stacks: [{
Outputs: [{
OutputKey: 'RestApiIdForApigCaching',
OutputValue: restApiId
}]
}]
};
}
}
Expand Down
11 changes: 10 additions & 1 deletion test/steps/given.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,19 @@ const endpoints_with_caching_enabled = (endpointCount) => {
return result;
}

const an_additional_endpoint = ({ method, path, caching }) => {
return {
method,
path,
caching
}
}

module.exports = {
a_serverless_instance,
a_serverless_function,
a_rest_api_id,
a_rest_api_id_for_deployment,
endpoints_with_caching_enabled
endpoints_with_caching_enabled,
an_additional_endpoint
}
Loading

0 comments on commit b3f4979

Please sign in to comment.