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