Source code for catcher_modules.service.docker

from abc import abstractmethod, ABCMeta
from catcher.steps.external_step import ExternalStep
from catcher.steps.step import update_variables
from catcher.utils.logger import debug


class DockerCmd(metaclass=ABCMeta):
    @abstractmethod
    def action(self, variables):
        pass


class NetworkBasedCmd(metaclass=ABCMeta):
    def __init__(self, network=None, **kwargs) -> None:
        super().__init__()
        self._network = network

    def network(self, variables):
        return self._ensure_network(self._network or variables['TEST_NAME'])

    @staticmethod
    def _ensure_network(name: str):
        import docker
        client = docker.from_env()
        filtered = client.networks.list(names=[name])
        if not filtered:
            return client.networks.create(name, driver="bridge").id
        return filtered[0].id


class IdBasedCmd:
    def __init__(self, **kwargs: dict) -> None:
        self._id = kwargs.get('name', kwargs.get('hash'))
        if self._id is None:
            raise ValueError('no id for container')

    def get_container(self):
        import docker
        client = docker.from_env()
        return client.containers.get(self._id)


class StopCmd(IdBasedCmd, DockerCmd):
    def __init__(self, delete=False, **kwargs: dict) -> None:
        IdBasedCmd.__init__(self, **kwargs)
        self.delete = delete

    def action(self, variables):
        container = self.get_container()
        res = container.stop()
        if self.delete:
            debug('Removing {}'.format(container.name))
            container.remove()
        return res


class StatusCmd(IdBasedCmd, DockerCmd):
    def action(self, variables):
        return self.get_container().status


class InspectCmd(IdBasedCmd, DockerCmd):
    def action(self, variables):
        return self.get_container().attrs


class DisconnectCmd(IdBasedCmd, NetworkBasedCmd, DockerCmd):
    def __init__(self, **kwargs: dict) -> None:
        IdBasedCmd.__init__(self, **kwargs)
        NetworkBasedCmd.__init__(self, **kwargs)

    def action(self, variables):
        import docker
        client = docker.from_env()
        container = self.get_container()
        if not container or container.status == 'exited':
            raise ValueError('Container exited; Can\'t disconnect.')
        return client.networks.get(self.network(variables)).disconnect(container)


class ConnectCmd(IdBasedCmd, NetworkBasedCmd,  DockerCmd):
    def __init__(self, **kwargs: dict) -> None:
        IdBasedCmd.__init__(self, **kwargs)
        NetworkBasedCmd.__init__(self, **kwargs)

    def action(self, variables: str):
        import docker
        client = docker.from_env()
        container = self.get_container()
        if not container or container.status == 'exited':
            raise ValueError('Container exited; can\'t connect.')
        return client.networks.get(self.network(variables)).connect(container)


class LogsCmd(IdBasedCmd, DockerCmd):
    def action(self, variables):
        return self.get_container().logs().decode()


class StartCmd(NetworkBasedCmd, DockerCmd):
    def __init__(self, image: str, **kwargs: dict) -> None:
        super().__init__(**kwargs)
        self._image = image
        self._name = kwargs.get('name')
        self._cmd = kwargs.get('cmd')
        self._detached = kwargs.get('detached', True)
        self._ports = kwargs.get('ports')
        self._env = kwargs.get('environment')
        self._volumes = kwargs.get('volumes', {})

    def action(self, variables):
        import docker
        client = docker.from_env()
        volumes = dict([(k, {'bind': v, 'mode': 'rw'}) for k, v in self._volumes.items()])
        output = client.containers.run(self._image,
                                       self._cmd,
                                       name=self._name,
                                       detach=self._detached,
                                       network=self.network(variables),
                                       ports=self._ports,
                                       environment=self._env,
                                       volumes=volumes)
        if not self._detached:
            return output.decode()
        else:
            return output.id


class ExecCmd(IdBasedCmd, DockerCmd):
    def __init__(self, cmd: str, **kwargs: dict) -> None:
        super().__init__(**kwargs)
        self._cmd = cmd
        self._dir = kwargs.get('dir')
        self._user = kwargs.get('user', 'root')
        self._env = kwargs.get('environment', None)

    def action(self, variables):
        res = self.get_container().exec_run(cmd=self._cmd,
                                            workdir=self._dir,
                                            user=self._user,
                                            environment=self._env)
        if res.exit_code != 0:
            raise Exception(res.output.decode())
        return res.output.decode()


class CmdFactory:
    @staticmethod
    def get_cmd(command: dict) -> DockerCmd:
        if 'start' in command:
            return StartCmd(**command['start'])
        if 'exec' in command:
            return ExecCmd(**command['exec'])
        if 'stop' in command:
            return StopCmd(**command['stop'])
        if 'status' in command:
            return StatusCmd(**command['status'])
        if 'connect' in command:
            return ConnectCmd(**command['connect'])
        if 'disconnect' in command:
            return DisconnectCmd(**command['disconnect'])
        if 'logs' in command:
            return LogsCmd(**command['logs'])
        if 'inspect' in command:
            return InspectCmd(**command['inspect'])
        raise ValueError('Unknown command: ' + str(command))


[docs]class Docker(ExternalStep): """ Allows you to start/stop/disconnect/connect/exec commands, get logs and statuses of `Docker <https://www.docker.com/>`_ containers. Is very useful when you need to run something like `Mockserver <https://www.mock-server.com/>`_ and/or simulate network disconnects. :Input: :start: run container. Return hash. - image: container's image. - name: container's name. *Optional* - cmd: command to run in the container. *Optional* - detached: should it be run detached? *Optional* (default is True) - ports: dictionary of ports to bind. Keys - container ports, values - host ports. - environment: a dictionary of environment variables - volumes: a dictionary of volumes - network: network name. *Optional* (default is current test's name) :stop: stop a container. - name: container's name. *Optional* - hash: container's hash. *Optional* Either name or hash should present - delete: delete a container. *Optional* (default is false) :status: get the container status. - name: container's name. *Optional* - hash: container's hash. *Optional* Either name or hash should present :disconnect: disconnect a container from a network (network failure simulation) - name: container's name. *Optional* - hash: container's hash. *Optional* Either name or hash should present - network: network name. *Optional* (default is current test's name) :connect: connect a container to a network. All containers share the same network per test. - name: container's name. *Optional* - hash: container's hash. *Optional* Either name or hash should present - network: network name. *Optional* (default is current test's name) :exec: execute a command inside a running container. - name: container's name. *Optional* - hash: container's hash. *Optional* Either name or hash should present - cmd: command to execute. - dir: directory, where this command will be executed. *Optional* - user: user to execute this command. *Optional* (default is root) - environment: a dictionary of environment variables :logs: get container's logs. - name: container's name. *Optional* - hash: container's hash. *Optional* Either name or hash should present :inspect: get container's inspect information - name: container's name. *Optional* - hash: container's hash. *Optional* Either name or hash should present :Useful hack: if you are going to run docker step from a docker image - you'd need to mount your host's docker ``/var/run/docker.sock`` directory to the catcher image. docker installation is not included in the catcher's docker image to avoid docker-in-docker problem. :Examples: Run blocking command in a new container and check the output. :: steps: - docker: start: image: 'alpine' cmd: 'echo hello world' detached: false register: {echo: '{{ OUTPUT.strip() }}'} - check: equals: {the: '{{ echo }}', is: 'hello world'} Start named container detached with volumes and environment. :: - docker: start: image: 'my-backend-service' name: 'mock server' ports: '1080/tcp': 8000 environment: POOL_SIZE: 20 OTHER_URL: {{ service1.url }} volumes: '{{ CURRENT_DIR }}/data': '/data' '/tmp/logs': '/var/log/service' Exec command on running container. :: - docker: start: image: 'postgres:alpine' environment: POSTGRES_PASSWORD: test POSTGRES_USER: user POSTGRES_DB: test register: {hash: '{{ OUTPUT }}'} ... - docker: exec: hash: '{{ hash }}' cmd: > psql -U user -d test -c \ "CREATE TABLE test(rno integer, name character varying)" register: {create_result: '{{ OUTPUT.strip() }}'} Get container's logs. :: - docker: start: image: 'alpine' cmd: 'echo hello world' register: {id: '{{ OUTPUT }}'} - docker: logs: hash: '{{ id }}' register: {out: '{{ OUTPUT.strip() }}'} - check: equals: {the: '{{ out }}', is: 'hello world'} Disconnect a container from a network. :: - docker: disconnect: hash: '{{ hash }}' - http: get: url: 'http://localhost:8000/some/path' should_fail: true - docker: connect: hash: '{{ hash }}' """ @update_variables def action(self, includes: dict, variables: dict) -> dict or tuple: cmd = CmdFactory.get_cmd(self.simple_input(variables)) out = cmd.action(variables) return variables, out