sfn 0.0.1 → 0.3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -0
  3. data/LICENSE +13 -0
  4. data/README.md +142 -61
  5. data/bin/sfn +43 -0
  6. data/lib/chef/knife/knife_plugin_seed.rb +117 -0
  7. data/lib/sfn.rb +17 -0
  8. data/lib/sfn/cache.rb +385 -0
  9. data/lib/sfn/command.rb +45 -0
  10. data/lib/sfn/command/create.rb +87 -0
  11. data/lib/sfn/command/describe.rb +87 -0
  12. data/lib/sfn/command/destroy.rb +74 -0
  13. data/lib/sfn/command/events.rb +98 -0
  14. data/lib/sfn/command/export.rb +103 -0
  15. data/lib/sfn/command/import.rb +117 -0
  16. data/lib/sfn/command/inspect.rb +160 -0
  17. data/lib/sfn/command/list.rb +59 -0
  18. data/lib/sfn/command/promote.rb +17 -0
  19. data/lib/sfn/command/update.rb +95 -0
  20. data/lib/sfn/command/validate.rb +34 -0
  21. data/lib/sfn/command_module.rb +9 -0
  22. data/lib/sfn/command_module/base.rb +150 -0
  23. data/lib/sfn/command_module/stack.rb +166 -0
  24. data/lib/sfn/command_module/template.rb +147 -0
  25. data/lib/sfn/config.rb +106 -0
  26. data/lib/sfn/config/create.rb +35 -0
  27. data/lib/sfn/config/describe.rb +19 -0
  28. data/lib/sfn/config/destroy.rb +9 -0
  29. data/lib/sfn/config/events.rb +25 -0
  30. data/lib/sfn/config/export.rb +29 -0
  31. data/lib/sfn/config/import.rb +24 -0
  32. data/lib/sfn/config/inspect.rb +37 -0
  33. data/lib/sfn/config/list.rb +25 -0
  34. data/lib/sfn/config/promote.rb +23 -0
  35. data/lib/sfn/config/update.rb +20 -0
  36. data/lib/sfn/config/validate.rb +49 -0
  37. data/lib/sfn/monkey_patch.rb +8 -0
  38. data/lib/sfn/monkey_patch/stack.rb +200 -0
  39. data/lib/sfn/provider.rb +224 -0
  40. data/lib/sfn/utils.rb +23 -0
  41. data/lib/sfn/utils/debug.rb +31 -0
  42. data/lib/sfn/utils/json.rb +37 -0
  43. data/lib/sfn/utils/object_storage.rb +28 -0
  44. data/lib/sfn/utils/output.rb +79 -0
  45. data/lib/sfn/utils/path_selector.rb +99 -0
  46. data/lib/sfn/utils/ssher.rb +29 -0
  47. data/lib/sfn/utils/stack_exporter.rb +275 -0
  48. data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
  49. data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
  50. data/lib/sfn/version.rb +4 -0
  51. data/sfn.gemspec +19 -0
  52. metadata +110 -4
@@ -0,0 +1,34 @@
1
+ require 'pathname'
2
+ require 'sparkle_formation'
3
+ require 'sfn'
4
+
5
+ module Sfn
6
+ class Command
7
+ # Validate command
8
+ class Validate < Command
9
+
10
+ include Sfn::CommandModule::Base
11
+ include Sfn::CommandModule::Template
12
+
13
+ def execute!
14
+ file = load_template_file
15
+ file.delete('sfn_nested_stack')
16
+ ui.info "#{ui.color("Template Validation (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, '').sub(%r{^/}, '')}"
17
+ file = Sfn::Utils::StackParameterScrubber.scrub!(file)
18
+ file = translate_template(file)
19
+ begin
20
+ result = provider.connection.stacks.build(
21
+ :name => 'validation-stack',
22
+ :template => file
23
+ ).validate
24
+ ui.info ui.color(' -> VALID', :bold, :green)
25
+ rescue => e
26
+ ui.info ui.color(' -> INVALID', :bold, :red)
27
+ ui.fatal e.message
28
+ failed = true
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module CommandModule
5
+ autoload :Base, 'sfn/command_module/base'
6
+ autoload :Stack, 'sfn/command_module/stack'
7
+ autoload :Template, 'sfn/command_module/template'
8
+ end
9
+ end
@@ -0,0 +1,150 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module CommandModule
5
+ # Base module for CLIs
6
+ module Base
7
+
8
+ # Instance methods for cloudformation command classes
9
+ module InstanceMethods
10
+
11
+ # @return [KnifeCloudformation::Provider]
12
+ def provider
13
+ memoize(:provider, :direct) do
14
+ Sfn::Provider.new(
15
+ :miasma => config[:credentials],
16
+ :async => false,
17
+ :fetch => false
18
+ )
19
+ end
20
+ end
21
+
22
+ # Write exception information if debug is enabled
23
+ #
24
+ # @param e [Exception]
25
+ # @param args [String] extra strings to output
26
+ def _debug(e, *args)
27
+ if(config[:verbose])
28
+ ui.fatal "Exception information: #{e.class}: #{e.message}"
29
+ if(ENV['DEBUG'])
30
+ puts "#{e.backtrace.join("\n")}\n"
31
+ if(e.is_a?(Miasma::Error::ApiError))
32
+ ui.fatal "Response body: #{e.response.body.to_s.inspect}"
33
+ end
34
+ end
35
+ args.each do |string|
36
+ ui.fatal string
37
+ end
38
+ end
39
+ end
40
+
41
+ # Format snake cased key to title
42
+ #
43
+ # @param string [String, Symbol]
44
+ # @return [String
45
+ def as_title(string)
46
+ string.to_s.split('_').map(&:capitalize).join(' ')
47
+ end
48
+
49
+ # Get stack
50
+ #
51
+ # @param name [String] name of stack
52
+ # @return [Miasma::Models::Orchestration::Stack]
53
+ def stack(name)
54
+ provider.stacks.get(name)
55
+ end
56
+
57
+ # @return [Array<String>] attributes to display
58
+ def allowed_attributes
59
+ opts.fetch(:attributes, config.fetch(:attributes, default_attributes))
60
+ end
61
+
62
+ # @return [Array<String>] default attributes to display
63
+ def default_attributes
64
+ %w(timestamp stack_name id)
65
+ end
66
+
67
+ # Check if attribute is allowed for display
68
+ #
69
+ # @param attr [String]
70
+ # @return [TrueClass, FalseClass]
71
+ def attribute_allowed?(attr)
72
+ opts.fetch(:all_attributes, config[:all_attributes], allowed_attributes.include?(attr))
73
+ end
74
+
75
+ # Poll events on stack
76
+ #
77
+ # @param name [String] name of stack
78
+ def poll_stack(name)
79
+ provider.connection.stacks.reload
80
+ retry_attempts = 0
81
+ begin
82
+ events = Sfn::Command::Events.new({:poll => true}, [name]).execute!
83
+ rescue => e
84
+ if(retry_attempts < config.fetch(:max_poll_retries, 5).to_i)
85
+ retry_attempts += 1
86
+ warn "Unexpected error encountered (#{e.class}: #{e}) Retrying [retry count: #{retry_attempts}]"
87
+ sleep(1)
88
+ retry
89
+ else
90
+ raise
91
+ end
92
+ end
93
+ end
94
+
95
+ # Wrapper for information retrieval. Provides consistent error
96
+ # message for failures
97
+ #
98
+ # @param stack [String] stack name
99
+ # @param message [String] failure message
100
+ # @yield block to wrap error handling
101
+ # @return [Object] result of yield
102
+ def get_things(stack=nil, message=nil)
103
+ begin
104
+ yield
105
+ rescue => e
106
+ ui.fatal "#{message || 'Failed to retrieve information'}#{" for requested stack: #{stack}" if stack}"
107
+ ui.fatal "Reason: #{e}"
108
+ _debug(e)
109
+ exit 1
110
+ end
111
+ end
112
+
113
+ # Simple compat proxy method
114
+ #
115
+ # @return [Array<String>]
116
+ def name_args
117
+ arguments
118
+ end
119
+
120
+ # Fetches value from local configuration (#opts) and falls
121
+ # back to global configuration (#options)
122
+ #
123
+ # @return [Object]
124
+ def config
125
+ memoize(:config) do
126
+ options.deep_merge(opts)
127
+ end
128
+ end
129
+
130
+ end
131
+
132
+ class << self
133
+ def included(klass)
134
+ klass.instance_eval do
135
+
136
+ include Sfn::CommandModule::Base::InstanceMethods
137
+ include Sfn::Utils::JSON
138
+ include Sfn::Utils::Output
139
+ include Bogo::AnimalStrings
140
+ include Bogo::Memoization
141
+ include Bogo::Constants
142
+
143
+ end
144
+
145
+ end
146
+ end
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,166 @@
1
+ require 'sfn'
2
+ require 'sparkle_formation'
3
+
4
+ module Sfn
5
+ module CommandModule
6
+ # Stack handling helper methods
7
+ module Stack
8
+
9
+ module InstanceMethods
10
+
11
+ # unpacked stack name joiner/identifier
12
+ UNPACK_NAME_JOINER = '-sfn-'
13
+ # maximum number of attempts to get valid parameter value
14
+ MAX_PARAMETER_ATTEMPTS = 5
15
+
16
+ # Apply any defined remote stacks
17
+ #
18
+ # @param stack [Miasma::Models::Orchestration::Stack]
19
+ # @return [Miasma::Models::Orchestration::Stack]
20
+ def apply_stacks!(stack)
21
+ remote_stacks = config.fetch(:apply_stacks, [])
22
+ remote_stacks.each do |stack_name|
23
+ remote_stack = provider.connection.stacks.get(stack_name)
24
+ if(remote_stack)
25
+ apply_nested_stacks!(remote_stack, stack)
26
+ stack.apply_stack(remote_stack)
27
+ else
28
+ apply_unpacked_stack!(stack_name, stack)
29
+ end
30
+ end
31
+ stack
32
+ end
33
+
34
+ # Detect nested stacks and apply
35
+ #
36
+ # @param remote_stack [Miasma::Models::Orchestration::Stack] stack to inspect for nested stacks
37
+ # @param stack [Miasma::Models::Orchestration::Stack] current stack
38
+ # @return [Miasma::Models::Orchestration::Stack]
39
+ def apply_nested_stacks(remote_stack, stack)
40
+ remote_stack.resources.all.each do |resource|
41
+ if(resource.type == 'AWS::CloudFormation::Stack')
42
+ nested_stack = resource.expand
43
+ apply_nested_stacks!(nested_stack, stack)
44
+ stack.apply_stack(nested_stack)
45
+ end
46
+ end
47
+ stack
48
+ end
49
+
50
+ # Apply all stacks from an unpacked stack
51
+ #
52
+ # @param stack_name [String] name of parent stack
53
+ # @param stack [Miasma::Models::Orchestration::Stack]
54
+ # @return [Miasma::Models::Orchestration::Stack]
55
+ def apply_unpacked_stack!(stack_name, stack)
56
+ result = provider.connection.stacks.all.find_all do |remote_stack|
57
+ remote_stack.name.start_with?("#{stack_name}#{UNPACK_NAME_JOINER}")
58
+ end.sort_by(&:name).map do |remote_stack|
59
+ stack.apply_stack(remote_stack)
60
+ end
61
+ unless(result.empty?)
62
+ stack
63
+ else
64
+ ui.error "Failed to apply requested stack. Unable to locate. (#{stack_name})"
65
+ raise "Failed to locate stack: #{stack_name}"
66
+ end
67
+ end
68
+
69
+ # Unpack nested stack and run action on each stack, applying
70
+ # the previous stacks automatically
71
+ #
72
+ # @param name [String] container stack name
73
+ # @param file [Hash] stack template
74
+ # @param action [String] create or update
75
+ # @return [TrueClass]
76
+ def unpack_nesting(name, file, action)
77
+ config.apply_stacks ||= []
78
+ stack_count = 0
79
+ file['Resources'].each do |stack_resource_name, stack_resource|
80
+
81
+ nested_stack_name = "#{name}#{UNPACK_NAME_JOINER}#{Kernel.sprintf('%0.3d', stack_count)}-#{stack_resource_name}"
82
+ nested_stack_template = stack_resource['Properties']['Stack']
83
+
84
+ namespace.const_get(action.to_s.capitalize).new(
85
+ Smash.new(
86
+ :print_only => config[:print_only],
87
+ :template => nested_stack_template,
88
+ :parameters => config.fetch(:parameters, Smash.new).to_smash,
89
+ :apply_stacks => config[:apply_stacks],
90
+ :options => config[:options]
91
+ ),
92
+ [nested_stack_name]
93
+ ).execute!
94
+ unless(config[:print_only])
95
+ config[:apply_stacks].push(nested_stack_name).uniq!
96
+ end
97
+ config[:template] = nil
98
+ provider.connection.stacks.reload
99
+ stack_count += 1
100
+ end
101
+
102
+ true
103
+ end
104
+
105
+ # Prompt for parameter values and store result
106
+ #
107
+ # @param stack [Hash] stack template
108
+ # @return [Hash]
109
+ def populate_parameters!(stack, current_params={})
110
+ if(config[:interactive_parameters])
111
+ if(stack['Parameters'])
112
+ unless(config.get(:parameters))
113
+ config.set(:parameters, Smash.new)
114
+ end
115
+ stack.fetch('Parameters', {}).each do |k,v|
116
+ next if config[:parameters][k]
117
+ attempt = 0
118
+ valid = false
119
+ until(valid)
120
+ attempt += 1
121
+ default = config[:parameters].fetch(
122
+ k, current_params.fetch(
123
+ k, v['Default']
124
+ )
125
+ )
126
+ answer = ui.ask_question("#{k.split(/([A-Z]+[^A-Z]*)/).find_all{|s|!s.empty?}.join(' ')}", :default => default)
127
+ validation = Sfn::Utils::StackParameterValidator.validate(answer, v)
128
+ if(validation == true)
129
+ unless(answer == v['Default'])
130
+ config[:parameters][k] = answer
131
+ end
132
+ valid = true
133
+ else
134
+ validation.each do |validation_error|
135
+ ui.error validation_error.last
136
+ end
137
+ end
138
+ if(attempt > MAX_PARAMETER_ATTEMPTS)
139
+ ui.fatal 'Failed to receive allowed parameter!'
140
+ exit 1
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ stack
147
+ end
148
+
149
+ end
150
+
151
+ module ClassMethods
152
+ end
153
+
154
+ # Load methods into class and define options
155
+ #
156
+ # @param klass [Class]
157
+ def self.included(klass)
158
+ klass.class_eval do
159
+ extend Sfn::CommandModule::Stack::ClassMethods
160
+ include Sfn::CommandModule::Stack::InstanceMethods
161
+ end
162
+ end
163
+
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,147 @@
1
+ require 'sfn'
2
+ require 'sparkle_formation'
3
+
4
+ module Sfn
5
+ module CommandModule
6
+ # Template handling helper methods
7
+ module Template
8
+
9
+ # cloudformation directories that should be ignored
10
+ TEMPLATE_IGNORE_DIRECTORIES = %w(components dynamics registry)
11
+
12
+ module InstanceMethods
13
+
14
+ # Load the template file
15
+ #
16
+ # @param args [Symbol] options (:allow_missing)
17
+ # @return [Hash] loaded template
18
+ def load_template_file(*args)
19
+ unless(config[:template])
20
+ set_paths_and_discover_file!
21
+ unless(File.exists?(config[:file].to_s))
22
+ unless(args.include?(:allow_missing))
23
+ ui.fatal "Invalid formation file path provided: #{config[:file]}"
24
+ raise IOError.new "Failed to locate file: #{config[:file]}"
25
+ end
26
+ end
27
+ end
28
+ if(config[:template])
29
+ config[:template]
30
+ elsif(config[:file])
31
+ if(config[:processing])
32
+ sf = SparkleFormation.compile(config[:file], :sparkle)
33
+ if(sf.nested? && !sf.isolated_nests?)
34
+ raise TypeError.new('Template does not contain isolated stack nesting! Cannot process in existing state.')
35
+ end
36
+ if(sf.nested? && config[:apply_nesting])
37
+ sf.apply_nesting do |stack_name, stack_definition|
38
+ bucket = provider.connection.api_for(:storage).buckets.get(
39
+ config[:nesting_bucket]
40
+ )
41
+ if(config[:print_only])
42
+ "http://example.com/bucket/#{name_args.first}_#{stack_name}.json"
43
+ else
44
+ unless(bucket)
45
+ raise "Failed to locate configured bucket for stack template storage (#{bucket})!"
46
+ end
47
+ file = bucket.files.build
48
+ file.name = "#{name_args.first}_#{stack_name}.json"
49
+ file.content_type = 'text/json'
50
+ file.body = MultiJson.dump(Sfn::Utils::StackParameterScrubber.scrub!(stack_definition))
51
+ file.save
52
+ # TODO: what if we need extra params?
53
+ url = URI.parse(file.url)
54
+ "#{url.scheme}://#{url.host}#{url.path}"
55
+ end
56
+ end
57
+ else
58
+ sf.dump.merge('sfn_nested_stack' => !!sf.nested?)
59
+ end
60
+ else
61
+ _from_json(File.read(config[:file]))
62
+ end
63
+ end
64
+ end
65
+
66
+ # Apply template translation
67
+ #
68
+ # @param template [Hash]
69
+ # @return [Hash]
70
+ def translate_template(template)
71
+ if(klass_name = config[:translate])
72
+ klass = SparkleFormation::Translation.const_get(camel(klass_name))
73
+ args = {
74
+ :parameters => config.fetch(:options, :parameters, Smash.new)
75
+ }
76
+ if(chunk_size = config[:translate_chunk_size])
77
+ args.merge!(
78
+ :options => {
79
+ :serialization_chunk_size => chunk_size
80
+ }
81
+ )
82
+ end
83
+ translator = klass.new(template, args)
84
+ translator.translate!
85
+ template = translator.translated
86
+ ui.info "#{ui.color('Translation applied:', :bold)} #{ui.color(klass_name, :yellow)}"
87
+ end
88
+ template
89
+ end
90
+
91
+ # Set SparkleFormation paths and locate tempate
92
+ #
93
+ # @return [TrueClass]
94
+ def set_paths_and_discover_file!
95
+ if(config[:base_directory])
96
+ SparkleFormation.sparkle_path = config[:base_directory]
97
+ end
98
+ if(!config[:file] && config[:file_path_prompt])
99
+ root = File.expand_path(
100
+ config.fetch(:base_directory,
101
+ File.join(Dir.pwd, 'cloudformation')
102
+ )
103
+ ).split('/')
104
+ bucket = root.pop
105
+ root = root.join('/')
106
+ directory = File.join(root, bucket)
107
+ config[:file] = prompt_for_file(directory,
108
+ :directories_name => 'Collections',
109
+ :files_name => 'Templates',
110
+ :ignore_directories => TEMPLATE_IGNORE_DIRECTORIES
111
+ )
112
+ else
113
+ unless(Pathname(config[:file].to_s).absolute?)
114
+ base_dir = config[:base_directory].to_s
115
+ file = config[:file].to_s
116
+ pwd = Dir.pwd
117
+ config[:file] = [
118
+ File.join(base_dir, file),
119
+ File.join(pwd, file),
120
+ File.join(pwd, 'cloudformation', file)
121
+ ].detect do |file_path|
122
+ File.file?(file_path)
123
+ end
124
+ end
125
+ end
126
+ true
127
+ end
128
+
129
+ end
130
+
131
+ module ClassMethods
132
+ end
133
+
134
+ # Load methods into class and define options
135
+ #
136
+ # @param klass [Class]
137
+ def self.included(klass)
138
+ klass.class_eval do
139
+ extend Sfn::CommandModule::Template::ClassMethods
140
+ include Sfn::CommandModule::Template::InstanceMethods
141
+ include Sfn::Utils::PathSelector
142
+ end
143
+ end
144
+
145
+ end
146
+ end
147
+ end