dyna 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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