cf_deployer 1.2.8
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 +7 -0
- data/.gitignore +29 -0
- data/ChangeLog.md +16 -0
- data/DETAILS.md +268 -0
- data/FAQ.md +61 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +22 -0
- data/QUICKSTART.md +96 -0
- data/README.md +36 -0
- data/Rakefile +32 -0
- data/bin/cf_deploy +10 -0
- data/cf_deployer.gemspec +23 -0
- data/lib/cf_deployer/application.rb +74 -0
- data/lib/cf_deployer/application_error.rb +4 -0
- data/lib/cf_deployer/aws_constants.rb +3 -0
- data/lib/cf_deployer/cli.rb +111 -0
- data/lib/cf_deployer/component.rb +103 -0
- data/lib/cf_deployer/config_loader.rb +189 -0
- data/lib/cf_deployer/config_validation.rb +138 -0
- data/lib/cf_deployer/defaults.rb +10 -0
- data/lib/cf_deployer/deployment_strategy/auto_scaling_group_swap.rb +102 -0
- data/lib/cf_deployer/deployment_strategy/base.rb +88 -0
- data/lib/cf_deployer/deployment_strategy/blue_green.rb +70 -0
- data/lib/cf_deployer/deployment_strategy/cname_swap.rb +108 -0
- data/lib/cf_deployer/deployment_strategy/create_or_update.rb +57 -0
- data/lib/cf_deployer/driver/auto_scaling_group.rb +86 -0
- data/lib/cf_deployer/driver/cloud_formation_driver.rb +85 -0
- data/lib/cf_deployer/driver/dry_run.rb +27 -0
- data/lib/cf_deployer/driver/elb_driver.rb +17 -0
- data/lib/cf_deployer/driver/instance.rb +29 -0
- data/lib/cf_deployer/driver/route53_driver.rb +79 -0
- data/lib/cf_deployer/driver/verisign_driver.rb +21 -0
- data/lib/cf_deployer/hook.rb +32 -0
- data/lib/cf_deployer/logger.rb +34 -0
- data/lib/cf_deployer/stack.rb +154 -0
- data/lib/cf_deployer/status_presenter.rb +195 -0
- data/lib/cf_deployer/version.rb +3 -0
- data/lib/cf_deployer.rb +97 -0
- data/spec/fakes/instance.rb +32 -0
- data/spec/fakes/route53_client.rb +23 -0
- data/spec/fakes/stack.rb +65 -0
- data/spec/functional/deploy_spec.rb +73 -0
- data/spec/functional/kill_inactive_spec.rb +57 -0
- data/spec/functional_spec_helper.rb +3 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/unit/application_spec.rb +191 -0
- data/spec/unit/component_spec.rb +142 -0
- data/spec/unit/config_loader_spec.rb +356 -0
- data/spec/unit/config_validation_spec.rb +480 -0
- data/spec/unit/deployment_strategy/auto_scaling_group_swap_spec.rb +435 -0
- data/spec/unit/deployment_strategy/base_spec.rb +44 -0
- data/spec/unit/deployment_strategy/cname_swap_spec.rb +294 -0
- data/spec/unit/deployment_strategy/create_or_update_spec.rb +113 -0
- data/spec/unit/deployment_strategy/deployment_strategy_spec.rb +29 -0
- data/spec/unit/driver/auto_scaling_group_spec.rb +127 -0
- data/spec/unit/driver/cloud_formation_spec.rb +32 -0
- data/spec/unit/driver/elb_spec.rb +11 -0
- data/spec/unit/driver/instance_spec.rb +30 -0
- data/spec/unit/driver/route53_spec.rb +85 -0
- data/spec/unit/driver/verisign_spec.rb +18 -0
- data/spec/unit/hook_spec.rb +64 -0
- data/spec/unit/stack_spec.rb +150 -0
- data/spec/unit/status_presenter_spec.rb +108 -0
- metadata +197 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
module Driver
|
3
|
+
class Route53
|
4
|
+
def initialize(aws_route53 = nil)
|
5
|
+
@aws_route53 = aws_route53 || AWS::Route53.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def find_alias_target(target_zone_name, target_host_name)
|
9
|
+
target_zone = @aws_route53.hosted_zones.find { |z| z.name == trailing_dot(target_zone_name.downcase) }
|
10
|
+
raise ApplicationError.new('Target zone not found!') if target_zone.nil?
|
11
|
+
|
12
|
+
target_host = target_zone.resource_record_sets.find { |r| r.name == trailing_dot(target_host_name.downcase) }
|
13
|
+
return nil if target_host.nil? || target_host.alias_target.nil?
|
14
|
+
|
15
|
+
remove_trailing_dot(target_host.alias_target[:dns_name])
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_alias_target(target_zone_name, target_host_name, elb_hosted_zone_id, elb_dnsname)
|
19
|
+
Log.info "set alias target --Hosted Zone: #{target_zone_name} --Host Name: #{target_host_name} --ELB DNS Name: #{elb_dnsname} --ELB Zone ID: #{elb_hosted_zone_id}"
|
20
|
+
target_zone_name = trailing_dot(target_zone_name)
|
21
|
+
target_host_name = trailing_dot(target_host_name)
|
22
|
+
target_zone = @aws_route53.hosted_zones.find { |z| z.name == target_zone_name }
|
23
|
+
raise ApplicationError.new('Target zone not found!') if target_zone.nil?
|
24
|
+
|
25
|
+
change = {
|
26
|
+
action: "UPSERT",
|
27
|
+
resource_record_set: {
|
28
|
+
name: target_host_name,
|
29
|
+
type: "A",
|
30
|
+
alias_target: {
|
31
|
+
dns_name: elb_dnsname,
|
32
|
+
hosted_zone_id: elb_hosted_zone_id,
|
33
|
+
evaluate_target_health: false
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
batch = {
|
39
|
+
hosted_zone_id: target_zone.path,
|
40
|
+
change_batch: {
|
41
|
+
changes: [change]
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
CfDeployer::Driver::DryRun.guard "Skipping Route53 DNS update" do
|
46
|
+
change_resource_record_sets_with_retry(batch)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def change_resource_record_sets_with_retry(batch)
|
53
|
+
attempts = 0
|
54
|
+
while attempts < 20
|
55
|
+
begin
|
56
|
+
attempts = attempts + 1
|
57
|
+
@aws_route53.client.change_resource_record_sets(batch)
|
58
|
+
return
|
59
|
+
rescue Exception => e
|
60
|
+
Log.info "Failed to update alias target, trying again in 20 seconds."
|
61
|
+
sleep(20)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
raise ApplicationError.new('Failed to update Route53 alias target record!')
|
66
|
+
end
|
67
|
+
|
68
|
+
def trailing_dot(text)
|
69
|
+
return text if text[-1] == '.'
|
70
|
+
"#{text}."
|
71
|
+
end
|
72
|
+
|
73
|
+
def remove_trailing_dot(text)
|
74
|
+
return text[0..-2] if text && text[-1] == '.'
|
75
|
+
text
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
## To use this driver instead of Route53 (the default), use the setting 'dns-driver'
|
2
|
+
|
3
|
+
module CfDeployer
|
4
|
+
module Driver
|
5
|
+
class Verisign
|
6
|
+
|
7
|
+
def find_alias_target dns_zone, dns_fqdn
|
8
|
+
raise "Not Implemented"
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_alias_target dns_zone, dns_fqdn, elb_hosted_zone_id, elb_dnsname
|
12
|
+
raise "Not Implemented"
|
13
|
+
|
14
|
+
CfDeployer::Driver::DryRun.guard "Skipping Verisign DNS update" do
|
15
|
+
# do update here
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
class Hook
|
3
|
+
def initialize(name, body)
|
4
|
+
@name = name
|
5
|
+
@body = body
|
6
|
+
end
|
7
|
+
|
8
|
+
def run(context)
|
9
|
+
case @body
|
10
|
+
when Hash
|
11
|
+
if @body[:file]
|
12
|
+
file = File.expand_path(@body[:file], context[:config_dir])
|
13
|
+
execute(File.read(file), context, @body[:timeout])
|
14
|
+
else
|
15
|
+
execute(@body[:code], context, @body[:timeout])
|
16
|
+
end
|
17
|
+
when String
|
18
|
+
execute(@body, context)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def execute(hook, context, timeout = nil)
|
24
|
+
CfDeployer::Log.info("Running hook #{@name}")
|
25
|
+
context = context.dup
|
26
|
+
timeout = timeout || Defaults::Timeout
|
27
|
+
Timeout.timeout(timeout.to_f) do
|
28
|
+
eval(hook, binding)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
class Log
|
3
|
+
require 'log4r'
|
4
|
+
include Log4r
|
5
|
+
|
6
|
+
def self.debug(message)
|
7
|
+
log.debug message
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.info(message)
|
11
|
+
log.info message
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.log
|
15
|
+
return @log if @log
|
16
|
+
@log = Logger.new('cf_deployer')
|
17
|
+
outputter = Outputter.stdout
|
18
|
+
outputter.formatter = PatternFormatter.new(:pattern => "%d [%l] (%c) %M", :date_pattern => "%y-%m-%d %H:%M:%S")
|
19
|
+
@log.outputters = outputter
|
20
|
+
@log.level = Log4r::INFO
|
21
|
+
@log
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.level=(trace_level)
|
25
|
+
trace_level ||= 'info'
|
26
|
+
case trace_level.downcase
|
27
|
+
when 'debug'
|
28
|
+
log.level = Log4r::DEBUG
|
29
|
+
else
|
30
|
+
log.level = Log4r::INFO
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
class ResourceNotInReadyState < ApplicationError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Stack
|
6
|
+
SUCCESS_STATS = [:create_complete, :update_complete, :update_rollback_complete, :delete_complete]
|
7
|
+
READY_STATS = SUCCESS_STATS - [:delete_complete]
|
8
|
+
FAILED_STATS = [:create_failed, :update_failed, :delete_failed]
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(stack_name, component, context)
|
12
|
+
@stack_name = stack_name
|
13
|
+
@cf_driver = context[:cf_driver] || CfDeployer::Driver::CloudFormation.new(stack_name)
|
14
|
+
@context = context
|
15
|
+
@component = component
|
16
|
+
end
|
17
|
+
|
18
|
+
def deploy
|
19
|
+
config_dir = @context[:config_dir]
|
20
|
+
template = CfDeployer::ConfigLoader.component_json(@component, @context)
|
21
|
+
capabilities = @context[:capabilities] || []
|
22
|
+
tags = @context[:tags] || []
|
23
|
+
params = to_str(@context[:inputs].select{|key, value| @context[:defined_parameters].keys.include?(key)})
|
24
|
+
CfDeployer::Driver::DryRun.guard "Skipping deploy" do
|
25
|
+
exists? ? update_stack(template, params, capabilities, tags) : create_stack(template, params, capabilities, tags)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def outputs
|
30
|
+
return {} unless ready?
|
31
|
+
@cf_driver.outputs
|
32
|
+
end
|
33
|
+
|
34
|
+
def parameters
|
35
|
+
return {} unless ready?
|
36
|
+
@cf_driver.parameters
|
37
|
+
end
|
38
|
+
|
39
|
+
def output key
|
40
|
+
begin
|
41
|
+
@cf_driver.query_output(key) || (raise ApplicationError.new("'#{key}' is empty from stack #{name} output"))
|
42
|
+
rescue AWS::CloudFormation::Errors::ValidationError => e
|
43
|
+
raise ResourceNotInReadyState.new("Resource stack not in ready state yet, perhaps you should provision it first?")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete
|
48
|
+
if exists?
|
49
|
+
CfDeployer::Driver::DryRun.guard "Skipping delete" do
|
50
|
+
Log.info "deleting stack #{@stack_name}"
|
51
|
+
@cf_driver.delete_stack
|
52
|
+
wait_for_stack_to_delete
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def exists?
|
58
|
+
@cf_driver.stack_exists?
|
59
|
+
end
|
60
|
+
|
61
|
+
def ready?
|
62
|
+
READY_STATS.include? @cf_driver.stack_status
|
63
|
+
end
|
64
|
+
|
65
|
+
def status
|
66
|
+
if exists?
|
67
|
+
ready? ? :ready : :exists
|
68
|
+
else
|
69
|
+
:does_not_exist
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def resource_statuses
|
74
|
+
AWS.memoize do
|
75
|
+
resources = @cf_driver.resource_statuses.merge( { :asg_instances => {}, :instances => {} } )
|
76
|
+
if resources['AWS::AutoScaling::AutoScalingGroup']
|
77
|
+
resources['AWS::AutoScaling::AutoScalingGroup'].keys.each do |asg_name|
|
78
|
+
resources[:asg_instances][asg_name] = CfDeployer::Driver::AutoScalingGroup.new(asg_name).instance_statuses
|
79
|
+
end
|
80
|
+
end
|
81
|
+
if resources['AWS::EC2::Instance']
|
82
|
+
resources['AWS::EC2::Instance'].keys.each do |instance_id|
|
83
|
+
resources[:instances][instance_id] = CfDeployer::Driver::Instance.new(instance_id).status
|
84
|
+
end
|
85
|
+
end
|
86
|
+
resources
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def name
|
91
|
+
@stack_name
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def to_str(hash)
|
98
|
+
hash.each { |k,v| hash[k] = v.to_s }
|
99
|
+
end
|
100
|
+
|
101
|
+
def update_stack(template, params, capabilities, tags)
|
102
|
+
Log.info "Updating stack #{@stack_name}..."
|
103
|
+
@cf_driver.update_stack template,
|
104
|
+
:capabilities => capabilities,
|
105
|
+
:parameters => params
|
106
|
+
wait_for_stack_op_terminate
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_stack(template, params, capabilities, tags)
|
110
|
+
Log.info "Creating stack #{@stack_name}..."
|
111
|
+
@cf_driver.create_stack template,
|
112
|
+
:disable_rollback => true,
|
113
|
+
:capabilities => capabilities,
|
114
|
+
:tags => reformat_tags(tags),
|
115
|
+
:parameters => params
|
116
|
+
wait_for_stack_op_terminate
|
117
|
+
end
|
118
|
+
|
119
|
+
def stack_status
|
120
|
+
@cf_driver.stack_status
|
121
|
+
end
|
122
|
+
|
123
|
+
def wait_for_stack_op_terminate
|
124
|
+
stats = stack_status
|
125
|
+
while !SUCCESS_STATS.include?(stats)
|
126
|
+
sleep 15
|
127
|
+
stats = stack_status
|
128
|
+
raise ApplicationError.new("Resource stack update failed!") if FAILED_STATS.include? stats
|
129
|
+
Log.info "current status: #{stack_status}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def wait_for_stack_to_delete
|
134
|
+
Timeout::timeout(900){
|
135
|
+
while exists?
|
136
|
+
begin
|
137
|
+
Log.info "current status: #{stack_status}"
|
138
|
+
sleep 15
|
139
|
+
rescue AWS::CloudFormation::Errors::ValidationError => e
|
140
|
+
if e.message =~ /does not exist/
|
141
|
+
break # This is what we wanted anyways
|
142
|
+
else
|
143
|
+
raise e
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
def reformat_tags tags_hash
|
151
|
+
tags_hash.keys.map { |key| { 'Key' => key.to_s, 'Value' => tags_hash[key].to_s } }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
require 'rainbow/ext/string'
|
3
|
+
|
4
|
+
module CfDeployer
|
5
|
+
class StatusPresenter
|
6
|
+
|
7
|
+
VERBOSITY_3_SKIP = ['AWS::AutoScaling::AutoScalingGroup','AWS::EC2::Instance',:asg_instances, :instances]
|
8
|
+
|
9
|
+
PAD = " "
|
10
|
+
UNPADDED_TABLE_CELL_WIDTH = 85
|
11
|
+
|
12
|
+
def initialize status_info, verbosity
|
13
|
+
@verbosity = verbosity
|
14
|
+
@info = status_info
|
15
|
+
@output = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_json
|
19
|
+
filter_for_verbosity(@info).to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
def output
|
23
|
+
@output << table_seperator
|
24
|
+
@info.each do |component_name, stacks_hash|
|
25
|
+
@output << "\n#{centered(component_name.upcase)}\n"
|
26
|
+
@output << table_seperator
|
27
|
+
stack_cells = []
|
28
|
+
stacks_hash.each do |stack_name, stack_hash|
|
29
|
+
stack_output = ['']
|
30
|
+
|
31
|
+
stack_output << PAD + [ colorized_stack_name(stack_name, stack_hash),
|
32
|
+
stack_active_str(stack_hash[:active]).ljust(15),
|
33
|
+
stack_hash[:status].capitalize
|
34
|
+
].join(PAD)
|
35
|
+
|
36
|
+
if stack_hash[:resources] && @verbosity != 'stacks'
|
37
|
+
instances_status stack_output, component_name, stack_name, stack_hash[:resources][:instances], false
|
38
|
+
asgs_status stack_output, component_name, stack_name, stack_hash[:resources][:asg_instances]
|
39
|
+
resource_status stack_output, stack_hash[:resources] if @verbosity == 'all'
|
40
|
+
end
|
41
|
+
stack_output << ''
|
42
|
+
stack_cells << stack_output
|
43
|
+
end
|
44
|
+
stack_cells[1] ||= ['']
|
45
|
+
@output += tableize( stack_cells )
|
46
|
+
end
|
47
|
+
@output.join "\n"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def filter_for_verbosity info_hash
|
53
|
+
if @verbosity == 'stacks'
|
54
|
+
info_hash.each do |component, component_hash|
|
55
|
+
component_hash.each { |stack, stack_hash| stack_hash.delete :resources }
|
56
|
+
end
|
57
|
+
elsif @verbosity == 'instances'
|
58
|
+
info_hash.each do |component, component_hash|
|
59
|
+
component_hash.each do |stack, stack_hash|
|
60
|
+
if stack_hash[:resources]
|
61
|
+
stack_hash[:resources].select! do |resource_type, resources|
|
62
|
+
[:instances, :asg_instances].include? resource_type
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
info_hash
|
69
|
+
end
|
70
|
+
|
71
|
+
def colorized_stack_name stack_name, stack_hash
|
72
|
+
stack_color = case stack_name.split('').last
|
73
|
+
when 'B' then :cyan
|
74
|
+
when 'G' then :green
|
75
|
+
else :white
|
76
|
+
end
|
77
|
+
colorized_stack_name = " #{stack_name} ".color(stack_color).bright
|
78
|
+
stack_hash[:active] ? colorized_stack_name.inverse : colorized_stack_name
|
79
|
+
end
|
80
|
+
|
81
|
+
def stack_active_str active
|
82
|
+
case active
|
83
|
+
when true then 'Active'
|
84
|
+
when false then 'Inactive'
|
85
|
+
else ''
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def asgs_status output, component_name, stack_name, asg_hash
|
90
|
+
return if asg_hash.empty?
|
91
|
+
output << ''
|
92
|
+
output << "#{PAD * 2}AutoScalingGroups:"
|
93
|
+
asg_hash.each do |asg_name, asg_instances|
|
94
|
+
asg_color = status_color @info[component_name][stack_name][:resources]['AWS::AutoScaling::AutoScalingGroup'][asg_name]
|
95
|
+
output << ''
|
96
|
+
output << "#{PAD * 3}#{ Rainbow(asg_name).color asg_color }"
|
97
|
+
instances_status output, component_name, stack_name, asg_instances, true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def instances_status output, component_name, stack_name, instances_hash, in_asg
|
102
|
+
pad = PAD * 2
|
103
|
+
output << ''
|
104
|
+
output << "#{pad}Instances:" if (instances_hash.any? && !in_asg)
|
105
|
+
instances_hash.each do |instance_id, instance|
|
106
|
+
instance_pad = in_asg ? pad + PAD : pad
|
107
|
+
instance_color = instance_status_color instance[:status]
|
108
|
+
instance_line_parts = [ Rainbow(instance_id).color(instance_color),
|
109
|
+
instance[:public_ip_address],
|
110
|
+
instance[:private_ip_address],
|
111
|
+
instance[:image_id],
|
112
|
+
instance[:key_pair]
|
113
|
+
]
|
114
|
+
output << "#{PAD}#{instance_pad}" + instance_line_parts.join(PAD)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def resource_status output, resource_hash
|
119
|
+
resources_to_report = resource_hash.reject { |resource_type| VERBOSITY_3_SKIP.include? resource_type }
|
120
|
+
max_length = resources_to_report.map { |rtype, r| r.keys }.flatten.group_by(&:size).max.last.first.size
|
121
|
+
new_max = [ max_length, (UNPADDED_TABLE_CELL_WIDTH - 17 - (PAD.size * 4))].sort.first
|
122
|
+
|
123
|
+
resources_to_report.each do |resource_type, resources|
|
124
|
+
output << ''
|
125
|
+
output << "#{PAD * 2}#{resource_type.split('::').last}"
|
126
|
+
resources.each do |resource_id, resource_status|
|
127
|
+
truncated_id = middle_truncate_ljust(resource_id, new_max).color(status_color(resource_status))
|
128
|
+
output << "#{PAD * 3}#{truncated_id}#{PAD}#{resource_status}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def instance_status_color status
|
134
|
+
CfDeployer::Driver::Instance::GOOD_STATUSES.include?(status) ? :green : :red
|
135
|
+
end
|
136
|
+
|
137
|
+
def status_color status
|
138
|
+
status = status.downcase.to_sym
|
139
|
+
if CfDeployer::Stack::READY_STATS.include? status
|
140
|
+
:green
|
141
|
+
elsif CfDeployer::Stack::FAILED_STATS.include? status
|
142
|
+
:red
|
143
|
+
else
|
144
|
+
:white
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def tableize stack_cells
|
149
|
+
my_output = []
|
150
|
+
|
151
|
+
(col1, col2) = stack_cells
|
152
|
+
rows = stack_cells.map(&:size).max
|
153
|
+
|
154
|
+
rows.times do |i|
|
155
|
+
col1[i] ||= ''
|
156
|
+
col2[i] ||= ''
|
157
|
+
|
158
|
+
line = ''
|
159
|
+
line << col1[i].ljust(UNPADDED_TABLE_CELL_WIDTH + PAD.size + invisible_length(col1[i]))
|
160
|
+
line << '|'
|
161
|
+
line << col2[i]
|
162
|
+
my_output << line
|
163
|
+
end
|
164
|
+
my_output << table_seperator
|
165
|
+
my_output
|
166
|
+
end
|
167
|
+
|
168
|
+
def middle_truncate_ljust str, len
|
169
|
+
return str.ljust(len) if str.size <= len
|
170
|
+
|
171
|
+
replace_start = (len / 2).to_i - 4
|
172
|
+
replace_end = str.size - (len / 2).to_i
|
173
|
+
truncated = str[0..replace_start] + '...' + str[replace_end..str.size]
|
174
|
+
truncated.ljust len
|
175
|
+
end
|
176
|
+
|
177
|
+
def invisible_length str
|
178
|
+
str.size - visible_length(str)
|
179
|
+
end
|
180
|
+
|
181
|
+
def visible_length str
|
182
|
+
str.gsub(/\e\[[\d;]+m/,'').size
|
183
|
+
end
|
184
|
+
|
185
|
+
def table_seperator
|
186
|
+
"-" * (UNPADDED_TABLE_CELL_WIDTH + PAD.size) * 2
|
187
|
+
end
|
188
|
+
|
189
|
+
def centered the_string
|
190
|
+
width = (UNPADDED_TABLE_CELL_WIDTH + PAD.size) + (the_string.size / 2).to_i
|
191
|
+
the_string.rjust width
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
end
|
data/lib/cf_deployer.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'set'
|
3
|
+
require 'time'
|
4
|
+
require 'json'
|
5
|
+
require 'timeout'
|
6
|
+
require 'aws-sdk'
|
7
|
+
require 'erb'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'log4r'
|
10
|
+
require 'pp'
|
11
|
+
require 'forwardable'
|
12
|
+
|
13
|
+
require_relative 'cf_deployer/application_error'
|
14
|
+
require_relative 'cf_deployer/cli'
|
15
|
+
require_relative 'cf_deployer/application'
|
16
|
+
require_relative 'cf_deployer/aws_constants'
|
17
|
+
require_relative 'cf_deployer/component'
|
18
|
+
require_relative 'cf_deployer/config_loader'
|
19
|
+
require_relative 'cf_deployer/config_validation'
|
20
|
+
require_relative 'cf_deployer/stack'
|
21
|
+
require_relative 'cf_deployer/version'
|
22
|
+
require_relative 'cf_deployer/status_presenter'
|
23
|
+
require_relative 'cf_deployer/deployment_strategy/base'
|
24
|
+
require_relative 'cf_deployer/deployment_strategy/blue_green'
|
25
|
+
require_relative 'cf_deployer/deployment_strategy/auto_scaling_group_swap'
|
26
|
+
require_relative 'cf_deployer/deployment_strategy/cname_swap'
|
27
|
+
require_relative 'cf_deployer/deployment_strategy/create_or_update'
|
28
|
+
require_relative 'cf_deployer/driver/auto_scaling_group'
|
29
|
+
require_relative 'cf_deployer/driver/cloud_formation_driver'
|
30
|
+
require_relative 'cf_deployer/driver/dry_run'
|
31
|
+
require_relative 'cf_deployer/driver/elb_driver'
|
32
|
+
require_relative 'cf_deployer/driver/instance'
|
33
|
+
require_relative 'cf_deployer/driver/route53_driver'
|
34
|
+
require_relative 'cf_deployer/driver/verisign_driver'
|
35
|
+
require_relative 'cf_deployer/logger'
|
36
|
+
require_relative 'cf_deployer/hook'
|
37
|
+
require_relative 'cf_deployer/defaults'
|
38
|
+
|
39
|
+
module CfDeployer
|
40
|
+
|
41
|
+
AWS.config(:max_retries => 5)
|
42
|
+
|
43
|
+
def self.config opts
|
44
|
+
config = self.parseconfig opts, false
|
45
|
+
config[:components].each do |component, c_hash|
|
46
|
+
c_hash.delete :defined_parameters
|
47
|
+
end
|
48
|
+
puts config.select { |k,v| [:components, :environments, :environment, :application, :'config-file'].include? k.to_sym }.to_yaml
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.deploy opts
|
52
|
+
config = self.parseconfig opts
|
53
|
+
# AWS.config(:logger => Logger.new($stdout))
|
54
|
+
Application.new(config).deploy
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.destroy opts
|
58
|
+
config = self.parseconfig opts, false
|
59
|
+
# AWS.config(:logger => Logger.new($stdout))
|
60
|
+
Application.new(config).destroy
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.json opts
|
64
|
+
config = self.parseconfig opts, false
|
65
|
+
Application.new(config).json
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.status opts
|
69
|
+
config = self.parseconfig opts, false
|
70
|
+
status_info = Application.new(config).status opts[:component].first, opts[:verbosity]
|
71
|
+
presenter = CfDeployer::StatusPresenter.new status_info, opts[:verbosity]
|
72
|
+
|
73
|
+
puts opts[:'output-format'] == 'json' ? presenter.to_json : presenter.output
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.switch opts
|
77
|
+
config = self.parseconfig opts, false
|
78
|
+
Application.new(config).switch
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.kill_inactive opts
|
82
|
+
config = self.parseconfig opts, false
|
83
|
+
Application.new(config).kill_inactive
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def self.parseconfig options, validate_inputs = true
|
89
|
+
AWS.config(:region => options[:region]) if options[:region]
|
90
|
+
split_settings = options.dup
|
91
|
+
split_settings.merge!({:cli_overrides => {:settings => split_settings.delete(:settings), :inputs => split_settings.delete(:inputs)} })
|
92
|
+
config = ConfigLoader.new.load split_settings
|
93
|
+
ConfigValidation.new.validate config, validate_inputs
|
94
|
+
config
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Fakes
|
2
|
+
class Instance
|
3
|
+
|
4
|
+
SIMPLE_ATTRIBS = [ :id, :status, :public_ip_address, :private_ip_address, :image_id ]
|
5
|
+
|
6
|
+
attr_reader *SIMPLE_ATTRIBS
|
7
|
+
attr_reader :key_pair
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
defaults = {
|
11
|
+
:id => 'i-abcd1234',
|
12
|
+
:status => :running,
|
13
|
+
:public_ip_address => '1.2.3.4',
|
14
|
+
:private_ip_address => '192.168.100.200',
|
15
|
+
:image_id => 'ami-abcd1234',
|
16
|
+
:key_pair => 'awesome_users'
|
17
|
+
}
|
18
|
+
|
19
|
+
SIMPLE_ATTRIBS.each do |attrib|
|
20
|
+
instance_variable_set "@#{attrib}", (options[attrib] || defaults[attrib])
|
21
|
+
end
|
22
|
+
|
23
|
+
@key_pair = ::AWS::EC2::KeyPair.new (options[:key_pair] || defaults[:key_pair] )
|
24
|
+
end
|
25
|
+
|
26
|
+
def inspect
|
27
|
+
"#{self.class}<#{@name}>"
|
28
|
+
end
|
29
|
+
alias_method :to_s, :inspect
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Fakes
|
2
|
+
class AWSRoute53
|
3
|
+
attr_reader :fail_counter, :hosted_zones, :client
|
4
|
+
def initialize(opts = {})
|
5
|
+
@client = AWSRoute53Client.new(opts)
|
6
|
+
@hosted_zones = opts[:hosted_zones]
|
7
|
+
@fail_counter = 0
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class AWSRoute53Client
|
12
|
+
attr_reader :fail_counter
|
13
|
+
def initialize(opts = {})
|
14
|
+
@times_to_fail = opts[:times_to_fail]
|
15
|
+
@fail_counter = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def change_resource_record_sets(*args)
|
19
|
+
@fail_counter = @fail_counter + 1
|
20
|
+
raise 'Error' if @fail_counter <= @times_to_fail
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|