ecs_deploy_cli 0.4.0 → 0.5.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: f3c493de7ebb99888eb436db698b8215291b733983ac8b37a16e7842c8588d0d
4
- data.tar.gz: 34f5613fef2b6746196ed91b3c67d358c02bdafcf89703e1704c2b5e93ca987d
3
+ metadata.gz: 0be6eaeb2fd7c5c088b90f3b37983fa859d281620bbcc8c887159a22e8aaa1b8
4
+ data.tar.gz: 9b59b8669defa19eec6af2946c44b2ff0dff4c2a5b01497685923e09b5779f24
5
5
  SHA512:
6
- metadata.gz: aa4641ecfd920aa198310ef81cd00f9f173d0fa3c98066172792e0c4d74cd09009c1c000f20ee128be9374e037c3ada0002f44e8a51a0ac7895b1a376699eb22
7
- data.tar.gz: 3144d96e6f64b42faf4d47ffad4c2dfcbd4c818507978491b42586842730ae36a9c3d0cf367d00845dc347d87abc90e05ae7181264adec243d02078b19deee8d
6
+ metadata.gz: 103a4642983b04c15ed353be27367a317f7de14593676080ee493328756d17549a8717c647cabcef7739ce6000fc09649000f5ce20c7c6e6a0c1cc8857546c0c
7
+ data.tar.gz: 133202293bc169a56890081c13a8cf866ce77ac3c2c2a284a21b68f4010a00bd478d3eb9b636182ebc490bf210c4cf680dbe83205207d8847fd594b427af1733
@@ -97,6 +97,16 @@ module EcsDeployCli
97
97
  end
98
98
  end
99
99
 
100
+ def iam_client
101
+ @iam_client ||= begin
102
+ require 'aws-sdk-iam'
103
+ Aws::IAM::Client.new(
104
+ profile: ENV.fetch('AWS_PROFILE', 'default'),
105
+ region: config[:aws_region]
106
+ )
107
+ end
108
+ end
109
+
100
110
  def config
101
111
  @parser.config
102
112
  end
@@ -3,33 +3,43 @@
3
3
  module EcsDeployCli
4
4
  module Runners
5
5
  class Setup < Base
6
+ REQUIRED_ECS_ROLES = {
7
+ 'ecsInstanceRole' => 'https://docs.aws.amazon.com/batch/latest/userguide/instance_IAM_role.html',
8
+ 'ecsTaskExecutionRole' => 'https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html'
9
+ }.freeze
10
+ class SetupError < StandardError; end
11
+
6
12
  def run!
7
13
  services, resolved_tasks, _, cluster_options = @parser.resolve
8
14
 
15
+ ensure_ecs_roles_exists!
16
+
9
17
  setup_cluster! cluster_options
10
18
  setup_services! services, resolved_tasks: resolved_tasks
19
+ rescue SetupError => e
20
+ EcsDeployCli.logger.info e.message
11
21
  end
12
22
 
13
23
  private
14
24
 
15
25
  def setup_cluster!(cluster_options)
16
- clusters = ecs_client.describe_clusters(clusters: [config[:cluster]]).to_h[:clusters]
17
- if clusters.length == 1
26
+ if cluster_exists?
18
27
  EcsDeployCli.logger.info 'Cluster already created, skipping.'
19
28
  return
20
29
  end
21
30
 
22
31
  EcsDeployCli.logger.info "Creating cluster #{config[:cluster]}..."
23
32
 
33
+ create_keypair_if_required! cluster_options
24
34
  params = create_params(cluster_options)
25
35
 
26
36
  ecs_client.create_cluster(
27
37
  cluster_name: config[:cluster]
28
38
  )
39
+ EcsDeployCli.logger.info 'Cluster created, now running cloudformation...'
29
40
 
30
41
  stack_name = "EC2ContainerService-#{config[:cluster]}"
31
42
 
32
-
33
43
  cf_client.create_stack(
34
44
  stack_name: stack_name,
35
45
  template_body: File.read(File.join(__dir__, '..', 'cloudformation', 'default.yml')),
@@ -38,12 +48,13 @@ module EcsDeployCli
38
48
  )
39
49
 
40
50
  cf_client.wait_until(:stack_create_complete, { stack_name: stack_name }, delay: 30, max_attempts: 120)
41
- EcsDeployCli.logger.info "Cluster #{config[:cluster]} created!"
51
+ EcsDeployCli.logger.info "Cluster #{config[:cluster]} created! 🎉"
42
52
  end
43
53
 
44
54
  def setup_services!(services, resolved_tasks:)
45
55
  services.each do |service_name, service_definition|
46
- if ecs_client.describe_services(cluster: config[:cluster], services: [service_name]).to_h[:services].any?
56
+ existing_services = ecs_client.describe_services(cluster: config[:cluster], services: [service_name]).to_h[:services].filter { |s| s[:status] != 'INACTIVE' }
57
+ if existing_services.any?
47
58
  EcsDeployCli.logger.info "Service #{service_name} already created, skipping."
48
59
  next
49
60
  end
@@ -54,7 +65,7 @@ module EcsDeployCli
54
65
 
55
66
  ecs_client.create_service(
56
67
  cluster: config[:cluster],
57
- desired_count: 1,
68
+ desired_count: 1, # FIXME: this should be a parameter
58
69
  load_balancers: service_definition.as_definition(task_definition)[:load_balancers],
59
70
  service_name: service_name,
60
71
  task_definition: task_name
@@ -110,6 +121,29 @@ module EcsDeployCli
110
121
  }
111
122
  end
112
123
 
124
+ def cluster_exists?
125
+ clusters = ecs_client.describe_clusters(clusters: [config[:cluster]]).to_h[:clusters]
126
+
127
+ clusters.filter { |c| c[:status] != 'INACTIVE' }.length == 1
128
+ end
129
+
130
+ def ensure_ecs_roles_exists!
131
+ REQUIRED_ECS_ROLES.each do |role_name, link|
132
+ iam_client.get_role(role_name: role_name).to_h
133
+ rescue Aws::IAM::Errors::NoSuchEntity
134
+ raise SetupError, "IAM Role #{role_name} does not exist. Please create it: #{link}."
135
+ end
136
+ end
137
+
138
+ def create_keypair_if_required!(cluster_options)
139
+ ec2_client.describe_key_pairs(key_names: [cluster_options[:keypair_name]]).to_h[:key_pairs]
140
+ rescue Aws::EC2::Errors::InvalidKeyPairNotFound
141
+ EcsDeployCli.logger.info "Keypair \"#{cluster_options[:keypair_name]}\" not found, creating it..."
142
+ key_material = ec2_client.create_key_pair(key_name: cluster_options[:keypair_name]).to_h[:key_material]
143
+ File.write("#{cluster_options[:keypair_name]}.pem", key_material)
144
+ EcsDeployCli.logger.info "Created PEM file at #{Dir.pwd}/#{cluster_options[:keypair_name]}.pem"
145
+ end
146
+
113
147
  def format_cloudformation_params(params)
114
148
  params.map { |k, v| { parameter_key: k, parameter_value: v.to_s } }
115
149
  end
@@ -8,16 +8,13 @@ module EcsDeployCli
8
8
 
9
9
  crons.each do |cron_name, cron_definition|
10
10
  task_definition = tasks[cron_definition[:task_name]]
11
- raise "Undefined task #{cron_definition[:task_name].inspect} in (#{tasks.keys.inspect})" unless task_definition
11
+ unless task_definition
12
+ raise "Undefined task #{cron_definition[:task_name].inspect} in (#{tasks.keys.inspect})"
13
+ end
12
14
 
13
15
  updated_task = _update_task(task_definition)
14
16
 
15
- current_target = cwe_client.list_targets_by_rule(
16
- {
17
- rule: cron_name,
18
- limit: 1
19
- }
20
- ).to_h[:targets].first
17
+ current_target = load_or_init_target(cron_name)
21
18
 
22
19
  cwe_client.put_rule(
23
20
  cron_definition[:rule]
@@ -36,6 +33,18 @@ module EcsDeployCli
36
33
  EcsDeployCli.logger.info "Deployed scheduled task \"#{cron_name}\"!"
37
34
  end
38
35
  end
36
+
37
+ private
38
+
39
+ def load_or_init_target(cron_name)
40
+ cwe_client.list_targets_by_rule({ rule: cron_name, limit: 1 }).to_h[:targets].first
41
+ rescue Aws::CloudWatchEvents::Errors::ResourceNotFoundException
42
+ {
43
+ id: cron_name,
44
+ arn: "arn:aws:ecs:#{config[:aws_region]}:#{config[:aws_profile_id]}:cluster/#{config[:cluster]}",
45
+ role_arn: "arn:aws:iam::#{config[:aws_profile_id]}:role/ecsEventsRole"
46
+ }
47
+ end
39
48
  end
40
49
  end
41
50
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EcsDeployCli
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -6,11 +6,13 @@ require 'aws-sdk-cloudwatchlogs'
6
6
  require 'aws-sdk-ec2'
7
7
  require 'aws-sdk-ssm'
8
8
  require 'aws-sdk-cloudformation'
9
+ require 'aws-sdk-iam'
9
10
 
10
11
  describe EcsDeployCli::Runner do
11
12
  context 'defines task data' do
12
13
  let(:parser) { EcsDeployCli::DSL::Parser.load('spec/support/ECSFile') }
13
14
  subject { described_class.new(parser) }
15
+ let(:mock_iam_client) { Aws::IAM::Client.new(stub_responses: true) }
14
16
  let(:mock_cf_client) { Aws::CloudFormation::Client.new(stub_responses: true) }
15
17
  let(:mock_ssm_client) { Aws::SSM::Client.new(stub_responses: true) }
16
18
  let(:mock_ecs_client) { Aws::ECS::Client.new(stub_responses: true) }
@@ -90,24 +92,77 @@ describe EcsDeployCli::Runner do
90
92
  ENV['AWS_REGION'] = nil
91
93
  end
92
94
 
93
- it '#setup!' do
94
- mock_ssm_client.stub_responses(:get_parameter, {
95
- parameter: {
96
- name: '/aws/service/ecs/optimized-ami/amazon-linux-2/recommended',
97
- type: 'String',
98
- value: '{"schema_version":1,"image_name":"amzn2-ami-ecs-hvm-2.0.20210331-x86_64-ebs","image_id":"ami-03bbf53329af34379","os":"Amazon Linux 2","ecs_runtime_version":"Docker version 19.03.13-ce","ecs_agent_version":"1.51.0"}'
99
- }
100
- })
95
+ context '#setup!' do
96
+ before do
97
+ mock_ssm_client.stub_responses(
98
+ :get_parameter, {
99
+ parameter: {
100
+ name: '/aws/service/ecs/optimized-ami/amazon-linux-2/recommended',
101
+ type: 'String',
102
+ value: '{"schema_version":1,"image_name":"amzn2-ami-ecs-hvm-2.0.20210331-x86_64-ebs","image_id":"ami-03bbf53329af34379","os":"Amazon Linux 2","ecs_runtime_version":"Docker version 19.03.13-ce","ecs_agent_version":"1.51.0"}'
103
+ }
104
+ }
105
+ )
106
+ end
101
107
 
102
- expect(mock_cf_client).to receive(:wait_until)
103
- expect(mock_ecs_client).to receive(:create_service)
108
+ it 'setups the cluster correctly' do
109
+ expect(mock_ec2_client).to receive(:describe_key_pairs).and_return(key_pairs: [{ key_id: 'some' }])
104
110
 
105
- expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
106
- expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
107
- expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ssm_client).at_least(:once).and_return(mock_ssm_client)
108
- expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cf_client).at_least(:once).and_return(mock_cf_client)
111
+ expect(mock_iam_client).to receive(:get_role).at_least(:once).and_return({ role: { arn: 'some' } })
112
+ expect(mock_cf_client).to receive(:wait_until)
113
+ expect(mock_ecs_client).to receive(:create_service)
114
+
115
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ec2_client).at_least(:once).and_return(mock_ec2_client)
116
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:iam_client).at_least(:once).and_return(mock_iam_client)
117
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
118
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
119
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ssm_client).at_least(:once).and_return(mock_ssm_client)
120
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cf_client).at_least(:once).and_return(mock_cf_client)
121
+
122
+ subject.setup!
123
+ end
124
+
125
+ it 'fails if the IAM role is not setup' do
126
+ expect(EcsDeployCli.logger).to receive(:info).at_least(:once) do |message|
127
+ puts message
128
+ end
129
+
130
+ expect(mock_iam_client).to receive(:get_role).at_least(:once) do
131
+ raise Aws::IAM::Errors::NoSuchEntity.new(nil, 'some')
132
+ end
133
+
134
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:iam_client).at_least(:once).and_return(mock_iam_client)
135
+
136
+ expect { subject.setup! }.to output(/IAM Role ecsInstanceRole does not exist./).to_stdout
137
+ end
138
+
139
+ it 'fails if the cluster is already there' do
140
+ expect(mock_ecs_client).to receive(:describe_clusters).and_return(clusters: [{}])
141
+
142
+ expect(EcsDeployCli.logger).to receive(:info).at_least(:once) do |message|
143
+ puts message
144
+ end
145
+
146
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
147
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:iam_client).at_least(:once).and_return(mock_iam_client)
148
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
149
+
150
+ expect { subject.setup! }.to output(/Cluster already created, skipping./).to_stdout
151
+ end
109
152
 
110
- subject.setup!
153
+ it 'creates the keypair if not there' do
154
+ expect(mock_ec2_client).to receive(:describe_key_pairs) do
155
+ raise Aws::EC2::Errors::InvalidKeyPairNotFound.new(nil, 'some')
156
+ end
157
+
158
+ expect(mock_ec2_client).to receive(:create_key_pair) { raise 'created keypair' }
159
+
160
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ec2_client).at_least(:once).and_return(mock_ec2_client)
161
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:iam_client).at_least(:once).and_return(mock_iam_client)
162
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
163
+
164
+ expect { subject.setup! }.to raise_error('created keypair')
165
+ end
111
166
  end
112
167
 
113
168
  context '#ssh' do
@@ -119,9 +174,11 @@ describe EcsDeployCli::Runner do
119
174
  expect(mock_ec2_client).to receive(:describe_instances)
120
175
  .with(instance_ids: ['i-123123'])
121
176
  .and_return(
122
- double(reservations: [
123
- double(instances: [double(public_dns_name: 'test.com')])
124
- ])
177
+ double(
178
+ reservations: [
179
+ double(instances: [double(public_dns_name: 'test.com')])
180
+ ]
181
+ )
125
182
  )
126
183
 
127
184
  expect(Process).to receive(:fork) do |&block|
@@ -156,8 +213,8 @@ describe EcsDeployCli::Runner do
156
213
  .with(instance_ids: ['i-321321'])
157
214
  .and_return(
158
215
  double(reservations: [
159
- double(instances: [double(public_dns_name: 'test.com')])
160
- ])
216
+ double(instances: [double(public_dns_name: 'test.com')])
217
+ ])
161
218
  )
162
219
 
163
220
  expect(Process).to receive(:fork) do |&block|
@@ -195,16 +252,32 @@ describe EcsDeployCli::Runner do
195
252
  subject.run_task!('yourproject-cron', launch_type: 'FARGATE', security_groups: [], subnets: [])
196
253
  end
197
254
 
198
- it '#update_crons!' do
199
- mock_ecs_client.stub_responses(:register_task_definition, { task_definition: { family: 'some', revision: 1, task_definition_arn: 'arn:task:eu-central-1:xxxx' } })
255
+ context '#update_crons!' do
256
+ it 'creates missing crons' do
257
+ mock_ecs_client.stub_responses(:register_task_definition, { task_definition: { family: 'some', revision: 1, task_definition_arn: 'arn:task:eu-central-1:xxxx' } })
200
258
 
201
- mock_cwe_client.stub_responses(:list_targets_by_rule, { targets: [{ id: '123', arn: 'arn:123' }] })
259
+ expect(mock_cwe_client).to receive(:list_targets_by_rule) do
260
+ raise Aws::CloudWatchEvents::Errors::ResourceNotFoundException.new(nil, 'some')
261
+ end
202
262
 
203
- expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
204
- expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
205
- expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwe_client).at_least(:once).and_return(mock_cwe_client)
263
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
264
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
265
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwe_client).at_least(:once).and_return(mock_cwe_client)
266
+
267
+ subject.update_crons!
268
+ end
269
+
270
+ it 'updates existing crons' do
271
+ mock_ecs_client.stub_responses(:register_task_definition, { task_definition: { family: 'some', revision: 1, task_definition_arn: 'arn:task:eu-central-1:xxxx' } })
206
272
 
207
- subject.update_crons!
273
+ mock_cwe_client.stub_responses(:list_targets_by_rule, { targets: [{ id: '123', arn: 'arn:123' }] })
274
+
275
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
276
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
277
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwe_client).at_least(:once).and_return(mock_cwe_client)
278
+
279
+ subject.update_crons!
280
+ end
208
281
  end
209
282
 
210
283
  it '#update_services!' do
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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mònade
@@ -115,6 +115,20 @@ dependencies:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
117
  version: '1'
118
+ - !ruby/object:Gem::Dependency
119
+ name: aws-sdk-iam
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1'
125
+ type: :runtime
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1'
118
132
  - !ruby/object:Gem::Dependency
119
133
  name: colorize
120
134
  requirement: !ruby/object:Gem::Requirement
@@ -251,22 +265,22 @@ required_rubygems_version: !ruby/object:Gem::Requirement
251
265
  - !ruby/object:Gem::Version
252
266
  version: '0'
253
267
  requirements: []
254
- rubygems_version: 3.2.8
268
+ rubygems_version: 3.2.7
255
269
  signing_key:
256
270
  specification_version: 4
257
271
  summary: A command line interface to make ECS deployments easier
258
272
  test_files:
259
- - spec/ecs_deploy_cli/cli_spec.rb
260
- - spec/ecs_deploy_cli/dsl/cluster_spec.rb
261
- - spec/ecs_deploy_cli/dsl/container_spec.rb
262
- - spec/ecs_deploy_cli/dsl/cron_spec.rb
263
- - spec/ecs_deploy_cli/dsl/parser_spec.rb
264
- - spec/ecs_deploy_cli/dsl/service_spec.rb
265
- - spec/ecs_deploy_cli/dsl/task_spec.rb
266
- - spec/ecs_deploy_cli/runner_spec.rb
267
- - spec/ecs_deploy_cli/runners/base_spec.rb
268
273
  - spec/spec_helper.rb
269
274
  - spec/support/ECSFile
270
275
  - spec/support/ECSFile.minimal
271
- - spec/support/env_file.ext.yml
272
276
  - spec/support/env_file.yml
277
+ - spec/support/env_file.ext.yml
278
+ - spec/ecs_deploy_cli/runner_spec.rb
279
+ - spec/ecs_deploy_cli/cli_spec.rb
280
+ - spec/ecs_deploy_cli/runners/base_spec.rb
281
+ - spec/ecs_deploy_cli/dsl/task_spec.rb
282
+ - spec/ecs_deploy_cli/dsl/parser_spec.rb
283
+ - spec/ecs_deploy_cli/dsl/service_spec.rb
284
+ - spec/ecs_deploy_cli/dsl/cron_spec.rb
285
+ - spec/ecs_deploy_cli/dsl/cluster_spec.rb
286
+ - spec/ecs_deploy_cli/dsl/container_spec.rb