lambda_wrap 0.27.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,182 +1,461 @@
1
- require 'aws-sdk'
2
-
3
1
  module LambdaWrap
4
- # The DynamoDBManager simplifies setting up and destroying a DynamoDB database.
5
- #
6
- # Note: In case an environment specific DynamoDB tablename such as +<baseTableName>-production+ should be used, then
7
- # it has to be injected directly to the methods since not all environments necessarily need separated databases.
8
- class DynamoDbManager
9
- ##
10
- # The constructor does some basic setup
11
- # * Validating basic AWS configuration
12
- # * Creating the underlying client to interact with the AWS SDK.
13
- def initialize
14
- @client = Aws::DynamoDB::Client.new
15
- end
16
-
17
- def set_table_capacity(table_name, read_capacity, write_capacity)
18
- table_details = get_table_details(table_name)
19
- puts "Updating new read/write capacity for table #{table_name}.
20
- Read #{table_details.provisioned_throughput.read_capacity_units} ==> #{read_capacity}.
21
- Write #{table_details.provisioned_throughput.write_capacity_units} ==> #{write_capacity}."
22
- @client.update_table(
23
- table_name: table_name,
24
- provisioned_throughput: { read_capacity_units: read_capacity, write_capacity_units: write_capacity }
25
- )
2
+ # The DynamoTable class simplifies Creation, Updating, and Destroying Dynamo DB Tables.
3
+ # @since 1.0
4
+ class DynamoTable < AwsService
5
+ # Sets up the DynamoTable for the Dynamo DB Manager. Preloading the configuration in the constructor.
6
+ #
7
+ # @param [Hash] options The configuration for the DynamoDB Table.
8
+ # @option options [String] :table_name The name of the DynamoDB Table. A "Base Name" can be used here where the
9
+ # environment name can be appended upon deployment.
10
+ #
11
+ # @option options [Array<Hash>] :attribute_definitions ([{ attribute_name: 'Id', attribute_type: 'S' }]) An array of
12
+ # attributes that describe the key schema for the table and indexes. The Hash must have symbols: :attribute_name &
13
+ # :attribute_type. Please see AWS Documentation for the {http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DataModel.html
14
+ # Data Model}.
15
+ #
16
+ # @option options [Array<Hash>] :key_schema ([{ attribute_name: 'Id', key_type: 'HASH' }]) Specifies the attributes
17
+ # that make up the primary key for a table or an index. The attributes in key_schema must also be defined in the
18
+ # AttributeDefinitions array. Please see AWS Documentation for the {http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DataModel.html
19
+ # Data Model}.
20
+ #
21
+ # Each element in the array must be composed of:
22
+ # * <tt>:attribute_name</tt> - The name of this key attribute.
23
+ # * <tt>:key_type</tt> - The role that the key attribute will assume:
24
+ # * <tt>HASH</tt> - partition key
25
+ # * <tt>RANGE</tt> - sort key
26
+ #
27
+ # The partition key of an item is also known as its hash attribute. The term "hash attribute" derives from
28
+ # DynamoDB's usage of an internal hash function to evenly distribute data items across partitions, based on their
29
+ # partition key values.
30
+ #
31
+ # The sort key of an item is also known as its range attribute. The term "range attribute" derives from the way
32
+ # DynamoDB stores items with the same partition key physically close together, in sorted order by the sort key
33
+ # value.
34
+ #
35
+ # For a simple primary key (partition key), you must provide exactly one element with a <tt>KeyType</tt> of
36
+ # <tt>HASH</tt>.
37
+ #
38
+ # For a composite primary key (partition key and sort key), you must provide exactly two elements, in this order:
39
+ # The first element must have a <tt>KeyType</tt> of <tt>HASH</tt>, and the second element must have a
40
+ # <tt>KeyType</tt> of <tt>RANGE</tt>.
41
+ #
42
+ # For more information, see {http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html#WorkingWithTables.primary.key
43
+ # Specifying the Primary Key} in the <em>Amazon DynamoDB Developer Guide</em>.
44
+ #
45
+ # @option options [Integer] :read_capacity_units (1) The maximum number of strongly consistent reads consumed per
46
+ # second before DynamoDB returns a <tt>ThrottlingException</tt>. Must be at least 1. For current minimum and
47
+ # maximum provisioned throughput values, see {http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html
48
+ # Limits} in the <em>Amazon DynamoDB Developer Guide</em>.
49
+ #
50
+ # @option options [Integer] :write_capacity_units (1) The maximum number of writes consumed per second before
51
+ # DynamoDB returns a <tt>ThrottlingException</tt>. Must be at least 1. For current minimum and maximum
52
+ # provisioned throughput values, see {http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html
53
+ # Limits} in the <em>Amazon DynamoDB Developer Guide</em>.
54
+ #
55
+ # @option options [Array<Hash>] :local_secondary_indexes ([]) One or more local secondary indexes (the maximum is
56
+ # five) to be created on the table. Each index is scoped to a given partition key value. There is a 10 GB size
57
+ # limit per partition key value; otherwise, the size of a local secondary index is unconstrained.
58
+ #
59
+ # Each element in the array must be a Hash with these symbols:
60
+ # * <tt>:index_name</tt> - The name of the local secondary index. Must be unique only for this table.
61
+ # * <tt>:key_schema</tt> - Specifies the key schema for the local secondary index. The key schema must begin with
62
+ # the same partition key as the table.
63
+ # * <tt>:projection</tt> - Specifies attributes that are copied (projected) from the table into the index. These
64
+ # are in addition to the primary key attributes and index key attributes, which are automatically projected. Each
65
+ # attribute specification is composed of:
66
+ # * <tt>:projection_type</tt> - One of the following:
67
+ # * <tt>KEYS_ONLY</tt> - Only the index and primary keys are projected into the index.
68
+ # * <tt>INCLUDE</tt> - Only the specified table attributes are projected into the index. The list of projected
69
+ # attributes are in <tt>non_key_attributes</tt>.
70
+ # * <tt>ALL</tt> - All of the table attributes are projected into the index.
71
+ # * <tt>:non_key_attributes</tt> - A list of one or more non-key attribute names that are projected into the
72
+ # secondary index. The total count of attributes provided in NonKeyAttributes, summed across all of the
73
+ # secondary indexes, must not exceed 20. If you project the same attribute into two different indexes, this
74
+ # counts as two distinct attributes when determining the total.
75
+ #
76
+ # @option options [Array<Hash>] :global_secondary_indexes ([]) One or more global secondary indexes (the maximum is
77
+ # five) to be created on the table. Each global secondary index (Hash) in the array includes the following:
78
+ # * <tt>:index_name</tt> - The name of the global secondary index. Must be unique only for this table.
79
+ # * <tt>:key_schema</tt> - Specifies the key schema for the global secondary index.
80
+ # * <tt>:projection</tt> - Specifies attributes that are copied (projected) from the table into the index. These
81
+ # are in addition to the primary key attributes and index key attributes, which are automatically projected. Each
82
+ # attribute specification is composed of:
83
+ # * <tt>:projection_type</tt> - One of the following:
84
+ # * <tt>KEYS_ONLY</tt> - Only the index and primary keys are projected into the index.
85
+ # * <tt>INCLUDE</tt> - Only the specified table attributes are projected into the index. The list of projected
86
+ # attributes are in <tt>NonKeyAttributes</tt>.
87
+ # * <tt>ALL</tt> - All of the table attributes are projected into the index.
88
+ # * <tt>non_key_attributes</tt> - A list of one or more non-key attribute names that are projected into the
89
+ # secondary index. The total count of attributes provided in NonKeyAttributes, summed across all of the
90
+ # secondary indexes, must not exceed 20. If you project the same attribute into two different indexes, this
91
+ # counts as two distinct attributes when determining the total.
92
+ # * <tt>:provisioned_throughput</tt> - The provisioned throughput settings for the global secondary index,
93
+ # consisting of read and write capacity units.
94
+ #
95
+ # @option options [Boolean] :append_environment_on_deploy (false) Option to append the name of the environment to
96
+ # the table name upon deployment and teardown. DynamoDB Tables cannot shard data in a similar manner as how Lambda
97
+ # aliases and API Gateway Environments work. This option is supposed to help the user with naming tables instead
98
+ # of managing the environment names on their own.
99
+ def initialize(options)
100
+ default_options = { append_environment_on_deploy: false, read_capacity_units: 1, write_capacity_units: 1,
101
+ local_secondary_indexes: nil, global_secondary_indexes: nil,
102
+ attribute_definitions: [{ attribute_name: 'Id', attribute_type: 'S' }],
103
+ key_schema: [{ attribute_name: 'Id', key_type: 'HASH' }] }
104
+
105
+ options_with_defaults = options.reverse_merge(default_options)
106
+
107
+ @table_name = options_with_defaults[:table_name]
108
+ raise ArgumentError, ':table_name is required.' unless @table_name
109
+
110
+ @attribute_definitions = options_with_defaults[:attribute_definitions]
111
+ @key_schema = options_with_defaults[:key_schema]
112
+
113
+ # Verify that all of key_schema is defined in attribute_definitions
114
+ defined_in_attribute_definitions_guard(@key_schema)
115
+
116
+ @read_capacity_units = options_with_defaults[:read_capacity_units]
117
+ @write_capacity_units = options_with_defaults[:write_capacity_units]
118
+ provisioned_throughput_guard(read_capacity_units: @read_capacity_units,
119
+ write_capacity_units: @write_capacity_units)
120
+
121
+ unless @read_capacity_units >= 1 && @write_capacity_units >= 1 && (@read_capacity_units.is_a? Integer) &&
122
+ (@write_capacity_units.is_a? Integer)
123
+ raise ArgumentExecption, 'Read and Write Capacity must be positive integers.'
124
+ end
125
+
126
+ @local_secondary_indexes = options_with_defaults[:local_secondary_indexes]
127
+
128
+ if @local_secondary_indexes && @local_secondary_indexes.length > 5
129
+ raise ArgumentError, 'Can only have 5 LocalSecondaryIndexes per table!'
130
+ end
131
+ if @local_secondary_indexes && !@local_secondary_indexes.empty?
132
+ @local_secondary_indexes.each { |lsindex| defined_in_attribute_definitions_guard(lsindex[:key_schema]) }
133
+ end
134
+
135
+ @global_secondary_indexes = options_with_defaults[:global_secondary_indexes]
136
+
137
+ if @global_secondary_indexes && @global_secondary_indexes.length > 5
138
+ raise ArgumentError, 'Can only have 5 GlobalSecondaryIndexes per table1'
139
+ end
140
+ if @global_secondary_indexes && !@global_secondary_indexes.empty?
141
+ @global_secondary_indexes.each do |gsindex|
142
+ defined_in_attribute_definitions_guard(gsindex[:key_schema])
143
+ provisioned_throughput_guard(gsindex[:provisioned_throughput])
144
+ end
145
+ end
146
+
147
+ @append_environment_on_deploy = options_with_defaults[:append_environment_on_deploy]
26
148
  end
27
149
 
28
- ##
29
- # Updates the provisioned throughput read and write capacity of the requested table.
30
- # If the table does not exist an error message is displayed. If current read/write capacity
31
- # is equals to requested read/write capacity or the requested read/write capacity is 0 or less than 0
32
- # no table updation is performed.
33
- #
34
- # *Arguments*
35
- # [table_name] The table name of the dynamoDB to be updated.
36
- # [read_capacity] The read capacity the table should be updated with.
37
- # [write_capacity] The write capacity the table should be updated with.
38
- def update_table_capacity(table_name, read_capacity, write_capacity)
39
- table_details = get_table_details(table_name)
40
- raise "Update cannot be performed. Table #{table_name} does not exists." if table_details.nil?
41
-
42
- wait_until_table_available(table_name) if table_details.table_status != 'ACTIVE'
43
-
44
- if read_capacity <= 0 || write_capacity <= 0
45
- puts "Table: #{table_name} not updated. Read/Write capacity should be greater than or equal to 1."
46
- elsif read_capacity == table_details.provisioned_throughput.read_capacity_units ||
47
- write_capacity == table_details.provisioned_throughput.write_capacity_units
48
- puts "Table: #{table_name} not updated. Current and requested reads/writes are same."
49
- puts 'Current ReadCapacityUnits provisioned for the table: ' \
50
- "#{table_details.provisioned_throughput.read_capacity_units}."
51
- puts "Requested ReadCapacityUnits: #{read_capacity}."
52
- puts 'Current WriteCapacityUnits provisioned for the table: ' \
53
- "#{table_details.provisioned_throughput.write_capacity_units}."
54
- puts "Requested WriteCapacityUnits: #{write_capacity}."
150
+ # Deploys the DynamoDB Table to the target environment. If the @append_environment_on_deploy option is set, the
151
+ # table_name will be appended with a hyphen and the environment name. This will attempt to Create or Update with
152
+ # the parameters specified from the constructor. This may take a LONG time for it will wait for any new indexes to
153
+ # be available.
154
+ #
155
+ # @param environment_options [LambdaWrap::Environment] Target environment to deploy.
156
+ # @param client [Aws::DynamoDB::Client] Client to use with SDK. Should be passed in by the API class.
157
+ # @param region [String] AWS Region string. Should be passed in by the API class.
158
+ def deploy(environment_options, client, region = 'AWS_REGION')
159
+ super
160
+
161
+ puts "Deploying Table: #{@table_name} to Environment: #{environment_options.name}"
162
+
163
+ full_table_name = @table_name + (@append_environment_on_deploy ? "-#{environment_options.name}" : '')
164
+
165
+ table_details = retrieve_table_details(full_table_name)
166
+
167
+ if table_details.nil?
168
+ create_table(full_table_name)
55
169
  else
56
- response = @client.update_table(
57
- table_name: table_name,
58
- provisioned_throughput: { read_capacity_units: read_capacity, write_capacity_units: write_capacity }
59
- )
170
+ wait_until_table_is_available(full_table_name) if table_details[:table_status] != 'ACTIVE'
171
+ update_table(full_table_name, table_details)
172
+ end
60
173
 
61
- if response.table_description.table_status == 'UPDATING'
62
- puts "Updated new read/write capacity for table #{table_name}.
63
- Read capacity updated to: #{read_capacity}.
64
- Write capacity updated to: #{write_capacity}."
65
- else
66
- raise "Read and writes capacities was not updated for table: #{table_name}."
67
- end
174
+ puts "Dynamo Table #{full_table_name} is now available."
175
+ full_table_name
176
+ end
177
+
178
+ # Deletes the DynamoDB table specified by the table_name and the Environment name (if append_environment_on_deploy)
179
+ # was specified. Otherwise just deletes the table.
180
+ #
181
+ # @param environment_options [LambdaWrap::Environment] Target environment to teardown
182
+ # @param client [Aws::DynamoDB::Client] Client to use with SDK. Should be passed in by the API class.
183
+ # @param region [String] AWS Region string. Should be passed in by the API class.
184
+ def teardown(environment_options, client, region = 'AWS_REGION')
185
+ super
186
+ puts "Tearingdown Table: #{@table_name} from Environment: #{environment_options.name}"
187
+ full_table_name = @table_name + (@append_environment_on_deploy ? "-#{environment_options.name}" : '')
188
+ delete_table(full_table_name)
189
+ full_table_name
190
+ end
191
+
192
+ # Deletes all DynamoDB tables that are prefixed with the @table_name specified in the constructor.
193
+ # This is an attempt to tear down all DynamoTables that were deployed with the environment name appended.
194
+ #
195
+ # @param client [Aws::DynamoDB::Client] Client to use with SDK. Should be passed in by the API class.
196
+ # @param region [String] AWS Region string. Should be passed in by the API class.
197
+ def delete(client, region = 'AWS_REGION')
198
+ super
199
+ puts "Deleting all tables with prefix: #{@table_name}."
200
+ table_names = retrieve_prefixed_tables(@table_name)
201
+ table_names.each { |table_name| delete_table(table_name) }
202
+ puts "Deleted #{table_names.length} tables."
203
+ table_names.length
204
+ end
205
+
206
+ def to_s
207
+ return @table_name if @table_name && @table_name.is_a?(String)
208
+ super
209
+ end
210
+
211
+ private
212
+
213
+ def retrieve_table_details(full_table_name)
214
+ table_details = nil
215
+ begin
216
+ table_details = @client.describe_table(table_name: full_table_name).table
217
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
218
+ puts "Table #{full_table_name} does not exist."
68
219
  end
220
+ table_details
69
221
  end
70
222
 
71
223
  ##
72
- # Publishes the database and awaits until it is fully available. If the table already exists, it only adjusts the
73
- # read and write capacities upwards (it doesn't downgrade them to avoid a production environment being impacted with
74
- # a default setting of an automated script).
75
- #
76
- # *Arguments*
77
- # [table_name] The table name of the dynamoDB to be created.
78
- # [attribute_definitions] The dynamoDB attribute definitions to be used when the table is created.
79
- # [key_schema] The dynamoDB key definitions to be used when the table is created.
80
- # [read_capacity] The read capacity to configure for the dynamoDB table.
81
- # [write_capacity] The write capacity to configure for the dynamoDB table.
82
- # [local_secondary_indexes] The local secondary indexes to be created.
83
- # [global_secondary_indexes] The global secondary indexes to be created.
84
- def publish_database(
85
- table_name, attribute_definitions, key_schema, read_capacity, write_capacity, local_secondary_indexes = nil,
86
- global_secondary_indexes = nil
87
- )
88
- has_updates = false
89
-
90
- table_details = get_table_details(table_name)
91
-
92
- if !table_details.nil?
93
- wait_until_table_available(table_name) if table_details.table_status != 'ACTIVE'
94
-
95
- if read_capacity > table_details.provisioned_throughput.read_capacity_units ||
96
- write_capacity > table_details.provisioned_throughput.write_capacity_units
97
- set_table_capacity(table_name, read_capacity, write_capacity)
98
- has_updates = true
224
+ # Waits for the table to be available
225
+ def wait_until_table_is_available(full_table_name, delay = 5, max_attempts = 5)
226
+ puts "Waiting for Table #{full_table_name} to be available."
227
+ puts "Waiting with a #{delay} second delay between attempts, for a maximum of #{max_attempts} attempts."
228
+ max_time = Time.at(delay * max_attempts).utc.strftime('%H:%M:%S')
229
+ puts "Max waiting time will be: #{max_time} (approximate)."
230
+ # wait until the table has updated to being fully available
231
+ # waiting for ~2min at most; an error will be thrown afterwards
232
+
233
+ started_waiting_at = Time.now
234
+ max_attempts.times do |attempt|
235
+ puts "Attempt #{attempt + 1}/#{max_attempts}, \
236
+ #{Time.at(Time.now - started_waiting_at).utc.strftime('%H:%M:%S')}/#{max_time}"
237
+
238
+ details = retrieve_table_details(full_table_name)
239
+
240
+ if details.table_status != 'ACTIVE'
241
+ puts "Table: #{full_table_name} is not yet available. Status: #{details.table_status}. Retrying..."
99
242
  else
100
- puts "Table #{table_name} already exists and the desired read capacity of #{read_capacity} and " \
101
- "write capacity of #{write_capacity} has at least been configured. Downgrading capacity units is not " \
102
- 'supported. No changes were applied.'
243
+ updating_indexes = details.global_secondary_indexes.reject do |global_index|
244
+ global_index.index_status == 'ACTIVE'
245
+ end
246
+ return true if updating_indexes.empty?
247
+ puts 'Table is available, but the global indexes are not:'
248
+ puts(updating_indexes.map { |global_index| "#{global_index.index_name}, #{global_index.index_status}" })
103
249
  end
104
- else
105
- puts "Creating table #{table_name}."
106
- ad = attribute_definitions || [{ attribute_name: 'Id', attribute_type: 'S' }]
107
- ks = key_schema || [{ attribute_name: 'Id', key_type: 'HASH' }]
250
+ Kernel.sleep(delay.seconds)
251
+ end
108
252
 
109
- params = {
110
- table_name: table_name, key_schema: ks, attribute_definitions: ad,
111
- provisioned_throughput: {
112
- read_capacity_units: read_capacity, write_capacity_units: write_capacity
113
- }
114
- }
253
+ raise Exception, "Table #{full_table_name} did not become available after #{max_attempts} attempts. " \
254
+ 'Try again later or inspect the AWS console.'
255
+ end
256
+
257
+ # Updates the Dynamo Table. You can only perform one of the following update operations at once:
258
+ # * Modify the provisioned throughput settings of the table.
259
+ # * Enable or disable Streams on the table.
260
+ # * Remove a global secondary index from the table.
261
+ # * Create a new global secondary index on the table. Once the index begins backfilling,
262
+ # you can use UpdateTable to perform other operations.
263
+ def update_table(full_table_name, table_details)
264
+ # Determine if Provisioned Throughput needs to be updated.
265
+ if @read_capacity_units != table_details.provisioned_throughput.read_capacity_units &&
266
+ @write_capacity_units != table_details.provisioned_throughput.write_capacity_units
115
267
 
116
- params[:local_secondary_indexes] = local_secondary_indexes unless local_secondary_indexes.nil?
117
- params[:global_secondary_indexes] = global_secondary_indexes unless global_secondary_indexes.nil?
268
+ update_provisioned_throughput(
269
+ full_table_name, table_details.provisioned_throughput.read_capacity_units,
270
+ table_details.provisioned_throughput.write_capacity_units
271
+ )
118
272
 
119
- @client.create_table(params)
120
- has_updates = true
273
+ # Wait up to 30 minutes.
274
+ wait_until_table_is_available(full_table_name, 5, 360)
121
275
  end
122
276
 
123
- if has_updates
124
- wait_until_table_available(table_name)
125
- puts "DynamoDB table #{table_name} is now fully available."
277
+ # Determine if there are any Global Secondary Indexes to be deleted.
278
+ global_secondary_indexes_to_delete = build_global_index_deletes_array(table_details.global_secondary_indexes)
279
+ unless global_secondary_indexes_to_delete.empty?
280
+ # Loop through each index to delete, and send the update one at a time (restriction on the API).
281
+ until global_secondary_indexes_to_delete.empty?
282
+ delete_global_index(full_table_name, global_secondary_indexes_to_delete.pop)
283
+
284
+ # Wait up to 2 hours.
285
+ wait_until_table_is_available(full_table_name, 10, 720)
286
+ end
287
+ end
288
+
289
+ # Determine if there are updates to the Provisioned Throughput of the Global Secondary Indexes
290
+ global_secondary_index_updates = build_global_index_updates_array(table_details.global_secondary_indexes)
291
+ unless global_secondary_index_updates.empty?
292
+ update_global_indexes(full_table_name, global_secondary_index_updates)
293
+
294
+ # Wait up to 4 hours.
295
+ wait_until_table_is_available(full_table_name, 10, 1_440)
126
296
  end
297
+
298
+ # Determine if there are new Global Secondary Indexes to be created.
299
+ new_global_secondary_indexes = build_new_global_indexes_array(table_details.global_secondary_indexes)
300
+ return if new_global_secondary_indexes.empty?
301
+
302
+ create_global_indexes(full_table_name, new_global_secondary_indexes)
303
+
304
+ # Wait up to 4 hours.
305
+ wait_until_table_is_available(full_table_name, 10, 1_440)
127
306
  end
128
307
 
129
- ##
130
- # Deletes a DynamoDB table. It does not wait until the table has been deleted.
131
- #
132
- # *Arguments*
133
- # [table_name] The dynamoDB table name to delete.
134
- def delete_database(table_name)
135
- table_details = get_table_details(table_name)
308
+ def update_provisioned_throughput(full_table_name, old_read, old_write)
309
+ puts "Updating Provisioned Throughtput for #{full_table_name}"
310
+ puts "Setting Read Capacity Units From: #{old_read} To: #{@read_capacity_units}"
311
+ puts "Setting Write Capacty Units From: #{old_write} To: #{@write_capacity_units}"
312
+ @client.update_table(
313
+ table_name: full_table_name,
314
+ provisioned_throughput: { read_capacity_units: @read_capacity_units,
315
+ write_capacity_units: @write_capacity_units }
316
+ )
317
+ end
318
+
319
+ def build_global_index_deletes_array(current_global_indexes)
320
+ return [] if current_global_indexes.empty?
321
+ current_index_names = current_global_indexes.map(&:index_name)
322
+ target_index_names = @global_secondary_indexes.map { |gsindex| gsindex[:index_name] }
323
+ current_index_names - target_index_names
324
+ end
325
+
326
+ def delete_global_index(full_table_name, index_to_delete)
327
+ puts "Deleting Global Secondary Index: #{index_to_delete} from Table: #{full_table_name}"
328
+ @client.update_table(
329
+ table_name: full_table_name,
330
+ global_secondary_index_updates: [{ delete: { index_name: index_to_delete } }]
331
+ )
332
+ end
333
+
334
+ # Looks through the list current of Global Secondary Indexes and builds an array if the Provisioned Throughput
335
+ # in the intended Indexes are higher than the current Indexes.
336
+ def build_global_index_updates_array(current_global_indexes)
337
+ indexes_to_update = []
338
+ return indexes_to_update if current_global_indexes.empty?
339
+ current_global_indexes.each do |current_index|
340
+ @global_secondary_indexes.each do |target_index|
341
+ # Find the same named index
342
+ next unless target_index[:index_name] == current_index[:index_name]
343
+ # Skip unless a different ProvisionedThroughput is specified
344
+ break unless (target_index[:provisioned_throughput][:read_capacity_units] !=
345
+ current_index.provisioned_throughput.read_capacity_units) ||
346
+ (target_index[:provisioned_throughput][:write_capacity_units] !=
347
+ current_index.provisioned_throughput.write_capacity_units)
348
+ indexes_to_update << { index_name: target_index[:index_name],
349
+ provisioned_throughput: target_index[:provisioned_throughput] }
350
+ end
351
+ end
352
+ puts indexes_to_update
353
+ indexes_to_update
354
+ end
355
+
356
+ def update_global_indexes(full_table_name, global_secondary_index_updates)
357
+ puts "Updating Global Indexes for Table: #{full_table_name}"
358
+ puts(
359
+ global_secondary_index_updates.map do |index|
360
+ "#{index[:index_name]} -\
361
+ \tRead: #{index[:provisioned_throughput][:read_capacity_units]},\
362
+ \tWrite: #{index[:provisioned_throughput][:write_capacity_units]}"
363
+ end
364
+ )
365
+
366
+ @client.update_table(
367
+ table_name: full_table_name,
368
+ global_secondary_index_updates: global_secondary_index_updates.map { |index| { update: index } }
369
+ )
370
+ end
371
+
372
+ def build_new_global_indexes_array(current_global_indexes)
373
+ return [] if !@global_secondary_indexes || @global_secondary_indexes.empty?
374
+
375
+ index_names_to_create = @global_secondary_indexes.map { |gsindex| gsindex[:index_name] } -
376
+ current_global_indexes.map(&:index_name)
377
+
378
+ @global_secondary_indexes.select do |gsindex|
379
+ index_names_to_create.include?(gsindex[:index_name])
380
+ end
381
+ end
382
+
383
+ def create_global_indexes(full_table_name, new_global_secondary_indexes)
384
+ puts "Creating new Global Indexes for Table: #{full_table_name}"
385
+ puts(new_global_secondary_indexes.map { |index| index[:index_name].to_s })
386
+ @client.update_table(
387
+ table_name: full_table_name,
388
+ global_secondary_index_updates: new_global_secondary_indexes.map { |index| { create: index } }
389
+ )
390
+ end
391
+
392
+ def create_table(full_table_name)
393
+ puts "Creating table #{full_table_name}..."
394
+ @client.create_table(
395
+ table_name: full_table_name, attribute_definitions: @attribute_definitions,
396
+ key_schema: @key_schema,
397
+ provisioned_throughput: { read_capacity_units: @read_capacity_units,
398
+ write_capacity_units: @write_capacity_units },
399
+ local_secondary_indexes: @local_secondary_indexes,
400
+ global_secondary_indexes: @global_secondary_indexes
401
+ )
402
+ # Wait 60 seconds because "DescribeTable uses an eventually consistent query"
403
+ puts 'Sleeping for 60 seconds...'
404
+ Kernel.sleep(60)
405
+
406
+ # Wait for up to 2m.
407
+ wait_until_table_is_available(full_table_name, 5, 24)
408
+ end
409
+
410
+ def delete_table(full_table_name)
411
+ puts "Trying to delete Table: #{full_table_name}"
412
+ table_details = retrieve_table_details(full_table_name)
136
413
  if table_details.nil?
137
414
  puts 'Table did not exist. Nothing to delete.'
138
415
  else
139
- wait_until_table_available(table_name) if table_details.table_status != 'ACTIVE'
140
- @client.delete_table(table_name: table_name)
416
+ # Wait up to 30m
417
+ wait_until_table_available(full_table_name, 5, 360) if table_details.table_status != 'ACTIVE'
418
+ @client.delete_table(table_name: full_table_name)
141
419
  end
142
420
  end
143
421
 
144
- ##
145
- # Awaits a given status of a table.
146
- #
147
- # *Arguments*
148
- # [table_name] The dynamoDB table name to watch until it reaches an active status.
149
- def wait_until_table_available(table_name)
150
- max_attempts = 24
151
- delay_between_attempts = 5
422
+ def retrieve_prefixed_tables(prefix)
423
+ retrieve_all_table_names.select { |name| name =~ /#{Regexp.quote(prefix)}[a-zA-Z0-9_\-.]*/ }
424
+ end
152
425
 
153
- # wait until the table has updated to being fully available
154
- # waiting for ~2min at most; an error will be thrown afterwards
155
- begin
156
- @client.wait_until(:table_exists, table_name: table_name) do |w|
157
- w.max_attempts = max_attempts
158
- w.delay = delay_between_attempts
159
- w.before_wait do |attempts, _|
160
- puts "Waiting until table becomes available. Attempt #{attempts}/#{max_attempts} " \
161
- "with polling interval #{delay_between_attempts}."
426
+ def retrieve_all_table_names
427
+ tables = []
428
+ response = nil
429
+ loop do
430
+ response =
431
+ if !response || response.last_evaluated_table_name.nil? || response.last_evaluated_table_name.empty?
432
+ @client.list_tables(limit: 100)
433
+ else
434
+ @client.list_tables(limit: 100, exclusive_start_table_name: response.last_evaluated_table_name)
162
435
  end
436
+ tables.concat(response.table_names)
437
+ if response.table_names.empty? || response.last_evaluated_table_name.nil? ||
438
+ response.last_evaluated_table_name.empty?
439
+ return tables
163
440
  end
164
- rescue Aws::Waiters::Errors::TooManyAttemptsError => e
165
- puts "Table #{table_name} did not become available after #{e.attempts} attempts. " \
166
- 'Try again later or inspect the AWS console.'
167
441
  end
168
442
  end
169
443
 
170
- def get_table_details(table_name)
171
- table_details = nil
172
- begin
173
- table_details = @client.describe_table(table_name: table_name).table
174
- rescue Aws::DynamoDB::Errors::ResourceNotFoundException
175
- puts "Table #{table_name} does not exist."
444
+ def defined_in_attribute_definitions_guard(key_schema)
445
+ if Set.new(key_schema.map { |item| item[:attribute_name] })
446
+ .subset?(Set.new(@attribute_definitions.map { |item| item[:attribute_name] }))
447
+ return true
176
448
  end
177
- table_details
449
+ raise ArgumentError, 'Not all keys in the key_schema are defined in the attribute_definitions!'
178
450
  end
179
451
 
180
- private :wait_until_table_available, :get_table_details
452
+ def provisioned_throughput_guard(provisioned_throughput)
453
+ if provisioned_throughput[:read_capacity_units] >= 1 && provisioned_throughput[:write_capacity_units] >= 1 &&
454
+ provisioned_throughput[:read_capacity_units].is_a?(Integer) &&
455
+ provisioned_throughput[:write_capacity_units].is_a?(Integer)
456
+ return true
457
+ end
458
+ raise ArgumentError, 'Read and Write Capacity for all ProvisionedThroughput must be positive integers.'
459
+ end
181
460
  end
182
461
  end