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.
@@ -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
@@ -0,0 +1,9 @@
1
+ module Broadside
2
+ module InvalidConfiguration
3
+ extend LoggingUtils
4
+
5
+ def method_missing(m, *args, &block)
6
+ warn "Unknown configuration '#{m}' provided, ignoring."
7
+ end
8
+ end
9
+ end
@@ -1,111 +1,19 @@
1
1
  module Broadside
2
2
  class Deploy
3
- include Utils
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(target, opts = {})
15
- @target = target
16
- @command = opts[:command] || @target.command
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, "Missing tag" unless @tag
96
- "#{config.docker_image}:#{@tag}"
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: 1000
11
+ memory: 1024
13
12
  }
14
13
 
15
- def initialize(target, opts = {})
16
- super
17
- config.ecs.verify(:cluster, :poll_frequency)
14
+ def short
15
+ deploy
18
16
  end
19
17
 
20
- def deploy
21
- super do
22
- unless EcsManager.service_exists?(@target.cluster, family)
23
- exception "No service for #{family}! Please bootstrap or manually configure the service."
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
- update_task_revision
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
- unless EcsManager.get_latest_task_definition_arn(family)
50
- unless @target.task_definition_config
51
- raise ArgumentError, "No first task definition and no :task_definition_config in '#{family}' configuration"
52
- end
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(container_definition)]
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?(@target.cluster, family)
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(@target.cluster, family, @target.service_config)
48
+ EcsManager.create_service(cluster, family, @target.service_config)
75
49
  end
76
50
  end
77
51
 
78
- def rollback(count = @rollback)
79
- super do
80
- begin
81
- EcsManager.deregister_last_n_tasks_definitions(family, count)
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
- def scale
91
- super do
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
- def run
97
- super do
98
- run_commands(@command)
99
- end
65
+ info 'Rollback complete.'
100
66
  end
101
67
 
102
- def status
103
- super do
104
- ips = EcsManager.get_running_instance_ips(@target.cluster, family)
105
- info "\n---------------",
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 logtail
116
- super do
117
- ip = get_running_instance_ip
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
- def ssh
127
- super do
128
- ip = get_running_instance_ip
129
- debug "Establishing an SSH connection to ip #{ip}..."
130
- exec gen_ssh_cmd(ip)
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
- def bash
135
- super do
136
- ip = get_running_instance_ip
137
- debug "Running bash for running container at ip #{ip}..."
138
- search_pattern = Shellwords.shellescape(family)
139
- cmd = "docker exec -i -t `docker ps -n 1 --quiet --filter name=#{search_pattern}` bash"
140
- bash_cmd = gen_ssh_cmd(ip, tty: true) + " '#{cmd}'"
141
- exec bash_cmd
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 get_running_instance_ip
148
- EcsManager.get_running_instance_ips(@target.cluster, family).fetch(@target.instance)
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 configured options.
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
- exception "Can only update one container definition!" if updatable_container_definitions.size != 1
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. :container_definitions), so build the container first.
164
- updatable_container_definitions.first.merge!(container_definition)
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
- # reloads the service using the latest task definition
172
- def update_service
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=#{@target.scale} using task #{task_definition_arn}..."
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: @target.cluster,
178
- desired_count: @target.scale,
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
- exception('Failed to update service during deploy.', update_service_response.pretty_inspect)
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, { cluster: @target.cluster, services: [family] }) do |w|
188
- w.max_attempts = config.timeout ? config.timeout / config.ecs.poll_frequency : nil
189
- w.delay = config.ecs.poll_frequency
190
- seen_event = nil
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
- debug "(#{attempt}/#{w.max_attempts}) Polling ECS for events..."
194
- # skip first event since it doesn't apply to current request
195
- if response.services[0].events.first && response.services[0].events.first.id != seen_event && attempt > 1
196
- seen_event = response.services[0].events.first.id
197
- debug(response.services[0].events.first.message)
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(@target.cluster, family, task_arn).first
243
- debug "Found ip of container instance: #{ip}"
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 = "#{gen_ssh_cmd(ip)} \"docker ps -aqf 'label=com.amazonaws.ecs.task-arn=#{task_arn}'\""
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
- container_id = `#{find_container_id_cmd}`.strip
184
+ container_ids = `#{find_container_id_cmd}`.split
248
185
 
249
- get_container_logs_cmd = "#{gen_ssh_cmd(ip)} \"docker logs #{container_id}\""
250
- debug "Running command to get logs of container #{container_id}:\n#{get_container_logs_cmd}"
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
- logs = nil
253
- Open3.popen3(get_container_logs_cmd) do |_, stdout, stderr, _|
254
- logs = "STDOUT:--\n#{stdout.read}\nSTDERR:--\n#{stderr.read}"
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 container_definition
260
- configured_containers = (@target.task_definition_config || {})[:container_definitions]
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.env_vars,
202
+ command: @target.command,
203
+ environment: @target.ecs_env_vars,
269
204
  image: image_tag
270
205
  )
271
206
  end