Merge pull request #35 from mesosphere/dcos-365-update
DCOS-365 Implements updating of applications
This commit is contained in:
@@ -78,11 +78,16 @@ class Client(object):
|
|||||||
|
|
||||||
message = response.json().get('message')
|
message = response.json().get('message')
|
||||||
if message is None:
|
if message is None:
|
||||||
|
errors = response.json().get('errors')
|
||||||
|
if errors is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
'Marathon server did not return a message: %s',
|
'Marathon server did not return a message: %s',
|
||||||
response.json())
|
response.json())
|
||||||
return Error('Unknown error from Marathon')
|
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']))
|
return Error('Error: {}'.format(response.json()['message']))
|
||||||
|
|
||||||
def get_app(self, app_id, version=None):
|
def get_app(self, app_id, version=None):
|
||||||
@@ -214,8 +219,7 @@ class Client(object):
|
|||||||
logger.info('Got (%r): %r', response.status_code, response.json())
|
logger.info('Got (%r): %r', response.status_code, response.json())
|
||||||
|
|
||||||
if _success(response.status_code):
|
if _success(response.status_code):
|
||||||
deployment = response.json()['deploymentId']
|
return (response.json().get('deploymentId'), None)
|
||||||
return (deployment, None)
|
|
||||||
else:
|
else:
|
||||||
return (None, self._response_to_error(response))
|
return (None, self._response_to_error(response))
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Usage:
|
|||||||
dcos app show [--app-version=<app-version>] <app-id>
|
dcos app show [--app-version=<app-version>] <app-id>
|
||||||
dcos app start [--force] <app-id> [<instances>]
|
dcos app start [--force] <app-id> [<instances>]
|
||||||
dcos app stop [--force] <app-id>
|
dcos app stop [--force] <app-id>
|
||||||
|
dcos app update [--force] <app-id> [<properties>...]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Show this screen
|
-h, --help Show this screen
|
||||||
@@ -22,6 +23,12 @@ Options:
|
|||||||
negative integer and they represent the
|
negative integer and they represent the
|
||||||
version from the currently deployed
|
version from the currently deployed
|
||||||
application definition.
|
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 json
|
||||||
import os
|
import os
|
||||||
@@ -29,8 +36,8 @@ import sys
|
|||||||
|
|
||||||
import docopt
|
import docopt
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from dcos.api import (config, constants, emitting, errors, marathon, options,
|
from dcos.api import (config, constants, emitting, errors, jsonitem, marathon,
|
||||||
util)
|
options, util)
|
||||||
|
|
||||||
logger = util.get_logger(__name__)
|
logger = util.get_logger(__name__)
|
||||||
emitter = emitting.FlatEmitter()
|
emitter = emitting.FlatEmitter()
|
||||||
@@ -71,6 +78,9 @@ def main():
|
|||||||
if args['stop']:
|
if args['stop']:
|
||||||
return _stop(args['<app-id>'], args['--force'])
|
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__))
|
emitter.publish(options.make_generic_usage_error(__doc__))
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
@@ -305,6 +315,109 @@ def _stop(app_id, force):
|
|||||||
emitter.publish('Created deployment {}'.format(deployment))
|
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):
|
def _calculate_version(client, app_id, version):
|
||||||
"""
|
"""
|
||||||
:param client: Marathon client
|
:param client: Marathon client
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ def test_help():
|
|||||||
dcos app show [--app-version=<app-version>] <app-id>
|
dcos app show [--app-version=<app-version>] <app-id>
|
||||||
dcos app start [--force] <app-id> [<instances>]
|
dcos app start [--force] <app-id> [<instances>]
|
||||||
dcos app stop [--force] <app-id>
|
dcos app stop [--force] <app-id>
|
||||||
|
dcos app update [--force] <app-id> [<properties>...]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Show this screen
|
-h, --help Show this screen
|
||||||
@@ -31,6 +32,12 @@ Options:
|
|||||||
negative integer and they represent the
|
negative integer and they represent the
|
||||||
version from the currently deployed
|
version from the currently deployed
|
||||||
application definition.
|
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''
|
assert stderr == b''
|
||||||
|
|
||||||
@@ -231,6 +238,74 @@ def test_stop_already_stopped_app():
|
|||||||
_remove_app('zero-instance-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):
|
def _list_apps(app_id=None):
|
||||||
returncode, stdout, stderr = exec_command(['dcos', 'app', 'list'])
|
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