DCOS-364 Implements dcos app start
This commit is contained in:
@@ -105,12 +105,10 @@ class Client(object):
|
||||
'v2/apps{}/versions/{}'.format(app_id, version))
|
||||
|
||||
logger.info('Getting %r', url)
|
||||
|
||||
response = requests.get(url)
|
||||
|
||||
logger.info('Got (%r): %r', response.status_code, response.json())
|
||||
|
||||
if response.status_code == 200:
|
||||
if _success(response.status_code):
|
||||
# Looks like Marathon return different JSON for versions
|
||||
if version is None:
|
||||
return (response.json()['app'], None)
|
||||
@@ -136,12 +134,10 @@ class Client(object):
|
||||
url = self._create_url('v2/apps{}/versions'.format(app_id))
|
||||
|
||||
logger.info('Getting %r', url)
|
||||
|
||||
response = requests.get(url)
|
||||
|
||||
logger.info('Got (%r): %r', response.status_code, response.json())
|
||||
|
||||
if response.status_code == 200:
|
||||
if _success(response.status_code):
|
||||
if max_count is None:
|
||||
return (response.json()['versions'], None)
|
||||
else:
|
||||
@@ -159,7 +155,7 @@ class Client(object):
|
||||
url = self._create_url('v2/apps')
|
||||
response = requests.get(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
if _success(response.status_code):
|
||||
apps = response.json()['apps']
|
||||
return (apps, None)
|
||||
else:
|
||||
@@ -170,8 +166,8 @@ class Client(object):
|
||||
|
||||
:param app_resource: Application resource
|
||||
:type app_resource: dict, bytes or file
|
||||
:returns: Status of trying to start the application
|
||||
:rtype: Error
|
||||
:returns: the application description
|
||||
:rtype: (dict, Error)
|
||||
"""
|
||||
|
||||
url = self._create_url('v2/apps')
|
||||
@@ -182,13 +178,46 @@ class Client(object):
|
||||
else:
|
||||
app_json = app_resource
|
||||
|
||||
logger.info("Posting %r to %r", app_json, url)
|
||||
logger.info('Posting %r to %r', app_json, url)
|
||||
response = requests.post(url, json=app_json)
|
||||
logger.info('Got (%r): %r', response.status_code, response.json())
|
||||
|
||||
if response.status_code == 201:
|
||||
return None
|
||||
if _success(response.status_code):
|
||||
return (response.json(), None)
|
||||
else:
|
||||
return self._response_to_error(response)
|
||||
return (None, self._response_to_error(response))
|
||||
|
||||
def update_app(self, app_id, payload, force=None):
|
||||
"""Update an application.
|
||||
|
||||
:param app_id: the application id
|
||||
:type app_id: str
|
||||
:param payload: the json payload
|
||||
:type payload: dict
|
||||
:param force: whether to override running deployments.
|
||||
:type force: bool
|
||||
:returns: the resulting deployment ID.
|
||||
:rtype: (str, Error)
|
||||
"""
|
||||
|
||||
app_id = self._sanitize_app_id(app_id)
|
||||
|
||||
if force is None or not force:
|
||||
params = None
|
||||
else:
|
||||
params = {'force': True}
|
||||
|
||||
url = self._create_url('v2/apps{}'.format(app_id), params)
|
||||
|
||||
logger.info('Putting %r to %r', payload, url)
|
||||
response = requests.put(url, json=payload)
|
||||
logger.info('Got (%r): %r', response.status_code, response.json())
|
||||
|
||||
if _success(response.status_code):
|
||||
deployment = response.json()['deploymentId']
|
||||
return (deployment, None)
|
||||
else:
|
||||
return (None, self._response_to_error(response))
|
||||
|
||||
def scale_app(self, app_id, instances, force=None):
|
||||
"""Scales an application to the requested number of instances.
|
||||
@@ -215,7 +244,7 @@ class Client(object):
|
||||
url = self._create_url('v2/apps{}'.format(app_id), params)
|
||||
response = requests.put(url, json={'instances': int(instances)})
|
||||
|
||||
if response.status_code == 200:
|
||||
if _success(response.status_code):
|
||||
deployment = response.json()['deploymentId']
|
||||
return (deployment, None)
|
||||
else:
|
||||
@@ -257,7 +286,7 @@ class Client(object):
|
||||
url = self._create_url('v2/apps{}'.format(app_id), params)
|
||||
response = requests.delete(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
if _success(response.status_code):
|
||||
return None
|
||||
else:
|
||||
return self._response_to_error(response)
|
||||
@@ -281,3 +310,15 @@ class Error(errors.Error):
|
||||
"""
|
||||
|
||||
return self._message
|
||||
|
||||
|
||||
def _success(status_code):
|
||||
"""Returns true if the success status is between [200, 300).
|
||||
|
||||
:param response_status: the http response status
|
||||
:type response_status: int
|
||||
:returns: True for success status; False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
return status_code >= 200 and status_code < 300
|
||||
|
||||
@@ -7,7 +7,6 @@ import shutil
|
||||
import subprocess
|
||||
|
||||
import git
|
||||
import jsonschema
|
||||
import portalocker
|
||||
import pystache
|
||||
from dcos.api import errors, util
|
||||
@@ -59,10 +58,9 @@ def install(pkg, version, init_client, user_options, cfg):
|
||||
options = dict(list(default_options.items()) + list(user_options.items()))
|
||||
|
||||
# Validate options with the config schema
|
||||
try:
|
||||
jsonschema.validate(options, config_schema)
|
||||
except jsonschema.ValidationError as ve:
|
||||
return Error(ve.message)
|
||||
err = util.validate_json(options, config_schema)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# Insert option parameters into the init template
|
||||
init_template, tmpl_error = pkg.marathon_template(version)
|
||||
@@ -87,7 +85,8 @@ def install(pkg, version, init_client, user_options, cfg):
|
||||
# TODO(CD): Is this necessary / desirable at this point?
|
||||
|
||||
# Send the descriptor to init
|
||||
return init_client.add_app(init_desc)
|
||||
_, err = init_client.add_app(init_desc)
|
||||
return err
|
||||
|
||||
|
||||
def list_installed_packages(init_client):
|
||||
|
||||
@@ -6,6 +6,7 @@ import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import jsonschema
|
||||
from dcos.api import constants, errors
|
||||
|
||||
|
||||
@@ -118,3 +119,20 @@ def load_jsons(value):
|
||||
value,
|
||||
error)
|
||||
return (None, errors.DefaultError('Error loading JSON.'))
|
||||
|
||||
|
||||
def validate_json(instance, schema):
|
||||
"""Validate an instance under the given schema.
|
||||
|
||||
:param instance: the instance to validate
|
||||
:type instance: dict
|
||||
:param schema: the schema to validate with
|
||||
:type schema: dict
|
||||
:returns: an error if the validation failed; None otherwise
|
||||
:rtype: Error
|
||||
"""
|
||||
try:
|
||||
jsonschema.validate(instance, schema)
|
||||
return None
|
||||
except jsonschema.ValidationError as ve:
|
||||
return errors.DefaultError(ve.message)
|
||||
|
||||
@@ -5,6 +5,7 @@ Usage:
|
||||
dcos app list
|
||||
dcos app remove [--force] <app-id>
|
||||
dcos app show [--app-version=<app-version>] <app-id>
|
||||
dcos app start [--force] <app-id> [<instances>]
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
@@ -21,10 +22,12 @@ Options:
|
||||
version from the currently deployed
|
||||
application definition.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import docopt
|
||||
import pkg_resources
|
||||
from dcos.api import (config, constants, emitting, errors, marathon, options,
|
||||
util)
|
||||
|
||||
@@ -61,7 +64,14 @@ def main():
|
||||
if args['show']:
|
||||
return _show(args['<app-id>'], args['--app-version'])
|
||||
|
||||
if args['start']:
|
||||
return _start(
|
||||
args['<app-id>'],
|
||||
args['<instances>'],
|
||||
args['--force'])
|
||||
|
||||
emitter.publish(options.make_generic_usage_error(__doc__))
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
@@ -99,7 +109,7 @@ def _add():
|
||||
client = marathon.create_client(
|
||||
config.load_from_path(
|
||||
os.environ[constants.DCOS_CONFIG_ENV]))
|
||||
err = client.add_app(application_resource)
|
||||
_, err = client.add_app(application_resource)
|
||||
if err is not None:
|
||||
emitter.publish(err)
|
||||
return 1
|
||||
@@ -184,6 +194,78 @@ def _show(app_id, version):
|
||||
return 0
|
||||
|
||||
|
||||
def _start(app_id, instances, force):
|
||||
"""Start a Marathon application.
|
||||
|
||||
:param app_id: the id for the application
|
||||
:type app_id: str
|
||||
:param json_items: the list of json item
|
||||
: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]))
|
||||
|
||||
desc, err = client.get_app(app_id)
|
||||
if err is not None:
|
||||
emitter.publish(err)
|
||||
return 1
|
||||
|
||||
if desc['instances'] > 0:
|
||||
emitter.publish(
|
||||
'Application {!r} already started: {!r} instances.'.format(
|
||||
app_id,
|
||||
desc['instances']))
|
||||
return 1
|
||||
|
||||
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
|
||||
|
||||
# Set instances to 1 if not specified
|
||||
if instances is None:
|
||||
instances = 1
|
||||
else:
|
||||
instances, err = _parse_int(instances)
|
||||
if err is not None:
|
||||
emitter.publish(err)
|
||||
return 1
|
||||
|
||||
if instances <= 0:
|
||||
emitter.publish(
|
||||
'The number of instances must be positive: {!r}.'.format(
|
||||
instances))
|
||||
return 1
|
||||
|
||||
app_json['instances'] = instances
|
||||
|
||||
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 _calculate_version(client, app_id, version):
|
||||
"""
|
||||
:param client: Marathon client
|
||||
|
||||
@@ -138,7 +138,7 @@ def _start(app_resource_path, config):
|
||||
client = marathon.create_client(config)
|
||||
|
||||
with open(app_resource_path) as app_resource_file:
|
||||
err = client.add_app(app_resource_file)
|
||||
_, err = client.add_app(app_resource_file)
|
||||
if err is not None:
|
||||
print(err.error())
|
||||
return 1
|
||||
|
||||
215
dcos/data/marathon-schema.json
Normal file
215
dcos/data/marathon-schema.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^/?(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$"
|
||||
},
|
||||
"cmd": {
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"type": "string"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"instances": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"cpus": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"mem": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"disk": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"executor": {
|
||||
"type": "string"
|
||||
},
|
||||
"constraints": {
|
||||
},
|
||||
"uris": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"storeUrls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 65535
|
||||
}
|
||||
},
|
||||
"requirePorts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"backoffSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"backoffFactor": {
|
||||
"type": "number",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"container": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"docker": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "string"
|
||||
},
|
||||
"network": {
|
||||
"type": "string"
|
||||
},
|
||||
"portMappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"containerPort": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 65535
|
||||
},
|
||||
"hostPort": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 65535
|
||||
},
|
||||
"servicePort": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 65535
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"image"
|
||||
]
|
||||
},
|
||||
"volumes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"containerPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"hostPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"healthChecks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"protocol": {
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"portIndex": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"gracePeriodSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"intervalSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"timeoutSeconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"maxConsecutiveFailures": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^/?(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$"
|
||||
}
|
||||
},
|
||||
"upgradeStrategy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"minimumHealthCapacity": {
|
||||
"type": "number",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
@@ -14,7 +14,7 @@ def exec_command(cmd, env=None, stdin=None):
|
||||
:rtype: (int, bytes, bytes)
|
||||
"""
|
||||
|
||||
print('CMD: %r', cmd)
|
||||
print('CMD: {!r}'.format(cmd))
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
@@ -26,7 +26,7 @@ def exec_command(cmd, env=None, stdin=None):
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
# We should always print the stdout and stderr
|
||||
print('STDOUT: %s', stdout.decode('utf-8'))
|
||||
print('STDERR: %s', stderr.decode('utf-8'))
|
||||
print('STDOUT: {!r}'.format(stdout.decode('utf-8')))
|
||||
print('STDERR: {!r}'.format(stderr.decode('utf-8')))
|
||||
|
||||
return (process.returncode, stdout, stderr)
|
||||
|
||||
@@ -13,6 +13,7 @@ def test_help():
|
||||
dcos app list
|
||||
dcos app remove [--force] <app-id>
|
||||
dcos app show [--app-version=<app-version>] <app-id>
|
||||
dcos app start [--force] <app-id> [<instances>]
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
@@ -160,6 +161,36 @@ def test_show_bad_relative_app_version():
|
||||
_remove_app('zero-instance-app')
|
||||
|
||||
|
||||
def test_start_missing_app():
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'app', 'start', 'missing-id'])
|
||||
|
||||
assert returncode == 1
|
||||
assert stdout == b"Error: App '/missing-id' does not exist\n"
|
||||
assert stderr == b''
|
||||
|
||||
|
||||
def test_start_app():
|
||||
_add_app('tests/data/marathon/zero_instance_sleep.json')
|
||||
_start_app('zero-instance-app')
|
||||
_remove_app('zero-instance-app')
|
||||
|
||||
|
||||
def test_start_already_started_app():
|
||||
_add_app('tests/data/marathon/zero_instance_sleep.json')
|
||||
_start_app('zero-instance-app')
|
||||
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'app', 'start', 'zero-instance-app'])
|
||||
|
||||
assert returncode == 1
|
||||
assert (stdout ==
|
||||
b"Application 'zero-instance-app' already started: 1 instances.\n")
|
||||
assert stderr == b''
|
||||
|
||||
_remove_app('zero-instance-app')
|
||||
|
||||
|
||||
def _list_apps(app_id=None):
|
||||
returncode, stdout, stderr = exec_command(['dcos', 'app', 'list'])
|
||||
|
||||
@@ -212,3 +243,12 @@ def _show_app(app_id, version=None):
|
||||
assert stderr == b''
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _start_app(app_id):
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'app', 'start', app_id])
|
||||
|
||||
assert returncode == 0
|
||||
assert stdout.decode().startswith('Created deployment ')
|
||||
assert stderr == b''
|
||||
|
||||
5
setup.py
5
setup.py
@@ -20,7 +20,7 @@ setup(
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version=constants.version,
|
||||
|
||||
description='Dcos cli poc project',
|
||||
description='DCOS Command Line Interface',
|
||||
long_description=long_description,
|
||||
|
||||
# The project's main homepage.
|
||||
@@ -76,7 +76,6 @@ setup(
|
||||
'portalocker',
|
||||
'pystache',
|
||||
'requests',
|
||||
# 'toml',
|
||||
],
|
||||
|
||||
# List additional groups of dependencies here (e.g. development
|
||||
@@ -92,7 +91,7 @@ setup(
|
||||
# installed, specify them here. If using Python 2.6 or less, then these
|
||||
# have to be included in MANIFEST.in as well.
|
||||
package_data={
|
||||
'sample': ['package_data.dat'],
|
||||
'dcos': ['data/*'],
|
||||
},
|
||||
|
||||
# Although 'package_data' is the preferred approach, in some case you may
|
||||
|
||||
Reference in New Issue
Block a user