Skip to content

Commit c7206bc

Browse files
alexandelphiOleksandr Kononenko
andauthored
Added support to create 2019.11.21 version of Global Table (#27)
* Added support to create 2019 11 21 version of Global Table * Returned the lighter version of lodash.get back and updated package-lock.json README.md files * Added additional tips about v2 setup to README.md file Co-authored-by: Oleksandr Kononenko <o.kononenko@spd-ukraine.com>
1 parent 4d34495 commit c7206bc

File tree

5 files changed

+150
-31
lines changed

5 files changed

+150
-31
lines changed

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,40 @@ plugins:
2121
```yaml
2222
custom:
2323
globalTables:
24+
version: v1 # optional, default is 'v1' (2017.11.29), please use 'v2' for (2019.11.21) version creation
2425
regions: # list of regions in which you want to set up global tables
2526
- region-1
2627
- region-2
2728
createStack: false # optional flag, when set to false will not deploy the stack in new region(s) and will create the tables using AWS SDK.
29+
# if you use 'createStack: true' with 'version: v2', please add 'Condition' rule to your dynamodb to create it in the main region only,
30+
# other regions are going to be replicated automatically from the main region.
2831
```
2932

30-
_NOTE_: When creating global tables with `createStack: false`, any update the source table config is not replicated to global tables.
31-
33+
_NOTE_:
34+
1. When creating global tables with `createStack: false`, any update the source table config is not replicated to global tables.
35+
2. `version` field is backward compatible and not required (the field can be absent).
36+
If you want to use Global Table (Version 2019.11.21), please use `version: v2`.
37+
Also, we don't recommend using `v2` over `v1` in your existing project. The plugin doesn't support updating from `v1` to `v2`.
38+
More details about Global Tables you can find in the following link: [AWS DynamoDB Global Tables](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GlobalTables.html)
39+
3. Here is an example of using conditions, by default it's optional, but it's required for `createStack: true` with `version: v2` setup:
40+
```
41+
Conditions:
42+
RegionUSEast1: !Equals [ !Ref "AWS::Region", us-east-1 ]
43+
44+
MyDynamoDBTable:
45+
Condition: RegionUSEast1
46+
Type: 'AWS::DynamoDB::Table'
47+
DeletionPolicy: Retain
48+
Properties:
49+
....
50+
```
3251
## Revisions
3352
* 2.0.0
3453
- Updated the package to deploy the service stack in the new region(s) by default
3554
- Added support to setup auto-scaling on global tables when not using create stack feature.
3655
- Added unit-tests
3756
* 2.1.0
3857
- Added support for PAY_PER_REQUEST billing mode for `createStack: true` mode
58+
* 3.1.0
59+
- Added support to create a new version of Global Table (Version 2019.11.21)
3960

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "serverless-create-global-dynamodb-table",
3-
"version": "3.0.0",
3+
"version": "3.1.0",
44
"description": "serverless plugin that would create global dynamodb tables for specified tables",
55
"main": "src/index.js",
66
"scripts": {
@@ -34,7 +34,7 @@
3434
"sinon": "^7.4.2"
3535
},
3636
"dependencies": {
37-
"aws-sdk": "^2.224.1",
37+
"aws-sdk": "^2.804.0",
3838
"chalk": "^2.3.2",
3939
"lodash.get": "^4.4.2"
4040
},

src/helper.js

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,17 +184,29 @@ const createNewTableAndSetScalingPolicy = async function createNewTableAndSetSca
184184
* @param {string} region AWS region in which source table exists
185185
* @param {Array} newRegions List of regions in which global tables needs to be created
186186
* @param {string} tableName Dynamodb table name
187+
* @param {string} version It's version of global table needs to be setup
187188
* @param {Object} cli Serverless cli object
188189
* @returns {Object} List of regions in which gloabl table needs to be created and flag indicating
189190
* if some global table setup already exists.
190191
*/
191192
const getRegionsToCreateGlobalTablesIn = async function getRegionsToCreateGlobalTablesIn(
192-
dynamodb, region, newRegions, tableName, cli
193+
dynamodb, region, newRegions, tableName, version, cli
193194
) {
194195
try {
195-
const resp = await dynamodb.describeGlobalTable({ GlobalTableName: tableName }).promise()
196-
const regionsGlobalTableExists = resp.GlobalTableDescription.ReplicationGroup.map(rg => rg.RegionName);
197-
const missingRegions = [region].concat(newRegions).filter(r => !regionsGlobalTableExists.includes(r));
196+
let regionsGlobalTableExists = [];
197+
let missingRegions = []
198+
if (version === 'v2') {
199+
let resp = await dynamodb.describeTable({ TableName: tableName }).promise()
200+
if (resp.Table.Replicas !== undefined) {
201+
regionsGlobalTableExists = resp.Table.Replicas.map(rg => rg.RegionName);
202+
}
203+
missingRegions = newRegions.filter(r => !regionsGlobalTableExists.includes(r));
204+
} else {
205+
let resp = await dynamodb.describeGlobalTable({ GlobalTableName: tableName }).promise()
206+
regionsGlobalTableExists = resp.GlobalTableDescription.ReplicationGroup.map(rg => rg.RegionName);
207+
missingRegions = [region].concat(newRegions).filter(r => !regionsGlobalTableExists.includes(r));
208+
}
209+
198210
if (missingRegions.length === 0) {
199211
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Global table ${tableName} already exists in all the specified regions. Skipping creation...`)}`)
200212
return {missingRegions: [], addingNewRegions: false };
@@ -218,23 +230,23 @@ const getRegionsToCreateGlobalTablesIn = async function getRegionsToCreateGlobal
218230
* @param {string} region AWS region in which source table exists
219231
* @param {string} tableName Dymanodb table name
220232
* @param {Array} newRegions List of regions in which global table needs to be setup
233+
* @param {string} version It's version of global table needs to be setup
221234
* @param {boolean} createStack flag indicating if the tables were created using cloudformation
222235
* @param {Object} cli Serverless cli object
223236
*/
224237
const createGlobalTable = async function createGlobalTable(
225-
appAutoScaling, dynamodb, creds, region, tableName, newRegions, createStack, cli
238+
appAutoScaling, dynamodb, creds, region, tableName, newRegions, version, createStack, cli
226239
) {
227240

228241
const { missingRegions: regionsToUpdate, addingNewRegions } = await module.exports.getRegionsToCreateGlobalTablesIn(
229-
dynamodb, region, newRegions, tableName, cli
242+
dynamodb, region, newRegions, tableName, version, cli
230243
);
231-
232244
if (!regionsToUpdate.length) {
233245
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Global table setup already in place.`)}`);
234246
return;
235247
}
236248

237-
if (!createStack) {
249+
if (!createStack && version !== 'v2') {
238250
const tableDef = await dynamodb.describeTable({ TableName: tableName }).promise()
239251
const { ReadCapacityUnits, WriteCapacityUnits } = tableDef.Table.ProvisionedThroughput
240252
const { GlobalSecondaryIndexes, LocalSecondaryIndexes } = tableDef.Table;
@@ -301,6 +313,16 @@ const createGlobalTable = async function createGlobalTable(
301313
}));
302314
}
303315

316+
if (version === 'v2') {
317+
await module.exports.createGlobalTableV2(dynamodb, tableName, regionsToUpdate, cli);
318+
} else {
319+
await module.exports.createGlobalTableV1(dynamodb, tableName, region, regionsToUpdate, addingNewRegions, cli);
320+
}
321+
322+
}
323+
324+
const createGlobalTableV1 = async function createGlobalTableV1(dynamodb, tableName, region, regionsToUpdate, addingNewRegions, cli) {
325+
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Create global table setup (Version 2017.11.29) for ${tableName}...`)}`)
304326
if (!addingNewRegions) {
305327
const replicationGroup = [{ RegionName: region }]
306328
regionsToUpdate.forEach(r => replicationGroup.push({ RegionName: r }))
@@ -310,7 +332,6 @@ const createGlobalTable = async function createGlobalTable(
310332
ReplicationGroup: replicationGroup,
311333
}
312334
await dynamodb.createGlobalTable(param).promise()
313-
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Created global table setup for ${tableName}...`)}`)
314335
} else {
315336
const replicaUpdates = [];
316337
regionsToUpdate.forEach(r => replicaUpdates.push({ Create:{ RegionName: r }}));
@@ -319,8 +340,25 @@ const createGlobalTable = async function createGlobalTable(
319340
ReplicaUpdates: replicaUpdates,
320341
}
321342
await dynamodb.updateGlobalTable(param).promise();
322-
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Created global table setup for ${tableName}...`)}`)
323343
}
344+
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Created global table setup (Version 2017.11.29) for ${tableName}...`)}`)
345+
}
346+
347+
const createGlobalTableV2 = async function createGlobalTableV2(dynamodb, tableName, regionsToUpdate, cli) {
348+
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Create global table setup (Version 2019.11.21) for ${tableName}...`)}`)
349+
for (const region of regionsToUpdate) {
350+
const params = {
351+
TableName: tableName,
352+
ReplicaUpdates: [{ Create:{ RegionName: region }}],
353+
}
354+
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Wait for ${tableName} replication available...`)}`)
355+
await dynamodb.waitFor('tableExists', {TableName: tableName}).promise(); // it's gonna wait for "Active" status
356+
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`Start creating a replica for ${tableName} in ${region}`)}`)
357+
await dynamodb.updateTable(params).promise();
358+
await dynamodb.waitFor('tableExists', {TableName: tableName}).promise(); // it's gonna wait for "Active" status
359+
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`The replica for ${tableName} in ${region} has been created successfully`)}`)
360+
}
361+
cli.consoleLog(`CreateGlobalTable: ${chalk.yellow(`The global table setup (Version 2019.11.21) for ${tableName} has been created successfully`)}`)
324362
}
325363

326364
/**
@@ -392,6 +430,7 @@ const createGlobalDynamodbTable = async function createGlobalDynamodbTable(serve
392430
region,
393431
tableName,
394432
globalTablesOptions.regions,
433+
globalTablesOptions.version,
395434
false,
396435
cli
397436
)
@@ -414,6 +453,7 @@ const createGlobalDynamodbTable = async function createGlobalDynamodbTable(serve
414453
region,
415454
tableName,
416455
globalTablesOptions.regions,
456+
globalTablesOptions.version,
417457
true,
418458
cli
419459
)
@@ -428,6 +468,8 @@ module.exports = {
428468
checkStackCreateUpdateStatus,
429469
createGlobalDynamodbTable,
430470
createGlobalTable,
471+
createGlobalTableV1,
472+
createGlobalTableV2,
431473
createNewTableAndSetScalingPolicy,
432474
createUpdateCfnStack,
433475
getRegionsToCreateGlobalTablesIn,

test/unittest/index.js

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ describe('test getRegionsToCreateGlobalTablesIn function', () =>{
414414
'us-west-1',
415415
'us-east-1'
416416
]
417-
describe('no global table exists', () => {
417+
describe('no global table exists for v1', () => {
418418
before(() => {
419419
const error = new Error('forced error');
420420
error.code = 'GlobalTableNotFoundException';
@@ -426,7 +426,26 @@ describe('test getRegionsToCreateGlobalTablesIn function', () =>{
426426
dynamodb.describeGlobalTable.restore();
427427
});
428428
it ('should return all the new regions', async () => {
429-
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', serverless.cli);
429+
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', 'v1', serverless.cli);
430+
resp.missingRegions.should.have.length(2);
431+
resp.missingRegions.should.eql(newRegions);
432+
resp.addingNewRegions.should.eql(false);
433+
});
434+
});
435+
436+
describe('no global table exists for v2', () => {
437+
before(() => {
438+
const error = new Error('forced error');
439+
error.code = 'GlobalTableNotFoundException';
440+
sinon.stub(dynamodb, 'describeTable').returns({
441+
promise: () => { return Promise.reject(error)}
442+
});
443+
});
444+
after(() => {
445+
dynamodb.describeTable.restore();
446+
});
447+
it ('should return all the new regions', async () => {
448+
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', 'v2', serverless.cli);
430449
resp.missingRegions.should.have.length(2);
431450
resp.missingRegions.should.eql(newRegions);
432451
resp.addingNewRegions.should.eql(false);
@@ -446,7 +465,7 @@ describe('test getRegionsToCreateGlobalTablesIn function', () =>{
446465
});
447466
it ('should return all the new regions', async () => {
448467
try {
449-
await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', serverless.cli);
468+
await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', 'v1', serverless.cli);
450469
} catch (err) {
451470
err.code.should.eql('RandomError');
452471
}
@@ -471,7 +490,7 @@ describe('test getRegionsToCreateGlobalTablesIn function', () =>{
471490
dynamodb.describeGlobalTable.restore();
472491
});
473492
it ('should return no region', async () => {
474-
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', serverless.cli);
493+
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', 'v1', serverless.cli);
475494
resp.missingRegions.should.have.length(0);
476495
resp.addingNewRegions.should.eql(false);
477496
});
@@ -494,7 +513,31 @@ describe('test getRegionsToCreateGlobalTablesIn function', () =>{
494513
dynamodb.describeGlobalTable.restore();
495514
});
496515
it ('should return no region', async () => {
497-
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', serverless.cli);
516+
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', 'v1', serverless.cli);
517+
resp.missingRegions.should.have.length(1);
518+
resp.missingRegions[0].should.eql('us-east-1');
519+
resp.addingNewRegions.should.eql(true);
520+
});
521+
});
522+
523+
describe('global table exists in all the specified new regions for v2', () => {
524+
before(() => {
525+
sinon.stub(dynamodb, 'describeTable').returns({
526+
promise: () => { return Promise.resolve({
527+
Table: {
528+
Replicas: [
529+
{ RegionName: 'us-west-1'},
530+
{ RegionName: 'us-west-2'}
531+
]
532+
}
533+
})}
534+
});
535+
});
536+
after(() => {
537+
dynamodb.describeTable.restore();
538+
});
539+
it ('should return no region', async () => {
540+
const resp = await plugin.getRegionsToCreateGlobalTablesIn(dynamodb, 'us-west-2', newRegions, 'test-table', 'v2', serverless.cli);
498541
resp.missingRegions.should.have.length(1);
499542
resp.missingRegions[0].should.eql('us-east-1');
500543
resp.addingNewRegions.should.eql(true);
@@ -557,6 +600,12 @@ describe('test createGlobalTable function', () => {
557600
sandbox.stub(dynamodb, 'updateGlobalTable').returns({
558601
promise: () => { return Promise.resolve()}
559602
});
603+
sandbox.stub(dynamodb, 'updateTable').returns({
604+
promise: () => { return Promise.resolve()}
605+
});
606+
sandbox.stub(dynamodb, 'waitFor').returns({
607+
promise: () => { return Promise.resolve()}
608+
});
560609
});
561610
afterEach(() => {
562611
sandbox.restore();
@@ -568,12 +617,12 @@ describe('test createGlobalTable function', () => {
568617
missingRegions: [],
569618
addingNewRegions: false
570619
}));
571-
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], true, serverless.cli);
620+
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], 'v1',true, serverless.cli);
572621
sandbox.assert.notCalled(dynamodb.createGlobalTable);
573622
});
574623

575624
it ('should create global table', async () => {
576-
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], true, serverless.cli);
625+
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], 'v1', true, serverless.cli);
577626
sandbox.assert.calledOnce(dynamodb.createGlobalTable);
578627
});
579628

@@ -583,14 +632,14 @@ describe('test createGlobalTable function', () => {
583632
missingRegions: ['us-west-1'],
584633
addingNewRegions: true
585634
}));
586-
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], true, serverless.cli);
635+
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], 'v1', true, serverless.cli);
587636
sandbox.assert.notCalled(dynamodb.createGlobalTable);
588637
sandbox.assert.calledOnce(dynamodb.updateGlobalTable);
589638
});
590639

591640
context("when create stack is false", () => {
592641
it ('should create the table with ProvisionedThroughput if billing mode is not PAY_PER_REQUEST', async () => {
593-
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], false, serverless.cli);
642+
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], 'v1', false, serverless.cli);
594643
sandbox.assert.calledOnce(dynamodb.describeTable);
595644
plugin.createNewTableAndSetScalingPolicy.lastCall.args[2].should.eql({ AttributeDefinitions: {},
596645
KeySchema: '',
@@ -612,7 +661,7 @@ describe('test createGlobalTable function', () => {
612661

613662
it ('should create the table without ProvisionedThroughput if billing mode is PAY_PER_REQUEST', async () => {
614663
stubbedTable.Table.BillingModeSummary = { BillingMode: "PAY_PER_REQUEST" };
615-
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], false, serverless.cli);
664+
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], 'v1', false, serverless.cli);
616665
sandbox.assert.calledOnce(dynamodb.describeTable);
617666
plugin.createNewTableAndSetScalingPolicy.lastCall.args[2].should.eql({ AttributeDefinitions: {},
618667
KeySchema: '',
@@ -628,6 +677,13 @@ describe('test createGlobalTable function', () => {
628677
BillingMode: 'PAY_PER_REQUEST',
629678
Tags: {} });
630679
});
680+
681+
it ('should create the table by using v2 version', async () => {
682+
stubbedTable.Table.BillingModeSummary = { BillingMode: "PAY_PER_REQUEST" };
683+
await plugin.createGlobalTable(aas, dynamodb, serverless.getProvider().getCredentials(), 'us-west-2', 'test-table', ['us-east-2'], 'v2', false, serverless.cli);
684+
sandbox.assert.calledOnce(dynamodb.updateTable);
685+
sandbox.assert.calledTwice(dynamodb.waitFor);
686+
});
631687
})
632688
});
633689

0 commit comments

Comments
 (0)