Skip to content

Commit

Permalink
Create a class that can execute templated commands
Browse files Browse the repository at this point in the history
A new class was added that takes IPAContainer instance, list of command
templates and mapping with the expanded values, substitutes the commands using
the mappings and runs them succesively on the specifed container. This is the
basis for configurable command steps.

https://github.com/martbab/ipa-docker-test-runner/issues/4
  • Loading branch information
Martin Babinsky committed Nov 16, 2016
1 parent b5512c5 commit a1f5207
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 6 deletions.
6 changes: 3 additions & 3 deletions ipadocker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import docker

from ipadocker import config, constants, container
from ipadocker import command, config, constants, container


DEFAULT_MAKE_TARGET = 'rpms'
Expand Down Expand Up @@ -283,7 +283,7 @@ def run_action(ipaconfig, args, action):
except docker.errors.APIError as e:
logger.error("Docker API returned an error: %s", e)
raise
except container.ContainerExecError as e:
except command.ContainerExecError as e:
logger.error(e)
raise
except Exception as e:
Expand Down Expand Up @@ -328,7 +328,7 @@ def main():

try:
run_action(ipaconfig, args, action)
except container.ContainerExecError as e:
except command.ContainerExecError as e:
sys.exit(e.exit_code)
except Exception as e:
logger.debug(e, exc_info=e)
Expand Down
51 changes: 48 additions & 3 deletions ipadocker/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import logging
import string

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -34,8 +35,9 @@ def exec_command(docker_client, container_id, cmd):
:param container_id: ID of the running container
:param cmd: Command to run, either string or list
:returns: The exit code of command upon completion
:raises: ContainerExecError if the command failed for some reason
"""
exec_logger = logging.getLogger('.'.join([__name__, 'exec']))

if not isinstance(cmd, str):
command = ' '.join(cmd)
Expand All @@ -47,7 +49,50 @@ def exec_command(docker_client, container_id, cmd):
exec_id = docker_client.exec_create(container_id, cmd=bash_command)

for output in docker_client.exec_start(exec_id, stream=True):
logger.info(output.decode().rstrip())
exec_logger.info(output.decode().rstrip())

exec_status = docker_client.exec_inspect(exec_id)
return exec_status["ExitCode"]
exit_code = exec_status["ExitCode"]

if exit_code:
raise ContainerExecError(cmd, exit_code)


class ExecutionStep:
"""
A single step of execution in the container
:param commands: list of command string to execute, including interpolation
variables
:param template_mapping: a mapping containing key-value pairs to substitute
into the command strings
:param kwargs: additional keyword arguments for the template substitution
the keyword arguments take precedence over the values in the mapping as is
expected for the Template string engine
"""
def __init__(self, commands, template_mapping, **kwargs):
self.commands = []

for command in commands:
logger.debug("Command before substitution: %s", command)
cmd_template = string.Template(command)
self.commands.append(
cmd_template.substitute(template_mapping, **kwargs)
)

def __call__(self, container):
"""
Execute the commands in container
:params container: the IPAContainer instance holding container info
:raises: ContainerExecError when the process exists with non-zero
status
"""
container_id = container.container_id
docker_client = container.docker_client

for cmd in self.commands:
logger.info("Executing command: %s", cmd)
exec_command(docker_client, container_id, cmd)
64 changes: 64 additions & 0 deletions tests/test_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Author: Martin Babinsky <martbab@gmail.com>
# See LICENSE file for license

"""
Tests for command execution logic
"""

import pytest

from ipadocker import cli, command, config, constants


@pytest.fixture
def ipaconfig():
"""
flattened default IPA config
"""
return config.IPADockerConfig()


@pytest.fixture()
def flattened_config(request, ipaconfig):
return ipaconfig.flatten()


DEFAULT_BUILDS_SUBSTITUTED = {
'build': ['make {c.DEFAULT_MAKE_TARGET}'.format(c=cli)],
'install_server': [
('ipa-server-install -U --domain ipa.test '
'--realm IPA.TEST -p Secret123 -a Secret123 '
'--setup-dns --auto-forwarders'),
'ipa-kra-install -p Secret123'
],
}


@pytest.fixture(
params=[(k, v) for k, v in DEFAULT_BUILDS_SUBSTITUTED.items()])
def substituted_commands(request):
return request.param


@pytest.fixture
def default_namespace(request, substituted_commands):
parser = cli.make_parser()
subcommand_name = substituted_commands[0].replace('_', '-')
args = parser.parse_args([subcommand_name])
return vars(args)


def test_execution_step_instantiation(ipaconfig, flattened_config,
substituted_commands, default_namespace):
step_name, commands = substituted_commands
command_templates = ipaconfig['steps'][step_name]

step = command.ExecutionStep(command_templates, flattened_config,
**default_namespace)

assert step.commands == commands


def test_invalid_template_string(flattened_config):
with pytest.raises(KeyError):
command.ExecutionStep(['make ${invalid_var}'], flattened_config)

0 comments on commit a1f5207

Please sign in to comment.