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.
@@ -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