stack_master 0.6.0 → 0.7.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -0
  3. data/features/apply.feature +1 -1
  4. data/features/apply_with_s3.feature +106 -0
  5. data/features/step_definitions/stack_steps.rb +5 -0
  6. data/features/support/env.rb +1 -0
  7. data/lib/stack_master.rb +78 -58
  8. data/lib/stack_master/aws_driver/s3.rb +66 -0
  9. data/lib/stack_master/cli.rb +1 -0
  10. data/lib/stack_master/commands/apply.rb +52 -5
  11. data/lib/stack_master/commands/init.rb +2 -0
  12. data/lib/stack_master/commands/list_stacks.rb +2 -0
  13. data/lib/stack_master/commands/outputs.rb +2 -0
  14. data/lib/stack_master/commands/status.rb +3 -0
  15. data/lib/stack_master/parameter_resolver.rb +1 -1
  16. data/lib/stack_master/parameter_resolvers/secret.rb +2 -0
  17. data/lib/stack_master/stack.rb +20 -13
  18. data/lib/stack_master/stack_definition.rb +63 -13
  19. data/lib/stack_master/stack_differ.rb +2 -0
  20. data/lib/stack_master/stack_events/fetcher.rb +1 -1
  21. data/lib/stack_master/stack_events/presenter.rb +1 -1
  22. data/lib/stack_master/stack_events/streamer.rb +1 -1
  23. data/lib/stack_master/template_compiler.rb +3 -1
  24. data/lib/stack_master/template_compilers/cfndsl.rb +4 -2
  25. data/lib/stack_master/template_compilers/json.rb +4 -0
  26. data/lib/stack_master/template_compilers/sparkle_formation.rb +5 -0
  27. data/lib/stack_master/template_compilers/yaml.rb +4 -0
  28. data/lib/stack_master/test_driver/cloud_formation.rb +50 -36
  29. data/lib/stack_master/test_driver/s3.rb +34 -0
  30. data/lib/stack_master/testing.rb +1 -0
  31. data/lib/stack_master/utils.rb +19 -0
  32. data/lib/stack_master/version.rb +1 -1
  33. data/spec/fixtures/stack_master.yml +4 -1
  34. data/spec/stack_master/aws_driver/s3_spec.rb +130 -0
  35. data/spec/stack_master/commands/apply_spec.rb +26 -0
  36. data/spec/stack_master/config_spec.rb +8 -1
  37. data/spec/stack_master/parameter_resolver_spec.rb +5 -0
  38. data/spec/stack_master/sparkle_formation/user_data_file_spec.rb +2 -0
  39. data/spec/stack_master/stack_events/presenter_spec.rb +1 -1
  40. data/spec/stack_master/stack_spec.rb +23 -5
  41. data/spec/stack_master/template_compiler_spec.rb +1 -0
  42. data/spec/stack_master/template_compilers/cfndsl_spec.rb +2 -0
  43. data/spec/stack_master/test_driver/s3_spec.rb +17 -0
  44. data/stack_master.gemspec +0 -1
  45. metadata +10 -16
@@ -1,3 +1,5 @@
1
+ require "erb"
2
+
1
3
  module StackMaster
2
4
  module Commands
3
5
  class Init
@@ -1,3 +1,5 @@
1
+ require 'table_print'
2
+
1
3
  module StackMaster
2
4
  module Commands
3
5
  class ListStacks
@@ -1,3 +1,5 @@
1
+ require 'table_print'
2
+
1
3
  module StackMaster
2
4
  module Commands
3
5
  class Outputs
@@ -1,3 +1,6 @@
1
+ require 'table_print'
2
+ require 'ruby-progressbar'
3
+
1
4
  module StackMaster
2
5
  module Commands
3
6
  class Status
@@ -42,7 +42,7 @@ module StackMaster
42
42
  end
43
43
 
44
44
  def resolve_parameter_value(key, parameter_value)
45
- return parameter_value.to_s if Numeric === parameter_value
45
+ return parameter_value.to_s if Numeric === parameter_value || parameter_value == true || parameter_value == false
46
46
  return parameter_value.join(',') if Array === parameter_value
47
47
  return parameter_value unless Hash === parameter_value
48
48
  validate_parameter_value!(key, parameter_value)
@@ -1,3 +1,5 @@
1
+ require 'dotgpg'
2
+
1
3
  module StackMaster
2
4
  module ParameterResolvers
3
5
  class Secret < Resolver
@@ -1,19 +1,21 @@
1
1
  module StackMaster
2
2
  class Stack
3
3
  MAX_TEMPLATE_SIZE = 51200
4
+ MAX_S3_TEMPLATE_SIZE = 460800
4
5
 
5
- include Virtus.model
6
+ attr_reader :stack_name,
7
+ :region,
8
+ :stack_id,
9
+ :stack_status,
10
+ :parameters,
11
+ :template_body,
12
+ :notification_arns,
13
+ :outputs,
14
+ :stack_policy_body,
15
+ :tags,
16
+ :files
6
17
 
7
- attribute :stack_name, String
8
- attribute :region, String
9
- attribute :stack_id, String
10
- attribute :stack_status, String
11
- attribute :parameters, Hash
12
- attribute :template_body, String
13
- attribute :notification_arns, Array[String]
14
- attribute :outputs, Array
15
- attribute :stack_policy_body, String
16
- attribute :tags, Hash
18
+ include Utils::Initializable
17
19
 
18
20
  def template_hash
19
21
  if template_body
@@ -87,8 +89,13 @@ module StackMaster
87
89
  stack_policy_body: stack_policy_body)
88
90
  end
89
91
 
90
- def too_big?
91
- maybe_compressed_template_body.size > MAX_TEMPLATE_SIZE
92
+ def max_template_size(use_s3)
93
+ return MAX_S3_TEMPLATE_SIZE if use_s3
94
+ MAX_TEMPLATE_SIZE
95
+ end
96
+
97
+ def too_big?(use_s3 = false)
98
+ maybe_compressed_template_body.size > max_template_size(use_s3)
92
99
  end
93
100
 
94
101
  def aws_parameters
@@ -1,34 +1,84 @@
1
1
  module StackMaster
2
2
  class StackDefinition
3
- include Virtus.value_object(strict: true, required: false)
3
+ attr_accessor :region,
4
+ :stack_name,
5
+ :template,
6
+ :tags,
7
+ :notification_arns,
8
+ :base_dir,
9
+ :secret_file,
10
+ :stack_policy_file,
11
+ :additional_parameter_lookup_dirs,
12
+ :s3,
13
+ :files
4
14
 
5
- values do
6
- attribute :region, String
7
- attribute :stack_name, String
8
- attribute :template, String
9
- attribute :tags, Hash
10
- attribute :notification_arns, Array[String]
11
- attribute :base_dir, String
12
- attribute :secret_file, String
13
- attribute :stack_policy_file, String
14
- attribute :additional_parameter_lookup_dirs, Array[String]
15
+ include Utils::Initializable
16
+
17
+ def initialize(attributes = {})
18
+ @additional_parameter_lookup_dirs = []
19
+ @notification_arns = []
20
+ @s3 = {}
21
+ @files = []
22
+ super
23
+ end
24
+
25
+ def ==(other)
26
+ self.class === other &&
27
+ @region == other.region &&
28
+ @stack_name == other.stack_name &&
29
+ @template == other.template &&
30
+ @tags == other.tags &&
31
+ @notification_arns == other.notification_arns &&
32
+ @base_dir == other.base_dir &&
33
+ @secret_file == other.secret_file &&
34
+ @stack_policy_file == other.stack_policy_file &&
35
+ @additional_parameter_lookup_dirs == other.additional_parameter_lookup_dirs &&
36
+ @s3 == other.s3
37
+ end
38
+
39
+ def template_dir
40
+ File.join(base_dir, 'templates')
15
41
  end
16
42
 
17
43
  def template_file_path
18
- File.join(base_dir, 'templates', template)
44
+ File.join(template_dir, template)
45
+ end
46
+
47
+ def files_dir
48
+ File.join(base_dir, 'files')
49
+ end
50
+
51
+ def s3_files
52
+ files.inject({}) do |hash, file|
53
+ path = File.join(files_dir, file)
54
+ hash[file] = {
55
+ path: path,
56
+ body: File.read(path)
57
+ }
58
+ hash
59
+ end
60
+ end
61
+
62
+ def s3_template_file_name
63
+ Utils.change_extension(template, 'json')
19
64
  end
20
65
 
21
66
  def parameter_files
22
- [ default_parameter_file_path, region_parameter_file_path ] + additional_parameter_lookup_file_paths
67
+ [ default_parameter_file_path, region_parameter_file_path, additional_parameter_lookup_file_paths ].flatten.compact
23
68
  end
24
69
 
25
70
  def stack_policy_file_path
26
71
  File.join(base_dir, 'policies', stack_policy_file) if stack_policy_file
27
72
  end
28
73
 
74
+ def s3_configured?
75
+ !s3.nil?
76
+ end
77
+
29
78
  private
30
79
 
31
80
  def additional_parameter_lookup_file_paths
81
+ return unless additional_parameter_lookup_dirs
32
82
  additional_parameter_lookup_dirs.map do |a|
33
83
  File.join(base_dir, 'parameters', a, "#{underscored_stack_name}.yml")
34
84
  end
@@ -1,3 +1,5 @@
1
+ require "diffy"
2
+
1
3
  module StackMaster
2
4
  class StackDiffer
3
5
  def initialize(proposed_stack, current_stack)
@@ -1,5 +1,5 @@
1
1
  module StackMaster
2
- class StackEvents
2
+ module StackEvents
3
3
  class Fetcher
4
4
  def self.fetch(*args)
5
5
  new(*args).fetch
@@ -1,5 +1,5 @@
1
1
  module StackMaster
2
- class StackEvents
2
+ module StackEvents
3
3
  class Presenter
4
4
  def self.print_event(io, event)
5
5
  new(io).print_event(event)
@@ -1,5 +1,5 @@
1
1
  module StackMaster
2
- class StackEvents
2
+ module StackEvents
3
3
  class Streamer
4
4
  def self.stream(*args, &block)
5
5
  new(*args, &block).stream
@@ -3,7 +3,9 @@ module StackMaster
3
3
  TemplateCompilationFailed = Class.new(RuntimeError)
4
4
 
5
5
  def self.compile(config, template_file_path)
6
- template_compiler_for_file(template_file_path, config).compile(template_file_path)
6
+ compiler = template_compiler_for_file(template_file_path, config)
7
+ compiler.require_dependencies
8
+ compiler.compile(template_file_path)
7
9
  rescue
8
10
  raise TemplateCompilationFailed.new("Failed to compile #{template_file_path}.")
9
11
  end
@@ -1,9 +1,11 @@
1
1
  module StackMaster::TemplateCompilers
2
2
  class Cfndsl
3
+ def self.require_dependencies
4
+ require 'cfndsl'
5
+ end
3
6
 
4
7
  def self.compile(template_file_path)
5
- require 'cfndsl'
6
- CfnDsl.eval_file_with_extras(template_file_path).to_json
8
+ ::CfnDsl.eval_file_with_extras(template_file_path).to_json
7
9
  end
8
10
 
9
11
  StackMaster::TemplateCompiler.register(:cfndsl, self)
@@ -3,6 +3,10 @@ module StackMaster::TemplateCompilers
3
3
  MAX_TEMPLATE_SIZE = 51200
4
4
  private_constant :MAX_TEMPLATE_SIZE
5
5
 
6
+ def self.require_dependencies
7
+ require 'json'
8
+ end
9
+
6
10
  def self.compile(template_file_path)
7
11
  template_body = File.read(template_file_path)
8
12
  if template_body.size > MAX_TEMPLATE_SIZE
@@ -1,5 +1,10 @@
1
1
  module StackMaster::TemplateCompilers
2
2
  class SparkleFormation
3
+ def self.require_dependencies
4
+ require 'sparkle_formation'
5
+ require 'stack_master/sparkle_formation/user_data_file'
6
+ end
7
+
3
8
  def self.compile(template_file_path)
4
9
  ::SparkleFormation.sparkle_path = File.dirname(template_file_path)
5
10
  JSON.pretty_generate(::SparkleFormation.compile(template_file_path))
@@ -1,5 +1,9 @@
1
1
  module StackMaster::TemplateCompilers
2
2
  class Yaml
3
+ def self.require_dependencies
4
+ require 'yaml'
5
+ require 'json'
6
+ end
3
7
 
4
8
  def self.compile(template_file_path)
5
9
  template_body = File.read(template_file_path)
@@ -1,48 +1,62 @@
1
1
  module StackMaster
2
2
  module TestDriver
3
3
  class Stack
4
- include Virtus.model
5
- attribute :stack_id, String
6
- attribute :stack_name, String
7
- attribute :description, String
8
- attribute :parameters, Array[OpenStruct]
9
- attribute :creation_time, String
10
- attribute :last_update_time, String
11
- attribute :stack_status, String
12
- attribute :stack_status_reason, String
13
- attribute :disable_rollback, String
14
- attribute :notification_arns, Array
15
- attribute :timeout_in_minutes, Integer
16
- attribute :capabilities, Array
17
- attribute :outputs, Array[OpenStruct]
18
- attribute :tags, Array[OpenStruct]
4
+ attr_reader :stack_id,
5
+ :stack_name,
6
+ :description,
7
+ :parameters,
8
+ :creation_time,
9
+ :last_update_time,
10
+ :stack_status,
11
+ :stack_status_reason,
12
+ :disable_rollback,
13
+ :notification_arns,
14
+ :timeout_in_minutes,
15
+ :capabilities,
16
+ :outputs,
17
+ :tags
18
+
19
+ include Utils::Initializable
20
+
21
+ def parameters
22
+ @parameters.map do |hash|
23
+ OpenStruct.new(parameter_key: hash[:parameter_key],
24
+ parameter_value: hash[:parameter_value])
25
+ end
26
+ end
19
27
  end
20
28
 
21
29
  class StackEvent
22
- include Virtus.model
23
- attribute :stack_id, String
24
- attribute :event_id, String
25
- attribute :stack_name, String
26
- attribute :logical_resource_id, String
27
- attribute :physical_resource_id, String
28
- attribute :resource_type, String
29
- attribute :timestamp, Time
30
- attribute :resource_status, String
31
- attribute :resource_status_reason, String
32
- attribute :resource_properties, String
30
+ attr_reader :stack_id,
31
+ :event_id,
32
+ :stack_name,
33
+ :logical_resource_id,
34
+ :physical_resource_id,
35
+ :resource_type,
36
+ :timestamp,
37
+ :resource_status,
38
+ :resource_status_reason,
39
+ :resource_properties
40
+
41
+ include Utils::Initializable
42
+
43
+ def timestamp
44
+ Time.parse(@timestamp) if @timestamp
45
+ end
33
46
  end
34
47
 
35
48
  class StackResource
36
- include Virtus.model
37
- attribute :stack_name, String
38
- attribute :stack_id, String
39
- attribute :logical_resource_id, String
40
- attribute :physical_resource_id, String
41
- attribute :resource_type, String
42
- attribute :timestamp, Time
43
- attribute :resource_status, String
44
- attribute :resource_status_reason, String
45
- attribute :description, String
49
+ attr_reader :stack_name,
50
+ :stack_id,
51
+ :logical_resource_id,
52
+ :physical_resource_id,
53
+ :resource_type,
54
+ :timestamp,
55
+ :resource_status,
56
+ :resource_status_reason,
57
+ :description
58
+
59
+ include Utils::Initializable
46
60
  end
47
61
 
48
62
  class CloudFormation
@@ -0,0 +1,34 @@
1
+ module StackMaster
2
+ module TestDriver
3
+ class S3
4
+ def initialize
5
+ reset
6
+ end
7
+
8
+ def set_region(_)
9
+ end
10
+
11
+ def reset
12
+ @files = Hash.new { |hash, key| hash[key] = Hash.new }
13
+ end
14
+
15
+ def upload_files(bucket: nil, prefix: nil, region: nil, files: {})
16
+ return if files.empty?
17
+
18
+ files.each do |template, file|
19
+ object_key = [prefix, template].compact.join('/')
20
+ @files[bucket][object_key] = file[:body]
21
+ end
22
+ end
23
+
24
+ def url(bucket:, prefix:, region:, template:)
25
+ ["https://s3-#{region}.amazonaws.com", bucket, prefix, template].compact.join('/')
26
+ end
27
+
28
+ # test only method
29
+ def find_file(bucket:, object_key:)
30
+ @files[bucket][object_key]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -5,4 +5,5 @@ Aws.config[:stub_responses] = true
5
5
  require 'stack_master/test_driver/cloud_formation'
6
6
 
7
7
  StackMaster.cloud_formation_driver = StackMaster::TestDriver::CloudFormation.new
8
+ StackMaster.s3_driver = StackMaster::TestDriver::S3.new
8
9
  StackMaster.non_interactive!
@@ -1,7 +1,26 @@
1
1
  module StackMaster
2
2
  module Utils
3
+ module Initializable
4
+ def initialize(attributes = {})
5
+ self.attributes = attributes
6
+ end
7
+
8
+ def attributes=(attributes)
9
+ attributes.each do |k, v|
10
+ instance_variable_set("@#{k}", v)
11
+ end
12
+ end
13
+ end
14
+
3
15
  extend self
4
16
 
17
+ def change_extension(file_name, extension)
18
+ [
19
+ File.basename(file_name, '.*'),
20
+ extension
21
+ ].join('.')
22
+ end
23
+
5
24
  def hash_to_aws_parameters(params)
6
25
  params.inject([]) do |params, (key, value)|
7
26
  params << { parameter_key: key, parameter_value: value }