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 +4 -4
- data/bin/biosphere +18 -3
- data/lib/biosphere.rb +1 -0
- data/lib/biosphere/cli/terraformplanning.rb +250 -0
- data/lib/biosphere/deployment.rb +16 -4
- data/lib/biosphere/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18516adcf232287623fbc3b0a38a9fcbf3a06467
|
4
|
+
data.tar.gz: 25b96568a2cbbf6760d4cf8f5ac83c74d0111272
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a00f8ba95c94a69a16aad64144dbec2e5fecccb40a3a9078110ff3fb73215f0150a2822c1a0b395cac4234072f5fa7f239bfbda0cfec704bf5931367deb7c3ca
|
7
|
+
data.tar.gz: 1a34d3f63840b2cd4990d686d09cdc0e8cffc10084e35e176f0d1eac01967368b58acc702905c6ab2c68a845585ff86533869e9970e22c4cd34976e96e84431a
|
data/bin/biosphere
CHANGED
@@ -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
|
-
|
207
|
-
|
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
|
data/lib/biosphere.rb
CHANGED
@@ -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
|
data/lib/biosphere/deployment.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/biosphere/version.rb
CHANGED
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.
|
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-
|
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
|