knife-cloudformation 0.1.2 → 0.1.4

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.
@@ -0,0 +1,221 @@
1
+ require 'knife-cloudformation/aws_commons.rb'
2
+
3
+ module KnifeCloudformation
4
+ class AwsCommons
5
+ class Stack
6
+
7
+ include KnifeCloudformation::Utils::JSON
8
+
9
+ attr_reader :name, :raw_stack, :raw_resources, :common
10
+
11
+ class << self
12
+
13
+ include KnifeCloudformation::Utils::JSON
14
+
15
+ def create(name, definition, aws_common)
16
+ aws_common.aws(:cloud_formation).create_stack(name, definition)
17
+ new(name, aws_common)
18
+ end
19
+
20
+ def build_stack_definition(template, options={})
21
+ stack = Mash.new
22
+ options.each do |key, value|
23
+ format_key = key.split('_').map do |k|
24
+ "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"
25
+ end.join
26
+ stack[format_key] = value
27
+ end
28
+ enable_capabilities!(stack, template)
29
+ stack['TemplateBody'] = _to_json(template)
30
+ stack
31
+ end
32
+
33
+ # Currently only checking for IAM resources since that's all
34
+ # that is supported for creation
35
+ def enable_capabilities!(stack, template)
36
+ found = Array(template['Resources']).detect do |resource_name, resource|
37
+ resource['Type'].start_with?('AWS::IAM')
38
+ end
39
+ stack['Capabilities'] = ['CAPABILITY_IAM'] if found
40
+ nil
41
+ end
42
+
43
+ end
44
+
45
+ def initialize(name, common)
46
+ @name = name
47
+ @common = common
48
+ @memo = {}
49
+ load_stack
50
+ @force_refresh = false
51
+ @force_refresh = in_progress?
52
+ end
53
+
54
+ ## Actions ##
55
+
56
+ def update(definition)
57
+ res = common.aws(:cloud_formation).update_stack(name, definition)
58
+ reload!
59
+ res
60
+ end
61
+
62
+ def destroy
63
+ res = common.aws(:cloud_formation).delete_stack(name)
64
+ reload!
65
+ res
66
+ end
67
+
68
+ def load_stack
69
+ @raw_stack = common.aws(:cloud_formation).describe_stacks('StackName' => name).body['Stacks'].first
70
+ end
71
+
72
+ def load_resources
73
+ @raw_resources = common.aws(:cloud_formation).describe_stack_resources('StackName' => name).body['StackResources']
74
+ end
75
+
76
+ def refresh?(bool)
77
+ bool || (bool.nil? && @force_refresh)
78
+ end
79
+
80
+ def reload!
81
+ load_stack
82
+ load_resources
83
+ @force_refresh = in_progress?
84
+ @memo = {}
85
+ true
86
+ end
87
+
88
+ ## Information ##
89
+
90
+ def serialize
91
+ _to_json(to_hash)
92
+ end
93
+
94
+ def to_hash(extra_data={})
95
+ {
96
+ :template => template,
97
+ :parameters => parameters,
98
+ :capabilities => capabilities,
99
+ :disable_rollback => disable_rollback,
100
+ :notification_ARNs => notification_arns,
101
+ :timeout_in_minutes => timeout
102
+ }.merge(extra_data)
103
+ end
104
+
105
+ def template
106
+ unless(@memo[:template])
107
+ @memo[:template] = _from_json(
108
+ common.aws(:cloud_formation)
109
+ .get_template(name).body['TemplateBody']
110
+ )
111
+ end
112
+ @memo[:template]
113
+ end
114
+
115
+ def parameters
116
+ unless(@memo[:parameters])
117
+ @memo[:parameters] = Hash[*(
118
+ @raw_stack['Parameters'].map do |ary|
119
+ [ary['ParameterKey'], ary['ParameterValue']]
120
+ end.flatten
121
+ )]
122
+ end
123
+ @memo[:parameters]
124
+ end
125
+
126
+ def capabilities
127
+ @raw_stack['Capabilities']
128
+ end
129
+
130
+ def disable_rollback
131
+ @raw_stack['DisableRollback']
132
+ end
133
+
134
+ def notification_arns
135
+ @raw_stack['NotificationARNs']
136
+ end
137
+
138
+ def status(force_refresh=nil)
139
+ load_stack if refresh?(force_refresh)
140
+ @raw_stack['StackStatus']
141
+ end
142
+
143
+ def resources(force_refresh=nil)
144
+ load_resources if @raw_resources.nil? || refresh?(force_refresh)
145
+ @raw_resources
146
+ end
147
+
148
+ def events(all=false)
149
+ res = common.aws(:cloud_formation).describe_stack_events(name).body['StackEvents']
150
+ @memo[:events] ||= []
151
+ res.delete_if{|e| @memo[:events].include?(e['EventId'])}
152
+ @memo[:events] += res.map{|e| e['EventId']}
153
+ @memo[:events].uniq!
154
+ res
155
+ end
156
+
157
+ def outputs(style=:unformatted)
158
+ case style
159
+ when :formatted
160
+ Hash[*(
161
+ @raw_stack['Outputs'].map do |item|
162
+ [item['OutputKey'].gsub(/(?<![A-Z])([A-Z])/, '_\1').sub(/^_/, '').downcase.to_sym, item['OutputValue']]
163
+ end.flatten
164
+ )]
165
+ when :unformatted
166
+ Hash[*(
167
+ @raw_stack['Outputs'].map do |item|
168
+ [item['OutputKey'], item['OutputValue']]
169
+ end.flatten
170
+ )]
171
+ else
172
+ @raw_stack['Outputs']
173
+ end
174
+ end
175
+
176
+ ## State ##
177
+
178
+ def in_progress?
179
+ status.to_s.downcase.end_with?('in_progress')
180
+ end
181
+
182
+ def complete?
183
+ stat = status.to_s.downcase
184
+ stat.end_with?('complete') || stat.end_with?('failed')
185
+ end
186
+
187
+ def failed?
188
+ stat = status.to_s.downcase
189
+ stat.end_with?('failed') || (stat.include?('rollback') && stat.end_with?('complete'))
190
+ end
191
+
192
+ def success?
193
+ !failed?
194
+ end
195
+
196
+ ## Fog instance helpers ##
197
+
198
+ RESOURCE_FILTER_KEYS = {
199
+ :auto_scaling_group => 'AutoScalingGroupNames'
200
+ }
201
+
202
+ def expand_resource(resource)
203
+ kind = resource['ResourceType'].split('::')[1]
204
+ kind_snake = common.snake(kind)
205
+ aws = common.aws(kind_snake)
206
+ aws.send("#{common.snake(resource['ResourceType'].split('::').last).to_s.split('_').last}s").get(resource['PhysicalResourceId'])
207
+ end
208
+
209
+ def nodes
210
+ as_resources = resources.find_all{|r|r['ResourceType'] == 'AWS::AutoScaling::AutoScalingGroup'}
211
+ as_resources.map do |as_resource|
212
+ as_group = expand_resource(as_resource)
213
+ as_group.instances.map do |inst|
214
+ common.aws(:ec2).servers.get(inst.id)
215
+ end
216
+ end.flatten
217
+ end
218
+
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,78 @@
1
+ require 'knife-cloudformation/aws_commons/stack'
2
+
3
+ module KnifeCloudformation
4
+ class AwsCommons
5
+
6
+ class Stack
7
+
8
+ class ParameterValidator
9
+ class << self
10
+
11
+ include KnifeCloudformation::Utils::AnimalStrings
12
+
13
+ def validate(value, parameter_definition)
14
+ result = %w(AllowedValues AllowedPattern MaxLength MinLength MaxValue MinValue).map do |key|
15
+ if(parameter_definition[key])
16
+ res = self.send(snake(key), value, parameter_definition)
17
+ res == true ? true : [snake(key), res]
18
+ else
19
+ true
20
+ end
21
+ end
22
+ result.delete_if{|x| x == true}
23
+ result.empty? ? true : result
24
+ end
25
+
26
+ def allowed_values(value, pdef)
27
+ if(pdef['AllowedValues'].include?(value))
28
+ true
29
+ else
30
+ "Not an allowed value: #{pdef['AllowedValues'].join(', ')}"
31
+ end
32
+ end
33
+
34
+ def allowed_pattern(value, pdef)
35
+ if(value.match(/#{pdef['AllowedPattern']}/))
36
+ true
37
+ else
38
+ "Not a valid pattern. Must match: #{pdef['AllowedPattern']}"
39
+ end
40
+ end
41
+
42
+ def max_length(value, pdef)
43
+ if(value.length <= pdef['MaxLength'].to_i)
44
+ true
45
+ else
46
+ "Value must not exceed #{pdef['MaxLength']} characters"
47
+ end
48
+ end
49
+
50
+ def min_length(value, pdef)
51
+ if(value.length >= pdef['MinLength'].to_i)
52
+ true
53
+ else
54
+ "Value must be at least #{pdef['MinLength']} characters"
55
+ end
56
+ end
57
+
58
+ def max_value(value, pdef)
59
+ if(value.to_i <= pdef['MaxValue'].to_i)
60
+ true
61
+ else
62
+ "Value must not be greater than #{pdef['MaxValue']}"
63
+ end
64
+ end
65
+
66
+ def min_value(value, pdef)
67
+ if(value.to_i >= pdef['MinValue'].to_i)
68
+ true
69
+ else
70
+ "Value must not be less than #{pdef['MinValue']}"
71
+ end
72
+ end
73
+
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,15 +1,34 @@
1
1
  require 'chef/mash'
2
2
  require 'attribute_struct'
3
3
  require 'knife-cloudformation/sparkle_attribute'
4
+ require 'knife-cloudformation/utils'
4
5
 
5
6
  AttributeStruct.camel_keys = true
6
7
 
7
8
  module KnifeCloudformation
8
9
  class SparkleFormation
10
+
11
+ include KnifeCloudformation::Utils::AnimalStrings
12
+
9
13
  class << self
10
14
 
11
15
  attr_reader :dynamics
12
-
16
+ attr_reader :components_path
17
+ attr_reader :dynamics_path
18
+
19
+ def custom_paths
20
+ @_paths ||= {}
21
+ @_paths
22
+ end
23
+
24
+ def components_path=(path)
25
+ custom_paths[:sparkle_path] = path
26
+ end
27
+
28
+ def dynamics_path=(path)
29
+ custom_paths[:dynamics_directory] = path
30
+ end
31
+
13
32
  def compile(path)
14
33
  formation = self.instance_eval(IO.read(path), path, 1)
15
34
  formation.compile._dump
@@ -36,7 +55,7 @@ module KnifeCloudformation
36
55
  @loaded_dynamics.uniq!
37
56
  true
38
57
  end
39
-
58
+
40
59
  def dynamic(name, &block)
41
60
  @dynamics ||= Mash.new
42
61
  @dynamics[name] = block
@@ -50,17 +69,29 @@ module KnifeCloudformation
50
69
  raise "Failed to locate requested dynamic block for insertion: #{dynamic_name} (valid: #{@dynamics.keys.sort.join(', ')})"
51
70
  end
52
71
  end
72
+
73
+ def from_hash(hash)
74
+ struct = AttributeStruct.new
75
+ struct._camel_keys_set(:auto_discovery)
76
+ struct._load(hash)
77
+ struct._camel_keys_set(nil)
78
+ struct
79
+ end
53
80
  end
54
-
81
+
55
82
  attr_reader :name
56
83
  attr_reader :sparkle_path
57
84
  attr_reader :components
58
85
  attr_reader :load_order
59
-
86
+
60
87
  def initialize(name, options={})
61
88
  @name = name
62
- @sparkle_path = options[:sparkle_path] || File.join(Dir.pwd, 'cloudformation/components')
63
- @dynamics_directory = options[:dynamics_directory] || File.join(File.dirname(@sparkle_path), 'dynamics')
89
+ @sparkle_path = options[:sparkle_path] ||
90
+ self.class.custom_paths[:sparkle_path] ||
91
+ File.join(Dir.pwd, 'cloudformation/components')
92
+ @dynamics_directory = options[:dynamics_directory] ||
93
+ self.class.custom_paths[:dynamics_directory] ||
94
+ File.join(File.dirname(@sparkle_path), 'dynamics')
64
95
  self.class.load_dynamics!(@dynamics_directory)
65
96
  @components = Mash.new
66
97
  @load_order = []
@@ -84,7 +115,7 @@ module KnifeCloudformation
84
115
  @overrides = self.class.build(&block)
85
116
  self
86
117
  end
87
-
118
+
88
119
  # Returns compiled Mash instance
89
120
  def compile
90
121
  compiled = AttributeStruct.new
@@ -0,0 +1,69 @@
1
+ module KnifeCloudformation
2
+ module Utils
3
+ module JSON
4
+
5
+ def try_json_compat
6
+ unless(@_json_loaded)
7
+ begin
8
+ require 'chef/json_compat'
9
+ rescue
10
+ require "#{ENV['RUBY_JSON_LIB'] || 'json'}"
11
+ end
12
+ @_json_loaded = true
13
+ end
14
+ defined?(Chef::JSONCompat)
15
+ end
16
+
17
+ def _to_json(thing)
18
+ if(try_json_compat)
19
+ Chef::JSONCompat.to_json(thing)
20
+ else
21
+ JSON.dump(thing)
22
+ end
23
+ end
24
+
25
+ def _from_json(thing)
26
+ if(try_json_compat)
27
+ Chef::JSONCompat.from_json(thing)
28
+ else
29
+ JSON.read(thing)
30
+ end
31
+ end
32
+
33
+ def _format_json(thing)
34
+ thing = _from_json(thing) if thing.is_a?(String)
35
+ if(try_json_compat)
36
+ Chef::JSONCompat.to_json_pretty(thing)
37
+ else
38
+ JSON.pretty_generate(thing)
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ module AnimalStrings
45
+
46
+ def camel(string)
47
+ string.to_s.split('_').map{|k| "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"}.join
48
+ end
49
+
50
+ def snake(string)
51
+ string.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
52
+ end
53
+
54
+ end
55
+
56
+ module Ssher
57
+ def remote_file_contents(address, user, path, ssh_opts={})
58
+ require 'net/sftp'
59
+ content = nil
60
+ Net::SFTP.start(address, user, ssh_opts) do |con|
61
+ con.file.open(path) do |f|
62
+ content = f.read
63
+ end
64
+ end
65
+ content
66
+ end
67
+ end
68
+ end
69
+ end