applb 0.1.0

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.
@@ -0,0 +1,386 @@
1
+ require 'pp'
2
+ require 'applb/client_wrapper'
3
+ require 'applb/converter'
4
+ require 'applb/dsl'
5
+ require 'applb/dsl/load_balancer'
6
+ require 'applb/error'
7
+ require 'applb/filterable'
8
+ require 'applb/utils'
9
+
10
+ module Applb
11
+ class Client
12
+ include Filterable
13
+
14
+ MAGIC_COMMENT = <<-EOS
15
+ # -*- mode: ruby -*-
16
+ # vi: set ft=ruby :
17
+ EOS
18
+
19
+ def initialize(filepath, options = {})
20
+ @filepath = filepath
21
+ @options = options
22
+ end
23
+
24
+ def apply
25
+ dsl = load_file(@filepath)
26
+
27
+ dsl_ec2s = dsl.ec2s
28
+ aws_ec2s = client.load_balancers.group_by(&:vpc_id)
29
+
30
+ dsl.ec2s.each do |vpc_id, dsl_ec2|
31
+ aws_ec2 = aws_ec2s[vpc_id] || []
32
+
33
+ traverse_ec2(vpc_id, dsl_ec2, aws_ec2)
34
+ end
35
+ end
36
+
37
+ def export
38
+ result = {}
39
+
40
+ lbs = client.load_balancers
41
+ tags_by_arn = describe_tags(lbs)
42
+
43
+ lbs.each do |lb|
44
+ attributes = client.load_balancer_attributes(load_balancer_arn: lb.load_balancer_arn)
45
+ target_groups = describe_target_groups(lb)
46
+ listeners = describe_listeners(lb)
47
+ rules_by_listener_arn = listeners.each_with_object({}) do |listener, rules_by_listener_arn|
48
+ rules_by_listener_arn[listener.listener_arn] = describe_rules(listener)
49
+ end
50
+ (result[lb.vpc_id] ||= {})[lb.load_balancer_name] = export_lb(
51
+ lb,
52
+ attributes,
53
+ target_groups,
54
+ listeners,
55
+ rules_by_listener_arn,
56
+ )
57
+ end
58
+
59
+ path = Pathname.new(@filepath)
60
+ base_dir = path.parent
61
+ if @options[:split_more]
62
+ result.each do |vpc_id, lbs_by_name|
63
+ lbs_by_name.each do |name, lbs|
64
+ Converter.new({vpc_id => {name => lbs}}, tags_by_arn).convert do |vpc_id, dsl|
65
+ alb_base_dir = base_dir.join("#{vpc_id}")
66
+ FileUtils.mkdir_p(alb_base_dir)
67
+ alb_file = alb_base_dir.join("#{name}.alb")
68
+ Applb.logger.info("export #{alb_file}")
69
+ open(alb_file, 'wb') do |f|
70
+ f.puts MAGIC_COMMENT
71
+ f.puts dsl
72
+ end
73
+ end
74
+ end
75
+ end
76
+ elsif @options[:split]
77
+ Converter.new(result, tags_by_arn).convert do |vpc_id, dsl|
78
+ FileUtils.mkdir_p(base_dir)
79
+ alb_file = base_dir.join("#{vpc_id}.alb")
80
+ Applb.logger.info("export #{alb_file}")
81
+ open(alb_file, 'wb') do |f|
82
+ f.puts MAGIC_COMMENT
83
+ f.puts dsl
84
+ end
85
+ end
86
+ else
87
+ dsls = []
88
+ Converter.new(result, tags_by_arn).convert do |vpc_id, dsl|
89
+ dsls << dsl
90
+ end
91
+
92
+ FileUtils.mkdir_p(base_dir)
93
+ Applb.logger.info("export #{path}")
94
+ open(path, 'wb') do |f|
95
+ f.puts MAGIC_COMMENT
96
+ f.puts dsls.join("\n")
97
+ end
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def load_file(file)
104
+ open(file) do |f|
105
+ DSL.define(f.read, file, @options).result
106
+ end
107
+ end
108
+
109
+ def traverse_ec2(vpc_id, dsl_ec2, aws_ec2)
110
+ dsl_lb_by_name = dsl_ec2.load_balancers.group_by(&:name).each_with_object({}) do |(k, v), h|
111
+ h[k] = v.first if target?(k)
112
+ end
113
+ aws_lb_by_name = aws_ec2.group_by(&:load_balancer_name).each_with_object({}) do |(k, v), h|
114
+ h[k] = v.first if target?(k)
115
+ end
116
+
117
+ # create
118
+ dsl_lb_by_name.reject { |n| aws_lb_by_name[n] }.each do |name, dsl_lb|
119
+ aws_lb_by_name[name] = dsl_lb.create
120
+ end
121
+
122
+ # modify
123
+ dsl_lb_by_name.each do |name, dsl_lb|
124
+ next unless aws_lb = aws_lb_by_name.delete(name)
125
+
126
+ dsl_lb.aws(aws_lb)
127
+ traverse_lb(dsl_lb, aws_lb)
128
+ end
129
+
130
+ # delete
131
+ aws_lb_by_name.each do |name, aws_lb|
132
+ Applb.logger.info "Delete ELB v2 #{name}"
133
+ aws_tgs = client.describe_target_groups(
134
+ load_balancer_arn: aws_lb.load_balancer_arn,
135
+ ).target_groups
136
+ unless @options[:dry_run]
137
+ client.delete_load_balancer(aws_lb.load_balancer_arn)
138
+ # wait until load_balancer is deleted
139
+ sleep 3
140
+ end
141
+ aws_tgs.each do |tg|
142
+ Applb.logger.info "Delete target_group associated #{tg.target_group_name}"
143
+ next if @options[:dry_run]
144
+ client.delete_target_group(
145
+ target_group_arn: tg.target_group_arn,
146
+ )
147
+ end
148
+ end
149
+ end
150
+
151
+ def traverse_lb(dsl_lb, aws_lb)
152
+ dsl_lb.modify_subnets
153
+ dsl_lb.modify_security_groups
154
+ dsl_lb.modify_ip_address_type
155
+ dsl_lb.modify_load_balancer_attributes
156
+
157
+ traverse_target_groups(dsl_lb, aws_lb)
158
+ traverse_listeners(dsl_lb, aws_lb)
159
+ end
160
+
161
+ def traverse_target_groups(dsl_lb, aws_lb)
162
+ aws_tg_by_name = @client.target_groups(load_balancer_arn: aws_lb.load_balancer_arn).group_by(&:target_group_name).each_with_object({}) do |(k, v), h|
163
+ h[k] = v.first
164
+ end
165
+ dsl_tg_by_name = dsl_lb.target_groups.group_by(&:name).each_with_object({}) do |(k, v), h|
166
+ h[k] = v.first
167
+ end
168
+
169
+ # create
170
+ dsl_tg_by_name.reject { |n, _| aws_tg_by_name[n] }.each do |name, dsl_tg|
171
+ aws_tg_by_name[name] = dsl_tg.create
172
+ end
173
+
174
+ # modify
175
+ dsl_tg_by_name.each do |name, dsl_tg|
176
+ aws_tg = aws_tg_by_name.delete(name)
177
+ next unless aws_tg
178
+
179
+ dsl_tg.aws(aws_tg).modify
180
+ end
181
+
182
+ aws_tg_by_name.each do |name, aws_tg|
183
+ Applb.logger.info("Delete target group #{name}")
184
+ next if @options[:dry_run]
185
+ # client.modify_listener({}) TODO remove from listener first
186
+ client.delete_target_group(target_group_arn: aws_tg.target_group_arn)
187
+ end
188
+ end
189
+
190
+ def traverse_listeners(dsl_lb, aws_lb)
191
+ aws_listener_by_port = @client.listeners(load_balancer_arn: aws_lb.load_balancer_arn).group_by(&:port).each_with_object({}) do |(k, v), h|
192
+ h[k] = v.first
193
+ end
194
+
195
+ aws_target_group_by_name = @client.target_groups.group_by(&:target_group_name).each_with_object({}) do |(k, v), h|
196
+ h[k] = v.first
197
+ end
198
+ aws_target_group_by_arn = aws_target_group_by_name.each_with_object({}) do |(k, v), h|
199
+ h[v.target_group_arn] = v
200
+ end
201
+
202
+ dsl_listener_by_port = dsl_lb.listeners.group_by(&:port).each_with_object({}) do |(k, v), h|
203
+ dsl_listener = v.first
204
+ dsl_listener.load_balancer_arn = aws_lb.load_balancer_arn
205
+ h[k] = dsl_listener
206
+ end
207
+
208
+ # create
209
+ dsl_listener_by_port.reject { |port, _| aws_listener_by_port[port] }.each do |port, dsl_listener|
210
+ # resolve target_group_arn by target_group_name
211
+ target_group_name = dsl_listener.default_actions.first[:target_group_name]
212
+ if target_group_name
213
+ dsl_listener.default_actions.first[:target_group_arn] = aws_target_group_by_name[target_group_name].target_group_arn
214
+ end
215
+ aws_listener_by_port[port] = dsl_listener.create
216
+ end
217
+
218
+ # modify
219
+ dsl_listener_by_port.each do |port, dsl_listener|
220
+ aws_listener = aws_listener_by_port.delete(port)
221
+ next unless aws_listener
222
+
223
+ target_group_name = dsl_listener.default_actions.first[:target_group_name]
224
+ if target_group_name
225
+ dsl_listener.default_actions.first[:target_group_arn] = aws_target_group_by_name[target_group_name].target_group_arn
226
+ end
227
+ dsl_listener.aws(aws_listener).modify
228
+ traverse_rule(dsl_listener, aws_listener, aws_target_group_by_name)
229
+ end
230
+
231
+ # delete
232
+ aws_listener_by_port.each do |port, aws_listener|
233
+ aws_actions = client.describe_rules(listener_arn: aws_listener.listener_arn).rules.map(&:actions).flatten
234
+ Applb.logger.info("#{aws_lb.load_balancer_name} Delete listener for port #{port}")
235
+ unless @options[:dry_run]
236
+ client.delete_listener(listener_arn: aws_listener.listener_arn)
237
+ end
238
+ (aws_actions + aws_listener.default_actions).each do |action|
239
+ aws_tg = aws_target_group_by_arn.delete(action.target_group_arn)
240
+ next unless aws_tg
241
+ Applb.logger.info("#{aws_lb.load_balancer_name} Delete target_group associated #{aws_tg.target_group_name}")
242
+ next if @options[:dry_run]
243
+ client.delete_target_group(target_group_arn: action.target_group_arn)
244
+ end
245
+ end
246
+ end
247
+
248
+ class TargetGroupResolveError < Error
249
+ end
250
+
251
+ def traverse_rule(dsl_listener, aws_listener, aws_target_group_by_name)
252
+ dummy_idx = 0
253
+ dsl_rule_by_arn = (dsl_listener.rules || []).each_with_object({}) do |dsl_rule, h|
254
+ # give dummy arn for grouping
255
+ arn = dsl_rule.rule_arn ? dsl_rule.rule_arn : "dummy-#{(dummy_idx += 1)}"
256
+ # set listener_arn here
257
+ dsl_rule.listener_arn = aws_listener.listener_arn
258
+ h[arn] = dsl_rule
259
+ end
260
+ aws_rules = client.describe_rules(listener_arn: aws_listener.listener_arn).rules
261
+ aws_rule_by_arn = aws_rules.reject(&:is_default).group_by(&:rule_arn).each_with_object({}) do |(k, v), h|
262
+ h[k] = v.first
263
+ end
264
+
265
+ # create
266
+ dsl_rule_by_arn.reject { |arn, _| aws_rule_by_arn[arn] }.each do |arn, dsl_rule|
267
+ # resolve target_group_arn by target_group_name
268
+ target_group_name = dsl_rule.actions.first[:target_group_name]
269
+ if target_group_name
270
+ Applb.logger.debug("Resolve target_group_arn by target_group_name. -> #{target_group_name}")
271
+ aws_tg = aws_target_group_by_name[target_group_name]
272
+ unless aws_tg
273
+ Applb.logger.error("AWS target groups by name:\n#{aws_target_group_by_name.pretty_inspect}")
274
+ raise TargetGroupResolveError.new("target_group_name #{target_group_name}")
275
+ end
276
+ dsl_rule.actions.first[:target_group_arn] = aws_tg.target_group_arn
277
+ end
278
+
279
+ rule = dsl_rule.create
280
+ next if @options[:dry_run]
281
+ dsl_rule_by_arn[rule.rule_arn] = dsl_rule_by_arn.delete(arn)
282
+ end
283
+
284
+ # modify
285
+ dsl_rule_by_arn.each do |arn, dsl_rule|
286
+ aws_rule = aws_rule_by_arn.delete(arn)
287
+ next unless aws_rule
288
+
289
+ # resolve target_group_arn by target_group_name
290
+ target_group_name = dsl_rule.actions.first[:target_group_name]
291
+ if target_group_name
292
+ Applb.logger.debug("Resolve target_group_arn by target_group_name. -> #{target_group_name}")
293
+ aws_tg = aws_target_group_by_name[target_group_name]
294
+ unless aws_tg
295
+ Applb.logger.error("AWS target groups by name:\n#{aws_target_group_by_name.pretty_inspect}")
296
+ raise TargetGroupResolveError.new("target_group_name #{target_group_name}")
297
+ end
298
+ dsl_rule.actions.first[:target_group_arn] = aws_tg.target_group_arn
299
+ end
300
+
301
+ dsl_rule.aws(aws_rule).modify
302
+ end
303
+
304
+ # delete
305
+ aws_rule_by_arn.values.each do |aws_rule|
306
+ Applb.logger.info("Delete rule #{aws_rule.conditions.first[:values].first}")
307
+ next if @options[:dry_run]
308
+ Applb.logger.debug("deleting rule_arn #{aws_rule.rule_arn}")
309
+ client.delete_rule(rule_arn: aws_rule.rule_arn)
310
+ end
311
+ end
312
+
313
+ # @param [Aws::ElasticLodaBalancingV2::Types::LoadBalancer] lb
314
+ def describe_tags(lbs)
315
+ result = {}
316
+ arns = lbs.map(&:load_balancer_arn)
317
+ unless arns.empty?
318
+ resp = client.describe_tags(resource_arns: lbs.map(&:load_balancer_arn))
319
+ resp.tag_descriptions.each do |tag_desc|
320
+ result[tag_desc.resource_arn] = Hash[tag_desc.tags.map { |tag| [tag.key, tag.value] }]
321
+ end
322
+ end
323
+ result
324
+ end
325
+
326
+ def describe_target_groups(lb)
327
+ client.target_groups(load_balancer_arn: lb.load_balancer_arn)
328
+ end
329
+
330
+ def describe_listeners(lb)
331
+ client.listeners(load_balancer_arn: lb.load_balancer_arn)
332
+ end
333
+
334
+ def describe_rules(listener)
335
+ client.rules(listener_arn: listener.listener_arn)
336
+ end
337
+
338
+ # @param [Aws::ElasticLodaBalancingV2::Types::LoadBalancer] lb
339
+ # @param [Array<Types::LoadBalancerAttribute>] attrs
340
+ # @param [Array<Types::TargetGroup>] target_groups
341
+ # @param [Array<Types::Listener>] listeners
342
+ # @param [Hash] rules_by_listener_arn
343
+ def export_lb(lb, attrs, target_groups, listeners, rules_by_listener_arn)
344
+ {
345
+ availability_zones: lb.availability_zones.map { |az| Hash[az.each_pair.to_a] },
346
+ canonical_hosted_zone_id: lb.canonical_hosted_zone_id,
347
+ created_time: lb.created_time,
348
+ dns_name: lb.dns_name,
349
+ ip_address_type: lb.ip_address_type,
350
+ load_balancer_arn: lb.load_balancer_arn,
351
+ load_balancer_name: lb.load_balancer_name,
352
+ scheme: lb.scheme,
353
+ security_groups: lb.security_groups,
354
+ state: lb.state,
355
+ type: lb.type,
356
+ vpc_id: lb.vpc_id,
357
+ attributes: export_attributes(attrs),
358
+ target_groups: target_groups,
359
+ listeners: listeners,
360
+ rules_by_listener_arn: rules_by_listener_arn,
361
+ }
362
+ end
363
+
364
+ # @param [Array<Types::LoadBalancerAttribute>] attrs
365
+ def export_attributes(attrs)
366
+ result = {}
367
+ attrs.each do |attr|
368
+ case attr.key
369
+ when 'access_logs.s3.enabled' then
370
+ (result['access_logs'] ||= {s3: {}})[:s3][:enabled] = attr.value
371
+ when 'access_logs.s3.prefix' then
372
+ result['access_logs'] ||= {s3: {}}[:s3][:prefix] = attr.value
373
+ when 'access_logs.s3.bucket' then
374
+ result['access_logs'] ||= {s3: {}}[:s3][:bucket] = attr.value
375
+ else
376
+ result[attr.key] = attr.value
377
+ end
378
+ end
379
+ result
380
+ end
381
+
382
+ def client
383
+ @client ||= ClientWrapper.new(@options)
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,85 @@
1
+ require 'forwardable'
2
+ require 'aws-sdk'
3
+
4
+ module Applb
5
+ class ClientWrapper
6
+ extend Forwardable
7
+ def_delegators :@client, *%i/
8
+ describe_load_balancer_attributes describe_tags set_subnets set_security_groups
9
+ set_ip_address_type modify_load_balancer_attributes create_target_group
10
+ modify_target_group delete_target_group create_listener modify_listener
11
+ delete_listener describe_rules delete_rule create_load_balancer
12
+ create_rule modify_rule set_rule_priorities describe_target_groups/
13
+
14
+ def initialize(options)
15
+ @includes = options[:includes] || []
16
+ @excludes = options[:excludes] || []
17
+ @client = Aws::ElasticLoadBalancingV2::Client.new
18
+ end
19
+
20
+ def load_balancers
21
+ results = []
22
+ next_marker = nil
23
+ begin
24
+ resp = @client.describe_load_balancers(marker: next_marker)
25
+ resp.load_balancers.each do |lb|
26
+ results << lb if target?(lb)
27
+ end
28
+ next_marker = resp.next_marker
29
+ end while next_marker
30
+ results
31
+ end
32
+
33
+ def delete_load_balancer(arn)
34
+ @client.delete_load_balancer(load_balancer_arn: arn)
35
+ end
36
+
37
+ def target_groups(*argv)
38
+ results = []
39
+ next_marker = nil
40
+ begin
41
+ resp = @client.describe_target_groups(*argv)
42
+ results.push(*resp.target_groups)
43
+ end while next_marker
44
+ results
45
+ end
46
+
47
+ def listeners(*argv)
48
+ results = []
49
+ next_marker = nil
50
+ begin
51
+ resp = @client.describe_listeners(*argv)
52
+ results.push(*resp.listeners)
53
+ end while next_marker
54
+ results
55
+ end
56
+
57
+ def rules(*argv)
58
+ results = []
59
+ next_marker = nil
60
+ begin
61
+ resp = @client.describe_rules(*argv)
62
+ results.push(*resp.rules)
63
+ end while next_marker
64
+ results
65
+ end
66
+
67
+ def load_balancer_attributes(*argv)
68
+ resp = @client.describe_load_balancer_attributes(*argv)
69
+ resp.attributes
70
+ end
71
+
72
+ private
73
+
74
+ def target?(lb)
75
+ name = lb.load_balancer_name
76
+ unless @includes.empty?
77
+ return @includes.include?(name)
78
+ end
79
+ unless @excludes.empty?
80
+ return !@excludes.any? { |regex| name =~ regex }
81
+ end
82
+ true
83
+ end
84
+ end
85
+ end