DCOS-364 Implements dcos app start

This commit is contained in:
José Armando García Sancio
2015-02-13 06:56:58 +00:00
parent 080e80bfb7
commit 0b0b491fa0
9 changed files with 423 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"
]
}

View File

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

View File

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

View File

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