import contextlib import json import os import re import threading from dcos import constants import pytest from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from .common import (app, assert_command, assert_lines, config_set, config_unset, exec_command, list_deployments, popen_tty, show_app, watch_all_deployments, watch_deployment) _ZERO_INSTANCE_APP_INSTANCES = 100 def test_help(): with open('tests/data/help/marathon.txt') as content: assert_command(['dcos', 'marathon', '--help'], stdout=content.read().encode('utf-8')) def test_version(): assert_command(['dcos', 'marathon', '--version'], stdout=b'dcos-marathon version SNAPSHOT\n') def test_info(): assert_command(['dcos', 'marathon', '--info'], stdout=b'Deploy and manage applications on the DCOS\n') def test_about(): returncode, stdout, stderr = exec_command(['dcos', 'marathon', 'about']) assert returncode == 0 assert stderr == b'' result = json.loads(stdout.decode('utf-8')) assert result['name'] == "marathon" @pytest.fixture def missing_env(): env = os.environ.copy() env.update({ constants.PATH_ENV: os.environ[constants.PATH_ENV], constants.DCOS_CONFIG_ENV: os.path.join("tests", "data", "marathon", "missing_marathon_params.toml") }) return env def test_missing_config(missing_env): assert_command( ['dcos', 'marathon', 'app', 'list'], returncode=1, stderr=(b'Missing required config parameter: "core.dcos_url". ' b'Please run `dcos config set core.dcos_url `.\n'), env=missing_env) def test_empty_list(): _list_apps() def test_add_app(): with _zero_instance_app(): _list_apps('zero-instance-app') def test_add_app_through_http(): with _zero_instance_app_through_http(): _list_apps('zero-instance-app') def test_add_app_bad_resource(): stderr = (b'Can\'t read from resource: bad_resource.\n' b'Please check that it exists.\n') assert_command(['dcos', 'marathon', 'app', 'add', 'bad_resource'], returncode=1, stderr=stderr) def test_add_app_with_filename(): with _zero_instance_app(): _list_apps('zero-instance-app') def test_remove_app(): with _zero_instance_app(): pass _list_apps() def test_add_bad_json_app(): with open('tests/data/marathon/apps/bad.json') as fd: returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'add'], stdin=fd) assert returncode == 1 assert stdout == b'' assert stderr.decode('utf-8').startswith('Error loading JSON: ') def test_add_existing_app(): with _zero_instance_app(): app_path = 'tests/data/marathon/apps/zero_instance_sleep_v2.json' with open(app_path) as fd: stderr = b"Application '/zero-instance-app' already exists\n" assert_command(['dcos', 'marathon', 'app', 'add'], returncode=1, stderr=stderr, stdin=fd) def test_show_app(): with _zero_instance_app(): show_app('zero-instance-app') def test_show_absolute_app_version(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') result = show_app('zero-instance-app') show_app('zero-instance-app', result['version']) def test_show_relative_app_version(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') show_app('zero-instance-app', "-1") def test_show_missing_relative_app_version(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') stderr = b"Application 'zero-instance-app' only has 2 version(s).\n" assert_command(['dcos', 'marathon', 'app', 'show', '--app-version=-2', 'zero-instance-app'], returncode=1, stderr=stderr) def test_show_missing_absolute_app_version(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'show', '--app-version=2000-02-11T20:39:32.972Z', 'zero-instance-app']) assert returncode == 1 assert stdout == b'' assert stderr.decode('utf-8').startswith( "Error: App '/zero-instance-app' does not exist") def test_show_bad_app_version(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') stderr = (b'Error: Invalid format: "20:39:32.972Z" is malformed at ' b'":39:32.972Z"\n') assert_command( ['dcos', 'marathon', 'app', 'show', '--app-version=20:39:32.972Z', 'zero-instance-app'], returncode=1, stderr=stderr) def test_show_bad_relative_app_version(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') assert_command( ['dcos', 'marathon', 'app', 'show', '--app-version=2', 'zero-instance-app'], returncode=1, stderr=b"Relative versions must be negative: 2\n") def test_start_missing_app(): assert_command( ['dcos', 'marathon', 'app', 'start', 'missing-id'], returncode=1, stderr=b"Error: App '/missing-id' does not exist\n") def test_start_app(): with _zero_instance_app(): _start_app('zero-instance-app') def test_start_already_started_app(): with _zero_instance_app(): _start_app('zero-instance-app') stdout = (b"Application 'zero-instance-app' already " b"started: 1 instances.\n") assert_command( ['dcos', 'marathon', 'app', 'start', 'zero-instance-app'], returncode=1, stdout=stdout) def test_stop_missing_app(): assert_command(['dcos', 'marathon', 'app', 'stop', 'missing-id'], returncode=1, stderr=b"Error: App '/missing-id' does not exist\n") def test_stop_app(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'stop', 'zero-instance-app']) assert returncode == 0 assert stdout.decode().startswith('Created deployment ') assert stderr == b'' def test_stop_already_stopped_app(): with _zero_instance_app(): stdout = (b"Application 'zero-instance-app' already " b"stopped: 0 instances.\n") assert_command( ['dcos', 'marathon', 'app', 'stop', 'zero-instance-app'], returncode=1, stdout=stdout) def test_update_missing_app(): assert_command(['dcos', 'marathon', 'app', 'update', 'missing-id'], stderr=b"Error: App '/missing-id' does not exist\n", returncode=1) def test_update_bad_type(): with _zero_instance_app(): returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'update', 'zero-instance-app', 'cpus="a string"']) stderr_end = b"""{ "details": [ { "errors": [ "error.expected.jsnumber" ], "path": "/cpus" } ], "message": "Invalid JSON" } """ assert returncode == 1 assert stderr_end in stderr assert stdout == b'' def test_update_invalid_request(): returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'update', '{', 'instances']) assert returncode == 1 assert stdout == b'' stderr = stderr.decode() # TODO (tamar): this becomes 'Error: App '/{' does not exist\n"' # in Marathon 0.11.0 assert stderr.startswith('Error on request') assert stderr.endswith('HTTP 400: Bad Request\n') def test_app_add_invalid_request(): path = os.path.join( 'tests', 'data', 'marathon', 'apps', 'app_add_400.json') returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'add', path]) assert returncode == 1 assert stdout == b'' assert re.match(b"Error on request \[POST .*\]: HTTP 400: Bad Request:", stderr) stderr_end = b"""{ "details": [ { "errors": [ "host is not a valid network type" ], "path": "/container/docker/network" } ], "message": "Invalid JSON" } """ assert stderr.endswith(stderr_end) def test_update_app(): with _zero_instance_app(): returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'update', 'zero-instance-app', 'cpus=1', 'mem=20', "cmd='sleep 100'"]) assert returncode == 0 assert stdout.decode().startswith('Created deployment ') assert stderr == b'' def test_update_app_from_stdin(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') def test_restarting_stopped_app(): with _zero_instance_app(): stdout = (b"Unable to perform rolling restart of application '" b"/zero-instance-app' because it has no running tasks\n") assert_command( ['dcos', 'marathon', 'app', 'restart', 'zero-instance-app'], returncode=1, stdout=stdout) def test_restarting_missing_app(): assert_command(['dcos', 'marathon', 'app', 'restart', 'missing-id'], returncode=1, stderr=b"Error: App '/missing-id' does not exist\n") def test_restarting_app(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'restart', 'zero-instance-app']) assert returncode == 0 assert stdout.decode().startswith('Created deployment ') assert stderr == b'' def test_killing_app(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() task_set_1 = set([task['id'] for task in _list_tasks(3, 'zero-instance-app')]) returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'kill', 'zero-instance-app']) assert returncode == 0 assert stdout.decode().startswith('Killed tasks: ') assert stderr == b'' watch_all_deployments() task_set_2 = set([task['id'] for task in _list_tasks(app_id='zero-instance-app')]) assert len(task_set_1.intersection(task_set_2)) == 0 def test_killing_scaling_app(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() _list_tasks(3) command = ['dcos', 'marathon', 'app', 'kill', '--scale', 'zero-instance-app'] returncode, stdout, stderr = exec_command(command) assert returncode == 0 assert stdout.decode().startswith('Started deployment: ') assert stdout.decode().find('version') > -1 assert stdout.decode().find('deploymentId') > -1 assert stderr == b'' watch_all_deployments() _list_tasks(0) def test_killing_with_host_app(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() existing_tasks = _list_tasks(3, 'zero-instance-app') task_hosts = set([task['host'] for task in existing_tasks]) if len(task_hosts) <= 1: pytest.skip('test needs 2 or more agents to succeed, ' 'only {} agents available'.format(len(task_hosts))) assert len(task_hosts) > 1 kill_host = list(task_hosts)[0] expected_to_be_killed = set([task['id'] for task in existing_tasks if task['host'] == kill_host]) not_to_be_killed = set([task['id'] for task in existing_tasks if task['host'] != kill_host]) assert len(not_to_be_killed) > 0 assert len(expected_to_be_killed) > 0 command = ['dcos', 'marathon', 'app', 'kill', '--host', kill_host, 'zero-instance-app'] returncode, stdout, stderr = exec_command(command) assert stdout.decode().startswith('Killed tasks: ') assert stderr == b'' new_tasks = set([task['id'] for task in _list_tasks()]) assert not_to_be_killed.intersection(new_tasks) == not_to_be_killed assert len(expected_to_be_killed.intersection(new_tasks)) == 0 def test_kill_stopped_app(): with _zero_instance_app(): returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'kill', 'zero-instance-app']) assert returncode == 1 assert stdout.decode().startswith('Killed tasks: []') def test_kill_missing_app(): returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'kill', 'app']) assert returncode == 1 assert stdout.decode() == '' stderr_expected = "Error: App '/app' does not exist" assert stderr.decode().strip() == stderr_expected def test_list_version_missing_app(): assert_command( ['dcos', 'marathon', 'app', 'version', 'list', 'missing-id'], returncode=1, stderr=b"Error: App '/missing-id' does not exist\n") def test_list_version_negative_max_count(): assert_command(['dcos', 'marathon', 'app', 'version', 'list', 'missing-id', '--max-count=-1'], returncode=1, stderr=b'Maximum count must be a positive number: -1\n') def test_list_version_app(): with _zero_instance_app(): _list_versions('zero-instance-app', 1) _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') _list_versions('zero-instance-app', 2) def test_list_version_max_count(): with _zero_instance_app(): _update_app( 'zero-instance-app', 'tests/data/marathon/apps/update_zero_instance_sleep.json') _list_versions('zero-instance-app', 1, 1) _list_versions('zero-instance-app', 2, 2) _list_versions('zero-instance-app', 2, 3) def test_list_empty_deployment(): list_deployments(0) def test_list_deployment(): with _zero_instance_app(): _start_app('zero-instance-app', _ZERO_INSTANCE_APP_INSTANCES) list_deployments(1) def test_list_deployment_table(): """Simple sanity check for listing deployments with a table output. The more specific testing is done in unit tests. """ with _zero_instance_app(): _start_app('zero-instance-app', _ZERO_INSTANCE_APP_INSTANCES) assert_lines(['dcos', 'marathon', 'deployment', 'list'], 2) def test_list_deployment_missing_app(): with _zero_instance_app(): _start_app('zero-instance-app') list_deployments(0, 'missing-id') def test_list_deployment_app(): with _zero_instance_app(): _start_app('zero-instance-app', _ZERO_INSTANCE_APP_INSTANCES) list_deployments(1, 'zero-instance-app') def test_rollback_missing_deployment(): assert_command( ['dcos', 'marathon', 'deployment', 'rollback', 'missing-deployment'], returncode=1, stderr=b'Error: DeploymentPlan missing-deployment does not exist\n') def test_rollback_deployment(): with _zero_instance_app(): _start_app('zero-instance-app', _ZERO_INSTANCE_APP_INSTANCES) result = list_deployments(1, 'zero-instance-app') returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'deployment', 'rollback', result[0]['id']]) result = json.loads(stdout.decode('utf-8')) assert returncode == 0 assert 'deploymentId' in result assert 'version' in result assert stderr == b'' watch_all_deployments() list_deployments(0) def test_stop_deployment(): with _zero_instance_app(): _start_app('zero-instance-app', _ZERO_INSTANCE_APP_INSTANCES) result = list_deployments(1, 'zero-instance-app') assert_command( ['dcos', 'marathon', 'deployment', 'stop', result[0]['id']]) list_deployments(0) def test_watching_missing_deployment(): watch_deployment('missing-deployment', 1) def test_watching_deployment(): with _zero_instance_app(): _start_app('zero-instance-app', 10) result = list_deployments(1, 'zero-instance-app') watch_deployment(result[0]['id'], 60) list_deployments(0, 'zero-instance-app') def test_list_empty_task(): _list_tasks(0) def test_list_empty_task_not_running_app(): with _zero_instance_app(): _list_tasks(0) def test_list_tasks(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() _list_tasks(3) def test_list_tasks_table(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() assert_lines(['dcos', 'marathon', 'task', 'list'], 4) def test_list_app_tasks(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() _list_tasks(3, 'zero-instance-app') def test_list_missing_app_tasks(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() _list_tasks(0, 'missing-id') def test_show_missing_task(): returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'task', 'show', 'missing-id']) stderr = stderr.decode('utf-8') assert returncode == 1 assert stdout == b'' assert stderr.startswith("Task '") assert stderr.endswith("' does not exist\n") def test_show_task(): with _zero_instance_app(): _start_app('zero-instance-app', 3) watch_all_deployments() result = _list_tasks(3, 'zero-instance-app') returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'task', 'show', result[0]['id']]) result = json.loads(stdout.decode('utf-8')) assert returncode == 0 assert result['appId'] == '/zero-instance-app' assert stderr == b'' def test_bad_configuration(): config_set('marathon.url', 'http://localhost:88888') returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'list']) assert returncode == 1 assert stdout == b'' assert stderr.startswith( b"URL [http://localhost:88888/v2/info] is unreachable") config_unset('marathon.url') def test_app_locked_error(): with app('tests/data/marathon/apps/sleep_many_instances.json', '/sleep-many-instances', wait=False): assert_command( ['dcos', 'marathon', 'app', 'stop', 'sleep-many-instances'], returncode=1, stderr=(b'App or group is locked by one or more deployments. ' b'Override with --force.\n')) def test_app_add_no_tty(): proc, master = popen_tty('dcos marathon app add') stdout, stderr = proc.communicate() os.close(master) print(stdout) print(stderr) assert proc.wait() == 1 assert stdout == b'' assert stderr == (b"We currently don't support reading from the TTY. " b"Please specify an application JSON.\n" b"E.g.: dcos marathon app add < app_resource.json\n") def _list_apps(app_id=None): returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'list', '--json']) result = json.loads(stdout.decode('utf-8')) if app_id is None: assert len(result) == 0 else: assert len(result) == 1 assert result[0]['id'] == '/' + app_id assert returncode == 0 assert stderr == b'' return result def _start_app(app_id, instances=None): cmd = ['dcos', 'marathon', 'app', 'start', app_id] if instances is not None: cmd.append(str(instances)) returncode, stdout, stderr = exec_command(cmd) assert returncode == 0 assert stdout.decode().startswith('Created deployment ') assert stderr == b'' def _update_app(app_id, file_path): with open(file_path) as fd: returncode, stdout, stderr = exec_command( ['dcos', 'marathon', 'app', 'update', app_id], stdin=fd) assert returncode == 0 assert stdout.decode().startswith('Created deployment ') assert stderr == b'' def _list_versions(app_id, expected_count, max_count=None): cmd = ['dcos', 'marathon', 'app', 'version', 'list', app_id] if max_count is not None: cmd.append('--max-count={}'.format(max_count)) returncode, stdout, stderr = exec_command(cmd) result = json.loads(stdout.decode('utf-8')) assert returncode == 0 assert isinstance(result, list) assert len(result) == expected_count assert stderr == b'' def _list_tasks(expected_count=None, app_id=None): cmd = ['dcos', 'marathon', 'task', 'list', '--json'] if app_id is not None: cmd.append(app_id) returncode, stdout, stderr = exec_command(cmd) result = json.loads(stdout.decode('utf-8')) assert returncode == 0 if expected_count: assert len(result) == expected_count assert stderr == b'' return result @contextlib.contextmanager def _zero_instance_app(): with app('tests/data/marathon/apps/zero_instance_sleep.json', 'zero-instance-app'): yield @contextlib.contextmanager def _zero_instance_app_through_http(): class JSONRequestHandler (BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() self.wfile.write(open( 'tests/data/marathon/apps/zero_instance_sleep.json', 'rb').read()) host = 'localhost' port = 12345 server = HTTPServer((host, port), JSONRequestHandler) thread = threading.Thread(target=server.serve_forever) thread.setDaemon(True) thread.start() with app('http://{}:{}'.format(host, port), 'zero-instance-app'): try: yield finally: server.shutdown()