Completed
Push — master ( 553be1...b5a472 )
by Fabian
01:55
created

inspect_errors()   C

Complexity

Conditions 7

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
c 1
b 0
f 0
dl 0
loc 42
ccs 24
cts 24
cp 1
crap 7
rs 5.5
1 1
from __future__ import print_function, absolute_import
2
3 1
from os import getenv
4 1
from time import sleep
5
6 1
import click
7 1
import getpass
8 1
from datetime import datetime, timedelta
9
10 1
from ecs_deploy.ecs import DeployAction, ScaleAction, RunAction, EcsClient, \
11
    TaskPlacementError
12 1
from ecs_deploy.newrelic import Deployment
13
14
15 1
@click.group()
16
def ecs():  # pragma: no cover
17
    pass
18
19
20 1
def get_client(access_key_id, secret_access_key, region, profile):
21 1
    return EcsClient(access_key_id, secret_access_key, region, profile)
22
23
24 1
@click.command()
25 1
@click.argument('cluster')
26 1
@click.argument('service')
27 1
@click.option('-t', '--tag',
28
              help='Changes the tag for ALL container images')
29 1
@click.option('-i', '--image', type=(str, str), multiple=True,
30
              help='Overwrites the image for a container: '
31
                   '<container> <image>')
32 1
@click.option('-c', '--command', type=(str, str), multiple=True,
33
              help='Overwrites the command in a container: '
34
                   '<container> <command>')
35 1
@click.option('-e', '--env', type=(str, str, str), multiple=True,
36
              help='Adds or changes an environment variable: '
37
                   '<container> <name> <value>')
38 1
@click.option('-r', '--role', type=str,
39
              help='Sets the task\'s role ARN: <task role ARN>')
40 1
@click.option('--task', type=str,
41
              help='Task definition to be deployed. Can be a task ARN '
42
                   'or a task family with optional revision')
43 1
@click.option('--region', required=False,
44
              help='AWS region (e.g. eu-central-1)')
45 1
@click.option('--access-key-id', required=False,
46
              help='AWS access key id')
47 1
@click.option('--secret-access-key', required=False,
48
              help='AWS secret access key')
49 1
@click.option('--profile', required=False,
50
              help='AWS configuration profile name')
51 1
@click.option('--timeout', required=False, default=300, type=int,
52
              help='Amount of seconds to wait for deployment before '
53
                   'command fails (default: 300)')
54 1
@click.option('--ignore-warnings', is_flag=True,
55
              help='Do not fail deployment on warnings (port already in use '
56
                   'or insufficient memory/CPU)')
57 1
@click.option('--newrelic-apikey', required=False,
58
              help='New Relic API Key for recording the deployment')
59 1
@click.option('--newrelic-appid', required=False,
60
              help='New Relic App ID for recording the deployment')
61 1
@click.option('--comment', required=False,
62
              help='Description/comment for recording the deployment')
63 1
@click.option('--user', required=False,
64
              help='User who executes the deployment (used for recording)')
65
def deploy(cluster, service, tag, image, command, env, role, task, region,
66
           access_key_id, secret_access_key, profile, timeout, newrelic_apikey,
67
           newrelic_appid, comment, user, ignore_warnings):
68
    """
69
    Redeploy or modify a service.
70
71
    \b
72
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
73
    SERVICE is the name of your service (e.g. 'my-app') within ECS.
74
75
    When not giving any other options, the task definition will not be changed.
76
    It will just be duplicated, so that all container images will be pulled
77
    and redeployed.
78
    """
79
80 1
    try:
81 1
        client = get_client(access_key_id, secret_access_key, region, profile)
82 1
        deployment = DeployAction(client, cluster, service)
83
84 1
        if task:
85 1
            td = deployment.get_task_definition(task)
86 1
            click.secho('Deploying based on task definition: %s' % task)
87
        else:
88 1
            td = deployment.get_current_task_definition(deployment.service)
89
90 1
        td.set_images(tag, **{key: value for (key, value) in image})
91 1
        td.set_commands(**{key: value for (key, value) in command})
92 1
        td.set_environment(env)
93 1
        td.set_role_arn(role)
94 1
        print_diff(td)
95
96 1
        click.secho('Creating new task definition revision')
97 1
        new_td = deployment.update_task_definition(td)
98
99 1
        click.secho(
100
            'Successfully created revision: %d' % new_td.revision,
101
            fg='green'
102
        )
103 1
        click.secho(
104
            'Successfully deregistered revision: %d\n' % td.revision,
105
            fg='green'
106
        )
107
108 1
        record_deployment(tag, newrelic_apikey, newrelic_appid, comment, user)
109
110 1
        click.secho('Updating service')
111 1
        deployment.deploy(new_td)
112 1
        click.secho('Successfully changed task definition to: %s:%s\n' %
113
                    (new_td.family, new_td.revision), fg='green')
114
115 1
        wait_for_finish(
116
            action=deployment,
117
            timeout=timeout,
118
            title='Deploying task definition',
119
            success_message='Deployment successful',
120
            failure_message='Deployment failed',
121
            ignore_warnings=ignore_warnings
122
        )
123
124 1
    except Exception as e:
125 1
        click.secho('%s\n' % str(e), fg='red', err=True)
126 1
        exit(1)
127
128
129 1
@click.command()
130 1
@click.argument('cluster')
131 1
@click.argument('service')
132 1
@click.argument('desired_count', type=int)
133 1
@click.option('--region',
134
              help='AWS region (e.g. eu-central-1)')
135 1
@click.option('--access-key-id',
136
              help='AWS access key id')
137 1
@click.option('--secret-access-key',
138
              help='AWS secret access key')
139 1
@click.option('--profile',
140
              help='AWS configuration profile name')
141 1
@click.option('--timeout', default=300, type=int,
142
              help='AWS configuration profile')
143 1
@click.option('--ignore-warnings', is_flag=True,
144
              help='Do not fail deployment on warnings (port already in use '
145
                   'or insufficient memory/CPU)')
146
def scale(cluster, service, desired_count, access_key_id, secret_access_key,
147
          region, profile, timeout, ignore_warnings):
148
    """
149
    Scale a service up or down.
150
151
    \b
152
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
153
    SERVICE is the name of your service (e.g. 'my-app') within ECS.
154
    DESIRED_COUNT is the number of tasks your service should run.
155
    """
156 1
    try:
157 1
        client = get_client(access_key_id, secret_access_key, region, profile)
158 1
        scaling = ScaleAction(client, cluster, service)
159 1
        click.secho('Updating service')
160 1
        scaling.scale(desired_count)
161 1
        click.secho(
162
            'Successfully changed desired count to: %s\n' % desired_count,
163
            fg='green'
164
        )
165 1
        wait_for_finish(
166
            action=scaling,
167
            timeout=timeout,
168
            title='Scaling service',
169
            success_message='Scaling successful',
170
            failure_message='Scaling failed',
171
            ignore_warnings=ignore_warnings
172
        )
173
174 1
    except Exception as e:
175 1
        click.secho('%s\n' % str(e), fg='red', err=True)
176 1
        exit(1)
177
178
179 1
@click.command()
180 1
@click.argument('cluster')
181 1
@click.argument('task')
182 1
@click.argument('count', required=False, default=1)
183 1
@click.option('-c', '--command', type=(str, str), multiple=True,
184
              help='Overwrites the command in a container: '
185
                   '<container> <command>')
186 1
@click.option('-e', '--env', type=(str, str, str), multiple=True,
187
              help='Adds or changes an environment variable: '
188
                   '<container> <name> <value>')
189 1
@click.option('--region',
190
              help='AWS region (e.g. eu-central-1)')
191 1
@click.option('--access-key-id',
192
              help='AWS access key id')
193 1
@click.option('--secret-access-key',
194
              help='AWS secret access key')
195 1
@click.option('--profile',
196
              help='AWS configuration profile name')
197
def run(cluster, task, count, command, env, region, access_key_id,
198
        secret_access_key, profile):
199
    """
200
    Run a one-off task.
201
202
    \b
203
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
204
    TASK is the name of your task definition (e.g. 'my-task') within ECS.
205
    COMMAND is the number of tasks your service should run.
206
    """
207 1
    try:
208 1
        client = get_client(access_key_id, secret_access_key, region, profile)
209 1
        action = RunAction(client, cluster)
210
211 1
        td = action.get_task_definition(task)
212 1
        td.set_commands(**{key: value for (key, value) in command})
213 1
        td.set_environment(env)
214 1
        print_diff(td, 'Using task definition: %s' % task)
215
216 1
        action.run(td, count, 'ECS Deploy')
217
218 1
        click.secho(
219
            'Successfully started %d instances of task: %s' % (
220
                len(action.started_tasks),
221
                td.family_revision
222
            ),
223
            fg='green'
224
        )
225
226 1
        for started_task in action.started_tasks:
227 1
            click.secho('- %s' % started_task['taskArn'], fg='green')
228 1
        click.secho(' ')
229
230 1
    except Exception as e:
231 1
        click.secho('%s\n' % str(e), fg='red', err=True)
232 1
        exit(1)
233
234
235 1
def wait_for_finish(action, timeout, title, success_message, failure_message,
236
                    ignore_warnings):
237 1
    click.secho(title, nl=False)
238 1
    waiting = True
239 1
    waiting_timeout = datetime.now() + timedelta(seconds=timeout)
240 1
    service = action.get_service()
241 1
    inspected_until = None
242 1
    while waiting and datetime.now() < waiting_timeout:
243 1
        click.secho('.', nl=False)
244 1
        sleep(1)
245 1
        service = action.get_service()
246 1
        inspected_until = inspect_errors(
247
            service=service,
248
            failure_message=failure_message,
249
            ignore_warnings=ignore_warnings,
250
            since=inspected_until,
251
            timeout=False
252
        )
253 1
        waiting = not action.is_deployed(service)
254
255 1
    inspect_errors(
256
        service=service,
257
        failure_message=failure_message,
258
        ignore_warnings=ignore_warnings,
259
        since=inspected_until,
260
        timeout=waiting
261
    )
262
263 1
    click.secho('\n%s\n' % success_message, fg='green')
264
265
266 1
def record_deployment(revision, api_key, app_id, comment, user):
267 1
    api_key = getenv('NEW_RELIC_API_KEY', api_key)
268 1
    app_id = getenv('NEW_RELIC_APP_ID', app_id)
269
270 1
    if not revision or not api_key or not app_id:
271 1
        return False
272
273 1
    user = user or getpass.getuser()
274
275 1
    click.secho('Recording deployment in New Relic', nl=False)
276
277 1
    deployment = Deployment(api_key, app_id, user)
278 1
    deployment.deploy(revision, '', comment)
279
280 1
    click.secho('\nDone\n', fg='green')
281
282 1
    return True
283
284
285 1
def print_diff(task_definition, title='Updating task definition'):
286 1
    if task_definition.diff:
287 1
        click.secho(title)
288 1
        for diff in task_definition.diff:
289 1
            click.secho(str(diff), fg='blue')
290 1
        click.secho('')
291
292
293 1
def inspect_errors(service, failure_message, ignore_warnings, since, timeout):
294 1
    error = False
295 1
    last_error_timestamp = since
296
297 1
    warnings = service.get_warnings(since)
298 1
    for timestamp in warnings:
299 1
        message = warnings[timestamp]
300 1
        click.secho('')
301 1
        if ignore_warnings:
302 1
            last_error_timestamp = timestamp
303 1
            click.secho(
304
                text='%s\nWARNING: %s' % (timestamp, message),
305
                fg='yellow',
306
                err=False
307
            )
308 1
            click.secho('Continuing.', nl=False)
309
        else:
310 1
            click.secho(
311
                text='%s\nERROR: %s\n' % (timestamp, message),
312
                fg='red',
313
                err=True
314
            )
315 1
            error = True
316
317 1
    if service.older_errors:
318 1
        click.secho('')
319 1
        click.secho('Older errors', fg='yellow', err=True)
320 1
        for timestamp in service.older_errors:
321 1
            click.secho(
322
                text='%s\n%s\n' % (timestamp, service.older_errors[timestamp]),
323
                fg='yellow',
324
                err=True
325
            )
326
327 1
    if timeout:
328 1
        error = True
329 1
        failure_message += ' (timeout)'
330
331 1
    if error:
332 1
        raise TaskPlacementError(failure_message)
333
334 1
    return last_error_timestamp
335
336
337 1
ecs.add_command(deploy)
338 1
ecs.add_command(scale)
339 1
ecs.add_command(run)
340
341
if __name__ == '__main__':  # pragma: no cover
342
    ecs()
343