docker_container: fix handling of command and entrypoint in a backwards-compatible way (#186)

* Fix handling of command and entrypoint in a backwards-compatible way.

* Fix copy'n'paste error.

* Fix some more.

* Improve documentation.

* Keep command and entrypoint as lists and not as strings.

* Simplify code, since we're already emitting the deprecation warning in this case during parameter processing.

* Change default only in community.docker 3.0.0.

* Update tests/integration/targets/docker_container/tasks/tests/options.yml

Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com>

* Apply suggestion to more places.

Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com>
This commit is contained in:
Felix Fontein
2021-08-02 19:31:46 +02:00
committed by GitHub
parent 2a9bc7f74e
commit 6f52747693
3 changed files with 299 additions and 22 deletions
+75 -12
View File
@@ -66,6 +66,7 @@ options:
description:
- Command to execute when the container starts. A command may be either a string or a list.
- Prior to version 2.4, strings were split on commas.
- See I(command_handling) for differences in how strings and lists are handled.
type: raw
comparisons:
description:
@@ -103,6 +104,24 @@ options:
choices:
- compatibility
- no_defaults
command_handling:
description:
- The default behavior for I(command) (when provided as a list) and I(entrypoint) is to
convert them to strings without considering shell quoting rules. (For comparing idempotency,
the resulting string is split considering shell quoting rules.)
- Also, setting I(command) to an empty list of string, and setting I(entrypoint) to an empty
list will be handled as if these options are not specified. This is different from idempotency
handling for other container-config related options.
- When this is set to C(compatiblity), which is the default until community.docker 3.0.0, the
current behavior will be kept.
- When this is set to C(correct), these options are kept as lists, and an empty value or empty
list will be handled correctly for idempotency checks.
- In community.docker 3.0.0, the default will change to C(correct).
type: str
choices:
- compatibility
- correct
version_added: 1.9.0
cpu_period:
description:
- Limit CPU CFS (Completely Fair Scheduler) period.
@@ -296,6 +315,7 @@ options:
entrypoint:
description:
- Command that overwrites the default C(ENTRYPOINT) of the image.
- See I(command_handling) for differences in how strings and lists are handled.
type: list
elements: str
etc_hosts:
@@ -1186,6 +1206,7 @@ status:
'''
import os
import pipes
import re
import shlex
import traceback
@@ -1224,6 +1245,14 @@ except Exception:
pass
def shell_join(parts):
if getattr(shlex, 'quote', None):
quote = shlex.quote
else:
quote = pipes.quote
return ' '.join([quote(part) for part in parts])
REQUIRES_CONVERSION_TO_BYTES = [
'kernel_memory',
'memory',
@@ -1480,14 +1509,47 @@ class TaskParameters(DockerBaseClass):
# Ensure the MAC address uses colons instead of hyphens for later comparison
self.mac_address = self.mac_address.replace('-', ':')
if self.entrypoint:
# convert from list to str.
self.entrypoint = ' '.join([to_text(x, errors='surrogate_or_strict') for x in self.entrypoint])
if client.module.params['command_handling'] == 'correct':
if self.entrypoint is not None:
self.entrypoint = [to_text(x, errors='surrogate_or_strict') for x in self.entrypoint]
if self.command:
# convert from list to str
if isinstance(self.command, list):
self.command = ' '.join([to_text(x, errors='surrogate_or_strict') for x in self.command])
if self.command is not None:
if not isinstance(self.command, list):
# convert from str to list
self.command = shlex.split(to_text(self.command, errors='surrogate_or_strict'))
self.command = [to_text(x, errors='surrogate_or_strict') for x in self.command]
else:
will_change = False
if self.entrypoint:
# convert from list to str.
correct_entrypoint = [to_text(x, errors='surrogate_or_strict') for x in self.entrypoint]
self.entrypoint = shlex.split(' '.join([to_text(x, errors='surrogate_or_strict') for x in self.entrypoint]))
self.entrypoint = [to_text(x, errors='surrogate_or_strict') for x in self.entrypoint]
if correct_entrypoint != self.entrypoint:
will_change = True
elif self.entrypoint is not None:
will_change = True
if self.command:
# convert from list to str
if isinstance(self.command, list):
correct_command = [to_text(x, errors='surrogate_or_strict') for x in self.command]
self.command = shlex.split(' '.join([to_text(x, errors='surrogate_or_strict') for x in self.command]))
self.command = [to_text(x, errors='surrogate_or_strict') for x in self.command]
if correct_command != self.command:
will_change = True
else:
self.command = shlex.split(to_text(self.command, errors='surrogate_or_strict'))
self.command = [to_text(x, errors='surrogate_or_strict') for x in self.command]
elif self.command is not None:
will_change = True
if will_change and client.module.params['command_handling'] is None:
client.module.deprecate(
'The command_handling option will change its default value from "compatibility" to '
'"correct" in community.docker 3.0.0. To remove this warning, please specify an explicit value for it now',
version='3.0.0', collection_name='community.docker'
)
self.mounts_opt, self.expected_mounts = self._process_mounts()
@@ -2545,9 +2607,9 @@ class Container(DockerBaseClass):
return expected_devices
def _get_expected_entrypoint(self):
if not self.parameters.entrypoint:
if self.parameters.client.module.params['command_handling'] != 'correct' and not self.parameters.entrypoint:
return None
return shlex.split(self.parameters.entrypoint)
return self.parameters.entrypoint
def _get_expected_ports(self):
if self.parameters.published_ports is None:
@@ -2719,9 +2781,9 @@ class Container(DockerBaseClass):
def _get_expected_cmd(self):
self.log('_get_expected_cmd')
if not self.parameters.command:
if self.parameters.client.module.params['command_handling'] != 'correct' and not self.parameters.command:
return None
return shlex.split(self.parameters.command)
return self.parameters.command
def _convert_simple_dict_to_list(self, param_name, join_with=':'):
if getattr(self.parameters, param_name, None) is None:
@@ -3247,7 +3309,7 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
__NON_CONTAINER_PROPERTY_OPTIONS = tuple([
'env_file', 'force_kill', 'keep_volumes', 'ignore_image', 'name', 'pull', 'purge_networks',
'recreate', 'restart', 'state', 'networks', 'cleanup', 'kill_signal',
'output_logs', 'paused', 'removal_wait_timeout', 'default_host_ip',
'output_logs', 'paused', 'removal_wait_timeout', 'default_host_ip', 'command_handling',
] + list(DOCKER_COMMON_ARGS.keys()))
def _parse_comparisons(self):
@@ -3468,6 +3530,7 @@ def main():
command=dict(type='raw'),
comparisons=dict(type='dict'),
container_default_behavior=dict(type='str', choices=['compatibility', 'no_defaults']),
command_handling=dict(type='str', choices=['compatibility', 'correct']),
cpu_period=dict(type='int'),
cpu_quota=dict(type='int'),
cpus=dict(type='float'),