ecs_deploy_cli 0.2.2 → 0.5.1

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.
@@ -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.2'
4
+ VERSION = '0.5.1'
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
@@ -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!
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'aws-sdk-cloudwatchevents'
5
+ require 'aws-sdk-cloudwatchlogs'
6
+ require 'aws-sdk-ec2'
7
+ require 'aws-sdk-ssm'
8
+ require 'aws-sdk-cloudformation'
9
+
10
+ describe EcsDeployCli::Runners::Base do
11
+ let(:parser) { EcsDeployCli::DSL::Parser.load('spec/support/ECSFile') }
12
+ subject { described_class.new(parser) }
13
+ let(:mock_cf_client) { Aws::CloudFormation::Client.new(stub_responses: true) }
14
+ let(:mock_ssm_client) { Aws::SSM::Client.new(stub_responses: true) }
15
+ let(:mock_ecs_client) { Aws::ECS::Client.new(stub_responses: true) }
16
+ let(:mock_ec2_client) { Aws::EC2::Client.new(stub_responses: true) }
17
+ let(:mock_cwl_client) { Aws::CloudWatchLogs::Client.new(stub_responses: true) }
18
+ let(:mock_cwe_client) do
19
+ Aws::CloudWatchEvents::Client.new(stub_responses: true)
20
+ end
21
+
22
+ around(:each) do |example|
23
+ ENV['AWS_PROFILE_ID'] = '123123123'
24
+ ENV['AWS_REGION'] = 'us-east-1'
25
+ example.run
26
+ ENV['AWS_PROFILE_ID'] = nil
27
+ ENV['AWS_REGION'] = nil
28
+ end
29
+
30
+ context '#update_task' do
31
+ it 'creates cloud watch logs if missing' do
32
+ _, tasks, = parser.resolve
33
+
34
+ expect(mock_cwl_client).to receive(:describe_log_groups).at_least(:once).and_return({ log_groups: [] })
35
+ expect(mock_cwl_client).to receive(:create_log_group).at_least(:once)
36
+ expect(mock_ecs_client).to receive(:register_task_definition).at_least(:once).and_return({ task_definition: { family: 'some', revision: '1' } })
37
+
38
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
39
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
40
+
41
+ subject.update_task(tasks.values.first)
42
+ end
43
+
44
+ it 'creates no cloudwatch log group if is already there' do
45
+ _, tasks, = parser.resolve
46
+
47
+ expect(mock_cwl_client).to receive(:describe_log_groups).at_least(:once).and_return({ log_groups: [{}] })
48
+ expect(mock_cwl_client).not_to receive(:create_log_group)
49
+ expect(mock_ecs_client).to receive(:register_task_definition).at_least(:once).and_return({ task_definition: { family: 'some', revision: '1' } })
50
+
51
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:cwl_client).at_least(:once).and_return(mock_cwl_client)
52
+ expect_any_instance_of(EcsDeployCli::Runners::Base).to receive(:ecs_client).at_least(:once).and_return(mock_ecs_client)
53
+
54
+ subject.update_task(tasks.values.first)
55
+ end
56
+ end
57
+ end
data/spec/support/ECSFile CHANGED
@@ -4,7 +4,14 @@ aws_region ENV.fetch('AWS_REGION', 'eu-central-1')
4
4
  aws_profile_id ENV['AWS_PROFILE_ID']
5
5
 
6
6
  # Defining the cluster name
7
- cluster 'yourproject-cluster'
7
+ cluster 'yourproject-cluster' do
8
+ instance_type 't3.small'
9
+ keypair_name 'test'
10
+
11
+ vpc do
12
+ availability_zones 'eu-central-1a', 'eu-central-1b', 'eu-central-1c'
13
+ end
14
+ end
8
15
 
9
16
  # This is used as a template for the next two containers, it will not be used inside a task
10
17
  container :base_container do
@@ -49,6 +56,11 @@ end
49
56
  # The main service
50
57
  service :'yourproject-service' do
51
58
  task :yourproject
59
+ load_balancer :'yourproject-load-balancer' do
60
+ target_group_arn 'loader-target-group/123abc'
61
+ container_name :web
62
+ container_port 80
63
+ end
52
64
  end
53
65
 
54
66
  # A task for cron jobs