First support for Podman (runner + builder)
- '--default-runtime' now supports podman. - podman is now supported in both runner & builder code. On the TODO (for a separate patch): - paunch commands support both runners (in cmd.py) - rename docker to container to make it more clear. Change-Id: I14afdc4a4d377026ca9469897eba71a75a932b06
This commit is contained in:
parent
7e3a56033b
commit
48744077f3
|
@ -18,6 +18,7 @@ import pbr.version
|
|||
import yaml
|
||||
|
||||
from paunch.builder import compose1
|
||||
from paunch.builder import podman
|
||||
from paunch import runner
|
||||
|
||||
__version__ = pbr.version.VersionInfo('paunch').version_string()
|
||||
|
@ -47,21 +48,28 @@ def apply(config_id, config, managed_by, labels=None, docker_cmd=None,
|
|||
overall success of the apply.
|
||||
:rtype: tuple
|
||||
"""
|
||||
# TODO(emilien) Call the right runner based on default_runtime
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
if default_runtime == 'docker':
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
builder = compose1.ComposeV1Builder(
|
||||
config_id=config_id,
|
||||
config=config,
|
||||
runner=r,
|
||||
labels=labels
|
||||
)
|
||||
elif default_runtime == 'podman':
|
||||
r = runner.PodmanRunner(managed_by, docker_cmd=docker_cmd)
|
||||
builder = podman.PodmanBuilder(
|
||||
config_id=config_id,
|
||||
config=config,
|
||||
runner=r,
|
||||
labels=labels
|
||||
)
|
||||
else:
|
||||
LOG.error("container runtime not supported: %s" % default_runtime)
|
||||
return builder.apply()
|
||||
|
||||
|
||||
def cleanup(config_ids, managed_by, docker_cmd=None):
|
||||
def cleanup(config_ids, managed_by, docker_cmd=None, default_runtime='docker'):
|
||||
"""Delete containers no longer applied, rename others to preferred name.
|
||||
|
||||
:param list config_ids: List of config IDs still applied. All containers
|
||||
|
@ -70,25 +78,44 @@ def cleanup(config_ids, managed_by, docker_cmd=None):
|
|||
:param str managed_by: Name of the tool managing the containers. Only
|
||||
containers labelled with this will be modified.
|
||||
:param str docker_cmd: Optional override to the docker command to run.
|
||||
:param str default_runtime: Optional override to the default runtime used
|
||||
for containers.
|
||||
"""
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
if default_runtime == 'docker':
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
elif default_runtime == 'podman':
|
||||
r = runner.PodmanRunner(managed_by, docker_cmd=docker_cmd)
|
||||
LOG.warning("paunch cleanup is partially supported with podman")
|
||||
else:
|
||||
LOG.error("container runtime not supported: %s" % default_runtime)
|
||||
r.delete_missing_configs(config_ids)
|
||||
r.rename_containers()
|
||||
|
||||
|
||||
def list(managed_by, docker_cmd=None):
|
||||
def list(managed_by, docker_cmd=None, default_runtime='docker'):
|
||||
"""List all containers associated with all config IDs.
|
||||
|
||||
:param str managed_by: Name of the tool managing the containers. Only
|
||||
containers labelled with this will be modified.
|
||||
:param str docker_cmd: Optional override to the docker command to run.
|
||||
:param str default_runtime: Optional override to the default runtime used
|
||||
for containers.
|
||||
|
||||
:returns a dict where the key is the config ID and the value is a list of
|
||||
'docker inspect' dicts for each container.
|
||||
:rtype: defaultdict(list)
|
||||
"""
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
if default_runtime == 'docker':
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
elif default_runtime == 'podman':
|
||||
r = runner.PodmanRunner(managed_by, docker_cmd=docker_cmd)
|
||||
else:
|
||||
LOG.error("container runtime not supported: %s" % default_runtime)
|
||||
return r.list_configs()
|
||||
|
||||
|
||||
def debug(config_id, container_name, action, config, managed_by, labels=None,
|
||||
docker_cmd=None):
|
||||
docker_cmd=None, default_runtime='docker'):
|
||||
"""Execute supplied container configuration.
|
||||
|
||||
:param str config_id: Unique config ID, should not be re-used until any
|
||||
|
@ -104,19 +131,32 @@ def debug(config_id, container_name, action, config, managed_by, labels=None,
|
|||
:param dict labels: Optional keys/values of labels to apply to containers
|
||||
created with this invocation.
|
||||
:param str docker_cmd: Optional override to the docker command to run.
|
||||
:param str default_runtime: Optional override to the default runtime used
|
||||
for containers.
|
||||
|
||||
:returns integer return value from running command or failure for any
|
||||
other reason.
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
builder = compose1.ComposeV1Builder(
|
||||
config_id=config_id,
|
||||
config=config,
|
||||
runner=r,
|
||||
labels=labels
|
||||
)
|
||||
if default_runtime == 'docker':
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
builder = compose1.ComposeV1Builder(
|
||||
config_id=config_id,
|
||||
config=config,
|
||||
runner=r,
|
||||
labels=labels
|
||||
)
|
||||
elif default_runtime == 'podman':
|
||||
r = runner.PodmanRunner(managed_by, docker_cmd=docker_cmd)
|
||||
builder = podman.PodmanBuilder(
|
||||
config_id=config_id,
|
||||
config=config,
|
||||
runner=r,
|
||||
labels=labels
|
||||
)
|
||||
else:
|
||||
LOG.error("container runtime not supported: %s" % default_runtime)
|
||||
if action == 'print-cmd':
|
||||
cmd = [
|
||||
r.docker_cmd,
|
||||
|
@ -144,17 +184,25 @@ def debug(config_id, container_name, action, config, managed_by, labels=None,
|
|||
'"print-cmd", or "run"')
|
||||
|
||||
|
||||
def delete(config_ids, managed_by, docker_cmd=None):
|
||||
def delete(config_ids, managed_by, docker_cmd=None, default_runtime='docker'):
|
||||
"""Delete containers with the specified config IDs.
|
||||
|
||||
:param list config_ids: List of config IDs to delete the containers for.
|
||||
:param str managed_by: Name of the tool managing the containers. Only
|
||||
containers labelled with this will be modified.
|
||||
:param str docker_cmd: Optional override to the docker command to run.
|
||||
:param str default_runtime: Optional override to the default runtime used
|
||||
for containers.
|
||||
"""
|
||||
if not config_ids:
|
||||
LOG.warn('No config IDs specified')
|
||||
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
if default_runtime == 'docker':
|
||||
r = runner.DockerRunner(managed_by, docker_cmd=docker_cmd)
|
||||
elif default_runtime == 'podman':
|
||||
r = runner.PodmanRunner(managed_by, docker_cmd=docker_cmd)
|
||||
LOG.warning("paunch cleanup is partially supported with podman")
|
||||
else:
|
||||
LOG.error("container runtime not supported: %s" % default_runtime)
|
||||
for conf_id in config_ids:
|
||||
r.remove_containers(conf_id)
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
# 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.
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import tenacity
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseBuilder(object):
|
||||
|
||||
def __init__(self, config_id, config, runner, labels):
|
||||
self.config_id = config_id
|
||||
self.config = config
|
||||
self.labels = labels
|
||||
self.runner = runner
|
||||
|
||||
def apply(self):
|
||||
|
||||
stdout = []
|
||||
stderr = []
|
||||
pull_returncode = self.pull_missing_images(stdout, stderr)
|
||||
if pull_returncode != 0:
|
||||
return stdout, stderr, pull_returncode
|
||||
|
||||
self.delete_missing_and_updated()
|
||||
|
||||
deploy_status_code = 0
|
||||
key_fltr = lambda k: self.config[k].get('start_order', 0)
|
||||
|
||||
container_names = self.runner.container_names(self.config_id)
|
||||
desired_names = set([cn[-1] for cn in container_names])
|
||||
|
||||
for container in sorted(self.config, key=key_fltr):
|
||||
LOG.debug("Running container: %s" % container)
|
||||
action = self.config[container].get('action', 'run')
|
||||
exit_codes = self.config[container].get('exit_codes', [0])
|
||||
|
||||
if action == 'run':
|
||||
if container in desired_names:
|
||||
LOG.debug('Skipping existing container: %s' % container)
|
||||
continue
|
||||
|
||||
cmd = [
|
||||
self.runner.docker_cmd,
|
||||
'run',
|
||||
'--name',
|
||||
self.runner.unique_container_name(container)
|
||||
]
|
||||
self.label_arguments(cmd, container)
|
||||
self.container_run_args(cmd, container)
|
||||
elif action == 'exec':
|
||||
cmd = [self.runner.docker_cmd, 'exec']
|
||||
self.docker_exec_args(cmd, container)
|
||||
|
||||
(cmd_stdout, cmd_stderr, returncode) = self.runner.execute(cmd)
|
||||
if cmd_stdout:
|
||||
stdout.append(cmd_stdout)
|
||||
if cmd_stderr:
|
||||
stderr.append(cmd_stderr)
|
||||
|
||||
if returncode not in exit_codes:
|
||||
LOG.error("Error running %s. [%s]\n" % (cmd, returncode))
|
||||
LOG.error("stdout: %s" % cmd_stdout)
|
||||
LOG.error("stderr: %s" % cmd_stderr)
|
||||
deploy_status_code = returncode
|
||||
else:
|
||||
LOG.debug('Completed $ %s' % ' '.join(cmd))
|
||||
LOG.info("stdout: %s" % cmd_stdout)
|
||||
LOG.info("stderr: %s" % cmd_stderr)
|
||||
return stdout, stderr, deploy_status_code
|
||||
|
||||
def delete_missing_and_updated(self):
|
||||
container_names = self.runner.container_names(self.config_id)
|
||||
for cn in container_names:
|
||||
container = cn[0]
|
||||
|
||||
# if the desired name is not in the config, delete it
|
||||
if cn[-1] not in self.config:
|
||||
LOG.debug("Deleting container (removed): %s" % container)
|
||||
self.runner.remove_container(container)
|
||||
continue
|
||||
|
||||
ex_data_str = self.runner.inspect(
|
||||
container, '{{index .Config.Labels "config_data"}}')
|
||||
if not ex_data_str:
|
||||
LOG.debug("Deleting container (no config_data): %s"
|
||||
% container)
|
||||
self.runner.remove_container(container)
|
||||
continue
|
||||
|
||||
try:
|
||||
ex_data = json.loads(ex_data_str)
|
||||
except Exception:
|
||||
ex_data = None
|
||||
|
||||
new_data = self.config.get(cn[-1])
|
||||
if new_data != ex_data:
|
||||
LOG.debug("Deleting container (changed config_data): %s"
|
||||
% container)
|
||||
self.runner.remove_container(container)
|
||||
|
||||
# deleting containers is an opportunity for renames to their
|
||||
# preferred name
|
||||
self.runner.rename_containers()
|
||||
|
||||
def label_arguments(self, cmd, container):
|
||||
if self.labels:
|
||||
for i, v in self.labels.items():
|
||||
cmd.extend(['--label', '%s=%s' % (i, v)])
|
||||
cmd.extend([
|
||||
'--label',
|
||||
'config_id=%s' % self.config_id,
|
||||
'--label',
|
||||
'container_name=%s' % container,
|
||||
'--label',
|
||||
'managed_by=%s' % self.runner.managed_by,
|
||||
'--label',
|
||||
'config_data=%s' % json.dumps(self.config.get(container))
|
||||
])
|
||||
|
||||
def boolean_arg(self, cconfig, cmd, key, arg):
|
||||
if cconfig.get(key, False):
|
||||
cmd.append(arg)
|
||||
|
||||
def string_arg(self, cconfig, cmd, key, arg, transform=None):
|
||||
if key in cconfig:
|
||||
if transform:
|
||||
value = transform(cconfig[key])
|
||||
else:
|
||||
value = cconfig[key]
|
||||
cmd.append('%s=%s' % (arg, value))
|
||||
|
||||
def list_or_string_arg(self, cconfig, cmd, key, arg):
|
||||
if key not in cconfig:
|
||||
return
|
||||
value = cconfig[key]
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
for v in value:
|
||||
if v:
|
||||
cmd.append('%s=%s' % (arg, v))
|
||||
|
||||
def list_arg(self, cconfig, cmd, key, arg):
|
||||
if key not in cconfig:
|
||||
return
|
||||
value = cconfig[key]
|
||||
for v in value:
|
||||
if v:
|
||||
cmd.append('%s=%s' % (arg, v))
|
||||
|
||||
def docker_exec_args(self, cmd, container):
|
||||
cconfig = self.config[container]
|
||||
if 'privileged' in cconfig:
|
||||
cmd.append('--privileged=%s' % str(cconfig['privileged']).lower())
|
||||
if 'user' in cconfig:
|
||||
cmd.append('--user=%s' % cconfig['user'])
|
||||
command = self.command_argument(cconfig.get('command'))
|
||||
# for exec, the first argument is the container name,
|
||||
# make sure the correct one is used
|
||||
if command:
|
||||
command[0] = self.runner.discover_container_name(
|
||||
command[0], self.config_id)
|
||||
cmd.extend(command)
|
||||
|
||||
def pull_missing_images(self, stdout, stderr):
|
||||
images = set()
|
||||
for container in self.config:
|
||||
cconfig = self.config[container]
|
||||
image = cconfig.get('image')
|
||||
if image:
|
||||
images.add(image)
|
||||
|
||||
returncode = 0
|
||||
|
||||
for image in sorted(images):
|
||||
|
||||
# only pull if the image does not exist locally
|
||||
if self.runner.inspect(image, format='exists', type='image'):
|
||||
continue
|
||||
|
||||
try:
|
||||
(cmd_stdout, cmd_stderr) = self._pull(image)
|
||||
except PullException as e:
|
||||
returncode = e.rc
|
||||
cmd_stdout = e.stdout
|
||||
cmd_stderr = e.stderr
|
||||
LOG.error("Error pulling %s. [%s]\n" % (image, returncode))
|
||||
LOG.error("stdout: %s" % e.stdout)
|
||||
LOG.error("stderr: %s" % e.stderr)
|
||||
else:
|
||||
LOG.debug('Pulled %s' % image)
|
||||
LOG.info("stdout: %s" % cmd_stdout)
|
||||
LOG.info("stderr: %s" % cmd_stderr)
|
||||
|
||||
if cmd_stdout:
|
||||
stdout.append(cmd_stdout)
|
||||
if cmd_stderr:
|
||||
stderr.append(cmd_stderr)
|
||||
|
||||
return returncode
|
||||
|
||||
@tenacity.retry( # Retry up to 4 times with jittered exponential backoff
|
||||
reraise=True,
|
||||
wait=tenacity.wait_random_exponential(multiplier=1, max=10),
|
||||
stop=tenacity.stop_after_attempt(4)
|
||||
)
|
||||
def _pull(self, image):
|
||||
cmd = [self.runner.docker_cmd, 'pull', image]
|
||||
(stdout, stderr, rc) = self.runner.execute(cmd)
|
||||
if rc != 0:
|
||||
raise PullException(stdout, stderr, rc)
|
||||
return stdout, stderr
|
||||
|
||||
@staticmethod
|
||||
def command_argument(command):
|
||||
if not command:
|
||||
return []
|
||||
if not isinstance(command, list):
|
||||
return command.split()
|
||||
return command
|
||||
|
||||
def lower(self, a):
|
||||
return str(a).lower()
|
||||
|
||||
def duration(self, a):
|
||||
if isinstance(a, (int, float)):
|
||||
return a
|
||||
|
||||
# match groups of the format 5h34m56s
|
||||
m = re.match('^'
|
||||
'(([\d\.]+)h)?'
|
||||
'(([\d\.]+)m)?'
|
||||
'(([\d\.]+)s)?'
|
||||
'(([\d\.]+)ms)?'
|
||||
'(([\d\.]+)us)?'
|
||||
'$', a)
|
||||
|
||||
if not m:
|
||||
# fallback to parsing string as a number
|
||||
return float(a)
|
||||
|
||||
n = 0.0
|
||||
if m.group(2):
|
||||
n += 3600 * float(m.group(2))
|
||||
if m.group(4):
|
||||
n += 60 * float(m.group(4))
|
||||
if m.group(6):
|
||||
n += float(m.group(6))
|
||||
if m.group(8):
|
||||
n += float(m.group(8)) / 1000.0
|
||||
if m.group(10):
|
||||
n += float(m.group(10)) / 1000000.0
|
||||
return n
|
||||
|
||||
|
||||
class PullException(Exception):
|
||||
|
||||
def __init__(self, stdout, stderr, rc):
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.rc = rc
|
|
@ -11,262 +11,14 @@
|
|||
# under the License.
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import tenacity
|
||||
|
||||
from paunch.builder import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseBuilder(object):
|
||||
|
||||
def __init__(self, config_id, config, runner, labels):
|
||||
self.config_id = config_id
|
||||
self.config = config
|
||||
self.labels = labels
|
||||
self.runner = runner
|
||||
|
||||
def apply(self):
|
||||
|
||||
stdout = []
|
||||
stderr = []
|
||||
pull_returncode = self.pull_missing_images(stdout, stderr)
|
||||
if pull_returncode != 0:
|
||||
return stdout, stderr, pull_returncode
|
||||
|
||||
self.delete_missing_and_updated()
|
||||
|
||||
deploy_status_code = 0
|
||||
key_fltr = lambda k: self.config[k].get('start_order', 0)
|
||||
|
||||
container_names = self.runner.container_names(self.config_id)
|
||||
desired_names = set([cn[-1] for cn in container_names])
|
||||
|
||||
for container in sorted(self.config, key=key_fltr):
|
||||
LOG.debug("Running container: %s" % container)
|
||||
action = self.config[container].get('action', 'run')
|
||||
exit_codes = self.config[container].get('exit_codes', [0])
|
||||
|
||||
if action == 'run':
|
||||
if container in desired_names:
|
||||
LOG.debug('Skipping existing container: %s' % container)
|
||||
continue
|
||||
|
||||
cmd = [
|
||||
self.runner.docker_cmd,
|
||||
'run',
|
||||
'--name',
|
||||
self.runner.unique_container_name(container)
|
||||
]
|
||||
self.label_arguments(cmd, container)
|
||||
self.container_run_args(cmd, container)
|
||||
elif action == 'exec':
|
||||
cmd = [self.runner.docker_cmd, 'exec']
|
||||
self.docker_exec_args(cmd, container)
|
||||
|
||||
(cmd_stdout, cmd_stderr, returncode) = self.runner.execute(cmd)
|
||||
if cmd_stdout:
|
||||
stdout.append(cmd_stdout)
|
||||
if cmd_stderr:
|
||||
stderr.append(cmd_stderr)
|
||||
|
||||
if returncode not in exit_codes:
|
||||
LOG.error("Error running %s. [%s]\n" % (cmd, returncode))
|
||||
LOG.error("stdout: %s" % cmd_stdout)
|
||||
LOG.error("stderr: %s" % cmd_stderr)
|
||||
deploy_status_code = returncode
|
||||
else:
|
||||
LOG.debug('Completed $ %s' % ' '.join(cmd))
|
||||
LOG.info("stdout: %s" % cmd_stdout)
|
||||
LOG.info("stderr: %s" % cmd_stderr)
|
||||
return stdout, stderr, deploy_status_code
|
||||
|
||||
def delete_missing_and_updated(self):
|
||||
container_names = self.runner.container_names(self.config_id)
|
||||
for cn in container_names:
|
||||
container = cn[0]
|
||||
|
||||
# if the desired name is not in the config, delete it
|
||||
if cn[-1] not in self.config:
|
||||
LOG.debug("Deleting container (removed): %s" % container)
|
||||
self.runner.remove_container(container)
|
||||
continue
|
||||
|
||||
ex_data_str = self.runner.inspect(
|
||||
container, '{{index .Config.Labels "config_data"}}')
|
||||
if not ex_data_str:
|
||||
LOG.debug("Deleting container (no config_data): %s"
|
||||
% container)
|
||||
self.runner.remove_container(container)
|
||||
continue
|
||||
|
||||
try:
|
||||
ex_data = json.loads(ex_data_str)
|
||||
except Exception:
|
||||
ex_data = None
|
||||
|
||||
new_data = self.config.get(cn[-1])
|
||||
if new_data != ex_data:
|
||||
LOG.debug("Deleting container (changed config_data): %s"
|
||||
% container)
|
||||
self.runner.remove_container(container)
|
||||
|
||||
# deleting containers is an opportunity for renames to their
|
||||
# preferred name
|
||||
self.runner.rename_containers()
|
||||
|
||||
def label_arguments(self, cmd, container):
|
||||
if self.labels:
|
||||
for i, v in self.labels.items():
|
||||
cmd.extend(['--label', '%s=%s' % (i, v)])
|
||||
cmd.extend([
|
||||
'--label',
|
||||
'config_id=%s' % self.config_id,
|
||||
'--label',
|
||||
'container_name=%s' % container,
|
||||
'--label',
|
||||
'managed_by=%s' % self.runner.managed_by,
|
||||
'--label',
|
||||
'config_data=%s' % json.dumps(self.config.get(container))
|
||||
])
|
||||
|
||||
def boolean_arg(self, cconfig, cmd, key, arg):
|
||||
if cconfig.get(key, False):
|
||||
cmd.append(arg)
|
||||
|
||||
def string_arg(self, cconfig, cmd, key, arg, transform=None):
|
||||
if key in cconfig:
|
||||
if transform:
|
||||
value = transform(cconfig[key])
|
||||
else:
|
||||
value = cconfig[key]
|
||||
cmd.append('%s=%s' % (arg, value))
|
||||
|
||||
def list_or_string_arg(self, cconfig, cmd, key, arg):
|
||||
if key not in cconfig:
|
||||
return
|
||||
value = cconfig[key]
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
for v in value:
|
||||
if v:
|
||||
cmd.append('%s=%s' % (arg, v))
|
||||
|
||||
def list_arg(self, cconfig, cmd, key, arg):
|
||||
if key not in cconfig:
|
||||
return
|
||||
value = cconfig[key]
|
||||
for v in value:
|
||||
if v:
|
||||
cmd.append('%s=%s' % (arg, v))
|
||||
|
||||
def docker_exec_args(self, cmd, container):
|
||||
cconfig = self.config[container]
|
||||
if 'privileged' in cconfig:
|
||||
cmd.append('--privileged=%s' % str(cconfig['privileged']).lower())
|
||||
if 'user' in cconfig:
|
||||
cmd.append('--user=%s' % cconfig['user'])
|
||||
command = self.command_argument(cconfig.get('command'))
|
||||
# for exec, the first argument is the container name,
|
||||
# make sure the correct one is used
|
||||
if command:
|
||||
command[0] = self.runner.discover_container_name(
|
||||
command[0], self.config_id)
|
||||
cmd.extend(command)
|
||||
|
||||
def pull_missing_images(self, stdout, stderr):
|
||||
images = set()
|
||||
for container in self.config:
|
||||
cconfig = self.config[container]
|
||||
image = cconfig.get('image')
|
||||
if image:
|
||||
images.add(image)
|
||||
|
||||
returncode = 0
|
||||
|
||||
for image in sorted(images):
|
||||
|
||||
# only pull if the image does not exist locally
|
||||
if self.runner.inspect(image, format='exists', type='image'):
|
||||
continue
|
||||
|
||||
try:
|
||||
(cmd_stdout, cmd_stderr) = self._pull(image)
|
||||
except PullException as e:
|
||||
returncode = e.rc
|
||||
cmd_stdout = e.stdout
|
||||
cmd_stderr = e.stderr
|
||||
LOG.error("Error pulling %s. [%s]\n" % (image, returncode))
|
||||
LOG.error("stdout: %s" % e.stdout)
|
||||
LOG.error("stderr: %s" % e.stderr)
|
||||
else:
|
||||
LOG.debug('Pulled %s' % image)
|
||||
LOG.info("stdout: %s" % cmd_stdout)
|
||||
LOG.info("stderr: %s" % cmd_stderr)
|
||||
|
||||
if cmd_stdout:
|
||||
stdout.append(cmd_stdout)
|
||||
if cmd_stderr:
|
||||
stderr.append(cmd_stderr)
|
||||
|
||||
return returncode
|
||||
|
||||
@tenacity.retry( # Retry up to 4 times with jittered exponential backoff
|
||||
reraise=True,
|
||||
wait=tenacity.wait_random_exponential(multiplier=1, max=10),
|
||||
stop=tenacity.stop_after_attempt(4)
|
||||
)
|
||||
def _pull(self, image):
|
||||
cmd = [self.runner.docker_cmd, 'pull', image]
|
||||
(stdout, stderr, rc) = self.runner.execute(cmd)
|
||||
if rc != 0:
|
||||
raise PullException(stdout, stderr, rc)
|
||||
return stdout, stderr
|
||||
|
||||
@staticmethod
|
||||
def command_argument(command):
|
||||
if not command:
|
||||
return []
|
||||
if not isinstance(command, list):
|
||||
return command.split()
|
||||
return command
|
||||
|
||||
def lower(self, a):
|
||||
return str(a).lower()
|
||||
|
||||
def duration(self, a):
|
||||
if isinstance(a, (int, float)):
|
||||
return a
|
||||
|
||||
# match groups of the format 5h34m56s
|
||||
m = re.match('^'
|
||||
'(([\d\.]+)h)?'
|
||||
'(([\d\.]+)m)?'
|
||||
'(([\d\.]+)s)?'
|
||||
'(([\d\.]+)ms)?'
|
||||
'(([\d\.]+)us)?'
|
||||
'$', a)
|
||||
|
||||
if not m:
|
||||
# fallback to parsing string as a number
|
||||
return float(a)
|
||||
|
||||
n = 0.0
|
||||
if m.group(2):
|
||||
n += 3600 * float(m.group(2))
|
||||
if m.group(4):
|
||||
n += 60 * float(m.group(4))
|
||||
if m.group(6):
|
||||
n += float(m.group(6))
|
||||
if m.group(8):
|
||||
n += float(m.group(8)) / 1000.0
|
||||
if m.group(10):
|
||||
n += float(m.group(10)) / 1000000.0
|
||||
return n
|
||||
|
||||
|
||||
class ComposeV1Builder(BaseBuilder):
|
||||
class ComposeV1Builder(base.BaseBuilder):
|
||||
|
||||
def __init__(self, config_id, config, runner, labels=None):
|
||||
super(ComposeV1Builder, self).__init__(config_id, config, runner,
|
||||
|
@ -348,11 +100,3 @@ class ComposeV1Builder(BaseBuilder):
|
|||
|
||||
cmd.append(cconfig.get('image', ''))
|
||||
cmd.extend(self.command_argument(cconfig.get('command')))
|
||||
|
||||
|
||||
class PullException(Exception):
|
||||
|
||||
def __init__(self, stdout, stderr, rc):
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.rc = rc
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
# 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from paunch.builder import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PodmanBuilder(base.BaseBuilder):
|
||||
|
||||
def __init__(self, config_id, config, runner, labels=None):
|
||||
super(PodmanBuilder, self).__init__(config_id, config, runner,
|
||||
labels)
|
||||
|
||||
def container_run_args(self, cmd, container):
|
||||
cconfig = self.config[container]
|
||||
if cconfig.get('detach', True):
|
||||
cmd.append('--detach=true')
|
||||
self.list_or_string_arg(cconfig, cmd, 'env_file', '--env-file')
|
||||
# TODO(sbaker): support the dict layout for this property
|
||||
for v in cconfig.get('environment', []):
|
||||
if v:
|
||||
cmd.append('--env=%s' % v)
|
||||
self.boolean_arg(cconfig, cmd, 'remove', '--rm')
|
||||
self.boolean_arg(cconfig, cmd, 'interactive', '--interactive')
|
||||
self.boolean_arg(cconfig, cmd, 'tty', '--tty')
|
||||
self.string_arg(cconfig, cmd, 'net', '--net')
|
||||
self.string_arg(cconfig, cmd, 'ipc', '--ipc')
|
||||
self.string_arg(cconfig, cmd, 'pid', '--pid')
|
||||
self.string_arg(cconfig, cmd, 'uts', '--uts')
|
||||
# TODO(sbaker): implement ulimits property, deprecate this ulimit
|
||||
# property
|
||||
for u in cconfig.get('ulimit', []):
|
||||
if u:
|
||||
cmd.append('--ulimit=%s' % u)
|
||||
|
||||
self.string_arg(cconfig, cmd, 'privileged', '--privileged', self.lower)
|
||||
self.string_arg(cconfig, cmd, 'user', '--user')
|
||||
self.list_arg(cconfig, cmd, 'group_add', '--group-add')
|
||||
self.list_arg(cconfig, cmd, 'volumes', '--volume')
|
||||
self.list_arg(cconfig, cmd, 'volumes_from', '--volumes-from')
|
||||
# TODO(sbaker): deprecate log_tag, implement log_driver, log_opt
|
||||
if 'log_tag' in cconfig:
|
||||
cmd.append('--log-opt=tag=%s' % cconfig['log_tag'])
|
||||
self.string_arg(cconfig, cmd, 'cpu_shares', '--cpu-shares')
|
||||
self.string_arg(cconfig, cmd, 'security_opt', '--security-opt')
|
||||
self.string_arg(cconfig, cmd, 'stop_signal', '--stop-signal')
|
||||
|
||||
self.string_arg(cconfig, cmd,
|
||||
'stop_grace_period', '--stop-timeout',
|
||||
self.duration)
|
||||
|
||||
cmd.append(cconfig.get('image', ''))
|
||||
cmd.extend(self.command_argument(cconfig.get('command')))
|
|
@ -129,24 +129,6 @@ class BaseRunner(object):
|
|||
configs[conf_id].append(self.inspect(container))
|
||||
return configs
|
||||
|
||||
|
||||
class DockerRunner(BaseRunner):
|
||||
|
||||
def __init__(self, managed_by, docker_cmd='docker'):
|
||||
docker_cmd = docker_cmd or 'docker'
|
||||
super(DockerRunner, self).__init__(managed_by, docker_cmd)
|
||||
|
||||
def remove_containers(self, conf_id):
|
||||
for container in self.containers_in_config(conf_id):
|
||||
self.remove_container(container)
|
||||
|
||||
def remove_container(self, container):
|
||||
cmd = [self.docker_cmd, 'rm', '-f', container]
|
||||
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
|
||||
if returncode != 0:
|
||||
LOG.error('Error removing container: %s' % container)
|
||||
LOG.error(cmd_stderr)
|
||||
|
||||
def container_names(self, conf_id=None):
|
||||
# list every container name, and its container_name label
|
||||
cmd = [
|
||||
|
@ -167,6 +149,17 @@ class DockerRunner(BaseRunner):
|
|||
if line:
|
||||
yield line.split()
|
||||
|
||||
def remove_containers(self, conf_id):
|
||||
for container in self.containers_in_config(conf_id):
|
||||
self.remove_container(container)
|
||||
|
||||
def remove_container(self, container):
|
||||
cmd = [self.docker_cmd, 'rm', '-f', container]
|
||||
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
|
||||
if returncode != 0:
|
||||
LOG.error('Error removing container: %s' % container)
|
||||
LOG.error(cmd_stderr)
|
||||
|
||||
def rename_containers(self):
|
||||
current_containers = []
|
||||
need_renaming = {}
|
||||
|
@ -192,9 +185,29 @@ class DockerRunner(BaseRunner):
|
|||
self.rename_container(current, desired)
|
||||
current_containers.append(desired)
|
||||
|
||||
|
||||
class DockerRunner(BaseRunner):
|
||||
|
||||
def __init__(self, managed_by, docker_cmd=None):
|
||||
docker_cmd = docker_cmd or 'docker'
|
||||
super(DockerRunner, self).__init__(managed_by, docker_cmd)
|
||||
|
||||
def rename_container(self, container, name):
|
||||
cmd = [self.docker_cmd, 'rename', container, name]
|
||||
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
|
||||
if returncode != 0:
|
||||
LOG.error('Error renaming container: %s' % container)
|
||||
LOG.error(cmd_stderr)
|
||||
|
||||
|
||||
class PodmanRunner(BaseRunner):
|
||||
|
||||
def __init__(self, managed_by, docker_cmd=None):
|
||||
docker_cmd = docker_cmd or 'podman'
|
||||
super(PodmanRunner, self).__init__(managed_by, docker_cmd)
|
||||
|
||||
def rename_container(self, container, name):
|
||||
# TODO(emilien) podman doesn't support rename, we'll handle it
|
||||
# in paunch itself, probably.
|
||||
LOG.warning("container renaming isn't supported by podman")
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,524 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
|
||||
import collections
|
||||
import inspect
|
||||
import json
|
||||
import mock
|
||||
import tenacity
|
||||
|
||||
from paunch.builder import compose1
|
||||
from paunch import runner
|
||||
from paunch.tests import base
|
||||
|
||||
|
||||
class TestBaseBuilder(base.TestCase):
|
||||
|
||||
def test_apply(self):
|
||||
orig_call = tenacity.wait.wait_random_exponential.__call__
|
||||
orig_argspec = inspect.getargspec(orig_call)
|
||||
config = {
|
||||
'one': {
|
||||
'start_order': 0,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'two': {
|
||||
'start_order': 1,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'three': {
|
||||
'start_order': 2,
|
||||
'image': 'centos:6',
|
||||
},
|
||||
'four': {
|
||||
'start_order': 10,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'four_ls': {
|
||||
'action': 'exec',
|
||||
'start_order': 20,
|
||||
'command': ['four', 'ls', '-l', '/']
|
||||
}
|
||||
}
|
||||
|
||||
r = runner.DockerRunner(managed_by='tester', docker_cmd='docker')
|
||||
exe = mock.Mock()
|
||||
exe.side_effect = [
|
||||
('exists', '', 0), # inspect for image centos:6
|
||||
('', '', 1), # inspect for missing image centos:7
|
||||
('Pulled centos:7', 'ouch', 1), # pull centos:6 fails
|
||||
('Pulled centos:7', '', 0), # pull centos:6 succeeds
|
||||
('', '', 0), # ps for delete_missing_and_updated container_names
|
||||
('', '', 0), # ps for after delete_missing_and_updated renames
|
||||
('', '', 0), # ps to only create containers which don't exist
|
||||
('Created one-12345678', '', 0),
|
||||
('Created two-12345678', '', 0),
|
||||
('Created three-12345678', '', 0),
|
||||
('Created four-12345678', '', 0),
|
||||
('a\nb\nc', '', 0)
|
||||
]
|
||||
r.discover_container_name = lambda n, c: '%s-12345678' % n
|
||||
r.unique_container_name = lambda n: '%s-12345678' % n
|
||||
r.execute = exe
|
||||
|
||||
with mock.patch('tenacity.wait.wait_random_exponential.__call__') as f:
|
||||
f.return_value = 0
|
||||
with mock.patch('inspect.getargspec') as mock_args:
|
||||
mock_args.return_value = orig_argspec
|
||||
builder = compose1.ComposeV1Builder('foo', config, r)
|
||||
|
||||
stdout, stderr, deploy_status_code = builder.apply()
|
||||
self.assertEqual(0, deploy_status_code)
|
||||
self.assertEqual([
|
||||
'Pulled centos:7',
|
||||
'Created one-12345678',
|
||||
'Created two-12345678',
|
||||
'Created three-12345678',
|
||||
'Created four-12345678',
|
||||
'a\nb\nc'
|
||||
], stdout)
|
||||
self.assertEqual([], stderr)
|
||||
|
||||
exe.assert_has_calls([
|
||||
# inspect existing image centos:6
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:6']
|
||||
),
|
||||
# inspect and pull missing image centos:7
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:7']
|
||||
),
|
||||
# first pull attempt fails
|
||||
mock.call(
|
||||
['docker', 'pull', 'centos:7']
|
||||
),
|
||||
# second pull attempt succeeds
|
||||
mock.call(
|
||||
['docker', 'pull', 'centos:7']
|
||||
),
|
||||
# ps for delete_missing_and_updated container_names
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# ps for after delete_missing_and_updated renames
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# ps to only create containers which don't exist
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# run one
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'one-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['one']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# run two
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'two-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=two',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['two']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# run three
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'three-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=three',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['three']),
|
||||
'--detach=true', 'centos:6']
|
||||
),
|
||||
# run four
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'four-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=four',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['four']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# execute within four
|
||||
mock.call(
|
||||
['docker', 'exec', 'four-12345678', 'ls', '-l', '/']
|
||||
),
|
||||
])
|
||||
|
||||
def test_apply_idempotency(self):
|
||||
config = {
|
||||
# not running yet
|
||||
'one': {
|
||||
'start_order': 0,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
# running, but with a different config
|
||||
'two': {
|
||||
'start_order': 1,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
# running with the same config
|
||||
'three': {
|
||||
'start_order': 2,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
# not running yet
|
||||
'four': {
|
||||
'start_order': 10,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'four_ls': {
|
||||
'action': 'exec',
|
||||
'start_order': 20,
|
||||
'command': ['four', 'ls', '-l', '/']
|
||||
}
|
||||
}
|
||||
|
||||
r = runner.DockerRunner(managed_by='tester', docker_cmd='docker')
|
||||
exe = mock.Mock()
|
||||
exe.side_effect = [
|
||||
# inspect for image centos:7
|
||||
('exists', '', 0),
|
||||
# ps for delete_missing_and_updated container_names
|
||||
('''five five
|
||||
six six
|
||||
two-12345678 two
|
||||
three-12345678 three''', '', 0),
|
||||
# rm five
|
||||
('', '', 0),
|
||||
# rm six
|
||||
('', '', 0),
|
||||
# inspect two
|
||||
('{"start_order": 1, "image": "centos:6"}', '', 0),
|
||||
# rm two, changed config data
|
||||
('', '', 0),
|
||||
# inspect three
|
||||
('{"start_order": 2, "image": "centos:7"}', '', 0),
|
||||
# ps for after delete_missing_and_updated renames
|
||||
('', '', 0),
|
||||
# ps to only create containers which don't exist
|
||||
('three-12345678 three', '', 0),
|
||||
('Created one-12345678', '', 0),
|
||||
('Created two-12345678', '', 0),
|
||||
('Created four-12345678', '', 0),
|
||||
('a\nb\nc', '', 0)
|
||||
]
|
||||
r.discover_container_name = lambda n, c: '%s-12345678' % n
|
||||
r.unique_container_name = lambda n: '%s-12345678' % n
|
||||
r.execute = exe
|
||||
|
||||
builder = compose1.ComposeV1Builder('foo', config, r)
|
||||
stdout, stderr, deploy_status_code = builder.apply()
|
||||
self.assertEqual(0, deploy_status_code)
|
||||
self.assertEqual([
|
||||
'Created one-12345678',
|
||||
'Created two-12345678',
|
||||
'Created four-12345678',
|
||||
'a\nb\nc'
|
||||
], stdout)
|
||||
self.assertEqual([], stderr)
|
||||
|
||||
exe.assert_has_calls([
|
||||
# inspect image centos:7
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:7']
|
||||
),
|
||||
# ps for delete_missing_and_updated container_names
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# rm containers not in config
|
||||
mock.call(['docker', 'rm', '-f', 'five']),
|
||||
mock.call(['docker', 'rm', '-f', 'six']),
|
||||
# rm two, changed config
|
||||
mock.call(['docker', 'inspect', '--type', 'container',
|
||||
'--format', '{{index .Config.Labels "config_data"}}',
|
||||
'two-12345678']),
|
||||
mock.call(['docker', 'rm', '-f', 'two-12345678']),
|
||||
# check three, config hasn't changed
|
||||
mock.call(['docker', 'inspect', '--type', 'container',
|
||||
'--format', '{{index .Config.Labels "config_data"}}',
|
||||
'three-12345678']),
|
||||
# ps for after delete_missing_and_updated renames
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# ps to only create containers which don't exist
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# run one
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'one-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['one']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# run two
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'two-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=two',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['two']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# don't run three, its already running
|
||||
# run four
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'four-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=four',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['four']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# execute within four
|
||||
mock.call(
|
||||
['docker', 'exec', 'four-12345678', 'ls', '-l', '/']
|
||||
),
|
||||
])
|
||||
|
||||
def test_apply_failed_pull(self):
|
||||
orig_call = tenacity.wait.wait_random_exponential.__call__
|
||||
orig_argspec = inspect.getargspec(orig_call)
|
||||
config = {
|
||||
'one': {
|
||||
'start_order': 0,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'two': {
|
||||
'start_order': 1,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'three': {
|
||||
'start_order': 2,
|
||||
'image': 'centos:6',
|
||||
},
|
||||
'four': {
|
||||
'start_order': 10,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'four_ls': {
|
||||
'action': 'exec',
|
||||
'start_order': 20,
|
||||
'command': ['four', 'ls', '-l', '/']
|
||||
}
|
||||
}
|
||||
|
||||
r = runner.DockerRunner(managed_by='tester', docker_cmd='docker')
|
||||
exe = mock.Mock()
|
||||
exe.side_effect = [
|
||||
('exists', '', 0), # inspect for image centos:6
|
||||
('', '', 1), # inspect for missing image centos:7
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 failure
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 2
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 3
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 4
|
||||
]
|
||||
r.execute = exe
|
||||
|
||||
with mock.patch('tenacity.wait.wait_random_exponential.__call__') as f:
|
||||
f.return_value = 0
|
||||
with mock.patch('inspect.getargspec') as mock_args:
|
||||
mock_args.return_value = orig_argspec
|
||||
builder = compose1.ComposeV1Builder('foo', config, r)
|
||||
|
||||
stdout, stderr, deploy_status_code = builder.apply()
|
||||
self.assertEqual(1, deploy_status_code)
|
||||
self.assertEqual(['Pulling centos:7'], stdout)
|
||||
self.assertEqual(['ouch'], stderr)
|
||||
|
||||
exe.assert_has_calls([
|
||||
# inspect existing image centos:6
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:6']
|
||||
),
|
||||
# inspect and pull missing image centos:7
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:7']
|
||||
),
|
||||
mock.call(
|
||||
['docker', 'pull', 'centos:7']
|
||||
),
|
||||
])
|
||||
|
||||
@mock.patch('paunch.runner.DockerRunner', autospec=True)
|
||||
def test_label_arguments(self, runner):
|
||||
r = runner.return_value
|
||||
r.managed_by = 'tester'
|
||||
builder = compose1.ComposeV1Builder('foo', {}, r)
|
||||
cmd = []
|
||||
builder.label_arguments(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=null'],
|
||||
cmd)
|
||||
|
||||
labels = collections.OrderedDict()
|
||||
labels['foo'] = 'bar'
|
||||
labels['bar'] = 'baz'
|
||||
|
||||
builder = compose1.ComposeV1Builder('foo', {}, r, labels=labels)
|
||||
cmd = []
|
||||
builder.label_arguments(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['--label', 'foo=bar',
|
||||
'--label', 'bar=baz',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=null'],
|
||||
cmd)
|
||||
|
||||
def test_durations(self):
|
||||
config = {
|
||||
'a': {'stop_grace_period': 123},
|
||||
'b': {'stop_grace_period': 123.5},
|
||||
'c': {'stop_grace_period': '123.3'},
|
||||
'd': {'stop_grace_period': '2.5s'},
|
||||
'e': {'stop_grace_period': '10s'},
|
||||
'f': {'stop_grace_period': '1m30s'},
|
||||
'g': {'stop_grace_period': '2h32m'},
|
||||
'h': {'stop_grace_period': '5h34m56s'},
|
||||
'i': {'stop_grace_period': '1h1m1s1ms1us'},
|
||||
}
|
||||
builder = compose1.ComposeV1Builder('foo', config, None)
|
||||
|
||||
result = {
|
||||
'a': '--stop-timeout=123',
|
||||
'b': '--stop-timeout=123.5',
|
||||
'c': '--stop-timeout=123.3',
|
||||
'd': '--stop-timeout=2.5',
|
||||
'e': '--stop-timeout=10.0',
|
||||
'f': '--stop-timeout=90.0',
|
||||
'g': '--stop-timeout=9120.0',
|
||||
'h': '--stop-timeout=20096.0',
|
||||
'i': '--stop-timeout=3661.001001',
|
||||
}
|
||||
|
||||
for container, arg in result.items():
|
||||
cmd = []
|
||||
builder.container_run_args(cmd, container)
|
||||
self.assertIn(arg, cmd)
|
||||
|
||||
def test_container_run_args_lists(self):
|
||||
config = {
|
||||
'one': {
|
||||
'image': 'centos:7',
|
||||
'detach': False,
|
||||
'command': 'ls -l /foo',
|
||||
'remove': True,
|
||||
'tty': True,
|
||||
'interactive': True,
|
||||
'environment': ['FOO=BAR', 'BAR=BAZ'],
|
||||
'env_file': ['/tmp/foo.env', '/tmp/bar.env'],
|
||||
'ulimit': ['nofile=1024', 'nproc=1024'],
|
||||
'volumes': ['/foo:/foo:rw', '/bar:/bar:ro'],
|
||||
'volumes_from': ['two', 'three'],
|
||||
'group_add': ['docker', 'zuul']
|
||||
}
|
||||
}
|
||||
builder = compose1.ComposeV1Builder('foo', config, None)
|
||||
|
||||
cmd = ['docker', 'run', '--name', 'one']
|
||||
builder.container_run_args(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['docker', 'run', '--name', 'one',
|
||||
'--env-file=/tmp/foo.env', '--env-file=/tmp/bar.env',
|
||||
'--env=FOO=BAR', '--env=BAR=BAZ',
|
||||
'--rm', '--interactive', '--tty',
|
||||
'--ulimit=nofile=1024', '--ulimit=nproc=1024',
|
||||
'--group-add=docker', '--group-add=zuul',
|
||||
'--volume=/foo:/foo:rw', '--volume=/bar:/bar:ro',
|
||||
'--volumes-from=two', '--volumes-from=three',
|
||||
'centos:7', 'ls', '-l', '/foo'],
|
||||
cmd
|
||||
)
|
||||
|
||||
@mock.patch('paunch.runner.DockerRunner', autospec=True)
|
||||
def test_docker_exec_args(self, runner):
|
||||
r = runner.return_value
|
||||
r.discover_container_name.return_value = 'one-12345678'
|
||||
config = {
|
||||
'one': {
|
||||
'command': 'ls -l /foo',
|
||||
'privileged': True,
|
||||
'user': 'bar'
|
||||
}
|
||||
}
|
||||
self.builder = compose1.ComposeV1Builder(
|
||||
'foo', config, runner.return_value)
|
||||
|
||||
cmd = ['docker', 'exec']
|
||||
self.builder.docker_exec_args(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['docker', 'exec',
|
||||
'--privileged=true', '--user=bar',
|
||||
'one-12345678', '-l', '/foo'],
|
||||
cmd
|
||||
)
|
||||
|
||||
def test_command_argument(self):
|
||||
b = compose1.ComposeV1Builder
|
||||
self.assertEqual([], b.command_argument(None))
|
||||
self.assertEqual([], b.command_argument(''))
|
||||
self.assertEqual([], b.command_argument([]))
|
||||
self.assertEqual(
|
||||
['ls', '-l', '/foo-bar'],
|
||||
b.command_argument(['ls', '-l', '/foo-bar'])
|
||||
)
|
||||
self.assertEqual(
|
||||
['ls', '-l', '/foo-bar'],
|
||||
b.command_argument('ls -l /foo-bar')
|
||||
)
|
||||
self.assertEqual(
|
||||
['ls', '-l', '/foo bar'],
|
||||
b.command_argument(['ls', '-l', '/foo bar'])
|
||||
)
|
||||
# don't expect quoted spaces to do the right thing
|
||||
self.assertEqual(
|
||||
['ls', '-l', '"/foo', 'bar"'],
|
||||
b.command_argument('ls -l "/foo bar"')
|
||||
)
|
|
@ -12,519 +12,11 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import inspect
|
||||
import json
|
||||
import mock
|
||||
import tenacity
|
||||
|
||||
from paunch.builder import compose1
|
||||
from paunch import runner
|
||||
from paunch.tests import base
|
||||
from paunch.tests import test_builder_base as tbb
|
||||
|
||||
|
||||
class TestBaseBuilder(base.TestCase):
|
||||
|
||||
def test_apply(self):
|
||||
orig_call = tenacity.wait.wait_random_exponential.__call__
|
||||
orig_argspec = inspect.getargspec(orig_call)
|
||||
config = {
|
||||
'one': {
|
||||
'start_order': 0,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'two': {
|
||||
'start_order': 1,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'three': {
|
||||
'start_order': 2,
|
||||
'image': 'centos:6',
|
||||
},
|
||||
'four': {
|
||||
'start_order': 10,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'four_ls': {
|
||||
'action': 'exec',
|
||||
'start_order': 20,
|
||||
'command': ['four', 'ls', '-l', '/']
|
||||
}
|
||||
}
|
||||
|
||||
r = runner.DockerRunner(managed_by='tester', docker_cmd='docker')
|
||||
exe = mock.Mock()
|
||||
exe.side_effect = [
|
||||
('exists', '', 0), # inspect for image centos:6
|
||||
('', '', 1), # inspect for missing image centos:7
|
||||
('Pulled centos:7', 'ouch', 1), # pull centos:6 fails
|
||||
('Pulled centos:7', '', 0), # pull centos:6 succeeds
|
||||
('', '', 0), # ps for delete_missing_and_updated container_names
|
||||
('', '', 0), # ps for after delete_missing_and_updated renames
|
||||
('', '', 0), # ps to only create containers which don't exist
|
||||
('Created one-12345678', '', 0),
|
||||
('Created two-12345678', '', 0),
|
||||
('Created three-12345678', '', 0),
|
||||
('Created four-12345678', '', 0),
|
||||
('a\nb\nc', '', 0)
|
||||
]
|
||||
r.discover_container_name = lambda n, c: '%s-12345678' % n
|
||||
r.unique_container_name = lambda n: '%s-12345678' % n
|
||||
r.execute = exe
|
||||
|
||||
with mock.patch('tenacity.wait.wait_random_exponential.__call__') as f:
|
||||
f.return_value = 0
|
||||
with mock.patch('inspect.getargspec') as mock_args:
|
||||
mock_args.return_value = orig_argspec
|
||||
builder = compose1.ComposeV1Builder('foo', config, r)
|
||||
|
||||
stdout, stderr, deploy_status_code = builder.apply()
|
||||
self.assertEqual(0, deploy_status_code)
|
||||
self.assertEqual([
|
||||
'Pulled centos:7',
|
||||
'Created one-12345678',
|
||||
'Created two-12345678',
|
||||
'Created three-12345678',
|
||||
'Created four-12345678',
|
||||
'a\nb\nc'
|
||||
], stdout)
|
||||
self.assertEqual([], stderr)
|
||||
|
||||
exe.assert_has_calls([
|
||||
# inspect existing image centos:6
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:6']
|
||||
),
|
||||
# inspect and pull missing image centos:7
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:7']
|
||||
),
|
||||
# first pull attempt fails
|
||||
mock.call(
|
||||
['docker', 'pull', 'centos:7']
|
||||
),
|
||||
# second pull attempt succeeds
|
||||
mock.call(
|
||||
['docker', 'pull', 'centos:7']
|
||||
),
|
||||
# ps for delete_missing_and_updated container_names
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# ps for after delete_missing_and_updated renames
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# ps to only create containers which don't exist
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# run one
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'one-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['one']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# run two
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'two-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=two',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['two']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# run three
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'three-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=three',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['three']),
|
||||
'--detach=true', 'centos:6']
|
||||
),
|
||||
# run four
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'four-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=four',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['four']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# execute within four
|
||||
mock.call(
|
||||
['docker', 'exec', 'four-12345678', 'ls', '-l', '/']
|
||||
),
|
||||
])
|
||||
|
||||
def test_apply_idempotency(self):
|
||||
config = {
|
||||
# not running yet
|
||||
'one': {
|
||||
'start_order': 0,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
# running, but with a different config
|
||||
'two': {
|
||||
'start_order': 1,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
# running with the same config
|
||||
'three': {
|
||||
'start_order': 2,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
# not running yet
|
||||
'four': {
|
||||
'start_order': 10,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'four_ls': {
|
||||
'action': 'exec',
|
||||
'start_order': 20,
|
||||
'command': ['four', 'ls', '-l', '/']
|
||||
}
|
||||
}
|
||||
|
||||
r = runner.DockerRunner(managed_by='tester', docker_cmd='docker')
|
||||
exe = mock.Mock()
|
||||
exe.side_effect = [
|
||||
# inspect for image centos:7
|
||||
('exists', '', 0),
|
||||
# ps for delete_missing_and_updated container_names
|
||||
('''five five
|
||||
six six
|
||||
two-12345678 two
|
||||
three-12345678 three''', '', 0),
|
||||
# rm five
|
||||
('', '', 0),
|
||||
# rm six
|
||||
('', '', 0),
|
||||
# inspect two
|
||||
('{"start_order": 1, "image": "centos:6"}', '', 0),
|
||||
# rm two, changed config data
|
||||
('', '', 0),
|
||||
# inspect three
|
||||
('{"start_order": 2, "image": "centos:7"}', '', 0),
|
||||
# ps for after delete_missing_and_updated renames
|
||||
('', '', 0),
|
||||
# ps to only create containers which don't exist
|
||||
('three-12345678 three', '', 0),
|
||||
('Created one-12345678', '', 0),
|
||||
('Created two-12345678', '', 0),
|
||||
('Created four-12345678', '', 0),
|
||||
('a\nb\nc', '', 0)
|
||||
]
|
||||
r.discover_container_name = lambda n, c: '%s-12345678' % n
|
||||
r.unique_container_name = lambda n: '%s-12345678' % n
|
||||
r.execute = exe
|
||||
|
||||
builder = compose1.ComposeV1Builder('foo', config, r)
|
||||
stdout, stderr, deploy_status_code = builder.apply()
|
||||
self.assertEqual(0, deploy_status_code)
|
||||
self.assertEqual([
|
||||
'Created one-12345678',
|
||||
'Created two-12345678',
|
||||
'Created four-12345678',
|
||||
'a\nb\nc'
|
||||
], stdout)
|
||||
self.assertEqual([], stderr)
|
||||
|
||||
exe.assert_has_calls([
|
||||
# inspect image centos:7
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:7']
|
||||
),
|
||||
# ps for delete_missing_and_updated container_names
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# rm containers not in config
|
||||
mock.call(['docker', 'rm', '-f', 'five']),
|
||||
mock.call(['docker', 'rm', '-f', 'six']),
|
||||
# rm two, changed config
|
||||
mock.call(['docker', 'inspect', '--type', 'container',
|
||||
'--format', '{{index .Config.Labels "config_data"}}',
|
||||
'two-12345678']),
|
||||
mock.call(['docker', 'rm', '-f', 'two-12345678']),
|
||||
# check three, config hasn't changed
|
||||
mock.call(['docker', 'inspect', '--type', 'container',
|
||||
'--format', '{{index .Config.Labels "config_data"}}',
|
||||
'three-12345678']),
|
||||
# ps for after delete_missing_and_updated renames
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# ps to only create containers which don't exist
|
||||
mock.call(
|
||||
['docker', 'ps', '-a',
|
||||
'--filter', 'label=managed_by=tester',
|
||||
'--filter', 'label=config_id=foo',
|
||||
'--format', '{{.Names}} {{.Label "container_name"}}']
|
||||
),
|
||||
# run one
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'one-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['one']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# run two
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'two-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=two',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['two']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# don't run three, its already running
|
||||
# run four
|
||||
mock.call(
|
||||
['docker', 'run', '--name', 'four-12345678',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=four',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=%s' % json.dumps(config['four']),
|
||||
'--detach=true', 'centos:7']
|
||||
),
|
||||
# execute within four
|
||||
mock.call(
|
||||
['docker', 'exec', 'four-12345678', 'ls', '-l', '/']
|
||||
),
|
||||
])
|
||||
|
||||
def test_apply_failed_pull(self):
|
||||
orig_call = tenacity.wait.wait_random_exponential.__call__
|
||||
orig_argspec = inspect.getargspec(orig_call)
|
||||
config = {
|
||||
'one': {
|
||||
'start_order': 0,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'two': {
|
||||
'start_order': 1,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'three': {
|
||||
'start_order': 2,
|
||||
'image': 'centos:6',
|
||||
},
|
||||
'four': {
|
||||
'start_order': 10,
|
||||
'image': 'centos:7',
|
||||
},
|
||||
'four_ls': {
|
||||
'action': 'exec',
|
||||
'start_order': 20,
|
||||
'command': ['four', 'ls', '-l', '/']
|
||||
}
|
||||
}
|
||||
|
||||
r = runner.DockerRunner(managed_by='tester', docker_cmd='docker')
|
||||
exe = mock.Mock()
|
||||
exe.side_effect = [
|
||||
('exists', '', 0), # inspect for image centos:6
|
||||
('', '', 1), # inspect for missing image centos:7
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 failure
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 2
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 3
|
||||
('Pulling centos:7', 'ouch', 1), # pull centos:7 retry 4
|
||||
]
|
||||
r.execute = exe
|
||||
|
||||
with mock.patch('tenacity.wait.wait_random_exponential.__call__') as f:
|
||||
f.return_value = 0
|
||||
with mock.patch('inspect.getargspec') as mock_args:
|
||||
mock_args.return_value = orig_argspec
|
||||
builder = compose1.ComposeV1Builder('foo', config, r)
|
||||
|
||||
stdout, stderr, deploy_status_code = builder.apply()
|
||||
self.assertEqual(1, deploy_status_code)
|
||||
self.assertEqual(['Pulling centos:7'], stdout)
|
||||
self.assertEqual(['ouch'], stderr)
|
||||
|
||||
exe.assert_has_calls([
|
||||
# inspect existing image centos:6
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:6']
|
||||
),
|
||||
# inspect and pull missing image centos:7
|
||||
mock.call(
|
||||
['docker', 'inspect', '--type', 'image',
|
||||
'--format', 'exists', 'centos:7']
|
||||
),
|
||||
mock.call(
|
||||
['docker', 'pull', 'centos:7']
|
||||
),
|
||||
])
|
||||
|
||||
@mock.patch('paunch.runner.DockerRunner', autospec=True)
|
||||
def test_label_arguments(self, runner):
|
||||
r = runner.return_value
|
||||
r.managed_by = 'tester'
|
||||
builder = compose1.ComposeV1Builder('foo', {}, r)
|
||||
cmd = []
|
||||
builder.label_arguments(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=null'],
|
||||
cmd)
|
||||
|
||||
labels = collections.OrderedDict()
|
||||
labels['foo'] = 'bar'
|
||||
labels['bar'] = 'baz'
|
||||
|
||||
builder = compose1.ComposeV1Builder('foo', {}, r, labels=labels)
|
||||
cmd = []
|
||||
builder.label_arguments(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['--label', 'foo=bar',
|
||||
'--label', 'bar=baz',
|
||||
'--label', 'config_id=foo',
|
||||
'--label', 'container_name=one',
|
||||
'--label', 'managed_by=tester',
|
||||
'--label', 'config_data=null'],
|
||||
cmd)
|
||||
|
||||
def test_durations(self):
|
||||
config = {
|
||||
'a': {'stop_grace_period': 123},
|
||||
'b': {'stop_grace_period': 123.5},
|
||||
'c': {'stop_grace_period': '123.3'},
|
||||
'd': {'stop_grace_period': '2.5s'},
|
||||
'e': {'stop_grace_period': '10s'},
|
||||
'f': {'stop_grace_period': '1m30s'},
|
||||
'g': {'stop_grace_period': '2h32m'},
|
||||
'h': {'stop_grace_period': '5h34m56s'},
|
||||
'i': {'stop_grace_period': '1h1m1s1ms1us'},
|
||||
}
|
||||
builder = compose1.ComposeV1Builder('foo', config, None)
|
||||
|
||||
result = {
|
||||
'a': '--stop-timeout=123',
|
||||
'b': '--stop-timeout=123.5',
|
||||
'c': '--stop-timeout=123.3',
|
||||
'd': '--stop-timeout=2.5',
|
||||
'e': '--stop-timeout=10.0',
|
||||
'f': '--stop-timeout=90.0',
|
||||
'g': '--stop-timeout=9120.0',
|
||||
'h': '--stop-timeout=20096.0',
|
||||
'i': '--stop-timeout=3661.001001',
|
||||
}
|
||||
|
||||
for container, arg in result.items():
|
||||
cmd = []
|
||||
builder.container_run_args(cmd, container)
|
||||
self.assertIn(arg, cmd)
|
||||
|
||||
def test_container_run_args_lists(self):
|
||||
config = {
|
||||
'one': {
|
||||
'image': 'centos:7',
|
||||
'detach': False,
|
||||
'command': 'ls -l /foo',
|
||||
'remove': True,
|
||||
'tty': True,
|
||||
'interactive': True,
|
||||
'environment': ['FOO=BAR', 'BAR=BAZ'],
|
||||
'env_file': ['/tmp/foo.env', '/tmp/bar.env'],
|
||||
'ulimit': ['nofile=1024', 'nproc=1024'],
|
||||
'volumes': ['/foo:/foo:rw', '/bar:/bar:ro'],
|
||||
'volumes_from': ['two', 'three'],
|
||||
'group_add': ['docker', 'zuul']
|
||||
}
|
||||
}
|
||||
builder = compose1.ComposeV1Builder('foo', config, None)
|
||||
|
||||
cmd = ['docker', 'run', '--name', 'one']
|
||||
builder.container_run_args(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['docker', 'run', '--name', 'one',
|
||||
'--env-file=/tmp/foo.env', '--env-file=/tmp/bar.env',
|
||||
'--env=FOO=BAR', '--env=BAR=BAZ',
|
||||
'--rm', '--interactive', '--tty',
|
||||
'--ulimit=nofile=1024', '--ulimit=nproc=1024',
|
||||
'--group-add=docker', '--group-add=zuul',
|
||||
'--volume=/foo:/foo:rw', '--volume=/bar:/bar:ro',
|
||||
'--volumes-from=two', '--volumes-from=three',
|
||||
'centos:7', 'ls', '-l', '/foo'],
|
||||
cmd
|
||||
)
|
||||
|
||||
@mock.patch('paunch.runner.DockerRunner', autospec=True)
|
||||
def test_docker_exec_args(self, runner):
|
||||
r = runner.return_value
|
||||
r.discover_container_name.return_value = 'one-12345678'
|
||||
config = {
|
||||
'one': {
|
||||
'command': 'ls -l /foo',
|
||||
'privileged': True,
|
||||
'user': 'bar'
|
||||
}
|
||||
}
|
||||
self.builder = compose1.ComposeV1Builder(
|
||||
'foo', config, runner.return_value)
|
||||
|
||||
cmd = ['docker', 'exec']
|
||||
self.builder.docker_exec_args(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['docker', 'exec',
|
||||
'--privileged=true', '--user=bar',
|
||||
'one-12345678', '-l', '/foo'],
|
||||
cmd
|
||||
)
|
||||
|
||||
def test_command_argument(self):
|
||||
b = compose1.ComposeV1Builder
|
||||
self.assertEqual([], b.command_argument(None))
|
||||
self.assertEqual([], b.command_argument(''))
|
||||
self.assertEqual([], b.command_argument([]))
|
||||
self.assertEqual(
|
||||
['ls', '-l', '/foo-bar'],
|
||||
b.command_argument(['ls', '-l', '/foo-bar'])
|
||||
)
|
||||
self.assertEqual(
|
||||
['ls', '-l', '/foo-bar'],
|
||||
b.command_argument('ls -l /foo-bar')
|
||||
)
|
||||
self.assertEqual(
|
||||
['ls', '-l', '/foo bar'],
|
||||
b.command_argument(['ls', '-l', '/foo bar'])
|
||||
)
|
||||
# don't expect quoted spaces to do the right thing
|
||||
self.assertEqual(
|
||||
['ls', '-l', '"/foo', 'bar"'],
|
||||
b.command_argument('ls -l "/foo bar"')
|
||||
)
|
||||
|
||||
|
||||
class TestComposeV1Builder(TestBaseBuilder):
|
||||
class TestComposeV1Builder(tbb.TestBaseBuilder):
|
||||
def test_docker_run_args(self):
|
||||
config = {
|
||||
'one': {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
|
||||
from paunch.builder import podman
|
||||
from paunch.tests import test_builder_base as base
|
||||
|
||||
|
||||
class TestPodmanBuilder(base.TestBaseBuilder):
|
||||
def test_docker_run_args(self):
|
||||
config = {
|
||||
'one': {
|
||||
'image': 'centos:7',
|
||||
'privileged': True,
|
||||
'user': 'bar',
|
||||
'net': 'host',
|
||||
'ipc': 'host',
|
||||
'pid': 'container:bar',
|
||||
'uts': 'host',
|
||||
'restart': 'always',
|
||||
'env_file': '/tmp/foo.env',
|
||||
'log_tag': '{{.ImageName}}/{{.Name}}/{{.ID}}',
|
||||
'cpu_shares': 600,
|
||||
'security_opt': 'label:disable'
|
||||
}
|
||||
}
|
||||
builder = podman.PodmanBuilder('foo', config, None)
|
||||
|
||||
cmd = ['podman', 'run', '--name', 'one']
|
||||
builder.container_run_args(cmd, 'one')
|
||||
self.assertEqual(
|
||||
['podman', 'run', '--name', 'one',
|
||||
'--detach=true', '--env-file=/tmp/foo.env',
|
||||
'--net=host', '--ipc=host', '--pid=container:bar',
|
||||
'--uts=host', '--privileged=true', '--user=bar',
|
||||
'--log-opt=tag={{.ImageName}}/{{.Name}}/{{.ID}}',
|
||||
'--cpu-shares=600',
|
||||
'--security-opt=label:disable', 'centos:7'],
|
||||
cmd
|
||||
)
|
|
@ -187,9 +187,6 @@ class TestBaseRunner(base.TestCase):
|
|||
'three': [{'e': 'f'}, {'e': 'f'}, {'e': 'f'}]
|
||||
}, result)
|
||||
|
||||
|
||||
class TestDockerRunner(TestBaseRunner):
|
||||
|
||||
@mock.patch('subprocess.Popen')
|
||||
def test_remove_containers(self, popen):
|
||||
self.mock_execute(popen, 'one\ntwo\nthree', '', 0)
|
||||
|
@ -262,6 +259,9 @@ two-12345678 two
|
|||
['two-12345678', 'two']
|
||||
], names)
|
||||
|
||||
|
||||
class TestDockerRunner(TestBaseRunner):
|
||||
|
||||
@mock.patch('subprocess.Popen')
|
||||
def test_rename_containers(self, popen):
|
||||
ps_result = '''one one
|
||||
|
@ -286,3 +286,7 @@ four-12345678 four
|
|||
mock.call('three-12345678', 'three'),
|
||||
mock.call('four-12345678', 'four')
|
||||
], any_order=True)
|
||||
|
||||
|
||||
class PodmanRunner(TestBaseRunner):
|
||||
pass
|
||||
|
|
Loading…
Reference in New Issue