summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/sops/plugins/vars/sops.py
blob: 8b83a06b3ea9bc760d9af7008c2f44d80bd4a32c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# -*- coding: utf-8 -*-
#
# 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 (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
    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 group_vars/ and host_vars/ directories.
        - Files are encrypted prior to reading, making this plugin an effective companion to host_group_vars plugin.
        - Files are restricted to .sops.yaml, .sops.yml, .sops.json 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 which should be YAML or JSON or vaulted versions of these."
          - 'This affects vars_files, include_vars, inventory and vars plugins among others.'
        type: list
        elements: string
      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
    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 descrypt sops-encrypted in-memory data.
        - module: community.sops.load_vars
'''

import os
from ansible.errors import AnsibleParserError
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.plugins.vars import BaseVarsPlugin
from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible.utils.vars import combine_vars
from ansible_collections.community.sops.plugins.module_utils.sops import Sops

from ansible.utils.display import Display
display = Display()


FOUND = {}
DECRYPTED = {}
DEFAULT_VALID_EXTENSIONS = [".sops.yaml", ".sops.yml", ".sops.json"]


class VarsModule(BaseVarsPlugin):

    def get_vars(self, loader, path, entities, cache=None):
        ''' parses the inventory file '''

        if not isinstance(entities, list):
            entities = [entities]

        super(VarsModule, self).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 {}

        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=DEFAULT_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 DEFAULT_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:
                            file_content = Sops.decrypt(found, display=display, get_option_value=get_option_value)
                            DECRYPTED[found] = file_content
                        new_data = loader.load(file_content)
                        if new_data:  # ignore empty files
                            data = combine_vars(data, new_data)

                except Exception as e:
                    raise AnsibleParserError(to_native(e))

        return data