diff --git a/changelogs/fragments/49688-docker_secret-config-base64-data.yml b/changelogs/fragments/49688-docker_secret-config-base64-data.yml new file mode 100644 index 00000000000..4606c6ba2f4 --- /dev/null +++ b/changelogs/fragments/49688-docker_secret-config-base64-data.yml @@ -0,0 +1,3 @@ +minor_changes: +- "docker_secret - ``data`` can now accept Base64-encoded data via the new ``data_is_b64`` option. + This allows to pass binary data or JSON data in unmodified form. (https://github.com/ansible/ansible/issues/35119)" diff --git a/lib/ansible/modules/cloud/docker/docker_config.py b/lib/ansible/modules/cloud/docker/docker_config.py index ac0a9fe2c3a..68a73d461ba 100644 --- a/lib/ansible/modules/cloud/docker/docker_config.py +++ b/lib/ansible/modules/cloud/docker/docker_config.py @@ -32,6 +32,14 @@ options: - The value of the config. Required when state is C(present). required: false type: str + data_is_b64: + description: + - If set to C(true), the data is assumed to be Base64 encoded and will be + decoded before being used. + - To use binary C(data), it is better to keep it Base64 encoded and let it + be decoded by this option. + default: false + type: bool labels: description: - "A map of key:value meta data, where both the I(key) and I(value) are expected to be a string." @@ -80,7 +88,11 @@ EXAMPLES = ''' - name: Create config foo (from a file on the control machine) docker_config: name: foo - data: "{{ lookup('file', '/path/to/config/file') }}" + # If the file is JSON or binary, Ansible might modify it (because + # it is first decoded and later re-encoded). Base64-encoding the + # file directly after reading it prevents this to happen. + data: "{{ lookup('file', '/path/to/config/file') | base64 }}" + data_is_b64: true state: present - name: Change the config data @@ -144,12 +156,13 @@ config_id: sample: 'hzehrmyjigmcp2gb6nlhmjqcv' ''' +import base64 import hashlib try: from docker.errors import APIError except ImportError: - # missing docker-py handled in ansible.module_utils.docker + # missing docker-py handled in ansible.module_utils.docker_common pass from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass, compare_generic @@ -170,13 +183,18 @@ class ConfigManager(DockerBaseClass): self.name = parameters.get('name') self.state = parameters.get('state') self.data = parameters.get('data') + if self.data is not None: + if parameters.get('data_is_b64'): + self.data = base64.b64decode(self.data) + else: + self.data = to_bytes(self.data) self.labels = parameters.get('labels') self.force = parameters.get('force') self.data_key = None def __call__(self): if self.state == 'present': - self.data_key = hashlib.sha224(to_bytes(self.data)).hexdigest() + self.data_key = hashlib.sha224(self.data).hexdigest() self.present() elif self.state == 'absent': self.absent() @@ -252,6 +270,7 @@ def main(): name=dict(type='str', required=True), state=dict(type='str', choices=['absent', 'present'], default='present'), data=dict(type='str'), + data_is_b64=dict(type='bool', default=False), labels=dict(type='dict'), force=dict(type='bool', default=False) ) diff --git a/lib/ansible/modules/cloud/docker/docker_secret.py b/lib/ansible/modules/cloud/docker/docker_secret.py index a5aa00e5779..dcd924b049f 100644 --- a/lib/ansible/modules/cloud/docker/docker_secret.py +++ b/lib/ansible/modules/cloud/docker/docker_secret.py @@ -23,19 +23,30 @@ version_added: "2.4" description: - Create and remove Docker secrets in a Swarm environment. Similar to `docker secret create` and `docker secret rm`. - Adds to the metadata of new secrets 'ansible_key', an encrypted hash representation of the data, which is then used - - in future runs to test if a secret has changed. - - If 'ansible_key is not present, then a secret will not be updated unless the C(force) option is set. + in future runs to test if a secret has changed. If 'ansible_key is not present, then a secret will not be updated + unless the C(force) option is set. - Updates to secrets are performed by removing the secret and creating it again. options: data: description: - - String. The value of the secret. Required when state is C(present). + - The value of the secret. Required when state is C(present). required: false + type: str + data_is_b64: + description: + - If set to C(true), the data is assumed to be Base64 encoded and will be + decoded before being used. + - To use binary C(data), it is better to keep it Base64 encoded and let it + be decoded by this option. + default: false + type: bool + version_added: "2.8" labels: description: - "A map of key:value meta data, where both the I(key) and I(value) are expected to be a string." - If new meta data is provided, or existing meta data is modified, the secret will be updated by removing it and creating it again. required: false + type: dict force: description: - Use with state C(present) to always remove and recreate an existing secret. @@ -46,6 +57,7 @@ options: description: - The name of the secret. required: true + type: str state: description: - Set to C(present), if the secret should exist, and C(absent), if it should not. @@ -72,10 +84,14 @@ author: EXAMPLES = ''' -- name: Create secret foo +- name: Create secret foo (from a file on the control machine) docker_secret: name: foo - data: Hello World! + # If the file is JSON or binary, Ansible might modify it (because + # it is first decoded and later re-encoded). Base64-encoding the + # file directly after reading it prevents this to happen. + data: "{{ lookup('file', '/path/to/secret/file') | base64 }}" + data_is_b64: true state: present - name: Change the secret data @@ -134,11 +150,12 @@ RETURN = ''' secret_id: description: - The ID assigned by Docker to the secret object. - returned: success + returned: success and C(state == "present") type: str sample: 'hzehrmyjigmcp2gb6nlhmjqcv' ''' +import base64 import hashlib try: @@ -165,13 +182,18 @@ class SecretManager(DockerBaseClass): self.name = parameters.get('name') self.state = parameters.get('state') self.data = parameters.get('data') + if self.data is not None: + if parameters.get('data_is_b64'): + self.data = base64.b64decode(self.data) + else: + self.data = to_bytes(self.data) self.labels = parameters.get('labels') self.force = parameters.get('force') self.data_key = None def __call__(self): if self.state == 'present': - self.data_key = hashlib.sha224(to_bytes(self.data)).hexdigest() + self.data_key = hashlib.sha224(self.data).hexdigest() self.present() elif self.state == 'absent': self.absent() @@ -247,6 +269,7 @@ def main(): name=dict(type='str', required=True), state=dict(type='str', choices=['absent', 'present'], default='present'), data=dict(type='str', no_log=True), + data_is_b64=dict(type='bool', default=False), labels=dict(type='dict'), force=dict(type='bool', default=False) ) diff --git a/test/integration/targets/docker_config/tasks/test_docker_config.yml b/test/integration/targets/docker_config/tasks/test_docker_config.yml index 9956e5447be..d4482004930 100644 --- a/test/integration/targets/docker_config/tasks/test_docker_config.yml +++ b/test/integration/targets/docker_config/tasks/test_docker_config.yml @@ -70,6 +70,19 @@ that: - not output.changed + - name: Create config again (base64) + docker_config: + name: db_password + data: b3BlbnNlc2FtZSE= + data_is_b64: true + state: present + register: output + + - name: assert create config (base64) is idempotent + assert: + that: + - not output.changed + - name: Update config docker_config: name: db_password diff --git a/test/integration/targets/docker_secret/tasks/test_secrets.yml b/test/integration/targets/docker_secret/tasks/test_secrets.yml index 03321ee1ddc..b99f059e052 100644 --- a/test/integration/targets/docker_secret/tasks/test_secrets.yml +++ b/test/integration/targets/docker_secret/tasks/test_secrets.yml @@ -70,6 +70,19 @@ that: - not output.changed + - name: Create secret again (base64) + docker_secret: + name: db_password + data: b3BlbnNlc2FtZSE= + data_is_b64: true + state: present + register: output + + - name: assert create secret (base64) is idempotent + assert: + that: + - not output.changed + - name: Update secret docker_secret: name: db_password