Merge pull request #35 from mesosphere/dcos-365-update

DCOS-365 Implements updating of applications
This commit is contained in:
José Armando García Sancio
2015-02-19 16:47:17 -08:00
4 changed files with 205 additions and 8 deletions

View File

@@ -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))

View File

@@ -7,6 +7,7 @@ Usage:
dcos app show [--app-version=<app-version>] <app-id>
dcos app start [--force] <app-id> [<instances>]
dcos app stop [--force] <app-id>
dcos app update [--force] <app-id> [<properties>...]
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:
<app-id> The application id
<properties> 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['<app-id>'], args['--force'])
if args['update']:
return _update(args['<app-id>'], args['<properties>'], 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

View File

@@ -16,6 +16,7 @@ def test_help():
dcos app show [--app-version=<app-version>] <app-id>
dcos app start [--force] <app-id> [<instances>]
dcos app stop [--force] <app-id>
dcos app update [--force] <app-id> [<properties>...]
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:
<app-id> The application id
<properties> 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'])

View File

@@ -0,0 +1,5 @@
{
"cpus": 1,
"mem": 20,
"cmd": "sleep 100"
}