stacker-yaml 0.1.0

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