├── tests ├── __init__.py ├── context.py ├── test_cli.py ├── test_watcher.py └── test_runner.py ├── requirements.txt ├── MANIFEST.in ├── setup.cfg ├── contrib ├── example_scripts │ └── container │ │ ├── die │ │ └── start ├── rpmbuild.Dockerfile ├── des.service ├── Dockerfile └── build-rpm.sh ├── circle.yml ├── des ├── log.py ├── util.py ├── __init__.py ├── arg.py ├── watcher.py ├── cli.py └── runner.py ├── tox.ini ├── Makefile ├── update_docs.sh ├── .gitignore ├── LICENSE.md ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | docker-py 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | include contrib/* -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [metadata] 5 | description-file=README.rst -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath('..')) 4 | 5 | import des 6 | -------------------------------------------------------------------------------- /contrib/example_scripts/container/die: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Remove the /etc/hosts mapping for a container 3 | sed -i "s/^.*$ACTOR_ID.*\$//" /etc/hosts -------------------------------------------------------------------------------- /contrib/rpmbuild.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | RUN yum -y install rpm-build python-setuptools 3 | ENTRYPOINT ["/usr/bin/rpmbuild","--rebuild"] 4 | -------------------------------------------------------------------------------- /contrib/des.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Docker Event Scripts 3 | Requires=docker.service 4 | After=docker.service 5 | 6 | [Service] 7 | Restart=on-failure 8 | ExecStart=/usr/local/bin/des -d /etc/docker/events.d 9 | 10 | [Install] 11 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /contrib/example_scripts/container/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Map container name to IP in /etc/hosts 3 | CONTAINER_IP=$(docker inspect -f '{{.NetworkSettings.IPAddress}}' $ACTOR_ID) 4 | grep -o ^$CONTAINER_IP /etc/hosts || echo "$CONTAINER_IP $ACTOR_ATTRIBUTES_NAME $ACTOR_ID" >> /etc/hosts -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | post: 5 | - pyenv global 2.7.11 3.5.1 6 | 7 | general: 8 | artifacts: 9 | - dist 10 | 11 | dependencies: 12 | override: 13 | - make clean 14 | - pip install -U tox coverage 15 | test: 16 | override: 17 | - make ci 18 | post: 19 | - bash <(curl -s https://codecov.io/bash) 20 | -------------------------------------------------------------------------------- /des/log.py: -------------------------------------------------------------------------------- 1 | '''Configures logging''' 2 | 3 | from logging import getLogger, Formatter, StreamHandler, INFO, DEBUG 4 | 5 | GLOBAL_LOGGER = getLogger(__name__) 6 | GLOBAL_LOGGER.setLevel(INFO) 7 | SH = StreamHandler() 8 | SH.setLevel(DEBUG) 9 | FORMATTER = Formatter( 10 | '%(asctime)s - docker-event-script - %(levelname)s - %(message)s' 11 | ) 12 | SH.setFormatter(FORMATTER) 13 | GLOBAL_LOGGER.addHandler(SH) 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | [tox] 6 | envlist = py27,py35 7 | 8 | [testenv] 9 | deps = 10 | docker-py 11 | mock 12 | coverage 13 | commands = coverage run -p setup.py test 14 | -------------------------------------------------------------------------------- /contrib/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | COPY ./ /src 3 | RUN apk add -U python3 jq curl && \ 4 | pip3 --no-cache-dir --log /dev/null install --upgrade pip && \ 5 | pip3 --no-cache-dir --log /dev/null install /src && \ 6 | rm -Rf /var/cache/apk/* && \ 7 | des -d /etc/docker-events -c 8 | ENTRYPOINT ["/usr/bin/des"] 9 | CMD ["-d", "/etc/docker-events"] 10 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | '''Testing the CLI functions''' 2 | 3 | from .context import des 4 | 5 | from unittest import TestCase 6 | import mock 7 | 8 | class TestCli(TestCase): 9 | def setUp(self): 10 | pass 11 | 12 | @mock.patch('os.path.exists') 13 | def test_create_dirs_basedir_exists(self, mocked_exists): 14 | mocked_exists.return_value = True 15 | with self.assertRaises(OSError): 16 | des.cli.create_dirs('/test/dir') 17 | -------------------------------------------------------------------------------- /des/util.py: -------------------------------------------------------------------------------- 1 | '''Utility functions''' 2 | 3 | import collections 4 | 5 | def flatten(parent_dict, parent_key='', sep='_'): 6 | '''Flatten a nested dict into a single layer''' 7 | items = [] 8 | for key, val in parent_dict.items(): 9 | new_key = parent_key + sep + key if parent_key else key 10 | if isinstance(val, collections.MutableMapping): 11 | items.extend(flatten(val, new_key, sep=sep).items()) 12 | else: 13 | items.append((new_key, val)) 14 | return dict(items) 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: dist 2 | 3 | test: 4 | tox -r 5 | 6 | coverage: test 7 | coverage combine 8 | coverage xml 9 | 10 | dist-rpm: 11 | contrib/build-rpm.sh 12 | 13 | dist: 14 | python setup.py sdist 15 | python setup.py bdist_egg 16 | @echo 'Distribution file(s) created in dist/' 17 | 18 | pip: 19 | python setup.py sdist upload 20 | 21 | install: 22 | pip install -U . 23 | 24 | clean: 25 | rm -Rf .cache .eggs build .coverage* htmlcov coverage.xml __pycache__ */__pycache__ dist .tox *.egg-info 26 | 27 | ci: test coverage dist 28 | -------------------------------------------------------------------------------- /update_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # build the docs 4 | cd docs 5 | make clean 6 | make html 7 | cd .. 8 | 9 | # commit and push 10 | git add -A 11 | git commit -m "building and pushing docs" 12 | git push origin master 13 | 14 | # switch branches and pull the data we want 15 | git checkout gh-pages 16 | rm -rf . 17 | touch .nojekyll 18 | git checkout master docs/build/html 19 | mv ./docs/build/html/* ./ 20 | rm -rf ./docs 21 | git add -A 22 | git commit -m "publishing updated docs..." 23 | git push origin gh-pages 24 | 25 | # switch back 26 | git checkout master -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # pycharm 7 | .idea/ 8 | .idea 9 | .vscode 10 | 11 | # Packages 12 | *.egg 13 | *.egg-info 14 | build 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | dist 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage* 31 | .tox 32 | nosetests.xml 33 | coverage.xml 34 | 35 | # Complexity 36 | output/*.html 37 | output/*/index.html 38 | 39 | # Sphinx 40 | docs/_build 41 | 42 | # Cookiecutter 43 | output/ 44 | 45 | # virtualenv 46 | py27/ 47 | py35/ 48 | -------------------------------------------------------------------------------- /contrib/build-rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | basedir="$(cd "$(dirname $0)/../"; pwd)" 3 | cd $basedir 4 | 5 | ${basedir}/py27/bin/python setup.py bdist_rpm \ 6 | --requires python-docker-py,python-setuptools \ 7 | --conflicts des \ 8 | --provides des 9 | 10 | rm -Rf ${basedir}/.tmp 11 | mkdir -p ${basedir}/.tmp 12 | 13 | docker run --rm \ 14 | -v "${basedir}/dist:/dist" \ 15 | -v "${basedir}/.tmp:/root/rpmbuild/RPMS/noarch" \ 16 | elcolio/des-rpmbuild $(find dist -regex '.*des.*\.src\.rpm$') 17 | 18 | rm -f ${basedir}/dist/*.rpm 19 | 20 | mv ${basedir}/.tmp/*.rpm ${basedir}/dist/ 21 | 22 | rm -Rf ${basedir}/.tmp 23 | -------------------------------------------------------------------------------- /des/__init__.py: -------------------------------------------------------------------------------- 1 | '''Set the version''' 2 | 3 | __version__ = '0.1.3' 4 | 5 | EVENT_TYPES = { 6 | 'container': ['attach', 'commit', 'copy', 'create', 'destroy', 7 | 'detach', 'die', 'exec_create', 'exec_detach', 8 | 'exec_start', 'export', 'kill', 'oom', 'pause', 9 | 'rename', 'resize', 'restart', 'start', 'stop', 10 | 'top', 'unpause', 'update'], 11 | 'image': ['delete', 'import', 'load', 'pull', 12 | 'push', 'save', 'tag', 'untag'], 13 | 'volume': ['create', 'mount', 'unmount', 'destroy'], 14 | 'network': ['create', 'connect', 'disconnect', 'destroy'], 15 | 'daemon': ['reload'] 16 | } 17 | -------------------------------------------------------------------------------- /des/arg.py: -------------------------------------------------------------------------------- 1 | '''Handle CLI args''' 2 | from argparse import ArgumentParser 3 | from des.__init__ import __version__ 4 | 5 | ARGPARSER = ArgumentParser( 6 | description='Run scripts in response to Docker events' 7 | ) 8 | 9 | ARGPARSER.version = __version__ 10 | 11 | ARGPARSER.add_argument( 12 | '-c', 13 | '--create', 14 | dest='create', 15 | action='store_true', 16 | help='Create script dir structure and exit') 17 | 18 | ARGPARSER.add_argument( 19 | '-e', 20 | dest='docker_url', 21 | action='store', 22 | default="unix:///var/run/docker.sock", 23 | help='Docker endpoint (Default: unix:///var/run/docker.sock)') 24 | 25 | ARGPARSER.add_argument( 26 | '-d', 27 | dest='script_dir', 28 | action='store', 29 | default="/etc/docker/events.d", 30 | help='Event script directory') 31 | 32 | ARGPARSER.add_argument( 33 | '-v', 34 | '--verbose', 35 | dest='verbose', 36 | action='store_true', 37 | help='Verbose output') 38 | 39 | ARGPARSER.add_argument( 40 | '--version', 41 | dest='version', 42 | action='store_true', 43 | help='Print version and exit') 44 | -------------------------------------------------------------------------------- /tests/test_watcher.py: -------------------------------------------------------------------------------- 1 | """Testing the Watcher class""" 2 | 3 | from unittest import TestCase 4 | 5 | import mock 6 | 7 | from .context import des 8 | 9 | 10 | class TestWatcher(TestCase): 11 | def setUp(self): 12 | self.event_dicts = [ 13 | dict(Type='container', Action='start'), 14 | dict(Type='container', Action='die'), 15 | dict(Type='network', Action='create'), 16 | ] 17 | 18 | self.test_watcher = des.watcher.Watcher() 19 | mock_docker_client = mock.MagicMock() 20 | mock_docker_client.events.return_value = iter(self.event_dicts) 21 | self.test_watcher.client = mock_docker_client 22 | 23 | def test_create_watcher(self): 24 | self.assertTrue(isinstance(self.test_watcher, des.watcher.Watcher)) 25 | 26 | def test_watch_events_passes_events_to_action(self): 27 | action = mock.MagicMock() 28 | self.test_watcher.wait_event(action) 29 | action.assert_has_calls([mock.call(d) for d in self.event_dicts], any_order=True) 30 | 31 | def test_watch_events_ensure_callable(self): 32 | with self.assertRaises(ValueError): 33 | self.test_watcher.wait_event(None) 34 | 35 | def test_watch_events_handle_runtime_error(self): 36 | err_action = mock.MagicMock(side_effect=RuntimeError) 37 | self.assertIsNone(self.test_watcher.wait_event(err_action)) 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Cole Brumley 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /des/watcher.py: -------------------------------------------------------------------------------- 1 | '''Watcher Class definition''' 2 | 3 | import os 4 | from datetime import datetime 5 | 6 | # from docker import Client 7 | import docker 8 | from des.log import GLOBAL_LOGGER as logger 9 | 10 | 11 | class Watcher(object): 12 | '''Docker daemon monitor object''' 13 | 14 | def __init__(self, 15 | endpoint='unix:///var/run/docker.sock', 16 | http_timeout=10, 17 | api_version='auto', 18 | tls_config=None): 19 | 20 | self.endpoint = endpoint 21 | self.client = docker.Client( 22 | base_url=endpoint, 23 | version=api_version, 24 | timeout=http_timeout, 25 | tls=tls_config) 26 | 27 | def wait_event(self, action): 28 | '''Wait for events''' 29 | 30 | if not callable(action): 31 | raise ValueError 32 | 33 | logger.info("watching " + self.endpoint + " for container events") 34 | for event in self.client.events(since=datetime.utcnow(), decode=True): 35 | event_key = event['Type'] + os.path.sep + event['Action'] 36 | logger.debug("Detected actionable event " + event_key + ": " + str(event)) 37 | try: 38 | output = action(event) 39 | logger.debug('Finished handling ' + event_key) 40 | if output: 41 | logger.debug("SCRIPT OUTPUT: " + str(output)) 42 | except RuntimeError as err: 43 | logger.error('Failed to execute task in reponse to "' + 44 | event_key + '" :' + str(err)) 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from codecs import open 2 | from os import path 3 | from setuptools import find_packages, setup 4 | 5 | __version__ = '0.1.3' 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | # Get the long description from the README file 10 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | # get the dependencies and installs 14 | with open(path.join(here, 'requirements.txt'), encoding='utf-8') as f: 15 | all_reqs = f.read().split('\n') 16 | 17 | install_requires = [x.strip() for x in all_reqs if 'git+' not in x] 18 | dependency_links = [x.strip().replace('git+', '') for x in all_reqs if 'git+' not in x] 19 | 20 | setup( 21 | name='docker-event-scripts', 22 | version=__version__, 23 | description='Run scripts in response to Docker events.', 24 | long_description=long_description, 25 | url='https://github.com/colebrumley/des', 26 | download_url='https://github.com/colebrumley/des/releases/tag/v' + __version__, 27 | license='MIT', 28 | classifiers=[ 29 | 'Development Status :: 3 - Alpha', 30 | 'Intended Audience :: Developers', 31 | 'Programming Language :: Python', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3.5', 34 | ], 35 | keywords=['docker','event', 'watch', 'monitor', 'script'], 36 | entry_points={ 37 | 'console_scripts': ['des=des.cli:main'], 38 | }, 39 | packages=find_packages(exclude=['docs', 'tests*', 'contrib']), 40 | include_package_data=True, 41 | author='Cole Brumley', 42 | install_requires=install_requires, 43 | dependency_links=dependency_links, 44 | author_email='cole.brumley@gmail.com' 45 | ) 46 | -------------------------------------------------------------------------------- /des/cli.py: -------------------------------------------------------------------------------- 1 | '''CLI entrypoint''' 2 | 3 | from logging import DEBUG 4 | from os import chmod, mkdir, path 5 | 6 | from des.__init__ import EVENT_TYPES as type_list, __version__ 7 | from des.arg import ARGPARSER as parser 8 | from des.log import GLOBAL_LOGGER as logger 9 | from des.runner import ScriptRunner 10 | from des.watcher import Watcher 11 | 12 | 13 | def write_script(loc): 14 | '''Write an empty script to the filesystem''' 15 | logger.info('Creating script '+ loc) 16 | with open(loc, mode='x') as filehandle: 17 | filehandle.write('#!/bin/sh\n') 18 | filehandle.close() 19 | chmod(loc, 0o777) 20 | 21 | 22 | def create_dirs(basedir): 23 | '''Create a generic script dir scaffold''' 24 | if path.exists(basedir): 25 | raise OSError(basedir+' exists!') 26 | 27 | mkdir(basedir) 28 | 29 | for key, event_ary in type_list.items(): 30 | key_path = basedir+path.sep+key 31 | if not path.exists(key_path): 32 | mkdir(key_path) 33 | 34 | for event in event_ary: 35 | event_path = key_path+path.sep+event 36 | if not path.exists(event_path): 37 | write_script(event_path) 38 | 39 | 40 | def main(): 41 | '''Main CLI entrypoint for des''' 42 | args = parser.parse_args() 43 | 44 | if args.version: 45 | print(__version__) 46 | exit(0) 47 | 48 | if args.verbose: 49 | logger.setLevel(DEBUG) 50 | 51 | if args.create: 52 | logger.info('Creating template script directory at '+args.script_dir) 53 | create_dirs(args.script_dir) 54 | exit(0) 55 | 56 | watcher = Watcher() 57 | runner = ScriptRunner(args.script_dir) 58 | 59 | watcher.wait_event(runner.run) 60 | -------------------------------------------------------------------------------- /des/runner.py: -------------------------------------------------------------------------------- 1 | '''Script Runner class''' 2 | 3 | from os.path import sep, exists 4 | from subprocess import STDOUT, CalledProcessError, check_output 5 | 6 | from des.log import GLOBAL_LOGGER as logger 7 | from des.util import flatten 8 | 9 | 10 | class ScriptRunner(object): 11 | '''Executes a script, passing event details via the ENV''' 12 | 13 | def __init__(self, path): 14 | self.basedir = path 15 | 16 | def script_exec(self, script, envarg): 17 | '''Actually execute the script/command''' 18 | try: 19 | result = check_output( 20 | script, 21 | stderr=STDOUT, 22 | env=envarg, 23 | shell=True) 24 | return result 25 | except CalledProcessError as err: 26 | logger.error('Script '+script+' Failed! Exit='+ \ 27 | str(err.returncode)+' Message="'+str(err.output)+'"') 28 | 29 | def build_env(self, event): 30 | '''Translate the event dict into environment variables''' 31 | env_dict = dict() 32 | for key, val in flatten(event).items(): 33 | env_dict[str(key).upper()] = str(val) 34 | return env_dict 35 | 36 | def run(self, event_dict): 37 | '''Handle a docker event''' 38 | 39 | script = self.basedir + sep + event_dict.get('Type') + \ 40 | sep + str(event_dict.get('Action')).split(':')[0] 41 | 42 | if exists(script) == False: 43 | logger.warning('Unable to handle event! No script exists at ' + script) 44 | return 45 | 46 | env_dict = self.build_env(event_dict) 47 | 48 | logger.info('Running script ' + script) 49 | logger.debug('Script ENV: ' + str(env_dict)) 50 | self.script_exec(script, env_dict) 51 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Docker Event Scripts 2 | =============================== 3 | 4 | *Run scripts in response to Docker events.* 5 | 6 | .. image:: https://circleci.com/gh/colebrumley/des.svg?style=svg 7 | .. image:: https://codecov.io/gh/colebrumley/des/branch/master/graph/badge.svg 8 | 9 | **Version**: 0.1.3 10 | **Author**: Cole Brumley 11 | 12 | Overview 13 | -------- 14 | 15 | Use the ``des`` command to monitor `Docker `_ for events. When an event occurs, ``des`` executes a script with the event metadata exported to the environment. 16 | 17 | Installation / Usage 18 | -------------------- 19 | 20 | To install use pip: 21 | 22 | pip install docker-event-scripts 23 | 24 | The script directory is defined via the ``-d`` flag, and defaults to ``/etc/docker/events.d``. Scripts should be placed in sub-directories in the format ``SCRIPT_DIR/EVENT_TYPE/EVENT`` and should be marked executable. 25 | 26 | The environment variables set during each script run are a flattened version of the raw API event dictionary. If you're writing a script for an event you're unfamiliar with, you may want to run it once with ``printenv`` to see what's available. 27 | 28 | See the Docker `docs `_ 29 | for the full list of possible events. 30 | 31 | To generate a full directory structure with dummy scripts for every event, run ``des -c``. 32 | 33 | Contributing 34 | ------------ 35 | 36 | Please submit issues or pull requests via the `GitHub repo `_. 37 | 38 | 39 | Example 40 | ------- 41 | 42 | Using the default script directory, when this example container was started using ``docker run -it --rm alpine sh``, the script at ``/etc/docker/events.d/container/start`` was executed with the following environment: 43 | 44 | 45 | ACTION=start 46 | ACTOR_ATTRIBUTES_IMAGE=alpine 47 | TIME=1471131434 48 | ACTOR_ID=467730e17a0ac265eae034d21cf633755aa57d03483ae479b623bc5569d6c274 49 | STATUS=start 50 | ACTOR_ATTRIBUTES_NAME=backstabbing_lichterman 51 | ID=467730e17a0ac265eae034d21cf633755aa57d03483ae479b623bc5569d6c274 52 | FROM=alpine 53 | TYPE=container 54 | TIMENANO=1471131434853322607 55 | 56 | -------------------------------------------------------------------------------- /tests/test_runner.py: -------------------------------------------------------------------------------- 1 | '''Testing the ScriptRunner class''' 2 | 3 | import os 4 | from sys import getdefaultencoding 5 | from unittest import TestCase 6 | 7 | import mock 8 | 9 | from .context import des 10 | 11 | 12 | class TestRunner(TestCase): 13 | def setUp(self): 14 | self.test_runner = des.runner.ScriptRunner('/tmp') 15 | 16 | def test_create(self): 17 | '''Make sure that TestRunner is in fact a TestRunner''' 18 | self.assertTrue(isinstance(self.test_runner, des.runner.ScriptRunner)) 19 | 20 | def test_build_env_dict_translation(self): 21 | '''Test docker event to environment dict translation''' 22 | event_dict = { 23 | 'status': 'start', 24 | 'Type': 'container', 25 | 'Actor': { 26 | 'Attributes': { 27 | 'Name': 'test-01', 28 | 'Image': 'alpine' 29 | } 30 | } 31 | } 32 | 33 | anticipated_result = { 34 | 'STATUS': 'start', 35 | 'TYPE': 'container', 36 | 'ACTOR_ATTRIBUTES_NAME': 'test-01', 37 | 'ACTOR_ATTRIBUTES_IMAGE': 'alpine' 38 | } 39 | 40 | result = self.test_runner.build_env(event_dict) 41 | self.assertDictEqual(result, anticipated_result) 42 | 43 | def test_script_exec_handles_good_scripts(self): 44 | '''Test good executions''' 45 | expected_output = bytes('ENV_VAR_1=env_val_1\nPWD='+ \ 46 | os.getcwd() +'\n', getdefaultencoding()) 47 | 48 | result = self.test_runner.script_exec('/usr/bin/printenv', 49 | {'ENV_VAR_1': 'env_val_1'}) 50 | 51 | self.assertEqual(result, expected_output) 52 | 53 | def test_script_exec_handles_script_errors(self): 54 | '''Test bad executions''' 55 | self.assertIsNone(self.test_runner.script_exec('/bin/false', {})) 56 | 57 | def test_run_handles_missing_scripts(self): 58 | '''Test missing scripts''' 59 | event = dict(Type='missing', Action='file') 60 | self.assertIsNone(self.test_runner.run(event)) 61 | 62 | def test_run_handles_existing_scripts(self): 63 | '''Test existing scripts''' 64 | self.test_runner.basedir = '' 65 | event = dict(Type='bin', Action='true') 66 | exec_mock = mock.MagicMock() 67 | self.test_runner.script_exec = exec_mock 68 | self.assertIsNone(self.test_runner.run(event)) 69 | exec_mock.assert_called_once_with('/bin/true', {'TYPE': 'bin', 'ACTION': 'true'}) 70 | --------------------------------------------------------------------------------