ansible-infra/ansible_collections/community/sops/plugins/vars/sops.py
Stefan Bethke 2aed20393f
Some checks failed
/ Ansible Lint (push) Failing after 5m45s
/ Ansible Lint (pull_request) Failing after 4m59s
Vendor Galaxy Roles and Collections
2026-02-06 22:07:16 +01:00

246 lines
11 KiB
Python

# Copyright (c) 2018 Edoardo Tenani <e.tenani@arduino.cc> (@endorama)
# 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 annotations
DOCUMENTATION = r"""
name: sops
author: Edoardo Tenani (@endorama) <e.tenani@arduino.cc>
short_description: Loading SOPS-encrypted vars files
version_added: '0.1.0'
description:
- Load encrypted YAML files into corresponding groups/hosts in C(group_vars/) and C(host_vars/) directories.
- Files are encrypted prior to reading, making this plugin an effective companion to P(ansible.builtin.host_group_vars#vars)
plugin.
- Files are restricted to V(.sops.yaml), V(.sops.yml), V(.sops.json) extensions, unless configured otherwise with O(valid_extensions).
- Hidden files are ignored.
options:
valid_extensions:
default: [".sops.yml", ".sops.yaml", ".sops.json"]
description:
- Check all of these extensions when looking for 'variable' files.
- These files must be SOPS encrypted YAML or JSON files.
- By default the plugin will produce errors when encountering files matching these extensions that are not SOPS encrypted.
This behavior can be controlled with the O(handle_unencrypted_files) option.
type: list
elements: string
ini:
- key: valid_extensions
section: community.sops
version_added: 1.7.0
env:
- name: ANSIBLE_VARS_SOPS_PLUGIN_VALID_EXTENSIONS
version_added: 1.7.0
stage:
version_added: 0.2.0
ini:
- key: vars_stage
section: community.sops
env:
- name: ANSIBLE_VARS_SOPS_PLUGIN_STAGE
cache:
description:
- Whether to cache decrypted files or not.
- If the cache is disabled, the files will be decrypted for almost every task. This is very slow!
- Only disable caching if you modify the variable files during a playbook run and want the updated result to be available
from the next task on.
- 'Note that setting O(stage=inventory) has the same effect as setting O(cache=true): the variables will be loaded only
once (during inventory loading) and the vars plugin will not be called for every task.'
type: bool
default: true
version_added: 0.2.0
ini:
- key: vars_cache
section: community.sops
env:
- name: ANSIBLE_VARS_SOPS_PLUGIN_CACHE
disable_vars_plugin_temporarily:
description:
- Temporarily disable this plugin.
- Useful if ansible-inventory is supposed to be run without decrypting secrets (in AWX for instance).
type: bool
default: false
version_added: 1.3.0
env:
- name: SOPS_ANSIBLE_AWX_DISABLE_VARS_PLUGIN_TEMPORARILY
handle_unencrypted_files:
description:
- How to handle files that match the extensions in O(valid_extensions) that are not SOPS encrypted.
- The default value V(error) will produce an error.
- The value V(skip) will simply skip these files. This requires SOPS 3.9.0 or later.
- The value V(warn) will skip these files and emit a warning. This requires SOPS 3.9.0 or later.
- B(Note) that this will not help if the store SOPS uses cannot parse the file, for example because it is no valid JSON/YAML/...
file despite its file extension. For extensions other than the default ones SOPS uses the binary store, which tries
to parse the file as JSON.
type: string
choices:
- skip
- warn
- error
default: error
version_added: 1.8.0
ini:
- key: handle_unencrypted_files
section: community.sops
env:
- name: ANSIBLE_VARS_SOPS_PLUGIN_HANDLE_UNENCRYPTED_FILES
extends_documentation_fragment:
- ansible.builtin.vars_plugin_staging
- community.sops.sops
- community.sops.sops.ansible_env
- community.sops.sops.ansible_ini
seealso:
- plugin: community.sops.sops
plugin_type: lookup
description: The sops lookup can be used decrypt SOPS-encrypted files.
- plugin: community.sops.decrypt
plugin_type: filter
description: The decrypt filter can be used to decrypt SOPS-encrypted in-memory data.
- module: community.sops.load_vars
"""
import os
from collections.abc import Sequence, Mapping
from ansible.errors import AnsibleParserError
from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.plugins.vars import BaseVarsPlugin
from ansible.utils.display import Display
from ansible.utils.vars import combine_vars
from ansible_collections.community.sops.plugins.module_utils.sops import Sops, SopsError
try:
from ansible.template import trust_as_template as _trust_as_template
HAS_DATATAGGING = True
except ImportError:
HAS_DATATAGGING = False
display = Display()
FOUND = {}
DECRYPTED = {}
def _make_safe(value):
if isinstance(value, str):
# must come *before* Sequence, as strings are also instances of Sequence
if HAS_DATATAGGING and isinstance(value, str):
return _trust_as_template(value)
return value
if isinstance(value, Sequence):
return [_make_safe(v) for v in value]
if isinstance(value, Mapping):
return dict((k, _make_safe(v)) for k, v in value.items())
return value
class VarsModule(BaseVarsPlugin):
def get_vars(self, loader, path, entities, cache=None):
''' parses the inventory file '''
if not isinstance(entities, list):
entities = [entities]
super().get_vars(loader, path, entities)
def get_option_value(argument_name):
return self.get_option(argument_name)
if cache is None:
cache = self.get_option('cache')
if self.get_option('disable_vars_plugin_temporarily'):
return {}
valid_extensions = self.get_option('valid_extensions')
handle_unencrypted_files = self.get_option('handle_unencrypted_files')
data = {}
for entity in entities:
if isinstance(entity, Host):
subdir = 'host_vars'
elif isinstance(entity, Group):
subdir = 'group_vars'
else:
raise AnsibleParserError("Supplied entity must be Host or Group, got %s instead" % (type(entity)))
# avoid 'chroot' type inventory hostnames /path/to/chroot
if not entity.name.startswith(os.path.sep):
try:
found_files = []
# load vars
b_opath = os.path.realpath(to_bytes(os.path.join(self._basedir, subdir)))
opath = to_text(b_opath)
key = '%s.%s' % (entity.name, opath)
self._display.vvvv("key: %s" % (key))
if cache and key in FOUND:
found_files = FOUND[key]
else:
# no need to do much if path does not exist for basedir
if os.path.exists(b_opath):
if os.path.isdir(b_opath):
self._display.debug("\tprocessing dir %s" % opath)
# NOTE: iterating without extension allow retrieving files recursively
# A filter is then applied by iterating on all results and filtering by
# extension.
# See:
# - https://github.com/ansible-collections/community.sops/pull/6
found_files = loader.find_vars_files(opath, entity.name, extensions=valid_extensions, allow_dir=False)
found_files.extend([file_path for file_path in loader.find_vars_files(opath, entity.name)
if any(to_text(file_path).endswith(extension) for extension in valid_extensions)])
FOUND[key] = found_files
else:
self._display.warning("Found %s that is not a directory, skipping: %s" % (subdir, opath))
for found in found_files:
if cache and found in DECRYPTED:
file_content = DECRYPTED[found]
else:
sops_runner = Sops.get_sops_runner_from_options(get_option_value, display=display)
if handle_unencrypted_files != 'error' and not sops_runner.has_filestatus():
raise AnsibleParserError(
'Cannot use handle_unencrypted_files=%s with SOPS %s' % (handle_unencrypted_files, sops_runner.version_string)
)
try:
file_content = sops_runner.decrypt(found, get_option_value=get_option_value)
except SopsError as exc:
skip = False
if sops_runner.has_filestatus():
# Check whether sops thinks the file might be encrypted. If it thinks it is not,
# skip it. Otherwise, re-raise the original error
try:
file_status = sops_runner.get_filestatus(found)
if not file_status.encrypted:
if handle_unencrypted_files == 'skip':
self._display.vvvv("SOPS vars plugin: skipping unencrypted file %s" % found)
skip = True
elif handle_unencrypted_files == 'warn':
self._display.warning("SOPS vars plugin: skipping unencrypted file %s" % found)
skip = True
elif handle_unencrypted_files == 'error':
raise AnsibleParserError("SOPS vars plugin: file %s is not encrypted" % found)
except SopsError as status_exc:
# The filestatus operation can fail for example if sops cannot parse the file
# as JSON/YAML. In that case, also re-raise the original error
self._display.warning("SOPS vars plugin: cannot obtain file status of %s: %s" % (found, status_exc))
if skip:
continue
raise
DECRYPTED[found] = file_content
new_data = _make_safe(loader.load(file_content))
if new_data: # ignore empty files
data = combine_vars(data, new_data)
except AnsibleParserError:
raise
except SopsError as e:
raise AnsibleParserError(to_native(e))
except Exception as e:
raise AnsibleParserError('Unexpected error in the SOPS vars plugin: %s' % to_native(e))
return data