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.
- data/CHANGELOG.md +6 -0
- data/lib/chef/knife/cloudformation_base.rb +109 -143
- data/lib/chef/knife/cloudformation_create.rb +66 -103
- data/lib/chef/knife/cloudformation_describe.rb +35 -17
- data/lib/chef/knife/cloudformation_destroy.rb +12 -12
- data/lib/chef/knife/cloudformation_events.rb +24 -42
- data/lib/chef/knife/cloudformation_inspect.rb +84 -0
- data/lib/chef/knife/cloudformation_list.rb +5 -17
- data/lib/chef/knife/cloudformation_update.rb +3 -3
- data/lib/knife-cloudformation/aws_commons.rb +118 -0
- data/lib/knife-cloudformation/aws_commons/stack.rb +221 -0
- data/lib/knife-cloudformation/aws_commons/stack_parameter_validator.rb +78 -0
- data/lib/knife-cloudformation/sparkle_formation.rb +38 -7
- data/lib/knife-cloudformation/utils.rb +69 -0
- data/lib/knife-cloudformation/version.rb +1 -1
- metadata +7 -2
@@ -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] ||
|
63
|
-
|
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
|