diff --git a/changelogs/fragments/370-add-image-label-mismatch.yml b/changelogs/fragments/370-add-image-label-mismatch.yml new file mode 100644 index 000000000..da13fbe99 --- /dev/null +++ b/changelogs/fragments/370-add-image-label-mismatch.yml @@ -0,0 +1,2 @@ +minor_changes: +- "docker_container - added ``image_label_mismatch`` parameter (https://github.com/ansible-collections/community.docker/issues/314, https://github.com/ansible-collections/community.docker/pull/370)." diff --git a/plugins/modules/docker_container.py b/plugins/modules/docker_container.py index f339558ad..98a56c9f1 100644 --- a/plugins/modules/docker_container.py +++ b/plugins/modules/docker_container.py @@ -394,6 +394,22 @@ - Can also be an image ID. If this is the case, the image is assumed to be available locally. The I(pull) option is ignored for this case. type: str + image_label_mismatch: + description: + - How to handle labels inherited from the image that are not set explicitly. + - When C(ignore), labels that are present in the image but not specified in I(labels) will be + ignored. This is useful to avoid having to specify the image labels in I(labels) while keeping + labels I(comparisons) C(strict). + - When C(fail), if there are labels present in the image which are not set from I(labels), the + module will fail. This prevents introducing unexpected labels from the base image. + - "B(Warning:) This option is ignored unless C(labels: strict) or C(*: strict) is specified in + the I(comparisons) option." + type: str + choices: + - 'ignore' + - 'fail' + default: ignore + version_added: 2.6.0 init: description: - Run an init inside the container that forwards signals and reaps processes. @@ -2167,6 +2183,7 @@ def __init__(self, container, parameters): self.parameters.expected_env = None self.parameters.expected_device_requests = None self.parameters_map = dict() + self.parameters_map['expected_labels'] = 'labels' self.parameters_map['expected_links'] = 'links' self.parameters_map['expected_ports'] = 'expected_ports' self.parameters_map['expected_exposed'] = 'exposed_ports' @@ -2177,6 +2194,7 @@ def __init__(self, container, parameters): self.parameters_map['expected_env'] = 'env' self.parameters_map['expected_entrypoint'] = 'entrypoint' self.parameters_map['expected_binds'] = 'volumes' + self.parameters_map['expected_labels'] = 'labels' self.parameters_map['expected_cmd'] = 'command' self.parameters_map['expected_devices'] = 'devices' self.parameters_map['expected_healthcheck'] = 'healthcheck' @@ -2248,6 +2266,7 @@ def has_different_configuration(self, image): self.parameters.expected_exposed = self._get_expected_exposed(image) self.parameters.expected_volumes = self._get_expected_volumes(image) self.parameters.expected_binds = self._get_expected_binds(image) + self.parameters.expected_labels = self._get_expected_labels(image) self.parameters.expected_ulimits = self._get_expected_ulimits(self.parameters.ulimits) self.parameters.expected_sysctls = self._get_expected_sysctls(self.parameters.sysctls) self.parameters.expected_etc_hosts = self._convert_simple_dict_to_list('etc_hosts') @@ -2301,7 +2320,7 @@ def has_different_configuration(self, image): expected_exposed=expected_exposed, groups=host_config.get('GroupAdd'), ipc_mode=host_config.get("IpcMode"), - labels=config.get('Labels'), + expected_labels=config.get('Labels'), expected_links=host_config.get('Links'), mac_address=config.get('MacAddress', network.get('MacAddress')), memory_swappiness=host_config.get('MemorySwappiness'), @@ -2403,6 +2422,20 @@ def has_different_configuration(self, image): # expected_healthcheck comparison in this case. continue + if key == 'expected_labels' and compare['comparison'] == 'strict' and self.parameters.image_label_mismatch == 'fail': + # If there are labels from the base image that should be removed and + # base_image_mismatch is fail we want raise an error. + image_labels = self._get_image_labels(image) + would_remove_labels = [] + for label in image_labels: + if label not in self.parameters.labels: + # Format label for error message + would_remove_labels.append(label) + if would_remove_labels: + msg = ("Some labels should be removed but are present in the base image. You can set image_label_mismatch to 'ignore' to ignore" + " this error. Labels: {0}") + self.fail(msg.format(', '.join(['"%s"' % label for label in would_remove_labels]))) + # no match. record the differences p = getattr(self.parameters, key) c = value @@ -2641,6 +2674,23 @@ def _get_expected_binds(self, image): self.log(result, pretty_print=True) return result + def _get_expected_labels(self, image): + if self.parameters.labels is None: + return None + if self.parameters.image_label_mismatch == 'ignore': + expected_labels = dict(self._get_image_labels(image)) + else: + expected_labels = {} + expected_labels.update(self.parameters.labels) + return expected_labels + + def _get_image_labels(self, image): + if not image: + return {} + + # Can't use get('Labels', {}) because 'Labels' may be present and be None + return image[self.parameters.client.image_inspect_source].get('Labels') or {} + def _get_expected_device_requests(self): if self.parameters.device_requests is None: return None @@ -3552,6 +3602,7 @@ def main(): hostname=dict(type='str'), ignore_image=dict(type='bool', default=False), image=dict(type='str'), + image_label_mismatch=dict(type='str', choices=['ignore', 'fail'], default='ignore'), init=dict(type='bool'), interactive=dict(type='bool'), ipc_mode=dict(type='str'), diff --git a/tests/integration/targets/docker_container/tasks/main.yml b/tests/integration/targets/docker_container/tasks/main.yml index 66e3d7bc7..17ab5f5b6 100644 --- a/tests/integration/targets/docker_container/tasks/main.yml +++ b/tests/integration/targets/docker_container/tasks/main.yml @@ -22,6 +22,7 @@ set_fact: cname_prefix: "{{ 'ansible-docker-test-%0x' % ((2**32) | random) }}" cnames: [] + inames: [] dnetworks: [] - debug: @@ -41,6 +42,11 @@ force_kill: yes with_items: "{{ cnames }}" diff: no + - name: "Make sure all images are removed" + docker_image: + name: "{{ item }}" + state: absent + with_items: "{{ inames }}" - name: "Make sure all networks are removed" docker_network: name: "{{ item }}" diff --git a/tests/integration/targets/docker_container/tasks/tests/options.yml b/tests/integration/targets/docker_container/tasks/tests/options.yml index c2828b0fc..4b898b7e8 100644 --- a/tests/integration/targets/docker_container/tasks/tests/options.yml +++ b/tests/integration/targets/docker_container/tasks/tests/options.yml @@ -2194,6 +2194,169 @@ - ignore_image is not changed - image_change is changed +#################################################################### +## image_label_mismatch ############################################ +#################################################################### + +- name: Registering image name + set_fact: + iname_labels: "{{ cname_prefix ~ '-labels' }}" +- name: Registering image name + set_fact: + inames: "{{ inames + [iname_labels] }}" +- name: build image with labels + command: + cmd: "docker build --label img_label=base --tag {{ iname_labels }} -" + stdin: "FROM {{ docker_test_image_alpine }}" + +- name: image_label_mismatch + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + register: image_label_mismatch_1 + +- name: image_label_mismatch (ignore,unmanaged labels) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: ignore + state: started + register: image_label_mismatch_2 + +- name: image_label_mismatch (ignore,missing img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: ignore + labels: {} + state: started + register: image_label_mismatch_3 + +- name: image_label_mismatch (ignore,match img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: ignore + labels: + img_label: base + state: started + register: image_label_mismatch_4 + +- name: image_label_mismatch (ignore,mismatched img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: ignore + labels: + img_label: override + state: started + force_kill: yes + register: image_label_mismatch_5 + +- name: image_label_mismatch (ignore,remove img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: ignore + labels: {} + state: started + force_kill: yes + register: image_label_mismatch_6 + +- name: image_label_mismatch (fail,unmanaged labels) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: fail + state: started + register: image_label_mismatch_7 + +- name: image_label_mismatch (fail,non-strict,missing img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: fail + labels: {} + state: started + register: image_label_mismatch_8 + +- name: image_label_mismatch (fail,strict,missing img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: fail + comparisons: + labels: strict + labels: {} + state: started + ignore_errors: yes + register: image_label_mismatch_9 + +- name: image_label_mismatch (fail,match img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: fail + labels: + img_label: base + state: started + register: image_label_mismatch_10 + +- name: image_label_mismatch (fail,mismatched img label) + docker_container: + image: "{{ iname_labels }}" + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + image_label_mismatch: fail + labels: + img_label: override + state: started + force_kill: yes + register: image_label_mismatch_11 + +- name: cleanup container + docker_container: + name: "{{ cname }}" + state: absent + force_kill: yes + diff: no + +- name: cleanup image + docker_image: + name: "{{ iname_labels }}" + state: absent + diff: no + +- assert: + that: + - image_label_mismatch_1 is changed + - image_label_mismatch_1.container.Config.Labels.img_label == "base" + - image_label_mismatch_2 is not changed + - image_label_mismatch_3 is not changed + - image_label_mismatch_4 is not changed + - image_label_mismatch_5 is changed + - image_label_mismatch_5.container.Config.Labels.img_label == "override" + - image_label_mismatch_6 is changed + - image_label_mismatch_6.container.Config.Labels.img_label == "base" + - image_label_mismatch_7 is not changed + - image_label_mismatch_8 is not changed + - image_label_mismatch_9 is failed + - >- + image_label_mismatch_9.msg == ("Some labels should be removed but are present in the base image. You can set image_label_mismatch to 'ignore' to ignore this error. " ~ 'Labels: "img_label"') + - image_label_mismatch_10 is not changed + - image_label_mismatch_11 is changed + #################################################################### ## ipc_mode ######################################################## ####################################################################