broadside 2.0.0 → 3.0.0.pre.prerelease
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 +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
|