Merge pull request #35 from mesosphere/dcos-365-update
DCOS-365 Implements updating of applications
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
5
tests/data/marathon/update_zero_instance_sleep.json
Normal file
5
tests/data/marathon/update_zero_instance_sleep.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cpus": 1,
|
||||
"mem": 20,
|
||||
"cmd": "sleep 100"
|
||||
}
|
||||
Reference in New Issue
Block a user