From 6289061bd02dbd332eef9da4c7138e424ee8f2f1 Mon Sep 17 00:00:00 2001 From: Craig Allwardt Date: Mon, 1 Jun 2020 16:18:57 -0400 Subject: [PATCH 1/2] Added prototype docker wrapper and fixture for docker --- volttrontesting/fixtures/docker_wrapper.py | 75 ++++++++++++++++++++++ volttrontesting/testutils/test_fixtures.py | 19 +++++- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 volttrontesting/fixtures/docker_wrapper.py diff --git a/volttrontesting/fixtures/docker_wrapper.py b/volttrontesting/fixtures/docker_wrapper.py new file mode 100644 index 0000000000..e6bb4e6140 --- /dev/null +++ b/volttrontesting/fixtures/docker_wrapper.py @@ -0,0 +1,75 @@ +try: + import docker + + HAS_DOCKER = True +except ImportError: + HAS_DOCKER = False + +import time +import contextlib + +# Only allow this function if docker is available from the pip library. +if HAS_DOCKER: + + @contextlib.contextmanager + def create_container(image_name: str, ports: dict = None, env: dict = None, command: (list, str) = None, + startup_time_seconds: int = 30) -> \ + (docker.models.containers.Container, None): + """ Creates a container instance in a context that will clean up after itself. + + This is a wrapper around the docker api documented at https://docker-py.readthedocs.io/en/stable/containers.html. + Some things that this will do automatically for the caller is make the container be ephemeral by removing it + once the context manager is cleaned up. + + This function is being used for long running processes. Short processes may not work correctly because + the execution time might be too short for the returning of data from the container. + + Usage: + + with create_container("mysql", {"3306/tcp": 3306}): + # connect to localhost:3306 with mysql using connector + + :param image_name: The image name (from dockerhub) that is to be instantiated + :param ports: + a dictionary following the convention {'portincontainre/protocoll': portonhost} + + :: + # example port exposing mysql's known port. + {'3306/tcp': 3306} + :param env: + :param command: string or list of commands to run during the startup of the container. + :param startup_time_seconds: Allow this many seconds for the startup of the container before raising a + runtime exception (Download of image and instantiation could take a while) + + :return: + A container object (https://docker-py.readthedocs.io/en/stable/containers.html.) or None + """ + + # Create docker client (Uses localhost as agent connection. + client = docker.from_env() + if ":" in image_name: + client.images.pull(image_name) + else: + # So all tags aren't pulled. According to docs https://docker-py.readthedocs.io/en/stable/images.html. + client.images.pull(image_name + ":latest") + container = client.containers.run(image_name, ports=ports, environment=env, auto_remove=True, detach=True) + + if container is None: + raise RuntimeError(f"Unable to run image {image_name}") + + error_time = time.time() + startup_time_seconds + invalid = False + while container.status != 'running': + if time.time() > error_time: + invalid = True + break + time.sleep(0.1) + container.reload() + + if invalid: + yield None + else: + yield container + + container.kill() + diff --git a/volttrontesting/testutils/test_fixtures.py b/volttrontesting/testutils/test_fixtures.py index 1e26bbb69b..55dbc58e6f 100644 --- a/volttrontesting/testutils/test_fixtures.py +++ b/volttrontesting/testutils/test_fixtures.py @@ -1,6 +1,12 @@ - +import pytest from volttrontesting.fixtures.volttron_platform_fixtures import volttron_instance_web +try: + SKIP_DOCKER = False + from volttrontesting.fixtures.docker_wrapper import create_container +except ImportError: + SKIP_DOCKER = True + def test_web_setup_properly(volttron_instance_web): instance = volttron_instance_web @@ -9,4 +15,15 @@ def test_web_setup_properly(volttron_instance_web): assert instance.bind_web_address == instance.volttron_central_address +@pytest.mark.skipif(SKIP_DOCKER, reason="No docker available in api (install pip install docker) for availability") +def test_docker_wrapper(): + with create_container("mysql") as container: + print(container.status) + print(container.logs()) + + +@pytest.mark.skipif(SKIP_DOCKER, reason="No docker available in api (install pip install docker) for availability") +def test_docker_run_crate_latest(): + with create_container("crate", {"4200/tcp": 4200}) as container: + assert container.status == 'running' From 2559ebe99ffd029ae505bd2ae066a3b0df38dbf1 Mon Sep 17 00:00:00 2001 From: Craig <3979063+craig8@users.noreply.github.com> Date: Wed, 17 Jun 2020 15:17:50 -0700 Subject: [PATCH 2/2] Update docker_wrapper.py --- volttrontesting/fixtures/docker_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volttrontesting/fixtures/docker_wrapper.py b/volttrontesting/fixtures/docker_wrapper.py index e6bb4e6140..5aa025de2a 100644 --- a/volttrontesting/fixtures/docker_wrapper.py +++ b/volttrontesting/fixtures/docker_wrapper.py @@ -31,7 +31,7 @@ def create_container(image_name: str, ports: dict = None, env: dict = None, comm :param image_name: The image name (from dockerhub) that is to be instantiated :param ports: - a dictionary following the convention {'portincontainre/protocoll': portonhost} + a dictionary following the convention {'portincontainre/protocol': portonhost} :: # example port exposing mysql's known port.