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.
@@ -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