cfer 0.1.1
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.
- checksums.yaml +15 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +221 -0
- data/Rakefile +41 -0
- data/bin/cfer +13 -0
- data/bin/cfer-dbg +25 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/cfer-demo.gif +0 -0
- data/cfer.gemspec +37 -0
- data/examples/instance.rb +84 -0
- data/examples/vpc.rb +84 -0
- data/lib/cfer.rb +236 -0
- data/lib/cfer/block.rb +33 -0
- data/lib/cfer/cfn/aws.rb +29 -0
- data/lib/cfer/cfn/client.rb +164 -0
- data/lib/cfer/cli.rb +138 -0
- data/lib/cfer/core/client.rb +15 -0
- data/lib/cfer/core/fn.rb +55 -0
- data/lib/cfer/core/resource.rb +44 -0
- data/lib/cfer/core/stack.rb +170 -0
- data/lib/cfer/util/error.rb +31 -0
- data/lib/cfer/version.rb +3 -0
- data/lib/cferext/aws/ec2/instance.rb +12 -0
- data/lib/cferext/provisioning.rb +6 -0
- metadata +271 -0
data/lib/cfer/cfn/aws.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Cfer::Cfn::AWS
|
2
|
+
class << self
|
3
|
+
include Cfer::Core
|
4
|
+
def account_id
|
5
|
+
Fn::ref 'AWS::AccountId'
|
6
|
+
end
|
7
|
+
|
8
|
+
def notification_arns
|
9
|
+
Fn::ref 'AWS::NotificationARNs'
|
10
|
+
end
|
11
|
+
|
12
|
+
def no_value
|
13
|
+
Fn::ref 'AWS::NoValue'
|
14
|
+
end
|
15
|
+
|
16
|
+
def region
|
17
|
+
Fn::ref 'AWS::Region'
|
18
|
+
end
|
19
|
+
|
20
|
+
def stack_id
|
21
|
+
Fn::ref 'AWS::StackId'
|
22
|
+
end
|
23
|
+
|
24
|
+
def stack_name
|
25
|
+
Fn::ref 'AWS::StackName'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require_relative '../core/client'
|
2
|
+
module Cfer::Cfn
|
3
|
+
|
4
|
+
class Client < Cfer::Core::Client
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@name = options[:stack_name]
|
9
|
+
options.delete :stack_name
|
10
|
+
@cfn = Aws::CloudFormation::Client.new(options)
|
11
|
+
flush_cache
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_stack(*args)
|
15
|
+
begin
|
16
|
+
@cfn.create_stack(*args)
|
17
|
+
rescue Aws::CloudFormation::Errors::AlreadyExistsException
|
18
|
+
raise Cfer::Util::StackExistsError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def responds_to?(method)
|
23
|
+
@cfn.responds_to? method
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method, *args, &block)
|
27
|
+
@cfn.send(method, *args, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def resolve(param)
|
31
|
+
# See if the value follows the form @<stack>.<output>
|
32
|
+
m = /^@(.+?)\.(.+)$/.match(param)
|
33
|
+
|
34
|
+
if m
|
35
|
+
fetch_output(m[1], m[2])
|
36
|
+
else
|
37
|
+
param
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def converge(stack, options = {})
|
43
|
+
Preconditions.check(@name).is_not_nil
|
44
|
+
Preconditions.check(stack) { is_not_nil and has_type(Cfer::Core::Stack) }
|
45
|
+
|
46
|
+
response = validate_template(template_body: stack.to_cfn)
|
47
|
+
|
48
|
+
parameters = response.parameters.map do |tmpl_param|
|
49
|
+
cfn_param = stack.parameters[tmpl_param.parameter_key] || raise(Cfer::Util::CferError, "Parameter #{tmpl_param.parameter_key} was required, but not specified")
|
50
|
+
cfn_param = resolve(cfn_param)
|
51
|
+
|
52
|
+
output_val = tmpl_param.no_echo ? '*****' : cfn_param
|
53
|
+
Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key}=#{output_val}"
|
54
|
+
|
55
|
+
{
|
56
|
+
parameter_key: tmpl_param.parameter_key,
|
57
|
+
parameter_value: cfn_param,
|
58
|
+
use_previous_value: false
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
options = {
|
63
|
+
stack_name: name,
|
64
|
+
template_body: stack.to_cfn,
|
65
|
+
parameters: parameters,
|
66
|
+
capabilities: response.capabilities
|
67
|
+
}
|
68
|
+
|
69
|
+
created = false
|
70
|
+
cfn_stack = begin
|
71
|
+
created = true
|
72
|
+
create_stack options
|
73
|
+
rescue Cfer::Util::StackExistsError
|
74
|
+
update_stack options
|
75
|
+
end
|
76
|
+
|
77
|
+
flush_cache
|
78
|
+
cfn_stack
|
79
|
+
end
|
80
|
+
|
81
|
+
# Yields to the given block for each CloudFormation event that qualifies, given the specified options.
|
82
|
+
# @param options [Hash] The options hash
|
83
|
+
# @option options [Fixnum] :number The maximum number of already-existing CloudFormation events to yield.
|
84
|
+
# @option options [Boolean] :follow Set to true to wait until the stack enters a `COMPLETE` or `FAILED` state, yielding events as they occur.
|
85
|
+
def tail(options = {}, &block)
|
86
|
+
q = []
|
87
|
+
event_id_highwater = nil
|
88
|
+
counter = 0
|
89
|
+
number = options[:number] || 0
|
90
|
+
for_each_event name do |event|
|
91
|
+
q.unshift event if counter < number
|
92
|
+
counter = counter + 1
|
93
|
+
end
|
94
|
+
|
95
|
+
while q.size > 0
|
96
|
+
event = q.shift
|
97
|
+
yield event
|
98
|
+
event_id_highwater = event.event_id
|
99
|
+
end
|
100
|
+
|
101
|
+
running = true
|
102
|
+
if options[:follow]
|
103
|
+
while running
|
104
|
+
stack_status = describe_stacks(stack_name: name).stacks.first.stack_status
|
105
|
+
running = running && (/.+_(COMPLETE|FAILED)$/.match(stack_status) == nil)
|
106
|
+
|
107
|
+
yielding = true
|
108
|
+
for_each_event name do |event|
|
109
|
+
if event_id_highwater == event.event_id
|
110
|
+
yielding = false
|
111
|
+
end
|
112
|
+
|
113
|
+
if yielding
|
114
|
+
q.unshift event
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
while q.size > 0
|
119
|
+
event = q.shift
|
120
|
+
yield event
|
121
|
+
event_id_highwater = event.event_id
|
122
|
+
end
|
123
|
+
|
124
|
+
sleep 1 if running unless options[:no_sleep]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def fetch_stack(stack_name = @name)
|
130
|
+
@stack_cache[stack_name] ||= describe_stacks(stack_name: stack_name).stacks.first.to_h
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_h
|
134
|
+
@stack.to_h
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def flush_cache
|
140
|
+
@stack_cache = {}
|
141
|
+
end
|
142
|
+
|
143
|
+
def fetch_output(stack_name, output_name)
|
144
|
+
stack = fetch_stack(stack_name)
|
145
|
+
|
146
|
+
output = stack[:outputs].find do |o|
|
147
|
+
o[:output_key] == output_name
|
148
|
+
end
|
149
|
+
|
150
|
+
if output
|
151
|
+
output[:output_value]
|
152
|
+
else
|
153
|
+
raise CferError, "Stack #{stack_name} has no output value named `#{output_name}`"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def for_each_event(stack_name)
|
158
|
+
describe_stack_events(stack_name: stack_name).stack_events.each do |event|
|
159
|
+
yield event
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
data/lib/cfer/cli.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'rainbow'
|
3
|
+
require 'table_print'
|
4
|
+
|
5
|
+
module Cfer
|
6
|
+
class Cli < Thor
|
7
|
+
|
8
|
+
namespace 'cfer'
|
9
|
+
class_option :verbose, type: :boolean, default: false
|
10
|
+
class_option :profile, type: :string, aliases: :p, desc: 'The AWS profile to use from your credentials file'
|
11
|
+
class_option :region, type: :string, aliases: :r, desc: 'The AWS region to use'
|
12
|
+
class_option :pretty_print, type: :boolean, default: :true, desc: 'Render JSON in a more human-friendly format'
|
13
|
+
|
14
|
+
def self.template_options
|
15
|
+
|
16
|
+
method_option :parameters,
|
17
|
+
type: :hash,
|
18
|
+
desc: 'The CloudFormation parameters to pass to the stack',
|
19
|
+
default: {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.stack_options
|
23
|
+
method_option :output_format,
|
24
|
+
type: :string,
|
25
|
+
desc: 'The output format of the stack [table|json]',
|
26
|
+
default: 'table'
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'converge [OPTIONS] <stack-name>', 'Converges a cloudformation stack according to the template'
|
30
|
+
#method_option :git_lock,
|
31
|
+
# type: :boolean,
|
32
|
+
# default: true,
|
33
|
+
# desc: 'When enabled, Cfer will not converge a stack in a dirty git tree'
|
34
|
+
|
35
|
+
method_option :on_failure,
|
36
|
+
type: :string,
|
37
|
+
default: 'DELETE',
|
38
|
+
desc: 'The action to take if the stack creation fails'
|
39
|
+
method_option :follow,
|
40
|
+
aliases: :f,
|
41
|
+
type: :boolean,
|
42
|
+
default: true,
|
43
|
+
desc: 'Follow stack events on standard output while the changes are made.'
|
44
|
+
method_option :number,
|
45
|
+
type: :numeric,
|
46
|
+
default: 1,
|
47
|
+
desc: 'Prints the last (n) stack events.'
|
48
|
+
method_option :template,
|
49
|
+
aliases: :t,
|
50
|
+
type: :string,
|
51
|
+
desc: 'Override the stack filename (defaults to <stack-name>.rb)'
|
52
|
+
template_options
|
53
|
+
stack_options
|
54
|
+
def converge(stack_name)
|
55
|
+
Cfer.converge! stack_name, options
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'describe <stack>', 'Fetches and prints information about a CloudFormation'
|
59
|
+
stack_options
|
60
|
+
def describe(stack_name)
|
61
|
+
Cfer.describe! stack_name, options
|
62
|
+
end
|
63
|
+
|
64
|
+
desc 'tail <stack>', 'Follows stack events on standard output as they occur'
|
65
|
+
method_option :follow,
|
66
|
+
aliases: :f,
|
67
|
+
type: :boolean,
|
68
|
+
default: false,
|
69
|
+
desc: 'Follow stack events on standard output while the changes are made.'
|
70
|
+
method_option :number,
|
71
|
+
aliases: :n,
|
72
|
+
type: :numeric,
|
73
|
+
default: 10,
|
74
|
+
desc: 'Prints the last (n) stack events.'
|
75
|
+
stack_options
|
76
|
+
def tail(stack_name)
|
77
|
+
Cfer.tail! stack_name, options
|
78
|
+
end
|
79
|
+
|
80
|
+
desc 'generate [OPTIONS] <template.rb>', 'Generates a CloudFormation template by evaluating a Cfer template'
|
81
|
+
long_desc <<-LONGDESC
|
82
|
+
Generates a CloudFormation template by evaluating a Cfer template.
|
83
|
+
LONGDESC
|
84
|
+
template_options
|
85
|
+
def generate(tmpl)
|
86
|
+
Cfer.generate! tmpl, options
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.main(args)
|
90
|
+
Cfer::LOGGER.debug "Cfer version #{Cfer::VERSION}"
|
91
|
+
begin
|
92
|
+
Cli.start(args)
|
93
|
+
rescue Aws::Errors::NoSuchProfileError => e
|
94
|
+
Cfer::LOGGER.error "#{e.message}. Specify a valid profile with the --profile option."
|
95
|
+
exit 1
|
96
|
+
rescue Aws::Errors::MissingRegionError => e
|
97
|
+
Cfer::LOGGER.error "Missing region. Specify a valid AWS region with the --region option, or use the AWS_REGION environment variable."
|
98
|
+
exit 1
|
99
|
+
rescue Interrupt
|
100
|
+
Cfer::LOGGER.info 'Caught interrupt. Goodbye.'
|
101
|
+
rescue Cfer::Util::TemplateError => e
|
102
|
+
Cfer::LOGGER.fatal "Template error: #{e.message}"
|
103
|
+
Cfer::LOGGER.fatal Cfer::Cli.format_backtrace(e.template_backtrace) unless e.template_backtrace.empty?
|
104
|
+
exit 1
|
105
|
+
rescue Cfer::Util::CferError => e
|
106
|
+
Cfer::LOGGER.error "#{e.message}"
|
107
|
+
exit 1
|
108
|
+
rescue StandardError => e
|
109
|
+
Cfer::LOGGER.fatal "#{e.class.name}: #{e.message}"
|
110
|
+
Cfer::LOGGER.fatal Cfer::Cli.format_backtrace(e.backtrace) unless e.backtrace.empty?
|
111
|
+
|
112
|
+
if Cfer::DEBUG
|
113
|
+
Pry::rescued(e)
|
114
|
+
else
|
115
|
+
Cfer::Util.bug_report(e)
|
116
|
+
end
|
117
|
+
exit 1
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def cfn(opts = {})
|
124
|
+
@cfn ||= opts
|
125
|
+
end
|
126
|
+
private
|
127
|
+
def self.format_backtrace(bt)
|
128
|
+
"Backtrace: #{bt.join("\n from ")}"
|
129
|
+
end
|
130
|
+
def self.exit_on_failure?
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
end
|
138
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Cfer::Core
|
2
|
+
class Client
|
3
|
+
def converge
|
4
|
+
raise Cfer::Util::CferError, 'converge not implemented on this client'
|
5
|
+
end
|
6
|
+
|
7
|
+
def tail(options = {}, &block)
|
8
|
+
raise Cfer::Util::CferError, 'tail not implemented on this client'
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolve(param)
|
12
|
+
param
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/cfer/core/fn.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Cfer::Core::Fn
|
2
|
+
class << self
|
3
|
+
def join(sep, args)
|
4
|
+
{"Fn::Join" => [sep, args]}
|
5
|
+
end
|
6
|
+
|
7
|
+
def ref(r)
|
8
|
+
{"Ref" => r}
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_att(r, att)
|
12
|
+
{"Fn::GetAtt" => [r, att]}
|
13
|
+
end
|
14
|
+
|
15
|
+
def find_in_map(map_name, key1, key2)
|
16
|
+
{"Fn::FindInMap" => [map_name, key1, key2]}
|
17
|
+
end
|
18
|
+
|
19
|
+
def select(i, o)
|
20
|
+
{"Fn::Select" => [i, o]}
|
21
|
+
end
|
22
|
+
|
23
|
+
def base64(v)
|
24
|
+
{"Fn::Base64" => v}
|
25
|
+
end
|
26
|
+
|
27
|
+
def condition(cond)
|
28
|
+
{"Condition" => cond}
|
29
|
+
end
|
30
|
+
|
31
|
+
def and(conds)
|
32
|
+
{"Fn::And" => [conds]}
|
33
|
+
end
|
34
|
+
|
35
|
+
def equals(a, b)
|
36
|
+
{"Fn::Equals" => [a, b]}
|
37
|
+
end
|
38
|
+
|
39
|
+
def if(cond, t, f)
|
40
|
+
{"Fn::If" => [cond, t, f]}
|
41
|
+
end
|
42
|
+
|
43
|
+
def not(cond)
|
44
|
+
{"Fn::Not" => cond}
|
45
|
+
end
|
46
|
+
|
47
|
+
def or(conds)
|
48
|
+
{"Fn::Or" => conds}
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_azs(region)
|
52
|
+
{"Fn::GetAZs" => region}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Cfer::Cfn
|
2
|
+
class Resource < Cfer::Block
|
3
|
+
NON_PROXIED_METHODS = [:parameters, :options]
|
4
|
+
|
5
|
+
def initialize(name, type, **options, &block)
|
6
|
+
@name = name
|
7
|
+
|
8
|
+
self[:Type] = type
|
9
|
+
self.merge!(options)
|
10
|
+
self[:Properties] = HashWithIndifferentAccess.new
|
11
|
+
build_from_block(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def tag(k, v, **options)
|
15
|
+
self[:Properties][:Tags] ||= []
|
16
|
+
self[:Properties][:Tags].unshift({"Key" => k, "Value" => v}.merge(options))
|
17
|
+
end
|
18
|
+
|
19
|
+
def properties(**keyvals)
|
20
|
+
self[:Properties].merge!(keyvals)
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to?(method_sym)
|
24
|
+
!NON_PROXIED_METHODS.include?(method_sym)
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(method_sym, *arguments, &block)
|
28
|
+
key = camelize_property(method_sym)
|
29
|
+
case arguments.size
|
30
|
+
when 0
|
31
|
+
Cfer::Core::Fn::ref(method_sym)
|
32
|
+
when 1
|
33
|
+
properties key => arguments.first
|
34
|
+
else
|
35
|
+
properties key => arguments
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def camelize_property(sym)
|
41
|
+
sym.to_s.camelize.to_sym
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|