fum 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.
- data/.gitignore +5 -0
- data/Gemfile +7 -0
- data/LICENSE +15 -0
- data/README.rdoc +219 -0
- data/Rakefile +1 -0
- data/bin/fum +5 -0
- data/examples/hello.fum +6 -0
- data/fum.gemspec +27 -0
- data/lib/fum.rb +12 -0
- data/lib/fum/application.rb +117 -0
- data/lib/fum/command.rb +21 -0
- data/lib/fum/command_manager.rb +44 -0
- data/lib/fum/commands/events.rb +58 -0
- data/lib/fum/commands/launch.rb +110 -0
- data/lib/fum/commands/list.rb +83 -0
- data/lib/fum/commands/repair.rb +45 -0
- data/lib/fum/commands/status.rb +45 -0
- data/lib/fum/commands/tail.rb +89 -0
- data/lib/fum/commands/template.rb +301 -0
- data/lib/fum/commands/terminate.rb +54 -0
- data/lib/fum/dns.rb +135 -0
- data/lib/fum/lang/fum_file.rb +30 -0
- data/lib/fum/lang/stage.rb +86 -0
- data/lib/fum/lang/zone.rb +33 -0
- data/lib/fum/stage_analyzer.rb +172 -0
- data/lib/fum/util.rb +10 -0
- data/lib/fum/version.rb +3 -0
- metadata +140 -0
@@ -0,0 +1,301 @@
|
|
1
|
+
require 'formatador'
|
2
|
+
require 'formatador/table'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Fum
|
6
|
+
module Commands
|
7
|
+
class Template < Fum::Command
|
8
|
+
|
9
|
+
def parse_options
|
10
|
+
options = Trollop::options do
|
11
|
+
banner "usage: template [options] <environment-id>, where options are:"
|
12
|
+
opt :options, "Describe Configuration Options"
|
13
|
+
opt :json, "Output in JSON format"
|
14
|
+
opt :application, "Application name (for creating templates only)", :type => :string
|
15
|
+
opt :description, "Description (update/create only)", :type => :string
|
16
|
+
#opt :prompt, "Prompt for options when updating or creating"
|
17
|
+
opt :stack, "Use the specified solution stack.", :type => :string
|
18
|
+
opt :create, "Create the specified template"
|
19
|
+
opt :update, "Update the specified template"
|
20
|
+
opt :delete, "Delete the specified template"
|
21
|
+
opt :list, "List all templates with some details"
|
22
|
+
opt :settings, "Print the current settings of the template"
|
23
|
+
opt :compare, "Compares two or more templates"
|
24
|
+
opt :from_template, "Base template to use when creating a new template", :type => :string
|
25
|
+
opt :from_json, "JSON File to use for create or update", :type => :string
|
26
|
+
opt :from_application, "Application to use for source template", :type => :string
|
27
|
+
end
|
28
|
+
if options[:options] || options[:settings] || options[:create] || options[:update] || options[:delete]
|
29
|
+
if ARGV.empty?
|
30
|
+
die "Please specify a template name for this operation"
|
31
|
+
else
|
32
|
+
options[:template_name] = ARGV.shift
|
33
|
+
end
|
34
|
+
elsif options[:compare]
|
35
|
+
die "Please specify two templates to compare" unless ARGV.length >= 2
|
36
|
+
options[:template_names] = ARGV.dup
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
options
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# * :type
|
46
|
+
def execute(options)
|
47
|
+
app = @application.main_decl
|
48
|
+
|
49
|
+
# TODO make sure only one of these is is set, for now do least destructive order.
|
50
|
+
if options[:settings] || options[:update] || options[:options] || options[:delete]
|
51
|
+
|
52
|
+
template = Fog::AWS[:beanstalk].templates.get(app.name, options[:template_name])
|
53
|
+
die "No configuration template named #{options[:template_name]}" if template.nil?
|
54
|
+
|
55
|
+
if options[:settings]
|
56
|
+
display_settings(template.option_settings, options)
|
57
|
+
elsif options[:options]
|
58
|
+
display_options(template.options, options)
|
59
|
+
elsif options[:update]
|
60
|
+
update_template(template, options)
|
61
|
+
elsif options[:delete]
|
62
|
+
template.destroy
|
63
|
+
puts "Deleted template #{template.name}."
|
64
|
+
end
|
65
|
+
elsif options[:create]
|
66
|
+
create_template(options)
|
67
|
+
elsif options[:compare]
|
68
|
+
compare_settings(options)
|
69
|
+
elsif options[:list]
|
70
|
+
list_templates(options)
|
71
|
+
elsif options[:stack]
|
72
|
+
begin
|
73
|
+
config_opts = Fog::AWS[:beanstalk].describe_configuration_options('SolutionStackName' => options[:stack])
|
74
|
+
config_opts = config_opts.body['DescribeConfigurationOptionsResult']['Options']
|
75
|
+
display_options(config_opts, options)
|
76
|
+
rescue Fog::AWS::ElasticBeanstalk::InvalidParameterError => ex
|
77
|
+
die ex.message
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def list_templates(options)
|
84
|
+
templates = Fog::AWS[:beanstalk].templates
|
85
|
+
|
86
|
+
templates = templates.sort_by { |a| [ a.application_name, a.name ] }
|
87
|
+
|
88
|
+
table = []
|
89
|
+
templates.each { |template|
|
90
|
+
table << {
|
91
|
+
'name' => template.name,
|
92
|
+
'application' => template.application_name,
|
93
|
+
'description' => template.description,
|
94
|
+
'created' => template.created_at,
|
95
|
+
'updated' => template.updated_at
|
96
|
+
}
|
97
|
+
}
|
98
|
+
Formatador.display_compact_table(table, %w(application name description created updated))
|
99
|
+
end
|
100
|
+
|
101
|
+
def display_settings(values, options)
|
102
|
+
values = values.sort_by { |a| [ a["Namespace"], a["OptionName"]]}
|
103
|
+
if options[:json]
|
104
|
+
# Prune nil values in JSON, or we can't update from JSON
|
105
|
+
values = values.select { |v| !v["Value"].nil? }
|
106
|
+
puts JSON.pretty_generate(values)
|
107
|
+
else
|
108
|
+
values.each { |value|
|
109
|
+
value['Namespace'] = "[bold]#{value['Namespace']}[/]"
|
110
|
+
}
|
111
|
+
Formatador.display_compact_table(values, %w(Namespace OptionName Value))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def compare_settings(options)
|
116
|
+
templates = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
|
117
|
+
table = []
|
118
|
+
all_names = {}
|
119
|
+
columns = %w(Namespace Name)
|
120
|
+
|
121
|
+
options[:template_names].each { |name|
|
122
|
+
template = Fog::AWS[:beanstalk].templates.get(@application.main_decl.name, name)
|
123
|
+
die "No configuration template named #{name}" if template.nil?
|
124
|
+
settings = template.option_settings #.sort_by { |a| [ a["Namespace"], a["OptionName"]]}
|
125
|
+
#templates[name] = settings
|
126
|
+
|
127
|
+
settings.each { |setting|
|
128
|
+
namespace = setting['Namespace']
|
129
|
+
option_name = setting['OptionName']
|
130
|
+
|
131
|
+
# Maintain a list of all unique namespace/name combinations
|
132
|
+
names = all_names[setting['Namespace']] || []
|
133
|
+
names << setting['OptionName'] unless names.include?(setting['OptionName'])
|
134
|
+
all_names[setting['Namespace']] = names
|
135
|
+
|
136
|
+
# Build Index for comparison later
|
137
|
+
|
138
|
+
templates[name][namespace][option_name] = setting['Value']
|
139
|
+
}
|
140
|
+
|
141
|
+
columns << name
|
142
|
+
}
|
143
|
+
|
144
|
+
all_names.each { |namespace, option_names|
|
145
|
+
|
146
|
+
option_names.each { |option_name|
|
147
|
+
line = {
|
148
|
+
'Namespace' => namespace,
|
149
|
+
'Name' => option_name
|
150
|
+
}
|
151
|
+
|
152
|
+
values = {}
|
153
|
+
prev_value = :init
|
154
|
+
values_differ = false
|
155
|
+
|
156
|
+
options[:template_names].each { |template_name|
|
157
|
+
value = templates[template_name][namespace][option_name]
|
158
|
+
|
159
|
+
if !values_differ && prev_value != :init
|
160
|
+
values_differ = value != prev_value
|
161
|
+
prev_value = value
|
162
|
+
end
|
163
|
+
prev_value = value if prev_value == :init
|
164
|
+
|
165
|
+
values[template_name] = value
|
166
|
+
}
|
167
|
+
|
168
|
+
if values_differ
|
169
|
+
values.each { |key, value|
|
170
|
+
values[key] = "[bold]#{value.to_s}[/]"
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
table << line.merge(values)
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
table.sort_by { |a| [ a["Namespace"], a["Name"]]}
|
179
|
+
|
180
|
+
Formatador.display_compact_table(table, columns)
|
181
|
+
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
def display_options(values, options)
|
186
|
+
values = values.sort_by { |a| [ a["Namespace"], a["Name"]]}
|
187
|
+
|
188
|
+
if options[:json]
|
189
|
+
puts JSON.pretty_generate(values)
|
190
|
+
else
|
191
|
+
values.each { |value|
|
192
|
+
constraints = ''
|
193
|
+
if value['MinValue'] && value['MaxValue']
|
194
|
+
constraints = "Range(#{value['MinValue']}-#{value['MaxValue']})"
|
195
|
+
elsif value['ValueOptions']
|
196
|
+
type = value['ValueType'] == 'List' ? 'List' : 'Scalar'
|
197
|
+
constraints = "#{type}(#{value['ValueOptions'].join(', ')})"
|
198
|
+
elsif value['Regex']
|
199
|
+
constraints = "Regex(#{value['Regex']['Label']} = #{value['Regex']['Pattern']})"
|
200
|
+
elsif value['ValueType'] == 'Boolean'
|
201
|
+
constraints = "Boolean(true, false)"
|
202
|
+
end
|
203
|
+
|
204
|
+
if value['MaxLength']
|
205
|
+
max_length_constraint = "MaxLength(#{value['MaxLength']})"
|
206
|
+
constraints += ', ' unless constraints.empty?
|
207
|
+
constraints += max_length_constraint
|
208
|
+
end
|
209
|
+
value['Constraints'] = constraints
|
210
|
+
}
|
211
|
+
Formatador.display_compact_table(values, %w(Namespace Name DefaultValue Constraints))
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
def create_template(options)
|
217
|
+
|
218
|
+
die "Must specify application name " if options[:application].nil?
|
219
|
+
|
220
|
+
create_opts = {
|
221
|
+
:name => options[:template_name],
|
222
|
+
:application_name => options[:application]
|
223
|
+
}
|
224
|
+
|
225
|
+
create_opts[:solution_stack_name] = options[:stack] if options[:stack]
|
226
|
+
create_opts[:description] = options[:description] unless options[:description].nil?
|
227
|
+
create_opts[:source_configuration] = {
|
228
|
+
'ApplicationName' => options[:from_application].nil? ? options[:application] : options[:from_application],
|
229
|
+
'TemplateName' => options[:from_template]
|
230
|
+
} if options[:from_template]
|
231
|
+
|
232
|
+
begin
|
233
|
+
template = Fog::AWS[:beanstalk].templates.create(create_opts)
|
234
|
+
puts "Created template #{template.name}"
|
235
|
+
rescue Fog::AWS::ElasticBeanstalk::InvalidParameterError => ex
|
236
|
+
die ex
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
def update_template(template, options)
|
242
|
+
|
243
|
+
settings = []
|
244
|
+
if options[:from_json]
|
245
|
+
settings = JSON.parse(File.read(options[:from_json]))
|
246
|
+
# TODO add some sanity checks, verify array of hashes, etc.
|
247
|
+
end
|
248
|
+
new_attributes = {
|
249
|
+
:option_settings => settings
|
250
|
+
}
|
251
|
+
new_attributes[:description] = options[:description] unless options[:description].nil?
|
252
|
+
|
253
|
+
begin
|
254
|
+
template.modify(new_attributes)
|
255
|
+
puts "Updated template #{template.name}"
|
256
|
+
rescue Fog::AWS::ElasticBeanstalk::InvalidParameterError => ex
|
257
|
+
die ex
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
# Work in progress on prompting for options
|
265
|
+
def prompt_options(template, options)
|
266
|
+
require 'highline/import'
|
267
|
+
opts = template.options.sort_by { |a| [ a["Namespace"], a["Name"]]}
|
268
|
+
option_settings = template.option_settings
|
269
|
+
|
270
|
+
opts.each { |option|
|
271
|
+
|
272
|
+
answer_type = nil
|
273
|
+
if option['MinValue'] && option['MaxValue']
|
274
|
+
answer_type = Integer
|
275
|
+
elsif option['ValueOptions']
|
276
|
+
#type = value['ValueType'] == 'List' ? 'List' : 'Scalar'
|
277
|
+
answer_type = option['ValueOptions']
|
278
|
+
answer_type << ""
|
279
|
+
elsif option['Regex']
|
280
|
+
#answer_type = "Regex(#{value['Regex']['Label']} = #{value['Regex']['Pattern']})"
|
281
|
+
elsif option['ValueType'] == 'Boolean'
|
282
|
+
answer_type = %w(true false)
|
283
|
+
end
|
284
|
+
|
285
|
+
value = ask("#{option['Namespace']}:#{option['Name']} ? ", answer_type) { |q|
|
286
|
+
setting = option_settings.select { |a| a['OptionName'] == option['Name'] && a['Namespace'] == option['Namespace']}.shift
|
287
|
+
q.default = setting['Value'] unless setting.nil?
|
288
|
+
q.readline = true
|
289
|
+
if option['ValueType'] == 'List'
|
290
|
+
q.gather = ''
|
291
|
+
end
|
292
|
+
|
293
|
+
}
|
294
|
+
#pp value
|
295
|
+
}
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Fum
|
2
|
+
module Commands
|
3
|
+
class Terminate < Fum::Command
|
4
|
+
|
5
|
+
def parse_options
|
6
|
+
opts = Trollop::options do
|
7
|
+
banner "usage: terminate [options] <stage>, where options are:"
|
8
|
+
opt :all, "Terminate all environments for this stage (use with caution)."
|
9
|
+
end
|
10
|
+
if ARGV.empty?
|
11
|
+
die "Please specify a stage name type to terminate."
|
12
|
+
end
|
13
|
+
opts[:stage_name] = ARGV.shift
|
14
|
+
opts
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# * :type
|
20
|
+
def execute(options)
|
21
|
+
stage_name = options[:stage_name]
|
22
|
+
stage_decl = stage(@application.main_decl, stage_name)
|
23
|
+
|
24
|
+
analyzer = StageAnalyzer.new(stage_decl)
|
25
|
+
|
26
|
+
analyzer.analyze(options)
|
27
|
+
|
28
|
+
env_info = analyzer.env_map.values
|
29
|
+
|
30
|
+
targets = []
|
31
|
+
|
32
|
+
if options[:all]
|
33
|
+
targets = env_info.map { |e| e[:env] }
|
34
|
+
else
|
35
|
+
targets = env_info.select { |e| e[:state] == :inactive }.map { |e| e[:env] }
|
36
|
+
end
|
37
|
+
|
38
|
+
if targets.length > 0
|
39
|
+
targets.each { |target|
|
40
|
+
if target.ready?
|
41
|
+
puts "Terminating inactive environment #{target.name}."
|
42
|
+
target.destroy
|
43
|
+
end
|
44
|
+
|
45
|
+
}
|
46
|
+
else
|
47
|
+
puts "No environments to terminate."
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/fum/dns.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
module Fum
|
2
|
+
|
3
|
+
# Mixin for common DNS methods
|
4
|
+
module DNS
|
5
|
+
|
6
|
+
require 'fog'
|
7
|
+
|
8
|
+
def dns
|
9
|
+
Fog::DNS[:AWS]
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_zones(stage_decl, env, options)
|
13
|
+
zones = stage_decl.zones
|
14
|
+
|
15
|
+
hosted_zone_name_id = nil
|
16
|
+
dns_name = nil
|
17
|
+
|
18
|
+
unless options[:noop]
|
19
|
+
lb = env.load_balancer
|
20
|
+
|
21
|
+
hosted_zone_name_id = lb.hosted_zone_name_id
|
22
|
+
dns_name = lb.dns_name
|
23
|
+
end
|
24
|
+
|
25
|
+
zones.each { |zone| update_zone(zone, hosted_zone_name_id, dns_name, env.cname, options) }
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_zone(zone_decl, hosted_zone_name_id, dns_name, env_cname, options)
|
30
|
+
dns = Fog::DNS[:aws]
|
31
|
+
|
32
|
+
zone = dns.zones.all({:domain => zone_decl.name}).shift
|
33
|
+
die "Could not find zone #{zone_decl.name} in account." unless zone
|
34
|
+
|
35
|
+
puts "Updating records in zone #{zone.domain}"
|
36
|
+
create_list = []
|
37
|
+
modify_list = []
|
38
|
+
|
39
|
+
zone_decl.records.each { |record|
|
40
|
+
fqdn = "#{record[:name]}.#{zone_decl.name}."
|
41
|
+
|
42
|
+
create_opts = {
|
43
|
+
:name => fqdn,
|
44
|
+
:type => record[:type]
|
45
|
+
}
|
46
|
+
|
47
|
+
case record[:type]
|
48
|
+
when 'CNAME'
|
49
|
+
create_opts[:value] = case record[:target]
|
50
|
+
when :elb
|
51
|
+
ensure_trailing_dot(dns_name)
|
52
|
+
when :env
|
53
|
+
ensure_trailing_dot(env_cname)
|
54
|
+
end
|
55
|
+
when 'A'
|
56
|
+
create_opts[:alias_target] = {
|
57
|
+
:hosted_zone_id => hosted_zone_name_id,
|
58
|
+
:dns_name => dns_name
|
59
|
+
}
|
60
|
+
else
|
61
|
+
raise RuntimeError, "Unknown type #{record[:type]}"
|
62
|
+
end
|
63
|
+
|
64
|
+
existing = find_records(zone, fqdn)
|
65
|
+
|
66
|
+
if existing.length > 1
|
67
|
+
# We do not currently handle this case, which would occur if AAAA records exist or weighted/latency records used.
|
68
|
+
puts "Cannot update record #{fqdn} in zone #{zone} because more than one A, AAAA, or CNAME record already exists."
|
69
|
+
end
|
70
|
+
|
71
|
+
if existing.length == 0
|
72
|
+
create_list << create_opts
|
73
|
+
else
|
74
|
+
modify_list << {
|
75
|
+
:record => existing.shift,
|
76
|
+
:create_opts => create_opts
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
}
|
81
|
+
|
82
|
+
# We do not currently do this atomically, but one record at a time. Should probably move to atomic at some
|
83
|
+
# point.
|
84
|
+
|
85
|
+
new_records = []
|
86
|
+
|
87
|
+
create_list.each { |create_options|
|
88
|
+
puts "Creating #{create_options[:type]} record with name #{create_options[:name]} in zone #{zone.domain}"
|
89
|
+
unless options[:noop]
|
90
|
+
new_records << zone.records.create(create_options)
|
91
|
+
end
|
92
|
+
}
|
93
|
+
|
94
|
+
modify_list.each { |record|
|
95
|
+
puts "Updating #{record[:create_opts][:type]} record with name #{record[:create_opts][:name]} in zone #{zone.domain}"
|
96
|
+
unless options[:noop]
|
97
|
+
record[:record].modify(record[:create_opts])
|
98
|
+
new_records << record[:record]
|
99
|
+
end
|
100
|
+
}
|
101
|
+
|
102
|
+
puts "Waiting for DNS records to sync in zone #{zone.domain}..."
|
103
|
+
# Wait for records to be ready.
|
104
|
+
new_records.each { |record|
|
105
|
+
record.wait_for { record.ready? }
|
106
|
+
}
|
107
|
+
puts "Updated records are now in sync in zone #{zone.domain}"
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def find_records(zone, name)
|
113
|
+
existing_records = zone.records.all({:name => name})
|
114
|
+
|
115
|
+
matching = []
|
116
|
+
types = ['A', 'AAAA', 'CNAME']
|
117
|
+
existing_records.each { |record|
|
118
|
+
matching << record if record.name == name && types.include?(record.type)
|
119
|
+
}
|
120
|
+
matching
|
121
|
+
end
|
122
|
+
|
123
|
+
# Appends a trailing . to the given name if it doesn't have one
|
124
|
+
def ensure_trailing_dot(name)
|
125
|
+
name = "#{name}." unless name.nil? || name.end_with?(".")
|
126
|
+
name
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns true if the specified dns names equal, ignoring any trailing "."
|
130
|
+
def dns_names_equal(a, b)
|
131
|
+
ensure_trailing_dot(a) == ensure_trailing_dot(b)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|