diff --git a/README.rst b/README.rst index 5b0f619..f94e5af 100644 --- a/README.rst +++ b/README.rst @@ -71,7 +71,10 @@ For detailed information about the available actions, arguments and options, run Examples -------- -All examples assume, that authentication has already been configured. +All examples assume, that authentication has already been configured. + +Deployment +---------- Simple Redeploy =============== @@ -134,6 +137,18 @@ To change the command of a specific container, run the following command:: This will modify the **webserver** container and change its command to "nginx". + +Set a task role +=============== +To change or set the role, the service's task should run as, use the following command:: + + $ ecs deploy my-cluster my-service -r arn:aws:iam::123456789012:role/MySpecialEcsTaskRole + +This will set the task role to "MySpecialEcsTaskRole". + +Scaling +------- + Scale a service =============== To change the number of running tasks and scale a service up and down, run this command:: @@ -141,6 +156,9 @@ To change the number of running tasks and scale a service up and down, run this $ ecs scale my-cluster my-service 4 +Running a Task +-------------- + Run a one-off task ================== To run a one-off task, based on an existing task-definition, run this command:: diff --git a/ecs_deploy/cli.py b/ecs_deploy/cli.py index 6178bc9..23dbc54 100644 --- a/ecs_deploy/cli.py +++ b/ecs_deploy/cli.py @@ -27,6 +27,7 @@ def get_client(access_key_id, secret_access_key, region, profile): @click.option('-i', '--image', type=(str, str), multiple=True, help='Overwrites the image for a container: ') @click.option('-c', '--command', type=(str, str), multiple=True, help='Overwrites the command in a container: ') @click.option('-e', '--env', type=(str, str, str), multiple=True, help='Adds or changes an environment variable: ') +@click.option('-r', '--role', type=str, help='Sets the task\'s role ARN: ') @click.option('--region', required=False, help='AWS region') @click.option('--access-key-id', required=False, help='AWS access key id') @click.option('--secret-access-key', required=False, help='AWS secret access yey') @@ -36,7 +37,7 @@ def get_client(access_key_id, secret_access_key, region, profile): @click.option('--newrelic-appid', required=False, help='New Relic App ID for recording the deployment') @click.option('--comment', required=False, help='Description/comment for recording the deployment') @click.option('--user', required=False, help='User who executes the deployment (used for recording)') -def deploy(cluster, service, tag, image, command, env, access_key_id, secret_access_key, region, profile, timeout, +def deploy(cluster, service, tag, image, command, env, role, access_key_id, secret_access_key, region, profile, timeout, newrelic_apikey, newrelic_appid, comment, user): """ Redeploy or modify a service. @@ -57,6 +58,7 @@ def deploy(cluster, service, tag, image, command, env, access_key_id, secret_acc task_definition.set_images(tag, **{key: value for (key, value) in image}) task_definition.set_commands(**{key: value for (key, value) in command}) task_definition.set_environment(env) + task_definition.set_role_arn(role) print_diff(task_definition) click.secho('Creating new task definition revision') diff --git a/ecs_deploy/ecs.py b/ecs_deploy/ecs.py index 4aadc09..77803b2 100644 --- a/ecs_deploy/ecs.py +++ b/ecs_deploy/ecs.py @@ -25,8 +25,13 @@ def list_tasks(self, cluster_name, service_name): def describe_tasks(self, cluster_name, task_arns): return self.boto.describe_tasks(cluster=cluster_name, tasks=task_arns) - def register_task_definition(self, family, containers, volumes): - return self.boto.register_task_definition(family=family, containerDefinitions=containers, volumes=volumes) + def register_task_definition(self, family, containers, volumes, role_arn): + return self.boto.register_task_definition( + family=family, + containerDefinitions=containers, + volumes=volumes, + taskRoleArn=role_arn or '' + ) def deregister_task_definition(self, task_definition_arn): return self.boto.deregister_task_definition(taskDefinition=task_definition_arn) @@ -134,6 +139,10 @@ def arn(self): def family(self): return self.get(u'family') + @property + def role_arn(self): + return self.get(u'taskRoleArn') + @property def revision(self): return self.get(u'revision') @@ -216,6 +225,12 @@ def validate_container_options(self, **container_options): if container_name not in self.container_names: raise UnknownContainerError(u'Unknown container: %s' % container_name) + def set_role_arn(self, role_arn): + if role_arn: + diff = EcsTaskDefinitionDiff(None, u'role_arn', role_arn, self[u'taskRoleArn']) + self[u'taskRoleArn'] = role_arn + self._diff.append(diff) + class EcsTaskDefinitionDiff(object): def __init__(self, container, field, value, old_value): @@ -225,8 +240,20 @@ def __init__(self, container, field, value, old_value): self.old_value = old_value def __repr__(self): - return u"Changed %s of container '%s' to: %s (was: %s)" % \ - (self.field, self.container, dumps(self.value), dumps(self.old_value)) + if self.container: + return u"Changed %s of container '%s' to: %s (was: %s)" % ( + self.field, + self.container, + dumps(self.value), + dumps(self.old_value) + ) + else: + return u"Changed %s to: %s (was: %s)" % ( + self.field, + dumps(self.value), + dumps(self.old_value) + ) + class EcsAction(object): @@ -259,8 +286,12 @@ def get_task_definition(self, task_definition): return task_definition def update_task_definition(self, task_definition): - response = self._client.register_task_definition(task_definition.family, task_definition.containers, - task_definition.volumes) + response = self._client.register_task_definition( + task_definition.family, + task_definition.containers, + task_definition.volumes, + task_definition.role_arn + ) new_task_definition = EcsTaskDefinition(response[u'taskDefinition']) self._client.deregister_task_definition(task_definition.arn) return new_task_definition diff --git a/setup.py b/setup.py index 8199294..4293566 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ """ from setuptools import find_packages, setup -dependencies = ['click', 'botocore', 'boto3', 'future', 'requests'] +dependencies = ['click', 'botocore', 'boto3>=1.4.0', 'future', 'requests'] setup( name='ecs-deploy', diff --git a/tests/test_cli.py b/tests/test_cli.py index c2991d1..88591ba 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -69,6 +69,20 @@ def test_deploy(get_client, runner): assert u"Updating task definition" not in result.output +@patch('ecs_deploy.cli.get_client') +def test_deploy_with_role_arn(get_client, runner): + get_client.return_value = EcsTestClient('acces_key', 'secret_key') + result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '-r', 'arn:new:role')) + assert result.exit_code == 0 + assert not result.exception + assert u'Successfully created revision: 2' in result.output + assert u'Successfully deregistered revision: 1' in result.output + assert u'Successfully changed task definition to: test-task:2' in result.output + assert u'Deployment successful' in result.output + assert u"Updating task definition" in result.output + assert u"Changed role_arn to: \"arn:new:role\" (was: \"arn:test:role:1\")" in result.output + + @patch('ecs_deploy.cli.get_client') def test_deploy_new_tag(get_client, runner): get_client.return_value = EcsTestClient('acces_key', 'secret_key') diff --git a/tests/test_ecs.py b/tests/test_ecs.py index bce8f60..9a0b811 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -19,6 +19,7 @@ DESIRED_COUNT = 2 TASK_DEFINITION_FAMILY_1 = u'test-task' TASK_DEFINITION_REVISION_1 = 1 +TASK_DEFINITION_ROLE_ARN_1 = u'arn:test:role:1' TASK_DEFINITION_ARN_1 = u'arn:aws:ecs:eu-central-1:123456789012:task-definition/%s:%s' % (TASK_DEFINITION_FAMILY_1, TASK_DEFINITION_REVISION_1) TASK_DEFINITION_VOLUMES_1 = [] @@ -42,6 +43,7 @@ u'taskDefinitionArn': TASK_DEFINITION_ARN_1, u'family': TASK_DEFINITION_FAMILY_1, u'revision': TASK_DEFINITION_REVISION_1, + u'taskRoleArn': TASK_DEFINITION_ROLE_ARN_1, u'volumes': deepcopy(TASK_DEFINITION_VOLUMES_1), u'containerDefinitions': deepcopy(TASK_DEFINITION_CONTAINERS_1), } @@ -51,6 +53,7 @@ u'family': TASK_DEFINITION_FAMILY_2, u'revision': TASK_DEFINITION_REVISION_2, u'volumes': deepcopy(TASK_DEFINITION_VOLUMES_2), + u'taskRoleArn': '', u'containerDefinitions': deepcopy(TASK_DEFINITION_CONTAINERS_2), } @@ -423,9 +426,14 @@ def test_client_describe_tasks(client): def test_client_register_task_definition(client): containers = [{u'name': u'foo'}] volumes = [{u'foo': u'bar'}] - client.register_task_definition(u'family', containers, volumes) - client.boto.register_task_definition.assert_called_once_with(family=u'family', containerDefinitions=containers, - volumes=volumes) + role_arn = 'arn:test:role' + client.register_task_definition(u'family', containers, volumes, role_arn) + client.boto.register_task_definition.assert_called_once_with( + family=u'family', + containerDefinitions=containers, + volumes=volumes, + taskRoleArn=role_arn + ) def test_client_deregister_task_definition(client): @@ -522,9 +530,15 @@ def test_update_task_definition(client, task_definition): new_task_definition = action.update_task_definition(task_definition) assert isinstance(new_task_definition, EcsTaskDefinition) - client.register_task_definition.assert_called_once_with(task_definition.family, task_definition.containers, - task_definition.volumes) - client.deregister_task_definition.assert_called_once_with(task_definition.arn) + client.register_task_definition.assert_called_once_with( + task_definition.family, + task_definition.containers, + task_definition.volumes, + task_definition.role_arn, + ) + client.deregister_task_definition.assert_called_once_with( + task_definition.arn + ) @patch.object(EcsClient, '__init__') @@ -685,7 +699,7 @@ def list_tasks(self, cluster_name, service_name): def describe_tasks(self, cluster_name, task_arns): return deepcopy(RESPONSE_DESCRIBE_TASKS) - def register_task_definition(self, family, containers, volumes): + def register_task_definition(self, family, containers, volumes, role_arn): return deepcopy(RESPONSE_TASK_DEFINITION_2) def deregister_task_definition(self, task_definition_arn):