biosphere 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6e587744cb4b79991175f86016bcd023fce26456
4
- data.tar.gz: e7b5a2b107cf724d4cc7f319528e55daf5396c84
3
+ metadata.gz: 18516adcf232287623fbc3b0a38a9fcbf3a06467
4
+ data.tar.gz: 25b96568a2cbbf6760d4cf8f5ac83c74d0111272
5
5
  SHA512:
6
- metadata.gz: cd23b2e88b6a75729ec8e18b3e2e39929d526ecbc07ea87d0c710639c610d191e3b225c0876ad93a487e00489103e07bedb6e2100bf3b82c697ae3b6a33287e3
7
- data.tar.gz: 85e154e3075e05572e3ea6b5d35d7591a8a3b302502044e3bc0479b0e3f1dc5078760043d5061edbcca62bbb41256ad0c506d4af5984cd3d9ffa8a35b35b5b7f
6
+ metadata.gz: a00f8ba95c94a69a16aad64144dbec2e5fecccb40a3a9078110ff3fb73215f0150a2822c1a0b395cac4234072f5fa7f239bfbda0cfec704bf5931367deb7c3ca
7
+ data.tar.gz: 1a34d3f63840b2cd4990d686d09cdc0e8cffc10084e35e176f0d1eac01967368b58acc702905c6ab2c68a845585ff86533869e9970e22c4cd34976e96e84431a
@@ -203,8 +203,23 @@ elsif ARGV[0] == "commit" && options.src
203
203
 
204
204
  s3.set_lock()
205
205
  s3.retrieve("#{options.build_dir}/#{deployment}.tfstate")
206
- tf_plan = %x( terraform plan -state=#{options.build_dir}/#{deployment}.tfstate #{options.build_dir} )
207
- puts "\n" + tf_plan
206
+ tf_plan_str = %x( terraform plan -state=#{options.build_dir}/#{deployment}.tfstate -out #{options.build_dir}/plan #{options.build_dir} )
207
+ tfplanning = Biosphere::CLI::TerraformPlanning.new()
208
+ plan = tfplanning.generate_plan(suite.deployments[deployment], tf_plan_str)
209
+ if !plan
210
+ STDERR.puts "Error parsing tf plan output"
211
+ s3.release_lock()
212
+ exit
213
+ end
214
+
215
+ # Print the raw terraform output
216
+ puts "\n" + tf_plan_str
217
+
218
+ # Print our pretty short plan
219
+ plan.print
220
+
221
+ targets = plan.get_resources.collect { |x| "-target=#{x}" }.join(" ")
222
+
208
223
  answer = ""
209
224
  while answer.empty? || (answer != "y" && answer != "n")
210
225
  print "\nDoes the plan look reasonable? (Answering yes will apply the changes) y/n: "
@@ -219,7 +234,7 @@ elsif ARGV[0] == "commit" && options.src
219
234
  #tf_apply = %x( terraform apply -state=#{state_file} #{options.build_dir})
220
235
  #puts "\n" + tf_apply
221
236
  begin
222
- PTY.spawn("terraform apply -state=#{state_file} #{options.build_dir}") do |stdout, stdin, pid|
237
+ PTY.spawn("terraform apply #{targets} -state=#{state_file} #{options.build_dir}") do |stdout, stdin, pid|
223
238
  begin
224
239
  stdout.each { |line| puts line }
225
240
  rescue Errno::EIO
@@ -11,5 +11,6 @@ require "biosphere/state"
11
11
  require "biosphere/deployment"
12
12
  require "biosphere/terraformproxy"
13
13
  require "biosphere/suite"
14
+ require "biosphere/cli/terraformplanning"
14
15
  require "biosphere/s3"
15
16
 
@@ -0,0 +1,250 @@
1
+ require 'pp'
2
+ require 'treetop'
3
+ require 'colorize'
4
+
5
+ class Biosphere
6
+
7
+ class CLI
8
+
9
+ class TerraformPlanning
10
+
11
+ class TerraformPlan
12
+ attr_accessor :items
13
+ def initialize()
14
+ @items = []
15
+ end
16
+
17
+ def length
18
+ @items.length
19
+ end
20
+
21
+ def print(out = STDOUT)
22
+ last_target_group = nil
23
+ @items.sort_by { |a| a[:target_group] }.each do |item|
24
+ if last_target_group != item[:target_group]
25
+ if item[:target_group] != ""
26
+ out.write "\nTarget group: #{item[:target_group]}\n"
27
+ else
28
+ out.write "\nNot in any target groups:\n"
29
+ end
30
+ last_target_group = item[:target_group]
31
+ end
32
+ out.write "\t"
33
+ out.write "#{item[:resource_name]} "
34
+ if item[:action] == :create
35
+ out.write "(#{item[:reason]})".colorize(:green)
36
+ elsif item[:action] == :relaunch
37
+ out.write "(#{item[:reason]})".colorize(:yellow)
38
+ elsif item[:action] == :destroy
39
+ out.write "(#{item[:reason]})".colorize(:red)
40
+ elsif item[:action] == :change
41
+ out.write "(#{item[:reason]})".colorize(:yellow)
42
+ else
43
+ out.write "(#{item[:reason]}) unknown action: #{item[:action]}".colorize(:red)
44
+ end
45
+ out.write "\n"
46
+ end
47
+
48
+ if has_unpicked_resources()
49
+
50
+ out.write "\nWARNING: Not all resource changes will be applied!".colorize(:yellow)
51
+ out.write "\nYou need to do \"biosphere commit\" again when you have verified that it is safe to do so!".colorize(:yellow)
52
+ end
53
+ end
54
+
55
+ def has_unpicked_resources()
56
+ @items.find_index { |x| x[:action] == :not_picked } != nil
57
+ end
58
+
59
+ def get_resources()
60
+ @items.select { |x| x[:action] != :not_picked }.collect { |x| x[:resource_name] }
61
+ end
62
+ end
63
+
64
+ def generate_plan(deployment, tf_output_str)
65
+ data = parse_terraform_plan_output(tf_output_str)
66
+
67
+ plan = build_terraform_targetting_plan(deployment, data)
68
+
69
+ return plan
70
+ end
71
+
72
+ # returns object which contains interesting information on
73
+ # the terraform plan output.
74
+ #
75
+ # :relaunches contains a list of resources which will be changed
76
+ # by a relaunch
77
+ def parse_terraform_plan_output(str)
78
+ relaunches = []
79
+ changes = []
80
+ new_resources = []
81
+ lines = str.split("\n")
82
+ lines.each do |line|
83
+ # the gsub is to strip possible ansi colors away
84
+ # the match is to pick the TF notation about how the resource is about to change following the resource name itself
85
+ m = line.gsub(/\e\[[0-9;]*m/, "").match(/^([-~+\/]+)\s(.+)$/)
86
+ if m
87
+ # If the resource action contains a minus ('-' or '-/+') then
88
+ # we know that the action will be destructive.
89
+ if m[1].match(/[-]/)
90
+ relaunches << m[2]
91
+ elsif m[1] == "~"
92
+ changes << m[2]
93
+ elsif m[1] == "+"
94
+ new_resources << m[2]
95
+ end
96
+ end
97
+ end
98
+ return {
99
+ :relaunches => relaunches,
100
+ :changes => changes,
101
+ :new_resources => new_resources,
102
+ }
103
+ end
104
+
105
+ def build_terraform_targetting_plan(deployment, changes)
106
+
107
+ # This function will output an array of objects which describe a proper and safe
108
+ # plan for terraform resources to be applied.
109
+ #
110
+ # We can include following sets in this array:
111
+ # - resources which do not belong to any group
112
+ # - resources which belong to a group where count(group) == 1
113
+ # - a single resource from each group where count(group) > 1
114
+ #
115
+ # Each item is an object with the following fields:
116
+ # - :resource_name
117
+ # - :target_group (might be null)
118
+ # - :reason (human readable reason)
119
+ # - :action (symbol :not_picked, :relaunch, :change, :create, :destroy)
120
+ #
121
+ plan = TerraformPlan.new()
122
+
123
+ group_changes_map = {}
124
+ resource_to_target_group_map = {}
125
+
126
+ resources_not_in_any_target_group = {}
127
+ deployment.resources.each do |resource|
128
+ belong_to_target_group = false
129
+ resource_name = resource[:type] + "." + resource[:name]
130
+
131
+ deployment.target_groups.each do |group_name, resources|
132
+ if resources.include?(resource_name)
133
+ resource_to_target_group_map[resource_name] = group_name
134
+ belong_to_target_group = true
135
+ end
136
+ end
137
+
138
+ if !belong_to_target_group
139
+ resources_not_in_any_target_group[resource_name] = {
140
+ :resource_name => resource_name,
141
+ :target_group => "",
142
+ :reason => :no_target_group,
143
+ :action => :relaunch
144
+ }
145
+ end
146
+ end
147
+
148
+ # Easy case first: new resources. We just want to lookup the group so that we can show that to the user
149
+ changes[:new_resources].each do |change|
150
+ group = resource_to_target_group_map[change]
151
+ if group
152
+ plan.items << {
153
+ :resource_name => change,
154
+ :target_group => group,
155
+ :reason => "new resource",
156
+ :action => :create
157
+ }
158
+ else
159
+ plan.items << {
160
+ :resource_name => change,
161
+ :target_group => "",
162
+ :reason => "new resource",
163
+ :action => :create
164
+ }
165
+
166
+ end
167
+ end
168
+
169
+ # Easy case first: new resources. We just want to lookup the group so that we can show that to the user
170
+ changes[:changes].each do |change|
171
+ group = resource_to_target_group_map[change]
172
+ if group
173
+ plan.items << {
174
+ :resource_name => change,
175
+ :target_group => group,
176
+ :reason => "non-destructive change",
177
+ :action => :change
178
+ }
179
+ else
180
+ plan.items << {
181
+ :resource_name => change,
182
+ :target_group => "",
183
+ :reason => "non-destructive change",
184
+ :action => :change
185
+ }
186
+
187
+ end
188
+ end
189
+
190
+ # Relaunches are more complex: we need to bucket resources based on group, so that we can later pick just one change from each group
191
+ changes[:relaunches].each do |change|
192
+ group = resource_to_target_group_map[change]
193
+ if group
194
+ group_changes_map[group] = (group_changes_map[group] ||= []) << change
195
+ elsif resources_not_in_any_target_group[change]
196
+ # this handles a change to a resource which does not belong to any target group
197
+ plan.items << resources_not_in_any_target_group[change]
198
+ else
199
+ # this handles the case where a resource was removed from the definition and
200
+ # now terraform wants to destroy this resource
201
+ plan.items << {
202
+ :resource_name => change,
203
+ :target_group => "",
204
+ :reason => "resource definition has been removed",
205
+ :action => :destroy
206
+ }
207
+
208
+ end
209
+ end
210
+
211
+ # Handle safe groups: just one changing resource in the group
212
+ safe_groups = group_changes_map.select { |name, resources| resources.length <= 1 }
213
+ safe_groups.each do |group_name, resources|
214
+ resources.each do |resource_name|
215
+ plan.items << {
216
+ :resource_name => resource_name,
217
+ :target_group => group_name,
218
+ :reason => "only member in its group",
219
+ :action => :relaunch
220
+ }
221
+ end
222
+ end
223
+
224
+ # Handle problematic groups: select one from each group where count(group) > 1
225
+ problematic_groups = group_changes_map.select { |name, resources| resources.length > 1 }
226
+ problematic_groups.each do |group_name, resources|
227
+ original_length = resources.length
228
+ plan.items << {
229
+ :resource_name => resources.shift,
230
+ :target_group => group_name,
231
+ :reason => "group has total #{original_length} resources. Picked this as the first",
232
+ :action => :relaunch
233
+ }
234
+
235
+ resources.each do |resource_name|
236
+ plan.items << {
237
+ :resource_name => resource_name,
238
+ :target_group => group_name,
239
+ :reason => "not selected from this group",
240
+ :action => :not_picked
241
+ }
242
+ end
243
+
244
+ end
245
+
246
+ return plan
247
+ end
248
+ end
249
+ end
250
+ end
@@ -5,7 +5,7 @@ class Biosphere
5
5
 
6
6
  class Deployment
7
7
 
8
- attr_reader :export, :name, :_settings, :feature_manifests
8
+ attr_reader :export, :name, :_settings, :feature_manifests, :target_groups, :resources
9
9
  attr_accessor :state, :node
10
10
  def initialize(*args)
11
11
 
@@ -56,6 +56,7 @@ class Biosphere
56
56
  @actions = {}
57
57
  @deployments = []
58
58
  @outputs = []
59
+ @target_groups = {}
59
60
 
60
61
  if @feature_manifests
61
62
  node[:feature_manifests] = @feature_manifests
@@ -65,6 +66,11 @@ class Biosphere
65
66
 
66
67
  end
67
68
 
69
+ def add_resource_to_target_group(resource_type, resource_name, target_group)
70
+ name = resource_type + "." + resource_name
71
+ (@target_groups[target_group] ||= []) << name
72
+ end
73
+
68
74
  def setup(settings)
69
75
  end
70
76
 
@@ -103,7 +109,7 @@ class Biosphere
103
109
  @delayed << delayed_call
104
110
  end
105
111
 
106
- def resource(type, name, &block)
112
+ def resource(type, name, target_group = nil, &block)
107
113
  if self.name
108
114
  name = self.name + "_" + name
109
115
  end
@@ -119,10 +125,14 @@ class Biosphere
119
125
  :location => caller[0] + "a"
120
126
  }
121
127
 
128
+ if target_group
129
+ add_resource_to_target_group(type, name, target_group)
130
+ end
131
+
122
132
  if block_given?
123
133
  resource[:block] = block
124
134
  else
125
- STDERR.puts("WARNING: No block set for resource call '#{type}', '#{name}' at #{caller[0]}")
135
+ #STDERR.puts("WARNING: No block set for resource call '#{type}', '#{name}' at #{caller[0]}")
126
136
  end
127
137
 
128
138
  @resources << resource
@@ -194,7 +204,9 @@ class Biosphere
194
204
  # And finish with our own resources
195
205
  @resources.each do |resource|
196
206
  proxy = ResourceProxy.new(self)
197
- proxy.instance_eval(&resource[:block])
207
+ if resource[:block]
208
+ proxy.instance_eval(&resource[:block])
209
+ end
198
210
 
199
211
  @export["resource"][resource[:type].to_s][resource[:name].to_s] = proxy.output
200
212
  end
@@ -1,3 +1,3 @@
1
1
  class Biosphere
2
- Version = "0.2.0"
2
+ Version = "0.2.1"
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.0
4
+ version: 0.2.1
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-05-09 00:00:00.000000000 Z
11
+ date: 2017-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - '='
137
137
  - !ruby/object:Gem::Version
138
138
  version: 0.0.3
139
+ - !ruby/object:Gem::Dependency
140
+ name: treetop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '='
144
+ - !ruby/object:Gem::Version
145
+ version: 1.6.8
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '='
151
+ - !ruby/object:Gem::Version
152
+ version: 1.6.8
139
153
  description: "Terraform's HCL lacks quite many programming features like iterators,
140
154
  true variables, advanced string manipulation, functions etc.\n\n This Ruby tool
141
155
  provides an easy-to-use DSL to define Terraform compatible .json files which can
@@ -149,6 +163,7 @@ files:
149
163
  - bin/biosphere
150
164
  - examples/example.rb
151
165
  - lib/biosphere.rb
166
+ - lib/biosphere/cli/terraformplanning.rb
152
167
  - lib/biosphere/deployment.rb
153
168
  - lib/biosphere/ipaddressallocator.rb
154
169
  - lib/biosphere/kube.rb