stacker 0.0.2 → 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: 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