broadside 1.1.1 → 1.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 419c835965bd07e3dc7b0ea768d9c627862e8499
4
- data.tar.gz: ee3747f4a5e54723d804f29421059cdef842afb8
3
+ metadata.gz: 06c6097999d33e216468be90c482378b894496aa
4
+ data.tar.gz: e34ca4894bd642e4ea163f81f34f1b660b9689ba
5
5
  SHA512:
6
- metadata.gz: 2e3072860cf4b3c6c4329a125fd0727a02375269d6c4e08acd5bef4ab135d7fa4de7da3cd5012b0e71e01afc3844819f10c29ee8cc8b7a891a68c86dd51cfcd9
7
- data.tar.gz: d1bfa94f90f463704cdda69f51df19576759eb25fa1ad584ba466527959775542f9823821c6ae2cfb9f6a13ae1ec5cc96994f1a0f291f77c407f03e50f6196aa
6
+ metadata.gz: b583cb598536cd02b403d929ff237ea9632d124126b7f7abd03ab481f76de155a4ec489f83cfab771acc1bbf67f67fdee1112e3a1c462c6e2c1341890fae4161
7
+ data.tar.gz: aebd8291a1c8b21f7ada0f827834250aa601d7e0d0f199394d6a5523ee6905cde8b8b32f70e5bef1871333f3459dd9698165544d81f6fe86a360c194ecd65bb2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 1.2.0
2
+ - [#32](https://github.com/lumoslabs/broadside/pull/32): Deploys will also update service configs defined in a deploy target (see full list in the [AWS Docs](https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#create_service-instance_method))
3
+ - Updates additional container definition configs like cpu, memory. See full list in the [AWS Docs](https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method)
4
+ - [#24](https://github.com/lumoslabs/broadside/pull/24): Refactored most ECS-specific utility methods into a separate class
5
+
1
6
  # 1.1.1
2
7
  - [#25](https://github.com/lumoslabs/broadside/issues/25): Fix issue with undefined local variable 'ecs'
3
8
 
data/README.md CHANGED
@@ -41,7 +41,10 @@ Broadside.configure do |config|
41
41
  env_file: '../.env.staging'
42
42
  },
43
43
  # Example with a task_definition and service configuration which you use to bootstrap a service and
44
- # initial task definition
44
+ # initial task definition. Accepts all the options AWS does - read their documentation for details:
45
+ #
46
+ # Service config: https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#create_service-instance_method
47
+ # Task Definition Config: https://docs.aws.amazon.com/sdkforruby/api/Aws/ECS/Client.html#register_task_definition-instance_method
45
48
  game_save_as_json_blob_stream: {
46
49
  scale: 1,
47
50
  command: ['java', '-cp', '*:.', 'path.to.MyClass'],
data/bin/broadside CHANGED
@@ -27,7 +27,7 @@ def add_shared_deploy_configs(subcmd)
27
27
 
28
28
  subcmd.action do |global_options, options, args|
29
29
  _DeployObj = Kernel.const_get("Broadside::#{Broadside.config.deploy.type.capitalize}Deploy")
30
- _DeployObj.new(options).send(subcmd.name)
30
+ _DeployObj.new(options).public_send(subcmd.name)
31
31
  end
32
32
  end
33
33
 
@@ -42,7 +42,7 @@ accept Fixnum do |val|
42
42
  val.to_i
43
43
  end
44
44
 
45
- desc 'Bootstrap your service and task definition.'
45
+ desc 'Bootstrap your service and task definition from the configured definition.'
46
46
  command :bootstrap do |b|
47
47
  add_shared_deploy_configs(b)
48
48
  end
@@ -168,7 +168,7 @@ on_error do |exception|
168
168
  # false skips default error handling
169
169
  case exception
170
170
  when Broadside::MissingVariableError
171
- error exception.message, "\nRun your last command with --help for more information."
171
+ error exception.message, "Run your last command with --help for more information."
172
172
  false
173
173
  else
174
174
  true
data/lib/broadside.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  require 'broadside/error'
2
2
  require 'broadside/utils'
3
3
  require 'broadside/configuration'
4
- require 'broadside/configuration/struct'
5
- require 'broadside/configuration/aws'
6
- require 'broadside/configuration/base'
4
+ require 'broadside/configuration/config_struct'
5
+ require 'broadside/configuration/aws_config'
6
+ require 'broadside/configuration/base_config'
7
7
  require 'broadside/configuration/deploy_config'
8
8
  require 'broadside/configuration/ecs_config'
9
9
  require 'broadside/deploy'
10
10
  require 'broadside/deploy/ecs_deploy'
11
+ require 'broadside/deploy/ecs_manager'
11
12
  require 'broadside/version'
12
13
 
13
14
  module Broadside
@@ -31,7 +31,7 @@ module Broadside
31
31
  scale: ->(target_attribute) { validate_types([Fixnum], target_attribute) },
32
32
  env_file: ->(target_attribute) { validate_types([String], target_attribute) },
33
33
  command: ->(target_attribute) { validate_types([Array, NilClass], target_attribute) },
34
- predeploy_commands: ->(target_attribute) { validate_predeploy(target_attribute) },
34
+ predeploy_commands: ->(target_attribute) { validate_predeploy_commands(target_attribute) },
35
35
  service_config: ->(target_attribute) { validate_types([Hash, NilClass], target_attribute) },
36
36
  task_definition_config: ->(target_attribute) { validate_types([Hash, NilClass], target_attribute) }
37
37
  }
@@ -57,24 +57,24 @@ module Broadside
57
57
  # Checks existence of provided target
58
58
  def validate_targets!
59
59
  @targets.each do |target, configuration|
60
- TARGET_ATTRIBUTE_VALIDATIONS.each do |var, validation|
60
+ invalid_messages = TARGET_ATTRIBUTE_VALIDATIONS.map do |var, validation|
61
61
  message = validation.call(configuration[var])
62
+ message.nil? ? nil : "Deploy target '#{@target}' parameter '#{var}' is invalid: #{message}"
63
+ end.compact
62
64
 
63
- unless message.nil?
64
- exception "Deploy target '#{@target}' parameter '#{var}' is invalid: #{message}"
65
- end
65
+ unless invalid_messages.empty?
66
+ raise ArgumentError, invalid_messages.join("\n")
66
67
  end
67
68
  end
68
69
 
69
70
  unless @targets.has_key?(@target)
70
- exception "Could not find deploy target #{@target} in configuration !"
71
+ raise ArgumentError, "Could not find deploy target #{@target} in configuration !"
71
72
  end
72
73
  end
73
74
 
74
75
  # Loads deploy target data using provided target
75
76
  def load_target!
76
77
  validate_targets!
77
-
78
78
  env_file = Pathname.new(@targets[@target][:env_file])
79
79
 
80
80
  unless env_file.absolute?
@@ -84,13 +84,14 @@ module Broadside
84
84
 
85
85
  if env_file.exist?
86
86
  vars = Dotenv.load(env_file)
87
- @env_vars = vars.map { |k,v| {'name'=> k, 'value' => v } }
87
+ @env_vars = vars.map { |k, v| { 'name'=> k, 'value' => v } }
88
88
  else
89
- exception "Could not find file '#{env_file}' for loading environment variables !"
89
+ raise ArgumentError, "Could not find file '#{env_file}' for loading environment variables !"
90
90
  end
91
91
 
92
92
  @scale ||= @targets[@target][:scale]
93
93
  @command = @targets[@target][:command]
94
+ # TODO: what's up with predeploy_commands. ||= []?
94
95
  @predeploy_commands = @targets[@target][:predeploy_commands] if @targets[@target][:predeploy_commands]
95
96
  @service_config = @targets[@target][:service_config]
96
97
  @task_definition_config = @targets[@target][:task_definition_config]
@@ -99,18 +100,18 @@ module Broadside
99
100
  private
100
101
 
101
102
  def self.validate_types(types, target_attribute)
102
- unless types.include?(target_attribute.class)
103
- return "'#{target_attribute}' must be of type [#{types.join('|')}], got '#{target_attribute.class}' !"
103
+ if types.include?(target_attribute.class)
104
+ nil
105
+ else
106
+ "'#{target_attribute}' must be of type [#{types.join('|')}], got '#{target_attribute.class}' !"
104
107
  end
105
-
106
- nil
107
108
  end
108
109
 
109
- def self.validate_predeploy(target_attribute)
110
- return nil if target_attribute.nil?
111
- return 'predeploy_commands must be an array' unless target_attribute.is_a?(Array)
110
+ def self.validate_predeploy_commands(commands)
111
+ return nil if commands.nil?
112
+ return 'predeploy_commands must be an array' unless commands.is_a?(Array)
112
113
 
113
- messages = target_attribute.reject { |cmd| cmd.is_a?(Array) }.map do |command|
114
+ messages = commands.reject { |cmd| cmd.is_a?(Array) }.map do |command|
114
115
  "predeploy_command '#{command}' must be an array" unless command.is_a?(Array)
115
116
  end
116
117
  messages.empty? ? nil : messages.join(', ')
@@ -1,4 +1,3 @@
1
- require 'active_support/core_ext/hash'
2
1
  require 'aws-sdk'
3
2
  require 'open3'
4
3
  require 'pp'
@@ -7,8 +6,6 @@ require 'shellwords'
7
6
 
8
7
  module Broadside
9
8
  class EcsDeploy < Deploy
10
-
11
- DEFAULT_DESIRED_COUNT = 0
12
9
  DEFAULT_CONTAINER_DEFINITION = {
13
10
  cpu: 1,
14
11
  essential: true,
@@ -22,7 +19,13 @@ module Broadside
22
19
 
23
20
  def deploy
24
21
  super do
25
- exception "Service #{family} does not exist!" unless service_exists?
22
+ unless EcsManager.service_exists?(config.ecs.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
28
+
26
29
  update_task_revision
27
30
 
28
31
  begin
@@ -37,36 +40,49 @@ module Broadside
37
40
  error 'Deploy failed! Rolling back...'
38
41
  rollback(1)
39
42
  error 'Deployment did not finish successfully.'
40
- raise
43
+ raise e
41
44
  end
42
45
  end
43
46
  end
44
47
 
45
48
  def bootstrap
46
- unless get_latest_task_def_id
47
- # Right now this creates a useless first revision and then update_task_revision will create a 2nd one
48
- raise ArgumentError, "No first task definition and cannot create one" unless @deploy_config.task_definition_config
49
+ if EcsManager.get_latest_task_definition_arn(family)
50
+ info("Task definition for #{family} already exists.")
51
+ else
52
+ unless @deploy_config.task_definition_config
53
+ raise ArgumentError, "No first task definition and no :task_definition_config in '#{family}' configuration"
54
+ end
49
55
 
50
56
  info "Creating an initial task definition for '#{family}' from the config..."
51
- create_task_definition(family, @deploy_config.task_definition_config)
57
+
58
+ EcsManager.ecs.register_task_definition(
59
+ @deploy_config.task_definition_config.merge(
60
+ family: family,
61
+ container_definitions: [DEFAULT_CONTAINER_DEFINITION.merge(container_definition)]
62
+ )
63
+ )
52
64
  end
53
65
 
54
- unless service_exists?
55
- raise ArgumentError, "Service doesn't exist and cannot be created" unless @deploy_config.service_config
66
+ if EcsManager.service_exists?(config.ecs.cluster, family)
67
+ info("Service for #{family} already exists.")
68
+ else
69
+ unless @deploy_config.service_config
70
+ raise ArgumentError, "Service doesn't exist and no :service_config in '#{family}' configuration"
71
+ end
56
72
 
57
73
  info "Service '#{family}' doesn't exist, creating..."
58
- create_service(family, @deploy_config.service_config)
74
+ EcsManager.create_service(config.ecs.cluster, family, @deploy_config.service_config)
59
75
  end
60
76
  end
61
77
 
62
78
  def rollback(count = @deploy_config.rollback)
63
79
  super do
64
80
  begin
65
- deregister_tasks(count)
81
+ EcsManager.deregister_last_n_tasks_definitions(family, count)
66
82
  update_service
67
- rescue StandardError => e
83
+ rescue StandardError
68
84
  error 'Rollback failed to complete!'
69
- raise e
85
+ raise
70
86
  end
71
87
  end
72
88
  end
@@ -84,7 +100,7 @@ module Broadside
84
100
  begin
85
101
  run_command(@deploy_config.command)
86
102
  ensure
87
- deregister_tasks(1)
103
+ EcsManager.deregister_last_n_tasks_definitions(family, 1)
88
104
  end
89
105
  end
90
106
  end
@@ -95,22 +111,19 @@ module Broadside
95
111
  update_task_revision
96
112
 
97
113
  begin
98
- @deploy_config.predeploy_commands.each do |command|
99
- run_command(command)
100
- end
114
+ @deploy_config.predeploy_commands.each { |command| run_command(command) }
101
115
  ensure
102
- deregister_tasks(1)
116
+ EcsManager.deregister_last_n_tasks_definitions(family, 1)
103
117
  end
104
118
  end
105
119
  end
106
120
 
107
121
  def status
108
122
  super do
109
- td = get_latest_task_def
110
- ips = get_running_instance_ips
123
+ ips = EcsManager.get_running_instance_ips(config.ecs.cluster, family)
111
124
  info "\n---------------",
112
125
  "\nDeployed task definition information:\n",
113
- Rainbow(PP.pp(td, '')).blue,
126
+ Rainbow(PP.pp(EcsManager.get_latest_task_definition(family), '')).blue,
114
127
  "\nPrivate ips of instances running containers:\n",
115
128
  Rainbow(ips.join(' ')).blue,
116
129
  "\n\nssh command:\n#{Rainbow(gen_ssh_cmd(ips.first)).cyan}",
@@ -120,7 +133,7 @@ module Broadside
120
133
 
121
134
  def logtail
122
135
  super do
123
- ip = get_running_instance_ips.fetch(@deploy_config.instance)
136
+ ip = get_running_instance_ip
124
137
  debug "Tailing logs for running container at ip #{ip}..."
125
138
  search_pattern = Shellwords.shellescape(family)
126
139
  cmd = "docker logs -f --tail=10 `docker ps -n 1 --quiet --filter name=#{search_pattern}`"
@@ -131,7 +144,7 @@ module Broadside
131
144
 
132
145
  def ssh
133
146
  super do
134
- ip = get_running_instance_ips.fetch(@deploy_config.instance)
147
+ ip = get_running_instance_ip
135
148
  debug "Establishing an SSH connection to ip #{ip}..."
136
149
  exec gen_ssh_cmd(ip)
137
150
  end
@@ -139,7 +152,7 @@ module Broadside
139
152
 
140
153
  def bash
141
154
  super do
142
- ip = get_running_instance_ips.fetch(@deploy_config.instance)
155
+ ip = get_running_instance_ip
143
156
  debug "Running bash for running container at ip #{ip}..."
144
157
  search_pattern = Shellwords.shellescape(family)
145
158
  cmd = "docker exec -i -t `docker ps -n 1 --quiet --filter name=#{search_pattern}` bash"
@@ -150,141 +163,100 @@ module Broadside
150
163
 
151
164
  private
152
165
 
153
- # removes latest n task definitions
154
- def deregister_tasks(count)
155
- get_task_definition_arns.last(count).each do |td_id|
156
- ecs_client.deregister_task_definition({task_definition: td_id})
157
- debug "Deregistered #{td_id}"
158
- end
166
+ def get_running_instance_ip
167
+ EcsManager.get_running_instance_ips(config.ecs.cluster, family).fetch(@deploy_config.instance)
159
168
  end
160
169
 
161
- # creates a new task revision using current directory's env vars and provided tag
170
+ # Creates a new task revision using current directory's env vars, provided tag, and configured options.
171
+ # Currently can only handle a single container definition.
162
172
  def update_task_revision
163
- new_task_def = create_new_task_revision
164
-
165
- new_task_def[:container_definitions].select { |c| c[:name] == family }.first.tap do |container_def|
166
- container_def[:environment] = @deploy_config.env_vars
167
- container_def[:image] = image_tag
168
- container_def[:command] = @deploy_config.command
169
- end
170
-
171
- debug "Creating a new task definition..."
172
- new_task_def_id = ecs_client.register_task_definition(new_task_def).task_definition.task_definition_arn
173
- debug "Successfully created #{new_task_def_id}"
174
- end
175
-
176
- def create_service(name, options = {})
177
- ecs_client.create_service(
178
- {
179
- cluster: config.ecs.cluster,
180
- desired_count: DEFAULT_DESIRED_COUNT,
181
- service_name: name,
182
- task_definition: name
183
- }.deep_merge(options)
173
+ revision = EcsManager.get_latest_task_definition(family).except(
174
+ :requires_attributes,
175
+ :revision,
176
+ :status,
177
+ :task_definition_arn
184
178
  )
185
- end
179
+ updatable_container_definitions = revision[:container_definitions].select { |c| c[:name] == family }
180
+ exception "Can only update one container definition!" if updatable_container_definitions.size != 1
186
181
 
187
- def create_task_definition(name, options = {})
188
- # Deep merge doesn't work with arrays, so build the hash and merge later
189
- container_definitions = DEFAULT_CONTAINER_DEFINITION.merge(
190
- name: name,
191
- command: @deploy_config.command,
192
- environment: @deploy_config.env_vars,
193
- image: image_tag,
194
- ).merge(options[:container_definitions].first || {})
182
+ # Deep merge doesn't work well with arrays (e.g. :container_definitions), so build the container first.
183
+ updatable_container_definitions.first.merge!(container_definition)
184
+ revision.deep_merge!((@deploy_config.task_definition_config || {}).except(:container_definitions))
195
185
 
196
- ecs_client.register_task_definition(
197
- { family: name }.deep_merge(options).merge(container_definitions: [container_definitions])
198
- )
186
+ task_definition = EcsManager.ecs.register_task_definition(revision).task_definition
187
+ debug "Successfully created #{task_definition.task_definition_arn}"
199
188
  end
200
189
 
201
190
  # reloads the service using the latest task definition
202
191
  def update_service
203
- td = get_latest_task_def_id
204
- debug "Updating #{family} with scale=#{@deploy_config.scale} using task #{td}..."
205
- resp = ecs_client.update_service({
192
+ task_definition_arn = EcsManager.get_latest_task_definition_arn(family)
193
+ debug "Updating #{family} with scale=#{@deploy_config.scale} using task #{task_definition_arn}..."
194
+
195
+ update_service_response = EcsManager.ecs.update_service({
206
196
  cluster: config.ecs.cluster,
197
+ desired_count: @deploy_config.scale,
207
198
  service: family,
208
- task_definition: get_latest_task_def_id,
209
- desired_count: @deploy_config.scale
210
- })
199
+ task_definition: task_definition_arn
200
+ }.deep_merge(@deploy_config.service_config || {}))
211
201
 
212
- if resp.successful?
213
- begin
214
- ecs_client.wait_until(:services_stable, {cluster: config.ecs.cluster, services: [family]}) do |w|
215
- w.max_attempts = @deploy_config.timeout.nil? ? @deploy_config.timeout : @deploy_config.timeout / config.ecs.poll_frequency
216
- w.delay = config.ecs.poll_frequency
217
- seen_event = nil
218
- w.before_wait do |attempt, response|
219
- debug "(#{attempt}/#{w.max_attempts}) Polling ECS for events..."
220
- # skip first event since it doesn't apply to current request
221
- if response.services[0].events.first &&
222
- response.services[0].events.first.id != seen_event &&
223
- attempt > 1
224
- seen_event = response.services[0].events.first.id
225
- debug(response.services[0].events.first.message)
226
- end
227
- end
202
+ unless update_service_response.successful?
203
+ exception('Failed to update service during deploy.', update_service_response.pretty_inspect)
204
+ end
205
+
206
+ EcsManager.ecs.wait_until(:services_stable, { cluster: config.ecs.cluster, services: [family] }) do |w|
207
+ w.max_attempts = @deploy_config.timeout ? @deploy_config.timeout / config.ecs.poll_frequency : nil
208
+ w.delay = config.ecs.poll_frequency
209
+ seen_event = nil
210
+
211
+ w.before_wait do |attempt, response|
212
+ debug "(#{attempt}/#{w.max_attempts}) Polling ECS for events..."
213
+ # skip first event since it doesn't apply to current request
214
+ if response.services[0].events.first && response.services[0].events.first.id != seen_event && attempt > 1
215
+ seen_event = response.services[0].events.first.id
216
+ debug(response.services[0].events.first.message)
228
217
  end
229
- rescue Aws::Waiters::Errors::TooManyAttemptsError
230
- exception 'Deploy did not finish in the expected amount of time.'
231
218
  end
232
- else
233
- exception 'Failed to update service during deploy.'
234
219
  end
235
220
  end
236
221
 
237
222
  def run_command(command)
238
223
  command_name = command.join(' ')
239
- resp = ecs_client.run_task({
240
- cluster: config.ecs.cluster,
241
- task_definition: get_latest_task_def_id,
242
- overrides: {
243
- container_overrides: [
244
- {
245
- name: family,
246
- command: command
247
- },
248
- ],
249
- },
250
- count: 1,
251
- started_by: "before_deploy:#{command_name}"[0...36]
252
- })
253
-
254
- if resp.successful?
255
- task_id = resp.tasks[0].task_arn
256
- debug "Launched #{command_name} task #{task_id}"
257
- debug "Waiting for #{command_name} to complete..."
258
- ecs_client.wait_until(:tasks_stopped, {cluster: config.ecs.cluster, tasks: [task_id]}) do |w|
259
- w.max_attempts = nil
260
- w.delay = config.ecs.poll_frequency
261
- w.before_attempt do |attempt|
262
- debug "Attempt #{attempt}: waiting for #{command_name} to complete..."
263
- end
264
- end
265
- debug 'Task finished running, getting logs...'
266
- info "#{command_name} task container logs:\n#{get_container_logs(task_id)}"
267
- if (code = get_task_exit_code(task_id)) == 0
268
- debug "#{command_name} task #{task_id} exited with status code 0"
269
- else
270
- exception "#{command_name} task #{task_id} exited with a non-zero status code #{code}!"
224
+ run_task_response = EcsManager.run_task(config.ecs.cluster, family, command)
225
+
226
+ unless run_task_response.successful? && run_task_response.tasks.try(:[], 0)
227
+ exception("Failed to run #{command_name} task.", run_task_response.pretty_inspect)
228
+ end
229
+
230
+ task_arn = run_task_response.tasks[0].task_arn
231
+ debug "Launched #{command_name} task #{task_arn}, waiting for completion..."
232
+
233
+ EcsManager.ecs.wait_until(:tasks_stopped, { cluster: config.ecs.cluster, tasks: [task_arn] }) do |w|
234
+ w.max_attempts = nil
235
+ w.delay = config.ecs.poll_frequency
236
+ w.before_attempt do |attempt|
237
+ debug "Attempt #{attempt}: waiting for #{command_name} to complete..."
271
238
  end
239
+ end
240
+
241
+ info "#{command_name} task container logs:\n#{get_container_logs(task_arn)}"
242
+
243
+ if (code = EcsManager.get_task_exit_code(config.ecs.cluster, task_arn, family)) == 0
244
+ debug "#{command_name} task #{task_arn} exited with status code 0"
272
245
  else
273
- raise "Failed to run #{command_name} task."
246
+ exception "#{command_name} task #{task_arn} exited with a non-zero status code #{code}!"
274
247
  end
275
248
  end
276
249
 
277
- def get_container_logs(task_id)
278
- ip = get_running_instance_ips(task_id).first
250
+ def get_container_logs(task_arn)
251
+ ip = EcsManager.get_running_instance_ips(config.ecs.cluster, family, task_arn).first
279
252
  debug "Found ip of container instance: #{ip}"
280
253
 
281
- find_container_id_cmd = "#{gen_ssh_cmd(ip)} \"docker ps -aqf 'label=com.amazonaws.ecs.task-arn=#{task_id}'\""
254
+ find_container_id_cmd = "#{gen_ssh_cmd(ip)} \"docker ps -aqf 'label=com.amazonaws.ecs.task-arn=#{task_arn}'\""
282
255
  debug "Running command to find container id:\n#{find_container_id_cmd}"
283
256
  container_id = `#{find_container_id_cmd}`.strip
284
257
 
285
258
  get_container_logs_cmd = "#{gen_ssh_cmd(ip)} \"docker logs #{container_id}\""
286
- debug "Running command to get logs of container #{container_id}:",
287
- "\n#{get_container_logs_cmd}"
259
+ debug "Running command to get logs of container #{container_id}:\n#{get_container_logs_cmd}"
288
260
 
289
261
  logs = nil
290
262
  Open3.popen3(get_container_logs_cmd) do |_, stdout, stderr, _|
@@ -293,99 +265,18 @@ module Broadside
293
265
  logs
294
266
  end
295
267
 
296
- def get_task_exit_code(task_id)
297
- task = ecs_client.describe_tasks({cluster: config.ecs.cluster, tasks: [task_id]}).tasks.first
298
- container = task.containers.select { |c| c.name == family }.first
299
- container.exit_code
300
- end
301
-
302
- def get_running_instance_ips(task_ids = nil)
303
- task_arns = nil
304
- if task_ids.nil?
305
- task_arns = get_task_arns
306
- if task_arns.empty?
307
- exception "No running tasks found for '#{family}' on cluster '#{config.ecs.cluster}' !"
308
- end
309
- elsif task_ids.class == String
310
- task_arns = [task_ids]
311
- else
312
- task_arns = task_ids
313
- end
314
-
315
- tasks = ecs_client.describe_tasks({cluster: config.ecs.cluster, tasks: task_arns}).tasks
316
- container_instance_arns = tasks.map { |t| t.container_instance_arn }
317
- container_instances = ecs_client.describe_container_instances({
318
- cluster: config.ecs.cluster, container_instances: container_instance_arns
319
- }).container_instances
320
- ec2_instance_ids = container_instances.map { |ci| ci.ec2_instance_id }
321
- reservations = ec2_client.describe_instances({instance_ids: ec2_instance_ids}).reservations
322
- instances = reservations.map { |r| r.instances }.flatten
323
-
324
- instances.map { |i| i.private_ip_address }
325
- end
326
-
327
- def get_latest_task_def
328
- ecs_client.describe_task_definition({task_definition: get_latest_task_def_id}).task_definition.to_h
329
- end
330
-
331
- def get_task_arns
332
- all_results(:list_tasks, :task_arns, { cluster: config.ecs.cluster, family: family })
333
- end
334
-
335
- def get_task_definition_arns
336
- all_results(:list_task_definitions, :task_definition_arns, { family_prefix: family })
337
- end
338
-
339
- def list_task_definition_families
340
- all_results(:list_task_definition_families, :families)
341
- end
342
-
343
- def list_services
344
- all_results(:list_services, :service_arns, { cluster: config.ecs.cluster })
345
- end
346
-
347
- def get_latest_task_def_id
348
- get_task_definition_arns.last
349
- end
350
-
351
- def create_new_task_revision
352
- task_def = get_latest_task_def
353
- task_def.delete(:task_definition_arn)
354
- task_def.delete(:requires_attributes)
355
- task_def.delete(:revision)
356
- task_def.delete(:status)
357
- task_def
358
- end
359
-
360
- def service_exists?
361
- services = ecs_client.describe_services({ cluster: config.ecs.cluster, services: [family] })
362
- services.failures.empty? && !services.services.empty?
363
- end
364
-
365
- def ecs_client
366
- @ecs_client ||= Aws::ECS::Client.new({
367
- region: config.aws.region,
368
- credentials: config.aws.credentials
369
- })
370
- end
371
-
372
- def ec2_client
373
- @ec2_client ||= Aws::EC2::Client.new({
374
- region: config.aws.region,
375
- credentials: config.aws.credentials
376
- })
377
- end
378
-
379
- def all_results(method, key, args = {})
380
- page = ecs_client.public_send(method, args)
381
- results = page.send(key)
382
-
383
- while page.next_token
384
- page = ecs_client.public_send(method, args.merge(next_token: page.next_token))
385
- results += page.send(key)
268
+ def container_definition
269
+ configured_containers = (@deploy_config.task_definition_config || {})[:container_definitions]
270
+ if configured_containers && configured_containers.size > 1
271
+ raise ArgumentError, 'Creating > 1 container definition not supported yet'
386
272
  end
387
273
 
388
- results
274
+ (configured_containers.try(:first) || {}).merge(
275
+ name: family,
276
+ command: @deploy_config.command,
277
+ environment: @deploy_config.env_vars,
278
+ image: image_tag
279
+ )
389
280
  end
390
281
  end
391
- end
282
+ end
@@ -0,0 +1,130 @@
1
+ require 'active_support/core_ext/hash'
2
+ require 'active_support/core_ext/array'
3
+
4
+ module Broadside
5
+ class EcsManager
6
+ DEFAULT_DESIRED_COUNT = 0
7
+
8
+ class << self
9
+ include Utils
10
+
11
+ def ecs
12
+ @ecs_client ||= Aws::ECS::Client.new(
13
+ region: Broadside.config.aws.region,
14
+ credentials: Broadside.config.aws.credentials
15
+ )
16
+ end
17
+
18
+ def create_service(cluster, name, options = {})
19
+ ecs.create_service(
20
+ {
21
+ cluster: cluster,
22
+ desired_count: DEFAULT_DESIRED_COUNT,
23
+ service_name: name,
24
+ task_definition: name
25
+ }.deep_merge(options)
26
+ )
27
+ end
28
+
29
+ # removes latest n task definitions
30
+ def deregister_last_n_tasks_definitions(name, count)
31
+ get_task_definition_arns(name).last(count).each do |arn|
32
+ ecs.deregister_task_definition(task_definition: arn)
33
+ debug "Deregistered #{arn}"
34
+ end
35
+ end
36
+
37
+ def get_latest_task_definition(name)
38
+ return nil unless get_latest_task_definition_arn(name)
39
+ ecs.describe_task_definition(task_definition: get_latest_task_definition_arn(name)).task_definition.to_h
40
+ end
41
+
42
+ def get_latest_task_definition_arn(name)
43
+ get_task_definition_arns(name).last
44
+ end
45
+
46
+ def get_running_instance_ips(cluster, family, task_arns = nil)
47
+ task_arns = task_arns ? Array.wrap(task_arns) : get_task_arns(cluster, family)
48
+ exception "No running tasks found for '#{family}' on cluster '#{cluster}'!" if task_arns.empty?
49
+
50
+ tasks = ecs.describe_tasks(cluster: cluster, tasks: task_arns).tasks
51
+ container_instances = ecs.describe_container_instances(
52
+ cluster: cluster,
53
+ container_instances: tasks.map(&:container_instance_arn),
54
+ ).container_instances
55
+ ec2_instance_ids = container_instances.map(&:ec2_instance_id)
56
+
57
+ reservations = ec2_client.describe_instances(instance_ids: ec2_instance_ids).reservations
58
+ instances = reservations.map(&:instances).flatten
59
+ instances.map(&:private_ip_address)
60
+ end
61
+
62
+ def get_task_arns(cluster, family)
63
+ all_results(:list_tasks, :task_arns, { cluster: cluster, family: family })
64
+ end
65
+
66
+ def get_task_definition_arns(family)
67
+ all_results(:list_task_definitions, :task_definition_arns, { family_prefix: family })
68
+ end
69
+
70
+ def get_task_exit_code(cluster, task_arn, name)
71
+ task = ecs.describe_tasks({ cluster: cluster, tasks: [task_arn] }).tasks.first
72
+ container = task.containers.select { |c| c.name == name }.first
73
+ container.exit_code
74
+ end
75
+
76
+ def list_task_definition_families
77
+ all_results(:list_task_definition_families, :families)
78
+ end
79
+
80
+ def list_services(cluster)
81
+ all_results(:list_services, :service_arns, { cluster: cluster })
82
+ end
83
+
84
+ def run_task(cluster, name, command)
85
+ fail ArgumentError, "#{command} must be an array" unless command.is_a?(Array)
86
+
87
+ ecs.run_task(
88
+ cluster: cluster,
89
+ task_definition: get_latest_task_definition_arn(name),
90
+ overrides: {
91
+ container_overrides: [
92
+ {
93
+ name: name,
94
+ command: command
95
+ }
96
+ ]
97
+ },
98
+ count: 1,
99
+ started_by: "before_deploy:#{command.join(' ')}"[0...36]
100
+ )
101
+ end
102
+
103
+ def service_exists?(cluster, family)
104
+ services = ecs.describe_services(cluster: cluster, services: [family])
105
+ services.failures.empty? && services.services.any?
106
+ end
107
+
108
+ private
109
+
110
+ def all_results(method, key, args = {})
111
+ page = ecs.public_send(method, args)
112
+ results = page.public_send(key)
113
+
114
+ while page.next_token
115
+ page = ecs.public_send(method, args.merge(next_token: page.next_token))
116
+ results += page.public_send(key)
117
+ end
118
+
119
+ results
120
+ end
121
+
122
+ def ec2_client
123
+ @ec2_client ||= Aws::EC2::Client.new(
124
+ region: config.aws.region,
125
+ credentials: config.aws.credentials
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
@@ -17,7 +17,7 @@ module Broadside
17
17
  end
18
18
 
19
19
  def exception(*args)
20
- raise Broadside::Error, args.join(' ')
20
+ raise Broadside::Error, args.join("\n")
21
21
  end
22
22
 
23
23
  def config
@@ -1,3 +1,3 @@
1
1
  module Broadside
2
- VERSION = '1.1.1'
2
+ VERSION = '1.2.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: broadside
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Leung
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-03 00:00:00.000000000 Z
11
+ date: 2016-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -149,13 +149,14 @@ files:
149
149
  - broadside.gemspec
150
150
  - lib/broadside.rb
151
151
  - lib/broadside/configuration.rb
152
- - lib/broadside/configuration/aws.rb
153
- - lib/broadside/configuration/base.rb
152
+ - lib/broadside/configuration/aws_config.rb
153
+ - lib/broadside/configuration/base_config.rb
154
+ - lib/broadside/configuration/config_struct.rb
154
155
  - lib/broadside/configuration/deploy_config.rb
155
156
  - lib/broadside/configuration/ecs_config.rb
156
- - lib/broadside/configuration/struct.rb
157
157
  - lib/broadside/deploy.rb
158
158
  - lib/broadside/deploy/ecs_deploy.rb
159
+ - lib/broadside/deploy/ecs_manager.rb
159
160
  - lib/broadside/error.rb
160
161
  - lib/broadside/utils.rb
161
162
  - lib/broadside/version.rb