From 59ff1b2a164cff88f6de4d1225cc25c74a242bc9 Mon Sep 17 00:00:00 2001 From: Caleb Lemoine <21261388+circa10a@users.noreply.github.com> Date: Wed, 7 Nov 2018 22:22:25 -0600 Subject: [PATCH] Keep tags (#48) * rebase * docs * remove extra spaces * remove extra spaces * remove user install * save 5s --- README.md | 15 ++++++++++++++- ouroboros/cli.py | 16 ++++++++-------- ouroboros/defaults.py | 1 + ouroboros/image.py | 4 +++- ouroboros/main.py | 2 +- run_tests.sh | 2 +- setup.py | 2 +- tests/integration/main_test.py | 5 ++--- tests/unit/cli_test.py | 21 +++++++++++++++++++++ tests/unit/main_unit_test.py | 2 ++ 10 files changed, 54 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fd2545a0..e9ef7af9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A python-based alternative to [watchtower](https://github.com/v2tec/watchtower) ## Overview -Ouroboros will monitor all running docker containers or those you specify and update said containers to the latest available image in the remote registry using the `latest` tag with the same parameters that were used when the container was first created such as volume/bind mounts, docker network connections, environment variables, restart policies, entrypoints, commands, etc. +Ouroboros will monitor all running docker containers or those you specify and update said containers to the latest available image in the remote registry using the `latest` tag with the same parameters that were used when the container was first created such as volume/bind mounts, docker network connections, environment variables, restart policies, entrypoints, commands, etc. While ouroboros updates images to `latest` by default, that can be [overridden](#Options) to only monitor updates of a specific tag. Similar to [watchtower](https://github.com/v2tec/watchtower). - Push your image to your registry and simply wait a couple of minutes for ouroboros to find the new image and redeploy your container autonomously. - Limit your server ssh access @@ -97,6 +97,9 @@ docker run --rm circa10a/ouroboros --help - `--cleanup`, `-c` Remove the older docker image if a new one is found and updated. - Default is `False`. - Environment variable: `CLEANUP=true` +- `--keep-tag`, `-k` Only monitor if updates are made to the tag of the image that the container was created with instead of using `latest`. + - Default is `False`. + - Environment variable: `KEEPTAG=true` ### Private Registries @@ -120,6 +123,16 @@ docker run -d --name ouroboros \ ## Examples +### Monitor for updates for original tag + Instead of always updating to `latest` you can specify if you would like Ouroboros to only check for updates for your original container's image tag. + e.g. If your container was start with `nginx:1.14-alpine` using `--keep-tag` will poll the docker registry and compare digests. If there is a new image for `nginx:1.14-alpine`, ouroboros will update your container using the newly patched version. + > Default is `False` + ```bash +docker run -d --name ouroboros \ + -v /var/run/docker.sock:/var/run/docker.sock \ + circa10a/ouroboros --keep-tag +``` + ### Update containers on a remote host Ouroboros can monitor things other than just local, pass the `--url` argument to update a system with the Docker API exposed. diff --git a/ouroboros/cli.py b/ouroboros/cli.py index 452de799..94a35efc 100644 --- a/ouroboros/cli.py +++ b/ouroboros/cli.py @@ -53,24 +53,27 @@ def parse(sysargs): parser.add_argument('-u', '--url', default=defaults.LOCAL_UNIX_SOCKET, 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, dest="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 [], dest="monitor", + 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('-n', '--ignore', nargs='+', default=environ.get('IGNORE') or [], dest='ignore', help='Which container(s) to ignore.') parser.add_argument('-l', '--loglevel', choices=['notset', 'debug', 'info', 'warn', 'error', 'critical'], - dest="loglevel", default=environ.get('LOGLEVEL') or 'info', + 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", + 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, dest="cleanup", + parser.add_argument('-c', '--cleanup', default=environ.get('CLEANUP') or False, dest='cleanup', help='Remove old images after updating', action='store_true') + + parser.add_argument('-k', '--keep-tag', default=environ.get('KEEPTAG') or False, dest='keep_tag', + help='Check for image updates of the same tag instead of pulling latest', action='store_true') args = parser.parse_args(sysargs) if not args.url: @@ -80,6 +83,3 @@ def parse(sysargs): args.url = args.url if checkURI(args.url) else defaults.LOCAL_UNIX_SOCKET return args - - - diff --git a/ouroboros/defaults.py b/ouroboros/defaults.py index ebe515c2..4fa73f4c 100644 --- a/ouroboros/defaults.py +++ b/ouroboros/defaults.py @@ -4,3 +4,4 @@ LOGLEVEL = 'info' RUNONCE = False CLEANUP = False +KEEPTAG = False diff --git a/ouroboros/image.py b/ouroboros/image.py index ae28f144..3849511d 100644 --- a/ouroboros/image.py +++ b/ouroboros/image.py @@ -13,9 +13,11 @@ def check_credentials(): return {} -def pull_latest(image, api_client): +def pull_latest(image, keep_tag, api_client): """Return tag of latest image pulled""" latest_image = image['RepoTags'][0].split(':')[0] + ':latest' + if keep_tag: + latest_image = image['RepoTags'][0] log.debug(f'Pulling image: {latest_image}') api_client.pull(latest_image, auth_config=check_credentials()) return api_client.inspect_image(latest_image) diff --git a/ouroboros/main.py b/ouroboros/main.py index f42e5ae4..7ea66e1f 100644 --- a/ouroboros/main.py +++ b/ouroboros/main.py @@ -16,7 +16,7 @@ def main(args, api_client): current_image = api_client.inspect_image(running_container['Config']['Image']) try: - latest_image = image.pull_latest(image=current_image, api_client=api_client) + latest_image = image.pull_latest(image=current_image, keep_tag=args.keep_tag, api_client=api_client) except docker.errors.APIError as e: log.error(e) continue diff --git a/run_tests.sh b/run_tests.sh index e3a7d1d0..bc5bc1e9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -10,7 +10,7 @@ cd "$( dirname "$0" )" function list_missing_modules { comm -2 -3 \ <( sort ./requirements-dev.txt ) \ - <( pip list format=columns | awk '$0=$1' | sort ) + <( pip list --format=columns | awk '$0=$1' | sort ) } # create associative array of missing modules diff --git a/setup.py b/setup.py index 7e41a3d8..d0da75dc 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read_reqs(requirements): setup( name='ouroboros-cli', - version='0.2.2', + version='0.2.3', description='Automatically update running docker containers', long_description=readme(), long_description_content_type='text/markdown', diff --git a/tests/integration/main_test.py b/tests/integration/main_test.py index bb495423..18ff5b1e 100644 --- a/tests/integration/main_test.py +++ b/tests/integration/main_test.py @@ -59,7 +59,7 @@ def test_create_container(): def test_main(mocker, caplog): mocker.patch('sys.argv', ['']) mocker.patch.dict('os.environ', - {'INTERVAL': '6', + {'INTERVAL': '1', 'LOGLEVEL': 'debug', 'RUNONCE': 'true', 'CLEANUP': 'true', @@ -79,8 +79,7 @@ def test_container_updated(mocker): 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 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 diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 3a5a6c95..4e3010f0 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -145,3 +145,24 @@ def test_cleanup_env_var(mocker, cleanup_env_var, cleanup_env_var_result): mocker.patch('ouroboros.cli') args = cli.parse([]) assert args.cleanup == cleanup_env_var_result + + +@pytest.mark.parametrize('keeptag_args, keeptag_result', [ + (['-k', ], True), + (['--keep-tag', ], True) +]) +def test_keeptag_args(mocker, keeptag_args, keeptag_result): + mocker.patch('ouroboros.cli') + args = cli.parse(keeptag_args) + assert args.keep_tag == keeptag_result + + +@pytest.mark.parametrize('keeptag_env_var, keeptag_env_var_result', [ + ({'KEEPTAG': 'true'}, 'true'), + ({'_KEEPTAG': ''}, defaults.KEEPTAG), +]) +def test_keeptag_env_var(mocker, keeptag_env_var, keeptag_env_var_result): + mocker.patch.dict('os.environ', keeptag_env_var) + mocker.patch('ouroboros.cli') + args = cli.parse([]) + assert args.keep_tag == keeptag_env_var_result \ No newline at end of file diff --git a/tests/unit/main_unit_test.py b/tests/unit/main_unit_test.py index de845119..bd36dcd1 100644 --- a/tests/unit/main_unit_test.py +++ b/tests/unit/main_unit_test.py @@ -33,6 +33,7 @@ def test_main_full(fake_namespace, fake_api, caplog): fake_namespace.monitor = ["testName1"] fake_namespace.ignore = ["derp"] fake_namespace.cleanup = True + fake_namespace.keep_tag = True fake_api.inspect_container.return_value = container_object # called twice fake_api.containers.return_value = [container_object] # called twice @@ -64,6 +65,7 @@ def test_main_exception(fake_namespace, fake_api, caplog): fake_namespace.monitor = ["testName1"] fake_namespace.ignore = ["derp"] fake_namespace.cleanup = True + fake_namespace.keep_tag = True fake_api.inspect_container.return_value = container_object # called twice fake_api.containers.return_value = [container_object] # called twice