goodplay/goodplay

View on GitHub
goodplay/docker_support/__init__.py

Summary

Maintainability
A
1 hr
Test Coverage
# -*- coding: utf-8 -*-

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)

from cached_property import cached_property
from compose.cli.command import get_project
from compose.service import BuildError
from pytest import fail


def is_docker_compose_file(path):
    is_file = path.check(file=True)
    uses_docker_compose_naming = (
        path.basename.startswith('docker-compose.') and
        path.basename.endswith('.yml') and
        '..' not in path.basename)

    return is_file and uses_docker_compose_naming


def config_name_self_and_above(config_name, suffix=None):
    splitted_config_name = config_name.split('.')
    prefix = [splitted_config_name[0]]
    if not suffix:
        suffix = [splitted_config_name[-1]]
    environment_parts = splitted_config_name[1:-1]

    if environment_parts and environment_parts[-1] == 'override':
        environment_parts.pop()

    while environment_parts:
        yield '.'.join(prefix + environment_parts + suffix)
        environment_parts.pop()

    yield '.'.join(prefix + environment_parts + suffix)


def environment_names_for_playbook_path(playbook_path):
    return [environment_name_for_config_path(config_path)
            for config_path in config_paths_for_playbook_path(playbook_path)]


def config_path_for_environment_name(playbook_path, environment_name):
    for config_path in config_paths_for_playbook_path(playbook_path):
        if environment_name_for_config_path(config_path) == environment_name:
            return config_path


def config_paths_for_playbook_path(playbook_path):  # noqa: R701
    base_path = playbook_path.dirpath()

    # get relative config names sorted descending by len
    config_names = [path.relto(base_path) for path in base_path.listdir(is_docker_compose_file)]
    config_names.sort()
    config_names.sort(key=len, reverse=True)

    config_names_set = set(config_names)

    config_paths = []
    already_visited = set()

    for config_name in config_names:
        if config_name in already_visited:
            continue

        config_path = []
        continue_adding_config_paths = True

        for potential_config_override_name, potential_config_name in zip(
                config_name_self_and_above(config_name, suffix=['override', 'yml']),
                config_name_self_and_above(config_name)):
            if continue_adding_config_paths and potential_config_override_name in config_names_set:
                config_path.insert(0, potential_config_override_name)
            if continue_adding_config_paths and potential_config_name in config_names_set:
                continue_adding_config_paths = False
                config_path.insert(0, potential_config_name)

            already_visited.add(potential_config_override_name)
            already_visited.add(potential_config_name)

        config_paths.append(config_path)

    return config_paths


def environment_name_for_config_path(config_path):
    if config_path:
        environment_parts = config_path[-1].split('.')[1:-1]

        if environment_parts and environment_parts[-1] == 'override':
            environment_parts.pop()

        return '.'.join(environment_parts)


class DockerRunner(object):
    def __init__(self, ctx, environment_name=None):
        self.ctx = ctx
        self.environment_name = environment_name

    @cached_property
    def project(self):
        config_path = config_path_for_environment_name(
            self.ctx.playbook_path, self.environment_name)

        return get_project(
            project_dir=self.ctx.playbook_dir_path.strpath,
            config_path=config_path,
            project_name=self.ctx.compose_project_name(self.environment_name))

    def setup(self):
        if self.environment_name is None:
            return

        try:
            # ensure image builds are up-to-date
            # do not pull as image might only be available locally
            self.project.build(pull=False)
        except BuildError as err:
            fail("building service '{0}' failed with reason '{1}'".format(
                err.service.name, err.reason))

        self.project.up()

        inventory_lines = []
        for service in self.project.services:
            container_name = service.get_container().name
            inventory_line = ' '.join((
                service.name,
                'ansible_connection="goodplaydocker"',
                'ansible_become_method="dockerexec"',
                'ansible_host="{}"'.format(container_name),
            ))
            inventory_lines.append(inventory_line)
        inventory_content = '\n'.join(inventory_lines)

        # choose name 'inventory_goodplay' as it comes after 'inventory' when compared,
        # alphanumerical, otherwise Ansible does not merge in the ansible_connection
        # information correctly and thus tries to use an SSH connection
        self.ctx.extended_inventory_path.join('inventory_goodplay').write(inventory_content)

    def teardown(self):
        if self.environment_name is None:
            return

        self.project.kill()
        self.project.down(remove_image_type=False, include_volumes=True)