From ed6a26de0c0e12e2a16e1123c147297fcdefe5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Armando=20Garc=C3=ADa=20Sancio?= Date: Thu, 19 Feb 2015 01:04:44 +0000 Subject: [PATCH] DCOS-365 Implements updating of applications --- dcos/api/marathon.py | 16 ++- dcos/cli/app/main.py | 117 +++++++++++++++++- integrations/cli/test_app.py | 75 +++++++++++ .../marathon/update_zero_instance_sleep.json | 5 + 4 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 tests/data/marathon/update_zero_instance_sleep.json diff --git a/dcos/api/marathon.py b/dcos/api/marathon.py index d7fcfd4..b2e1f45 100644 --- a/dcos/api/marathon.py +++ b/dcos/api/marathon.py @@ -78,10 +78,15 @@ class Client(object): message = response.json().get('message') if message is None: - logger.error( - 'Marathon server did not return a message: %s', - response.json()) - return Error('Unknown error from Marathon') + errors = response.json().get('errors') + if errors is None: + logger.error( + 'Marathon server did not return a message: %s', + response.json()) + return Error('Unknown error from Marathon') + + msg = '\n'.join(error['error'] for error in errors) + return Error('Error(s): {}'.format(msg)) return Error('Error: {}'.format(response.json()['message'])) @@ -214,8 +219,7 @@ class Client(object): logger.info('Got (%r): %r', response.status_code, response.json()) if _success(response.status_code): - deployment = response.json()['deploymentId'] - return (deployment, None) + return (response.json().get('deploymentId'), None) else: return (None, self._response_to_error(response)) diff --git a/dcos/cli/app/main.py b/dcos/cli/app/main.py index b8a0ec9..a0be169 100644 --- a/dcos/cli/app/main.py +++ b/dcos/cli/app/main.py @@ -7,6 +7,7 @@ Usage: dcos app show [--app-version=] dcos app start [--force] [] dcos app stop [--force] + dcos app update [--force] [...] Options: -h, --help Show this screen @@ -22,6 +23,12 @@ Options: negative integer and they represent the version from the currently deployed application definition. + +Positional arguments: + The application id + 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. """ import json import os @@ -29,8 +36,8 @@ import sys import docopt import pkg_resources -from dcos.api import (config, constants, emitting, errors, marathon, options, - util) +from dcos.api import (config, constants, emitting, errors, jsonitem, marathon, + options, util) logger = util.get_logger(__name__) emitter = emitting.FlatEmitter() @@ -71,6 +78,9 @@ def main(): if args['stop']: return _stop(args[''], args['--force']) + if args['update']: + return _update(args[''], args[''], args['--force']) + emitter.publish(options.make_generic_usage_error(__doc__)) return 1 @@ -305,6 +315,109 @@ def _stop(app_id, force): emitter.publish('Created deployment {}'.format(deployment)) +def _update(app_id, json_items, force): + """ + :param app_id: the id of the application + :type app_id: str + :param json_items: json update items + :type json_items: list of str + :param force: whether to override running deployments + :type force: bool + :returns: process status + :rtype: int + """ + + # Check that the application exists + client = marathon.create_client( + config.load_from_path( + os.environ[constants.DCOS_CONFIG_ENV])) + + _, err = client.get_app(app_id) + if err is not None: + emitter.publish(err) + return 1 + + if len(json_items) == 0: + if sys.stdin.isatty(): + # We don't support TTY right now. In the future we will start an + # editor + emitter.publish( + "We currently don't support reading from the TTY. Please " + "specify an application JSON.\n" + "E.g. dcos app update < app_update.json") + return 1 + else: + return _update_from_stdin(app_id, force) + + schema = json.loads( + pkg_resources.resource_string( + 'dcos', + 'data/marathon-schema.json').decode('utf-8')) + + app_json = {} + + # Need to add the 'id' because it is required + app_json['id'] = app_id + + for json_item in json_items: + key_value, err = jsonitem.parse_json_item(json_item, schema) + if err is not None: + emitter.publish(err) + return 1 + + key, value = key_value + if key in app_json: + emitter.publish( + 'Key {!r} was specified more than once'.format(key)) + return 1 + else: + app_json[key] = value + + err = util.validate_json(app_json, schema) + if err is not None: + emitter.publish(err) + return 1 + + deployment, err = client.update_app(app_id, app_json, force) + if err is not None: + emitter.publish(err) + return 1 + + emitter.publish('Created deployment {}'.format(deployment)) + + return 0 + + +def _update_from_stdin(app_id, force): + """ + :param app_id: the id of the application + :type app_id: str + :param force: whether to override running deployments + :type force: bool + :returns: process status + :rtype: int + """ + + logger.info('Updating %r from JSON object from stdin', app_id) + + application_resource, err = util.load_jsons(sys.stdin.read()) + if err is not None: + emitter.publish(err) + return 1 + + # Add application to marathon + client = marathon.create_client( + config.load_from_path( + os.environ[constants.DCOS_CONFIG_ENV])) + + _, err = client.update_app(app_id, application_resource, force) + if err is not None: + emitter.publish(err) + return 1 + + return 0 + + def _calculate_version(client, app_id, version): """ :param client: Marathon client diff --git a/integrations/cli/test_app.py b/integrations/cli/test_app.py index 71b95f2..a2e7150 100644 --- a/integrations/cli/test_app.py +++ b/integrations/cli/test_app.py @@ -16,6 +16,7 @@ def test_help(): dcos app show [--app-version=] dcos app start [--force] [] dcos app stop [--force] + dcos app update [--force] [...] Options: -h, --help Show this screen @@ -31,6 +32,12 @@ Options: negative integer and they represent the version from the currently deployed application definition. + +Positional arguments: + The application id + 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. """ assert stderr == b'' @@ -231,6 +238,74 @@ def test_stop_already_stopped_app(): _remove_app('zero-instance-app') +def test_update_missing_app(): + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'update', 'missing-id']) + + assert returncode == 1 + assert stdout == b"Error: App '/missing-id' does not exist\n" + assert stderr == b'' + + +def test_update_missing_field(): + _add_app('tests/data/marathon/zero_instance_sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'update', 'zero-instance-app', 'missing="a string"']) + + assert returncode == 1 + assert stdout.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') + + +def test_update_bad_type(): + _add_app('tests/data/marathon/zero_instance_sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'update', 'zero-instance-app', 'cpus="a string"']) + + assert returncode == 1 + assert stdout.decode('utf-8').startswith( + "Unable to parse 'a string' as a float: could not convert string to " + "float: ") + assert stderr == b'' + + _remove_app('zero-instance-app') + + +def test_update_app(): + _add_app('tests/data/marathon/zero_instance_sleep.json') + + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'update', 'zero-instance-app', + 'cpus=1', 'mem=20', "cmd='sleep 100'"]) + + assert returncode == 0 + assert stdout.decode().startswith('Created deployment ') + assert stderr == b'' + + _remove_app('zero-instance-app') + + +def test_update_app_from_stdin(): + _add_app('tests/data/marathon/zero_instance_sleep.json') + + with open('tests/data/marathon/update_zero_instance_sleep.json') as fd: + returncode, stdout, stderr = exec_command( + ['dcos', 'app', 'update', 'zero-instance-app'], + stdin=fd) + + assert returncode == 0 + assert stdout == b'' + assert stderr == b'' + + _remove_app('zero-instance-app') + + def _list_apps(app_id=None): returncode, stdout, stderr = exec_command(['dcos', 'app', 'list']) diff --git a/tests/data/marathon/update_zero_instance_sleep.json b/tests/data/marathon/update_zero_instance_sleep.json new file mode 100644 index 0000000..dde4269 --- /dev/null +++ b/tests/data/marathon/update_zero_instance_sleep.json @@ -0,0 +1,5 @@ +{ + "cpus": 1, + "mem": 20, + "cmd": "sleep 100" +}