diff --git a/dcos/api/marathon.py b/dcos/api/marathon.py index e163170..e6a1a1f 100644 --- a/dcos/api/marathon.py +++ b/dcos/api/marathon.py @@ -101,7 +101,7 @@ class Client(object): logger.info('Getting %r', url) response = requests.get(url) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): # Looks like Marathon return different JSON for versions @@ -138,7 +138,7 @@ class Client(object): logger.info('Getting %r', url) response = requests.get(url) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): if max_count is None: @@ -159,7 +159,7 @@ class Client(object): logger.info('Getting %r', url) response = requests.get(url) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): apps = response.json()['apps'] @@ -186,7 +186,7 @@ class Client(object): logger.info('Posting %r to %r', app_json, url) response = requests.post(url, json=app_json) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): return (response.json(), None) @@ -217,7 +217,7 @@ class Client(object): logger.info('Putting %r to %r', payload, url) response = requests.put(url, json=payload) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): return (response.json().get('deploymentId'), None) @@ -248,7 +248,7 @@ class Client(object): logger.info('Putting to %r', url) response = requests.put(url, json={'instances': int(instances)}) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): deployment = response.json()['deploymentId'] @@ -320,7 +320,7 @@ class Client(object): logger.info('Posting %r', url) response = requests.post(url) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): return (response.json(), None) @@ -340,7 +340,7 @@ class Client(object): logger.info('Getting %r', url) response = requests.get(url) - logger.info('Got (%r): %r', response.status_code, response.json()) + logger.info('Got (%r): %r', response.status_code, response.text) if _success(response.status_code): if app_id is not None: @@ -356,6 +356,60 @@ class Client(object): else: return (None, self._response_to_error(response)) + def _cancel_deployment(self, deployment_id, force): + """Cancels an application deployment. + + :param deployment_id: the deployment id + :type deployment_id: str + :param force: if set to `False`, stop the deployment and + create a new rollback deployment to reinstate the + previous configuration. If set to `True`, simply stop the + deployment. + :type force: bool + :returns: an error if unable to rollback the deployment; None otherwise + :rtype: Error + """ + + if not force: + params = None + else: + params = {'force': 'true'} + + url = self._create_url( + 'v2/deployments/{}'.format(deployment_id), + params) + + logger.info('Deleting %r', url) + response = requests.delete(url) + logger.info('Got (%r): %r', response.status_code, response.text) + + if _success(response.status_code): + return None + else: + return self._response_to_error(response) + + def rollback_deployment(self, deployment_id): + """Rolls back an application deployment. + + :param deployment_id: the deployment id + :type deployment_id: str + :returns: an error if unable to rollback the deployment; None otherwise + :rtype: Error + """ + + return self._cancel_deployment(deployment_id, False) + + def stop_deployment(self, deployment_id): + """Stops an application deployment. + + :param deployment_id: the deployment id + :type deployment_id: str + :returns: an error if unable to stop the deployment; None otherwise + :rtype: Error + """ + + return self._cancel_deployment(deployment_id, True) + class Error(errors.Error): """ Class for describing errors while talking to the Marathon server. diff --git a/dcos/cli/app/main.py b/dcos/cli/app/main.py index ccd44c2..33e8748 100644 --- a/dcos/cli/app/main.py +++ b/dcos/cli/app/main.py @@ -2,6 +2,8 @@ Usage: dcos app add dcos app deployment list [] + dcos app deployment rollback + dcos app deployment stop dcos app info dcos app list dcos app remove [--force] @@ -31,6 +33,7 @@ Options: Positional arguments: The application id + The deployment id The number of instances to start Optional key-value pairs to be included in the command. The separator between the key and value @@ -79,10 +82,6 @@ def _cmds(): """ return [ - cmds.Command(hierarchy=['info'], arg_keys=[], function=_info), - - cmds.Command(hierarchy=['add'], arg_keys=[], function=_add), - cmds.Command( hierarchy=['version', 'list'], arg_keys=['', '--max-count'], @@ -93,6 +92,20 @@ def _cmds(): arg_keys=[''], function=_deployment_list), + cmds.Command( + hierarchy=['deployment', 'rollback'], + arg_keys=[''], + function=_deployment_rollback), + + cmds.Command( + hierarchy=['deployment', 'stop'], + arg_keys=[''], + function=_deployment_stop), + + cmds.Command(hierarchy=['info'], arg_keys=[], function=_info), + + cmds.Command(hierarchy=['add'], arg_keys=[], function=_add), + cmds.Command(hierarchy=['list'], arg_keys=[], function=_list), cmds.Command( @@ -536,6 +549,46 @@ def _deployment_list(app_id): return 0 +def _deployment_stop(deployment_id): + """ + :param deployment_id: the application id + :type deployment_di: str + :returns: process status + :rtype: int + """ + + client = marathon.create_client( + config.load_from_path( + os.environ[constants.DCOS_CONFIG_ENV])) + + err = client.stop_deployment(deployment_id) + if err is not None: + emitter.publish(err) + return 1 + + return 0 + + +def _deployment_rollback(deployment_id): + """ + :param deployment_id: the application id + :type deployment_di: str + :returns: process status + :rtype: int + """ + + client = marathon.create_client( + config.load_from_path( + os.environ[constants.DCOS_CONFIG_ENV])) + + err = client.rollback_deployment(deployment_id) + if err is not None: + emitter.publish(err) + return 1 + + return 0 + + def _update_from_stdin(app_id, force): """ :param app_id: the id of the application diff --git a/integrations/cli/test_app.py b/integrations/cli/test_app.py index 83db135..5655cd8 100644 --- a/integrations/cli/test_app.py +++ b/integrations/cli/test_app.py @@ -11,6 +11,8 @@ def test_help(): assert stdout == b"""Usage: dcos app add dcos app deployment list [] + dcos app deployment rollback + dcos app deployment stop dcos app info dcos app list dcos app remove [--force] @@ -40,6 +42,7 @@ Options: Positional arguments: The application id + The deployment id The number of instances to start Optional key-value pairs to be included in the command. The separator between the key and value @@ -416,59 +419,71 @@ def test_list_version_max_count(): def test_list_empty_deployment(): - returncode, stdout, stderr = exec_command( - ['dcos', 'app', 'deployment', 'list']) - - assert returncode == 0 - assert stdout == b'[]\n' - assert stderr == b'' + _list_deployments(0) def test_list_deployment(): _add_app('tests/data/marathon/zero_instance_sleep.json') - _start_app('zero-instance-app') - - returncode, stdout, stderr = exec_command( - ['dcos', 'app', 'deployment', 'list']) - - result = json.loads(stdout.decode('utf-8')) - - assert returncode == 0 - assert len(result) == 1 - assert stderr == b'' - + _start_app('zero-instance-app', 3) + _list_deployments(1) _remove_app('zero-instance-app') def test_list_deployment_missing_app(): _add_app('tests/data/marathon/zero_instance_sleep.json') _start_app('zero-instance-app') - - returncode, stdout, stderr = exec_command( - ['dcos', 'app', 'deployment', 'list', 'missing-id']) - - result = json.loads(stdout.decode('utf-8')) - - assert returncode == 0 - assert len(result) == 0 - assert stderr == b'' - + _list_deployments(0, 'missing-id') _remove_app('zero-instance-app') def test_list_deployment_app(): _add_app('tests/data/marathon/zero_instance_sleep.json') - _start_app('zero-instance-app') + _start_app('zero-instance-app', 3) + _list_deployments(1, 'zero-instance-app') + _remove_app('zero-instance-app') + + +def test_rollback_missing_deployment(): + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'deployment', 'rollback', 'missing-deployment']) + + assert returncode == 1 + assert (stdout == + b'Error: DeploymentPlan missing-deployment does not exist\n') + assert stderr == b'' + + +def test_rollback_deployment(): + _add_app('tests/data/marathon/zero_instance_sleep.json') + _start_app('zero-instance-app', 3) + result = _list_deployments(1, 'zero-instance-app') returncode, stdout, stderr = exec_command( - ['dcos', 'app', 'deployment', 'list', 'zero-instance-app']) - - result = json.loads(stdout.decode('utf-8')) + ['dcos', 'app', 'deployment', 'rollback', result[0]['id']]) assert returncode == 0 - assert len(result) == 1 + assert stdout == b'' assert stderr == b'' + _list_deployments(0) + + _remove_app('zero-instance-app') + + +def test_stop_deployment(): + _add_app('tests/data/marathon/zero_instance_sleep.json') + _start_app('zero-instance-app', 3) + result = _list_deployments(1, 'zero-instance-app') + + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'deployment', 'stop', result[0]['id']]) + + assert returncode == 0 + assert stdout == b'' + assert stderr == b'' + + _list_deployments(0) + _remove_app('zero-instance-app') @@ -526,9 +541,12 @@ def _show_app(app_id, version=None): return result -def _start_app(app_id): - returncode, stdout, stderr = exec_command( - ['dcos', 'app', 'start', app_id]) +def _start_app(app_id, instances=None): + cmd = ['dcos', 'app', 'start', app_id] + if instances is not None: + cmd.append(str(instances)) + + returncode, stdout, stderr = exec_command(cmd) assert returncode == 0 assert stdout.decode().startswith('Created deployment ') @@ -560,4 +578,18 @@ def _list_versions(app_id, expected_count, max_count=None): assert len(result) == expected_count assert stderr == b'' + +def _list_deployments(expected_count, app_id=None): + cmd = ['dcos', 'app', 'deployment', 'list'] + if app_id is not None: + cmd.append(app_id) + + returncode, stdout, stderr = exec_command(cmd) + + result = json.loads(stdout.decode('utf-8')) + + assert returncode == 0 + assert len(result) == expected_count + assert stderr == b'' + return result