ecs_deploy_cli 0.1.0 → 0.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
  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