diff --git a/ouroboros/__init__.py b/ouroboros/__init__.py index d3fd25a0..ff17833a 100644 --- a/ouroboros/__init__.py +++ b/ouroboros/__init__.py @@ -1 +1 @@ -VERSION='0.1.2' +VERSION='0.1.3' diff --git a/ouroboros/cli.py b/ouroboros/cli.py index 860a5a2a..ecc6b07f 100644 --- a/ouroboros/cli.py +++ b/ouroboros/cli.py @@ -5,12 +5,7 @@ import defaults host = '' -interval = '' -monitor = [] -loglevel = '' api_client = None -run_once = None -cleanup = None def checkURI(uri): @@ -53,21 +48,27 @@ def get_interval_env(): return False -def parser(sysargs): +def parse(sysargs): """Declare command line options""" - global host, interval, monitor, loglevel, api_client, run_once, cleanup + global host, api_client parser = argparse.ArgumentParser(description='ouroboros', epilog='Example: python3 main.py -u tcp://1.2.3.4:5678 -i 20 -m container1 container2 -l warn') parser.add_argument('-u', '--url', help='Url for tcp host (defaults to "unix://var/run/docker.sock")') - parser.add_argument('-i', '--interval', type=int, default=get_interval_env() or defaults.INTERVAL, + + parser.add_argument('-i', '--interval', type=int, default=get_interval_env() or defaults.INTERVAL, dest="interval", help='Interval in seconds between checking for updates (defaults to 300s)') - parser.add_argument('-m', '--monitor', nargs='+', default=environ.get('MONITOR') or [], + + parser.add_argument('-m', '--monitor', nargs='+', default=environ.get('MONITOR') or [], dest="monitor", help='Which container to monitor (defaults to all running).') + parser.add_argument('-l', '--loglevel', choices=['notset', 'debug', 'info', 'warn', 'error', 'critical'], - default=environ.get('LOGLEVEL') or 'info', help='Change logger mode (defaults to info)') - parser.add_argument('-r', '--runonce', default=environ.get('RUNONCE') or False, + dest="loglevel", default=environ.get('LOGLEVEL') or 'info', + help='Change logger mode (defaults to info)') + + parser.add_argument('-r', '--runonce', default=environ.get('RUNONCE') or False, dest="run_once", help='Only run ouroboros once then exit', action='store_true') - parser.add_argument('-c', '--cleanup', default=environ.get('CLEANUP') or False, + + parser.add_argument('-c', '--cleanup', default=environ.get('CLEANUP') or False, dest="cleanup", help='Remove old images after updating', action='store_true') args = parser.parse_args(sysargs) @@ -76,10 +77,5 @@ def parser(sysargs): if not checkURI(host): host = defaults.LOCAL_UNIX_SOCKET - interval = args.interval - monitor = args.monitor - loglevel = args.loglevel - run_once = args.runonce - cleanup = args.cleanup api_client = docker.APIClient(base_url=host) return args diff --git a/ouroboros/container.py b/ouroboros/container.py index 1f25ab2d..17a7b358 100644 --- a/ouroboros/container.py +++ b/ouroboros/container.py @@ -3,6 +3,7 @@ log = logging.getLogger(__name__) + def new_container_properties(old_container, new_image): """Store object for spawning new container in place of the one with outdated image""" props = { @@ -16,49 +17,62 @@ def new_container_properties(old_container, new_image): } return props + def running(): """Return running container objects list""" running_containers = [] try: - for container in cli.api_client.containers(filters={'status': 'running'}): + for container in cli.api_client.containers( + filters={'status': 'running'}): if 'ouroboros' not in container['Image']: - running_containers.append(cli.api_client.inspect_container(container)) + running_containers.append( + cli.api_client.inspect_container(container)) return running_containers - except: - log.critical(f'Can\'t connect to Docker API at {cli.api_client.base_url}') + except BaseException: + log.critical( + f'Can\'t connect to Docker API at {cli.api_client.base_url}') -def to_monitor(): + +def to_monitor(monitor=None): """Return filtered running container objects list""" running_containers = [] try: - if cli.monitor: - for container in cli.api_client.containers(filters={'name': cli.monitor, 'status': 'running'}): - running_containers.append(cli.api_client.inspect_container(container)) + if monitor: + for container in cli.api_client.containers( + filters={'name': monitor, 'status': 'running'}): + running_containers.append( + cli.api_client.inspect_container(container)) else: running_containers.extend(running()) log.info(f'{len(running_containers)} running container(s) matched filter') return running_containers - except: - log.critical(f'Can\'t connect to Docker API at {cli.api_client.base_url}') + except BaseException: + log.critical( + f'Can\'t connect to Docker API at {cli.api_client.base_url}') + def get_name(container_object): """Parse out first name of container""" return container_object['Name'].replace('/', '') + def stop(container_object): """Stop out of date container""" log.debug(f'Stopping container: {get_name(container_object)}') return cli.api_client.stop(container_object) + def remove(container_object): """Remove out of date container""" log.debug(f'Removing container: {get_name(container_object)}') return cli.api_client.remove_container(container_object) + def create_new(config): """Create new container with latest image""" return cli.api_client.create_container(**config) + def start(container_object): """Start newly created container with latest image""" log.debug(f"Starting container: {container_object['Id']}") diff --git a/ouroboros/main.py b/ouroboros/main.py index e7637c0d..87ab4855 100644 --- a/ouroboros/main.py +++ b/ouroboros/main.py @@ -10,7 +10,7 @@ from logger import set_logger -def main(): +def main(args): """Find running containers and update them with images using latest tag""" log = logging.getLogger(__name__) if not container.running(): @@ -33,18 +33,18 @@ def main(): container.remove(container_object=running_container) new_container = container.create_new(config=new_config) container.start(container_object=new_container) - if cli.cleanup: + if args.cleanup: image.remove(old_image=current_image) updated_count += 1 log.info(f'{updated_count} container(s) updated') - if cli.run_once: + if args.run_once: exit(0) if __name__ == "__main__": - cli.parser(argv[1:]) - logging.basicConfig(**set_logger(cli.loglevel)) - schedule.every(cli.interval).seconds.do(main) + args = cli.parse(argv[1:]) + logging.basicConfig(**set_logger(args.loglevel)) + schedule.every(args.interval).seconds.do(main, args=args) while True: schedule.run_pending() diff --git a/tests/integration/main_test.py b/tests/integration/main_test.py index d3b3e5e2..3fb77a93 100644 --- a/tests/integration/main_test.py +++ b/tests/integration/main_test.py @@ -13,67 +13,86 @@ test_host_port = 1234 test_container_mount_dest = '/tmp' test_container_props = { - 'name': test_container_name, - 'image': test_image, - 'command': 'tail -f /dev/null', - 'detach': True, - 'ports': [ - '5678' - ], - 'environment': [ - 'testEnvVar=testVar' - ], - 'volumes': [ - '/tmp' - ] + 'name': test_container_name, + 'image': test_image, + 'command': 'tail -f /dev/null', + 'detach': True, + 'ports': [ + '5678' + ], + 'environment': [ + 'testEnvVar=testVar' + ], + 'volumes': [ + '/tmp' + ] } + def test_create_network(): - api_client.create_network(test_network) - assert api_client.inspect_network(test_network)['Name'] == test_network + api_client.create_network(test_network) + assert api_client.inspect_network(test_network)['Name'] == test_network def test_create_container(): - api_client.pull(repository=test_repo, tag=test_tag) - api_client.create_container(**test_container_props, networking_config=api_client.create_networking_config({test_network: api_client.create_endpoint_config()}), - host_config=api_client.create_host_config(binds=[f"{test_container_props['volumes'][0]}:{test_container_mount_dest}"], - port_bindings={test_container_props['ports'][0]: test_host_port})) - api_client.start(test_container_name) - running_container = api_client.containers(filters={'name': test_container_name})[0] - assert running_container['State'] == 'running' - assert running_container['Id'] in api_client.inspect_network(test_network)['Containers'] + api_client.pull(repository=test_repo, tag=test_tag) + api_client.create_container(**test_container_props, networking_config=api_client.create_networking_config({test_network: api_client.create_endpoint_config()}), + host_config=api_client.create_host_config(binds=[f"{test_container_props['volumes'][0]}:{test_container_mount_dest}"], + port_bindings={test_container_props['ports'][0]: test_host_port})) + api_client.start(test_container_name) + running_container = api_client.containers( + filters={'name': test_container_name})[0] + assert running_container['State'] == 'running' + assert running_container['Id'] in api_client.inspect_network(test_network)[ + 'Containers'] + def test_main(mocker): - mocker.patch('sys.argv', ['']) - mocker.patch.dict('os.environ', {'INTERVAL': '6', 'LOGLEVEL': 'debug', 'RUNONCE': 'true', 'CLEANUP': 'true', 'MONITOR': test_container_name}) - with pytest.raises(SystemExit): - assert imp.load_source('__main__', 'ouroboros/main.py') == SystemExit + mocker.patch('sys.argv', ['']) + mocker.patch.dict('os.environ', + {'INTERVAL': '6', + 'LOGLEVEL': 'debug', + 'RUNONCE': 'true', + 'CLEANUP': 'true', + 'MONITOR': test_container_name}) + mocker.patch('ouroboros.cli.api_client', api_client) + + with pytest.raises(SystemExit): + assert imp.load_source('__main__', 'ouroboros/main.py') == SystemExit + def test_container_updated(mocker): - running_container = api_client.containers(filters={'name': test_container_name})[0] - new_container = api_client.inspect_container(running_container) - host_port = new_container['HostConfig']['PortBindings'][f"{test_container_props['ports'][0]}/tcp"][0]['HostPort'] - assert new_container['State']['Status'] == 'running' - assert new_container['Config']['Image'] == f'{test_repo}:latest' - assert new_container['Config']['Cmd'] == test_container_props['command'].split() - assert test_container_props['environment'][0] in new_container['Config']['Env'] - assert new_container['Mounts'][0]['Source'] == test_container_props['volumes'][0] - assert new_container['Mounts'][0]['Destination'] == test_container_mount_dest - assert host_port == str(test_host_port) + running_container = api_client.containers( + filters={'name': test_container_name})[0] + new_container = api_client.inspect_container(running_container) + host_port = new_container['HostConfig']['PortBindings'][ + f"{test_container_props['ports'][0]}/tcp"][0]['HostPort'] + assert new_container['State']['Status'] == 'running' + assert new_container['Config']['Image'] == f'{test_repo}:latest' + assert new_container['Config']['Cmd'] == test_container_props['command'].split( + ) + assert test_container_props['environment'][0] in new_container['Config']['Env'] + assert new_container['Mounts'][0]['Source'] == test_container_props['volumes'][0] + assert new_container['Mounts'][0]['Destination'] == test_container_mount_dest + assert host_port == str(test_host_port) + def test_rm_updated_container(): - running_container = api_client.containers(filters={'name': test_container_name})[0] - api_client.stop(running_container) - api_client.remove_container(running_container) - assert api_client.containers(filters={'name': test_container_name}) == [] + running_container = api_client.containers( + filters={'name': test_container_name})[0] + api_client.stop(running_container) + api_client.remove_container(running_container) + assert api_client.containers(filters={'name': test_container_name}) == [] + def test_rm_test_images(): - images = [test_image, f'{test_repo}:latest'] - for image in images: - if api_client.images(image): - api_client.remove_image(image) - assert api_client.images(name=image) == [] + images = [test_image, f'{test_repo}:latest'] + for image in images: + if api_client.images(image): + api_client.remove_image(image) + assert api_client.images(name=image) == [] + def test_rm_network(): - api_client.remove_network(test_network) - assert api_client.networks(names=test_network) == [] + api_client.remove_network(test_network) + assert api_client.networks(names=test_network) == [] diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 5f0a6e13..236a1244 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -1,8 +1,8 @@ import pytest -from os import environ import ouroboros.cli as cli import ouroboros.defaults as defaults + def test_checkURI(): assert cli.checkURI('tcp://0.0.0.0:1234') assert not cli.checkURI('tcp:/0.0.0.0') @@ -20,46 +20,51 @@ def test_checkURI(): (['-u', 'tcp://0.0.0.0:1234'], 'tcp://0.0.0.0:1234'), (['--url', 'tcp://0.0.0.0:1234'], 'tcp://0.0.0.0:1234') ]) - def test_url_args(mocker, url_args, url_result): mocker.patch('ouroboros.cli') - cli.parser(url_args) + cli.parse(url_args) assert cli.host == url_result # Interval + + @pytest.mark.parametrize('interval_env, interval_env_result', [ ({'INTERVAL': 't'}, False), ({'INTERVAL': '10'}, 10), ]) - def test_get_interval_env(mocker, interval_env, interval_env_result): mocker.patch.dict('os.environ', interval_env) assert cli.get_interval_env() == interval_env_result + def test_interval_arg_invalid_value(mocker): mocker.patch('ouroboros.cli') with pytest.raises(SystemExit) as pytest_wrapped_e: - cli.parser(['--interval', 'test']) - assert pytest_wrapped_e.type == SystemExit + cli.parse(['--interval', 'test']) + assert pytest_wrapped_e.type == SystemExit + def test_interval_arg_valid_value(mocker): mocker.patch('ouroboros.cli') - assert cli.parser(['--interval', 0]) + assert cli.parse(['--interval', 0]) # Monitor + + @pytest.mark.parametrize('monitor_args, monitor_result', [ (['-m', 'test1', 'test2', 'test3'], ['test1', 'test2', 'test3']), (['--monitor', 'test1', 'test2', 'test3'], ['test1', 'test2', 'test3']), (['-m', ''], ['']), (['--monitor', ''], ['']) ]) - def test_monitor_args(mocker, monitor_args, monitor_result): mocker.patch('ouroboros.cli') - cli.parser(monitor_args) - assert cli.monitor == monitor_result + args = cli.parse(monitor_args) + assert args.monitor == monitor_result # Loglevel + + @pytest.mark.parametrize('loglevel_args, loglevel_result', [ (['-l', 'notset'], 'notset'), (['-l', 'info'], 'info'), @@ -74,61 +79,60 @@ def test_monitor_args(mocker, monitor_args, monitor_result): (['--loglevel', 'error'], 'error'), (['--loglevel', 'critical'], 'critical') ]) - def test_loglevel_args(mocker, loglevel_args, loglevel_result): mocker.patch('ouroboros.cli') - cli.parser(loglevel_args) - assert cli.loglevel == loglevel_result + args = cli.parse(loglevel_args) + assert args.loglevel == loglevel_result + @pytest.mark.parametrize('loglevel_env_var, loglevel_env_var_result', [ ({'LOGLEVEL': 'debug'}, 'debug'), ({'_LOGLEVEL': ''}, defaults.LOGLEVEL), ]) - def test_loglevel_env_var(mocker, loglevel_env_var, loglevel_env_var_result): mocker.patch.dict('os.environ', loglevel_env_var) mocker.patch('ouroboros.cli') - cli.parser([]) - assert cli.loglevel == loglevel_env_var_result + args = cli.parse([]) + assert args.loglevel == loglevel_env_var_result + @pytest.mark.parametrize('runonce_args, runonce_result', [ (['-r', ], True), (['--runonce', ], True) ]) - def test_runonce_args(mocker, runonce_args, runonce_result): mocker.patch('ouroboros.cli') - cli.parser(runonce_args) - assert cli.run_once == runonce_result + args = cli.parse(runonce_args) + assert args.run_once == runonce_result + @pytest.mark.parametrize('runonce_env_var, runonce_env_var_result', [ ({'RUNONCE': 'true'}, 'true'), ({'_RUNONCE': ''}, defaults.RUNONCE), ]) - def test_runonce_env_var(mocker, runonce_env_var, runonce_env_var_result): mocker.patch.dict('os.environ', runonce_env_var) mocker.patch('ouroboros.cli') - cli.parser([]) - assert cli.run_once == runonce_env_var_result + args = cli.parse([]) + assert args.run_once == runonce_env_var_result + @pytest.mark.parametrize('cleanup_args, cleanup_result', [ (['-c', ], True), (['--cleanup', ], True) ]) - def test_cleanup_args(mocker, cleanup_args, cleanup_result): mocker.patch('ouroboros.cli') - cli.parser(cleanup_args) - assert cli.cleanup == cleanup_result + args = cli.parse(cleanup_args) + assert args.cleanup == cleanup_result + @pytest.mark.parametrize('cleanup_env_var, cleanup_env_var_result', [ ({'CLEANUP': 'true'}, 'true'), ({'_CLEANUP': ''}, defaults.CLEANUP), ]) - def test_cleanup_env_var(mocker, cleanup_env_var, cleanup_env_var_result): mocker.patch.dict('os.environ', cleanup_env_var) mocker.patch('ouroboros.cli') - cli.parser([]) - assert cli.cleanup == cleanup_env_var_result + args = cli.parse([]) + assert args.cleanup == cleanup_env_var_result diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 291a5882..deaff0e2 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -1,21 +1,49 @@ import pytest import ouroboros.container as container -import ouroboros.defaults as defaults from container_object import container_object + @pytest.fixture() def fake_container(): return container_object + def test_new_container_properties(fake_container): latest = 'busybox:latest' - new_container = container.new_container_properties(fake_container, new_image=latest) + new_container = container.new_container_properties( + fake_container, new_image=latest) assert new_container['name'] == 'testName1' assert new_container['image'] == latest assert new_container['host_config'] == fake_container['HostConfig'] assert new_container['labels'] == fake_container['Config']['Labels'] - assert new_container['entrypoint'] == fake_container['Config']['Entrypoint'] - assert new_container['environment'] == fake_container['Config']['Env'] + assert new_container['entrypoint'] == fake_container['Config']['Entrypoint'] + assert new_container['environment'] == fake_container['Config']['Env'] + def test_get_name(fake_container): - assert container.get_name(fake_container) == 'testName1' \ No newline at end of file + assert container.get_name(fake_container) == 'testName1' + + +def test_to_monitor(mocker): + mocker.patch.object(container.cli, 'api_client') + container.cli.api_client.containers.return_value = [] + + result = container.to_monitor(monitor='test') + assert result == [] + container.cli.api_client.containers.assert_called_once() + + +def test_to_monitor_exception(mocker, caplog): + mocker.patch.object(container.cli, 'api_client') + container.cli.api_client.containers.side_effect = BaseException('I blew up!!') + + container.to_monitor(monitor='test') + assert 'connect to Docker API' in caplog.text + + +def test_running_exception(mocker, caplog): + mocker.patch.object(container.cli, 'api_client') + container.cli.api_client.containers.side_effect = BaseException("I'm blasting off again!") + + container.running() + assert 'connect to Docker API' in caplog.text \ No newline at end of file