import json import re import time import pytest from .helpers.common import assert_command, exec_command from .helpers.marathon import (add_pod, pod, pod_spec_json, pods, remove_pod, watch_all_deployments) from ..fixtures.marathon import (DOUBLE_POD_FILE_PATH, DOUBLE_POD_ID, GOOD_POD_FILE_PATH, GOOD_POD_ID, POD_KILL_FILE_PATH, POD_KILL_ID, pod_list_fixture, TRIPLE_POD_FILE_PATH, TRIPLE_POD_ID, UNGOOD_POD_FILE_PATH, UPDATED_GOOD_POD_FILE_PATH) _POD_BASE_CMD = ['dcos', 'marathon', 'pod'] _POD_ADD_CMD = _POD_BASE_CMD + ['add'] _POD_KILL_CMD = _POD_BASE_CMD + ['kill'] _POD_LIST_CMD = _POD_BASE_CMD + ['list'] _POD_REMOVE_CMD = _POD_BASE_CMD + ['remove'] _POD_SHOW_CMD = _POD_BASE_CMD + ['show'] _POD_UPDATE_CMD = _POD_BASE_CMD + ['update'] def test_pod_add_from_file(): add_pod(GOOD_POD_FILE_PATH) remove_pod(GOOD_POD_ID, force=False) watch_all_deployments() def test_pod_list(): expected_json = pod_list_fixture() with pods({GOOD_POD_FILE_PATH: GOOD_POD_ID, DOUBLE_POD_FILE_PATH: DOUBLE_POD_ID, TRIPLE_POD_FILE_PATH: TRIPLE_POD_ID}): _assert_pod_list_json(expected_json) _assert_pod_list_table() def test_pod_update_does_not_support_properties(): cmd = _POD_UPDATE_CMD + ['any-pod', 'foo=bar'] returncode, stdout, stderr = exec_command(cmd) assert returncode == 1 assert stdout.startswith(b'Command not recognized\n') assert stderr == b'' def test_pod_update_from_stdin(): with pod(GOOD_POD_FILE_PATH, GOOD_POD_ID): # The deployment will never complete _assert_pod_update_from_stdin( extra_args=[], pod_json_file_path=UNGOOD_POD_FILE_PATH) # Override above failed deployment _assert_pod_update_from_stdin( extra_args=['--force'], pod_json_file_path=UPDATED_GOOD_POD_FILE_PATH) watch_all_deployments() @pytest.mark.skipif( True, reason='https://mesosphere.atlassian.net/browse/DCOS-13368') def test_pod_kill(): with pod(POD_KILL_FILE_PATH, POD_KILL_ID): kill_1, keep, kill_2 = _get_pod_instance_ids(POD_KILL_ID, 3) remove_args = [POD_KILL_ID, kill_1, kill_2] assert_command(_POD_KILL_CMD + remove_args) new_instance_ids = _get_pod_instance_ids(POD_KILL_ID, 3) assert keep in new_instance_ids assert kill_1 not in new_instance_ids assert kill_2 not in new_instance_ids # Marathon spins up new instances to replace the killed ones assert len(new_instance_ids) == 3 def _pod_add_from_stdin(file_path): cmd = _POD_ADD_CMD with open(file_path) as fd: returncode, stdout, stderr = exec_command(cmd, stdin=fd) assert returncode == 0 assert re.fullmatch('Created deployment \S+\n', stdout.decode('utf-8')) assert stderr == b'' watch_all_deployments() def _assert_pod_list_json(expected_json): cmd = _POD_LIST_CMD + ['--json'] returncode, stdout, stderr = exec_command(cmd) assert returncode == 0 assert stderr == b'' actual_json = json.loads(stdout.decode('utf-8')) _assert_pod_list_json_subset(expected_json, actual_json) def _assert_pod_list_json_subset(expected_json, actual_json): actual_pods_by_id = {pod['id']: pod for pod in actual_json} for expected_pod in expected_json: pod_id = expected_pod['id'] actual_pod = actual_pods_by_id[pod_id] pod_spec_json(expected_pod['spec'], actual_pod['spec']) assert len(actual_json) == len(expected_json) def _assert_pod_list_table(): _wait_for_instances({'/double-pod': 2, '/good-pod': 1, '/winston': 1}) returncode, stdout, stderr = exec_command(_POD_LIST_CMD) assert returncode == 0 assert stderr == b'' stdout_lines = stdout.decode('utf-8').split('\n') pattern = r'ID\+TASKS +INSTANCES +VERSION +STATUS +STATUS SINCE +WAITING *' assert re.fullmatch(pattern, stdout_lines[0]) assert stdout_lines[1].startswith('/double-pod') assert stdout_lines[2].startswith(' |-thing-1') assert stdout_lines[3].startswith(' |-thing-2') assert stdout_lines[4].startswith('/good-pod') assert stdout_lines[5].startswith(' |-good-container') assert stdout_lines[6].startswith('/winston') assert stdout_lines[7].startswith(' |-the-cat') assert stdout_lines[8].startswith(' |-thing-1') assert stdout_lines[9].startswith(' |-thing-2') assert stdout_lines[10] == '' assert len(stdout_lines) == 11 def _assert_pod_update_from_stdin(extra_args, pod_json_file_path): cmd = _POD_UPDATE_CMD + [GOOD_POD_ID] + extra_args with open(pod_json_file_path) as fd: returncode, stdout, stderr = exec_command(cmd, stdin=fd) assert returncode == 0 assert re.fullmatch('Created deployment \S+\n', stdout.decode('utf-8')) assert stderr == b'' def _wait_for_instances(expected_instances, max_attempts=10): """Polls the `pod list` command until the instance counts are as expected. :param expected_instances: a mapping from pod ID to instance count :type expected_instances: {} :param max_attempts: give up and fail the test after this many attempts :type max_attempts: int :rtype: None """ for attempt in range(max_attempts): returncode, stdout, stderr = exec_command(_POD_LIST_CMD + ['--json']) assert returncode == 0 assert stderr == b'' status_json = json.loads(stdout.decode('utf-8')) actual_instances = {pod_obj['id']: len(pod_obj.get('instances', [])) for pod_obj in status_json} if actual_instances == expected_instances: return time.sleep(1) else: assert False, "Timed out waiting for expected instance counts" def _get_pod_instance_ids(pod_id, target_instance_count): """Waits for the given pod to reach a target instance count, then returns the IDs of all instances. :param pod_id: the pod to retrieve the instance IDs from :type pod_id: str :param target_instance_count: waits until the number of instances reaches this number :type target_instance_count: int :returns: a tuple of the pod's instance IDs :rtype: tuple(str) """ _wait_for_instances({'/{}'.format(pod_id): target_instance_count}) show_cmd = _POD_SHOW_CMD + [pod_id] returncode, stdout, stderr = exec_command(show_cmd) assert returncode == 0 assert stderr == b'' pod_status_json = json.loads(stdout.decode('utf-8')) instances_json = pod_status_json['instances'] return tuple(instance['id'] for instance in instances_json)