broadside 2.0.0 → 3.0.0.pre.prerelease
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitmodules +0 -3
- data/.hound.yml +2 -0
- data/.rubocop.yml +1080 -0
- data/CHANGELOG.md +41 -3
- data/README.md +54 -66
- data/bin/broadside +2 -174
- data/broadside.gemspec +6 -6
- data/lib/broadside.rb +32 -22
- data/lib/broadside/command.rb +130 -0
- data/lib/broadside/configuration/aws_configuration.rb +25 -0
- data/lib/broadside/configuration/configuration.rb +74 -0
- data/lib/broadside/configuration/invalid_configuration.rb +9 -0
- data/lib/broadside/deploy.rb +7 -99
- data/lib/broadside/ecs/ecs_deploy.rb +114 -179
- data/lib/broadside/ecs/ecs_manager.rb +68 -24
- data/lib/broadside/error.rb +3 -8
- data/lib/broadside/gli/commands.rb +147 -0
- data/lib/broadside/gli/global.rb +72 -0
- data/lib/broadside/logging_utils.rb +9 -0
- data/lib/broadside/target.rb +59 -73
- data/lib/broadside/version.rb +1 -1
- metadata +43 -28
- data/lib/broadside/configuration.rb +0 -58
- data/lib/broadside/configuration/aws_config.rb +0 -14
- data/lib/broadside/configuration/ecs_config.rb +0 -13
- data/lib/broadside/configuration/verify_instance_variables.rb +0 -11
- data/lib/broadside/predeploy_commands.rb +0 -7
- data/lib/broadside/utils.rb +0 -27
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'tty-table'
|
4
|
+
|
5
|
+
module Broadside
|
6
|
+
module Command
|
7
|
+
extend LoggingUtils
|
8
|
+
|
9
|
+
DEFAULT_TAIL_LINES = 10
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def targets
|
13
|
+
table_header = nil
|
14
|
+
table_rows = []
|
15
|
+
|
16
|
+
Broadside.config.targets.each do |_, target|
|
17
|
+
task_definition = EcsManager.get_latest_task_definition(target.family)
|
18
|
+
service_tasks_running = EcsManager.get_task_arns(
|
19
|
+
target.cluster,
|
20
|
+
target.family,
|
21
|
+
service_name: target.family,
|
22
|
+
desired_status: 'RUNNING'
|
23
|
+
).size
|
24
|
+
|
25
|
+
if task_definition.nil?
|
26
|
+
warn "Skipping deploy target '#{target.name}' as it does not have a configured task_definition."
|
27
|
+
next
|
28
|
+
end
|
29
|
+
|
30
|
+
container_definitions = task_definition[:container_definitions].select { |c| c[:name] == target.family }
|
31
|
+
warn "Only displaying 1/#{container_definitions.size} containers" if container_definitions.size > 1
|
32
|
+
container_definition = container_definitions.first
|
33
|
+
|
34
|
+
row_data = target.to_h.merge(
|
35
|
+
Image: container_definition[:image],
|
36
|
+
CPU: container_definition[:cpu],
|
37
|
+
Memory: container_definition[:memory],
|
38
|
+
Revision: task_definition[:revision],
|
39
|
+
Tasks: "#{service_tasks_running}/#{target.scale}"
|
40
|
+
)
|
41
|
+
|
42
|
+
table_header ||= row_data.keys.map(&:to_s)
|
43
|
+
table_rows << row_data.values
|
44
|
+
end
|
45
|
+
|
46
|
+
table = TTY::Table.new(header: table_header, rows: table_rows)
|
47
|
+
puts table.render(:ascii, padding: [0, 1])
|
48
|
+
end
|
49
|
+
|
50
|
+
def status(options)
|
51
|
+
target = Broadside.config.get_target_by_name!(options[:target])
|
52
|
+
cluster = target.cluster
|
53
|
+
family = target.family
|
54
|
+
pastel = Pastel.new
|
55
|
+
debug "Getting status information about #{family}"
|
56
|
+
|
57
|
+
output = [
|
58
|
+
pastel.underline('Current task definition information:'),
|
59
|
+
pastel.blue(PP.pp(EcsManager.get_latest_task_definition(family), ''))
|
60
|
+
]
|
61
|
+
|
62
|
+
if options[:verbose]
|
63
|
+
output << [
|
64
|
+
pastel.underline('Current service information:'),
|
65
|
+
pastel.bright_blue(PP.pp(EcsManager.ecs.describe_services(cluster: cluster, services: [family]), ''))
|
66
|
+
]
|
67
|
+
end
|
68
|
+
|
69
|
+
task_arns = EcsManager.get_task_arns(cluster, family)
|
70
|
+
if task_arns.empty?
|
71
|
+
output << ["No running tasks found.\n"]
|
72
|
+
else
|
73
|
+
ips = EcsManager.get_running_instance_ips(cluster, family)
|
74
|
+
|
75
|
+
if options[:verbose]
|
76
|
+
output << [
|
77
|
+
pastel.underline('Task information:'),
|
78
|
+
pastel.bright_cyan(PP.pp(EcsManager.ecs.describe_tasks(cluster: cluster, tasks: task_arns), ''))
|
79
|
+
]
|
80
|
+
end
|
81
|
+
|
82
|
+
output << [
|
83
|
+
pastel.underline('Private IPs of instances running tasks:'),
|
84
|
+
pastel.cyan(ips.map { |ip| "#{ip}: #{Broadside.config.ssh_cmd(ip)}" }.join("\n")) + "\n"
|
85
|
+
]
|
86
|
+
end
|
87
|
+
|
88
|
+
puts output.join("\n")
|
89
|
+
end
|
90
|
+
|
91
|
+
def logtail(options)
|
92
|
+
lines = options[:lines] || DEFAULT_TAIL_LINES
|
93
|
+
target = Broadside.config.get_target_by_name!(options[:target])
|
94
|
+
ip = get_running_instance_ip!(target, *options[:instance])
|
95
|
+
info "Tailing logs for running container at #{ip}..."
|
96
|
+
|
97
|
+
cmd = "docker logs -f --tail=#{lines} `#{docker_ps_cmd(target.family)}`"
|
98
|
+
exec(Broadside.config.ssh_cmd(ip) + " '#{cmd}'")
|
99
|
+
end
|
100
|
+
|
101
|
+
def ssh(options)
|
102
|
+
target = Broadside.config.get_target_by_name!(options[:target])
|
103
|
+
ip = get_running_instance_ip!(target, *options[:instance])
|
104
|
+
info "Establishing SSH connection to #{ip}..."
|
105
|
+
|
106
|
+
exec(Broadside.config.ssh_cmd(ip))
|
107
|
+
end
|
108
|
+
|
109
|
+
def bash(options)
|
110
|
+
target = Broadside.config.get_target_by_name!(options[:target])
|
111
|
+
ip = get_running_instance_ip!(target, *options[:instance])
|
112
|
+
info "Running bash for running container at #{ip}..."
|
113
|
+
|
114
|
+
cmd = "docker exec -i -t `#{docker_ps_cmd(target.family)}` bash"
|
115
|
+
exec(Broadside.config.ssh_cmd(ip, tty: true) + " '#{cmd}'")
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def get_running_instance_ip!(target, instance_index = 0)
|
121
|
+
EcsManager.check_service_and_task_definition_state!(target)
|
122
|
+
EcsManager.get_running_instance_ips!(target.cluster, target.family).fetch(instance_index)
|
123
|
+
end
|
124
|
+
|
125
|
+
def docker_ps_cmd(family)
|
126
|
+
"docker ps -n 1 --quiet --filter name=#{Shellwords.shellescape(family)}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Broadside
|
2
|
+
class AwsConfiguration
|
3
|
+
include ActiveModel::Model
|
4
|
+
include InvalidConfiguration
|
5
|
+
|
6
|
+
validates :region, presence: true, strict: ConfigurationError
|
7
|
+
validates :ecs_poll_frequency, numericality: { only_integer: true, strict: ConfigurationError }
|
8
|
+
validates_each(:credentials) do |_, _, val|
|
9
|
+
raise ConfigurationError, 'credentials is not of type Aws::Credentials' unless val.is_a?(Aws::Credentials)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor(
|
13
|
+
:credentials,
|
14
|
+
:ecs_default_cluster,
|
15
|
+
:ecs_poll_frequency,
|
16
|
+
:region
|
17
|
+
)
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@credentials = Aws::SharedCredentials.new.credentials
|
21
|
+
@ecs_poll_frequency = 2
|
22
|
+
@region = 'us-east-1'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Broadside
|
4
|
+
class Configuration
|
5
|
+
include ActiveModel::Model
|
6
|
+
include LoggingUtils
|
7
|
+
include InvalidConfiguration
|
8
|
+
|
9
|
+
attr_reader(
|
10
|
+
:aws,
|
11
|
+
:targets
|
12
|
+
)
|
13
|
+
attr_accessor(
|
14
|
+
:application,
|
15
|
+
:config_file,
|
16
|
+
:default_docker_image,
|
17
|
+
:logger,
|
18
|
+
:prehook,
|
19
|
+
:posthook,
|
20
|
+
:ssh,
|
21
|
+
:timeout
|
22
|
+
)
|
23
|
+
|
24
|
+
validates :application, :targets, :logger, presence: true
|
25
|
+
validates_each(:aws) { |_, _, val| val.validate }
|
26
|
+
|
27
|
+
validates_each(:ssh) do |record, attr, val|
|
28
|
+
record.errors.add(attr, 'is not a hash') unless val.is_a?(Hash)
|
29
|
+
|
30
|
+
if (proxy = val[:proxy])
|
31
|
+
record.errors.add(attr, 'bad proxy config') unless proxy[:host] && proxy[:port] && proxy[:port].is_a?(Integer)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@aws = AwsConfiguration.new
|
37
|
+
@logger = ::Logger.new(STDOUT)
|
38
|
+
@logger.level = ::Logger::INFO
|
39
|
+
@logger.datetime_format = '%Y-%m-%d_%H:%M:%S'
|
40
|
+
@ssh = {}
|
41
|
+
@timeout = 600
|
42
|
+
end
|
43
|
+
|
44
|
+
# Transform deploy target configs to Target objects
|
45
|
+
def targets=(targets_hash)
|
46
|
+
raise ConfigurationError, ':targets must be a hash' unless targets_hash.is_a?(Hash)
|
47
|
+
|
48
|
+
@targets = targets_hash.inject({}) do |h, (target_name, config)|
|
49
|
+
h.merge(target_name => Target.new(target_name, config))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_target_by_name!(name)
|
54
|
+
@targets.fetch(name) { raise ArgumentError, "Deploy target '#{name}' does not exist!" }
|
55
|
+
end
|
56
|
+
|
57
|
+
def ssh_cmd(ip, options = {})
|
58
|
+
cmd = 'ssh -o StrictHostKeyChecking=no'
|
59
|
+
cmd << ' -t -t' if options[:tty]
|
60
|
+
cmd << " -i #{@ssh[:keyfile]}" if @ssh[:keyfile]
|
61
|
+
if (proxy = @ssh[:proxy])
|
62
|
+
cmd << ' -o ProxyCommand="ssh -q'
|
63
|
+
cmd << " -i #{proxy[:keyfile]}" if proxy[:keyfile]
|
64
|
+
cmd << ' '
|
65
|
+
cmd << "#{proxy[:user]}@" if proxy[:user]
|
66
|
+
cmd << "#{proxy[:host]} nc #{ip} #{proxy[:port]}\""
|
67
|
+
end
|
68
|
+
cmd << ' '
|
69
|
+
cmd << "#{@ssh[:user]}@" if @ssh[:user]
|
70
|
+
cmd << ip.to_s
|
71
|
+
cmd
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/broadside/deploy.rb
CHANGED
@@ -1,111 +1,19 @@
|
|
1
1
|
module Broadside
|
2
2
|
class Deploy
|
3
|
-
include
|
4
|
-
include VerifyInstanceVariables
|
3
|
+
include LoggingUtils
|
5
4
|
|
6
|
-
attr_reader
|
7
|
-
:command,
|
8
|
-
:instance,
|
9
|
-
:lines,
|
10
|
-
:tag,
|
11
|
-
:target
|
12
|
-
)
|
5
|
+
attr_reader :target
|
13
6
|
|
14
|
-
def initialize(
|
15
|
-
@target
|
16
|
-
@
|
17
|
-
@instance = opts[:instance] || @target.instance
|
18
|
-
@lines = opts[:lines] || 10
|
19
|
-
@rollback = opts[:rollback] || 1
|
20
|
-
@scale = opts[:scale] || @target.scale
|
21
|
-
@tag = opts[:tag]
|
22
|
-
end
|
23
|
-
|
24
|
-
def short
|
25
|
-
deploy
|
26
|
-
end
|
27
|
-
|
28
|
-
def full
|
29
|
-
config.verify(:ssh)
|
30
|
-
verify(:tag)
|
31
|
-
|
32
|
-
info "Running predeploy commands for #{family}..."
|
33
|
-
run_commands(@target.predeploy_commands)
|
34
|
-
info 'Predeploy complete.'
|
35
|
-
|
36
|
-
deploy
|
37
|
-
end
|
38
|
-
|
39
|
-
def deploy
|
40
|
-
verify(:tag)
|
41
|
-
|
42
|
-
info "Deploying #{image_tag} to #{family}..."
|
43
|
-
yield
|
44
|
-
info 'Deployment complete.'
|
45
|
-
end
|
46
|
-
|
47
|
-
def rollback(count = @rollback)
|
48
|
-
info "Rolling back #{count} release for #{family}..."
|
49
|
-
yield
|
50
|
-
info 'Rollback complete.'
|
51
|
-
end
|
52
|
-
|
53
|
-
def scale
|
54
|
-
info "Rescaling #{family} with scale=#{@scale}"
|
55
|
-
yield
|
56
|
-
info 'Rescaling complete.'
|
57
|
-
end
|
58
|
-
|
59
|
-
def run
|
60
|
-
config.verify(:ssh)
|
61
|
-
verify(:tag, :command)
|
62
|
-
info "Running command [#{@command}] for #{family}..."
|
63
|
-
yield
|
64
|
-
info 'Complete.'
|
65
|
-
end
|
66
|
-
|
67
|
-
def status
|
68
|
-
info "Getting status information about #{family}"
|
69
|
-
yield
|
70
|
-
info 'Complete.'
|
71
|
-
end
|
72
|
-
|
73
|
-
def logtail
|
74
|
-
verify(:instance)
|
75
|
-
yield
|
76
|
-
end
|
77
|
-
|
78
|
-
def ssh
|
79
|
-
verify(:instance)
|
80
|
-
yield
|
81
|
-
end
|
82
|
-
|
83
|
-
def bash
|
84
|
-
verify(:instance)
|
85
|
-
yield
|
7
|
+
def initialize(options = {})
|
8
|
+
@target = Broadside.config.get_target_by_name!(options[:target])
|
9
|
+
@tag = options[:tag] || @target.tag
|
86
10
|
end
|
87
11
|
|
88
12
|
private
|
89
13
|
|
90
|
-
def family
|
91
|
-
"#{config.application}_#{@target.name}"
|
92
|
-
end
|
93
|
-
|
94
14
|
def image_tag
|
95
|
-
raise ArgumentError,
|
96
|
-
"#{
|
97
|
-
end
|
98
|
-
|
99
|
-
def gen_ssh_cmd(ip, options = { tty: false })
|
100
|
-
opts = config.ssh || {}
|
101
|
-
cmd = 'ssh -o StrictHostKeyChecking=no'
|
102
|
-
cmd << ' -t -t' if options[:tty]
|
103
|
-
cmd << " -i #{opts[:keyfile]}" if opts[:keyfile]
|
104
|
-
if opts[:proxy]
|
105
|
-
cmd << " -o ProxyCommand=\"ssh #{opts[:proxy][:host]} nc #{ip} #{opts[:proxy][:port]}\""
|
106
|
-
end
|
107
|
-
cmd << " #{opts[:user]}@#{ip}"
|
108
|
-
cmd
|
15
|
+
raise ArgumentError, 'Missing tag!' unless @tag
|
16
|
+
"#{@target.docker_image}:#{@tag}"
|
109
17
|
end
|
110
18
|
end
|
111
19
|
end
|
@@ -1,156 +1,127 @@
|
|
1
|
-
require 'aws-sdk'
|
2
1
|
require 'open3'
|
3
|
-
require 'pp'
|
4
|
-
require 'rainbow'
|
5
|
-
require 'shellwords'
|
6
2
|
|
7
3
|
module Broadside
|
8
4
|
class EcsDeploy < Deploy
|
5
|
+
delegate :cluster, to: :target
|
6
|
+
delegate :family, to: :target
|
7
|
+
|
9
8
|
DEFAULT_CONTAINER_DEFINITION = {
|
10
9
|
cpu: 1,
|
11
10
|
essential: true,
|
12
|
-
memory:
|
11
|
+
memory: 1024
|
13
12
|
}
|
14
13
|
|
15
|
-
def
|
16
|
-
|
17
|
-
config.ecs.verify(:cluster, :poll_frequency)
|
14
|
+
def short
|
15
|
+
deploy
|
18
16
|
end
|
19
17
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
end
|
25
|
-
unless EcsManager.get_latest_task_definition_arn(family)
|
26
|
-
exception "No task definition for '#{family}'! Please bootstrap or manually configure the task definition."
|
27
|
-
end
|
18
|
+
def full
|
19
|
+
info "Running predeploy commands for #{family}..."
|
20
|
+
run_commands(@target.predeploy_commands, started_by: 'predeploy')
|
21
|
+
info 'Predeploy complete.'
|
28
22
|
|
29
|
-
|
30
|
-
|
31
|
-
begin
|
32
|
-
update_service
|
33
|
-
rescue SignalException::Interrupt
|
34
|
-
error 'Caught interrupt signal, rolling back...'
|
35
|
-
rollback(1)
|
36
|
-
error 'Deployment did not finish successfully.'
|
37
|
-
raise
|
38
|
-
rescue StandardError => e
|
39
|
-
error e.inspect, "\n", e.backtrace.join("\n")
|
40
|
-
error 'Deploy failed! Rolling back...'
|
41
|
-
rollback(1)
|
42
|
-
error 'Deployment did not finish successfully.'
|
43
|
-
raise e
|
44
|
-
end
|
45
|
-
end
|
23
|
+
deploy
|
46
24
|
end
|
47
25
|
|
48
26
|
def bootstrap
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
27
|
+
if EcsManager.get_latest_task_definition_arn(family)
|
28
|
+
info "Task definition for #{family} already exists."
|
29
|
+
else
|
30
|
+
raise ConfigurationError, "No :task_definition_config for #{family}" unless @target.task_definition_config
|
54
31
|
info "Creating an initial task definition for '#{family}' from the config..."
|
55
32
|
|
56
33
|
EcsManager.ecs.register_task_definition(
|
57
34
|
@target.task_definition_config.merge(
|
58
35
|
family: family,
|
59
|
-
container_definitions: [DEFAULT_CONTAINER_DEFINITION.merge(
|
36
|
+
container_definitions: [DEFAULT_CONTAINER_DEFINITION.merge(configured_container_definition)]
|
60
37
|
)
|
61
38
|
)
|
62
39
|
end
|
63
40
|
|
64
|
-
run_commands(@target.bootstrap_commands)
|
41
|
+
run_commands(@target.bootstrap_commands, started_by: 'bootstrap')
|
65
42
|
|
66
|
-
if EcsManager.service_exists?(
|
43
|
+
if EcsManager.service_exists?(cluster, family)
|
67
44
|
info("Service for #{family} already exists.")
|
68
45
|
else
|
69
|
-
unless @target.service_config
|
70
|
-
raise ArgumentError, "Service doesn't exist and no :service_config in '#{family}' configuration"
|
71
|
-
end
|
72
|
-
|
46
|
+
raise ConfigurationError, "No :service_config for #{family}" unless @target.service_config
|
73
47
|
info "Service '#{family}' doesn't exist, creating..."
|
74
|
-
EcsManager.create_service(
|
48
|
+
EcsManager.create_service(cluster, family, @target.service_config)
|
75
49
|
end
|
76
50
|
end
|
77
51
|
|
78
|
-
def rollback(
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
update_service
|
83
|
-
rescue StandardError
|
84
|
-
error 'Rollback failed to complete!'
|
85
|
-
raise
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
52
|
+
def rollback(options = {})
|
53
|
+
count = options[:rollback] || 1
|
54
|
+
info "Rolling back #{count} release(s) for #{family}..."
|
55
|
+
EcsManager.check_service_and_task_definition_state!(@target)
|
89
56
|
|
90
|
-
|
91
|
-
|
92
|
-
update_service
|
57
|
+
begin
|
58
|
+
EcsManager.deregister_last_n_tasks_definitions(family, count)
|
59
|
+
update_service(options)
|
60
|
+
rescue StandardError
|
61
|
+
error 'Rollback failed to complete!'
|
62
|
+
raise
|
93
63
|
end
|
94
|
-
end
|
95
64
|
|
96
|
-
|
97
|
-
super do
|
98
|
-
run_commands(@command)
|
99
|
-
end
|
65
|
+
info 'Rollback complete.'
|
100
66
|
end
|
101
67
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
"\nDeployed task definition information:\n",
|
107
|
-
Rainbow(PP.pp(EcsManager.get_latest_task_definition(family), '')).blue,
|
108
|
-
"\nPrivate ips of instances running containers:\n",
|
109
|
-
Rainbow(ips.join(' ')).blue,
|
110
|
-
"\n\nssh command:\n#{Rainbow(gen_ssh_cmd(ips.first)).cyan}",
|
111
|
-
"\n---------------\n"
|
112
|
-
end
|
68
|
+
def scale(options = {})
|
69
|
+
info "Rescaling #{family} with scale=#{@scale}..."
|
70
|
+
update_service(options)
|
71
|
+
info 'Rescaling complete.'
|
113
72
|
end
|
114
73
|
|
115
|
-
def
|
116
|
-
|
117
|
-
|
118
|
-
debug "Tailing logs for running container at ip #{ip}..."
|
119
|
-
search_pattern = Shellwords.shellescape(family)
|
120
|
-
cmd = "docker logs -f --tail=#{@lines} `docker ps -n 1 --quiet --filter name=#{search_pattern}`"
|
121
|
-
tail_cmd = gen_ssh_cmd(ip) + " '#{cmd}'"
|
122
|
-
exec tail_cmd
|
123
|
-
end
|
124
|
-
end
|
74
|
+
def run_commands(commands, options = {})
|
75
|
+
return if commands.nil? || commands.empty?
|
76
|
+
update_task_revision
|
125
77
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
end
|
132
|
-
end
|
78
|
+
begin
|
79
|
+
commands.each do |command|
|
80
|
+
command_name = "'#{command.join(' ')}'"
|
81
|
+
task_arn = EcsManager.run_task(cluster, family, command, options).tasks[0].task_arn
|
82
|
+
info "Launched #{command_name} task #{task_arn}, waiting for completion..."
|
133
83
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
84
|
+
EcsManager.ecs.wait_until(:tasks_stopped, cluster: cluster, tasks: [task_arn]) do |w|
|
85
|
+
w.max_attempts = nil
|
86
|
+
w.delay = Broadside.config.aws.ecs_poll_frequency
|
87
|
+
w.before_attempt do |attempt|
|
88
|
+
info "Attempt #{attempt}: waiting for #{command_name} to complete..."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
exit_status = EcsManager.get_task_exit_status(cluster, task_arn, family)
|
93
|
+
raise EcsError, "#{command_name} failed to start:\n'#{exit_status[:reason]}'" if exit_status[:exit_code].nil?
|
94
|
+
raise EcsError, "#{command_name} nonzero exit code: #{exit_status[:exit_code]}!" unless exit_status[:exit_code].zero?
|
95
|
+
|
96
|
+
info "#{command_name} task container logs:\n#{get_container_logs(task_arn)}"
|
97
|
+
info "#{command_name} task #{task_arn} complete"
|
98
|
+
end
|
99
|
+
ensure
|
100
|
+
EcsManager.deregister_last_n_tasks_definitions(family, 1)
|
142
101
|
end
|
143
102
|
end
|
144
103
|
|
145
104
|
private
|
146
105
|
|
147
|
-
def
|
148
|
-
EcsManager.
|
106
|
+
def deploy
|
107
|
+
current_scale = EcsManager.current_service_scale(@target)
|
108
|
+
update_task_revision
|
109
|
+
|
110
|
+
begin
|
111
|
+
update_service
|
112
|
+
rescue Interrupt, StandardError => e
|
113
|
+
msg = e.is_a?(Interrupt) ? 'Caught interrupt signal' : "#{e.class}: #{e.message}"
|
114
|
+
error "#{msg}, rolling back..."
|
115
|
+
# In case of failure during deploy, rollback to the previously configured scale
|
116
|
+
rollback(scale: current_scale)
|
117
|
+
error 'Deployment did not finish successfully.'
|
118
|
+
raise e
|
119
|
+
end
|
149
120
|
end
|
150
121
|
|
151
|
-
# Creates a new task revision using current directory's env vars, provided tag, and
|
152
|
-
# Currently can only handle a single container definition.
|
122
|
+
# Creates a new task revision using current directory's env vars, provided tag, and @target.task_definition_config
|
153
123
|
def update_task_revision
|
124
|
+
EcsManager.check_task_definition_state!(target)
|
154
125
|
revision = EcsManager.get_latest_task_definition(family).except(
|
155
126
|
:requires_attributes,
|
156
127
|
:revision,
|
@@ -158,114 +129,78 @@ module Broadside
|
|
158
129
|
:task_definition_arn
|
159
130
|
)
|
160
131
|
updatable_container_definitions = revision[:container_definitions].select { |c| c[:name] == family }
|
161
|
-
|
132
|
+
raise Error, 'Can only update one container definition!' if updatable_container_definitions.size != 1
|
162
133
|
|
163
|
-
# Deep merge doesn't work well with arrays (e.g.
|
164
|
-
updatable_container_definitions.first.merge!(
|
134
|
+
# Deep merge doesn't work well with arrays (e.g. container_definitions), so build the container first.
|
135
|
+
updatable_container_definitions.first.merge!(configured_container_definition)
|
165
136
|
revision.deep_merge!((@target.task_definition_config || {}).except(:container_definitions))
|
166
137
|
|
167
138
|
task_definition = EcsManager.ecs.register_task_definition(revision).task_definition
|
168
139
|
debug "Successfully created #{task_definition.task_definition_arn}"
|
169
140
|
end
|
170
141
|
|
171
|
-
|
172
|
-
|
142
|
+
def update_service(options = {})
|
143
|
+
scale = options[:scale] || @target.scale
|
144
|
+
raise ArgumentError, ':scale not provided' unless scale
|
145
|
+
|
146
|
+
EcsManager.check_service_and_task_definition_state!(target)
|
173
147
|
task_definition_arn = EcsManager.get_latest_task_definition_arn(family)
|
174
|
-
debug "Updating #{family} with scale=#{
|
148
|
+
debug "Updating #{family} with scale=#{scale} using task_definition #{task_definition_arn}..."
|
175
149
|
|
176
150
|
update_service_response = EcsManager.ecs.update_service({
|
177
|
-
cluster:
|
178
|
-
desired_count:
|
151
|
+
cluster: cluster,
|
152
|
+
desired_count: scale,
|
179
153
|
service: family,
|
180
154
|
task_definition: task_definition_arn
|
181
155
|
}.deep_merge(@target.service_config || {}))
|
182
156
|
|
183
157
|
unless update_service_response.successful?
|
184
|
-
|
158
|
+
raise EcsError, "Failed to update service:\n#{update_service_response.pretty_inspect}"
|
185
159
|
end
|
186
160
|
|
187
|
-
EcsManager.ecs.wait_until(:services_stable,
|
188
|
-
|
189
|
-
w.delay = config.
|
190
|
-
|
161
|
+
EcsManager.ecs.wait_until(:services_stable, cluster: cluster, services: [family]) do |w|
|
162
|
+
timeout = Broadside.config.timeout
|
163
|
+
w.delay = Broadside.config.aws.ecs_poll_frequency
|
164
|
+
w.max_attempts = timeout ? timeout / w.delay : nil
|
165
|
+
seen_event_id = nil
|
191
166
|
|
192
167
|
w.before_wait do |attempt, response|
|
193
|
-
|
194
|
-
#
|
195
|
-
if response.services[0].events.first && response.services[0].events.first.id !=
|
196
|
-
|
197
|
-
|
168
|
+
info "(#{attempt}/#{w.max_attempts || Float::INFINITY}) Polling ECS for events..."
|
169
|
+
# Skip first event since it doesn't apply to current request
|
170
|
+
if response.services[0].events.first && response.services[0].events.first.id != seen_event_id && attempt > 1
|
171
|
+
seen_event_id = response.services[0].events.first.id
|
172
|
+
info response.services[0].events.first.message
|
198
173
|
end
|
199
174
|
end
|
200
175
|
end
|
201
176
|
end
|
202
177
|
|
203
|
-
def run_commands(commands)
|
204
|
-
return if commands.nil? || commands.empty?
|
205
|
-
|
206
|
-
update_task_revision
|
207
|
-
|
208
|
-
begin
|
209
|
-
Array.wrap(commands).each do |command|
|
210
|
-
command_name = command.join(' ')
|
211
|
-
run_task_response = EcsManager.run_task(@target.cluster, family, command)
|
212
|
-
|
213
|
-
unless run_task_response.successful? && run_task_response.tasks.try(:[], 0)
|
214
|
-
exception("Failed to run #{command_name} task.", run_task_response.pretty_inspect)
|
215
|
-
end
|
216
|
-
|
217
|
-
task_arn = run_task_response.tasks[0].task_arn
|
218
|
-
debug "Launched #{command_name} task #{task_arn}, waiting for completion..."
|
219
|
-
|
220
|
-
EcsManager.ecs.wait_until(:tasks_stopped, { cluster: @target.cluster, tasks: [task_arn] }) do |w|
|
221
|
-
w.max_attempts = nil
|
222
|
-
w.delay = config.ecs.poll_frequency
|
223
|
-
w.before_attempt do |attempt|
|
224
|
-
debug "Attempt #{attempt}: waiting for #{command_name} to complete..."
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
info "#{command_name} task container logs:\n#{get_container_logs(task_arn)}"
|
229
|
-
|
230
|
-
if (code = EcsManager.get_task_exit_code(@target.cluster, task_arn, family)) == 0
|
231
|
-
debug "#{command_name} task #{task_arn} exited with status code 0"
|
232
|
-
else
|
233
|
-
exception "#{command_name} task #{task_arn} exited with a non-zero status code #{code}!"
|
234
|
-
end
|
235
|
-
end
|
236
|
-
ensure
|
237
|
-
EcsManager.deregister_last_n_tasks_definitions(family, 1)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
178
|
def get_container_logs(task_arn)
|
242
|
-
ip = EcsManager.get_running_instance_ips(
|
243
|
-
debug "Found
|
179
|
+
ip = EcsManager.get_running_instance_ips!(cluster, family, task_arn).first
|
180
|
+
debug "Found IP of container instance: #{ip}"
|
244
181
|
|
245
|
-
find_container_id_cmd = "#{
|
182
|
+
find_container_id_cmd = "#{Broadside.config.ssh_cmd(ip)} \"docker ps -aqf 'label=com.amazonaws.ecs.task-arn=#{task_arn}'\""
|
246
183
|
debug "Running command to find container id:\n#{find_container_id_cmd}"
|
247
|
-
|
184
|
+
container_ids = `#{find_container_id_cmd}`.split
|
248
185
|
|
249
|
-
|
250
|
-
|
186
|
+
logs = ''
|
187
|
+
container_ids.each do |container_id|
|
188
|
+
get_container_logs_cmd = "#{Broadside.config.ssh_cmd(ip)} \"docker logs #{container_id}\""
|
189
|
+
debug "Running command to get logs of container #{container_id}:\n#{get_container_logs_cmd}"
|
251
190
|
|
252
|
-
|
253
|
-
|
254
|
-
|
191
|
+
Open3.popen3(get_container_logs_cmd) do |_, stdout, stderr, _|
|
192
|
+
logs << "STDOUT (#{container_id}):\n--\n#{stdout.read}\nSTDERR (#{container_id}):\n--\n#{stderr.read}\n"
|
193
|
+
end
|
255
194
|
end
|
195
|
+
|
256
196
|
logs
|
257
197
|
end
|
258
198
|
|
259
|
-
def
|
260
|
-
|
261
|
-
if configured_containers && configured_containers.size > 1
|
262
|
-
raise ArgumentError, 'Creating > 1 container definition not supported yet'
|
263
|
-
end
|
264
|
-
|
265
|
-
(configured_containers.try(:first) || {}).merge(
|
199
|
+
def configured_container_definition
|
200
|
+
(@target.task_definition_config.try(:[], :container_definitions).try(:first) || {}).merge(
|
266
201
|
name: family,
|
267
|
-
command: @command,
|
268
|
-
environment: @target.
|
202
|
+
command: @target.command,
|
203
|
+
environment: @target.ecs_env_vars,
|
269
204
|
image: image_tag
|
270
205
|
)
|
271
206
|
end
|