Skip to content

Commit 39f2521

Browse files
authored
Merge pull request #1 from nicka/feature/initial-release
Feature/initial release
2 parents d96db89 + 5266fbb commit 39f2521

File tree

8 files changed

+382
-1
lines changed

8 files changed

+382
-1
lines changed

.eslintignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.tmp/
2+
build/
3+
coverage/
4+
docs/
5+
node_modules/
6+
results/
7+
.serverless

.eslintrc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"extends": "airbnb-base",
3+
"globals": {
4+
"afterAll": true,
5+
"applyToAll": true,
6+
"afterEach": true,
7+
"beforeAll": true,
8+
"beforeEach": true,
9+
"describe": true,
10+
"expect": true,
11+
"jest": true,
12+
"register": true,
13+
"it": true,
14+
"test": true
15+
},
16+
"rules": {
17+
"no-console": 0,
18+
"strict": 0,
19+
"import/prefer-default-export": 0,
20+
"import/no-extraneous-dependencies": [
21+
"error", {
22+
"devDependencies": ["**/*.test.js", "**/*.spec.js"]
23+
}
24+
]
25+
}
26+
}

.travis.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
language: node_js
2+
node_js:
3+
- '4'
4+
cache:
5+
directories:
6+
- node_modules
7+
branches:
8+
only:
9+
- develop
10+
- master
11+
- "/^feature\\/.*$/"
12+
- "/^release\\/.*$/"
13+
install:
14+
- travis_retry npm install
15+
after_success:
16+
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
[![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com)
2+
[![Coverage-Status](https://coveralls.io/repos/github/nicka/serverless-plugin-diff/badge.svg?branch=master)](https://coveralls.io/github/nicka/serverless-plugin-diff?branch=master)
3+
[![Build-Status](https://travis-ci.org/nicka/serverless-plugin-diff.svg?branch=master)](https://travis-ci.org/nicka/serverless-plugin-diff)
24

35
# Serverless CloudFormation Diff
46

@@ -11,9 +13,11 @@ Plugin for Serverless Framework v1.x which compares your locale AWS CloudFormati
1113
## Usage
1214

1315
```bash
14-
serverless deploy --diff --noDeploy --stage REPLACEME --region REPLACEME
16+
serverless deploy diff --stage REPLACEME --region REPLACEME
1517
```
1618

19+
<img width="1255" alt="screen shot 2016-11-05 at 14 53 04" src="https://cloud.githubusercontent.com/assets/195404/20030536/9e1a552c-a367-11e6-8e6d-2043f2a5d038.png">
20+
1721
## Install
1822

1923
Execute npm install in your Serverless project.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
exports[`serverless-plugin-write-env-vars diff successfully triggers diff runs diff with custom diff tool 1`] = `
2+
"1c1
3+
< {\"foo\":\"bar\"}
4+
\\ No newline at end of file
5+
---
6+
> {\"foo\":\"foo\"}
7+
\\ No newline at end of file
8+
"
9+
`;
10+
11+
exports[`serverless-plugin-write-env-vars diff successfully triggers diff runs diff with custom localTemplate template 1`] = `
12+
"1c1
13+
< {\"foo\":\"custom\"}
14+
\\ No newline at end of file
15+
---
16+
> {\"foo\":\"foo\"}
17+
\\ No newline at end of file
18+
"
19+
`;
20+
21+
exports[`serverless-plugin-write-env-vars diff successfully triggers diff runs diff with defaults 1`] = `
22+
"1c1
23+
< {\"foo\":\"bar\"}
24+
\\ No newline at end of file
25+
---
26+
> {\"foo\":\"foo\"}
27+
\\ No newline at end of file
28+
"
29+
`;
30+
31+
exports[`serverless-plugin-write-env-vars diff unsuccessfully triggers diff could not find locally compiled template 1`] = `".serverless/cloudformation-template-update-stack.json could not be found: run \"sls deploy --noDeploy\" first."`;
32+
33+
exports[`serverless-plugin-write-env-vars downloadTemplate with successful CloudFormation call downloads currently deployed template 1`] = `
34+
"{
35+
\"foo\": \"bar\"
36+
}"
37+
`;
38+
39+
exports[`serverless-plugin-write-env-vars downloadTemplate with unsuccessful CloudFormation call could not download deployed template 1`] = `"Stack with id foo-foo does not exist"`;

lib/index.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict';
2+
3+
const AWS = require('aws-sdk');
4+
const fs = require('fs-promise');
5+
const exec = require('child-process-promise').exec;
6+
const Q = require('q');
7+
8+
class ServerlessPlugin {
9+
constructor(serverless, options) {
10+
this.serverless = serverless;
11+
this.options = options;
12+
13+
this.commands = {
14+
diff: {
15+
usage: 'Helps you start your first Serverless plugin',
16+
lifecycleEvents: ['diff'],
17+
options: {
18+
diffTool: {
19+
usage:
20+
'Specify the diff tool you want to use '
21+
+ '(e.g. "--diffTool \'ksdiff\'")',
22+
required: false,
23+
shortcut: 'dt',
24+
},
25+
localTemplate: {
26+
usage:
27+
'Specify your locally compiled CloudFormation template'
28+
+ '(e.g. "--localTemplate \'./serverless/'
29+
+ 'cloudformation-template-update-stack.json\'")',
30+
required: false,
31+
shortcut: 'lt',
32+
},
33+
},
34+
},
35+
};
36+
37+
this.hooks = {
38+
'before:diff:diff': this.downloadTemplate.bind(this),
39+
'diff:diff': this.diff.bind(this),
40+
};
41+
42+
this.options.stage = this.options.stage
43+
|| (this.serverless.service.defaults && this.serverless.service.defaults.stage)
44+
|| 'dev';
45+
this.options.region = this.options.region
46+
|| (this.serverless.service.defaults && this.serverless.service.defaults.region)
47+
|| 'us-east-1';
48+
this.options.diffTool = this.options.diffTool
49+
|| 'diff';
50+
this.options.localTemplate = this.options.localTemplate
51+
|| '.serverless/cloudformation-template-update-stack.json';
52+
this.options.orgTemplate = this.options.localTemplate.replace('.json', '.org.json');
53+
54+
AWS.config.update({ region: this.options.region });
55+
56+
this.cloudFormation = new AWS.CloudFormation();
57+
}
58+
59+
downloadTemplate() {
60+
const deferred = Q.defer();
61+
const orgTemplate = this.options.orgTemplate;
62+
const params = {
63+
StackName: `${this.serverless.service.service}-${this.options.stage}`,
64+
TemplateStage: 'Processed',
65+
};
66+
67+
this.serverless.cli.log('Downloading currently deployed template');
68+
69+
this.cloudFormation.getTemplate(params, (err, data) => {
70+
if (err) {
71+
deferred.reject(err.message);
72+
} else {
73+
let templateBody = JSON.parse(data.TemplateBody);
74+
templateBody = JSON.stringify(templateBody, null, 2);
75+
76+
fs.writeFile(orgTemplate, templateBody)
77+
.then(() => deferred.resolve())
78+
.catch(fsErr => deferred.reject(fsErr.message));
79+
}
80+
});
81+
82+
return deferred.promise;
83+
}
84+
85+
diff() {
86+
const deferred = Q.defer();
87+
const diffTool = this.options.diffTool;
88+
const localTemplate = this.options.localTemplate;
89+
const orgTemplate = this.options.orgTemplate;
90+
91+
this.serverless.cli.log('Running diff against deployed template');
92+
93+
fs.stat(localTemplate)
94+
.then(() => {
95+
exec(`${diffTool} ${orgTemplate} ${localTemplate} || true`)
96+
.then((result) => {
97+
const diffData = result.stdout;
98+
console.log(diffData);
99+
deferred.resolve(diffData);
100+
})
101+
.catch(execErr => deferred.reject(execErr.message));
102+
})
103+
.catch((err) => {
104+
if (err.code === 'ENOENT') {
105+
const errorPrefix = `${localTemplate} could not be found:`;
106+
deferred.reject(`${errorPrefix} run "sls deploy --noDeploy" first.`);
107+
}
108+
});
109+
110+
return deferred.promise;
111+
}
112+
}
113+
114+
module.exports = ServerlessPlugin;

lib/index.test.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
const AWS = require('aws-sdk-mock');
2+
const fs = require('fs-promise');
3+
const Plugin = require('./index');
4+
5+
const slsDir = '.serverless';
6+
const templatePrefix = `${slsDir}/cloudformation-template-update-stack`;
7+
const exampleTemplate = `${templatePrefix}.json`;
8+
const exampleOrgTemplate = `${templatePrefix}.org.json`;
9+
const slsDefaults = {
10+
service: {
11+
service: 'foo',
12+
defaults: {
13+
stage: 'foo',
14+
region: 'eu-west-1',
15+
},
16+
},
17+
cli: {
18+
log: jest.fn(),
19+
},
20+
};
21+
22+
beforeAll(() => {
23+
if (!fs.existsSync(slsDir)) {
24+
fs.mkdirSync(slsDir);
25+
}
26+
});
27+
28+
describe('serverless-plugin-write-env-vars', () => {
29+
describe('constructor', () => {
30+
it('sets the correct defaults', () => {
31+
const plugin = new Plugin(slsDefaults, {});
32+
33+
expect(plugin.options.stage).toBe('foo');
34+
expect(plugin.options.region).toBe('eu-west-1');
35+
expect(plugin.options.diffTool).toBe('diff');
36+
expect(plugin.options.localTemplate).toBe(`${templatePrefix}.json`);
37+
expect(plugin.options.orgTemplate).toBe(`${templatePrefix}.org.json`);
38+
});
39+
40+
it('registers the appropriate hooks', () => {
41+
const plugin = new Plugin(slsDefaults, {});
42+
43+
expect(typeof plugin.hooks['before:diff:diff']).toBe('function');
44+
expect(typeof plugin.hooks['diff:diff']).toBe('function');
45+
});
46+
});
47+
48+
describe('downloadTemplate', () => {
49+
afterEach(() => AWS.restore('CloudFormation'));
50+
51+
describe('with successful CloudFormation call', () => {
52+
beforeEach(() =>
53+
AWS.mock('CloudFormation', 'getTemplate', {
54+
TemplateBody: '{"foo":"bar"}',
55+
})
56+
);
57+
58+
it('downloads currently deployed template', () => {
59+
const plugin = new Plugin(slsDefaults, {});
60+
61+
return plugin.downloadTemplate()
62+
.then(() =>
63+
fs.readFile(`${templatePrefix}.org.json`, { encoding: 'utf8' })
64+
.then(data => expect(data).toMatchSnapshot())
65+
);
66+
});
67+
});
68+
69+
describe('with unsuccessful CloudFormation call', () => {
70+
beforeEach(() =>
71+
AWS.mock('CloudFormation', 'getTemplate', (Object, callback) =>
72+
callback(new Error('Stack with id foo-foo does not exist'), null)
73+
)
74+
);
75+
76+
it('could not download deployed template', () => {
77+
const plugin = new Plugin(slsDefaults, {});
78+
79+
return plugin.downloadTemplate()
80+
.catch(err => expect(err).toMatchSnapshot());
81+
});
82+
});
83+
});
84+
85+
describe('diff', () => {
86+
beforeEach(() =>
87+
fs.writeFile(exampleTemplate, '{"foo":"foo"}')
88+
.then(() => fs.writeFile(exampleOrgTemplate, '{"foo":"bar"}'))
89+
);
90+
91+
describe('successfully triggers diff', () => {
92+
it('runs diff with defaults', () => {
93+
const plugin = new Plugin(slsDefaults, {});
94+
95+
return plugin.diff()
96+
.then(data => expect(data).toMatchSnapshot());
97+
});
98+
99+
it('runs diff with custom diff tool', () => {
100+
const plugin = new Plugin(slsDefaults, {
101+
diffTool: 'diff',
102+
});
103+
104+
return plugin.diff()
105+
.then(data => expect(data).toMatchSnapshot());
106+
});
107+
});
108+
109+
describe('successfully triggers diff', () => {
110+
const customTemplate = `${templatePrefix}-foo.json`;
111+
const customOrgTemplate = `${templatePrefix}-foo.org.json`;
112+
113+
beforeEach(() =>
114+
fs.writeFile(customTemplate, '{"foo":"foo"}')
115+
.then(() => fs.writeFile(customOrgTemplate, '{"foo":"custom"}'))
116+
);
117+
118+
it('runs diff with custom localTemplate template', () => {
119+
const plugin = new Plugin(slsDefaults, {
120+
localTemplate: customTemplate,
121+
});
122+
123+
return plugin.diff()
124+
.then(data => expect(data).toMatchSnapshot());
125+
});
126+
});
127+
128+
describe('unsuccessfully triggers diff', () => {
129+
beforeEach(() => fs.unlink(exampleTemplate));
130+
131+
it('could not find locally compiled template', () => {
132+
const plugin = new Plugin(slsDefaults, {});
133+
134+
return plugin.diff()
135+
.catch(err => expect(err).toMatchSnapshot());
136+
});
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)