sfn 0.0.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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