diff --git a/cli/dcoscli/marathon/main.py b/cli/dcoscli/marathon/main.py index 5e4848f..84cf57e 100644 --- a/cli/dcoscli/marathon/main.py +++ b/cli/dcoscli/marathon/main.py @@ -22,6 +22,7 @@ Usage: dcos marathon task show dcos marathon group add [] dcos marathon group list [--json] + dcos marathon group scale [--force] dcos marathon group show [--group-version=] dcos marathon group remove [--force] dcos marathon group update [--force] [...] @@ -96,6 +97,8 @@ Positional Arguments: stdin. The task id + + The factor to scale an application group by """ import json import os @@ -240,6 +243,11 @@ def _cmds(): arg_keys=['', '', '--force'], function=_group_update), + cmds.Command( + hierarchy=['marathon', 'group', 'scale'], + arg_keys=['', '', '--force'], + function=_group_scale), + cmds.Command( hierarchy=['marathon', 'about'], arg_keys=[], @@ -645,6 +653,25 @@ def _update(app_id, properties, force): return 0 +def _group_scale(group_id, scale_factor, force): + """ + :param group_id: the id of the group + :type group_id: str + :param scale_factor: scale factor for application group + :type scale_factor: str + :param force: whether to override running deployments + :type force: bool + :returns: process return code + :rtype: int + """ + + client = marathon.create_client() + scale_factor = util.parse_float(scale_factor) + deployment = client.scale_group(group_id, scale_factor, force) + emitter.publish('Created deployment {}'.format(deployment)) + return 0 + + def _validate_update(current_resource, properties, schema): """ Validate resource ("app" or "group") update diff --git a/cli/tests/data/help/marathon.txt b/cli/tests/data/help/marathon.txt index 4ef84ff..c63e917 100644 --- a/cli/tests/data/help/marathon.txt +++ b/cli/tests/data/help/marathon.txt @@ -22,6 +22,7 @@ Usage: dcos marathon task show dcos marathon group add [] dcos marathon group list [--json] + dcos marathon group scale [--force] dcos marathon group show [--group-version=] dcos marathon group remove [--force] dcos marathon group update [--force] [...] @@ -96,3 +97,5 @@ Positional Arguments: stdin. The task id + + The factor to scale an application group by diff --git a/cli/tests/data/marathon/groups/scale.json b/cli/tests/data/marathon/groups/scale.json new file mode 100644 index 0000000..e78f655 --- /dev/null +++ b/cli/tests/data/marathon/groups/scale.json @@ -0,0 +1,15 @@ +{ + "groups": [ + { + "apps": [ + { + "id": "sleep", + "cmd": "sleep 1000", + "instances": 1 + } + ], + "id": "sleep" + } + ], + "id": "scale-group" +} diff --git a/cli/tests/integrations/test_marathon.py b/cli/tests/integrations/test_marathon.py index f03c9ff..7807916 100644 --- a/cli/tests/integrations/test_marathon.py +++ b/cli/tests/integrations/test_marathon.py @@ -39,6 +39,7 @@ Usage: dcos marathon task show dcos marathon group add [] dcos marathon group list [--json] + dcos marathon group scale [--force] dcos marathon group show [--group-version=] dcos marathon group remove [--force] dcos marathon group update [--force] [...] @@ -113,6 +114,8 @@ Positional Arguments: stdin. The task id + + The factor to scale an application group by """ assert_command(['dcos', 'marathon', '--help'], stdout=stdout) diff --git a/cli/tests/integrations/test_marathon_groups.py b/cli/tests/integrations/test_marathon_groups.py index 1c4bfc9..783afaa 100644 --- a/cli/tests/integrations/test_marathon_groups.py +++ b/cli/tests/integrations/test_marathon_groups.py @@ -123,6 +123,56 @@ def test_update_missing_field(): "Possible properties are: ") +def test_scale_group(): + _deploy_group('tests/data/marathon/groups/scale.json') + returncode, stdout, stderr = exec_command(['dcos', 'marathon', 'group', + 'scale', 'scale-group', '2']) + assert stderr == b'' + assert returncode == 0 + watch_all_deployments() + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'group', 'show', + 'scale-group']) + res = json.loads(stdout.decode('utf-8')) + + assert res['groups'][0]['apps'][0]['instances'] == 2 + _remove_group('scale-group') + + +def test_scale_group_not_exist(): + returncode, stdout, stderr = exec_command(['dcos', 'marathon', 'group', + 'scale', 'scale-group', '2']) + assert stderr == b'' + watch_all_deployments() + returncode, stdout, stderr = exec_command( + ['dcos', 'marathon', 'group', 'show', + 'scale-group']) + res = json.loads(stdout.decode('utf-8')) + + assert len(res['apps']) == 0 + _remove_group('scale-group') + + +def test_scale_group_when_scale_factor_negative(): + _deploy_group('tests/data/marathon/groups/scale.json') + returncode, stdout, stderr = exec_command(['dcos', 'marathon', 'group', + 'scale', 'scale-group', '-2']) + assert b'Command not recognized' in stdout + assert returncode == 1 + watch_all_deployments() + _remove_group('scale-group') + + +def test_scale_group_when_scale_factor_not_float(): + _deploy_group('tests/data/marathon/groups/scale.json') + returncode, stdout, stderr = exec_command(['dcos', 'marathon', 'group', + 'scale', 'scale-group', '1.a']) + assert stderr == b'Error parsing string as float\n' + assert returncode == 1 + watch_all_deployments() + _remove_group('scale-group') + + def _remove_group(group_id): assert_command(['dcos', 'marathon', 'group', 'remove', group_id]) diff --git a/dcos/marathon.py b/dcos/marathon.py index bcf8ac1..f8e7881 100644 --- a/dcos/marathon.py +++ b/dcos/marathon.py @@ -388,6 +388,35 @@ class Client(object): deployment = response.json()['deploymentId'] return deployment + def scale_group(self, group_id, scale_factor, force=None): + """Scales a group with the requested scale-factor. + :param group_id: the ID of the group to scale + :type group_id: str + :param scale_factor: the requested value of scale-factor + :type scale_factor: float + :param force: whether to override running deployments + :type force: bool + :returns: the resulting deployment ID + :rtype: bool + """ + + group_id = self.normalize_app_id(group_id) + + if not force: + params = None + else: + params = {'force': 'true'} + + url = self._create_url('v2/groups{}'.format(group_id)) + + response = http.put(url, + params=params, + json={'scaleBy': scale_factor}, + timeout=self._timeout) + + deployment = response.json()['deploymentId'] + return deployment + def stop_app(self, app_id, force=None): """Scales an application to zero instances. diff --git a/dcos/util.py b/dcos/util.py index ea58b32..26e1184 100644 --- a/dcos/util.py +++ b/dcos/util.py @@ -454,6 +454,25 @@ def parse_int(string): raise DCOSException('Error parsing string as int') +def parse_float(string): + """Parse string and an float + + :param string: string to parse as an float + :type string: str + :returns: the float value of the string + :rtype: float + """ + + try: + return float(string) + except: + logger.error( + 'Unhandled exception while parsing string as float: %r', + string) + + raise DCOSException('Error parsing string as float') + + def render_mustache_json(template, data): """Render the supplied mustache template and data as a JSON value