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.
- 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
|