diff --git a/dcos/api/emitting.py b/dcos/api/emitting.py index 8577390..73dd1a6 100644 --- a/dcos/api/emitting.py +++ b/dcos/api/emitting.py @@ -1,3 +1,5 @@ +from __future__ import print_function + import abc import collections import json @@ -60,13 +62,16 @@ def print_handler(event): :type event: str, dict or dcos.api.errors.Error """ - if isinstance(event, basestring): + if event is None: + # Do nothing + pass + elif isinstance(event, basestring): print(event) elif isinstance(event, collections.Mapping) or isinstance(event, list): json.dump(event, sys.stdout, sort_keys=True, indent=2) print('') elif isinstance(event, errors.Error): - print(event.error()) + print(event.error(), file=sys.stderr) else: logger.error( 'Unable to print event. Type not supported: %s, %r.', diff --git a/dcos/api/marathon.py b/dcos/api/marathon.py index edc972d..f0b4583 100644 --- a/dcos/api/marathon.py +++ b/dcos/api/marathon.py @@ -319,8 +319,8 @@ class Client(object): def get_deployment(self, deployment_id): """Returns a deployment. - :param deployemnt_id: the id of the application to restart - :type deployemnt_id: str + :param deployment_id: the deployment id + :type deployment_id: str :returns: a deployment :rtype: (dict, Error) """ @@ -343,7 +343,7 @@ class Client(object): def get_deployments(self, app_id=None): """Returns a list of deployments, optionally limited to an app. - :param app_id: the id of the application to restart + :param app_id: the id of the application :type app_id: str :returns: a list of deployments :rtype: list of dict @@ -422,13 +422,12 @@ class Client(object): :param app_id: the id of the application to restart :type app_id: str :returns: a list of tasks - :rtype: list of dict + :rtype: (list of dict, dcos.api.errors.Error) """ url = self._create_url('v2/tasks') response, error = http.get(url, response_to_error=_response_to_error) - if error is not None: return (None, error) @@ -443,6 +442,28 @@ class Client(object): return (tasks, None) + def get_task(self, task_id): + """Returns a task + + :param task_id: the id of the task + :type task_id: str + :returns: a tasks + :rtype: (dict, dcos.api.errors.Error) + """ + + url = self._create_url('v2/tasks') + + response, error = http.get(url, response_to_error=_response_to_error) + if error is not None: + return (None, error) + + task = next( + (task for task in response.json()['tasks'] + if task_id == task['id']), + None) + + return (task, None) + def normalize_app_id(app_id): """Normalizes the application id. diff --git a/dcos/cli/app/main.py b/dcos/cli/app/main.py index cbf9573..d8ebf20 100644 --- a/dcos/cli/app/main.py +++ b/dcos/cli/app/main.py @@ -14,6 +14,7 @@ Usage: dcos app start [--force] [] dcos app stop [--force] dcos app task list [] + dcos app task show dcos app update [--force] [...] dcos app version list [--max-count=] @@ -45,6 +46,7 @@ Positional arguments: Optional key-value pairs to be included in the command. The separator between the key and value must be the '=' character. E.g. cpus=2.0 + The task id """ import json import os @@ -120,6 +122,11 @@ def _cmds(): arg_keys=[''], function=_task_list), + cmds.Command( + hierarchy=['task', 'show'], + arg_keys=[''], + function=_task_show), + cmds.Command(hierarchy=['info'], arg_keys=[], function=_info), cmds.Command( @@ -683,6 +690,33 @@ def _task_list(app_id): return 0 +def _task_show(task_id): + """ + :param task_id: the task id + :type task_id: str + :returns: process status + :rtype: int + """ + + client = marathon.create_client( + config.load_from_path( + os.environ[constants.DCOS_CONFIG_ENV])) + + task, err = client.get_task(task_id) + if err is not None: + emitter.publish(err) + return 1 + + if task is None: + emitter.publish( + errors.DefaultError("Task '{}' does not exist".format(task_id))) + return 1 + + emitter.publish(task) + + 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 b52c352..215dc0d 100644 --- a/integrations/cli/test_app.py +++ b/integrations/cli/test_app.py @@ -22,6 +22,7 @@ def test_help(): dcos app start [--force] [] dcos app stop [--force] dcos app task list [] + dcos app task show dcos app update [--force] [...] dcos app version list [--max-count=] @@ -53,6 +54,7 @@ Positional arguments: Optional key-value pairs to be included in the command. The separator between the key and value must be the '=' character. E.g. cpus=2.0 + The task id """ assert stderr == b'' @@ -108,8 +110,8 @@ def test_add_bad_json_app(): stdin=fd) assert returncode == 1 - assert stdout == b'Error loading JSON.\n' - assert stderr == b'' + assert stdout == b'' + assert stderr == b'Error loading JSON.\n' def test_add_existing_app(): @@ -164,9 +166,9 @@ def test_show_missing_relative_app_version(): ['dcos', 'app', 'show', '--app-version=-2', 'zero-instance-app']) assert returncode == 1 - assert (stdout == + assert stdout == b'' + assert (stderr == b"Application 'zero-instance-app' only has 2 version(s).\n") - assert stderr == b'' _remove_app('zero-instance-app') @@ -182,9 +184,9 @@ def test_show_missing_absolute_app_version(): 'zero-instance-app']) assert returncode == 1 - assert (stdout == + assert stdout == b'' + assert (stderr == b"Error: App '/zero-instance-app' does not exist\n") - assert stderr == b'' _remove_app('zero-instance-app') @@ -200,10 +202,10 @@ def test_show_bad_app_version(): 'zero-instance-app']) assert returncode == 1 - assert (stdout == + assert stdout == b'' + assert (stderr == (b'Error: Invalid format: "20:39:32.972Z" is malformed at ' b'":39:32.972Z"\n')) - assert stderr == b'' _remove_app('zero-instance-app') @@ -218,8 +220,8 @@ def test_show_bad_relative_app_version(): ['dcos', 'app', 'show', '--app-version=2', 'zero-instance-app']) assert returncode == 1 - assert (stdout == b"Relative versions must be negative: 2\n") - assert stderr == b'' + assert stdout == b'' + assert (stderr == b"Relative versions must be negative: 2\n") _remove_app('zero-instance-app') @@ -229,8 +231,8 @@ def test_start_missing_app(): ['dcos', 'app', 'start', 'missing-id']) assert returncode == 1 - assert stdout == b"Error: App '/missing-id' does not exist\n" - assert stderr == b'' + assert stdout == b'' + assert stderr == b"Error: App '/missing-id' does not exist\n" def test_start_app(): @@ -259,8 +261,8 @@ def test_stop_missing_app(): ['dcos', 'app', 'stop', 'missing-id']) assert returncode == 1 - assert stdout == b"Error: App '/missing-id' does not exist\n" - assert stderr == b'' + assert stdout == b'' + assert stderr == b"Error: App '/missing-id' does not exist\n" def test_stop_app(): @@ -298,8 +300,8 @@ def test_update_missing_app(): ['dcos', 'app', 'update', 'missing-id']) assert returncode == 1 - assert stdout == b"Error: App '/missing-id' does not exist\n" - assert stderr == b'' + assert stdout == b'' + assert stderr == b"Error: App '/missing-id' does not exist\n" def test_update_missing_field(): @@ -309,10 +311,10 @@ def test_update_missing_field(): ['dcos', 'app', 'update', 'zero-instance-app', 'missing="a string"']) assert returncode == 1 - assert stdout.decode('utf-8').startswith( + assert stdout == b'' + assert stderr.decode('utf-8').startswith( "The property 'missing' does not conform to the expected format. " "Possible values are: ") - assert stderr == b'' _remove_app('zero-instance-app') @@ -324,10 +326,10 @@ def test_update_bad_type(): ['dcos', 'app', 'update', 'zero-instance-app', 'cpus="a string"']) assert returncode == 1 - assert stdout.decode('utf-8').startswith( + assert stderr.decode('utf-8').startswith( "Unable to parse 'a string' as a float: could not convert string to " "float: ") - assert stderr == b'' + assert stdout == b'' _remove_app('zero-instance-app') @@ -375,8 +377,8 @@ def test_restarting_missing_app(): ['dcos', 'app', 'restart', 'missing-id']) assert returncode == 1 - assert stdout == b"Error: App '/missing-id' does not exist\n" - assert stderr == b'' + assert stdout == b'' + assert stderr == b"Error: App '/missing-id' does not exist\n" def test_restarting_app(): @@ -400,8 +402,8 @@ def test_list_version_missing_app(): ['dcos', 'app', 'version', 'list', 'missing-id']) assert returncode == 1 - assert stdout == b"Error: App '/missing-id' does not exist\n" - assert stderr == b'' + assert stdout == b'' + assert stderr == b"Error: App '/missing-id' does not exist\n" def test_list_version_negative_max_count(): @@ -409,8 +411,8 @@ def test_list_version_negative_max_count(): ['dcos', 'app', 'version', 'list', 'missing-id', '--max-count=-1']) assert returncode == 1 - assert stdout == b'Maximum count must be a positive number: -1\n' - assert stderr == b'' + assert stdout == b'' + assert stderr == b'Maximum count must be a positive number: -1\n' def test_list_version_app(): @@ -468,9 +470,9 @@ def test_rollback_missing_deployment(): ['dcos', 'app', 'deployment', 'rollback', 'missing-deployment']) assert returncode == 1 - assert (stdout == + assert stdout == b'' + assert (stderr == b'Error: DeploymentPlan missing-deployment does not exist\n') - assert stderr == b'' def test_rollback_deployment(): @@ -557,6 +559,37 @@ def test_list_missing_app_tasks(): _remove_app('zero-instance-app') +def test_show_missing_task(): + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'task', 'show', 'missing-id']) + + stderr = stderr.decode('utf-8') + + assert returncode == 1 + assert stdout == b'' + assert stderr.startswith("Task '") + assert stderr.endswith("' does not exist\n") + + +def test_show_task(): + _add_app('tests/data/marathon/zero_instance_sleep.json') + _start_app('zero-instance-app', 3) + result = _list_deployments(1, 'zero-instance-app') + _watch_deployment(result[0]['id'], 60) + result = _list_tasks(3, 'zero-instance-app') + + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'task', 'show', result[0]['id']]) + + result = json.loads(stdout.decode('utf-8')) + + assert returncode == 0 + assert result['appId'] == '/zero-instance-app' + assert stderr == b'' + + _remove_app('zero-instance-app') + + def _list_apps(app_id=None): returncode, stdout, stderr = exec_command(['dcos', 'app', 'list'])