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 +4 -4
- data/CHANGELOG.md +33 -0
- data/Gemfile.lock +31 -21
- data/README.md +14 -1
- data/dynamocli.gemspec +5 -2
- data/lib/dynamocli.rb +2 -1
- data/lib/dynamocli/aws/stack.rb +98 -0
- data/lib/dynamocli/aws/table.rb +64 -0
- data/lib/dynamocli/erase.rb +36 -182
- data/lib/dynamocli/import.rb +57 -9
- data/lib/dynamocli/table/cloudformation_table.rb +77 -0
- data/lib/dynamocli/table/standalone_table.rb +57 -0
- data/lib/dynamocli/version.rb +3 -1
- metadata +42 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1aafa6a05574f7dd3cf4b1dd979a9ca5469a0ab542b5201573c4a4e723c9bbf2
|
4
|
+
data.tar.gz: '011871424ef1cfff30254cbe834ca4bce362b1100928acd029911dc5eae8cff9'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2eb2d1d4e025cca2424aa8c93624ec831cf3b2a14c2cb48e9ef743c273ecd37fd52c79090627fa81d027f4bdd19d6c942bd4f5db2508939b8072e9e091606715
|
7
|
+
data.tar.gz: 3b73649c0163a4a3fdeafd00805e27f6bedc172c38bd96b8f5385ff4c7ad808d993b7083737d1d2c667484d0f6f673554f06e481c58e3fb226577317ae752e17
|
data/CHANGELOG.md
CHANGED
@@ -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
|
data/Gemfile.lock
CHANGED
@@ -1,54 +1,64 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dynamocli (0.1.
|
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.
|
14
|
-
aws-sdk-cloudformation (1.
|
15
|
-
aws-sdk-core (~> 3, >= 3.
|
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.
|
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.
|
23
|
-
aws-sdk-core (~> 3, >= 3.
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
rspec-
|
36
|
-
|
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.
|
39
|
-
rspec-mocks (3.
|
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.
|
42
|
-
rspec-support (3.
|
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 (~>
|
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
|
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.
|
data/dynamocli.gemspec
CHANGED
@@ -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", "~>
|
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
|
data/lib/dynamocli.rb
CHANGED
@@ -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
|
data/lib/dynamocli/erase.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
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
|
-
@
|
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
|
32
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
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
|
106
|
-
|
107
|
-
"
|
108
|
-
"
|
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
|
-
|
56
|
+
LOGGER.info(abort_message)
|
116
57
|
exit(0)
|
117
58
|
end
|
118
59
|
|
119
60
|
def abort_message
|
120
|
-
"
|
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
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
174
|
-
|
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
|
180
|
-
|
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
|
data/lib/dynamocli/import.rb
CHANGED
@@ -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(
|
32
|
+
extension = File.extname(file)
|
21
33
|
|
22
34
|
case extension
|
23
35
|
when ".csv"
|
24
|
-
records_from_csv(
|
36
|
+
records_from_csv(file)
|
25
37
|
else
|
26
|
-
|
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
|
-
|
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
|
-
|
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
|
38
|
-
|
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
|
-
{
|
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
|
data/lib/dynamocli/version.rb
CHANGED
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.
|
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:
|
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: '
|
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: '
|
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
|
-
|
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: '
|
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.
|
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: []
|