diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/sops/plugins/modules | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/sops/plugins/modules')
-rw-r--r-- | ansible_collections/community/sops/plugins/modules/load_vars.py | 117 | ||||
-rw-r--r-- | ansible_collections/community/sops/plugins/modules/sops_encrypt.py | 237 |
2 files changed, 354 insertions, 0 deletions
diff --git a/ansible_collections/community/sops/plugins/modules/load_vars.py b/ansible_collections/community/sops/plugins/modules/load_vars.py new file mode 100644 index 000000000..27e9ae8c2 --- /dev/null +++ b/ansible_collections/community/sops/plugins/modules/load_vars.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +author: Felix Fontein (@felixfontein) +module: load_vars +short_description: Load sops-encrypted variables from files, dynamically within a task +version_added: '0.1.0' +description: + - Loads sops-encrypted YAML/JSON variables dynamically from a file during task runtime. + - To assign included variables to a different host than C(inventory_hostname), + use C(delegate_to) and set C(delegate_facts=true). +options: + file: + description: + - The file name from which variables should be loaded. + - If the path is relative, it will look for the file in C(vars/) subdirectory of a role or relative to playbook. + type: path + name: + description: + - The name of a variable into which assign the included vars. + - If omitted (C(null)) they will be made top level vars. + type: str + expressions: + description: + - This option controls how Jinja2 expressions in values in the loaded file are handled. + - If set to C(ignore), expressions will not be evaluated, but treated as regular strings. + - If set to C(evaluate-on-load), expressions will be evaluated on execution of this module, + in other words, when the file is loaded. + - Unfortunately, there is no way for non-core modules to handle expressions "unsafe", + in other words, evaluate them only on use. This can only achieved by M(ansible.builtin.include_vars), + which unfortunately cannot handle sops-encrypted files. + type: str + default: ignore + choices: + - ignore + - evaluate-on-load +extends_documentation_fragment: + - community.sops.sops + - community.sops.attributes + - community.sops.attributes.facts + - community.sops.attributes.flow +attributes: + action: + support: full + async: + support: none + details: + - This action runs completely on the controller. + check_mode: + support: full + diff_mode: + support: N/A + details: + - This action does not modify state. + facts: + support: full +seealso: + - module: ansible.builtin.set_fact + - module: ansible.builtin.include_vars + - ref: playbooks_delegation + description: More information related to task delegation. + - ref: community.sops.sops lookup <ansible_collections.community.sops.sops_lookup> + description: The sops lookup can be used decrypt sops-encrypted files. + # - plugin: community.sops.sops + # plugin_type: lookup + - ref: community.sops.decrypt filter <ansible_collections.community.sops.decrypt_filter> + description: The decrypt filter can be used to descrypt sops-encrypted in-memory data. + # - plugin: community.sops.decrypt + # plugin_type: filter + - ref: community.sops.sops vars plugin <ansible_collections.community.sops.sops_vars> + description: The sops vars plugin can be used to load sops-encrypted host or group variables. + # - plugin: community.sops.sops + # plugin_type: vars +''' + +EXAMPLES = r''' +- name: Include variables of stuff.sops.yaml into the 'stuff' variable + community.sops.load_vars: + file: stuff.sops.yaml + name: stuff + expressions: evaluate-on-load # interpret Jinja2 expressions in stuf.sops.yaml on load-time! + +- name: Conditionally decide to load in variables into 'plans' when x is 0, otherwise do not + community.sops.load_vars: + file: contingency_plan.sops.yaml + name: plans + expressions: ignore # do not interpret possible Jinja2 expressions + when: x == 0 + +- name: Load variables into the global namespace + community.sops.load_vars: + file: contingency_plan.sops.yaml +''' + +RETURN = r''' +ansible_facts: + description: Variables that were included and their values. + returned: success + type: dict + sample: {'variable': 'value'} +ansible_included_var_files: + description: A list of files that were successfully included + returned: success + type: list + elements: str + sample: [ /path/to/file.sops.yaml ] +''' diff --git a/ansible_collections/community/sops/plugins/modules/sops_encrypt.py b/ansible_collections/community/sops/plugins/modules/sops_encrypt.py new file mode 100644 index 000000000..d4ba34353 --- /dev/null +++ b/ansible_collections/community/sops/plugins/modules/sops_encrypt.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +author: Felix Fontein (@felixfontein) +module: sops_encrypt +short_description: Encrypt data with sops +version_added: '0.1.0' +description: + - Allows to encrypt binary data (Base64 encoded), text data, JSON or YAML data with sops. +options: + path: + description: + - The sops encrypt file. + type: path + required: true + force: + description: + - Force rewriting the encrypted file. + type: bool + default: false + content_text: + description: + - The data to encrypt. Must be a Unicode text. + - Please note that the module might not be idempotent if the text can be parsed as JSON or YAML. + - Exactly one of I(content_text), I(content_binary), I(content_json) and I(content_yaml) must be specified. + type: str + content_binary: + description: + - The data to encrypt. Must be L(Base64 encoded,https://en.wikipedia.org/wiki/Base64) binary data. + - Please note that the module might not be idempotent if the data can be parsed as JSON or YAML. + - Exactly one of I(content_text), I(content_binary), I(content_json) and I(content_yaml) must be specified. + type: str + content_json: + description: + - The data to encrypt. Must be a JSON dictionary. + - Exactly one of I(content_text), I(content_binary), I(content_json) and I(content_yaml) must be specified. + type: dict + content_yaml: + description: + - The data to encrypt. Must be a YAML dictionary. + - Please note that Ansible only allows to pass data that can be represented as a JSON dictionary. + - Exactly one of I(content_text), I(content_binary), I(content_json) and I(content_yaml) must be specified. + type: dict +extends_documentation_fragment: + - ansible.builtin.files + - community.sops.sops + - community.sops.sops.encrypt_specific + - community.sops.attributes + - community.sops.attributes.files +attributes: + check_mode: + support: full + diff_mode: + support: none + safe_file_operations: + support: full +seealso: + - ref: community.sops.sops lookup <ansible_collections.community.sops.sops_lookup> + description: The sops lookup can be used decrypt sops-encrypted files. + # - plugin: community.sops.sops + # plugin_type: lookup +''' + +EXAMPLES = r''' +- name: Encrypt a secret text + community.sops.sops_encrypt: + path: text-data.sops + content_text: This is a secret text. + +- name: Encrypt the contents of a file + community.sops.sops_encrypt: + path: binary-data.sops + content_binary: "{{ lookup('ansible.builtin.file', '/path/to/file', rstrip=false) | b64encode }}" + +- name: Encrypt some datastructure as YAML + community.sops.sops_encrypt: + path: stuff.sops.yaml + content_yaml: "{{ result }}" +''' + +RETURN = r''' # ''' + + +import base64 +import json +import os +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_text + +from ansible_collections.community.sops.plugins.module_utils.io import write_file +from ansible_collections.community.sops.plugins.module_utils.sops import Sops, SopsError, get_sops_argument_spec + +try: + import yaml + YAML_IMP_ERR = None + HAS_YAML = True +except ImportError: + YAML_IMP_ERR = traceback.format_exc() + HAS_YAML = False + yaml = None + + +def get_data_type(module): + if module.params['content_text'] is not None: + return 'binary' + if module.params['content_binary'] is not None: + return 'binary' + if module.params['content_json'] is not None: + return 'json' + if module.params['content_yaml'] is not None: + return 'yaml' + module.fail_json(msg='Internal error: unknown content type') + + +def compare_encoded_content(module, binary_data, content): + if module.params['content_text'] is not None: + return content == module.params['content_text'].encode('utf-8') + if module.params['content_binary'] is not None: + return content == binary_data + if module.params['content_json'] is not None: + # Compare JSON + try: + return json.loads(content) == module.params['content_json'] + except Exception: + # Treat parsing errors as content not equal + return False + if module.params['content_yaml'] is not None: + # Compare YAML + try: + return yaml.safe_load(content) == module.params['content_yaml'] + except Exception: + # Treat parsing errors as content not equal + return False + module.fail_json(msg='Internal error: unknown content type') + + +def get_encoded_type_content(module, binary_data): + if module.params['content_text'] is not None: + return 'binary', module.params['content_text'].encode('utf-8') + if module.params['content_binary'] is not None: + return 'binary', binary_data + if module.params['content_json'] is not None: + return 'json', json.dumps(module.params['content_json']).encode('utf-8') + if module.params['content_yaml'] is not None: + return 'yaml', yaml.safe_dump(module.params['content_yaml']).encode('utf-8') + module.fail_json(msg='Internal error: unknown content type') + + +def main(): + argument_spec = dict( + path=dict(type='path', required=True), + force=dict(type='bool', default=False), + content_text=dict(type='str', no_log=True), + content_binary=dict(type='str', no_log=True), + content_json=dict(type='dict', no_log=True), + content_yaml=dict(type='dict', no_log=True), + ) + argument_spec.update(get_sops_argument_spec(add_encrypt_specific=True)) + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ('content_text', 'content_binary', 'content_json', 'content_yaml'), + ], + required_one_of=[ + ('content_text', 'content_binary', 'content_json', 'content_yaml'), + ], + supports_check_mode=True, + add_file_common_args=True, + ) + + # Check YAML + if module.params['content_yaml'] is not None and not HAS_YAML: + module.fail_json(msg=missing_required_lib('pyyaml'), exception=YAML_IMP_ERR) + + # Decode binary data + binary_data = None + if module.params['content_binary'] is not None: + try: + binary_data = base64.b64decode(module.params['content_binary']) + except Exception as e: + module.fail_json(msg='Cannot decode Base64 encoded data: {0}'.format(e)) + + path = module.params['path'] + directory = os.path.dirname(path) or None + changed = False + + def get_option_value(argument_name): + return module.params.get(argument_name) + + try: + if module.params['force'] or not os.path.exists(path): + # Simply encrypt + changed = True + else: + # Change detection: check if encrypted data equals new data + decrypted_content = Sops.decrypt( + path, decode_output=False, output_type=get_data_type(module), rstrip=False, + get_option_value=get_option_value, module=module, + ) + if not compare_encoded_content(module, binary_data, decrypted_content): + changed = True + + if changed and not module.check_mode: + input_type, input_data = get_encoded_type_content(module, binary_data) + output_type = None + if path.endswith('.json'): + output_type = 'json' + elif path.endswith('.yaml'): + output_type = 'yaml' + data = Sops.encrypt( + data=input_data, cwd=directory, input_type=input_type, output_type=output_type, + get_option_value=get_option_value, module=module, + ) + write_file(module, data) + except SopsError as e: + module.fail_json(msg=to_text(e)) + + file_args = module.load_file_common_arguments(module.params) + changed = module.set_fs_attributes_if_different(file_args, changed) + + module.exit_json(changed=changed) + + +if __name__ == '__main__': + main() |