dynamocli 0.1.3 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36b53c1ee892def9c34a5ad8b7c73c24b152a1695924f3be978822653e3eaed3
4
- data.tar.gz: 847ea3ae63cb8fd46092bba1fde2cbda1ca605236dbb6867f73ed93bca5e474a
3
+ metadata.gz: 1aafa6a05574f7dd3cf4b1dd979a9ca5469a0ab542b5201573c4a4e723c9bbf2
4
+ data.tar.gz: '011871424ef1cfff30254cbe834ca4bce362b1100928acd029911dc5eae8cff9'
5
5
  SHA512:
6
- metadata.gz: e153aa55521d0d7669cfa63194c01ed2cf096f5a8a4cf195bfa1258c41cc17c174df96dfb9ceba1d96e44dc4551684daa9378e8bf0c3bfdf1c4289ee6302453f
7
- data.tar.gz: efadb7ee0651538f7a99c533bf7743fa52ffee15068760853af7427e035dc4396bb2823e7bb1125ab9f897b1b417e7df7cbd3b5e8d4d358a2b534e8610388df9
6
+ metadata.gz: 2eb2d1d4e025cca2424aa8c93624ec831cf3b2a14c2cb48e9ef743c273ecd37fd52c79090627fa81d027f4bdd19d6c942bd4f5db2508939b8072e9e091606715
7
+ data.tar.gz: 3b73649c0163a4a3fdeafd00805e27f6bedc172c38bd96b8f5385ff4c7ad808d993b7083737d1d2c667484d0f6f673554f06e481c58e3fb226577317ae752e17
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.8] - 2020-08-18
6
+ ### Changed
7
+ - Update Rack.
8
+
9
+ ## [0.1.7] - 2020-08-18
10
+ ### Changed
11
+ - Add what Ruby version is required.
12
+
13
+ ## [0.1.6] - 2020-01-13
14
+ ### Added
15
+ - Option to import data from a CSV exported from AWS.
16
+
17
+ ### Changed
18
+ - Big refactoring in erase functionality.
19
+
20
+ ## [0.1.5] - 2019-08-08
21
+ ### Fixed
22
+ - Fix erase table without GSIs.
23
+ - Fix erase table with LCIs.
24
+ - Fix import data to a table with attributes types other than String.
25
+
26
+ ## [0.1.4] - 2019-07-29
27
+ ### Fixed
28
+ - Fix erase table with indexes.
29
+
30
+ ### Changed
31
+ - Add some life to logs.
32
+
5
33
  ## [0.1.3] - 2019-06-20
6
34
  ### Fixed
7
35
  - Specify the version of the cloudformation gem.
@@ -16,6 +44,11 @@ All notable changes to this project will be documented in this file.
16
44
  ### Added
17
45
  - Command to import data from a CSV file to a DynamoDB table.
18
46
 
47
+ [0.1.8]: https://github.com/matheussilvasantos/dynamocli/compare/v0.1.7...v0.1.8
48
+ [0.1.7]: https://github.com/matheussilvasantos/dynamocli/compare/v0.1.6...v0.1.7
49
+ [0.1.6]: https://github.com/matheussilvasantos/dynamocli/compare/v0.1.5...v0.1.6
50
+ [0.1.5]: https://github.com/matheussilvasantos/dynamocli/compare/v0.1.4...v0.1.5
51
+ [0.1.4]: https://github.com/matheussilvasantos/dynamocli/compare/v0.1.3...v0.1.4
19
52
  [0.1.3]: https://github.com/matheussilvasantos/dynamocli/compare/v0.1.2...v0.1.3
20
53
  [0.1.2]: https://github.com/matheussilvasantos/dynamocli/commit/6fd76a06819ff32464eeeae1f097bccd33f21387
21
54
  [0.1.0]: https://github.com/matheussilvasantos/dynamocli/releases/tag/v0.1.0
@@ -1,54 +1,64 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dynamocli (0.1.3)
4
+ dynamocli (0.1.6)
5
5
  aws-sdk-cloudformation (~> 1.23)
6
6
  aws-sdk-dynamodb (~> 1.28)
7
7
  thor (~> 0.20)
8
+ tty-logger (~> 0.1.0)
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
11
12
  specs:
12
13
  aws-eventstream (1.0.3)
13
- aws-partitions (1.177.0)
14
- aws-sdk-cloudformation (1.23.0)
15
- aws-sdk-core (~> 3, >= 3.56.0)
14
+ aws-partitions (1.263.0)
15
+ aws-sdk-cloudformation (1.29.0)
16
+ aws-sdk-core (~> 3, >= 3.71.0)
16
17
  aws-sigv4 (~> 1.1)
17
- aws-sdk-core (3.56.0)
18
+ aws-sdk-core (3.89.0)
18
19
  aws-eventstream (~> 1.0, >= 1.0.2)
19
- aws-partitions (~> 1.0)
20
+ aws-partitions (~> 1, >= 1.239.0)
20
21
  aws-sigv4 (~> 1.1)
21
22
  jmespath (~> 1.0)
22
- aws-sdk-dynamodb (1.31.0)
23
- aws-sdk-core (~> 3, >= 3.56.0)
23
+ aws-sdk-dynamodb (1.41.0)
24
+ aws-sdk-core (~> 3, >= 3.71.0)
24
25
  aws-sigv4 (~> 1.1)
25
26
  aws-sigv4 (1.1.0)
26
27
  aws-eventstream (~> 1.0, >= 1.0.2)
28
+ byebug (11.0.1)
27
29
  diff-lcs (1.3)
30
+ equatable (0.6.1)
28
31
  jmespath (1.4.0)
29
- rake (10.5.0)
30
- rspec (3.8.0)
31
- rspec-core (~> 3.8.0)
32
- rspec-expectations (~> 3.8.0)
33
- rspec-mocks (~> 3.8.0)
34
- rspec-core (3.8.0)
35
- rspec-support (~> 3.8.0)
36
- rspec-expectations (3.8.3)
32
+ pastel (0.7.3)
33
+ equatable (~> 0.6)
34
+ tty-color (~> 0.5)
35
+ rake (13.0.1)
36
+ rspec (3.9.0)
37
+ rspec-core (~> 3.9.0)
38
+ rspec-expectations (~> 3.9.0)
39
+ rspec-mocks (~> 3.9.0)
40
+ rspec-core (3.9.1)
41
+ rspec-support (~> 3.9.1)
42
+ rspec-expectations (3.9.0)
37
43
  diff-lcs (>= 1.2.0, < 2.0)
38
- rspec-support (~> 3.8.0)
39
- rspec-mocks (3.8.0)
44
+ rspec-support (~> 3.9.0)
45
+ rspec-mocks (3.9.1)
40
46
  diff-lcs (>= 1.2.0, < 2.0)
41
- rspec-support (~> 3.8.0)
42
- rspec-support (3.8.0)
47
+ rspec-support (~> 3.9.0)
48
+ rspec-support (3.9.2)
43
49
  thor (0.20.3)
50
+ tty-color (0.5.0)
51
+ tty-logger (0.1.0)
52
+ pastel (~> 0.7.0)
44
53
 
45
54
  PLATFORMS
46
55
  ruby
47
56
 
48
57
  DEPENDENCIES
49
58
  bundler (~> 1.17)
59
+ byebug
50
60
  dynamocli!
51
- rake (~> 10.0)
61
+ rake (~> 13.0)
52
62
  rspec (~> 3.0)
53
63
 
54
64
  BUNDLED WITH
data/README.md CHANGED
@@ -15,12 +15,15 @@ You have to configure AWS in your computer first. The program will use the AWS c
15
15
 
16
16
  - Import data from a CSV file to a DynamoDB table
17
17
 
18
+ If you have exported the CSV file you want to import from AWS DynamoDB console, you probaly want to modify the headers before importing the CSV file, because AWS exports the CSV file with a symbol indicating the type of the field in the header. You can pass the option `--exported-from-aws` to do that, the default is false.
19
+
18
20
  ```
19
21
  Usage:
20
22
  dynamocli import FILE -t, --table, --to=TABLE
21
23
 
22
24
  Options:
23
- -t, --table, --to=TABLE # table you want to import the data
25
+ -t, --table, --to=TABLE # table you want to import the data
26
+ [--exported-from-aws], [--no-exported-from-aws] # modify the headers before importing the csv
24
27
 
25
28
  Description:
26
29
  `dynamocli import` will import the data in from a file to a table specified.
@@ -56,6 +59,16 @@ From the DynamoDB Guidelines for Working with Tables documentation:
56
59
 
57
60
  > Deleting an entire table is significantly more efficient than removing items one-by-one, which essentially doubles the write throughput as you do as many delete operations as put operations.
58
61
 
62
+ ## Known Issues
63
+
64
+ ### Importing a CSV file with arrays and objects as values in it
65
+
66
+ Unfortunately, at this moment, this library cannot properly import array and objects. These values will appear as strings in the DynamoDB table.
67
+
68
+ ## Cross account or multiple profiles usage
69
+
70
+ You can run `dynamocli` passing the `AWS_PROFILE` environment variable with the profile you want to use, for example: `AWS_PROFILE=nondefaultprofile dynamocli erase users`.
71
+
59
72
  ## Development
60
73
 
61
74
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -18,14 +18,17 @@ Gem::Specification.new do |spec|
18
18
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
19
  end
20
20
  spec.bindir = "bin"
21
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.executables = spec.files.grep(%r{^bin/dynamocli}) { |f| File.basename(f) }
22
22
  spec.require_paths = ["lib"]
23
+ spec.required_ruby_version = ">= 2.3"
23
24
 
24
25
  spec.add_dependency "thor", "~> 0.20"
25
26
  spec.add_dependency "aws-sdk-dynamodb", "~> 1.28"
26
27
  spec.add_dependency "aws-sdk-cloudformation", "~> 1.23"
28
+ spec.add_dependency "tty-logger", "~> 0.1.0"
27
29
 
28
30
  spec.add_development_dependency "bundler", "~> 1.17"
29
- spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rake", "~> 13.0"
30
32
  spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "byebug"
31
34
  end
@@ -13,8 +13,9 @@ module Dynamocli
13
13
  > $ dynamo import users.csv --to users
14
14
  LONGDESC
15
15
  option :to, required: true, desc: "table you want to import the data", banner: "TABLE", aliases: ["-t", "--table"]
16
+ option "exported-from-aws", desc: "modify the headers before importing the csv", type: :boolean
16
17
  def import(file)
17
- Dynamocli::Import.new(file: file, table: options[:to]).start
18
+ Dynamocli::Import.new(file: file, table: options[:to], exported_from_aws: options["exported-from-aws"]).start
18
19
  end
19
20
 
20
21
  desc "erase TABLE", "erase all the data from the DynamoDB TABLE"
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "json"
5
+ require "yaml"
6
+ require "aws-sdk-cloudformation"
7
+ require "tty-logger"
8
+
9
+ module Dynamocli::AWS
10
+ class Stack
11
+ attr_reader :name, :resources, :template_body, :original_template, :template_without_table, :policy_body
12
+
13
+ extend Forwardable
14
+ def_delegators :stack_on_aws,
15
+ :parameters, :capabilities, :role_arn, :rollback_configuration, :notification_arns, :tags
16
+
17
+ def initialize(table_name:, table_resource:, cloudformation: nil, logger: nil)
18
+ @table_name = table_name
19
+ @table_resource = table_resource
20
+ @cloudformation = cloudformation || CLOUDFORMARTION.new
21
+ @logger = logger || LOGGER.new
22
+
23
+ set_attributes_now_because_they_will_change
24
+ end
25
+
26
+ def deploying?
27
+ current_status != DEPLOY_COMPLETED_KEY
28
+ end
29
+
30
+ private
31
+
32
+ CLOUDFORMARTION = Aws::CloudFormation::Client
33
+ LOGGER = TTY::Logger
34
+ DEPLOY_COMPLETED_KEY = "UPDATE_COMPLETE"
35
+ private_constant :CLOUDFORMARTION, :LOGGER, :DEPLOY_COMPLETED_KEY
36
+
37
+ attr_reader :table_name, :table_resource, :cloudformation, :logger, :stack_on_aws
38
+
39
+ def set_attributes_now_because_they_will_change
40
+ set_name
41
+ set_stack_on_aws
42
+ set_resources
43
+ set_template_body
44
+ set_original_template
45
+ set_template_without_table
46
+ set_policy_body
47
+ end
48
+
49
+ def set_name
50
+ @name ||= table_resource[:stack_name]
51
+ end
52
+
53
+ def set_stack_on_aws
54
+ @stack_on_aws ||= cloudformation.describe_stacks(stack_name: name)[0][0]
55
+ end
56
+
57
+ def set_resources
58
+ @resources ||= cloudformation.describe_stack_resources(physical_resource_id: table_name).to_h
59
+ end
60
+
61
+ def set_template_body
62
+ @template_body ||= cloudformation.get_template(stack_name: name).to_h[:template_body]
63
+ end
64
+
65
+ def set_original_template
66
+ @original_template ||= parse_template(template_body)
67
+ end
68
+
69
+ def set_template_without_table
70
+ @template_without_table ||= parse_template(template_body).tap do |template_without_table|
71
+ tables = original_template["Resources"].select { |_, v| v["Type"] == "AWS::DynamoDB::Table" }
72
+ table = tables.find { |_, v| v["Properties"]["TableName"] == table_name }
73
+
74
+ if tables.nil?
75
+ logger.error("table #{table_name} not found in the #{@name} stack")
76
+ exit(42)
77
+ end
78
+
79
+ logical_resource_id = table.first
80
+ template_without_table["Resources"].delete(logical_resource_id)
81
+ end
82
+ end
83
+
84
+ def parse_template(template)
85
+ JSON.parse(template)
86
+ rescue JSON::ParserError
87
+ YAML.load(template)
88
+ end
89
+
90
+ def set_policy_body
91
+ @policy_body ||= cloudformation.get_stack_policy(stack_name: name).stack_policy_body
92
+ end
93
+
94
+ def current_status
95
+ cloudformation.describe_stacks(stack_name: name)[0][0].stack_status
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "aws-sdk-dynamodb"
5
+
6
+ module Dynamocli::AWS
7
+ class Table
8
+ attr_reader :schema
9
+
10
+ extend Forwardable
11
+ def_delegators :table_on_aws, :delete
12
+
13
+ def initialize(table_name:, table_on_aws:, dynamodb: nil)
14
+ @table_name = table_name
15
+ @table_on_aws = table_on_aws
16
+ @dynamodb = dynamodb || DYNAMODB.new
17
+
18
+ set_schema_before_we_delete_the_table
19
+ end
20
+
21
+ def deleting?
22
+ status == DELETION_IN_PROCESSING_KEY
23
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ DYNAMODB = Aws::DynamoDB::Client
30
+ DELETION_IN_PROCESSING_KEY = "DELETING"
31
+ private_constant :DYNAMODB, :DELETION_IN_PROCESSING_KEY
32
+
33
+ attr_reader :table_name, :table_on_aws, :dynamodb
34
+
35
+ def status
36
+ dynamodb.describe_table(table_name: table_name).table.table_status
37
+ end
38
+
39
+ def set_schema_before_we_delete_the_table
40
+ @schema ||= dynamodb.describe_table(table_name: table_name).to_h[:table].tap do |schema|
41
+ schema.delete(:table_status)
42
+ schema.delete(:creation_date_time)
43
+ schema.delete(:table_size_bytes)
44
+ schema.delete(:item_count)
45
+ schema.delete(:table_arn)
46
+ schema.delete(:table_id)
47
+ schema[:provisioned_throughput]&.delete(:number_of_decreases_today)
48
+ schema[:local_secondary_indexes]&.each do |lsi|
49
+ lsi.delete(:index_status)
50
+ lsi.delete(:index_size_bytes)
51
+ lsi.delete(:item_count)
52
+ lsi.delete(:index_arn)
53
+ end
54
+ schema[:global_secondary_indexes]&.each do |gsi|
55
+ gsi.delete(:index_status)
56
+ gsi.delete(:index_size_bytes)
57
+ gsi.delete(:item_count)
58
+ gsi.delete(:index_arn)
59
+ gsi[:provisioned_throughput].delete(:number_of_decreases_today)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "yaml"
3
+ require "tty-logger"
5
4
  require "aws-sdk-dynamodb"
6
5
  require "aws-sdk-cloudformation"
6
+ require "dynamocli/table/cloudformation_table"
7
+ require "dynamocli/table/standalone_table"
8
+ require "dynamocli/aws/stack"
9
+ require "dynamocli/aws/table"
7
10
 
8
11
  class Dynamocli::Erase
9
12
  def initialize(table_name:, with_drift: false)
@@ -12,219 +15,70 @@ class Dynamocli::Erase
12
15
 
13
16
  @dynamodb = Aws::DynamoDB::Client.new
14
17
  @cloudformation = Aws::CloudFormation::Client.new
15
- @table = Aws::DynamoDB::Table.new(@table_name)
16
-
17
- set_schema
18
+ @table_on_aws = Aws::DynamoDB::Table.new(@table_name)
18
19
 
19
20
  @stack_resources = @cloudformation.describe_stack_resources(physical_resource_id: @table_name).to_h
20
-
21
- set_stack_information
22
- rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e
23
- STDERR.puts "ERROR: #{e.message}"
24
- exit(42)
25
21
  rescue Aws::CloudFormation::Errors::ValidationError
26
22
  @stack_resources = nil
27
23
  end
28
24
 
29
25
  def start
30
26
  erase_table
31
- rescue Aws::CloudFormation::Errors::ValidationError => e
32
- STDERR.puts "ERROR: #{e.message}"
27
+ rescue Aws::CloudFormation::Errors::ValidationError,
28
+ Aws::DynamoDB::Errors::ValidationException,
29
+ Aws::DynamoDB::Errors::ResourceNotFoundException => e
30
+ LOGGER.error(e.message)
33
31
  exit(42)
34
32
  end
35
33
 
36
34
  private
37
35
 
38
- def set_schema
39
- @schema = @dynamodb.describe_table(table_name: @table_name).to_h[:table].tap do |schema|
40
- schema.delete(:table_status)
41
- schema.delete(:creation_date_time)
42
- schema.delete(:table_size_bytes)
43
- schema.delete(:item_count)
44
- schema.delete(:table_arn)
45
- schema.delete(:table_id)
46
- schema[:provisioned_throughput].delete(:number_of_decreases_today)
47
- end
48
- end
49
-
50
- def set_stack_information
51
- return if @stack_resources.nil?
52
-
53
- set_stack_name
54
- set_stack
55
- set_templates
56
- rescue Aws::CloudFormation::Errors::ValidationError => e
57
- STDERR.puts "ERROR: #{e.message}"
58
- exit(42)
59
- end
60
-
61
- def set_stack_name
62
- table_resource = @stack_resources[:stack_resources].find do |resource|
63
- resource[:physical_resource_id] == @table_name
64
- end
65
- @stack_name = table_resource[:stack_name]
66
- end
67
-
68
- def set_stack
69
- @stack = @cloudformation.describe_stacks(stack_name: @stack_name)[0][0]
70
- end
71
-
72
- def set_templates
73
- template_body = @cloudformation.get_template(stack_name: @stack_name).to_h[:template_body]
74
- @original_template = parse_template(template_body)
75
- @template_without_table = parse_template(template_body)
36
+ LOGGER = TTY::Logger.new
37
+ private_constant :LOGGER
76
38
 
77
- tables = @original_template["Resources"].select { |_, v| v["Type"] == "AWS::DynamoDB::Table" }
78
- table = tables.find { |_, v| v["Properties"]["TableName"] == @table_name }
79
-
80
- if tables.nil?
81
- STDERR.puts "ERROR: table #{@table_name} not found in the #{@stack_name} stack"
82
- exit(42)
83
- end
84
-
85
- logical_resource_id = table.first
86
- @template_without_table["Resources"].delete(logical_resource_id)
87
- end
88
-
89
- def parse_template(template)
90
- JSON.parse(template)
91
- rescue JSON::ParserError
92
- YAML.load(template)
93
- end
39
+ attr_reader :table_name, :table_on_aws, :stack_resources
94
40
 
95
41
  def erase_table
96
- if @stack_resources.nil? || @with_drift
97
- check_if_user_wants_to_continue_with_recreation
98
- delete_and_recreate_the_table
99
- else
100
- check_if_user_wants_to_continue_with_deployment
101
- erase_table_through_cloudformation
102
- end
42
+ check_if_user_wants_to_continue
43
+ dynamocli_table.erase
103
44
  end
104
45
 
105
- def check_if_user_wants_to_continue_with_recreation
106
- STDOUT.print(
107
- "WARNING: You're going to drop and recreate your #{@table_name} table,\n" \
108
- "do you really want to continue?\n" \
109
- "(anything other than 'y' will cancel) > "
46
+ def check_if_user_wants_to_continue
47
+ LOGGER.warn(
48
+ "#{dynamocli_table.alert_message_before_continue} " \
49
+ "Do you really want to continue?"
110
50
  )
51
+ STDOUT.print("(anything other than 'y' will cancel) > ")
111
52
 
112
53
  confirmation = STDIN.gets.strip
113
54
  return if confirmation == "y"
114
55
 
115
- STDOUT.puts abort_message
56
+ LOGGER.info(abort_message)
116
57
  exit(0)
117
58
  end
118
59
 
119
60
  def abort_message
120
- "INFO: Erase of #{@table_name} table canceled"
121
- end
122
-
123
- def delete_and_recreate_the_table
124
- delete_table
125
- wait_for_deletion_to_complete
126
- create_table
127
- end
128
-
129
- def delete_table
130
- STDOUT.puts "INFO: Deleting the #{@table_name} table"
131
-
132
- @table.delete
133
-
134
- STDOUT.puts "INFO: #{@table_name} table deleted"
61
+ "Erase of #{@table_name} table canceled"
135
62
  end
136
63
 
137
- def wait_for_deletion_to_complete
138
- waiting_seconds = 0
139
- while get_table_status == "DELETING"
140
- STDOUT.puts "INFO: Waiting for deletion to complete"
141
- sleep waiting_seconds += 1
142
- end
143
- rescue Aws::DynamoDB::Errors::ResourceNotFoundException
144
- true
145
- end
146
-
147
- def get_table_status
148
- @dynamodb.describe_table(table_name: @table_name).table.table_status
149
- end
150
-
151
- def create_table
152
- STDOUT.puts "INFO: Creating the #{@table_name} table"
153
-
154
- @dynamodb.create_table(@schema)
155
-
156
- STDOUT.puts "INFO: #{@table_name} table created"
157
- end
158
-
159
- def check_if_user_wants_to_continue_with_deployment
160
- STDOUT.print(
161
- "WARNING: You are going to deploy and redeploy your #{@stack_name} stack\n" \
162
- "to drop and recreate the #{@table_name} table, do you really want to continue?\n" \
163
- "(anything other than 'y' will cancel) > "
164
- )
165
-
166
- confirmation = STDIN.gets.strip
167
- return if confirmation == "y"
168
-
169
- STDOUT.puts abort_message
170
- exit(0)
64
+ def dynamocli_table
65
+ @dynamocli_table ||=
66
+ if stack_resources.nil? || with_drift?
67
+ table = Dynamocli::AWS::Table.new(table_name: table_name, table_on_aws: table_on_aws)
68
+ Dynamocli::Table::StandaloneTable.new(table_name: table_name, table: table)
69
+ else
70
+ stack = Dynamocli::AWS::Stack.new(table_name: table_name, table_resource: table_resource)
71
+ Dynamocli::Table::CloudformationTable.new(table_name: table_name, stack: stack)
72
+ end
171
73
  end
172
74
 
173
- def erase_table_through_cloudformation
174
- deploy_stack_without_the_table
175
- wait_for_deployment_to_complete
176
- deploy_stack_with_the_original_template
75
+ def with_drift?
76
+ @with_drift
177
77
  end
178
78
 
179
- def deploy_stack_without_the_table
180
- STDOUT.puts "INFO: Deploying the stack without the #{@table_name} table"
181
-
182
- @cloudformation.update_stack(
183
- stack_name: @stack_name,
184
- template_body: @template_without_table.to_json,
185
- parameters: @stack.parameters.map(&:to_h),
186
- capabilities: @stack.capabilities,
187
- role_arn: @stack.role_arn,
188
- rollback_configuration: @stack.rollback_configuration.to_h,
189
- stack_policy_body: get_stack_policy_body,
190
- notification_arns: @stack.notification_arns,
191
- tags: @stack.tags.map(&:to_h)
192
- )
193
-
194
- STDOUT.puts "INFO: Stack deployed without the #{@table_name} table"
195
- end
196
-
197
- def get_stack_policy_body
198
- @cloudformation.get_stack_policy(stack_name: @stack_name).stack_policy_body
199
- end
200
-
201
- def wait_for_deployment_to_complete
202
- waiting_seconds = 0
203
- while get_stack_status != "UPDATE_COMPLETE"
204
- STDOUT.puts "INFO: Waiting for deployment to complete"
205
- sleep waiting_seconds += 1
79
+ def table_resource
80
+ @table_resource ||= stack_resources[:stack_resources].find do |resource|
81
+ resource[:physical_resource_id] == @table_name
206
82
  end
207
83
  end
208
-
209
- def get_stack_status
210
- @cloudformation.describe_stacks(stack_name: @stack_name)[0][0].stack_status
211
- end
212
-
213
- def deploy_stack_with_the_original_template
214
- STDOUT.puts "INFO: Deploying the stack with the #{@table_name} table"
215
-
216
- @cloudformation.update_stack(
217
- stack_name: @stack_name,
218
- template_body: @original_template.to_json,
219
- parameters: @stack.parameters.map(&:to_h),
220
- capabilities: @stack.capabilities,
221
- role_arn: @stack.role_arn,
222
- rollback_configuration: @stack.rollback_configuration.to_h,
223
- stack_policy_body: get_stack_policy_body,
224
- notification_arns: @stack.notification_arns,
225
- tags: @stack.tags.map(&:to_h)
226
- )
227
-
228
- STDOUT.puts "INFO: Stack deployed with the #{@table_name} table"
229
- end
230
84
  end
@@ -1,42 +1,90 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "csv"
4
+ require "tty-logger"
2
5
  require "aws-sdk-dynamodb"
3
6
 
4
7
  class Dynamocli::Import
8
+ LOGGER = TTY::Logger.new
5
9
  SUPPORTED_FILE_FORMATS = ["CSV"]
6
10
 
7
- def initialize(file:, table:)
11
+ def initialize(file:, table:, exported_from_aws: false)
8
12
  @file = file
9
13
  @table = table
14
+ @exported_from_aws = exported_from_aws
15
+ @dynamodb = Aws::DynamoDB::Client.new
10
16
  end
11
17
 
12
18
  def start
13
19
  records = get_records
14
20
  write_records_to_dynamodb_table(records)
21
+ LOGGER.success("#{records.size} record#{"s" if records.size != 1} imported to #{table}")
22
+ rescue Aws::DynamoDB::Errors::ValidationException => e
23
+ LOGGER.error(e.message)
24
+ exit(42)
15
25
  end
16
26
 
17
27
  private
18
28
 
29
+ attr_reader :file, :table, :dynamodb
30
+
19
31
  def get_records
20
- extension = File.extname(@file)
32
+ extension = File.extname(file)
21
33
 
22
34
  case extension
23
35
  when ".csv"
24
- records_from_csv(@file)
36
+ records_from_csv(file)
25
37
  else
26
- STDERR.puts "ERROR: Not supported file format. Only supported file formats are: #{SUPPORTED_FILE_FORMATS}"
38
+ LOGGER.error("Not supported file format. Only supported file formats are: #{SUPPORTED_FILE_FORMATS}")
27
39
  exit(42)
28
40
  end
29
41
  end
30
42
 
31
43
  def records_from_csv(csv)
32
- csv_options = { encoding: "UTF-8", headers: true, header_converters: :symbol, converters: :all }
44
+ set_custom_converter_for_csv
45
+ csv_options = { encoding: "UTF-8", headers: true, converters: :attribute_definitions }
33
46
  records_csv = CSV.read(csv, csv_options)
34
- records_csv.map(&:to_hash)
47
+ if exported_from_aws?
48
+ transform_records_csv_from_aws(records_csv)
49
+ else
50
+ records_csv.map(&:to_hash)
51
+ end
35
52
  end
36
53
 
37
- def write_records_to_dynamodb_table(records)
38
- dynamodb = Aws::DynamoDB::Client.new
54
+ def exported_from_aws?
55
+ @exported_from_aws
56
+ end
57
+
58
+ # When the CSV comes from AWS, the header of the CSV is like this: "email (S)".
59
+ # However, we cannot import the CSV with the header like this, we have to remove
60
+ # the part that specifies the type of the field before importing it.
61
+ RANGE_TO_REMOVE_TYPE_FROM_HEADER = (0..-5)
62
+ private_constant :RANGE_TO_REMOVE_TYPE_FROM_HEADER
63
+ def transform_records_csv_from_aws(records_csv)
64
+ records_csv.map do |record_csv|
65
+ record = record_csv.to_h
39
66
 
67
+ record.each_with_object({}) do |(key, value), records|
68
+ records[key[RANGE_TO_REMOVE_TYPE_FROM_HEADER]] = value
69
+ end
70
+ end
71
+ end
72
+
73
+ ATTRIBUTE_TYPES_CONVERTERS = {
74
+ "S" => :to_s.to_proc,
75
+ "N" => :to_i.to_proc,
76
+ "B" => Proc.new(&StringIO.method(:new))
77
+ }
78
+ def set_custom_converter_for_csv
79
+ attribute_definitions = dynamodb.describe_table(table_name: table).table.attribute_definitions
80
+ CSV::Converters[:attribute_definitions] = lambda do |value, info|
81
+ attribute_definition = attribute_definitions.find { |it| it.attribute_name == info.header }
82
+ return value if attribute_definition.nil?
83
+ ATTRIBUTE_TYPES_CONVERTERS[attribute_definition.attribute_type].call(value)
84
+ end
85
+ end
86
+
87
+ def write_records_to_dynamodb_table(records)
40
88
  slice_items_to_attend_batch_write_limit(records).each do |items|
41
89
  dynamodb.batch_write_item(request_items: format_request_items(items))
42
90
  end
@@ -48,6 +96,6 @@ class Dynamocli::Import
48
96
  end
49
97
 
50
98
  def format_request_items(items)
51
- { @table => items.map { |item| { put_request: { item: item } } } }
99
+ { table => items.map { |item| { put_request: { item: item } } } }
52
100
  end
53
101
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-cloudformation"
4
+ require "tty-logger"
5
+
6
+ module Dynamocli::Table
7
+ class CloudformationTable
8
+ def initialize(table_name:, stack:, cloudformation: nil, logger: nil)
9
+ @table_name = table_name
10
+ @stack = stack
11
+ @cloudformation = cloudformation || CLOUDFORMARTION.new
12
+ @logger = logger || LOGGER.new
13
+ end
14
+
15
+ def alert_message_before_continue
16
+ "You're going to deploy and redeploy your #{stack.name} stack to drop and recreate the #{@table_name} table!"
17
+ end
18
+
19
+ def erase
20
+ deploy_stack_without_the_table
21
+ wait_for_deployment_to_complete
22
+ deploy_stack_with_the_original_template
23
+ end
24
+
25
+ private
26
+
27
+ CLOUDFORMARTION = Aws::CloudFormation::Client
28
+ LOGGER = TTY::Logger
29
+ private_constant :CLOUDFORMARTION, :LOGGER
30
+
31
+ attr_reader :table_name, :stack, :cloudformation, :logger
32
+
33
+ def deploy_stack_without_the_table
34
+ logger.info("Deploying the stack without the #{table_name} table")
35
+
36
+ cloudformation.update_stack(
37
+ stack_name: stack.name,
38
+ template_body: stack.template_without_table.to_json,
39
+ parameters: stack.parameters.map(&:to_h),
40
+ capabilities: stack.capabilities,
41
+ role_arn: stack.role_arn,
42
+ rollback_configuration: stack.rollback_configuration.to_h,
43
+ stack_policy_body: stack.policy_body,
44
+ notification_arns: stack.notification_arns,
45
+ tags: stack.tags.map(&:to_h)
46
+ )
47
+
48
+ logger.success("Stack deployed without the #{table_name} table")
49
+ end
50
+
51
+ def wait_for_deployment_to_complete
52
+ waiting_seconds = 0
53
+ while stack.deploying?
54
+ logger.info("Waiting for deployment to complete")
55
+ sleep waiting_seconds += 1
56
+ end
57
+ end
58
+
59
+ def deploy_stack_with_the_original_template
60
+ logger.info("Deploying the stack with the #{table_name} table")
61
+
62
+ cloudformation.update_stack(
63
+ stack_name: stack.name,
64
+ template_body: stack.original_template.to_json,
65
+ parameters: stack.parameters.map(&:to_h),
66
+ capabilities: stack.capabilities,
67
+ role_arn: stack.role_arn,
68
+ rollback_configuration: stack.rollback_configuration.to_h,
69
+ stack_policy_body: stack.policy_body,
70
+ notification_arns: stack.notification_arns,
71
+ tags: stack.tags.map(&:to_h)
72
+ )
73
+
74
+ logger.success("Stack deployed with the #{table_name} table")
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-dynamodb"
4
+ require "tty-logger"
5
+
6
+ module Dynamocli::Table
7
+ class StandaloneTable
8
+ def initialize(table_name:, table:, dynamodb: nil, logger: nil)
9
+ @table_name = table_name
10
+ @table = table
11
+ @dynamodb = dynamodb || DYNAMODB.new
12
+ @logger = logger || LOGGER.new
13
+ end
14
+
15
+ def alert_message_before_continue
16
+ "You're going to drop and recreate your #{@table_name} table!"
17
+ end
18
+
19
+ def erase
20
+ delete_table
21
+ wait_for_deletion_to_complete
22
+ create_table
23
+ end
24
+
25
+ private
26
+
27
+ LOGGER = TTY::Logger
28
+ DYNAMODB = Aws::DynamoDB::Client
29
+ private_constant :LOGGER, :DYNAMODB
30
+
31
+ attr_reader :table_name, :table, :dynamodb, :logger
32
+
33
+ def delete_table
34
+ logger.info("Deleting the #{table_name} table")
35
+
36
+ table.delete
37
+
38
+ logger.success("#{table_name} table deleted")
39
+ end
40
+
41
+ def wait_for_deletion_to_complete
42
+ waiting_seconds = 0
43
+ while table.deleting?
44
+ logger.info("Waiting for deletion to complete")
45
+ sleep waiting_seconds += 1
46
+ end
47
+ end
48
+
49
+ def create_table
50
+ logger.info("Creating the #{table_name} table")
51
+
52
+ dynamodb.create_table(table.schema)
53
+
54
+ logger.success("#{table_name} table created")
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dynamocli
2
- VERSION = "0.1.3"
4
+ VERSION = "0.1.8"
3
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamocli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matheus Silva Santos de Oliveira
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-20 00:00:00.000000000 Z
11
+ date: 2020-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.23'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-logger
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.0
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +86,14 @@ dependencies:
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: '10.0'
89
+ version: '13.0'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: '10.0'
96
+ version: '13.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rspec
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -94,13 +108,25 @@ dependencies:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '3.0'
97
- description:
111
+ - !ruby/object:Gem::Dependency
112
+ name: byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
98
126
  email:
99
127
  - oliveira.matheussilvasantos@gmail.com
100
128
  executables:
101
- - console
102
129
  - dynamocli
103
- - setup
104
130
  extensions: []
105
131
  extra_rdoc_files: []
106
132
  files:
@@ -119,14 +145,18 @@ files:
119
145
  - bin/setup
120
146
  - dynamocli.gemspec
121
147
  - lib/dynamocli.rb
148
+ - lib/dynamocli/aws/stack.rb
149
+ - lib/dynamocli/aws/table.rb
122
150
  - lib/dynamocli/erase.rb
123
151
  - lib/dynamocli/import.rb
152
+ - lib/dynamocli/table/cloudformation_table.rb
153
+ - lib/dynamocli/table/standalone_table.rb
124
154
  - lib/dynamocli/version.rb
125
155
  homepage: https://github.com/matheussilvasantos/dynamocli
126
156
  licenses:
127
157
  - MIT
128
158
  metadata: {}
129
- post_install_message:
159
+ post_install_message:
130
160
  rdoc_options: []
131
161
  require_paths:
132
162
  - lib
@@ -134,15 +164,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
134
164
  requirements:
135
165
  - - ">="
136
166
  - !ruby/object:Gem::Version
137
- version: '0'
167
+ version: '2.3'
138
168
  required_rubygems_version: !ruby/object:Gem::Requirement
139
169
  requirements:
140
170
  - - ">="
141
171
  - !ruby/object:Gem::Version
142
172
  version: '0'
143
173
  requirements: []
144
- rubygems_version: 3.0.3
145
- signing_key:
174
+ rubygems_version: 3.1.2
175
+ signing_key:
146
176
  specification_version: 4
147
177
  summary: Utilities for interaction with AWS DynamoDB
148
178
  test_files: []