broadside 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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