knife-cloudformation 0.1.14 → 0.1.16

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 CHANGED
@@ -1,3 +1,8 @@
1
+ ## v0.1.16
2
+ * Fix exit code on stack destroy
3
+ * Update stack loading for single stack requests
4
+ * Add import and export functionality
5
+
1
6
  ## v0.1.14
2
7
  * Extract template building tools
3
8
  * Add support for custom CF locations and prompting
@@ -11,6 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.require_path = 'lib'
12
12
  s.add_dependency 'chef'
13
13
  s.add_dependency 'fog', '~> 1.17'
14
+ s.add_dependency 'unf'
14
15
  s.add_dependency 'net-sftp'
15
16
  s.add_dependency 'sparkle_formation', '~> 0.1.2'
16
17
  s.add_dependency 'redis-objects'
@@ -1,6 +1,6 @@
1
1
  require 'sparkle_formation'
2
2
  require 'pathname'
3
- require 'chef/knife/cloudformation_base'
3
+ require 'knife-cloudformation/cloudformation_base'
4
4
 
5
5
  class Chef
6
6
  class Knife
@@ -126,13 +126,17 @@ class Chef
126
126
  exit 1
127
127
  end
128
128
 
129
- set_paths_and_discover_file!
130
- unless(File.exists?(Chef::Config[:knife][:cloudformation][:file].to_s))
131
- ui.fatal "Invalid formation file path provided: #{Chef::Config[:knife][:cloudformation][:file]}"
132
- exit 1
129
+ unless(Chef::Config[:knife][:cloudformation][:template])
130
+ set_paths_and_discover_file!
131
+ unless(File.exists?(Chef::Config[:knife][:cloudformation][:file].to_s))
132
+ ui.fatal "Invalid formation file path provided: #{Chef::Config[:knife][:cloudformation][:file]}"
133
+ exit 1
134
+ end
133
135
  end
134
136
 
135
- if(Chef::Config[:knife][:cloudformation][:processing])
137
+ if(Chef::Config[:knife][:cloudformation][:template])
138
+ file = Chef::Config[:knife][:cloudformation][:template]
139
+ elsif(Chef::Config[:knife][:cloudformation][:processing])
136
140
  file = SparkleFormation.compile(Chef::Config[:knife][:cloudformation][:file])
137
141
  else
138
142
  file = _from_json(File.read(Chef::Config[:knife][:cloudformation][:file]))
@@ -143,7 +147,14 @@ class Chef
143
147
  exit 1
144
148
  end
145
149
  ui.info "#{ui.color('Cloud Formation: ', :bold)} #{ui.color(action_type, :green)}"
146
- ui.info " -> #{ui.color('Name:', :bold)} #{name} #{ui.color('Path:', :bold)} #{Chef::Config[:knife][:cloudformation][:file]} #{ui.color('(not pre-processed)', :yellow) if Chef::Config[:knife][:cloudformation][:disable_processing]}"
150
+ stack_info = "#{ui.color('Name:', :bold)} #{name}"
151
+ if(Chef::Config[:knife][:cloudformation][:path])
152
+ stack_info << " #{ui.color('Path:', :bold)} #{Chef::Config[:knife][:cloudformation][:file]}"
153
+ if(Chef::Config[:knife][:cloudformation][:disable_processing])
154
+ stack_info << " #{ui.color('(not pre-processed)', :yellow)}"
155
+ end
156
+ end
157
+ ui.info " -> #{stack_info}"
147
158
  populate_parameters!(file)
148
159
  stack_def = KnifeCloudformation::AwsCommons::Stack.build_stack_definition(file, Chef::Config[:knife][:cloudformation][:options])
149
160
  aws.create_stack(name, stack_def)
@@ -175,6 +186,7 @@ class Chef
175
186
  if(stack['Parameters'])
176
187
  Chef::Config[:knife][:cloudformation][:options][:parameters] ||= Mash.new
177
188
  stack['Parameters'].each do |k,v|
189
+ next if Chef::Config[:knife][:cloudformation][:options][:parameters][k]
178
190
  valid = false
179
191
  until(valid)
180
192
  default = Chef::Config[:knife][:cloudformation][:options][:parameters][k] || v['Default']
@@ -1,4 +1,4 @@
1
- require 'chef/knife/cloudformation_base'
1
+ require 'knife-cloudformation/cloudformation_base'
2
2
 
3
3
  class Chef
4
4
  class Knife
@@ -1,4 +1,4 @@
1
- require 'chef/knife/cloudformation_base'
1
+ require 'knife-cloudformation/cloudformation_base'
2
2
 
3
3
  class Chef
4
4
  class Knife
@@ -30,7 +30,7 @@ class Chef
30
30
  stacks.each do |stack_name|
31
31
  poll_stack(stack_name)
32
32
  end
33
- rescue Fog::AWS::CloudFormation::NotFound
33
+ rescue SystemExit, Fog::AWS::CloudFormation::NotFound
34
34
  # ignore this error since this is the end result we want!
35
35
  end
36
36
  ui.info " -> Destroyed Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold, :red)}"
@@ -1,4 +1,4 @@
1
- require 'chef/knife/cloudformation_base'
1
+ require 'knife-cloudformation/cloudformation_base'
2
2
 
3
3
  class Chef
4
4
  class Knife
@@ -0,0 +1,79 @@
1
+ require 'knife-cloudformation/cloudformation_base'
2
+ require 'knife-cloudformation/export'
3
+
4
+ class Chef
5
+ class Knife
6
+ class CloudformationExport < Knife
7
+
8
+ include KnifeCloudformation::KnifeBase
9
+
10
+ banner 'knife cloudformation export NAME'
11
+
12
+ # TODO: Add option for s3 exports
13
+
14
+ option(:path,
15
+ :long => '--export-path PATH',
16
+ :description => 'Path to write export JSON file',
17
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:export_path] = v }
18
+ )
19
+
20
+ option(:ignore_parameters,
21
+ :short => '-P NAME',
22
+ :long => '--exclude-parameter NAME',
23
+ :description => 'Exclude parameter from export (can be used multiple times)',
24
+ :proc => lambda{|v|
25
+ Chef::Config[:knife][:cloudformation][:ignore_parameters] ||= []
26
+ Chef::Config[:knife][:cloudformation][:ignore_parameters].push(v).uniq!
27
+ }
28
+ )
29
+
30
+ option(:chef_environment_parameter,
31
+ :long => '--chef-environment-parameter NAME',
32
+ :description => 'Parameter used within stack to specify Chef environment',
33
+ :proc => lambda{|v|
34
+ Chef::Config[:knife][:cloudformation][:chef_environment_parameter] = v
35
+ }
36
+ )
37
+
38
+ option(:chef_popsicle,
39
+ :long => '--[no-]freeze-run-list',
40
+ :boolean => true,
41
+ :default => true,
42
+ :description => 'Freezes first run files',
43
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:chef_popsicle] = v }
44
+ )
45
+
46
+ def run
47
+ if(Chef::Config[:knife][:cloudformation][:export_path])
48
+ file_path = File.join(
49
+ Chef::Config[:knife][:cloudformation][:export_path], "#{name_args.first}-#{Time.now.to_i}.json"
50
+ )
51
+ end
52
+ ui.info "#{ui.color('Stack Export:', :bold)} #{name_args.first}"
53
+ if(file_path)
54
+ ui.info " - Writing to: #{file_path}"
55
+ else
56
+ ui.info " - Printing to console"
57
+ end
58
+ ui.confirm 'Perform export'
59
+ ex_opts = {}
60
+ [:chef_popsicle, :chef_environment_parameter, :ignore_parameters].each do |key|
61
+ unless(Chef::Config[:knife][:cloudformation][key].nil?)
62
+ ex_opts[key] = Chef::Config[:knife][:cloudformation][key]
63
+ end
64
+ end
65
+ exporter = KnifeCloudformation::Export.new(name_args.first, ex_opts.merge(:aws_commons => aws))
66
+ result = exporter.export
67
+ if(file_path)
68
+ File.open(file_path, 'w') do |file|
69
+ file.puts _format_json(result)
70
+ end
71
+ else
72
+ ui.info _format_json(result)
73
+ end
74
+ ui.info "#{ui.color('Stack export', :bold)} (#{name_args.first}): #{ui.color('complete', :green)}"
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,35 @@
1
+ require 'knife-cloudformation/cloudformation_base'
2
+
3
+ class Chef
4
+ class Knife
5
+ class CloudformationImport < Knife
6
+
7
+ include KnifeCloudformation::KnifeBase
8
+
9
+ banner 'knife cloudformation import NEW_STACK_NAME JSON_EXPORT_FILE'
10
+
11
+ def run
12
+ stack_name, json_file = name_args
13
+ ui.info "#{ui.color('Stack Import', :bold)} (#{json_file})"
14
+ if(File.exists?(json_file))
15
+ stack = _from_json(File.read(json_file))
16
+ creator = Chef::Knife::CloudformationCreate.new
17
+ creator.name_args = [stack_name]
18
+ Chef::Config[:knife][:cloudformation][:template] = stack['template_body']
19
+ Chef::Config[:knife][:cloudformation][:options] = Mash.new
20
+ Chef::Config[:knife][:cloudformation][:options][:parameters] = Mash.new
21
+ stack['parameters'].each do |k,v|
22
+ Chef::Config[:knife][:cloudformation][:options][:parameters][k] = v
23
+ end
24
+ ui.info ' - Starting creation of import'
25
+ creator.run
26
+ ui.info "#{ui.color('Stack Import', :bold)} (#{json_file}): #{ui.color('complete', :green)}"
27
+ else
28
+ ui.error "Failed to locate JSON export file (#{json_file})"
29
+ exit 1
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -1,4 +1,4 @@
1
- require 'chef/knife/cloudformation_base'
1
+ require 'knife-cloudformation/cloudformation_base'
2
2
  require 'knife-cloudformation/utils'
3
3
 
4
4
  class Chef
@@ -1,4 +1,4 @@
1
- require 'chef/knife/cloudformation_base'
1
+ require 'knife-cloudformation/cloudformation_base'
2
2
 
3
3
  class Chef
4
4
  class Knife
@@ -1,4 +1,4 @@
1
- require 'chef/knife/cloudformation_create'
1
+ require 'knife-cloudformation/cloudformation_base'
2
2
 
3
3
  class Chef
4
4
  class Knife
@@ -229,7 +229,7 @@ module KnifeCloudformation
229
229
  def timeout_in_minutes
230
230
  @memo[:raw_stack].value['TimeoutInMinutes']
231
231
  end
232
- alias_method :timeout_in_minutes, :timeout
232
+ alias_method :timeout, :timeout_in_minutes
233
233
 
234
234
  def stack_id
235
235
  @memo[:raw_stack].value['StackId']
@@ -90,6 +90,7 @@ module KnifeCloudformation
90
90
  end
91
91
  if(@memo[:stacks].update_allowed? || args[:force_refresh])
92
92
  @memo[:stacks_lock].lock do
93
+ @_stacks_cached = true
93
94
  @memo[:stacks].value = aws(:cloud_formation).describe_stacks.body['Stacks']
94
95
  end
95
96
  end
@@ -118,8 +119,10 @@ module KnifeCloudformation
118
119
  [name, name.start_with?('arn:') ? name : id_from_stack_name(name)]
119
120
  end.map do |name, s_id|
120
121
  unless(@local[:stacks][s_id])
121
- seed = stacks.detect do |stk|
122
- stk['StackId'] == s_id
122
+ if(@_stacks_cached)
123
+ seed = stacks.detect do |stk|
124
+ stk['StackId'] == s_id
125
+ end
123
126
  end
124
127
  @local[:stacks][s_id] = Stack.new(name, self, seed)
125
128
  end
@@ -0,0 +1,173 @@
1
+ require 'knife-cloudformation/aws_commons'
2
+ require 'chef'
3
+
4
+ module KnifeCloudformation
5
+ class Export
6
+
7
+ DEFAULT_OPTIONS = {
8
+ :chef_popsicle => true,
9
+ :ignored_parameters => ['Environment', 'StackCreator'],
10
+ :chef_environment_parameter => 'Environment'
11
+ }
12
+
13
+ attr_reader :stack, :stack_name, :stack_id, :options, :aws_commons
14
+
15
+ def initialize(stack_name, options={})
16
+ @stack_name = stack_name
17
+ @options = DEFAULT_OPTIONS.merge(options)
18
+ if(aws_commons?)
19
+ @aws_commons = options[:aws_commons]
20
+ else
21
+ raise ArgumentError.new('Expecting `AwsCommons` instance but none provided!')
22
+ end
23
+ load_stack
24
+ end
25
+
26
+ def export
27
+ exported = stack.to_hash
28
+ if(chef_popsicle?)
29
+ freeze_runlists(exported)
30
+ end
31
+ remove_ignored_parameters(exported)
32
+ exported
33
+ end
34
+
35
+ def method_missing(*args)
36
+ m = args.first.to_s
37
+ if(m.end_with?('?') && options.has_key?(k = m.sub('?', '').to_sym))
38
+ !!options[k]
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ protected
45
+
46
+ def remove_ignored_parameters(export)
47
+ options[:ignored_parameters].each do |param|
48
+ export[:parameters].delete(param)
49
+ end
50
+ end
51
+
52
+ def chef_environment_name
53
+ if(chef_environment_parameter?)
54
+ name = stack[:parameters][options[:chef_environment_parameter]]
55
+ end
56
+ name || '_default'
57
+ end
58
+
59
+ def environment
60
+ unless(@env)
61
+ @env = Chef::Environment.load('imdev')
62
+ end
63
+ @env
64
+ end
65
+
66
+ def load_stack
67
+ @stack = AwsCommons::Stack.new(stack_name, aws_commons)
68
+ @stack_id = @stack.stack_id
69
+ @stack
70
+ end
71
+
72
+ def allowed_cookbook_version(cookbook)
73
+ restriction = environment.cookbook_versions[cookbook]
74
+ requirement = Gem::Requirement.new(restriction)
75
+ Chef::CookbookVersion.available_versions(cookbook).detect do |v|
76
+ requirement.satisfied_by?(Gem::Version.new(v))
77
+ end
78
+ end
79
+
80
+ def extract_runlist_item(item)
81
+ rl_item = item.is_a?(Chef::RunList::RunListItem) ? item : Chef::RunList::RunListItem.new(item)
82
+ static_content = Mash.new(:run_list => [])
83
+ if(rl_item.recipe?)
84
+ cookbook, recipe = rl_item.name.split('::')
85
+ peg_version = allowed_cookbook_version(cookbook)
86
+ static_content[:run_list] << "recipe[#{[cookbook, recipe || 'default'].join('::')}@#{peg_version}]"
87
+ elsif(rl_item.role?)
88
+ role = Chef::Role.load(rl_item.name)
89
+ role.run_list.each do |item|
90
+ static_content = Chef::Mixin::DeepMerge.merge(static_content, extract_runlist_item(item))
91
+ end
92
+ static_content = Chef::Mixin::DeepMerge.merge(
93
+ static_content, Chef::Mixin::DeepMerge.merge(role.default_attributes, role.override_attributes)
94
+ )
95
+ else
96
+ # dunno what this is
97
+ end
98
+ static_content
99
+ end
100
+
101
+ def unpack_and_freeze_runlist(first_run)
102
+ extracted_runlists = first_run['run_list'].map do |item|
103
+ extract_runlist_item(cf_replace(item))
104
+ end
105
+ first_run.delete('run_list')
106
+ first_run.replace(
107
+ extracted_runlists.inject(first_run) do |memo, first_run_item|
108
+ Chef::Mixin::DeepMerge.merge(memo, first_run_item)
109
+ end
110
+ )
111
+ end
112
+
113
+ def freeze_runlists(exported)
114
+ first_runs = locate_runlists(exported)
115
+ first_runs.each do |first_run|
116
+ unpack_and_freeze_runlist(first_run)
117
+ end
118
+ end
119
+
120
+ def locate_runlists(thing)
121
+ result = []
122
+ case thing
123
+ when Hash
124
+ if(thing['content'] && thing['content']['run_list'])
125
+ result << thing['content']
126
+ else
127
+ thing.each do |k,v|
128
+ result += locate_runlists(v)
129
+ end
130
+ end
131
+ when Array
132
+ thing.each do |v|
133
+ result += locate_runlists(v)
134
+ end
135
+ end
136
+ result
137
+ end
138
+
139
+ def cf_replace(hsh)
140
+ if(hsh.is_a?(Hash))
141
+ case hsh.keys.first
142
+ when 'Fn::Join'
143
+ cf_join(*hsh.values.first)
144
+ when 'Ref'
145
+ cf_ref(hsh.values.first)
146
+ else
147
+ hsh
148
+ end
149
+ else
150
+ hsh
151
+ end
152
+ end
153
+
154
+ def cf_ref(ref_name)
155
+ if(stack.parameters.has_key?(ref_name))
156
+ stack.parameters[ref_name]
157
+ else
158
+ raise KeyError.new("No parameter found with given reference name (#{ref_name}). " <<
159
+ "Only parameter based references supported!")
160
+ end
161
+ end
162
+
163
+ def cf_join(delim, args)
164
+ args.map do |arg|
165
+ if(arg.is_a?(Hash))
166
+ cf_replace(arg)
167
+ else
168
+ arg.to_s
169
+ end
170
+ end.join(delim)
171
+ end
172
+ end
173
+ end
@@ -2,5 +2,5 @@ module KnifeCloudformation
2
2
  class Version < Gem::Version
3
3
  end
4
4
 
5
- VERSION = Version.new('0.1.14')
5
+ VERSION = Version.new('0.1.16')
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-cloudformation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14
4
+ version: 0.1.16
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-31 00:00:00.000000000 Z
12
+ date: 2013-11-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chef
@@ -43,6 +43,22 @@ dependencies:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
45
  version: '1.17'
46
+ - !ruby/object:Gem::Dependency
47
+ name: unf
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
46
62
  - !ruby/object:Gem::Dependency
47
63
  name: net-sftp
48
64
  requirement: !ruby/object:Gem::Requirement
@@ -114,15 +130,18 @@ extensions: []
114
130
  extra_rdoc_files: []
115
131
  files:
116
132
  - lib/chef/knife/cloudformation_events.rb
117
- - lib/chef/knife/cloudformation_base.rb
118
133
  - lib/chef/knife/cloudformation_destroy.rb
119
134
  - lib/chef/knife/cloudformation_describe.rb
135
+ - lib/chef/knife/cloudformation_export.rb
120
136
  - lib/chef/knife/cloudformation_list.rb
121
137
  - lib/chef/knife/cloudformation_create.rb
122
138
  - lib/chef/knife/cloudformation_update.rb
123
139
  - lib/chef/knife/cloudformation_inspect.rb
140
+ - lib/chef/knife/cloudformation_import.rb
124
141
  - lib/knife-cloudformation.rb
142
+ - lib/knife-cloudformation/cloudformation_base.rb
125
143
  - lib/knife-cloudformation/version.rb
144
+ - lib/knife-cloudformation/export.rb
126
145
  - lib/knife-cloudformation/aws_commons.rb
127
146
  - lib/knife-cloudformation/cache.rb
128
147
  - lib/knife-cloudformation/utils.rb