stacker-yaml 0.1.0

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c0b70e11acb1afd956b95f53339a7c4a45e3a940
4
+ data.tar.gz: bef0a18196e611732827b74fa1b562b78cc8ab49
5
+ SHA512:
6
+ metadata.gz: fda992447c8032d5561281b5f1ca56ba077f440dbda399de53e01126c28831d2536071cb4b521cc7071d25491b3f3ee59407cd9c961be870289fe8288c7a40f8
7
+ data.tar.gz: b0466bc502fa74c1d4dd63c0bbaec0097e32a9874b0d1b9bf95dab12a70e63aecac260d3a5b4392a9816e9df2dd5e1f531b4bacabc4c76ea79b8e307361a40d2
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'stacker'
5
+ require 'stacker/cli'
6
+
7
+ trap('INT') { puts ''; exit }
8
+
9
+ Stacker::Cli.start ARGV
@@ -0,0 +1,4 @@
1
+ require 'stacker/region'
2
+ require 'stacker/stack'
3
+ require 'stacker/logging'
4
+ require 'stacker/version'
@@ -0,0 +1,271 @@
1
+ require 'active_support/core_ext/object/try'
2
+ require 'benchmark'
3
+ require 'stacker'
4
+ require 'thor'
5
+ require 'yaml'
6
+
7
+ module Stacker
8
+ class Cli < Thor
9
+ include Thor::Actions
10
+
11
+ default_path = ENV['STACKER_PATH'] || '.'
12
+ default_region = ENV['STACKER_REGION'] || 'us-east-1'
13
+ default_env = ENV['STACKER_ENVIRONMENT'] || 'development'
14
+
15
+ method_option :path, type: :string, default: default_path,
16
+ banner: 'project path'
17
+
18
+ method_option :region, type: :string, default: default_region,
19
+ banner: 'AWS region name'
20
+
21
+ method_option :environment, type: :string, default: default_env,
22
+ banner: 'Environment name (e.g. production, staging)'
23
+
24
+ method_option :allow_destructive, type: :boolean, default: false,
25
+ banner: 'allow destructive updates'
26
+
27
+ def initialize(*args); super(*args) end
28
+
29
+ desc "init [PATH]", "Create stacker project directories"
30
+ def init path = nil
31
+ init_project path || options['path']
32
+ end
33
+
34
+ desc "list", "List stacks"
35
+ def list
36
+ Stacker.logger.inspect region.stacks.map(&:name)
37
+ end
38
+
39
+ desc "show STACK_NAME", "Show details of a stack"
40
+ def show stack_name
41
+ with_one_or_all stack_name do |stack|
42
+ Stacker.logger.inspect(
43
+ 'Description' => stack.description,
44
+ 'Status' => stack.status,
45
+ 'Updated' => stack.last_updated_time || stack.creation_time,
46
+ 'Capabilities' => stack.capabilities.remote,
47
+ 'Parameters' => stack.parameters.remote,
48
+ 'Outputs' => stack.outputs
49
+ )
50
+ end
51
+ end
52
+
53
+ desc "status [STACK_NAME]", "Show stack status"
54
+ def status stack_name = nil
55
+ with_one_or_all(stack_name) do |stack|
56
+ begin
57
+ Stacker.logger.debug stack.status.indent
58
+ rescue Aws::CloudFormation::Errors::ValidationError,
59
+ Stacker::Stack::Error => err
60
+ Stacker.logger.error err.message
61
+ end
62
+ end
63
+ end
64
+
65
+ desc "diff [STACK_NAME]", "Show outstanding stack differences"
66
+ def diff stack_name = nil
67
+ with_one_or_all(stack_name) do |stack|
68
+ begin
69
+ resolve stack
70
+ next unless full_diff stack
71
+ rescue Aws::CloudFormation::Errors::ValidationError,
72
+ Stacker::Stack::Error => err
73
+ Stacker.logger.error err.message
74
+ end
75
+ end
76
+ end
77
+
78
+ desc "update [STACK_NAME]", "Create or update stack"
79
+ def update stack_name = nil
80
+ with_one_or_all(stack_name) do |stack|
81
+ begin
82
+ resolve stack
83
+
84
+ if stack.exists?
85
+ next unless full_diff stack
86
+
87
+ if yes? "Update remote template with these changes (y/n)?"
88
+ time = Benchmark.realtime do
89
+ stack.update allow_destructive: options['allow_destructive']
90
+ end
91
+ Stacker.logger.info formatted_time stack_name, 'updated', time
92
+ else
93
+ Stacker.logger.warn 'Update skipped'
94
+ end
95
+ else
96
+ if yes? "#{stack.name} does not exist. Create it (y/n)?"
97
+ time = Benchmark.realtime do
98
+ stack.create
99
+ end
100
+ Stacker.logger.info formatted_time stack_name, 'created', time
101
+ else
102
+ Stacker.logger.warn 'Create skipped'
103
+ end
104
+ end
105
+ rescue Aws::CloudFormation::Errors::ValidationError,
106
+ Stacker::Stack::Error => err
107
+ Stacker.logger.error err.message
108
+ end
109
+ end
110
+ end
111
+
112
+ desc "dump [STACK_NAME]", "Download stack template"
113
+ def dump stack_name = nil
114
+ with_one_or_all(stack_name) do |stack|
115
+ if stack.exists?
116
+ diff = stack.template.diff :down, :color
117
+ next Stacker.logger.warn 'Stack up-to-date' if diff.length == 0
118
+
119
+ Stacker.logger.debug "\n" + diff.indent
120
+ if yes? "Update local template with these changes (y/n)?"
121
+ stack.template.dump
122
+ else
123
+ Stacker.logger.warn 'Pull skipped'
124
+ end
125
+ else
126
+ Stacker.logger.warn "#{stack.name} does not exist"
127
+ end
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def init_project path
134
+ project_path = File.expand_path path
135
+
136
+ %w[ regions templates ].each do |dir|
137
+ directory_path = File.join project_path, dir
138
+ unless Dir.exists? directory_path
139
+ Stacker.logger.debug "Creating directory at #{directory_path}"
140
+ FileUtils.mkdir_p directory_path
141
+ end
142
+ end
143
+
144
+ region_path = File.join project_path, 'regions', 'us-east-1.yml'
145
+ unless File.exists? region_path
146
+ Stacker.logger.debug "Creating region file at #{region_path}"
147
+ File.open(region_path, 'w+') { |f| f.print <<-YAML }
148
+ defaults:
149
+ parameters:
150
+ CidrBlock: '10.0'
151
+ stacks:
152
+ - name: VPC
153
+ YAML
154
+ end
155
+ end
156
+
157
+ def formatted_time stack, action, benchmark
158
+ "Stack #{stack} #{action} in: #{(benchmark / 60).floor} min and #{(benchmark % 60).round} seconds."
159
+ end
160
+
161
+ def full_diff stack
162
+ templ_diff = stack.template.diff :color
163
+ param_diff = stack.parameters.diff :color
164
+
165
+ if (templ_diff + param_diff).length == 0
166
+ Stacker.logger.warn 'Stack up-to-date'
167
+ return false
168
+ end
169
+
170
+ Stacker.logger.info "\n#{templ_diff.indent}\n" if templ_diff.length > 0
171
+ Stacker.logger.info "\n#{param_diff.indent}\n" if param_diff.length > 0
172
+
173
+ Stacker.logger.info stack.pretty_change_set
174
+ true
175
+ end
176
+
177
+ def region
178
+ @region ||= begin
179
+ config_path = region_config_path
180
+ if File.exists? config_path
181
+ begin
182
+ config = YAML.load_file(config_path)
183
+ rescue Psych::SyntaxError => err
184
+ Stacker.logger.fatal err.message
185
+ exit 1
186
+ end
187
+
188
+ defaults = config.fetch 'defaults', {}
189
+ stacks = config.fetch 'stacks', {}
190
+ region_options = {
191
+ stack_prefix: environment_config.fetch('prefix', '')
192
+ }
193
+
194
+ Region.new options['region'], defaults, stacks, templates_path,
195
+ region_options
196
+ else
197
+ Stacker.logger.fatal "#{options['region']}.yml does not exist. Please configure or use stacker init"
198
+ exit 1
199
+ end
200
+ end
201
+ end
202
+
203
+ def region_config_path
204
+ region_path = if environments?
205
+ File.join working_path, 'environments', options['environment']
206
+ else
207
+ File.join working_path, 'regions'
208
+ end
209
+ File.join region_path, "#{options['region']}.yml"
210
+ end
211
+
212
+ def environments_path
213
+ File.join working_path, 'environments'
214
+ end
215
+
216
+ def environments?
217
+ File.exists? environments_path
218
+ end
219
+
220
+ def environment_config
221
+ config_path = File.join environments_path, 'config.yml'
222
+ return {} unless File.exists? config_path
223
+ YAML.load_file(config_path).fetch('environments', {}).fetch(
224
+ options['environment'],
225
+ {}
226
+ )
227
+ end
228
+
229
+ def resolve stack
230
+ return {} if stack.parameters.dependencies.none?
231
+
232
+ Stacker.logger.debug 'Resolving dependencies...'
233
+ stack.parameters.resolved
234
+ end
235
+
236
+ def with_one_or_all stack_name = nil, &block
237
+ yield_with_stack = proc do |stack|
238
+ Stacker.logger.info "#{stack.name}:"
239
+ yield stack
240
+ Stacker.logger.info ''
241
+ end
242
+
243
+ if stack_name
244
+ yield_with_stack.call region.stack(stack_name)
245
+ else
246
+ region.stacks.each(&yield_with_stack)
247
+ end
248
+
249
+ rescue Stacker::Stack::StackPolicyError => err
250
+ if options['allow_destructive']
251
+ Stacker.logger.fatal err.message
252
+ else
253
+ Stacker.logger.fatal 'Stack update policy prevents replacing or destroying resources.'
254
+ Stacker.logger.warn 'Try running again with \'--allow-destructive\''
255
+ end
256
+ exit 1
257
+ rescue Stacker::Stack::Error => err
258
+ Stacker.logger.fatal err.message
259
+ exit 1
260
+ end
261
+
262
+ def templates_path
263
+ File.join working_path, 'templates'
264
+ end
265
+
266
+ def working_path
267
+ File.expand_path options['path']
268
+ end
269
+
270
+ end
271
+ end
@@ -0,0 +1,31 @@
1
+ require 'diffy'
2
+ require 'json'
3
+
4
+ module Stacker
5
+ module Differ
6
+
7
+ module_function
8
+
9
+ def diff one, two, *args
10
+ down = args.include? :down
11
+
12
+ diff = Diffy::Diff.new(
13
+ (down ? one : two) + "\n",
14
+ (down ? two : one) + "\n",
15
+ context: 3,
16
+ include_diff_info: true
17
+ ).to_s(*args.select { |arg| arg == :color })
18
+
19
+ diff.gsub(/^(\x1B.+)?(\-{3}|\+{3}).+\n/, '').strip
20
+ end
21
+
22
+ def json_diff one, two, *args
23
+ diff JSON.pretty_generate(one), JSON.pretty_generate(two), *args
24
+ end
25
+
26
+ def yaml_diff one, two, *args
27
+ diff YAML.dump(one), YAML.dump(two), *args
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ require 'coderay'
2
+ require 'delegate'
3
+ require 'indentation'
4
+ require 'logger'
5
+ require 'rainbow'
6
+
7
+ module Stacker
8
+ module_function
9
+
10
+ class << self
11
+
12
+ class PrettyLogger < SimpleDelegator
13
+ def initialize logger
14
+ super
15
+
16
+ old_formatter = logger.formatter
17
+
18
+ logger.formatter = proc do |level, time, prog, msg|
19
+ unless msg.start_with?("\e")
20
+ color = case level
21
+ when 'FATAL' then :red
22
+ when 'WARN' then :yellow
23
+ when 'INFO' then :blue
24
+ when 'DEBUG' then '333333'
25
+ else :default
26
+ end
27
+ msg = msg.color(color)
28
+ end
29
+
30
+ old_formatter.call level, time, prog, msg
31
+ end
32
+ end
33
+
34
+ %w[ debug info warn fatal ].each do |level|
35
+ define_method level do |msg, opts = {}|
36
+ if opts.include? :highlight
37
+ msg = CodeRay.scan(msg, opts[:highlight]).terminal
38
+ end
39
+ __getobj__.__send__ level, msg
40
+ end
41
+ end
42
+
43
+ def inspect object
44
+ info object.to_yaml[4..-1].strip.indent, highlight: :yaml
45
+ end
46
+ end
47
+
48
+ def logger= logger
49
+ @logger = PrettyLogger.new logger
50
+ end
51
+
52
+ def logger
53
+ @logger ||= begin
54
+ logger = Logger.new STDOUT
55
+ logger.level = Logger::DEBUG
56
+ logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
57
+ PrettyLogger.new logger
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ require 'aws-sdk'
2
+ require 'stacker/stack'
3
+ require 'stacker/stack/errors'
4
+
5
+ module Stacker
6
+ class Region
7
+
8
+ attr_reader :name, :defaults, :stacks, :templates_path, :options
9
+
10
+ def initialize(name, defaults, stacks, templates_path, options={})
11
+ @name = name
12
+ @defaults = defaults
13
+ stack_prefix = options.fetch(:stack_prefix, '')
14
+ @stacks = stacks.map do |options|
15
+ begin
16
+ options['template_name'] ||= options['name']
17
+ options['name'] = stack_prefix + options['name']
18
+ Stack.new self, options.fetch('name'), options
19
+ rescue KeyError => err
20
+ Stacker.logger.fatal "Malformed YAML: #{err.message}"
21
+ exit 1
22
+ end
23
+ end
24
+ @templates_path = templates_path
25
+ @options = options
26
+ end
27
+
28
+ def client
29
+ @client ||= Aws::CloudFormation::Client.new region: name
30
+ end
31
+
32
+ def stack name
33
+ stacks.find { |s| s.name == name }.tap do |stk|
34
+ raise Stack::StackUndeclared.new name unless stk
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ require 'stacker/resolvers/resolver'
2
+
3
+ module Stacker
4
+ module Resolvers
5
+
6
+ class FileResolver < Resolver
7
+
8
+ def resolve
9
+ IO.read(ref).strip
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+ end