diff options
Diffstat (limited to 'python-dockering')
-rw-r--r-- | python-dockering/.gitignore | 67 | ||||
-rw-r--r-- | python-dockering/ChangeLog.md | 11 | ||||
-rw-r--r-- | python-dockering/LICENSE.txt | 32 | ||||
-rw-r--r-- | python-dockering/README.md | 3 | ||||
-rw-r--r-- | python-dockering/dockering/__init__.py | 21 | ||||
-rw-r--r-- | python-dockering/dockering/config_building.py | 269 | ||||
-rw-r--r-- | python-dockering/dockering/core.py | 136 | ||||
-rw-r--r-- | python-dockering/dockering/exceptions.py | 34 | ||||
-rw-r--r-- | python-dockering/dockering/utils.py | 31 | ||||
-rw-r--r-- | python-dockering/setup.py | 34 | ||||
-rw-r--r-- | python-dockering/tests/test_config_building.py | 150 | ||||
-rw-r--r-- | python-dockering/tests/test_core.py | 71 |
12 files changed, 859 insertions, 0 deletions
diff --git a/python-dockering/.gitignore b/python-dockering/.gitignore new file mode 100644 index 0000000..d11997c --- /dev/null +++ b/python-dockering/.gitignore @@ -0,0 +1,67 @@ +.cloudify +*.swp +*.swn +*.swo +.DS_Store +.project +.pydevproject +venv + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/python-dockering/ChangeLog.md b/python-dockering/ChangeLog.md new file mode 100644 index 0000000..67ae4f8 --- /dev/null +++ b/python-dockering/ChangeLog.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [1.2.0] + +* Add the ability to force reauthentication for Docker login +* Handle connection errors for Docker login diff --git a/python-dockering/LICENSE.txt b/python-dockering/LICENSE.txt new file mode 100644 index 0000000..cb8008a --- /dev/null +++ b/python-dockering/LICENSE.txt @@ -0,0 +1,32 @@ +============LICENSE_START======================================================= +org.onap.dcae +================================================================================ +Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +================================================================================ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============LICENSE_END========================================================= + +ECOMP is a trademark and service mark of AT&T Intellectual Property. + + +Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +=================================================================== +Licensed under the Creative Commons License, Attribution 4.0 Intl. (the "License"); +you may not use this documentation except in compliance with the License. +You may obtain a copy of the License at + https://creativecommons.org/licenses/by/4.0/ +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/python-dockering/README.md b/python-dockering/README.md new file mode 100644 index 0000000..fd8d436 --- /dev/null +++ b/python-dockering/README.md @@ -0,0 +1,3 @@ +# python-dockering + +Library used to manage Docker containers in DCAE. diff --git a/python-dockering/dockering/__init__.py b/python-dockering/dockering/__init__.py new file mode 100644 index 0000000..7c248d8 --- /dev/null +++ b/python-dockering/dockering/__init__.py @@ -0,0 +1,21 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +from dockering.core import * +from dockering.config_building import * diff --git a/python-dockering/dockering/config_building.py b/python-dockering/dockering/config_building.py new file mode 100644 index 0000000..d8e3c84 --- /dev/null +++ b/python-dockering/dockering/config_building.py @@ -0,0 +1,269 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +""" +Abstraction in Docker container configuration +""" +from dockering import utils +from dockering.exceptions import DockerConstructionError + + +# +# Methods to build container envs +# + +def create_envs_healthcheck(docker_config, default_interval="15s", + default_timeout="1s"): + """Extract health check environment variables for Docker containers + + Parameters + ---------- + docker_config: dict where there's an entry called "healthcheck" + + Returns + ------- + dict of Docker envs for healthcheck + """ + # TODO: This has been shamefully lifted from the dcae-cli and should probably + # shared as a library. The unit tests are there. The difference is that + # there are defaults that are passed in here but the defaults should really + # come from the component spec definition. The issue is that nothing forwards + # those defaults. + + envs = dict() + hc = docker_config["healthcheck"] + + # NOTE: For the multiple port, schema scenario, you can explicitly set port + # to schema. For example if image EXPOSE 8080, SERVICE_8080_CHECK_HTTP works. + # https://github.com/gliderlabs/registrator/issues/311 + + if hc["type"] == "http": + envs["SERVICE_CHECK_HTTP"] = hc["endpoint"] + elif hc["type"] == "https": + # WATCH: HTTPS health checks don't work. Seems like Registrator bug. + # Submitted issue https://github.com/gliderlabs/registrator/issues/516 + envs["SERVICE_CHECK_HTTPS"] = hc["endpoint"] + utils.logger.warn("Https-based health checks may not work because Registrator issue #516") + elif hc["type"] == "script": + envs["SERVICE_CHECK_SCRIPT"] = hc["script"] + elif hc["type"] == "docker": + # Note this is only supported in the AT&T open source version of registrator + envs["SERVICE_CHECK_DOCKER_SCRIPT"] = hc["script"] + else: + # You should never get here but not having an else block feels weird + raise DockerConstructionError("Unexpected health check type: {0}".format(hc["type"])) + + envs["SERVICE_CHECK_INTERVAL"] = hc.get("interval", default_interval) + envs["SERVICE_CHECK_TIMEOUT"] = hc.get("timeout", default_timeout) + + return envs + + +def create_envs(service_component_name, *envs): + """Merge all environment variables maps + + Creates a complete environment variables map that is to be used for creating + the container. + + Args: + ----- + envs: Arbitrary list of dicts where each dict is of the structure: + + { + <environment variable name>: <environment variable value>, + <environment variable name>: <environment variable value>, + ... + } + + Returns: + -------- + Dict of all environment variable name to environment variable value + """ + master_envs = { "HOSTNAME": service_component_name, + # For Registrator to register with generated name and not the + # image name + "SERVICE_NAME": service_component_name } + for envs_map in envs: + master_envs.update(envs_map) + return master_envs + + +# +# Methods for volume bindings +# + +def _parse_volumes_param(volumes): + """Parse volumes details for Docker containers from blueprint + + Takes in a list of dicts that contains Docker volume info and + transforms them into docker-py compliant (unflattened) data structures. + Look for the `volumes` parameters under the `run` method on + [this page](https://docker-py.readthedocs.io/en/stable/containers.html) + + Args: + volumes (list): List of + + { + "host": { + "path": <target path on host> + }, + "container": { + "bind": <target path in container>, + "mode": <read/write> + } + } + + Returns: + dict of the form + + { + <target path on host>: { + "bind": <target path in container>, + "mode": <read/write> + } + } + + if volumes is None then returns None + """ + if volumes: + return dict([ (vol["host"]["path"], vol["container"]) for vol in volumes ]) + else: + return None + + +# +# Utility methods used to help build the inputs to create the host_config +# + +def add_host_config_params_volumes(volumes=None, host_config_params=None): + """Add volumes input params + + Args: + ----- + volumes (list): List of + + { + "host": { + "path": <target path on host> + }, + "container": { + "bind": <target path in container>, + "mode": <read/write> + } + } + + host_config_params (dict): Target dict to accumulate host config inputs + + Returns: + -------- + Updated host_config_params + """ +# TODO: USE parse_volumes_param here! + if host_config_params == None: + host_config_params = {} + + host_config_params["binds"] = _parse_volumes_param(volumes) + return host_config_params + +def add_host_config_params_ports(ports=None, host_config_params=None): + """Add ports input params + + Args: + ----- + ports (list): Each port mapping entry is of the form + + "<container ports>:<host port>" + + host_config_params (dict): Target dict to accumulate host config inputs + + Returns: + -------- + Updated host_config_params + """ + if host_config_params == None: + host_config_params = {} + + if ports: + ports = [ port.split(":") for port in ports ] + port_bindings = { port[0]: { "HostPort": port[1] } for port in ports } + host_config_params["port_bindings"] = port_bindings + host_config_params["publish_all_ports"] = False + else: + host_config_params["publish_all_ports"] = True + + return host_config_params + +def add_host_config_params_dns(docker_host, host_config_params=None): + """Add dns input params + + This is not a generic implementation. This method will setup dns with the + expectation that a local consul agent is running on the docker host and will + service the dns requests. + + Args: + ----- + docker_host (string): Docker host ip address which will be used as the dns server + host_config_params (dict): Target dict to accumulate host config inputs + + Returns: + -------- + Updated host_config_params + """ + if host_config_params == None: + host_config_params = {} + + host_config_params["dns"] = [docker_host] + host_config_params["dns_search"] = ["service.consul"] + host_config_params["extra_hosts"] = { "consul": docker_host } + return host_config_params + + +def create_container_config(client, image, envs, host_config_params, tty=False): + """Create docker container config + + Args: + ----- + envs (dict): dict of environment variables to pass into the docker containers. + Gets passed into docker-py.create_container call + host_config_params (dict): Dict of input parameters to the docker-py + "create_host_config" method call + """ + # This is the 1.10.6 approach to binding volumes + # http://docker-py.readthedocs.io/en/1.10.6/volumes.html + volumes = host_config_params.get("bind", None) + target_volumes = [ target["bind"] for target in volumes.values() ] \ + if volumes else None + + host_config = client.create_host_config(**host_config_params) + + if "port_bindings" in host_config_params: + # TODO: Use six for creating the list of ports - six.iterkeys + ports = host_config_params["port_bindings"].keys() + else: + ports = None + + command = "" # This is required... + config = client.create_container_config(image, command, detach=True, tty=tty, + host_config=host_config, ports=ports, + environment=envs, volumes=target_volumes) + + utils.logger.info("Docker container config: {0}".format(config)) + + return config + diff --git a/python-dockering/dockering/core.py b/python-dockering/dockering/core.py new file mode 100644 index 0000000..dcd5908 --- /dev/null +++ b/python-dockering/dockering/core.py @@ -0,0 +1,136 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +import json +import docker +import requests +from dockering.exceptions import DockerError, DockerConnectionError +from dockering import config_building as cb +from dockering import utils + + +# TODO: Replace default for logins to source it from Consul..perhaps + +def create_client(hostname, port, reauth=False, logins=[]): + """Create Docker client + + Args: + ----- + reauth: (boolean) Forces reauthentication e.g. Docker login + """ + base_url = "tcp://{0}:{1}".format(hostname, port) + try: + client = docker.Client(base_url=base_url) + + for dcl in logins: + dcl["reauth"] = reauth + client.login(**dcl) + + return client + except requests.exceptions.ConnectionError as e: + raise DockerConnectionError(str(e)) + + +def create_container_using_config(client, service_component_name, container_config): + try: + image_name = container_config["Image"] + + if not client.images(image_name): + def parse_pull_response(response): + """Pull response is a giant string of JSON messages concatentated + by `\r\n`. This method returns back those messages in the form of + list of dicts.""" + # NOTE: There's a trailing `\r\n` so the last element is empty + # string. Remove that. + return list(map(json.loads, response.split("\r\n")[:-1])) + + def get_error_message(response): + """Attempts to pull out and return an error message from parsed + response if it exists else return None""" + return response[-1].get("error", None) + + # TODO: Implement this as verbose? + # for resp in client.pull(image, stream=True, decode=True): + response = parse_pull_response(client.pull(image_name)) + error_message = get_error_message(response) + + if error_message: + raise DockerError("Error pulling Docker image: {0}".format(error_message)) + else: + utils.logger.info("Pulled Docker image: {0}".format(image_name)) + + return client.create_container_from_config(container_config, + service_component_name) + except requests.exceptions.ConnectionError as e: + # This separates connection failures so that caller can decide what to do. + # Underlying errors this inspired were socket.errors that are sourced + # from http://www.virtsync.com/c-error-codes-include-errno + raise DockerConnectionError(str(e)) + except Exception as e: + raise DockerError(str(e)) + + +def create_container(client, image_name, service_component_name, envs, + host_config_params): + """Creates Docker container + + Args: + ----- + envs (dict): dict of environment variables to pass into the docker containers. + Gets passed into docker-py.create_container call + host_config_params (dict): Dict of input parameters to the docker-py + "create_host_config" method call + """ + config = cb.create_container_config(client, image_name, envs, host_config_params) + return create_container_using_config(client, service_component_name, config) + + +def start_container(client, container): + try: + # TODO: Have logic to inspect response and through NonRecoverableError + # when start fails. Docker-py docs don't quickly tell me what the + # response looks like. + response = client.start(container=container["Id"]) + utils.logger.info("Container started: {0}".format(container["Id"])) + + # TODO: Maybe check stats? + return container["Id"] + except Exception as e: + raise DockerError(str(e)) + + +def stop_then_remove_container(client, service_component_name): + try: + client.stop(service_component_name) + client.remove_container(service_component_name) + except docker.errors.NotFound as e: + raise DockerError("Container not found: {0}".format(service_component_name)) + except Exception as e: + raise DockerError(str(e)) + + +def remove_image(client, image_name): + """Remove the Docker image""" + try: + client.remove_image(image_name) + return True + except: + # Failure to remove image is not classified as terrible..for now + return False + diff --git a/python-dockering/dockering/exceptions.py b/python-dockering/dockering/exceptions.py new file mode 100644 index 0000000..62ea145 --- /dev/null +++ b/python-dockering/dockering/exceptions.py @@ -0,0 +1,34 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +""" +Library's exceptions +""" + +class DockerError(RuntimeError): + """General error""" + pass + +class DockerConnectionError(DockerError): + """Errors connecting to the Docker engine""" + pass + +class DockerConstructionError(DockerError): + """This class of error captures failures in trying to setup the container""" + pass diff --git a/python-dockering/dockering/utils.py b/python-dockering/dockering/utils.py new file mode 100644 index 0000000..e0f651e --- /dev/null +++ b/python-dockering/dockering/utils.py @@ -0,0 +1,31 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +""" +Utility module +""" +import logging + + +# Unified all logging through this single logger in order to easily monkeypatch +# this guy in the Cloudify docker plugin. I also tried monkeypatching a getter +# function that returns a logger but that didn't work. +# WATCH! The monkeypatching in the Cloudify plugin will not work if you import +# this logger with the following syntax: from dockering.utils import logger. +logger = logging.getLogger("dockering") diff --git a/python-dockering/setup.py b/python-dockering/setup.py new file mode 100644 index 0000000..1c51ab9 --- /dev/null +++ b/python-dockering/setup.py @@ -0,0 +1,34 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +import os +from setuptools import setup + +setup( + name='python-dockering', + description='Library used to manage Docker containers in DCAE', + version="1.2.0", + author="Michael Hwang", + email="dcae@lists.openecomp.org", + packages=['dockering'], + zip_safe=False, + install_requires=[ + "docker-py>=1.0.0,<2.0.0" + ] +) diff --git a/python-dockering/tests/test_config_building.py b/python-dockering/tests/test_config_building.py new file mode 100644 index 0000000..c9251e2 --- /dev/null +++ b/python-dockering/tests/test_config_building.py @@ -0,0 +1,150 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +from functools import partial +import pytest +import docker +from dockering import config_building as doc +from dockering.exceptions import DockerConstructionError + + +# The docker-py library sneakily expects version to "know" that there is an +# actual Docker API that you can connect with. +DOCKER_API_VERSION = "1.24" +create_host_config = partial(docker.utils.utils.create_host_config, + version=DOCKER_API_VERSION) + +def test_add_host_config_params_volumes(): + hcp = doc.add_host_config_params_volumes() + hc = create_host_config(**hcp) + expected = { 'NetworkMode': 'default' } + assert expected == hc + + volumes = [{"host": {"path": "some-path-host"}, + "container": {"bind": "some-path-container", "mode": "ro"}}] + hcp = doc.add_host_config_params_volumes(volumes=volumes) + hc = create_host_config(**hcp) + expected = {'Binds': ['some-path-host:some-path-container:ro'], 'NetworkMode': 'default'} + assert expected == hc + + +def test_add_host_config_params_ports(): + ports = [ "22:22", "80:80" ] + hcp = doc.add_host_config_params_ports(ports=ports) + hc = create_host_config(**hcp) + expected = {'PortBindings': {'22/tcp': [{'HostPort': '22', 'HostIp': ''}], + '80/tcp': [{'HostPort': '80', 'HostIp': ''}]}, 'NetworkMode': 'default'} + assert expected == hc + + hcp = doc.add_host_config_params_ports() + hc = create_host_config(**hcp) + expected = {'NetworkMode': 'default', 'PublishAllPorts': True} + assert expected == hc + + +def test_add_host_config_params_dns(): + docker_host = "192.168.1.1" + hcp = doc.add_host_config_params_dns(docker_host) + hc = create_host_config(**hcp) + expected = {'NetworkMode': 'default', 'DnsSearch': ['service.consul'], + 'Dns': ['192.168.1.1'], 'ExtraHosts': ['consul:192.168.1.1']} + assert expected == hc + + +def test_create_envs_healthcheck(): + endpoint = "/foo" + interval = "10s" + timeout = "1s" + + docker_config = { + "healthcheck": { + "type": "http", + "endpoint": endpoint, + "interval": interval, + "timeout": timeout + } + } + + expected = { + "SERVICE_CHECK_HTTP": endpoint, + "SERVICE_CHECK_INTERVAL": interval, + "SERVICE_CHECK_TIMEOUT": timeout + } + + assert expected == doc.create_envs_healthcheck(docker_config) + + docker_config["healthcheck"]["type"] = "https" + expected = { + "SERVICE_CHECK_HTTPS": endpoint, + "SERVICE_CHECK_INTERVAL": interval, + "SERVICE_CHECK_TIMEOUT": timeout + } + + assert expected == doc.create_envs_healthcheck(docker_config) + + # Good case for just script + + script = "/bin/boo" + docker_config["healthcheck"]["type"] = "script" + docker_config["healthcheck"]["script"] = script + expected = { + "SERVICE_CHECK_SCRIPT": script, + "SERVICE_CHECK_INTERVAL": interval, + "SERVICE_CHECK_TIMEOUT": timeout + } + + assert expected == doc.create_envs_healthcheck(docker_config) + + # Good case for Docker script + + script = "/bin/boo" + docker_config["healthcheck"]["type"] = "docker" + docker_config["healthcheck"]["script"] = script + expected = { + "SERVICE_CHECK_DOCKER_SCRIPT": script, + "SERVICE_CHECK_INTERVAL": interval, + "SERVICE_CHECK_TIMEOUT": timeout + } + + assert expected == doc.create_envs_healthcheck(docker_config) + + docker_config["healthcheck"]["type"] = None + with pytest.raises(DockerConstructionError): + doc.create_envs_healthcheck(docker_config) + + +def test_create_envs(): + service_component_name = "foo" + expected_env = { "HOSTNAME": service_component_name, "SERVICE_NAME": service_component_name, + "KEY_ONE": "value_z", "KEY_TWO": "value_y" } + env_one = { "KEY_ONE": "value_z" } + env_two = { "KEY_TWO": "value_y" } + + assert expected_env == doc.create_envs(service_component_name, env_one, env_two) + + +def test_parse_volumes_param(): + volumes = [{ "host": { "path": "/var/run/docker.sock" }, + "container": { "bind": "/tmp/docker.sock", "mode": "ro" } }] + + expected = {'/var/run/docker.sock': {'bind': '/tmp/docker.sock', 'mode': 'ro'}} + actual = doc._parse_volumes_param(volumes) + assert actual == expected + + assert None == doc._parse_volumes_param(None) diff --git a/python-dockering/tests/test_core.py b/python-dockering/tests/test_core.py new file mode 100644 index 0000000..e99dbd4 --- /dev/null +++ b/python-dockering/tests/test_core.py @@ -0,0 +1,71 @@ +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +import os +from functools import partial +import pytest +import docker +from dockering import core as doc +from dockering.exceptions import DockerError, DockerConnectionError + + +def test_create_client(): + # Bad - Could not connect to docker engine + + with pytest.raises(DockerConnectionError): + doc.create_client("fake", 2376, reauth=True) + + +# TODO: Does pytest provide an env file? +CONSUL_HOST = os.environ["CONSUL_HOST"] +EXTERNAL_IP = os.environ["EXTERNAL_IP"] + +@pytest.mark.skip(reason="Need to automatically setup Docker engine and maybe Consul") +def test_create_container(): + client = doc.create_client("127.0.0.1", 2376) + + scn = "unittest-registrator" + consul_host = CONSUL_HOST + # TODO: This may not work until we push the custom registrator into DockerHub + image_name = "registrator:latest" + envs = { "CONSUL_HOST": CONSUL_HOST, + "EXTERNAL_IP": EXTERNAL_IP } + volumes = {'/var/run/docker.sock': {'bind': '/tmp/docker.sock', 'mode': 'ro'}} + + hcp = doc.add_host_config_params_volumes(volumes=volumes) + container = doc.create_container(client, image_name, scn, envs, hcp) + + # Container is a dict with "Id". Check if container name matches scn. + + try: + inspect_result = client.inspect_container(scn) + import pprint + pprint.pprint(inspect_result) + + actual_mounts = inspect_result["Mounts"][0] + assert actual_mounts["Destination"] == volumes.values()[0]["bind"] + assert actual_mounts["Source"] == volumes.keys()[0] + except Exception as e: + raise e + finally: + # Execute teardown/cleanup + try: + doc.stop_then_remove_container(client, scn) + except: + print("Container removal failed") |