biosphere 0.2.14 → 0.2.17

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
  SHA1:
3
- metadata.gz: 503fe6f7ce0893d592c091476ad1713993007b4b
4
- data.tar.gz: 2cff76a658d968277319a0f67adb2539c490dded
3
+ metadata.gz: 668761124d495501e57b6581a66d3a461e20e1b6
4
+ data.tar.gz: 77b077d01ee5d1b57cfa2de40fe62bfe8bb282c9
5
5
  SHA512:
6
- metadata.gz: 2f47c9ff8a72bcb892b8e5c77c8aaf638c9db55117f8ed2bc3f0946bd77684a1e055a7ea9360ec3d7f3f8866b841fb25854d11713c3d82d4e15c2591099bd188
7
- data.tar.gz: 6afcf43b0e4e4d5dd8ca2f0c2c7df4c8fa20b81fd0c6be9977031f26d8e16adf0641a444ad805b3cdaa7d6f1a149e99b89ad4573571d3f4091c4581d7adba4aa
6
+ metadata.gz: 4fd8a19b4e2d1a045a077605d15f270170ec54f1e4dd20075704418a9dc54e7f215dc15dd1267d9876ac70f2d3e2832951abd6ebcc8b1cf853f7c4e84de62dd0
7
+ data.tar.gz: ce5820b8bb1762817df4582f24b3aeea759c9d2d90654f951331d57910d5ddd8fe2df1794db5c77881adb419f5ec123fba551a12d4a6ecad9c1d1b860f76b49b
@@ -273,7 +273,7 @@ elsif ARGV[0] == "statereset" && options.src
273
273
  state.save()
274
274
  s3.save("#{options.build_dir}/state.node") unless localmode
275
275
  suite.deployments.each do |name, deployment|
276
- s3.delete_object()
276
+ s3.delete_object("#{name}.tfstate")
277
277
  end
278
278
  end
279
279
 
@@ -302,21 +302,30 @@ elsif ARGV[0] == "commit" && options.src
302
302
  end
303
303
 
304
304
  s3.set_lock()
305
- s3.retrieve("#{options.build_dir}/#{deployment}.tfstate")
305
+ state_file = "#{options.build_dir}/#{deployment}.tfstate"
306
+ s3.retrieve(state_file)
306
307
  begin
307
- tf_plan_str = %x( terraform plan -state=#{options.build_dir}/#{deployment}.tfstate -out #{options.build_dir}/plan #{options.build_dir}/#{deployment} )
308
+ tf_plan_str = %x( terraform plan -state=#{state_file} #{options.build_dir}/#{deployment} )
308
309
  rescue Errno::ENOENT => e
309
310
  STDERR.puts "Could not find terraform. Install with with \"brew install terraform\"".colorize(:red)
310
311
  s3.release_lock()
311
312
  end
313
+
314
+ tf_graph_str = %x( terraform graph #{options.build_dir}/#{deployment} )
315
+
312
316
  tfplanning = Biosphere::CLI::TerraformPlanning.new()
313
- plan = tfplanning.generate_plan(suite.deployments[deployment], tf_plan_str)
317
+ plan = tfplanning.generate_plan(suite.deployments[deployment], tf_plan_str, tf_graph_str)
314
318
  if !plan
315
319
  STDERR.puts "Error parsing tf plan output"
316
320
  s3.release_lock()
317
321
  exit
318
322
  end
319
323
 
324
+ targets = plan.get_resources.collect { |x| "-target=#{x}" }.join(" ")
325
+ puts "Targets: #{targets}"
326
+
327
+ tf_plan_str = %x( terraform plan #{targets} -state=#{state_file} -out #{options.build_dir}/plan #{options.build_dir}/#{deployment} )
328
+
320
329
  # Print the raw terraform output
321
330
  puts "== TERRAFORM PLAN START ==".colorize(:green)
322
331
  puts "\n" + tf_plan_str
@@ -326,8 +335,6 @@ elsif ARGV[0] == "commit" && options.src
326
335
  puts "Target group listing:"
327
336
  plan.print
328
337
 
329
- targets = plan.get_resources.collect { |x| "-target=#{x}" }.join(" ")
330
- puts "Targets: #{targets}"
331
338
  answer = ""
332
339
  if !options.force
333
340
  while answer.empty? || (answer != "y" && answer != "n")
@@ -342,11 +349,8 @@ elsif ARGV[0] == "commit" && options.src
342
349
  puts "\nOk, will not proceed with commit"
343
350
  elsif answer == "y"
344
351
  puts "\nApplying the changes (this may take several minutes)"
345
- state_file = "#{options.build_dir}/#{deployment}.tfstate"
346
- #tf_apply = %x( terraform apply -state=#{state_file} #{options.build_dir})
347
- #puts "\n" + tf_apply
348
352
  begin
349
- PTY.spawn("terraform apply #{targets} -state=#{state_file} #{options.build_dir}/#{deployment}") do |stdout, stdin, pid|
353
+ PTY.spawn("terraform apply -state-out=#{state_file} #{options.build_dir}/plan") do |stdout, stdin, pid|
350
354
  begin
351
355
  stdout.each { |line| puts line }
352
356
  rescue Errno::EIO
@@ -375,8 +379,9 @@ elsif ARGV[0] == "commit" && options.src
375
379
  suite.deployments[deployment].load_outputs(state_file)
376
380
  state.node[:biosphere][:last_commit_time] = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
377
381
  state.save()
378
- s3.save("#{options.build_dir}/#{deployment}.tfstate")
382
+ s3.save(state_file)
379
383
  s3.save("#{options.build_dir}/state.node")
384
+ #File.delete("#{options.build_dir}/plan")
380
385
  end
381
386
 
382
387
  s3.release_lock()
@@ -4,6 +4,233 @@ require 'colorize'
4
4
 
5
5
  class Biosphere
6
6
 
7
+ class TerraformGraph
8
+
9
+ attr_accessor :graph
10
+
11
+ class Edge
12
+ attr_accessor :src, :dst, :length
13
+
14
+ def initialize(src, dst, length = 1)
15
+ @src = src
16
+ @dst = dst
17
+ @length = length
18
+ end
19
+ end
20
+
21
+
22
+ class Graph < Array
23
+ attr_reader :edges, :map
24
+
25
+ def initialize
26
+ @map = {}
27
+ @edges = []
28
+ end
29
+
30
+ def connect(src_name, dst_name, length = 1)
31
+ unless @map.include?(src_name)
32
+ raise ArgumentError, "No such vertex: #{src_name}"
33
+ end
34
+ unless @map.include?(dst_name)
35
+ raise ArgumentError, "No such vertex: #{dst_name}"
36
+ end
37
+
38
+ src = @map[src_name]
39
+ dst = @map[dst_name]
40
+
41
+ @edges.push Edge.new(src, dst, length)
42
+ end
43
+
44
+ def push_name(name)
45
+ if !@map[name]
46
+ index = @map.length + 1
47
+ @map[name] = index
48
+ self.push(index)
49
+ return index
50
+ else
51
+ return @map[name]
52
+ end
53
+ end
54
+
55
+ def connect_mutually(vertex1_name, vertex2_name, length = 1)
56
+ self.connect vertex1_name, vertex2_name, length
57
+ self.connect vertex2_name, vertex1_name, length
58
+ end
59
+
60
+ def neighbors(vertex_name)
61
+ vertex = @map[vertex_name]
62
+ neighbors = []
63
+ @edges.each do |edge|
64
+ neighbors.push edge.dst if edge.src == vertex
65
+ end
66
+ return neighbors.uniq.collect { |x| @map.key(x) }
67
+ end
68
+
69
+ def neighbors_by_index(vertex)
70
+ neighbors = []
71
+ @edges.each do |edge|
72
+ neighbors.push edge.dst if edge.src == vertex
73
+ end
74
+ return neighbors.uniq
75
+ end
76
+
77
+ def length_between(src_name, dst_name)
78
+ src = @map[src_name]
79
+ dst = @map[dst_name]
80
+
81
+ @edges.each do |edge|
82
+ return edge.length if edge.src == src and edge.dst == dst
83
+ end
84
+ nil
85
+ end
86
+
87
+ def length_between_index(src, dst)
88
+ @edges.each do |edge|
89
+ return edge.length if edge.src == src and edge.dst == dst
90
+ end
91
+ nil
92
+ end
93
+
94
+ def dijkstra(src_name, dst_name = nil)
95
+ src = @map[src_name]
96
+ dst = @map[dst_name]
97
+
98
+ distances = {}
99
+ previouses = {}
100
+ self.each do |vertex|
101
+ distances[vertex] = nil # Infinity
102
+ previouses[vertex] = nil
103
+ end
104
+ distances[src] = 0
105
+ vertices = self.clone
106
+ until vertices.empty?
107
+ nearest_vertex = vertices.inject do |a, b|
108
+ next b unless distances[a]
109
+ next a unless distances[b]
110
+ next a if distances[a] < distances[b]
111
+ b
112
+ end
113
+ break unless distances[nearest_vertex] # Infinity
114
+ if dst and nearest_vertex == dst
115
+ path = path_to_names(get_path(previouses, src, dst))
116
+ return { path: path, distance: distances[dst] }
117
+ end
118
+ neighbors = vertices.neighbors_by_index(nearest_vertex)
119
+ neighbors.each do |vertex|
120
+ alt = distances[nearest_vertex] + vertices.length_between_index(nearest_vertex, vertex)
121
+ if distances[vertex].nil? or alt < distances[vertex]
122
+ distances[vertex] = alt
123
+ previouses[vertex] = nearest_vertex
124
+ # decrease-key v in Q # ???
125
+ end
126
+ end
127
+ vertices.delete nearest_vertex
128
+ end
129
+ if dst
130
+ return nil
131
+ else
132
+ paths = {}
133
+ distances.each { |k, v| paths[k] = path_to_names(get_path(previouses, src, k)) }
134
+ return { paths: paths, distances: distances }
135
+ end
136
+ end
137
+
138
+ private
139
+ def path_to_names(path)
140
+ p = []
141
+ path.each do |index|
142
+ p << @map.key(index)
143
+ end
144
+
145
+ return p
146
+ end
147
+
148
+ def get_path(previouses, src, dest)
149
+ path = get_path_recursively(previouses, src, dest)
150
+ path.is_a?(Array) ? path.reverse : path
151
+ end
152
+
153
+ # Unroll through previouses array until we get to source
154
+ def get_path_recursively(previouses, src, dest)
155
+ return src if src == dest
156
+ raise ArgumentError, "No path from #{src} to #{dest}" if previouses[dest].nil?
157
+ [dest, get_path_recursively(previouses, src, previouses[dest])].flatten
158
+ end
159
+ end
160
+
161
+ def initialize()
162
+
163
+ end
164
+
165
+ def parse_line(line)
166
+ if (m = line.match(/"\[(.+?)\] (?<name>\S+?)(\((.+?)\))?" \[label/))
167
+ return {
168
+ :type => :node,
169
+ :name => m[:name]
170
+ }
171
+ elsif (m = line.match(/"\[(.+?)\] (?<from>\S+?)(\s\((.+?)\)){0,1}" -> "\[(.+?)\] (?<to>\S+?)(\s\((.+?)\)){0,1}"/))
172
+ return {
173
+ :type => :edge,
174
+ :from => m[:from],
175
+ :to => m[:to]
176
+ }
177
+ else
178
+ return nil
179
+ end
180
+ end
181
+
182
+ def load(data)
183
+
184
+ @graph = Graph.new
185
+
186
+ lines = data.split("\n")
187
+ lines.each do |line|
188
+
189
+ m = parse_line(line)
190
+ if m
191
+ if m[:type] == :node
192
+ @graph.push_name m[:name]
193
+ elsif m[:type] == :edge
194
+ @graph.push_name m[:from]
195
+ @graph.push_name m[:to]
196
+ @graph.connect(m[:from], m[:to], 1)
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ def get_blacklist_by_dependency(item)
203
+ path = @graph.dijkstra("root", item)
204
+ return path[:path]
205
+ end
206
+
207
+ def filter_tf_plan(plan)
208
+
209
+ blacklist = []
210
+ plan.items.each do |item|
211
+ if item[:action] == :not_picked
212
+ begin
213
+ blacklist << get_blacklist_by_dependency(item[:resource_name])
214
+ rescue ArgumentError => e
215
+ puts "Error: #{e}. item: #{item}"
216
+ end
217
+ end
218
+ end
219
+
220
+ blacklist.each do |blacklist_items|
221
+ blacklist_items.each do |blacklist_path_item|
222
+ plan.items.each do |item|
223
+ if item[:resource_name] == blacklist_path_item && item[:action] != :not_picked
224
+ item[:action] = :not_picked
225
+ item[:reason] = "not selected as dependent on #{blacklist_items[blacklist_items.index(item[:resource_name])+1..-1].join(" -> ")}"
226
+ end
227
+ end
228
+ end
229
+ end
230
+ return plan
231
+ end
232
+ end
233
+
7
234
  class CLI
8
235
 
9
236
  class TerraformPlanning
@@ -12,6 +239,7 @@ class Biosphere
12
239
  attr_accessor :items
13
240
  def initialize()
14
241
  @items = []
242
+ @graph = nil
15
243
  end
16
244
 
17
245
  def length
@@ -63,11 +291,20 @@ class Biosphere
63
291
  end
64
292
  end
65
293
 
66
- def generate_plan(deployment, tf_output_str)
294
+ def generate_plan(deployment, tf_output_str, tf_graph_str = nil)
295
+
296
+ if tf_graph_str
297
+ @graph = Biosphere::TerraformGraph.new
298
+ @graph.load(tf_graph_str)
299
+ end
300
+
67
301
  data = parse_terraform_plan_output(tf_output_str)
68
302
 
69
303
  plan = build_terraform_targetting_plan(deployment, data)
70
304
 
305
+ if @graph
306
+ plan = @graph.filter_tf_plan(plan)
307
+ end
71
308
  return plan
72
309
  end
73
310
 
@@ -192,7 +429,6 @@ class Biosphere
192
429
  # Relaunches are more complex: we need to bucket resources based on group, so that we can later pick just one change from each group
193
430
  changes[:relaunches].each do |change|
194
431
  group = resource_to_target_group_map[change]
195
- puts "group #{group} for change #{change}"
196
432
  if group
197
433
  group_changes_map[group] = (group_changes_map[group] ||= []) << change
198
434
  elsif resources_not_in_any_target_group[change]
@@ -214,7 +450,7 @@ class Biosphere
214
450
  # Handle safe groups: just one changing resource in the group
215
451
  safe_groups = group_changes_map.select { |name, resources| resources.length <= 1 }
216
452
  safe_groups.each do |group_name, resources|
217
- resources.each do |resource_name|
453
+ resources.each do |resource_name|
218
454
  plan.items << {
219
455
  :resource_name => resource_name,
220
456
  :target_group => group_name,
@@ -84,14 +84,18 @@ class Biosphere
84
84
  puts "Error doing api call: #{e}".colorize(:red)
85
85
  puts "This might be because you did not specify namespace in your resource: #{resource[:metadata]}".colorize(:yellow)
86
86
  else
87
- puts "Error calling API: #{e}"
87
+ puts "Error calling API (on RestClient::MethodNotAllowed): #{e}"
88
88
  end
89
89
  puts "rest_client: #{ns_prefix + resource_name}, client: #{client.rest_client[ns_prefix + resource_name]}"
90
+ puts "Dumpin resource request:"
91
+ pp resource.to_h.to_json
90
92
  raise e
91
93
 
92
94
  rescue RestClient::Exception => e
93
- puts "Error calling API: #{e}"
95
+ puts "Error calling API (on RestClient::Exception rescue): #{e}"
94
96
  puts "rest_client: #{ns_prefix + resource_name}, client: #{client.rest_client[ns_prefix + resource_name]}"
97
+ puts "Dumpin resource request:"
98
+ pp resource.to_h.to_json
95
99
  raise e
96
100
  end
97
101
  return {
@@ -1,3 +1,3 @@
1
1
  class Biosphere
2
- Version = "0.2.14"
2
+ Version = "0.2.17"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: biosphere
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.14
4
+ version: 0.2.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juho Mäkinen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-02 00:00:00.000000000 Z
11
+ date: 2017-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec