ecs_deploy_cli 0.2.1 → 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.
@@ -3,29 +3,63 @@
3
3
  module EcsDeployCli
4
4
  module Runners
5
5
  class SSH < Base
6
- def run!
7
- instance_ids = load_container_instances
8
- EcsDeployCli.logger.info "Found instances: #{instance_ids.join(', ')}"
6
+ def run!(params = {})
7
+ instance_ids = load_container_instances(params)
9
8
 
10
- dns_name = load_dns_name_from_instance_ids(instance_ids)
9
+ instance_id = choose_instance_id(instance_ids)
10
+ dns_name = load_dns_name_from_instance_id(instance_id)
11
11
  run_ssh(dns_name)
12
12
  end
13
13
 
14
14
  private
15
15
 
16
- def load_dns_name_from_instance_ids(instance_ids)
16
+ def choose_instance_id(instance_ids)
17
+ raise 'No instance found' if instance_ids.empty?
18
+ return instance_ids[0] if instance_ids.length == 1
19
+
20
+ instances_selection_text = instance_ids.map.with_index do |instance, index|
21
+ "#{index + 1}) #{instance}"
22
+ end.join("\n")
23
+
24
+ EcsDeployCli.logger.info(
25
+ "Found #{instance_ids.count} instances:\n#{instances_selection_text}\nSelect which one you want to access:"
26
+ )
27
+
28
+ index = select_index_from_array(instance_ids, retry_message: 'Invalid option. Select which one you want to access:')
29
+
30
+ instance_ids[index]
31
+ end
32
+
33
+ def select_index_from_array(array, retry_message:)
34
+ while (index = STDIN.gets.chomp)
35
+ if index =~ /\A[1-9][0-9]*\Z/ && (index.to_i - 1) < array.count
36
+ index = index.to_i - 1
37
+ break
38
+ end
39
+
40
+ EcsDeployCli.logger.info(retry_message)
41
+ end
42
+ index
43
+ end
44
+
45
+ def load_dns_name_from_instance_id(instance_id)
17
46
  response = ec2_client.describe_instances(
18
- instance_ids: instance_ids
47
+ instance_ids: [instance_id]
19
48
  )
20
49
 
21
50
  response.reservations[0].instances[0].public_dns_name
22
51
  end
23
52
 
24
- def load_container_instances
25
- instances = ecs_client.list_container_instances(
26
- cluster: config[:cluster]
27
- ).to_h[:container_instance_arns]
53
+ def load_container_instances(params = {})
54
+ task_arns = ecs_client.list_tasks(
55
+ **params.merge(cluster: config[:cluster])
56
+ ).to_h[:task_arns]
57
+
58
+ tasks = ecs_client.describe_tasks(
59
+ tasks: task_arns, cluster: config[:cluster]
60
+ ).to_h[:tasks]
28
61
 
62
+ instances = tasks.map { |task| task[:container_instance_arn] }.uniq
29
63
  response = ecs_client.describe_container_instances(
30
64
  cluster: config[:cluster],
31
65
  container_instances: instances
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EcsDeployCli
4
+ module Runners
5
+ class Status < Base
6
+ def run!(service)
7
+ services, = @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
+ puts ecs_client.describe_service(
16
+ cluster: config[:cluster],
17
+ service: service_name
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ 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.2.1'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -48,6 +48,14 @@ describe EcsDeployCli::CLI do
48
48
  described_class.start(['run-task', 'yourproject', '--subnets', 'subnet-123123', '--file', 'spec/support/ECSFile'])
49
49
  end
50
50
 
51
+ it 'runs setup' do
52
+ expect(runner).to receive(:setup!)
53
+ described_class.no_commands do
54
+ expect_any_instance_of(described_class).to receive(:runner).at_least(:once).and_return(runner)
55
+ end
56
+ expect { described_class.start(['setup', '--file', 'spec/support/ECSFile']) }.to output(/[WARNING]/).to_stdout
57
+ end
58
+
51
59
  it 'runs deploy' do
52
60
  expect(runner).to receive(:update_crons!)
53
61
  expect(runner).to receive(:update_services!).with(timeout: 500)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe EcsDeployCli::DSL::Cluster do
6
+ context 'defines cluster data' do
7
+ subject { described_class.new('mydata-cluster', { aws_profile_id: '123123', aws_region: 'eu-central-1' }) }
8
+
9
+ it '#vpc' do
10
+ subject.instances_count 1
11
+ subject.instance_type 't2.small'
12
+ subject.keypair_name 'test'
13
+
14
+ subject.vpc do
15
+ cidr '11.0.0.0/16'
16
+ subnet1 '11.0.0.0/24'
17
+ subnet2 '11.0.1.0/24'
18
+ subnet3 '11.0.2.0/24'
19
+ subnet_ids 'subnet-123', 'subnet-321', 'subnet-333'
20
+
21
+ availability_zones 'eu-central-1a', 'eu-central-1b', 'eu-central-1c'
22
+ end
23
+
24
+ expect(subject.as_definition).to eq(
25
+ {
26
+ device_name: '/dev/xvda',
27
+ ebs_volume_size: 22,
28
+ ebs_volume_type: 'gp2',
29
+ instances_count: 1,
30
+ instance_type: 't2.small',
31
+ keypair_name: 'test',
32
+ name: 'mydata-cluster',
33
+ root_device_name: '/dev/xvdcz',
34
+ root_ebs_volume_size: 30,
35
+ vpc: {
36
+ availability_zones: 'eu-central-1a,eu-central-1b,eu-central-1c',
37
+ cidr: '11.0.0.0/16',
38
+ id: nil,
39
+ subnet1: '11.0.0.0/24',
40
+ subnet2: '11.0.1.0/24',
41
+ subnet3: '11.0.2.0/24',
42
+ subnet_ids: 'subnet-123,subnet-321,subnet-333'
43
+ }
44
+ }
45
+ )
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe EcsDeployCli::DSL::Container do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe EcsDeployCli::DSL::Cron do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe EcsDeployCli::DSL::Parser do
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe EcsDeployCli::DSL::Service do
6
+ context 'defines service data' do
7
+ subject { described_class.new('test', { aws_profile_id: '123123', aws_region: 'eu-central-1' }) }
8
+
9
+ it 'has the correct name' do
10
+ expect(subject.as_definition({})[:service]).to eq('test')
11
+ end
12
+
13
+ it '#load_balancer' do
14
+ subject.load_balancer :'yourproject-load-balancer' do
15
+ target_group_arn 'loader-target-group/123abc'
16
+ container_name :web
17
+ container_port 80
18
+ end
19
+
20
+ expect(subject.as_definition({})[:load_balancers]).to eq(
21
+ [
22
+ {
23
+ container_name: :web,
24
+ container_port: 80,
25
+ target_group_arn: 'arn:aws:elasticloadbalancing:eu-central-1:123123:targetgroup/loader-target-group/123abc'
26
+ }
27
+ ]
28
+ )
29
+ end
30
+ end
31
+ end
@@ -2,14 +2,22 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'aws-sdk-cloudwatchevents'
5
+ require 'aws-sdk-cloudwatchlogs'
5
6
  require 'aws-sdk-ec2'
7
+ require 'aws-sdk-ssm'
8
+ require 'aws-sdk-cloudformation'
9
+ require 'aws-sdk-iam'
6
10
 
7
11
  describe EcsDeployCli::Runner do
8
12
  context 'defines task data' do
9
13
  let(:parser) { EcsDeployCli::DSL::Parser.load('spec/support/ECSFile') }
10
14
  subject { described_class.new(parser) }
15
+ let(:mock_iam_client) { Aws::IAM::Client.new(stub_responses: true) }
16
+ let(:mock_cf_client) { Aws::CloudFormation::Client.new(stub_responses: true) }
17
+ let(:mock_ssm_client) { Aws::SSM::Client.new(stub_responses: true) }
11
18
  let(:mock_ecs_client) { Aws::ECS::Client.new(stub_responses: true) }
12
19
  let(:mock_ec2_client) { Aws::EC2::Client.new(stub_responses: true) }
20
+ let(:mock_cwl_client) { Aws::CloudWatchLogs::Client.new(stub_responses: true) }
13
21
  let(:mock_cwe_client) do
14
22
  Aws::CloudWatchEvents::Client.new(stub_responses: true)
15
23
  end
@@ -84,28 +92,142 @@ describe EcsDeployCli::Runner do
84
92
  ENV['AWS_REGION'] = nil
85
93
  end
86
94
 
87
- it '#ssh' do
88
- expect(mock_ecs_client).to receive(:list_container_instances).and_return({ container_instance_arns: ['arn:123123'] })
89
- expect(mock_ecs_client).to receive(:describe_container_instances).and_return(double(container_instances: [double(ec2_instance_id: 'i-123123')]))
90
-
91
- expect(mock_ec2_client).to receive(:describe_instances)
92
- .with(instance_ids: ['i-123123'])
93
- .and_return(
94
- double(reservations: [
95
- double(instances: [double(public_dns_name: 'test.com')])
96
- ])
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
+ }
97
105
  )
106
+ end
107
+
108
+ it 'setups the cluster correctly' do
109
+ expect(mock_ec2_client).to receive(:describe_key_pairs).and_return(key_pairs: [{ key_id: 'some' }])
110
+
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)
98
114
 
99
- expect(Process).to receive(:fork) do |&block|
100
- block.call
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!
101
123
  end
102
- expect(Process).to receive(:wait)
103
124
 
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)
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
152
+
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
166
+ end
167
+
168
+ context '#ssh' do
169
+ it 'runs ssh on a single container instance' do
170
+ expect(mock_ecs_client).to receive(:list_tasks).and_return({ task_arns: ['arn:123123'] })
171
+ expect(mock_ecs_client).to receive(:describe_tasks).and_return({ tasks: [{ container_instance_arn: 'arn:instance:123123' }] })
172
+ expect(mock_ecs_client).to receive(:describe_container_instances).and_return(double(container_instances: [double(ec2_instance_id: 'i-123123')]))
173
+
174
+ expect(mock_ec2_client).to receive(:describe_instances)
175
+ .with(instance_ids: ['i-123123'])
176
+ .and_return(
177
+ double(
178
+ reservations: [
179
+ double(instances: [double(public_dns_name: 'test.com')])
180
+ ]
181
+ )
182
+ )
183
+
184
+ expect(Process).to receive(:fork) do |&block|
185
+ block.call
186
+ end
187
+ expect(Process).to receive(:wait)
188
+
189
+ expect_any_instance_of(EcsDeployCli::Runners::SSH).to receive(:exec).with('ssh ec2-user@test.com')
190
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
191
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ec2_client).at_least(:once).and_return(mock_ec2_client)
192
+
193
+ subject.ssh
194
+ end
195
+
196
+ it 'prompts which instance if there are multiple ones' do
197
+ expect(mock_ecs_client).to receive(:list_tasks).and_return({ task_arns: ['arn:123123', 'arn:321321'] })
198
+ expect(mock_ecs_client).to receive(:describe_tasks).and_return(
199
+ {
200
+ tasks: [
201
+ { container_instance_arn: 'arn:instance:123123' },
202
+ { container_instance_arn: 'arn:instance:321321' }
203
+ ]
204
+ }
205
+ )
206
+ expect(mock_ecs_client).to receive(:describe_container_instances).and_return(
207
+ double(container_instances: [double(ec2_instance_id: 'i-123123'), double(ec2_instance_id: 'i-321321')])
208
+ )
209
+
210
+ expect(STDIN).to receive(:gets).and_return('2')
107
211
 
108
- subject.ssh
212
+ expect(mock_ec2_client).to receive(:describe_instances)
213
+ .with(instance_ids: ['i-321321'])
214
+ .and_return(
215
+ double(reservations: [
216
+ double(instances: [double(public_dns_name: 'test.com')])
217
+ ])
218
+ )
219
+
220
+ expect(Process).to receive(:fork) do |&block|
221
+ block.call
222
+ end
223
+ expect(Process).to receive(:wait)
224
+
225
+ expect_any_instance_of(EcsDeployCli::Runners::SSH).to receive(:exec).with('ssh ec2-user@test.com')
226
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
227
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ec2_client).at_least(:once).and_return(mock_ec2_client)
228
+
229
+ subject.ssh
230
+ end
109
231
  end
110
232
 
111
233
  it '#diff' do
@@ -124,20 +246,38 @@ describe EcsDeployCli::Runner do
124
246
 
125
247
  mock_cwe_client.stub_responses(:run_task)
126
248
 
249
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
127
250
  expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
128
251
 
129
252
  subject.run_task!('yourproject-cron', launch_type: 'FARGATE', security_groups: [], subnets: [])
130
253
  end
131
254
 
132
- it '#update_crons!' do
133
- 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' } })
134
258
 
135
- 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
136
262
 
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)
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' } })
139
272
 
140
- 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
141
281
  end
142
282
 
143
283
  it '#update_services!' do
@@ -149,6 +289,7 @@ describe EcsDeployCli::Runner do
149
289
  )
150
290
  expect(mock_ecs_client).to receive(:wait_until)
151
291
 
292
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
152
293
  expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
153
294
 
154
295
  subject.update_services!