biosphere 0.2.0 → 0.2.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.
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