knife-cloudformation 0.1.2 → 0.1.4

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