miasma-terraform 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5c46a5855cfe72db0105261f00f0f48e36916aaa
4
+ data.tar.gz: 5fb33600986aa116c94a7ab1b7eb17abee71912a
5
+ SHA512:
6
+ metadata.gz: ee7697f996ecf4816f33c43fc26ab203db407347cd82a7725d4ed26368eca99ae9b894f69de6a41fc41137635400d39ab4ac4c98258641467530e3eb0ac60029
7
+ data.tar.gz: ee293821c6ae291fdcfe35a7cd391fa90d8c7b1e0a8cd24899a7a7633a01fb9943d20beca18767f513d857da17047821601889a031bd469842e970538d9f9398
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial release
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2017 Chris Roberts
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Miasma Terraform
2
+
3
+ Terraform API plugin for the miasma cloud library
4
+
5
+ ## Supported credential attributes:
6
+
7
+ Supported attributes used in the credentials section of API
8
+ configurations:
9
+
10
+ ```ruby
11
+ Miasma.api(
12
+ :type => :orchestration,
13
+ :provider => :terraform,
14
+ :credentials => {
15
+ ...
16
+ }
17
+ )
18
+ ```
19
+
20
+ ### Common attributes
21
+
22
+ * `terraform_driver` - Interface to use (`atlas`, `boule`, `local`)
23
+
24
+ ### Atlas attributes
25
+
26
+ _NOTE: Atlas support not yet available_
27
+
28
+ ```ruby
29
+ Miasma.api(
30
+ :type => :orchestration,
31
+ :provider => :terraform,
32
+ :credentials => {
33
+ :terraform_driver => :atlas
34
+ ...
35
+ }
36
+ )
37
+ ```
38
+
39
+ * `terraform_atlas_endpoint` - Atlas URL
40
+ * `terraform_atlas_token` - Atlas token
41
+
42
+ ### Boule attributes
43
+
44
+ ```ruby
45
+ Miasma.api(
46
+ :type => :orchestration,
47
+ :provider => :terraform,
48
+ :credentials => {
49
+ :terraform_driver => :boule
50
+ ...
51
+ }
52
+ )
53
+ ```
54
+
55
+ * `terraform_boule_endpoint` - Boule URL
56
+
57
+ ### Local attributes
58
+
59
+ ```ruby
60
+ Miasma.api(
61
+ :type => :orchestration,
62
+ :provider => :terraform,
63
+ :credentials => {
64
+ :terraform_driver => :local
65
+ ...
66
+ }
67
+ )
68
+ ```
69
+
70
+ * `terraform_local_directory` - Path to store stack data
71
+ * `terraform_local_scrub_destroyed` - Delete stack data directory on destroy
72
+
73
+ ## Current support matrix
74
+
75
+ |Model |Create|Read|Update|Delete|
76
+ |--------------|------|----|------|------|
77
+ |AutoScale | | | | |
78
+ |BlockStorage | | | | |
79
+ |Compute | | | | |
80
+ |DNS | | | | |
81
+ |LoadBalancer | | | | |
82
+ |Network | | | | |
83
+ |Orchestration | X | X | X | X |
84
+ |Queues | | | | |
85
+ |Storage | | | | |
86
+
87
+ ## Info
88
+ * Repository: https://github.com/miasma-rb/miasma-terraform
@@ -0,0 +1,3 @@
1
+ require 'miasma'
2
+ require 'miasma-terraform/version'
3
+ require 'miasma-terraform/stack'
@@ -0,0 +1,579 @@
1
+ require 'open3'
2
+ require 'stringio'
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
6
+ module MiasmaTerraform
7
+ class Stack
8
+
9
+ @@running_actions = []
10
+
11
+ # Register action to be cleaned up at exit
12
+ #
13
+ # @param action [Action]
14
+ # @return [NilClass]
15
+ def self.register_action(action)
16
+ unless(@@running_actions.include?(action))
17
+ @@running_actions.push(action)
18
+ end
19
+ nil
20
+ end
21
+
22
+ # Deregister action from at exit cleanup
23
+ #
24
+ # @param action [Action]
25
+ # @return [NilClass]
26
+ def self.deregister_action(action)
27
+ @@running_actions.delete(action)
28
+ nil
29
+ end
30
+
31
+ # Wait for all actions to complete
32
+ def self.cleanup_actions!
33
+ @@running_actions.map(&:complete!)
34
+ nil
35
+ end
36
+
37
+ # @return [Array<String>]
38
+ def self.list(container)
39
+ if(container.to_s.empty?)
40
+ raise ArgumentError.new 'Container directory must be set!'
41
+ end
42
+ if(File.directory?(container))
43
+ Dir.new(container).map do |entry|
44
+ next if entry.start_with?('.')
45
+ entry if File.directory?(File.join(container, entry))
46
+ end.compact
47
+ else
48
+ []
49
+ end
50
+ end
51
+
52
+ class Error < StandardError
53
+ class Busy < Error; end
54
+ class NotFound < Error; end
55
+ class CommandFailed < Error; end
56
+ class ValidateError < Error; end
57
+ end
58
+
59
+ REQUIRED_ATTRIBUTES = [:name, :container]
60
+
61
+ class Action
62
+ attr_reader :stdin, :waiter, :command, :options
63
+
64
+ # Create a new action to run
65
+ #
66
+ # @param command [String]
67
+ # @param opts [Hash]
68
+ # @return [self]
69
+ def initialize(command, opts={})
70
+ @command = command.dup.freeze
71
+ @options = opts.to_smash
72
+ @io_callbacks = []
73
+ @complete_callbacks = []
74
+ @start_callbacks = []
75
+ @cached_output = Smash.new(
76
+ :stdout => StringIO.new(''),
77
+ :stderr => StringIO.new('')
78
+ )
79
+ if(@options.delete(:auto_start))
80
+ start!
81
+ end
82
+ end
83
+
84
+ # Start the process
85
+ def start!
86
+ opts = Hash[@options.map{|k,v| [k.to_sym,v]}]
87
+ MiasmaTerraform::Stack.register_action(self)
88
+ @stdin, @stdout, @stderr, @waiter = Open3.popen3(@command, **opts)
89
+ @start_callbacks.each do |callback|
90
+ callback.call(self)
91
+ end
92
+ unless(@io_callbacks.empty? && @complete_callbacks.empty?)
93
+ manage_process!
94
+ end
95
+ true
96
+ end
97
+
98
+ # Wait for the process to complete
99
+ #
100
+ # @return [Process::Status]
101
+ def complete!
102
+ start! unless waiter
103
+ if(@process_manager)
104
+ @process_manager.join
105
+ end
106
+ result = waiter.value
107
+ MiasmaTerraform::Stack.deregister_action(self)
108
+ result
109
+ end
110
+
111
+ # @return [IO] stderr stream
112
+ def stderr
113
+ if(@stderr == :managed_io)
114
+ @cached_output[:stderr]
115
+ else
116
+ @stderr
117
+ end
118
+ end
119
+
120
+ # @return [IO] stdout stream
121
+ def stdout
122
+ if(@stdout == :managed_io)
123
+ @cached_output[:stdout]
124
+ else
125
+ @stdout
126
+ end
127
+ end
128
+
129
+ # Register a block to be run when process output
130
+ # is received
131
+ #
132
+ # @yieldparam line [String] output line
133
+ # @yieldparam type [Symbol] output type (:stdout or :stderr)
134
+ def on_io(&block)
135
+ @io_callbacks << block
136
+ self
137
+ end
138
+
139
+ # Register a block to be run when a process completes
140
+ #
141
+ # @yieldparam result [Process::Status]
142
+ # @yieldparam self [Action]
143
+ def on_complete(&block)
144
+ @complete_callbacks << block
145
+ self
146
+ end
147
+
148
+ # Register a block to be run when a process starts
149
+ #
150
+ # @yieldparam self [Action]
151
+ def on_start(&block)
152
+ @start_callbacks << block
153
+ self
154
+ end
155
+
156
+ protected
157
+
158
+ # Start reader thread for handling managed process output
159
+ def manage_process!
160
+ unless(@process_manager)
161
+ unless(@io_callbacks.empty?)
162
+ io_stdout = @stdout
163
+ io_stderr = @stderr
164
+ @stdout = @stderr = :managed_io
165
+ end
166
+ @process_manager = Thread.new do
167
+ if(io_stdout && io_stderr)
168
+ while(waiter.alive?)
169
+ IO.select([io_stdout, io_stderr])
170
+ [io_stdout, io_stderr].each do |io|
171
+ begin
172
+ content = io.read_nonblock(102400)
173
+ type = io == io_stdout ? :stdout : :stderr
174
+ @cached_output[type] << content
175
+ content = content.split("\n")
176
+ @io_callbacks.each do |callback|
177
+ content.each do |line|
178
+ callback.call(line, type)
179
+ end
180
+ end
181
+ rescue IO::WaitReadable, EOFError
182
+ # ignore
183
+ end
184
+ end
185
+ end
186
+ end
187
+ result = waiter.value
188
+ @complete_callbacks.each do |callback|
189
+ callback.call(result, self)
190
+ end
191
+ MiasmaTerraform::Stack.deregister_action(self)
192
+ result
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ attr_reader :actions
199
+ attr_reader :directory
200
+ attr_reader :container
201
+ attr_reader :name
202
+ attr_reader :bin
203
+ attr_reader :scrub_destroyed
204
+
205
+ def initialize(opts={})
206
+ @options = opts.to_smash
207
+ init!
208
+ @actions = []
209
+ @name = @options[:name]
210
+ @container = @options[:container]
211
+ @scrub_destroyed = @options.fetch(:scrub_destroyed, false)
212
+ @directory = File.join(container, name)
213
+ @bin = @options.fetch(:bin, 'terraform')
214
+ end
215
+
216
+ # @return [TrueClass, FalseClass] stack currently exists
217
+ def exists?
218
+ File.directory?(directory)
219
+ end
220
+
221
+ # @return [TrueClass, FalseClass] stack is currently active
222
+ def active?
223
+ actions.any? do |action|
224
+ action.waiter.alive?
225
+ end
226
+ end
227
+
228
+ # Save the TF stack
229
+ def save(opts={})
230
+ save_opts = opts.to_smash
231
+ type = exists? ? "update" : "create"
232
+ lock_stack
233
+ write_file(tf_path, save_opts[:template].to_json)
234
+ write_file(tfvars_path, save_opts[:parameters].to_json)
235
+ action = run_action('apply')
236
+ store_events(action)
237
+ action.on_start do |_|
238
+ update_info do |info|
239
+ info["state"] = "#{type}_in_progress"
240
+ info
241
+ end
242
+ end
243
+ action.on_complete do |status, this_action|
244
+ update_info do |info|
245
+ if(type == "create")
246
+ info["created_at"] = (Time.now.to_f * 1000).floor
247
+ end
248
+ info["updated_at"] = (Time.now.to_f * 1000).floor
249
+ info["state"] = status.success? ? "#{type}_complete" : "#{type}_failed"
250
+ info
251
+ end
252
+ unlock_stack
253
+ end
254
+ action.start!
255
+ true
256
+ end
257
+
258
+ # @return [Array<Hash>] resource list
259
+ def resources
260
+ must_exist do
261
+ if(has_state?)
262
+ action = run_action('state list', :auto_start)
263
+ # wait for action to complete
264
+ action.complete!
265
+ successful_action(action) do
266
+ resource_lines = action.stdout.read.split("\n").find_all do |line|
267
+ line.match(/^[^\s]/)
268
+ end
269
+ resource_lines.map do |line|
270
+ parts = line.split('.')
271
+ resource_info = Smash.new(
272
+ :type => parts[0],
273
+ :name => parts[1],
274
+ :status => 'UPDATE_COMPLETE'
275
+ )
276
+ action = run_action("state show #{line}", :auto_start)
277
+ action.complete!
278
+ successful_action(action) do
279
+ info = Smash.new
280
+ action.stdout.read.split("\n").each do |line|
281
+ parts = line.split('=').map(&:strip)
282
+ next if parts.size != 2
283
+ info[parts[0]] = parts[1]
284
+ end
285
+ resource_info[:physical_id] = info[:id] if info[:id]
286
+ end
287
+ resource_info
288
+ end
289
+ end
290
+ else
291
+ []
292
+ end
293
+ end
294
+ end
295
+
296
+ # @return [Array<Hash>] events list
297
+ def events
298
+ must_exist do
299
+ load_info.fetch(:events, []).map do |item|
300
+ new_item = item.dup
301
+ parts = item[:resource_name].to_s.split('.')
302
+ new_item[:resource_name] = parts[1]
303
+ new_item[:resource_type] = parts[0]
304
+ new_item
305
+ end
306
+ end
307
+ end
308
+
309
+ # @return [Hash] stack outputs
310
+ def outputs
311
+ must_exist do
312
+ if(has_state?)
313
+ action = run_action('output -json', :auto_start)
314
+ action.complete!
315
+ successful_action(action) do
316
+ result = JSON.parse(action.stdout.read).to_smash.map do |key, info|
317
+ [key, info[:value]]
318
+ end
319
+ Smash[result]
320
+ end
321
+ else
322
+ Smash.new
323
+ end
324
+ end
325
+ end
326
+
327
+ # @return [String] current stack template
328
+ def template
329
+ must_exist do
330
+ if(File.exists?(tf_path))
331
+ File.read(tf_path)
332
+ else
333
+ "{}"
334
+ end
335
+ end
336
+ end
337
+
338
+ # @return [Hash] stack information
339
+ def info
340
+ must_exist do
341
+ stack_data = load_info
342
+ Smash.new(
343
+ :id => name,
344
+ :name => name,
345
+ :state => stack_data[:state].to_s,
346
+ :status => stack_data[:state].to_s.upcase,
347
+ :updated_time => stack_data[:updated_at],
348
+ :creation_time => stack_data[:created_at],
349
+ :outputs => outputs
350
+ )
351
+ end
352
+ end
353
+
354
+ def validate(template)
355
+ errors = []
356
+ root_path = Dir.mktmpdir('miasma-')
357
+ template_path = File.join(root_path, 'main.tf')
358
+ File.write(template_path, template)
359
+ action = run_action('validate')
360
+ action.options[:chdir] = root_path
361
+ action.on_io do |line, type|
362
+ if(line.start_with?('*'))
363
+ errors << line
364
+ end
365
+ end.on_complete do |_|
366
+ FileUtils.rm_rf(root_path)
367
+ end
368
+ action.complete!
369
+ errors
370
+ end
371
+
372
+ # @return [TrueClass] destroy this stack
373
+ def destroy!
374
+ must_exist do
375
+ lock_stack
376
+ action = run_action('destroy -force')
377
+ action.on_start do |_|
378
+ update_info do |info|
379
+ info[:state] = "delete_in_progress"
380
+ info
381
+ end
382
+ end
383
+ action.on_complete do |*_|
384
+ unlock_stack
385
+ end
386
+ action.on_complete do |result, _|
387
+ unless(result.success?)
388
+ update_info do |info|
389
+ info[:state] = "delete_failed"
390
+ info
391
+ end
392
+ else
393
+ update_info do |info|
394
+ info[:state] = "delete_complete"
395
+ info
396
+ end
397
+ FileUtils.rm_rf(directory) if scrub_destroyed
398
+ end
399
+ end
400
+ action.start!
401
+ end
402
+ true
403
+ end
404
+
405
+ protected
406
+
407
+ # Start running a terraform process
408
+ def run_action(cmd, auto_start=false)
409
+ action = Action.new("#{bin} #{cmd} -no-color", :chdir => directory)
410
+ action.on_start do |this_action|
411
+ actions << this_action
412
+ end
413
+ action.on_complete do |_, this_action|
414
+ actions.delete(this_action)
415
+ end
416
+ action.start! if auto_start
417
+ action
418
+ end
419
+
420
+ # Validate stack exists before running block
421
+ def must_exist(lock=false)
422
+ if(exists?)
423
+ if(lock)
424
+ lock_stack do
425
+ yield
426
+ end
427
+ else
428
+ yield
429
+ end
430
+ else
431
+ raise Error::NotFound.new "Stack does not exist `#{name}`"
432
+ end
433
+ end
434
+
435
+ # Lock stack and run block
436
+ def lock_stack
437
+ FileUtils.mkdir_p(directory)
438
+ @lock_file = File.open(lock_path, File::RDWR|File::CREAT)
439
+ if(@lock_file.flock(File::LOCK_EX | File::LOCK_NB))
440
+ if(block_given?)
441
+ result = yield
442
+ @lock_file.flock(File::LOCK_UN)
443
+ @lock_file = nil
444
+ result
445
+ else
446
+ true
447
+ end
448
+ else
449
+ raise Error::Busy.new "Failed to aquire process lock for `#{name}`. Stack busy."
450
+ end
451
+ end
452
+
453
+ # Unlock stack
454
+ def unlock_stack
455
+ if(@lock_file)
456
+ @lock_file.flock(File::LOCK_UN)
457
+ @lock_file = nil
458
+ true
459
+ else
460
+ false
461
+ end
462
+ end
463
+
464
+ # @return [String] path to template file
465
+ def tf_path
466
+ File.join(directory, 'main.tf')
467
+ end
468
+
469
+ # @return [String] path to variables file
470
+ def tfvars_path
471
+ File.join(directory, 'terraform.tfvars')
472
+ end
473
+
474
+ # @return [String] path to internal info file
475
+ def info_path
476
+ File.join(directory, '.info.json')
477
+ end
478
+
479
+ # @return [String] path to state file
480
+ def tfstate_path
481
+ File.join(directory, 'terraform.tfstate')
482
+ end
483
+
484
+ # @return [TrueClass, FalseClass] stack has state
485
+ def has_state?
486
+ File.exists?(tfstate_path)
487
+ end
488
+
489
+ # @return [String] path to lock file
490
+ def lock_path
491
+ File.join(directory, '.lck')
492
+ end
493
+
494
+ # @return [Smash] stack info
495
+ def load_info
496
+ if(File.exists?(info_path))
497
+ result = JSON.parse(File.read(info_path)).to_smash
498
+ else
499
+ result = Smash.new
500
+ end
501
+ result[:created_at] = (Time.now.to_f * 1000).floor unless result[:created_at]
502
+ result[:state] = 'unknown' unless result[:state]
503
+ result
504
+ end
505
+
506
+ # @return [TrueClass]
507
+ def update_info
508
+ result = yield(load_info)
509
+ write_file(info_path, result.to_json)
510
+ true
511
+ end
512
+
513
+ # Raise exception if action was not completed successfully
514
+ def successful_action(action)
515
+ status = action.complete!
516
+ unless(status.success?)
517
+ raise Error::CommandFailed.new "Command failed `#{action.command}` - #{action.stderr.read}"
518
+ else
519
+ yield
520
+ end
521
+ end
522
+
523
+ # Store stack events generated by action
524
+ #
525
+ # @param action [Action]
526
+ def store_events(action)
527
+ action.on_io do |line, type|
528
+ result = line.match(/^(\*\s+)?(?<name>[^\s]+): (?<status>.+)$/)
529
+ if(result)
530
+ resource_name = result["name"]
531
+ resource_status = result["status"]
532
+ event = Smash.new(
533
+ :timestamp => (Time.now.to_f * 1000).floor,
534
+ :resource_name => resource_name,
535
+ :resource_status => resource_status,
536
+ :id => SecureRandom.uuid
537
+ )
538
+ update_info do |info|
539
+ info[:events] ||= []
540
+ info[:events].unshift(event)
541
+ info
542
+ end
543
+ end
544
+ end
545
+ end
546
+
547
+ # Validate initialization
548
+ def init!
549
+ missing_attrs = REQUIRED_ATTRIBUTES.find_all do |key|
550
+ !@options[key]
551
+ end
552
+ unless(missing_attrs.empty?)
553
+ raise ArgumentError.new("Missing required attributes: #{missing_attrs.sort}")
554
+ end
555
+ # TODO: Add tf bin check
556
+ end
557
+
558
+ # File write helper that proxies via temporary file
559
+ # to prevent corrupted writes on unexpected interrupt
560
+ #
561
+ # @param path [String] path to file
562
+ # @param contents [String] contents of file
563
+ # @return [TrueClass]
564
+ def write_file(path, contents=nil)
565
+ tmp_file = Tempfile.new('miasma')
566
+ yield(tmp_file) if block_given?
567
+ tmp_file.print(contents.to_s) if contents
568
+ tmp_file.close
569
+ FileUtils.mv(tmp_file.path, path)
570
+ true
571
+ end
572
+
573
+ end
574
+ end
575
+
576
+ # TODO: Don't be lazy and remove this setting
577
+ Thread.abort_on_exception = true
578
+
579
+ Kernel.at_exit{ MiasmaTerraform::Stack.cleanup_actions! }
@@ -0,0 +1,4 @@
1
+ module MiasmaTerraform
2
+ # Current library version
3
+ VERSION = Gem::Version.new('0.1.0')
4
+ end
@@ -0,0 +1,52 @@
1
+ require 'miasma'
2
+ require 'miasma-terraform'
3
+
4
+ module Miasma
5
+ module Contrib
6
+
7
+ # Terraform API core helper
8
+ class TerraformApiCore
9
+
10
+ # Common API methods
11
+ module ApiCommon
12
+
13
+ # Set attributes into model
14
+ #
15
+ # @param klass [Class]
16
+ def self.included(klass)
17
+ klass.class_eval do
18
+ attribute :terraform_driver, String, :required => true,
19
+ :allowed_values => ['atlas', 'boule', 'local'], :default => 'local',
20
+ :coerce => lambda{|v| v.to_s }
21
+ # Attributes required for Atlas driver
22
+ attribute :terraform_atlas_endpoint, String
23
+ attribute :terraform_atlas_token, String
24
+ # Attributes required for Boule driver
25
+ attribute :terraform_boule_endpoint, String
26
+ # Attributes required for local driver
27
+ attribute :terraform_local_directory, String
28
+ attribute :terraform_local_scrub_destroyed, [TrueClass, FalseClass], :default => false
29
+ end
30
+ end
31
+
32
+ def custom_setup(creds)
33
+ begin
34
+ driver_module = Miasma::Models::Orchestration::Terraform.const_get(
35
+ Bogo::Utility.camel(creds[:terraform_driver].to_s)
36
+ )
37
+ extend driver_module
38
+ rescue NameError
39
+ raise NotImplementedError.new "Requested driver not implemented `#{creds[:terraform_driver]}`"
40
+ end
41
+ end
42
+
43
+ def endpoint
44
+ terraform_boule_endpoint
45
+ end
46
+ end
47
+
48
+ end
49
+ end
50
+
51
+ Models::Orchestration.autoload :Terraform, 'miasma/contrib/terraform/orchestration'
52
+ end
@@ -0,0 +1,441 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+ module Models
5
+ class Orchestration
6
+ class Terraform < Orchestration
7
+
8
+ include Miasma::Contrib::TerraformApiCore::ApiCommon
9
+
10
+ module Local
11
+
12
+ # Generate wrapper stack
13
+ def terraform_stack(stack)
14
+ if(terraform_local_directory.to_s.empty?)
15
+ raise ArgumentError.new 'Attribute `terraform_local_directory` must be set for local mode usage'
16
+ end
17
+ tf_stack = memoize(stack.name, :direct) do
18
+ MiasmaTerraform::Stack.new(
19
+ :name => stack.name,
20
+ :container => terraform_local_directory,
21
+ :scrub_destroyed => terraform_local_scrub_destroyed
22
+ )
23
+ end
24
+ if(block_given?)
25
+ begin
26
+ yield(tf_stack)
27
+ rescue MiasmaTerraform::Stack::Error::NotFound
28
+ raise Miasma::Error::ApiError::RequestError.new(
29
+ "Failed to locate stack `#{stack.name}`",
30
+ :response => OpenStruct.new(:code => 404)
31
+ )
32
+ end
33
+ else
34
+ tf_stack
35
+ end
36
+ end
37
+
38
+ # Save the stack
39
+ #
40
+ # @param stack [Models::Orchestration::Stack]
41
+ # @return [Models::Orchestration::Stack]
42
+ def stack_save(stack)
43
+ tf_stack = nil
44
+ begin
45
+ tf_stack = terraform_stack(stack) do |tf|
46
+ tf.info
47
+ tf
48
+ end
49
+ rescue Miasma::Error::ApiError::RequestError
50
+ if(stack.persisted?)
51
+ raise
52
+ else
53
+ tf_stack = nil
54
+ end
55
+ end
56
+ if(!stack.persisted? && tf_stack)
57
+ raise Miasma::Error::ApiError::RequestError.new(
58
+ "Stack already exists `#{stack.name}`",
59
+ :response => OpenStruct.new(:code => 405)
60
+ )
61
+ end
62
+ tf_stack = terraform_stack(stack) unless tf_stack
63
+ tf_stack.save(
64
+ :template => stack.template,
65
+ :parameters => stack.parameters || {}
66
+ )
67
+ stack.id = stack.name
68
+ stack.valid_state
69
+ end
70
+
71
+ # Reload the stack data from the API
72
+ #
73
+ # @param stack [Models::Orchestration::Stack]
74
+ # @return [Models::Orchestration::Stack]
75
+ def stack_reload(stack)
76
+ if(stack.persisted?)
77
+ terraform_stack(stack) do |tf_stack|
78
+ s = tf_stack.info
79
+ stack.load_data(
80
+ :id => s[:id],
81
+ :created => s[:creation_time].to_s.empty? ? nil : Time.at(s[:creation_time].to_i / 1000.0),
82
+ :description => s[:description],
83
+ :name => s[:name],
84
+ :state => s[:status].downcase.to_sym,
85
+ :status => s[:status],
86
+ :status_reason => s[:stack_status_reason],
87
+ :updated => s[:updated_time].to_s.empty? ? nil : Time.at(s[:updated_time].to_i / 1000.0),
88
+ :outputs => s[:outputs].map{|k,v| {:key => k, :value => v}}
89
+ ).valid_state
90
+ end
91
+ end
92
+ stack
93
+ end
94
+
95
+ # Delete the stack
96
+ #
97
+ # @param stack [Models::Orchestration::Stack]
98
+ # @return [TrueClass, FalseClass]
99
+ def stack_destroy(stack)
100
+ if(stack.persisted?)
101
+ terraform_stack(stack).destroy!
102
+ true
103
+ else
104
+ false
105
+ end
106
+ end
107
+
108
+ # Fetch stack template
109
+ #
110
+ # @param stack [Stack]
111
+ # @return [Smash] stack template
112
+ def stack_template_load(stack)
113
+ if(stack.persisted?)
114
+ JSON.load(terraform_stack(stack).template).to_smash
115
+ else
116
+ Smash.new
117
+ end
118
+ end
119
+
120
+ # Validate stack template
121
+ #
122
+ # @param stack [Stack]
123
+ # @return [NilClass, String] nil if valid, string error message if invalid
124
+ def stack_template_validate(stack)
125
+ begin
126
+ terraform_stack(stack).validate(stack.template)
127
+ nil
128
+ rescue MiasmaTerraform::Error::Validation => e
129
+ MultiJson.load(e.response.body.to_s).to_smash.get(:error, :message)
130
+ end
131
+ end
132
+
133
+ # Return all stacks
134
+ #
135
+ # @param options [Hash] filter
136
+ # @return [Array<Models::Orchestration::Stack>]
137
+ # @todo check if we need any mappings on state set
138
+ def stack_all(options={})
139
+ MiasmaTerraform::Stack.list(terraform_local_directory).map do |stack_name|
140
+ s = terraform_stack(Stack.new(self, :name => stack_name)).info
141
+ Stack.new(
142
+ self,
143
+ :id => s[:id],
144
+ :created => s[:creation_time].to_s.empty? ? nil : Time.at(s[:creation_time].to_i / 1000.0),
145
+ :description => s[:description],
146
+ :name => s[:name],
147
+ :state => s[:status].downcase.to_sym,
148
+ :status => s[:status],
149
+ :status_reason => s[:stack_status_reason],
150
+ :updated => s[:updated_time].to_s.empty? ? nil : Time.at(s[:updated_time].to_i / 1000.0)
151
+ ).valid_state
152
+ end
153
+ end
154
+
155
+ # Return all resources for stack
156
+ #
157
+ # @param stack [Models::Orchestration::Stack]
158
+ # @return [Array<Models::Orchestration::Stack::Resource>]
159
+ def resource_all(stack)
160
+ terraform_stack(stack) do |tf_stack|
161
+ tf_stack.resources.map do |resource|
162
+ Stack::Resource.new(
163
+ stack,
164
+ :id => resource[:physical_id],
165
+ :name => resource[:name],
166
+ :type => resource[:type],
167
+ :logical_id => resource[:name],
168
+ :state => resource[:status].downcase.to_sym,
169
+ :status => resource[:status],
170
+ :status_reason => resource[:resource_status_reason],
171
+ :updated => resource[:updated_time].to_s.empty? ? Time.now : Time.parse(resource[:updated_time])
172
+ ).valid_state
173
+ end
174
+ end
175
+ end
176
+
177
+ # Reload the stack resource data from the API
178
+ #
179
+ # @param resource [Models::Orchestration::Stack::Resource]
180
+ # @return [Models::Orchestration::Resource]
181
+ def resource_reload(resource)
182
+ resource.stack.resources.reload
183
+ resource.stack.resources.get(resource.id)
184
+ end
185
+
186
+ # Return all events for stack
187
+ #
188
+ # @param stack [Models::Orchestration::Stack]
189
+ # @return [Array<Models::Orchestration::Stack::Event>]
190
+ def event_all(stack, marker = nil)
191
+ params = marker ? {:marker => marker} : {}
192
+ terraform_stack(stack) do |tf_stack|
193
+ tf_stack.events.map do |event|
194
+ Stack::Event.new(
195
+ stack,
196
+ :id => event[:id],
197
+ :resource_id => event[:physical_resource_id],
198
+ :resource_name => event[:resource_name],
199
+ :resource_logical_id => event[:resource_name],
200
+ :resource_state => event[:resource_status].downcase.to_sym,
201
+ :resource_status => event[:resource_status],
202
+ :resource_status_reason => event[:resource_status_reason],
203
+ :time => Time.at(event[:timestamp] / 1000.0)
204
+ ).valid_state
205
+ end
206
+ end
207
+ end
208
+
209
+ # Return all new events for event collection
210
+ #
211
+ # @param events [Models::Orchestration::Stack::Events]
212
+ # @return [Array<Models::Orchestration::Stack::Event>]
213
+ def event_all_new(events)
214
+ event_all(events.stack, events.all.first.id)
215
+ end
216
+
217
+ # Reload the stack event data from the API
218
+ #
219
+ # @param resource [Models::Orchestration::Stack::Event]
220
+ # @return [Models::Orchestration::Event]
221
+ def event_reload(event)
222
+ event.stack.events.reload
223
+ event.stack.events.get(event.id)
224
+ end
225
+ end
226
+
227
+ module Boule
228
+ # Save the stack
229
+ #
230
+ # @param stack [Models::Orchestration::Stack]
231
+ # @return [Models::Orchestration::Stack]
232
+ def stack_save(stack)
233
+ if(stack.persisted?)
234
+ stack.load_data(stack.attributes)
235
+ result = request(
236
+ :method => :put,
237
+ :path => "/terraform/stack/#{stack.name}",
238
+ :json => {
239
+ :template => MultiJson.dump(stack.template),
240
+ :parameters => stack.parameters || {}
241
+ }
242
+ )
243
+ stack.valid_state
244
+ else
245
+ stack.load_data(stack.attributes)
246
+ result = request(
247
+ :method => :post,
248
+ :path => "/terraform/stack/#{stack.name}",
249
+ :json => {
250
+ :template => MultiJson.dump(stack.template),
251
+ :parameters => stack.parameters || {}
252
+ }
253
+ )
254
+ stack.id = result.get(:body, :stack, :id)
255
+ stack.valid_state
256
+ end
257
+ end
258
+
259
+ # Reload the stack data from the API
260
+ #
261
+ # @param stack [Models::Orchestration::Stack]
262
+ # @return [Models::Orchestration::Stack]
263
+ def stack_reload(stack)
264
+ if(stack.persisted?)
265
+ result = request(
266
+ :method => :get,
267
+ :path => "/terraform/stack/#{stack.name}"
268
+ )
269
+ s = result.get(:body, :stack)
270
+ stack.load_data(
271
+ :id => s[:id],
272
+ :created => s[:creation_time].to_s.empty? ? nil : Time.at(s[:creation_time].to_i / 1000.0),
273
+ :description => s[:description],
274
+ :name => s[:name],
275
+ :state => s[:status].downcase.to_sym,
276
+ :status => s[:status],
277
+ :status_reason => s[:stack_status_reason],
278
+ :updated => s[:updated_time].to_s.empty? ? nil : Time.at(s[:updated_time].to_i / 1000.0),
279
+ :outputs => s[:outputs].map{|k,v| {:key => k, :value => v}}
280
+ ).valid_state
281
+ end
282
+ stack
283
+ end
284
+
285
+ # Delete the stack
286
+ #
287
+ # @param stack [Models::Orchestration::Stack]
288
+ # @return [TrueClass, FalseClass]
289
+ def stack_destroy(stack)
290
+ if(stack.persisted?)
291
+ request(
292
+ :method => :delete,
293
+ :path => "/terraform/stack/#{stack.name}"
294
+ )
295
+ true
296
+ else
297
+ false
298
+ end
299
+ end
300
+
301
+ # Fetch stack template
302
+ #
303
+ # @param stack [Stack]
304
+ # @return [Smash] stack template
305
+ def stack_template_load(stack)
306
+ if(stack.persisted?)
307
+ result = request(
308
+ :method => :get,
309
+ :path => "/terraform/template/#{stack.name}"
310
+ )
311
+ result.fetch(:body, Smash.new)
312
+ else
313
+ Smash.new
314
+ end
315
+ end
316
+
317
+ # Validate stack template
318
+ #
319
+ # @param stack [Stack]
320
+ # @return [NilClass, String] nil if valid, string error message if invalid
321
+ def stack_template_validate(stack)
322
+ begin
323
+ result = request(
324
+ :method => :post,
325
+ :path => '/terraform/validate',
326
+ :json => Smash.new(
327
+ :template => stack.template
328
+ )
329
+ )
330
+ nil
331
+ rescue Error::ApiError::RequestError => e
332
+ MultiJson.load(e.response.body.to_s).to_smash.get(:error, :message)
333
+ end
334
+ end
335
+
336
+ # Return all stacks
337
+ #
338
+ # @param options [Hash] filter
339
+ # @return [Array<Models::Orchestration::Stack>]
340
+ # @todo check if we need any mappings on state set
341
+ def stack_all(options={})
342
+ result = request(
343
+ :method => :get,
344
+ :path => '/terraform/stacks'
345
+ )
346
+ result.fetch(:body, :stacks, []).map do |s|
347
+ Stack.new(
348
+ self,
349
+ :id => s[:id],
350
+ :created => s[:creation_time].to_s.empty? ? nil : Time.at(s[:creation_time].to_i / 1000.0),
351
+ :description => s[:description],
352
+ :name => s[:name],
353
+ :state => s[:status].downcase.to_sym,
354
+ :status => s[:status],
355
+ :status_reason => s[:stack_status_reason],
356
+ :updated => s[:updated_time].to_s.empty? ? nil : Time.at(s[:updated_time].to_i / 1000.0)
357
+ ).valid_state
358
+ end
359
+ end
360
+
361
+ # Return all resources for stack
362
+ #
363
+ # @param stack [Models::Orchestration::Stack]
364
+ # @return [Array<Models::Orchestration::Stack::Resource>]
365
+ def resource_all(stack)
366
+ result = request(
367
+ :method => :get,
368
+ :path => "/terraform/resources/#{stack.name}"
369
+ )
370
+ result.fetch(:body, :resources, []).map do |resource|
371
+ Stack::Resource.new(
372
+ stack,
373
+ :id => resource[:physical_id],
374
+ :name => resource[:name],
375
+ :type => resource[:type],
376
+ :logical_id => resource[:name],
377
+ :state => resource[:status].downcase.to_sym,
378
+ :status => resource[:status],
379
+ :status_reason => resource[:resource_status_reason],
380
+ :updated => resource[:updated_time].to_s.empty? ? Time.now : Time.parse(resource[:updated_time])
381
+ ).valid_state
382
+ end
383
+ end
384
+
385
+ # Reload the stack resource data from the API
386
+ #
387
+ # @param resource [Models::Orchestration::Stack::Resource]
388
+ # @return [Models::Orchestration::Resource]
389
+ def resource_reload(resource)
390
+ resource.stack.resources.reload
391
+ resource.stack.resources.get(resource.id)
392
+ end
393
+
394
+ # Return all events for stack
395
+ #
396
+ # @param stack [Models::Orchestration::Stack]
397
+ # @return [Array<Models::Orchestration::Stack::Event>]
398
+ def event_all(stack, marker = nil)
399
+ params = marker ? {:marker => marker} : {}
400
+ result = request(
401
+ :path => "/terraform/events/#{stack.name}",
402
+ :method => :get,
403
+ :params => params
404
+ )
405
+ result.fetch(:body, :events, []).map do |event|
406
+ Stack::Event.new(
407
+ stack,
408
+ :id => event[:id],
409
+ :resource_id => event[:physical_resource_id],
410
+ :resource_name => event[:resource_name],
411
+ :resource_logical_id => event[:resource_name],
412
+ :resource_state => event[:resource_status].downcase.to_sym,
413
+ :resource_status => event[:resource_status],
414
+ :resource_status_reason => event[:resource_status_reason],
415
+ :time => Time.at(event[:timestamp] / 1000.0)
416
+ ).valid_state
417
+ end
418
+ end
419
+
420
+ # Return all new events for event collection
421
+ #
422
+ # @param events [Models::Orchestration::Stack::Events]
423
+ # @return [Array<Models::Orchestration::Stack::Event>]
424
+ def event_all_new(events)
425
+ event_all(events.stack, events.all.first.id)
426
+ end
427
+
428
+ # Reload the stack event data from the API
429
+ #
430
+ # @param resource [Models::Orchestration::Stack::Event]
431
+ # @return [Models::Orchestration::Event]
432
+ def event_reload(event)
433
+ event.stack.events.reload
434
+ event.stack.events.get(event.id)
435
+ end
436
+ end
437
+
438
+ end
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
+ require 'miasma-terraform/version'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'miasma-terraform'
5
+ s.version = MiasmaTerraform::VERSION.version
6
+ s.summary = 'Smoggy Terraform API'
7
+ s.author = 'Chris Roberts'
8
+ s.email = 'code@chrisroberts.org'
9
+ s.homepage = 'https://github.com/miasma-rb/miasma-terraform'
10
+ s.description = 'Smoggy Terraform API'
11
+ s.license = 'Apache 2.0'
12
+ s.require_path = 'lib'
13
+ s.add_development_dependency 'pry'
14
+ s.add_development_dependency 'minitest'
15
+ s.files = Dir['lib/**/*'] + %w(miasma-terraform.gemspec README.md CHANGELOG.md LICENSE)
16
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: miasma-terraform
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Roberts
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Smoggy Terraform API
42
+ email: code@chrisroberts.org
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CHANGELOG.md
48
+ - LICENSE
49
+ - README.md
50
+ - lib/miasma-terraform.rb
51
+ - lib/miasma-terraform/stack.rb
52
+ - lib/miasma-terraform/version.rb
53
+ - lib/miasma/contrib/terraform.rb
54
+ - lib/miasma/contrib/terraform/orchestration.rb
55
+ - miasma-terraform.gemspec
56
+ homepage: https://github.com/miasma-rb/miasma-terraform
57
+ licenses:
58
+ - Apache 2.0
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.4.8
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: Smoggy Terraform API
80
+ test_files: []