stacker 0.0.2 → 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: bc3ba2bbeac3d470e4d355cf4605df35e6640720
4
+ data.tar.gz: c6e53ace12414864ae41bbfe9a3ed32135daab50
5
+ SHA512:
6
+ metadata.gz: 669c65102b76e13165b7668e56631f6b97d02ee5643f703ed2f0cdffa41c8d8844ae320e6ccfc218582a41f2e27f1f5079ecbe1ca10dd736bc42b768caae6662
7
+ data.tar.gz: 25f33fa2551709a54a55cd5a3633374c183078f8dbb7508f4ff986d180e9470d0ce1910bedf3cc13a507ad7a936ab3b7c99aa20832b4ddd5e163d9ab89e01096
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'rubygems'
3
4
  require 'stacker'
4
5
  require 'stacker/cli'
5
6
 
6
- Stacker::CLI.start
7
+ trap('INT') { puts ''; exit }
8
+
9
+ Stacker::Cli.start ARGV
@@ -1,5 +1,4 @@
1
- require "stacker/version"
2
- require "stacker/aws"
3
- require "stacker/repo"
4
- require "stacker/stack"
5
- require "stacker/template"
1
+ require 'stacker/region'
2
+ require 'stacker/stack'
3
+ require 'stacker/logging'
4
+ require 'stacker/version'
@@ -1,119 +1,216 @@
1
- require 'trollop'
1
+ require 'benchmark'
2
+ require 'stacker'
3
+ require 'thor'
2
4
  require 'yaml'
3
5
 
4
6
  module Stacker
5
- module CLI
6
- def self.start
7
- read_variables
7
+ class Cli < Thor
8
+ include Thor::Actions
8
9
 
9
- @stack = Stack.new(:name => @opts[:name])
10
+ default_path = ENV['STACKER_PATH'] || '.'
11
+ default_region = ENV['STACKER_REGION'] || 'us-east-1'
10
12
 
11
- explain if verbose?
13
+ method_option :path, default: default_path, banner: 'project path'
14
+ method_option :region, default: default_region, banner: 'AWS region name'
15
+ def initialize(*args); super(*args) end
12
16
 
13
- self.send @cmd
17
+ desc "init [PATH]", "Create stacker project directories"
18
+ def init path = nil
19
+ init_project path || options['path']
14
20
  end
15
21
 
16
- def self.create
17
- build_template
18
- @stack.create(:template => @opts[:output])
19
- describe
22
+ desc "list", "List stacks"
23
+ def list
24
+ Stacker.logger.inspect region.stacks.map(&:name)
20
25
  end
21
26
 
22
- def self.delete
23
- @stack.delete
24
- describe
27
+ desc "show STACK_NAME", "Show details of a stack"
28
+ def show stack_name
29
+ with_one_or_all stack_name do |stack|
30
+ Stacker.logger.inspect(
31
+ 'Description' => stack.description,
32
+ 'Status' => stack.status,
33
+ 'Updated' => stack.last_updated_time || stack.creation_time,
34
+ 'Capabilities' => stack.capabilities.remote,
35
+ 'Parameters' => stack.parameters.remote,
36
+ 'Outputs' => stack.outputs
37
+ )
38
+ end
25
39
  end
26
40
 
27
- def self.describe
28
- status @stack.describe.to_yaml
41
+ desc "status [STACK_NAME]", "Show stack status"
42
+ def status stack_name = nil
43
+ with_one_or_all(stack_name) do |stack|
44
+ Stacker.logger.debug stack.status.indent
45
+ end
29
46
  end
30
47
 
31
- def self.events
32
- status @stack.events.to_yaml
48
+ desc "diff [STACK_NAME]", "Show outstanding stack differences"
49
+ def diff stack_name = nil
50
+ with_one_or_all(stack_name) do |stack|
51
+ resolve stack
52
+ next unless full_diff stack
53
+ end
33
54
  end
34
55
 
35
- def self.resources
36
- status @stack.resources.to_yaml
56
+ desc "update [STACK_NAME]", "Create or update stack"
57
+ def update stack_name = nil
58
+ with_one_or_all(stack_name) do |stack|
59
+ resolve stack
60
+
61
+ if stack.exists?
62
+ next unless full_diff stack
63
+
64
+ if yes? "Update remote template with these changes (y/n)?"
65
+ time = Benchmark.realtime do
66
+ stack.update
67
+ end
68
+ Stacker.logger.info time stack_name, 'updated', time
69
+ else
70
+ Stacker.logger.warn 'Update skipped'
71
+ end
72
+ else
73
+ if yes? "#{stack.name} does not exist. Create it (y/n)?"
74
+ time = Benchmark.realtime do
75
+ stack.create
76
+ end
77
+ Stacker.logger.info time stack_name, 'created', time
78
+ else
79
+ Stacker.logger.warn 'Create skipped'
80
+ end
81
+ end
82
+ end
37
83
  end
38
84
 
39
- def self.list
40
- Stack.list.each do |stack|
41
- status stack.to_yaml
85
+ desc "dump [STACK_NAME]", "Download stack template"
86
+ def dump stack_name = nil
87
+ with_one_or_all(stack_name) do |stack|
88
+ if stack.exists?
89
+ diff = stack.template.diff :down, :color
90
+ next Stacker.logger.warn 'Stack up-to-date' if diff.length == 0
91
+
92
+ Stacker.logger.debug "\n" + diff.indent
93
+ if yes? "Update local template with these changes (y/n)?"
94
+ stack.template.dump
95
+ else
96
+ Stacker.logger.warn 'Pull skipped'
97
+ end
98
+ else
99
+ Stacker.logger.warn "#{stack.name} does not exist"
100
+ end
42
101
  end
43
102
  end
44
103
 
45
- def self.build_template
46
- template = Template.new(:git_repo => @opts[:git_repo],
47
- :attributes => @opts[:attributes],
48
- :output => @opts[:output],
49
- :path => @opts[:path],
50
- :resources => @opts[:resources])
51
- template.build
52
- end
53
-
54
- def self.read_variables
55
- @cmd = ARGV.shift
56
-
57
- @opts = case @cmd
58
- when "create"
59
- Trollop::options do
60
- opt :attributes, "Attributes definition file", :default => 'attributes.json.erb'
61
- opt :git_repo, "Git repository to export.", :type => String #:default => 'git@github.com:brettweavnet/stacker-repo.git'
62
- opt :path, "Path to configuraiton repo. If a -g or --git_repo is specified, it will override this setting.", :type => String
63
- opt :name, "Name of stack to create.", :type => String
64
- opt :output, "Template output file.", :default => 'stack.json'
65
- opt :resources, "Resource definition file", :default => 'resources.json.erb'
66
- opt :silent, "Silent mode.", :default => false
67
- opt :temp_dir, "Directory to export configuration git repo.", :default => File.join(Dir.home, "stack-repo")
68
- opt :verbose, "Verbose mode.", :default => false
69
- end
70
- when "delete"
71
- Trollop::options do
72
- opt :name, "Name of stack to delete.", :type => String
73
- opt :silent, "Silent mode.", :default => false
74
- opt :verbose, "Verbose mode.", :default => false
75
- end
76
- when "describe"
77
- Trollop::options do
78
- opt :name, "Name of stack to describe.", :type => String
79
- opt :verbose, "Verbose mode.", :default => false
80
- end
81
- when "events"
82
- Trollop::options do
83
- opt :name, "Name of stack who's events to display.", :type => String
84
- opt :verbose, "Verbose mode.", :default => false
104
+ desc "fmt [STACK_NAME]", "Re-format template JSON"
105
+ def fmt stack_name = nil
106
+ with_one_or_all(stack_name) do |stack|
107
+ if stack.template.exists?
108
+ Stacker.logger.warn 'Formatting...'
109
+ stack.template.write
110
+ else
111
+ Stacker.logger.warn "#{stack.name} does not exist"
85
112
  end
86
- when "list"
87
- Trollop::options do
88
- opt :verbose, "Verbose mode.", :default => false
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def init_project path
119
+ project_path = File.expand_path path
120
+
121
+ %w[ regions templates ].each do |dir|
122
+ directory_path = File.join project_path, dir
123
+ unless Dir.exists? directory_path
124
+ Stacker.logger.debug "Creating directory at #{directory_path}"
125
+ FileUtils.mkdir_p directory_path
89
126
  end
90
- when "resources"
91
- Trollop::options do
92
- opt :name, "Name of stack who's resources to display.", :type => String
93
- opt :verbose, "Verbose mode.", :default => false
127
+ end
128
+
129
+ region_path = File.join project_path, 'regions', 'us-east-1.yml'
130
+ unless File.exists? region_path
131
+ Stacker.logger.debug "Creating region file at #{region_path}"
132
+ File.open(region_path, 'w+') { |f| f.print <<-YAML }
133
+ defaults:
134
+ parameters:
135
+ CidrBlock: '10.0'
136
+ stacks:
137
+ - name: VPC
138
+ YAML
139
+ end
140
+ end
141
+
142
+ def full_diff stack
143
+ templ_diff = stack.template.diff :color
144
+ param_diff = stack.parameters.diff :color
145
+
146
+ if (templ_diff + param_diff).length == 0
147
+ Stacker.logger.warn 'Stack up-to-date'
148
+ return false
149
+ end
150
+
151
+ Stacker.logger.info "\n#{templ_diff.indent}\n" if templ_diff.length > 0
152
+ Stacker.logger.info "\n#{param_diff.indent}\n" if param_diff.length > 0
153
+
154
+ true
155
+ end
156
+
157
+ def region
158
+ @region ||= begin
159
+ config_path = File.join working_path, 'regions', "#{options['region']}.yml"
160
+ if File.exists? config_path
161
+ begin
162
+ config = YAML.load_file(config_path)
163
+ rescue Psych::SyntaxError => err
164
+ Stacker.logger.fatal err.message
165
+ exit 1
166
+ end
167
+
168
+ defaults = config.fetch 'defaults', {}
169
+ stacks = config.fetch 'stacks', {}
170
+
171
+ Region.new options['region'], defaults, stacks, templates_path
172
+ else
173
+ Stacker.logger.fatal "#{options['region']}.yml does not exist. Please configure or use stacker init"
174
+ exit 1
94
175
  end
95
- else
96
- status "\nUnknown command: '#{@cmd}'.\n"
97
- status "\nstacker [create|describe|delete|list] --help\n"
98
- exit 1
99
176
  end
100
177
  end
101
178
 
102
- def self.explain
103
- status "\nExecuting #{@cmd} with following options:\n\n"
104
- status @opts.to_yaml
179
+ def resolve stack
180
+ return {} if stack.parameters.resolver.dependencies.none?
181
+ Stacker.logger.debug 'Resolving dependencies...'
182
+ stack.parameters.resolved
105
183
  end
106
184
 
107
- def self.verbose?
108
- @opts[:verbose] && !silent?
185
+ def time (stack, action, benchmark)
186
+ return "Stack #{stack} #{action} in: #{(benchmark / 60).floor} min and #{(benchmark % 60).round} seconds."
109
187
  end
110
188
 
111
- def self.silent?
112
- @opts[:silent] if @opts
189
+ def with_one_or_all stack_name = nil, &block
190
+ yield_with_stack = proc do |stack|
191
+ Stacker.logger.info "#{stack.name}:"
192
+ yield stack
193
+ Stacker.logger.info ''
194
+ end
195
+
196
+ if stack_name
197
+ yield_with_stack.call region.stack(stack_name)
198
+ else
199
+ region.stacks.each(&yield_with_stack)
200
+ end
201
+
202
+ rescue Stacker::Stack::Error => err
203
+ Stacker.logger.fatal err.message
204
+ exit 1
205
+ end
206
+
207
+ def templates_path
208
+ File.join working_path, 'templates'
113
209
  end
114
210
 
115
- def self.status(msg)
116
- printf("#{msg}") unless silent?
211
+ def working_path
212
+ File.expand_path options['path']
117
213
  end
214
+
118
215
  end
119
216
  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,61 @@
1
+ require 'coderay'
2
+ require 'delegate'
3
+ require 'indentation'
4
+ require 'rainbow'
5
+
6
+ module Stacker
7
+ module_function
8
+
9
+ class << self
10
+
11
+ class PrettyLogger < SimpleDelegator
12
+ def initialize logger
13
+ super
14
+
15
+ old_formatter = logger.formatter
16
+
17
+ logger.formatter = proc do |level, time, prog, msg|
18
+ unless msg.start_with?("\e")
19
+ color = case level
20
+ when 'FATAL' then :red
21
+ when 'WARN' then :yellow
22
+ when 'INFO' then :blue
23
+ when 'DEBUG' then '333333'
24
+ else :default
25
+ end
26
+ msg = msg.color(color)
27
+ end
28
+
29
+ old_formatter.call level, time, prog, msg
30
+ end
31
+ end
32
+
33
+ %w[ debug info warn fatal ].each do |level|
34
+ define_method level do |msg, opts = {}|
35
+ if opts.include? :highlight
36
+ msg = CodeRay.scan(msg, opts[:highlight]).terminal
37
+ end
38
+ __getobj__.__send__ level, msg
39
+ end
40
+ end
41
+
42
+ def inspect object
43
+ info object.to_yaml[4..-1].strip.indent, highlight: :yaml
44
+ end
45
+ end
46
+
47
+ def logger= logger
48
+ @logger = PrettyLogger.new logger
49
+ end
50
+
51
+ def logger
52
+ @logger ||= begin
53
+ logger = Logger.new STDOUT
54
+ logger.level = Logger::DEBUG
55
+ logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
56
+ PrettyLogger.new logger
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,32 @@
1
+ require 'aws-sdk'
2
+ require 'stacker/stack'
3
+
4
+ module Stacker
5
+ class Region
6
+
7
+ attr_reader :name, :defaults, :stacks, :templates_path
8
+
9
+ def initialize(name, defaults, stacks, templates_path)
10
+ @name = name
11
+ @defaults = defaults
12
+ @stacks = stacks.map do |options|
13
+ begin
14
+ Stack.new self, options.fetch('name'), options
15
+ rescue KeyError => err
16
+ Stacker.logger.fatal "Malformed YAML: #{err.message}"
17
+ exit 1
18
+ end
19
+ end
20
+ @templates_path = templates_path
21
+ end
22
+
23
+ def client
24
+ @client ||= AWS::CloudFormation.new region: name
25
+ end
26
+
27
+ def stack name
28
+ stacks.find { |s| s.name == name } || Stack.new(self, name)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ module Stacker
2
+ class Resolver
3
+
4
+ attr_reader :region, :parameters
5
+
6
+ def initialize region, parameters
7
+ @region, @parameters = region, parameters
8
+ end
9
+
10
+ def dependencies
11
+ @dependencies ||= parameters.select { |_, value|
12
+ value.is_a?(Hash)
13
+ }.map { |_, value|
14
+ "#{value.fetch('Stack')}.#{value.fetch('Output')}"
15
+ }
16
+ end
17
+
18
+ def resolved
19
+ @resolved ||= Hash[parameters.map do |name, value|
20
+ if value.is_a? Hash
21
+ stack = region.stack value.fetch('Stack')
22
+ value = stack.outputs.fetch value.fetch('Output')
23
+ end
24
+ [ name, value ]
25
+ end]
26
+ end
27
+
28
+ end
29
+ end
@@ -1,45 +1,129 @@
1
- module Stacker
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'aws-sdk'
4
+ require 'memoist'
5
+ require 'stacker/stack/capabilities'
6
+ require 'stacker/stack/parameters'
7
+ require 'stacker/stack/template'
2
8
 
9
+ module Stacker
3
10
  class Stack
4
- attr_accessor :name
5
11
 
6
- def self.list
7
- list = []
12
+ class Error < StandardError; end
13
+ class DoesNotExistError < Error; end
14
+ class MissingParameters < Error; end
15
+ class UpToDateError < Error; end
8
16
 
9
- stacks = AWS::CloudFormation::Stack.list
10
- stacks.body["Stacks"].each do |stack|
11
- list << stack['StackName']
12
- end
17
+ extend Memoist
18
+
19
+ CLIENT_METHODS = %w[
20
+ creation_time
21
+ description
22
+ exists?
23
+ last_updated_time
24
+ status
25
+ status_reason
26
+ ]
13
27
 
14
- list
28
+ attr_reader :region, :name, :options
29
+
30
+ def initialize region, name, options = {}
31
+ @region, @name, @options = region, name, options
15
32
  end
16
33
 
17
- def initialize(args={})
18
- self.name = args[:name] if args[:name]
19
- @stack = AWS::CloudFormation::Stack.new
34
+ def client
35
+ @client ||= region.client.stacks[name]
20
36
  end
21
37
 
22
- def create(args)
23
- file = File.open(args[:template], "rb")
24
- contents = file.read
25
- @stack.create(:name => name, :body => contents)
38
+ delegate *CLIENT_METHODS, to: :client
39
+ memoize *CLIENT_METHODS
40
+
41
+ %w[complete failed in_progress].each do |stage|
42
+ define_method(:"#{stage}?") { status =~ /#{stage.upcase}/ }
26
43
  end
27
44
 
28
- def delete
29
- @stack.delete(:name => name)
45
+ def template
46
+ @template ||= Template.new self
30
47
  end
31
48
 
32
- def describe
33
- @stack.describe(:name => name).body["Stacks"].first
49
+ def parameters
50
+ @parameters ||= Parameters.new self
34
51
  end
35
52
 
36
- def events
37
- @stack.events(:name => name).body["StackEvents"]
53
+ def capabilities
54
+ @capabilities ||= Capabilities.new self
38
55
  end
39
56
 
40
- def resources
41
- @stack.resources(:name => name).body["StackResources"]
57
+ def outputs
58
+ @outputs ||= begin
59
+ return {} unless complete?
60
+ Hash[client.outputs.map { |output| [ output.key, output.value ] }]
61
+ end
42
62
  end
43
- end
44
63
 
64
+ def create blocking = true
65
+ if exists?
66
+ Stacker.logger.warn 'Stack already exists'
67
+ return
68
+ end
69
+
70
+ if parameters.missing.any?
71
+ raise MissingParameters.new(
72
+ "Required parameters missing: #{parameters.missing.join ', '}"
73
+ )
74
+ end
75
+
76
+ Stacker.logger.info 'Creating stack'
77
+
78
+ region.client.stacks.create(
79
+ name,
80
+ template.local,
81
+ parameters: parameters.resolved,
82
+ capabilities: capabilities.local
83
+ )
84
+
85
+ wait_while_status 'CREATE_IN_PROGRESS' if blocking
86
+ rescue AWS::CloudFormation::Errors::ValidationError => err
87
+ raise Error.new err.message
88
+ end
89
+
90
+ def update blocking = true
91
+ if parameters.missing.any?
92
+ raise MissingParameters.new(
93
+ "Required parameters missing: #{parameters.missing.join ', '}"
94
+ )
95
+ end
96
+
97
+ Stacker.logger.info 'Updating stack'
98
+
99
+ client.update(
100
+ template: template.local,
101
+ parameters: parameters.resolved,
102
+ capabilities: capabilities.local
103
+ )
104
+
105
+ wait_while_status 'UPDATE_IN_PROGRESS' if blocking
106
+ rescue AWS::CloudFormation::Errors::ValidationError => err
107
+ case err.message
108
+ when /does not exist/
109
+ raise DoesNotExistError.new err.message
110
+ when /No updates/
111
+ raise UpToDateError.new err.message
112
+ else
113
+ raise Error.new err.message
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def wait_while_status wait_status
120
+ while flush_cache(:status) && status == wait_status
121
+ Stacker.logger.debug "#{name} Status => #{status}"
122
+ sleep 5
123
+ end
124
+ Stacker.logger.info "#{name} Status => #{status}"
125
+ end
126
+
127
+ end
45
128
  end
129
+
@@ -0,0 +1,17 @@
1
+ require 'stacker/stack/component'
2
+
3
+ module Stacker
4
+ class Stack
5
+ class Capabilities < Component
6
+
7
+ def local
8
+ @local ||= Array(stack.options.fetch 'capabilities', [])
9
+ end
10
+
11
+ def remote
12
+ @remote ||= client.capabilities
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ require 'stacker/stack'
2
+
3
+ module Stacker
4
+ class Stack
5
+ # an abstract base class for stack components (template, parameters)
6
+ class Component
7
+
8
+ attr_reader :stack
9
+
10
+ def initialize stack
11
+ @stack = stack
12
+ end
13
+
14
+ private
15
+
16
+ def client
17
+ stack.client
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,68 @@
1
+ require 'memoist'
2
+ require 'stacker/differ'
3
+ require 'stacker/resolver'
4
+ require 'stacker/stack/component'
5
+
6
+ module Stacker
7
+ class Stack
8
+ class Parameters < Component
9
+
10
+ extend Memoist
11
+
12
+ # everything required by the template
13
+ def template_definitions
14
+ stack.template.local.fetch 'Parameters', {}
15
+ end
16
+
17
+ def region_defaults
18
+ stack.region.defaults.fetch 'parameters', {}
19
+ end
20
+
21
+ # template defaults merged with region and stack-specific overrides
22
+ def local
23
+ region_defaults = stack.region.defaults.fetch 'parameters', {}
24
+
25
+ template_defaults = Hash[
26
+ template_definitions.select { |_, opts|
27
+ opts.key?('Default')
28
+ }.map { |name, opts|
29
+ [name, opts['Default']]
30
+ }
31
+ ]
32
+
33
+ available = template_defaults.merge(
34
+ region_defaults.merge(
35
+ stack.options.fetch 'parameters', {}
36
+ )
37
+ )
38
+
39
+ available.slice(*template_definitions.keys)
40
+ end
41
+
42
+ def missing
43
+ template_definitions.keys - local.keys
44
+ end
45
+
46
+ def remote
47
+ client.parameters
48
+ end
49
+ memoize :remote
50
+
51
+ def resolved
52
+ resolver.resolved
53
+ end
54
+ memoize :resolved
55
+
56
+ def resolver
57
+ Resolver.new stack.region, local
58
+ end
59
+ memoize :resolver
60
+
61
+ def diff *args
62
+ Differ.yaml_diff Hash[resolved.sort], Hash[remote.sort], *args
63
+ end
64
+ memoize :diff
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,86 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+ require 'json'
3
+ require 'memoist'
4
+ require 'stacker/differ'
5
+ require 'stacker/stack/component'
6
+
7
+ module Stacker
8
+ class Stack
9
+ class Template < Component
10
+
11
+ FORMAT_VERSION = '2010-09-09'
12
+
13
+ extend Memoist
14
+
15
+ def exists?
16
+ File.exists? path
17
+ end
18
+
19
+ def local
20
+ @local ||= begin
21
+ if exists?
22
+ template = JSON.parse File.read path
23
+ template['AWSTemplateFormatVersion'] ||= FORMAT_VERSION
24
+ template
25
+ else
26
+ {}
27
+ end
28
+ end
29
+ end
30
+
31
+ def remote
32
+ @remote ||= JSON.parse client.template
33
+ rescue AWS::CloudFormation::Errors::ValidationError => err
34
+ if err.message =~ /does not exist/
35
+ raise DoesNotExistError.new err.message
36
+ else
37
+ raise Error.new err.message
38
+ end
39
+ end
40
+
41
+ def diff *args
42
+ Differ.json_diff local, remote, *args
43
+ end
44
+ memoize :diff
45
+
46
+ def write value = local
47
+ File.write path, JSONFormatter.format(value)
48
+ end
49
+
50
+ def dump
51
+ write remote
52
+ end
53
+
54
+ private
55
+
56
+ def path
57
+ @path ||= File.join(
58
+ stack.region.templates_path,
59
+ "#{stack.options.fetch('template_name', stack.name)}.json"
60
+ )
61
+ end
62
+
63
+ class JSONFormatter
64
+ STR = '\"[^\"]+\"'
65
+
66
+ def self.format object
67
+ formatted = JSON.pretty_generate object
68
+
69
+ # put empty arrays on a single line
70
+ formatted.gsub! /: \[\s*\]/m, ': []'
71
+
72
+ # put { "Ref": ... } on a single line
73
+ formatted.gsub! /\{\s+\"Ref\"\:\s+(?<ref>#{STR})\s+\}/m,
74
+ '{ "Ref": \\k<ref> }'
75
+
76
+ # put { "Fn::GetAtt": ... } on a single line
77
+ formatted.gsub! /\{\s+\"Fn::GetAtt\"\: \[\s+(?<key>#{STR}),\s+(?<val>#{STR})\s+\]\s+\}/m,
78
+ '{ "Fn::GetAtt": [ \\k<key>, \\k<val> ] }'
79
+
80
+ formatted + "\n"
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -1,3 +1,3 @@
1
1
  module Stacker
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,118 +1,199 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stacker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
5
- prerelease:
4
+ version: 0.1.0
6
5
  platform: ruby
7
6
  authors:
8
- - Brett Weaver
7
+ - Cotap, Inc.
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-01-10 00:00:00.000000000 -08:00
13
- default_executable:
11
+ date: 2014-11-25 00:00:00.000000000 Z
14
12
  dependencies:
15
13
  - !ruby/object:Gem::Dependency
16
- name: rspec
17
- requirement: &84986040 !ruby/object:Gem::Requirement
18
- none: false
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
19
16
  requirements:
20
- - - ! '>='
17
+ - - ~>
21
18
  - !ruby/object:Gem::Version
22
- version: '0'
23
- type: :development
19
+ version: '4.0'
20
+ type: :runtime
24
21
  prerelease: false
25
- version_requirements: *84986040
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: coderay
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: diffy
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
26
69
  - !ruby/object:Gem::Dependency
27
- name: fog
28
- requirement: &84985830 !ruby/object:Gem::Requirement
29
- none: false
70
+ name: indentation
71
+ requirement: !ruby/object:Gem::Requirement
30
72
  requirements:
31
- - - ! '>='
73
+ - - ~>
32
74
  - !ruby/object:Gem::Version
33
- version: '0'
75
+ version: '0.0'
34
76
  type: :runtime
35
77
  prerelease: false
36
- version_requirements: *84985830
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.0'
37
83
  - !ruby/object:Gem::Dependency
38
- name: git
39
- requirement: &84985620 !ruby/object:Gem::Requirement
40
- none: false
84
+ name: memoist
85
+ requirement: !ruby/object:Gem::Requirement
41
86
  requirements:
42
- - - ! '>='
87
+ - - ~>
43
88
  - !ruby/object:Gem::Version
44
- version: '0'
89
+ version: '0.9'
45
90
  type: :runtime
46
91
  prerelease: false
47
- version_requirements: *84985620
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
48
97
  - !ruby/object:Gem::Dependency
49
- name: json
50
- requirement: &84985410 !ruby/object:Gem::Requirement
51
- none: false
98
+ name: rainbow
99
+ requirement: !ruby/object:Gem::Requirement
52
100
  requirements:
53
- - - ! '>='
101
+ - - ~>
54
102
  - !ruby/object:Gem::Version
55
- version: '0'
103
+ version: '1.1'
56
104
  type: :runtime
57
105
  prerelease: false
58
- version_requirements: *84985410
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '1.1'
59
111
  - !ruby/object:Gem::Dependency
60
- name: trollop
61
- requirement: &84985200 !ruby/object:Gem::Requirement
62
- none: false
112
+ name: thor
113
+ requirement: !ruby/object:Gem::Requirement
63
114
  requirements:
64
- - - ! '>='
115
+ - - ~>
65
116
  - !ruby/object:Gem::Version
66
- version: '0'
117
+ version: '0.18'
67
118
  type: :runtime
68
119
  prerelease: false
69
- version_requirements: *84985200
70
- description: stacker integrates with git and AWS cloud formation to rapidley build
71
- full application stacks from dynamic templates
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: '0.18'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '='
130
+ - !ruby/object:Gem::Version
131
+ version: 10.1.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '='
137
+ - !ruby/object:Gem::Version
138
+ version: 10.1.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '='
144
+ - !ruby/object:Gem::Version
145
+ version: 2.14.1
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '='
151
+ - !ruby/object:Gem::Version
152
+ version: 2.14.1
153
+ description: Stacker helps you assemple and manage CloudFormation stacks and dependencies.
72
154
  email:
73
- - brett@weaver.io
155
+ - martin@cotap.com
156
+ - evan@cotap.com
74
157
  executables:
75
158
  - stacker
76
159
  extensions: []
77
160
  extra_rdoc_files: []
78
161
  files:
79
- - .gitignore
80
- - .rvmrc
81
- - Gemfile
82
- - README.md
83
- - Rakefile
84
162
  - bin/stacker
85
163
  - lib/stacker.rb
86
- - lib/stacker/aws.rb
87
164
  - lib/stacker/cli.rb
88
- - lib/stacker/repo.rb
165
+ - lib/stacker/differ.rb
166
+ - lib/stacker/logging.rb
167
+ - lib/stacker/region.rb
168
+ - lib/stacker/resolver.rb
89
169
  - lib/stacker/stack.rb
90
- - lib/stacker/template.rb
170
+ - lib/stacker/stack/capabilities.rb
171
+ - lib/stacker/stack/component.rb
172
+ - lib/stacker/stack/parameters.rb
173
+ - lib/stacker/stack/template.rb
91
174
  - lib/stacker/version.rb
92
- - stacker.gemspec
93
- has_rdoc: true
94
- homepage: http://brettweavnet.github.com/stacker
95
- licenses: []
175
+ homepage: https://github.com/cotap/stacker
176
+ licenses:
177
+ - MIT
178
+ metadata: {}
96
179
  post_install_message:
97
180
  rdoc_options: []
98
181
  require_paths:
99
182
  - lib
100
183
  required_ruby_version: !ruby/object:Gem::Requirement
101
- none: false
102
184
  requirements:
103
- - - ! '>='
185
+ - - '>='
104
186
  - !ruby/object:Gem::Version
105
- version: '0'
187
+ version: 1.9.3
106
188
  required_rubygems_version: !ruby/object:Gem::Requirement
107
- none: false
108
189
  requirements:
109
- - - ! '>='
190
+ - - '>='
110
191
  - !ruby/object:Gem::Version
111
192
  version: '0'
112
193
  requirements: []
113
- rubyforge_project: stacker
114
- rubygems_version: 1.6.2
194
+ rubyforge_project:
195
+ rubygems_version: 2.2.2
115
196
  signing_key:
116
- specification_version: 3
117
- summary: stacker helps build cloud stacks
197
+ specification_version: 4
198
+ summary: Easily assemble CloudFormation stacks.
118
199
  test_files: []
data/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- *.gem
2
- .bundle
3
- Gemfile.lock
4
- pkg/*
5
- *.json
data/.rvmrc DELETED
@@ -1 +0,0 @@
1
- rvm use ruby-1.9.2-p180@stacker --create
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- # Specify your gem's dependencies in stacker.gemspec
4
- gemspec
data/README.md DELETED
@@ -1,21 +0,0 @@
1
- Stacker like to build full application stacks. Creates cloud formation templates from a set of attributes, resource templates and data files.
2
-
3
- Getting started, first get the gem
4
-
5
- gem install stacker
6
-
7
- Then set the following environment variables to your AWS credentials:
8
-
9
- export AWS_SECRET_ACCESS_KEY='my aws secret key'
10
-
11
- export AWS_ACCESS_KEY_ID='my aws access key'
12
-
13
- export STACKER_DEFAULT_KEY='the name of my ssh key in AWS'
14
-
15
- Create your first stack from our repository
16
-
17
- stacker create -g git@github.com:brettweavnet/stacker-repo.git -n my-new-app
18
-
19
- You can then see the status on the stack build
20
-
21
- stacker describe -n my_new_app
data/Rakefile DELETED
@@ -1 +0,0 @@
1
- require "bundler/gem_tasks"
@@ -1,47 +0,0 @@
1
- require 'fog'
2
-
3
- module Stacker
4
- module AWS
5
- module CloudFormation
6
- class Stack
7
- def initialize
8
- @connect = Stack.connect
9
- end
10
-
11
- def self.list
12
- Stack.connect.describe_stacks
13
- end
14
-
15
- def create(args)
16
- @connect.create_stack(args[:name], 'TemplateBody' => args[:body] )
17
- end
18
-
19
- def delete(args)
20
- @connect.delete_stack(args[:name])
21
- end
22
-
23
- def describe(args)
24
- @connect.describe_stacks('StackName' => args[:name])
25
- end
26
-
27
- def events(args)
28
- @connect.describe_stack_events(args[:name])
29
- end
30
-
31
- def resources(args)
32
- @connect.describe_stack_resources('StackName' => args[:name])
33
- end
34
-
35
- private
36
-
37
- def self.connect
38
- Fog::AWS::CloudFormation.new(
39
- :aws_access_key_id => ENV['AWS_ACCESS_KEY_ID'],
40
- :aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY']
41
- )
42
- end
43
-
44
- end
45
- end
46
- end
47
- end
@@ -1,22 +0,0 @@
1
- require "git"
2
-
3
- module Stacker
4
- class Repo
5
-
6
- attr_accessor :git_repo, :temp_repo_export_dir
7
-
8
- def initialize(args)
9
- self.git_repo = args[:git_repo]
10
- self.temp_repo_export_dir = "/tmp/repo-#{(0...8).map{65.+(rand(25)).chr}.join}"
11
- end
12
-
13
- def export
14
- Dir.mkdir(temp_repo_export_dir, 0700)
15
- Git.export(git_repo, temp_repo_export_dir)
16
- end
17
-
18
- def remove
19
- FileUtils.remove_dir(temp_repo_export_dir)
20
- end
21
- end
22
- end
@@ -1,77 +0,0 @@
1
- require "erb"
2
- require "json"
3
-
4
- module Stacker
5
-
6
- class Template
7
-
8
- attr_accessor :attributes_file, :git_repo, :output_file,
9
- :repo_path, :resources_file
10
-
11
- def initialize(args)
12
- self.attributes_file = args[:attributes]
13
- self.git_repo = args[:git_repo]
14
- self.output_file = args[:output]
15
- self.repo_path = args[:path]
16
- self.resources_file = args[:resources]
17
-
18
- @repo = Repo.new(:git_repo => git_repo)
19
- end
20
-
21
- def build
22
- begin
23
- export_git_repo if git_repo
24
- load_attributes
25
- load_data
26
- load_resources
27
- create_template
28
- ensure
29
- remove_git_repo if git_repo
30
- end
31
- end
32
-
33
- private
34
-
35
- def load_attributes
36
- template = IO.read("#{repo_path}/attributes/#{attributes_file}")
37
- attributes_json = ERB.new(template).result(binding).to_s
38
- parsed_attributes = JSON.parse(attributes_json)
39
- @data = parsed_attributes['data']
40
- @resources = parsed_attributes['resources']
41
- end
42
-
43
- def load_data
44
- data_dir = "#{repo_path}/data"
45
-
46
- Dir.entries(data_dir).each do |data_file|
47
- if data_file =~ /.erb$/
48
- template = IO.read("#{data_dir}/#{data_file}")
49
- data = ERB.new(template).result(binding).to_s
50
- instance_variable_set("@#{data_file.gsub(/.erb$/, '')}", data)
51
- end
52
- end
53
- end
54
-
55
- def load_resources
56
- resource_template = IO.read("#{repo_path}/resources/#{resources_file}")
57
- ERB.new(resource_template).result(binding)
58
- end
59
-
60
- def create_template
61
- output_file_handle = File.new(output_file,'w')
62
- output_file_handle.puts load_resources
63
- output_file_handle.close
64
- end
65
-
66
- def export_git_repo
67
- @repo.export
68
- self.repo_path = @repo.temp_repo_export_dir
69
- end
70
-
71
- def remove_git_repo
72
- @repo.remove
73
- end
74
-
75
- end
76
-
77
- end
@@ -1,26 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "stacker/version"
4
-
5
- Gem::Specification.new do |s|
6
- s.name = "stacker"
7
- s.version = Stacker::VERSION
8
- s.authors = ["Brett Weaver"]
9
- s.email = ["brett@weaver.io"]
10
- s.homepage = "http://brettweavnet.github.com/stacker"
11
- s.summary = %q{stacker helps build cloud stacks}
12
- s.description = %q{stacker integrates with git and AWS cloud formation to rapidley build full application stacks from dynamic templates}
13
-
14
- s.rubyforge_project = "stacker"
15
-
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
- s.require_paths = ["lib"]
20
-
21
- s.add_development_dependency "rspec"
22
- s.add_runtime_dependency "fog"
23
- s.add_runtime_dependency "git"
24
- s.add_runtime_dependency "json"
25
- s.add_runtime_dependency "trollop"
26
- end