dyna 0.1.1

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.
data/lib/dyna/dsl.rb ADDED
@@ -0,0 +1,54 @@
1
+ module Dyna
2
+ class DSL
3
+ include Dyna::TemplateHelper
4
+
5
+ class << self
6
+ def define(source, path)
7
+ self.new(path) do
8
+ eval(source, binding, path)
9
+ end
10
+ end
11
+
12
+ def convert(region, exported)
13
+ Converter.convert(region, exported)
14
+ end
15
+ end
16
+
17
+ attr_reader :result
18
+
19
+ def initialize(path, &block)
20
+ @path = path
21
+ @result = OpenStruct.new(:ddbs => {})
22
+
23
+ @context = Hashie::Mash.new(
24
+ :path => path,
25
+ :templates => {},
26
+ )
27
+
28
+ instance_eval(&block)
29
+ end
30
+
31
+ private
32
+ def template(name, &block)
33
+ @context.templates[name.to_s] = block
34
+ end
35
+
36
+ def require(file)
37
+ tablefile = (file =~ %r|\A/|) ? file : File.expand_path(File.join(File.dirname(@path), file))
38
+
39
+ if File.exist?(tablefile)
40
+ instance_eval(File.read(tablefile), tablefile)
41
+ elsif File.exist?(tablefile + '.rb')
42
+ instance_eval(File.read(tablefile + '.rb'), tablefile + '.rb')
43
+ else
44
+ Kernel.require(file)
45
+ end
46
+ end
47
+
48
+ def dynamo_db(region, &block)
49
+ ddb = @result.ddbs[region]
50
+ tables = ddb ? ddb.tables : []
51
+ @result.ddbs[region] = DynamoDB.new(@context, tables, &block).result
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,105 @@
1
+ module Dyna
2
+ class Exporter
3
+ include Filterable
4
+
5
+ class << self
6
+ def export(ddb, options = {})
7
+ self.new(ddb, options).export
8
+ end
9
+ end
10
+
11
+ def initialize(ddb, options = {})
12
+ @ddb = ddb
13
+ @options = options
14
+ end
15
+
16
+ def export
17
+ @ddb.list_tables.table_names
18
+ .reject { |name| should_skip(name) }
19
+ .sort
20
+ .each_with_object({}) do |table_name, result|
21
+ result[table_name] = self.class.export_table(@ddb, table_name)
22
+ end
23
+ end
24
+
25
+ def self.table_definition(describe_table)
26
+ {
27
+ table_name: describe_table.table_name,
28
+ key_schema: key_schema(describe_table),
29
+ attribute_definitions: attribute_definitions(describe_table),
30
+ provisioned_throughput: {
31
+ read_capacity_units: describe_table.provisioned_throughput.read_capacity_units,
32
+ write_capacity_units: describe_table.provisioned_throughput.write_capacity_units,
33
+ },
34
+ local_secondary_indexes: local_secondary_indexes(describe_table),
35
+ global_secondary_indexes: global_secondary_indexes(describe_table),
36
+ stream_specification: stream_specification(describe_table),
37
+ }
38
+ end
39
+
40
+ private
41
+ def self.export_table(ddb, table_name)
42
+ describe_table = ddb.describe_table(table_name: table_name).table
43
+ table_definition(describe_table)
44
+ end
45
+
46
+ def self.key_schema(table)
47
+ table.key_schema.map do |schema|
48
+ {
49
+ attribute_name: schema.attribute_name,
50
+ key_type: schema.key_type,
51
+ }
52
+ end
53
+ end
54
+
55
+ def self.attribute_definitions(table)
56
+ table.attribute_definitions.map do |definition|
57
+ {
58
+ attribute_name: definition.attribute_name,
59
+ attribute_type: definition.attribute_type,
60
+ }
61
+ end
62
+ end
63
+
64
+ def self.global_secondary_indexes(table)
65
+ return nil unless table.global_secondary_indexes
66
+ table.global_secondary_indexes.map do |index|
67
+ {
68
+ index_name: index.index_name,
69
+ key_schema: key_schema(index),
70
+ projection: {
71
+ projection_type: index.projection.projection_type,
72
+ non_key_attributes: index.projection.non_key_attributes,
73
+ },
74
+ provisioned_throughput: {
75
+ read_capacity_units: index.provisioned_throughput.read_capacity_units,
76
+ write_capacity_units: index.provisioned_throughput.write_capacity_units,
77
+ },
78
+ }
79
+ end
80
+ end
81
+
82
+ def self.local_secondary_indexes(table)
83
+ return nil unless table.local_secondary_indexes
84
+ table.local_secondary_indexes.map do |index|
85
+ {
86
+ index_name: index.index_name,
87
+ key_schema: key_schema(index),
88
+ projection: {
89
+ projection_type: index.projection.projection_type,
90
+ non_key_attributes: index.projection.non_key_attributes,
91
+ },
92
+ }
93
+ end
94
+ end
95
+
96
+ def self.stream_specification(table)
97
+ stream_spec = table.stream_specification
98
+ return nil unless stream_spec
99
+ {
100
+ stream_enabled: stream_spec.stream_enabled,
101
+ stream_view_type: stream_spec.stream_view_type,
102
+ }
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,12 @@
1
+ class Hash
2
+ def symbolize_keys
3
+ self.each_with_object({}) do |(k, v), h|
4
+ h[k.to_s.to_sym] = (v.is_a?(Hash) ? v.symbolize_keys : v)
5
+ if v.is_a?(Array)
6
+ h[k.to_s.to_sym] = v.each_with_object([]) do |h2, a|
7
+ a << (h2.is_a?(Hash) ? h2.symbolize_keys : h2)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ class String
2
+ @@colorize = false
3
+
4
+ class << self
5
+ def colorize=(value)
6
+ @@colorize = value
7
+ end
8
+
9
+ def colorize
10
+ @@colorize
11
+ end
12
+ end # of class methods
13
+
14
+ Term::ANSIColor::Attribute.named_attributes.map do |attribute|
15
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
16
+ def #{attribute.name}
17
+ if @@colorize
18
+ Term::ANSIColor.send(#{attribute.name.inspect}, self)
19
+ else
20
+ self
21
+ end
22
+ end
23
+ EOS
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ module Dyna
2
+ module Filterable
3
+ def should_skip(table_name)
4
+ if @options.table_names
5
+ unless @options.table_names.include?(table_name)
6
+ log(:debug, "skip table(with tables_names option) #{table_name}")
7
+ return true
8
+ end
9
+ end
10
+
11
+ if @options.exclude_table_names
12
+ if @options.exclude_table_names.any? {|regex| table_name =~ regex}
13
+ log(:debug, "skip table(with exclude_tables_names option) #{table_name}")
14
+ return true
15
+ end
16
+ end
17
+
18
+ false
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ module Dyna
2
+ class Logger < ::Logger
3
+ include Singleton
4
+
5
+ def initialize
6
+ super($stdout)
7
+
8
+ self.formatter = proc do |severity, datetime, progname, msg|
9
+ "#{msg}\n"
10
+ end
11
+
12
+ self.level = Logger::INFO
13
+ end
14
+
15
+ def set_debug(value)
16
+ self.level = value ? Logger::DEBUG : Logger::INFO
17
+ end
18
+
19
+ module ClientHelper
20
+ def log(level, message, color, log_id = nil)
21
+ message = "[#{level.to_s.upcase}] #{message}" unless level == :info
22
+ message << ": #{log_id}" if log_id
23
+ message << ' (dry-run)' if @options && @options.dry_run
24
+ logger = (@options && @options.logger) || Dyna::Logger.instance
25
+ message = message.send(color) if color
26
+ logger.send(level, message)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ module Dyna
2
+ module TemplateHelper
3
+ def include_template(template_name, context = {})
4
+ tmplt = @context.templates[template_name.to_s]
5
+
6
+ unless tmplt
7
+ raise "Template `#{template_name}` is not defined"
8
+ end
9
+
10
+ context_orig = @context
11
+ @context = @context.merge(context)
12
+ instance_eval(&tmplt)
13
+ @context = context_orig
14
+ end
15
+
16
+ def context
17
+ @context
18
+ end
19
+ end
20
+ end
data/lib/dyna/utils.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Dyna
2
+ class Utils
3
+ class << self
4
+ def diff(obj1, obj2, options = {})
5
+ diffy = Diffy::Diff.new(
6
+ obj1.pretty_inspect,
7
+ obj2.pretty_inspect,
8
+ :diff => '-u'
9
+ )
10
+
11
+ out = diffy.to_s(options[:color] ? :color : :text).gsub(/\s+\z/m, '')
12
+ out.gsub!(/^/, options[:indent]) if options[:indent]
13
+ out
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Dyna
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,31 @@
1
+ module Dyna
2
+ class DynamoDBWrapper
3
+ include Logger::ClientHelper
4
+
5
+ def initialize(ddb, options)
6
+ @ddb = ddb
7
+ @options = options.dup
8
+ end
9
+
10
+ def tables
11
+ @ddb.list_tables.table_names.map do |table_name|
12
+ describe_table = @ddb.describe_table(table_name: table_name).table
13
+ Table.new(@ddb, describe_table, @options)
14
+ end
15
+ end
16
+
17
+ def create(dsl)
18
+ log(:info, 'Create Table', :cyan, "#{dsl.table_name}")
19
+
20
+ unless @options.dry_run
21
+ result = @ddb.create_table(dsl.symbolize_keys)
22
+ @options.updated = true
23
+ result
24
+ end
25
+ end
26
+
27
+ def updated?
28
+ !!@options.updated
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,205 @@
1
+ module Dyna
2
+ class DynamoDBWrapper
3
+ class Table
4
+ extend Forwardable
5
+ include Logger::ClientHelper
6
+
7
+ def_delegators(
8
+ :@table,
9
+ :table_name
10
+ )
11
+
12
+ def initialize(ddb, table, options)
13
+ @ddb = ddb
14
+ @table = table
15
+ @options = options
16
+ end
17
+
18
+ def eql?(dsl)
19
+ definition_eql?(dsl)
20
+ end
21
+
22
+ def update(dsl)
23
+ unless provisioned_throughput_eql?(dsl)
24
+ wait_until_table_is_active
25
+ update_table(dsl_provisioned_throughput(dsl))
26
+ end
27
+ unless global_secondary_indexes_eql?(dsl)
28
+ wait_until_table_is_active
29
+ update_table_index(dsl, dsl_global_secondary_index_updates(dsl))
30
+ end
31
+ unless stream_specification_eql?(dsl)
32
+ wait_until_table_is_active
33
+ update_stream_specification(dsl_stream_specification(dsl))
34
+ end
35
+ end
36
+
37
+ def delete
38
+ log(:info, 'Delete Table', :red, "#{table_name}")
39
+
40
+ unless @options.dry_run
41
+ @ddb.delete_table(table_name: @table.table_name)
42
+ @options.updated = true
43
+ end
44
+ end
45
+
46
+ def definition
47
+ Exporter.table_definition(@table).symbolize_keys
48
+ end
49
+
50
+ def wait_until_table_is_active
51
+ log(:info, "waiting table #{@table.table_name} to be ACTIVE or deleted..", false)
52
+ loop do
53
+ begin
54
+ desc = @ddb.describe_table(table_name: table_name).table
55
+ rescue => e
56
+ break
57
+ end
58
+ status = desc.table_status
59
+ log(:info, "status... #{status}", false)
60
+ break if desc.table_status == 'ACTIVE'
61
+ sleep 3
62
+ end
63
+ end
64
+
65
+ private
66
+ def definition_eql?(dsl)
67
+ definition == dsl.definition
68
+ end
69
+
70
+ def provisioned_throughput_eql?(dsl)
71
+ self_provisioned_throughput == dsl_provisioned_throughput(dsl)
72
+ end
73
+
74
+ def self_provisioned_throughput
75
+ definition.select {|k,v| k == :provisioned_throughput}
76
+ end
77
+
78
+ def dsl_provisioned_throughput(dsl)
79
+ dsl.symbolize_keys.select {|k,v| k == :provisioned_throughput}
80
+ end
81
+
82
+ def global_secondary_indexes_eql?(dsl)
83
+ self_global_secondary_indexes == dsl_global_secondary_indexes(dsl)
84
+ end
85
+
86
+ def self_global_secondary_indexes
87
+ definition[:global_secondary_indexes]
88
+ end
89
+
90
+ def dsl_global_secondary_indexes(dsl)
91
+ dsl.symbolize_keys[:global_secondary_indexes]
92
+ end
93
+
94
+ def dsl_global_secondary_index_updates(dsl)
95
+ actual_by_name = (self_global_secondary_indexes || {}).group_by { |index| index[:index_name] }.each_with_object({}) do |(k, v), h|
96
+ h[k] = v.first
97
+ end
98
+ expect_by_name = (dsl_global_secondary_indexes(dsl) || {}).group_by { |index| index[:index_name] }.each_with_object({}) do |(k, v), h|
99
+ h[k] = v.first
100
+ end
101
+ params = []
102
+ expect_by_name.each do |index_name, expect_index|
103
+ actual_index = actual_by_name[index_name]
104
+ unless actual_index
105
+ unless params.empty?
106
+ log(:warn, 'Can not add multiple GSI at once', :yellow, index_name)
107
+ next
108
+ end
109
+ params << {create: expect_index}
110
+ end
111
+ end
112
+
113
+ expect_by_name.each do |index_name, expect_index|
114
+ actual_index = actual_by_name.delete(index_name)
115
+ if actual_index != nil &&
116
+ actual_index[:provisioned_throughput] != expect_index[:provisioned_throughput]
117
+ if params.any? { |param| param[:update] }
118
+ log(:warn, 'Can not update multiple GSI at once', :yellow, index_name)
119
+ next
120
+ end
121
+ params << {update: {
122
+ index_name: index_name,
123
+ provisioned_throughput: expect_index[:provisioned_throughput]
124
+ }}
125
+ end
126
+ end
127
+
128
+ actual_by_name.each do |index_name, actual_index|
129
+ if params.any? { |param| param[:delete] }
130
+ log(:warn, 'Can not delete multiple GSI at once', :yellow, index_name)
131
+ next
132
+ end
133
+ params << {delete: { index_name: index_name }}
134
+ end
135
+
136
+ params
137
+ end
138
+
139
+ def stream_specification_eql?(dsl)
140
+ actual = self_stream_specification
141
+ expect = dsl_stream_specification(dsl)
142
+ if (actual == nil || actual[:stream_specification] == nil) &&
143
+ (expect == nil || expect[:stream_specification] == nil || expect[:stream_specification][:stream_enabled] == false)
144
+ return true
145
+ end
146
+ actual == expect
147
+ end
148
+
149
+ def self_stream_specification
150
+ definition.select {|k,v| k == :stream_specification}
151
+ end
152
+
153
+ def dsl_stream_specification(dsl)
154
+ dsl.symbolize_keys.select {|k,v| k == :stream_specification}
155
+ end
156
+
157
+ def update_stream_specification(dsl)
158
+ log(:info, " table: #{@table.table_name}(update stream spec)\n".green + Dyna::Utils.diff(self_stream_specification, dsl, :color => @options.color, :indent => ' '), false)
159
+ unless @options.dry_run
160
+ params = { table_name: @table.table_name }.merge(dsl)
161
+ @ddb.update_table(params)
162
+ @options.updated = true
163
+ end
164
+ end
165
+
166
+ def update_table(dsl)
167
+ log(:info, " table: #{@table.table_name}\n".green + Dyna::Utils.diff(self_provisioned_throughput, dsl, :color => @options.color, :indent => ' '), false)
168
+ unless @options.dry_run
169
+ params = dsl.dup
170
+ params[:table_name] = @table.table_name
171
+ @ddb.update_table(params)
172
+ @options.updated = true
173
+ end
174
+ end
175
+
176
+ def update_table_index(dsl, index_params)
177
+ log(:info, " table: #{@table.table_name}(update GSI)".green, false)
178
+ index_params.each do |index_param|
179
+ if index_param[:create]
180
+ log(:info, " index: #{index_param[:create][:index_name]}(create GSI)".cyan, false)
181
+ log(:info, " => #{index_param[:create]}".cyan, false)
182
+ end
183
+ if index_param[:update]
184
+ log(:info, " index: #{index_param[:update][:index_name]}(update GSI)".green, false)
185
+ log(:info, " => #{index_param[:update]}".green, false)
186
+ end
187
+ if index_param[:delete]
188
+ log(:info, " index: #{index_param[:delete][:index_name]}(delete GSI)".red, false)
189
+ log(:info, " => #{index_param[:delete]}".red, false)
190
+ end
191
+ end
192
+
193
+ unless @options.dry_run
194
+ params = {
195
+ table_name: @table.table_name,
196
+ attribute_definitions: dsl.symbolize_keys[:attribute_definitions],
197
+ global_secondary_index_updates: index_params,
198
+ }
199
+ @ddb.update_table(params)
200
+ @options.updated = true
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
data/lib/dyna.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'forwardable'
2
+ require 'logger'
3
+ require 'term/ansicolor'
4
+ require 'diffy'
5
+ require 'hashie'
6
+ require 'singleton'
7
+
8
+ require 'aws-sdk'
9
+
10
+ require 'dyna/version'
11
+ require 'dyna/logger'
12
+ require 'dyna/filterable'
13
+ require 'dyna/client'
14
+ require 'dyna/exporter'
15
+ require 'dyna/template_helper'
16
+ require 'dyna/dsl'
17
+ require 'dyna/utils'
18
+ require 'dyna/dsl/converter'
19
+ require 'dyna/dsl/dynamo_db'
20
+ require 'dyna/dsl/table'
21
+ require 'dyna/ext/string-ext'
22
+ require 'dyna/ext/hash-ext'
23
+ require 'dyna/wrapper/table'
24
+ require 'dyna/wrapper/dynamo_db_wrapper'
25
+
26
+ module Dyna
27
+ end