ecs_deploy_cli 0.1.0 → 0.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
  SHA256:
3
- metadata.gz: e5f34210100053703e0137631635b625ea37cc1cb140e4fa432c3375c6cdf87a
4
- data.tar.gz: '08d8ead32a4b87cddb19b36539d661fc954e006a8ac3986a8dc60aaac93cd560'
3
+ metadata.gz: de429c09d039ddd636e85d23ec40bc20d7e91a09af10fefcc4124086494a8127
4
+ data.tar.gz: 57e24e217b08edf956a8b43894486a6db702756fedd2016b13e635fa9f5c2f3a
5
5
  SHA512:
6
- metadata.gz: 90cb49d9d8de03d5b82946493d2df2466dec05cd01c190e9673ed6023adf9b5167465032bc47076ac12f7f8bfc451fd78c8568aec3aede6fd82467b7369914ad
7
- data.tar.gz: f3dedd74c575d671fc65afb974086d8683c4eb3ee38074aec7d1dacb0b31632dd52813127bb39f1b3df95167353a8871308da8fbd885fd57cbb7f3ef34ca9941
6
+ metadata.gz: cecc078f5afcd85d0ca3600fd8d7db360dbf99d3cf27901aca2b15f15500864cc860de616b4bcf49bc29024b0301e3a6af41bb74808e1a94e216d45f1076346c
7
+ data.tar.gz: 4c66c16a9948dd255ca23f626341a3b052fda205297e71f8ffe185d4e3add1cca7dfe18e7d69800c735db55c606db3275a08a41f63bdafae9eba7a0a88de0abe
@@ -1,14 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
  require 'logger'
3
5
  require 'thor'
4
6
  require 'aws-sdk-ecs'
5
7
  require 'active_support/core_ext/hash/indifferent_access'
8
+ require 'active_support/concern'
6
9
 
7
10
  module EcsDeployCli
8
11
  def self.logger
9
12
  @logger ||= begin
10
13
  logger = Logger.new(STDOUT)
11
- logger.formatter = proc { |severity, datetime, progname, msg|
14
+ logger.formatter = proc { |_severity, _datetime, _progname, msg|
12
15
  "#{msg}\n"
13
16
  }
14
17
  logger.level = Logger::INFO
@@ -1,5 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EcsDeployCli
2
4
  class CLI < Thor
5
+ def self.exit_on_failure?
6
+ true
7
+ end
8
+
3
9
  desc 'validate', 'Validates your ECSFile'
4
10
  option :file, default: 'ECSFile'
5
11
  def validate
@@ -8,6 +14,13 @@ module EcsDeployCli
8
14
  puts 'Your ECSFile looks fine! 🎉'
9
15
  end
10
16
 
17
+ desc 'diff', 'Check differences between task definitions'
18
+ option :file, default: 'ECSFile'
19
+ def diff
20
+ @parser = load(options[:file])
21
+ runner.diff
22
+ end
23
+
11
24
  desc 'version', 'Updates all services defined in your ECSFile'
12
25
  def version
13
26
  puts "ECS Deploy CLI Version #{EcsDeployCli::VERSION}."
@@ -29,7 +42,7 @@ module EcsDeployCli
29
42
  runner.update_services! timeout: options[:timeout], service: options[:only]
30
43
  end
31
44
 
32
- desc 'deploy', 'Updates a single service defined in your ECSFile'
45
+ desc 'deploy', 'Updates all services and scheduled tasks at once'
33
46
  option :file, default: 'ECSFile'
34
47
  option :timeout, type: :numeric, default: 500
35
48
  def deploy
@@ -38,6 +51,21 @@ module EcsDeployCli
38
51
  runner.update_crons!
39
52
  end
40
53
 
54
+ desc 'run-task NAME', 'Manually runs a task defined in your ECSFile'
55
+ option :launch_type, default: 'FARGATE'
56
+ option :security_groups, default: '', type: :string
57
+ option :subnets, required: true, type: :string
58
+ option :file, default: 'ECSFile'
59
+ def run_task(task_name)
60
+ @parser = load(options[:file])
61
+ runner.run_task!(
62
+ task_name,
63
+ launch_type: options[:launch_type],
64
+ security_groups: options[:security_groups].split(','),
65
+ subnets: options[:subnets].split(',')
66
+ )
67
+ end
68
+
41
69
  desc 'ssh', 'Connects to ECS instance via SSH'
42
70
  option :file, default: 'ECSFile'
43
71
  def ssh
@@ -1,9 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EcsDeployCli
2
4
  module DSL
3
5
  module AutoOptions
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def allowed_options(*value)
10
+ @allowed_options = value
11
+ end
12
+
13
+ def _allowed_options
14
+ @allowed_options ||= []
15
+ end
16
+ end
17
+
4
18
  def method_missing(name, *args, &block)
5
19
  if args.count == 1 && !block
6
- EcsDeployCli.logger.info("Auto-added option security_group #{name.to_sym} = #{args.first}")
20
+ unless self.class._allowed_options.include?(name)
21
+ EcsDeployCli.logger.info("Used unhandled option #{name.to_sym} = #{args.first} in #{self.class.name}")
22
+ end
7
23
  _options[name.to_sym] = args.first
8
24
  else
9
25
  super
@@ -5,25 +5,23 @@ module EcsDeployCli
5
5
  class Container
6
6
  include AutoOptions
7
7
 
8
+ allowed_options :image, :cpu, :working_directory
9
+
8
10
  def initialize(name, config)
9
11
  @config = config
10
12
  _options[:name] = name.to_s
11
13
  end
12
14
 
13
- def image(value)
14
- _options[:image] = value
15
- end
16
-
17
15
  def command(*command)
18
16
  _options[:command] = command
19
17
  end
20
18
 
21
19
  def load_envs(name)
22
- _options[:environment] = (_options[:environment] || []) + YAML.safe_load(File.open(name))
20
+ _options[:environment] = (_options[:environment] || []) + YAML.safe_load(File.open(name), symbolize_names: true)
23
21
  end
24
22
 
25
23
  def env(key:, value:)
26
- (_options[:environment] ||= []) << { 'name' => key, 'value' => value }
24
+ (_options[:environment] ||= []) << { name: key, value: value }
27
25
  end
28
26
 
29
27
  def secret(key:, value:)
@@ -34,10 +32,6 @@ module EcsDeployCli
34
32
  (_options[:port_mappings] ||= []) << options
35
33
  end
36
34
 
37
- def cpu(value)
38
- _options[:cpu] = value
39
- end
40
-
41
35
  def memory(limit:, reservation:)
42
36
  _options[:memory] = limit
43
37
  _options[:memory_reservation] = reservation
@@ -55,9 +55,9 @@ module EcsDeployCli
55
55
  end
56
56
 
57
57
  def resolve
58
- resolved_containers = @containers.transform_values(&:as_definition)
59
- resolved_tasks = @tasks.transform_values { |t| t.as_definition(resolved_containers) }
60
- resolved_crons = @crons.transform_values { |t| t.as_definition(resolved_tasks) }
58
+ resolved_containers = (@containers || {}).transform_values(&:as_definition)
59
+ resolved_tasks = (@tasks || {}).transform_values { |t| t.as_definition(resolved_containers) }
60
+ resolved_crons = (@crons || {}).transform_values { |t| t.as_definition(resolved_tasks) }
61
61
  [@services, resolved_tasks, resolved_crons]
62
62
  end
63
63
 
@@ -5,6 +5,8 @@ module EcsDeployCli
5
5
  class Task
6
6
  include AutoOptions
7
7
 
8
+ allowed_options :requires_compatibilities, :network_mode
9
+
8
10
  def initialize(name, config)
9
11
  @config = config
10
12
  _options[:family] = name.to_s
@@ -1,3 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ecs_deploy_cli/runners/base'
4
+ require 'ecs_deploy_cli/runners/ssh'
5
+ require 'ecs_deploy_cli/runners/validate'
6
+ require 'ecs_deploy_cli/runners/diff'
7
+ require 'ecs_deploy_cli/runners/update_crons'
8
+ require 'ecs_deploy_cli/runners/update_services'
9
+ require 'ecs_deploy_cli/runners/run_task'
10
+
1
11
  module EcsDeployCli
2
12
  class Runner
3
13
  def initialize(parser)
@@ -5,149 +15,35 @@ module EcsDeployCli
5
15
  end
6
16
 
7
17
  def validate!
8
- @parser.resolve
18
+ EcsDeployCli::Runners::Validate.new(@parser).run!
9
19
  end
10
20
 
11
21
  def update_crons!
12
- _, tasks, crons = @parser.resolve
13
-
14
- crons.each do |cron_name, cron_definition|
15
- task_definition = tasks[cron_definition[:task_name]]
16
- raise "Undefined task #{cron_definition[:task_name].inspect} in (#{tasks.keys.inspect})" unless task_definition
17
-
18
- updated_task = _update_task(task_definition)
19
-
20
- current_target = cwe_client.list_targets_by_rule(
21
- {
22
- rule: cron_name,
23
- limit: 1
24
- }
25
- ).to_h[:targets].first
26
-
27
- cwe_client.put_rule(
28
- cron_definition[:rule]
29
- )
22
+ EcsDeployCli::Runners::UpdateCrons.new(@parser).run!
23
+ end
30
24
 
31
- cwe_client.put_targets(
32
- rule: cron_name,
33
- targets: [
34
- id: current_target[:id],
35
- arn: current_target[:arn],
36
- role_arn: current_target[:role_arn],
37
- input: cron_definition[:input].to_json,
38
- ecs_parameters: cron_definition[:ecs_parameters].merge(task_definition_arn: updated_task[:task_definition_arn])
39
- ]
40
- )
41
- EcsDeployCli.logger.info "Deployed scheduled task \"#{cron_name}\"!"
42
- end
25
+ def run_task!(task_name, launch_type:, security_groups:, subnets:)
26
+ EcsDeployCli::Runners::RunTask.new(@parser).run!(task_name, launch_type: launch_type, security_groups: security_groups, subnets: subnets)
43
27
  end
44
28
 
45
29
  def ssh
46
- instances = ecs_client.list_container_instances(
47
- cluster: config[:cluster]
48
- ).to_h[:container_instance_arns]
49
-
50
- response = ecs_client.describe_container_instances(
51
- cluster: config[:cluster],
52
- container_instances: instances
53
- )
54
-
55
- EcsDeployCli.logger.info "Found instances: #{response.container_instances.map(&:ec2_instance_id).join(', ')}"
56
-
57
- response = ec2_client.describe_instances(
58
- instance_ids: response.container_instances.map(&:ec2_instance_id)
59
- )
60
-
61
- dns_name = response.reservations[0].instances[0].public_dns_name
62
- EcsDeployCli.logger.info "Connecting to ec2-user@#{dns_name}..."
30
+ EcsDeployCli::Runners::SSH.new(@parser).run!
31
+ end
63
32
 
64
- Process.fork { exec("ssh ec2-user@#{dns_name}") }
65
- Process.wait
33
+ def diff
34
+ EcsDeployCli::Runners::Diff.new(@parser).run!
66
35
  end
67
36
 
68
37
  def update_services!(service: nil, timeout: 500)
69
- services, resolved_tasks = @parser.resolve
70
-
71
- services.each do |service_name, service_definition|
72
- next if !service.nil? && service != service_name
73
-
74
- task_definition = _update_task resolved_tasks[service_definition.options[:task]]
75
- task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
76
-
77
- ecs_client.update_service(
78
- cluster: config[:cluster],
79
- service: service_name,
80
- task_definition: "#{task_definition[:family]}:#{task_name}"
81
- )
82
- wait_for_deploy(service_name, task_name, timeout: timeout)
83
- EcsDeployCli.logger.info "Deployed service \"#{service_name}\"!"
84
- end
38
+ EcsDeployCli::Runners::UpdateServices.new(@parser).run!(service: service, timeout: timeout)
85
39
  end
86
40
 
87
41
  private
88
42
 
89
- def wait_for_deploy(service_name, task_name, timeout:)
90
- wait_data = { cluster: config[:cluster], services: [service_name] }
91
-
92
- started_at = Time.now
93
- ecs_client.wait_until(
94
- :services_stable, wait_data,
95
- max_attempts: nil,
96
- before_wait: lambda { |_, response|
97
- deployments = response.services.first.deployments
98
- log_deployments task_name, deployments
99
-
100
- throw :success if deployments.count == 1 && deployments[0].task_definition.end_with?(task_name)
101
- throw :failure if Time.now - started_at > timeout
102
- }
103
- )
104
- end
105
-
106
43
  def _update_task(definition)
107
44
  ecs_client.register_task_definition(
108
45
  definition
109
46
  ).to_h[:task_definition]
110
47
  end
111
-
112
- def log_deployments(task_name, deployments)
113
- EcsDeployCli.logger.info "Waiting for task: #{task_name} to become ok."
114
- EcsDeployCli.logger.info 'Deployment status:'
115
- deployments.each do |deploy|
116
- EcsDeployCli.logger.info "[#{deploy.status}] task=#{deploy.task_definition.split('/').last}, "\
117
- "desired_count=#{deploy.desired_count}, pending_count=#{deploy.pending_count}, running_count=#{deploy.running_count}, failed_tasks=#{deploy.failed_tasks}"
118
- end
119
- EcsDeployCli.logger.info ''
120
- end
121
-
122
- def ec2_client
123
- @ec2_client ||= begin
124
- require 'aws-sdk-ec2'
125
- Aws::EC2::Client.new(
126
- profile: ENV.fetch('AWS_PROFILE', 'default'),
127
- region: config[:aws_region]
128
- )
129
- end
130
- end
131
-
132
- def ecs_client
133
- @ecs_client ||= Aws::ECS::Client.new(
134
- profile: ENV.fetch('AWS_PROFILE', 'default'),
135
- region: config[:aws_region]
136
- )
137
- end
138
-
139
- def cwe_client
140
- @cwe_client ||= begin
141
- require 'aws-sdk-cloudwatchevents'
142
- Aws::CloudWatchEvents::Client.new(
143
- profile: ENV.fetch('AWS_PROFILE', 'default'),
144
- region: config[:aws_region]
145
- )
146
- end
147
- end
148
-
149
- def config
150
- @parser.config
151
- end
152
48
  end
153
49
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Base
6
+ def initialize(parser)
7
+ @parser = parser
8
+ end
9
+
10
+ def run!
11
+ raise NotImplementedError, 'abstract method'
12
+ end
13
+
14
+ protected
15
+
16
+ def _update_task(definition)
17
+ ecs_client.register_task_definition(
18
+ definition
19
+ ).to_h[:task_definition]
20
+ end
21
+
22
+ def ec2_client
23
+ @ec2_client ||= begin
24
+ require 'aws-sdk-ec2'
25
+ Aws::EC2::Client.new(
26
+ profile: ENV.fetch('AWS_PROFILE', 'default'),
27
+ region: config[:aws_region]
28
+ )
29
+ end
30
+ end
31
+
32
+ def ecs_client
33
+ @ecs_client ||= Aws::ECS::Client.new(
34
+ profile: ENV.fetch('AWS_PROFILE', 'default'),
35
+ region: config[:aws_region]
36
+ )
37
+ end
38
+
39
+ def cwe_client
40
+ @cwe_client ||= begin
41
+ require 'aws-sdk-cloudwatchevents'
42
+ Aws::CloudWatchEvents::Client.new(
43
+ profile: ENV.fetch('AWS_PROFILE', 'default'),
44
+ region: config[:aws_region]
45
+ )
46
+ end
47
+ end
48
+
49
+ def config
50
+ @parser.config
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Diff < Base
6
+ def run!
7
+ require 'hashdiff'
8
+ require 'colorize'
9
+
10
+ _, tasks, = @parser.resolve
11
+
12
+ tasks.each do |task_name, definition|
13
+ EcsDeployCli.logger.info '---'
14
+ EcsDeployCli.logger.info "Task: #{task_name}"
15
+
16
+ result = ecs_client.describe_task_definition(task_definition: "#{task_name}").to_h
17
+
18
+ current = result[:task_definition].except(:revision, :status, :registered_at, :registered_by, :requires_attributes, :task_definition_arn)
19
+
20
+ print_diff Hashdiff.diff(current.except(:container_definitions), definition.except(:container_definitions))
21
+
22
+ current[:container_definitions].zip(definition[:container_definitions]).each do |a, b|
23
+ EcsDeployCli.logger.info "Container #{a&.dig(:name) || 'NONE'} <=> #{b&.dig(:name) || 'NONE'}"
24
+
25
+ print_diff Hashdiff.diff(a, b) if a && b
26
+ end
27
+ EcsDeployCli.logger.info '---'
28
+ end
29
+ end
30
+
31
+ def print_diff(diff)
32
+ diff.each do |(op, path, *values)|
33
+ if op == '-'
34
+ EcsDeployCli.logger.info "#{op} #{path} => #{values.join(' ')}".colorize(:red)
35
+ elsif op == '+'
36
+ EcsDeployCli.logger.info "#{op} #{path} => #{values.join(' ')}".colorize(:green)
37
+ else
38
+ EcsDeployCli.logger.info "#{op} #{path} => #{values.join(' ')}".colorize(:light_blue)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class RunTask < Base
6
+ def run!(task, launch_type:, security_groups:, subnets:)
7
+ _, tasks, = @parser.resolve
8
+
9
+ task_definition = _update_task tasks[task]
10
+ task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
11
+
12
+ ecs_client.run_task(
13
+ cluster: config[:cluster],
14
+ task_definition: task_name,
15
+ network_configuration: {
16
+ awsvpc_configuration: {
17
+ subnets: subnets,
18
+ security_groups: security_groups,
19
+ assign_public_ip: 'ENABLED'
20
+ }
21
+ },
22
+ launch_type: launch_type
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class SSH < Base
6
+ def run!
7
+ instance_ids = load_container_instances
8
+ EcsDeployCli.logger.info "Found instances: #{instance_ids.join(', ')}"
9
+
10
+ dns_name = load_dns_name_from_instance_ids(instance_ids)
11
+ run_ssh(dns_name)
12
+ end
13
+
14
+ private
15
+
16
+ def load_dns_name_from_instance_ids(instance_ids)
17
+ response = ec2_client.describe_instances(
18
+ instance_ids: instance_ids
19
+ )
20
+
21
+ response.reservations[0].instances[0].public_dns_name
22
+ end
23
+
24
+ def load_container_instances
25
+ instances = ecs_client.list_container_instances(
26
+ cluster: config[:cluster]
27
+ ).to_h[:container_instance_arns]
28
+
29
+ response = ecs_client.describe_container_instances(
30
+ cluster: config[:cluster],
31
+ container_instances: instances
32
+ )
33
+
34
+ response.container_instances.map(&:ec2_instance_id)
35
+ end
36
+
37
+ def run_ssh(dns_name)
38
+ EcsDeployCli.logger.info "Connecting to ec2-user@#{dns_name}..."
39
+
40
+ Process.fork { exec("ssh ec2-user@#{dns_name}") }
41
+ Process.wait
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class UpdateCrons < Base
6
+ def run!
7
+ _, tasks, crons = @parser.resolve
8
+
9
+ crons.each do |cron_name, cron_definition|
10
+ task_definition = tasks[cron_definition[:task_name]]
11
+ raise "Undefined task #{cron_definition[:task_name].inspect} in (#{tasks.keys.inspect})" unless task_definition
12
+
13
+ updated_task = _update_task(task_definition)
14
+
15
+ current_target = cwe_client.list_targets_by_rule(
16
+ {
17
+ rule: cron_name,
18
+ limit: 1
19
+ }
20
+ ).to_h[:targets].first
21
+
22
+ cwe_client.put_rule(
23
+ cron_definition[:rule]
24
+ )
25
+
26
+ cwe_client.put_targets(
27
+ rule: cron_name,
28
+ targets: [
29
+ id: current_target[:id],
30
+ arn: current_target[:arn],
31
+ role_arn: current_target[:role_arn],
32
+ input: cron_definition[:input].to_json,
33
+ ecs_parameters: cron_definition[:ecs_parameters].merge(task_definition_arn: updated_task[:task_definition_arn])
34
+ ]
35
+ )
36
+ EcsDeployCli.logger.info "Deployed scheduled task \"#{cron_name}\"!"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class UpdateServices < Base
6
+ def run!(service: nil, timeout: 500)
7
+ services, resolved_tasks = @parser.resolve
8
+
9
+ services.each do |service_name, service_definition|
10
+ next if !service.nil? && service != service_name
11
+
12
+ task_definition = _update_task resolved_tasks[service_definition.options[:task]]
13
+ task_name = "#{task_definition[:family]}:#{task_definition[:revision]}"
14
+
15
+ ecs_client.update_service(
16
+ cluster: config[:cluster],
17
+ service: service_name,
18
+ task_definition: "#{task_definition[:family]}:#{task_name}"
19
+ )
20
+ wait_for_deploy(service_name, task_name, timeout: timeout)
21
+ EcsDeployCli.logger.info "Deployed service \"#{service_name}\"!"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def wait_for_deploy(service_name, task_name, timeout:)
28
+ wait_data = { cluster: config[:cluster], services: [service_name] }
29
+
30
+ started_at = Time.now
31
+ ecs_client.wait_until(
32
+ :services_stable, wait_data,
33
+ max_attempts: nil,
34
+ before_wait: lambda { |_, response|
35
+ deployments = response.services.first.deployments
36
+ log_deployments task_name, deployments
37
+
38
+ throw :success if deployments.count == 1 && deployments[0].task_definition.end_with?(task_name)
39
+ throw :failure if Time.now - started_at > timeout
40
+ }
41
+ )
42
+ end
43
+
44
+ def log_deployments(task_name, deployments)
45
+ EcsDeployCli.logger.info "Waiting for task: #{task_name} to become ok."
46
+ EcsDeployCli.logger.info 'Deployment status:'
47
+ deployments.each do |deploy|
48
+ EcsDeployCli.logger.info "[#{deploy.status}] task=#{deploy.task_definition.split('/').last}, "\
49
+ "desired_count=#{deploy.desired_count}, pending_count=#{deploy.pending_count}, running_count=#{deploy.running_count}, failed_tasks=#{deploy.failed_tasks}"
50
+ end
51
+ EcsDeployCli.logger.info ''
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Validate < Base
6
+ def run!
7
+ services, _, crons = @parser.resolve
8
+
9
+ validate_cluster!
10
+ validate_services!(services)
11
+ validate_crons!(crons)
12
+ end
13
+
14
+ private
15
+
16
+ def validate_cluster!
17
+ data = ecs_client.describe_clusters(clusters: [config[:cluster]])
18
+
19
+ raise "No such cluster #{config[:cluster]}." if data.to_h[:failures]&.any? || data.to_h[:clusters].length == 0
20
+ rescue Aws::ECS::Errors::ClusterNotFoundException
21
+ raise "No such cluster #{config[:cluster]}."
22
+ end
23
+
24
+ def validate_services!(services)
25
+ services&.each do |service_name, _|
26
+ data = ecs_client.describe_services(cluster: config[:cluster], services: [service_name])
27
+
28
+ raise "No such service #{service_name}." if data.to_h[:failures]&.any? || data.to_h[:services].length == 0
29
+ end
30
+ end
31
+
32
+ def validate_crons!(crons)
33
+ crons&.each do |cron_name, _|
34
+ items = cwe_client.list_targets_by_rule(
35
+ {
36
+ rule: cron_name,
37
+ limit: 1
38
+ }
39
+ )
40
+ raise "No such cron #{cron_name}." if items.targets.empty?
41
+ rescue Aws::CloudWatchEvents::Errors::ResourceNotFoundException
42
+ raise "No such cron #{cron_name}."
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EcsDeployCli
2
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe EcsDeployCli::CLI do
@@ -20,33 +22,63 @@ describe EcsDeployCli::CLI do
20
22
  expect { described_class.start(['version']) }.to output(/Version #{EcsDeployCli::VERSION}/).to_stdout
21
23
  end
22
24
 
25
+ it 'runs diff' do
26
+ expect(runner).to receive(:diff)
27
+ described_class.no_commands do
28
+ expect_any_instance_of(described_class).to receive(:runner).at_least(:once).and_return(runner)
29
+ end
30
+
31
+ described_class.start(['diff', '--file', 'spec/support/ECSFile'])
32
+ end
33
+
23
34
  it 'runs validate' do
35
+ expect(runner).to receive(:validate!)
36
+ described_class.no_commands do
37
+ expect_any_instance_of(described_class).to receive(:runner).at_least(:once).and_return(runner)
38
+ end
24
39
  expect { described_class.start(['validate', '--file', 'spec/support/ECSFile']) }.to output(/Your ECSFile looks fine! 🎉/).to_stdout
25
40
  end
26
41
 
42
+ it 'runs run-task' do
43
+ expect(runner).to receive(:run_task!)
44
+ described_class.no_commands do
45
+ expect_any_instance_of(described_class).to receive(:runner).at_least(:once).and_return(runner)
46
+ end
47
+
48
+ described_class.start(['run-task', 'yourproject', '--subnets', 'subnet-123123', '--file', 'spec/support/ECSFile'])
49
+ end
50
+
27
51
  it 'runs deploy' do
28
52
  expect(runner).to receive(:update_crons!)
29
53
  expect(runner).to receive(:update_services!).with(timeout: 500)
30
- expect_any_instance_of(described_class).to receive(:runner).at_least(:once).and_return(runner)
54
+ described_class.no_commands do
55
+ expect_any_instance_of(described_class).to receive(:runner).at_least(:once).and_return(runner)
56
+ end
31
57
  expect { described_class.start(['deploy', '--file', 'spec/support/ECSFile']) }.to output(/[WARNING]/).to_stdout
32
58
  end
33
59
 
34
60
  it 'runs deploy-services' do
35
61
  expect(runner).to receive(:update_services!)
36
- expect_any_instance_of(described_class).to receive(:runner).and_return(runner)
62
+ described_class.no_commands do
63
+ expect_any_instance_of(described_class).to receive(:runner).and_return(runner)
64
+ end
37
65
  expect { described_class.start(['deploy-services', '--file', 'spec/support/ECSFile']) }.to output(/[WARNING]/).to_stdout
38
66
  end
39
67
 
40
68
  it 'runs ssh' do
41
69
  expect(runner).to receive(:ssh)
42
- expect_any_instance_of(described_class).to receive(:runner).and_return(runner)
70
+ described_class.no_commands do
71
+ expect_any_instance_of(described_class).to receive(:runner).and_return(runner)
72
+ end
43
73
 
44
74
  described_class.start(['ssh', '--file', 'spec/support/ECSFile'])
45
75
  end
46
76
 
47
77
  it 'runs deploy-scheduled-tasks' do
48
78
  expect(runner).to receive(:update_crons!)
49
- expect_any_instance_of(described_class).to receive(:runner).and_return(runner)
79
+ described_class.no_commands do
80
+ expect_any_instance_of(described_class).to receive(:runner).at_least(:once).and_return(runner)
81
+ end
50
82
 
51
83
  described_class.start(['deploy-scheduled-tasks', '--file', 'spec/support/ECSFile'])
52
84
  end
@@ -20,10 +20,10 @@ describe EcsDeployCli::DSL::Container do
20
20
  expect(subject.as_definition[:environment]).to eq(
21
21
  [
22
22
  {
23
- 'name' => 'SOME', 'value' => 'env'
23
+ name: 'SOME', value: 'env'
24
24
  },
25
25
  {
26
- 'name' => 'SOME2', 'value' => 'env2'
26
+ name: 'SOME2', value: 'env2'
27
27
  }
28
28
  ]
29
29
  )
@@ -89,10 +89,10 @@ describe EcsDeployCli::DSL::Container do
89
89
  expect(subject.as_definition[:environment]).to eq(
90
90
  [
91
91
  {
92
- 'name' => 'RAILS_ENV', 'value' => 'production'
92
+ name: 'RAILS_ENV', value: 'production'
93
93
  },
94
94
  {
95
- 'name' => 'API_KEY', 'value' => '123123123'
95
+ name: 'API_KEY', value: '123123123'
96
96
  }
97
97
  ]
98
98
  )
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe EcsDeployCli::DSL::Task do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'aws-sdk-cloudwatchevents'
3
5
  require 'aws-sdk-ec2'
@@ -12,8 +14,65 @@ describe EcsDeployCli::Runner do
12
14
  Aws::CloudWatchEvents::Client.new(stub_responses: true)
13
15
  end
14
16
 
15
- it '#validate!' do
16
- expect { subject.validate! }.to raise_error('Missing required parameter aws_profile_id')
17
+ context '#validate!' do
18
+ it 'fails on missing params' do
19
+ expect { subject.validate! }.to raise_error('Missing required parameter aws_profile_id')
20
+ end
21
+
22
+ context 'with a minimal set of options' do
23
+ let(:parser) { EcsDeployCli::DSL::Parser.load('spec/support/ECSFile.minimal') }
24
+ it 'fails on missing params' do
25
+ mock_ecs_client.stub_responses(:describe_clusters, { clusters: [{ cluster_arn: 'arn:xxx', cluster_name: 'yourproject-cluster' }] })
26
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
27
+ subject.validate!
28
+ end
29
+ end
30
+
31
+ context 'with envs set' do
32
+ around(:each) do |example|
33
+ ENV['AWS_PROFILE_ID'] = '123123123'
34
+ ENV['AWS_REGION'] = 'us-east-1'
35
+ example.run
36
+ ENV['AWS_PROFILE_ID'] = nil
37
+ ENV['AWS_REGION'] = nil
38
+ end
39
+
40
+ it 'fails on missing cluster' do
41
+ mock_ecs_client.stub_responses(:describe_clusters, { failures: [{ arn: 'arn:xxx', reason: 'MISSING' }] })
42
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
43
+ expect { subject.validate! }.to raise_error('No such cluster yourproject-cluster.')
44
+ end
45
+
46
+ it 'fails on missing service' do
47
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
48
+
49
+ mock_ecs_client.stub_responses(:describe_clusters, { clusters: [{ cluster_arn: 'arn:xxx', cluster_name: 'yourproject-cluster' }] })
50
+ mock_ecs_client.stub_responses(:describe_services, { services: [], failures: [{}] })
51
+
52
+ expect { subject.validate! }.to raise_error('No such service yourproject-service.')
53
+ end
54
+
55
+ it 'fails on missing crons' do
56
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwe_client).at_least(:once).and_return(mock_cwe_client)
57
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
58
+
59
+ mock_ecs_client.stub_responses(:describe_clusters, { clusters: [{ cluster_arn: 'arn:xxx', cluster_name: 'yourproject-cluster' }] })
60
+ mock_ecs_client.stub_responses(:describe_services, { services: [{ service_arn: 'arn:xxx', service_name: 'yourproject-service' }] })
61
+
62
+ expect { subject.validate! }.to raise_error('No such cron scheduled_emails.')
63
+ end
64
+
65
+ it 'makes API calls to check if everything is there' do
66
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
67
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwe_client).at_least(:once).and_return(mock_cwe_client)
68
+
69
+ mock_ecs_client.stub_responses(:describe_clusters, { clusters: [{ cluster_arn: 'arn:xxx', cluster_name: 'yourproject-cluster' }] })
70
+ mock_cwe_client.stub_responses(:list_targets_by_rule, { targets: [{ id: '123', arn: 'arn:123' }] })
71
+ mock_ecs_client.stub_responses(:describe_services, { services: [{ service_arn: 'arn:xxx', service_name: 'yourproject-service' }] })
72
+
73
+ subject.validate!
74
+ end
75
+ end
17
76
  end
18
77
 
19
78
  context 'with envs set' do
@@ -30,33 +89,53 @@ describe EcsDeployCli::Runner do
30
89
  expect(mock_ecs_client).to receive(:describe_container_instances).and_return(double(container_instances: [double(ec2_instance_id: 'i-123123')]))
31
90
 
32
91
  expect(mock_ec2_client).to receive(:describe_instances)
33
- .with(instance_ids: ['i-123123'])
34
- .and_return(
35
- double(reservations: [
36
- double(instances: [double(public_dns_name: 'test.com')])
37
- ]
38
- )
39
- )
92
+ .with(instance_ids: ['i-123123'])
93
+ .and_return(
94
+ double(reservations: [
95
+ double(instances: [double(public_dns_name: 'test.com')])
96
+ ])
97
+ )
40
98
 
41
99
  expect(Process).to receive(:fork) do |&block|
42
100
  block.call
43
101
  end
44
102
  expect(Process).to receive(:wait)
45
103
 
46
- expect(subject).to receive(:exec).with('ssh ec2-user@test.com')
47
- expect(subject).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
48
- expect(subject).to receive(:ec2_client).at_least(:once).and_return(mock_ec2_client)
104
+ expect_any_instance_of(EcsDeployCli::Runners::SSH).to receive(:exec).with('ssh ec2-user@test.com')
105
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
106
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ec2_client).at_least(:once).and_return(mock_ec2_client)
49
107
 
50
108
  subject.ssh
51
109
  end
52
110
 
111
+ it '#diff' do
112
+ mock_ecs_client.stub_responses(:describe_task_definition)
113
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
114
+
115
+ expect(EcsDeployCli.logger).to receive(:info).at_least(:once) do |message|
116
+ puts message
117
+ end
118
+
119
+ expect { subject.diff }.to output(/Task: yourproject/).to_stdout
120
+ end
121
+
122
+ it '#run_task!' do
123
+ mock_ecs_client.stub_responses(:register_task_definition, { task_definition: { family: 'some', revision: 1, task_definition_arn: 'arn:task:eu-central-1:xxxx' } })
124
+
125
+ mock_cwe_client.stub_responses(:run_task)
126
+
127
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
128
+
129
+ subject.run_task!('yourproject-cron', launch_type: 'FARGATE', security_groups: [], subnets: [])
130
+ end
131
+
53
132
  it '#update_crons!' do
54
133
  mock_ecs_client.stub_responses(:register_task_definition, { task_definition: { family: 'some', revision: 1, task_definition_arn: 'arn:task:eu-central-1:xxxx' } })
55
134
 
56
135
  mock_cwe_client.stub_responses(:list_targets_by_rule, { targets: [{ id: '123', arn: 'arn:123' }] })
57
136
 
58
- expect(subject).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
59
- expect(subject).to receive(:cwe_client).at_least(:once).and_return(mock_cwe_client)
137
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
138
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwe_client).at_least(:once).and_return(mock_cwe_client)
60
139
 
61
140
  subject.update_crons!
62
141
  end
@@ -66,7 +145,7 @@ describe EcsDeployCli::Runner do
66
145
  expect(mock_ecs_client).to receive(:update_service)
67
146
  expect(mock_ecs_client).to receive(:wait_until)
68
147
 
69
- expect(subject).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
148
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
70
149
 
71
150
  subject.update_services!
72
151
  end
@@ -0,0 +1,12 @@
1
+ aws_region ENV.fetch('AWS_REGION', 'eu-central-1')
2
+
3
+ # Used to create ARNs
4
+ aws_profile_id '123123'
5
+
6
+ # Defining the cluster name
7
+ cluster 'yourproject-cluster'
8
+
9
+ # A container to exec cron jobs
10
+ container :cron do
11
+ command 'rails', 'runner'
12
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecs_deploy_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mònade
@@ -73,6 +73,34 @@ dependencies:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
75
  version: '1'
76
+ - !ruby/object:Gem::Dependency
77
+ name: colorize
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.1
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.8.1
90
+ - !ruby/object:Gem::Dependency
91
+ name: hashdiff
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ type: :runtime
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.0'
76
104
  - !ruby/object:Gem::Dependency
77
105
  name: thor
78
106
  requirement: !ruby/object:Gem::Requirement
@@ -134,6 +162,13 @@ files:
134
162
  - lib/ecs_deploy_cli/dsl/service.rb
135
163
  - lib/ecs_deploy_cli/dsl/task.rb
136
164
  - lib/ecs_deploy_cli/runner.rb
165
+ - lib/ecs_deploy_cli/runners/base.rb
166
+ - lib/ecs_deploy_cli/runners/diff.rb
167
+ - lib/ecs_deploy_cli/runners/run_task.rb
168
+ - lib/ecs_deploy_cli/runners/ssh.rb
169
+ - lib/ecs_deploy_cli/runners/update_crons.rb
170
+ - lib/ecs_deploy_cli/runners/update_services.rb
171
+ - lib/ecs_deploy_cli/runners/validate.rb
137
172
  - lib/ecs_deploy_cli/version.rb
138
173
  - spec/ecs_deploy_cli/cli_spec.rb
139
174
  - spec/ecs_deploy_cli/dsl/container_spec.rb
@@ -143,12 +178,14 @@ files:
143
178
  - spec/ecs_deploy_cli/runner_spec.rb
144
179
  - spec/spec_helper.rb
145
180
  - spec/support/ECSFile
181
+ - spec/support/ECSFile.minimal
146
182
  - spec/support/env_file.ext.yml
147
183
  - spec/support/env_file.yml
148
184
  homepage: https://rubygems.org/gems/ecs_deploy_cli
149
185
  licenses:
150
186
  - MIT
151
- metadata: {}
187
+ metadata:
188
+ source_code_uri: https://github.com/monade/ecs-deploy-cli
152
189
  post_install_message:
153
190
  rdoc_options: []
154
191
  require_paths:
@@ -164,13 +201,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
201
  - !ruby/object:Gem::Version
165
202
  version: '0'
166
203
  requirements: []
167
- rubygems_version: 3.2.7
204
+ rubygems_version: 3.1.4
168
205
  signing_key:
169
206
  specification_version: 4
170
207
  summary: A command line interface to make ECS deployments easier
171
208
  test_files:
172
209
  - spec/spec_helper.rb
173
210
  - spec/support/ECSFile
211
+ - spec/support/ECSFile.minimal
174
212
  - spec/support/env_file.yml
175
213
  - spec/support/env_file.ext.yml
176
214
  - spec/ecs_deploy_cli/runner_spec.rb