cfer 0.1.1

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