update ansible collections sops, docker and debops
This commit is contained in:
parent
f1135b818f
commit
e31ee5e39c
910 changed files with 27667 additions and 19786 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
|
@ -19,10 +20,19 @@ try:
|
|||
except ImportError:
|
||||
HAS_DATATAGGING = False
|
||||
|
||||
try:
|
||||
from ansible.plugins.action import VariableLayer # type: ignore[attr-defined]
|
||||
HAS_REGISTER_HOST_VARIABLES = True
|
||||
except ImportError:
|
||||
HAS_REGISTER_HOST_VARIABLES = False
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from ansible_collections.community.sops.plugins.plugin_utils.action_module import AnsibleActionModule
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
def _make_safe(value):
|
||||
def _make_safe(value: t.Any) -> t.Any:
|
||||
if HAS_DATATAGGING and isinstance(value, str):
|
||||
return _trust_as_template(value)
|
||||
return value
|
||||
|
|
@ -30,7 +40,7 @@ def _make_safe(value):
|
|||
|
||||
class ActionModule(ActionModuleBase):
|
||||
|
||||
def _load(self, filename, module):
|
||||
def _load(self, filename: str, module: AnsibleActionModule) -> dict:
|
||||
def get_option_value(argument_name):
|
||||
return module.params.get(argument_name)
|
||||
|
||||
|
|
@ -71,6 +81,7 @@ class ActionModule(ActionModuleBase):
|
|||
file=dict(type='path', required=True),
|
||||
name=dict(type='str'),
|
||||
expressions=dict(type='str', default='ignore', choices=['ignore', 'evaluate-on-load', 'lazy-evaluation']),
|
||||
return_method=dict(type='str', default='auto', choices=['auto', 'facts-only', 'vars-only']),
|
||||
),
|
||||
)
|
||||
argument_spec.argument_spec.update(get_sops_argument_spec())
|
||||
|
|
@ -81,7 +92,15 @@ class ActionModule(ActionModuleBase):
|
|||
if expressions == 'lazy-evaluation' and not HAS_DATATAGGING:
|
||||
module.fail_json(msg='expressions=lazy-evaluation requires ansible-core 2.19+ with Data Tagging support.')
|
||||
|
||||
data = dict()
|
||||
return_method_str = module.params['return_method']
|
||||
if return_method_str == 'auto':
|
||||
return_as_facts = not HAS_REGISTER_HOST_VARIABLES
|
||||
else:
|
||||
return_as_facts = return_method_str == 'facts-only'
|
||||
if not HAS_REGISTER_HOST_VARIABLES and not return_as_facts:
|
||||
module.fail_json(msg='return_method=vars-only requires ansible-core 2.21+')
|
||||
|
||||
data = {}
|
||||
files = []
|
||||
try:
|
||||
filename = self._find_needle('vars', module.params['file'])
|
||||
|
|
@ -94,8 +113,7 @@ class ActionModule(ActionModuleBase):
|
|||
if name is None:
|
||||
value = data
|
||||
else:
|
||||
value = dict()
|
||||
value[name] = data
|
||||
value = {name: data}
|
||||
|
||||
if expressions == 'evaluate-on-load':
|
||||
value = self._evaluate(value)
|
||||
|
|
@ -103,8 +121,14 @@ class ActionModule(ActionModuleBase):
|
|||
if expressions == 'lazy-evaluation':
|
||||
value = self._make_safe(value)
|
||||
|
||||
module.exit_json(
|
||||
ansible_included_var_files=files,
|
||||
ansible_facts=value,
|
||||
_ansible_no_log=True,
|
||||
)
|
||||
result = {
|
||||
'ansible_included_var_files': files,
|
||||
'_ansible_no_log': True,
|
||||
}
|
||||
|
||||
if return_as_facts:
|
||||
result['ansible_facts'] = value
|
||||
else:
|
||||
self.register_host_variables(variables=value, layer=VariableLayer.INCLUDE_VARS)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,15 @@ options:
|
|||
- Requires SOPS 3.7.0+.
|
||||
type: path
|
||||
version_added: 1.4.0
|
||||
age_key_cmd:
|
||||
description:
|
||||
- A command that SOPS will execute to obtain an age private key that SOPS can use to decrypt encrypted files.
|
||||
- Will be set as the E(SOPS_AGE_KEY_CMD) environment variable when calling SOPS.
|
||||
- Requires SOPS 3.10.0+.
|
||||
- When running this command and when SOPS 3.12.0+ is used, SOPS will set the E(SOPS_AGE_RECIPIENT) variable
|
||||
to the identity of the key it wants to use to decrypt.
|
||||
type: str
|
||||
version_added: 2.3.0
|
||||
age_ssh_private_keyfile:
|
||||
description:
|
||||
- The file containing the SSH private key that SOPS can use to decrypt encrypted files.
|
||||
|
|
@ -42,6 +51,15 @@ options:
|
|||
- Requires SOPS 3.10.0+.
|
||||
type: path
|
||||
version_added: 1.4.0
|
||||
age_ssh_private_key_cmd:
|
||||
description:
|
||||
- A command that SOPS will execute to obtain an SSH private key that SOPS can use to decrypt encrypted files.
|
||||
- Will be set as the E(SOPS_AGE_SSH_PRIVATE_KEY_CMD) environment variable when calling SOPS.
|
||||
- When running this command, SOPS will set the E(SOPS_AGE_RECIPIENT) variable to the identity of the key
|
||||
it wants to use to decrypt.
|
||||
- Requires SOPS 3.12.0+.
|
||||
type: str
|
||||
version_added: 2.3.0
|
||||
aws_profile:
|
||||
description:
|
||||
- The AWS profile to use for requests to AWS.
|
||||
|
|
@ -66,6 +84,23 @@ options:
|
|||
- Sets the environment variable E(AWS_SESSION_TOKEN) for the SOPS call.
|
||||
type: str
|
||||
version_added: 1.0.0
|
||||
gcp_oauth_access_token:
|
||||
description:
|
||||
- Provide a Google Cloud OAauth token.
|
||||
- Will be set as the E(GOOGLE_OAUTH_ACCESS_TOKEN) environment variable when calling SOPS.
|
||||
- Requires SOPS 3.10.0+.
|
||||
type: str
|
||||
version_added: 3.2.0
|
||||
gcp_kms_client_type:
|
||||
description:
|
||||
- Determine which client to use to communicate with Google Cloud Services.
|
||||
- Will be set as the E(SOPS_GCP_KMS_CLIENT_TYPE) environment variable when calling SOPS.
|
||||
- Requires SOPS 3.12.0+.
|
||||
type: str
|
||||
choices:
|
||||
rest: Use REST client
|
||||
grpc: Use gRPC client
|
||||
version_added: 3.2.0
|
||||
config_path:
|
||||
description:
|
||||
- Path to the SOPS configuration file.
|
||||
|
|
@ -90,6 +125,12 @@ options:
|
|||
version_added: 1.0.0
|
||||
"""
|
||||
|
||||
ANSIBLE_PLUGIN = r'''
|
||||
options:
|
||||
sops_binary:
|
||||
type: str
|
||||
'''
|
||||
|
||||
ANSIBLE_VARIABLES = r'''
|
||||
options:
|
||||
sops_binary:
|
||||
|
|
@ -101,9 +142,15 @@ options:
|
|||
age_keyfile:
|
||||
vars:
|
||||
- name: sops_age_keyfile
|
||||
age_key_cmd:
|
||||
vars:
|
||||
- name: sops_age_key_cmd
|
||||
age_ssh_private_keyfile:
|
||||
vars:
|
||||
- name: sops_age_ssh_private_keyfile
|
||||
age_ssh_private_key_cmd:
|
||||
vars:
|
||||
- name: sops_age_ssh_private_key_cmd
|
||||
aws_profile:
|
||||
vars:
|
||||
- name: sops_aws_profile
|
||||
|
|
@ -118,6 +165,12 @@ options:
|
|||
- name: sops_session_token
|
||||
- name: sops_aws_session_token
|
||||
version_added: 1.2.0
|
||||
gcp_oauth_access_token:
|
||||
vars:
|
||||
- name: sops_gcp_oauth_access_token
|
||||
gcp_kms_client_type:
|
||||
vars:
|
||||
- name: sops_gcp_kms_client_type
|
||||
config_path:
|
||||
vars:
|
||||
- name: sops_config_path
|
||||
|
|
@ -141,9 +194,15 @@ options:
|
|||
age_keyfile:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_AGE_KEYFILE
|
||||
age_key_cmd:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_AGE_KEY_CMD
|
||||
age_ssh_private_keyfile:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_AGE_SSH_PRIVATE_KEYFILE
|
||||
age_ssh_private_key_cmd:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_AGE_SSH_PRIVATE_KEY_CMD
|
||||
aws_profile:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_AWS_PROFILE
|
||||
|
|
@ -160,6 +219,12 @@ options:
|
|||
env:
|
||||
- name: ANSIBLE_SOPS_AWS_SESSION_TOKEN
|
||||
version_added: 1.2.0
|
||||
gcp_oauth_access_token:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_GCP_OAUTH_ACCESS_TOKEN
|
||||
gcp_kms_client_type:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_GCP_KMS_CLIENT_TYPE
|
||||
config_path:
|
||||
env:
|
||||
- name: ANSIBLE_SOPS_CONFIG_PATH
|
||||
|
|
@ -188,10 +253,18 @@ options:
|
|||
ini:
|
||||
- section: community.sops
|
||||
key: age_keyfile
|
||||
age_key_cmd:
|
||||
ini:
|
||||
- section: community.sops
|
||||
key: age_key_cmd
|
||||
age_ssh_private_keyfile:
|
||||
ini:
|
||||
- section: community.sops
|
||||
key: age_ssh_private_keyfile
|
||||
age_ssh_private_key_cmd:
|
||||
ini:
|
||||
- section: community.sops
|
||||
key: age_ssh_private_key_cmd
|
||||
aws_profile:
|
||||
ini:
|
||||
- section: community.sops
|
||||
|
|
@ -210,6 +283,13 @@ options:
|
|||
- section: community.sops
|
||||
key: aws_session_token
|
||||
version_added: 1.2.0
|
||||
# We do not provide an INI key for
|
||||
# gcp_oauth_access_token
|
||||
# to make sure that secrets cannot be provided in ansible.ini. Use environment variables or another mechanism for that.
|
||||
gcp_kms_client_type:
|
||||
ini:
|
||||
- section: community.sops
|
||||
key: gcp_kms_client_type
|
||||
config_path:
|
||||
ini:
|
||||
- section: community.sops
|
||||
|
|
@ -272,6 +352,14 @@ options:
|
|||
type: list
|
||||
elements: str
|
||||
version_added: 1.0.0
|
||||
huawei_cloud_kms:
|
||||
description:
|
||||
- HuaweiCloud KMS key IDs to use.
|
||||
- This corresponds to the SOPS C(--hckms) option.
|
||||
- Requires SOPS 3.12.0+.
|
||||
type: list
|
||||
elements: str
|
||||
version_added: 3.2.0
|
||||
unencrypted_suffix:
|
||||
description:
|
||||
- Override the unencrypted key suffix.
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ def pick_latest_version(version_list):
|
|||
version_list = [v for v in version_list if '-' not in v and '+' not in v]
|
||||
if not version_list:
|
||||
return ''
|
||||
return sorted(version_list, key=LooseVersion, reverse=True)[0]
|
||||
return max(version_list, key=LooseVersion)
|
||||
|
||||
|
||||
class FilterModule:
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native
|
|||
from ansible.utils.display import Display
|
||||
|
||||
from ansible_collections.community.sops.plugins.module_utils.sops import Sops, SopsError
|
||||
from ansible_collections.community.sops.plugins.plugin_utils._args import wrap_get_option_value_check_types
|
||||
|
||||
|
||||
_VALID_TYPES = set(['binary', 'json', 'yaml', 'dotenv', 'ini'])
|
||||
|
|
@ -115,7 +116,8 @@ _VALID_TYPES = set(['binary', 'json', 'yaml', 'dotenv', 'ini'])
|
|||
|
||||
def decrypt_filter(data, input_type='yaml', output_type='yaml', sops_binary='sops', rstrip=True, decode_output=True,
|
||||
aws_profile=None, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None,
|
||||
config_path=None, enable_local_keyservice=True, keyservice=None, age_key=None, age_keyfile=None, age_ssh_private_keyfile=None):
|
||||
config_path=None, enable_local_keyservice=True, keyservice=None, age_key=None, age_keyfile=None, age_ssh_private_keyfile=None,
|
||||
age_key_cmd=None, age_ssh_private_key_cmd=None, gcp_oauth_access_token=None, gcp_kms_client_type=None):
|
||||
'''Decrypt sops-encrypted data.'''
|
||||
|
||||
# Check parameters
|
||||
|
|
@ -150,16 +152,33 @@ def decrypt_filter(data, input_type='yaml', output_type='yaml', sops_binary='sop
|
|||
return enable_local_keyservice
|
||||
if argument_name == 'keyservice':
|
||||
return keyservice
|
||||
raise AssertionError('internal error: should not be reached')
|
||||
if argument_name == 'age_key_cmd':
|
||||
return age_key_cmd
|
||||
if argument_name == 'age_ssh_private_key_cmd':
|
||||
return age_ssh_private_key_cmd
|
||||
if argument_name == 'gcp_oauth_access_token':
|
||||
return gcp_oauth_access_token
|
||||
if argument_name == 'gcp_kms_client_type':
|
||||
return gcp_kms_client_type
|
||||
raise AssertionError('internal error: should not be reached') # pragma: no cover
|
||||
|
||||
# Decode
|
||||
data = to_bytes(data)
|
||||
try:
|
||||
output = Sops.decrypt(
|
||||
None, content=data, display=Display(), rstrip=rstrip, decode_output=decode_output,
|
||||
input_type=input_type, output_type=output_type, get_option_value=get_option_value)
|
||||
None,
|
||||
content=data,
|
||||
display=Display(),
|
||||
rstrip=rstrip,
|
||||
decode_output=decode_output,
|
||||
input_type=input_type,
|
||||
output_type=output_type,
|
||||
get_option_value=wrap_get_option_value_check_types(get_option_value, add_encrypt_specific=False),
|
||||
)
|
||||
except SopsError as e:
|
||||
raise AnsibleFilterError(to_native(e))
|
||||
except ValueError as e:
|
||||
raise AnsibleFilterError(f"Error in community.sops.decrypt filter: {e}")
|
||||
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ options:
|
|||
type: str
|
||||
version_added: 1.9.0
|
||||
extends_documentation_fragment:
|
||||
- community.sops.sops.ansible_plugin # must come before community.sops.sops!
|
||||
- community.sops.sops
|
||||
- community.sops.sops.ansible_variables
|
||||
- community.sops.sops.ansible_env
|
||||
|
|
@ -116,9 +117,12 @@ _raw:
|
|||
import base64
|
||||
|
||||
from ansible.errors import AnsibleLookupError
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.compat.version import LooseVersion
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.release import __version__ as ansible_version
|
||||
from ansible_collections.community.sops.plugins.module_utils.sops import Sops, SopsError
|
||||
from ansible_collections.community.sops.plugins.plugin_utils._args import wrap_get_option_value_plugin_path
|
||||
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
|
@ -137,8 +141,25 @@ class LookupModule(LookupBase):
|
|||
|
||||
ret = []
|
||||
|
||||
def get_option_value(argument_name):
|
||||
return self.get_option(argument_name)
|
||||
try:
|
||||
sops_binary, sops_binary_origin = self.get_option_and_origin("sops_binary")
|
||||
if sops_binary is None and (
|
||||
LooseVersion(ansible_version) < LooseVersion("2.18.8")
|
||||
or
|
||||
LooseVersion(ansible_version) == LooseVersion("2.19.0")
|
||||
):
|
||||
# Ansible-core < 2.18.8 and 2.19.0 are broken: https://github.com/ansible/ansible/issues/85480
|
||||
sops_binary = self.get_option("sops_binary")
|
||||
sops_binary_origin = "Direct"
|
||||
except AttributeError:
|
||||
# Ansible-core 2.15 has no get_option_and_origin()!
|
||||
sops_binary = self.get_option("sops_binary")
|
||||
sops_binary_origin = "Direct"
|
||||
get_option_value = wrap_get_option_value_plugin_path(
|
||||
self.get_option,
|
||||
sops_binary=sops_binary,
|
||||
sops_binary_origin=sops_binary_origin,
|
||||
)
|
||||
|
||||
for term in terms:
|
||||
display.debug("Sops lookup term: %s" % term)
|
||||
|
|
|
|||
|
|
@ -96,11 +96,15 @@ def _create_env_variable(argument_name):
|
|||
GENERAL_OPTIONS = {
|
||||
'age_key': _create_env_variable('SOPS_AGE_KEY'),
|
||||
'age_keyfile': _create_env_variable('SOPS_AGE_KEY_FILE'),
|
||||
'age_key_cmd': _create_env_variable('SOPS_AGE_KEY_CMD'),
|
||||
'age_ssh_private_keyfile': _create_env_variable('SOPS_AGE_SSH_PRIVATE_KEY_FILE'),
|
||||
'age_ssh_private_key_cmd': _create_env_variable('SOPS_AGE_SSH_PRIVATE_KEY_CMD'),
|
||||
'aws_profile': _create_single_arg('--aws-profile'),
|
||||
'aws_access_key_id': _create_env_variable('AWS_ACCESS_KEY_ID'),
|
||||
'aws_secret_access_key': _create_env_variable('AWS_SECRET_ACCESS_KEY'),
|
||||
'aws_session_token': _create_env_variable('AWS_SESSION_TOKEN'),
|
||||
'gcp_oauth_access_token': _create_env_variable('GOOGLE_OAUTH_ACCESS_TOKEN'),
|
||||
'gcp_kms_client_type': _create_env_variable('SOPS_GCP_KMS_CLIENT_TYPE'),
|
||||
'config_path': _create_single_arg('--config', pre=True),
|
||||
'enable_local_keyservice': _create_boolean('--enable-local-keyservice=false', invert=True),
|
||||
'keyservice': _create_repeated('--keyservice'),
|
||||
|
|
@ -114,6 +118,7 @@ ENCRYPT_OPTIONS = {
|
|||
'azure_kv': _create_comma_separated('--azure-kv'),
|
||||
'hc_vault_transit': _create_comma_separated('--hc-vault-transit'),
|
||||
'pgp': _create_comma_separated('--pgp'),
|
||||
'huawei_cloud_kms': _create_comma_separated('--hckms'),
|
||||
'unencrypted_suffix': _create_single_arg('--unencrypted-suffix'),
|
||||
'encrypted_suffix': _create_single_arg('--encrypted-suffix'),
|
||||
'unencrypted_regex': _create_single_arg('--unencrypted-regex'),
|
||||
|
|
@ -289,7 +294,7 @@ class SopsRunner(object):
|
|||
raise SopsError(path, 0, 'Cannot decode filestatus result: %s' % exc, operation='inspect')
|
||||
|
||||
|
||||
_SOPS_RUNNER_CACHE = dict()
|
||||
_SOPS_RUNNER_CACHE = dict() # type: dict[str, SopsRunner]
|
||||
|
||||
|
||||
class Sops():
|
||||
|
|
@ -357,9 +362,17 @@ def get_sops_argument_spec(add_encrypt_specific=False):
|
|||
'age_keyfile': {
|
||||
'type': 'path',
|
||||
},
|
||||
'age_key_cmd': {
|
||||
'type': 'str',
|
||||
'no_log': False,
|
||||
},
|
||||
'age_ssh_private_keyfile': {
|
||||
'type': 'path',
|
||||
},
|
||||
'age_ssh_private_key_cmd': {
|
||||
'type': 'str',
|
||||
'no_log': False,
|
||||
},
|
||||
'aws_profile': {
|
||||
'type': 'str',
|
||||
},
|
||||
|
|
@ -374,6 +387,14 @@ def get_sops_argument_spec(add_encrypt_specific=False):
|
|||
'type': 'str',
|
||||
'no_log': True,
|
||||
},
|
||||
'gcp_oauth_access_token': {
|
||||
'type': 'str',
|
||||
'no_log': True,
|
||||
},
|
||||
'gcp_kms_client_type': {
|
||||
'type': 'str',
|
||||
'choices': ['rest', 'grpc'],
|
||||
},
|
||||
'config_path': {
|
||||
'type': 'path',
|
||||
},
|
||||
|
|
@ -412,6 +433,10 @@ def get_sops_argument_spec(add_encrypt_specific=False):
|
|||
'type': 'list',
|
||||
'elements': 'str',
|
||||
},
|
||||
'huawei_cloud_kms': {
|
||||
'type': 'list',
|
||||
'elements': 'str',
|
||||
},
|
||||
'unencrypted_suffix': {
|
||||
'type': 'str',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -42,6 +42,23 @@ options:
|
|||
- ignore
|
||||
- evaluate-on-load
|
||||
- lazy-evaluation
|
||||
return_method:
|
||||
description:
|
||||
- Determine how to return the results.
|
||||
- Because of limitations of ansible-core < 2.21, the results were returned as Ansible facts.
|
||||
Since ansible-core 2.21, it is possible to actually set variables, similar to how M(ansible.builtin.include_vars)
|
||||
is working.
|
||||
type: str
|
||||
default: auto
|
||||
choices:
|
||||
auto:
|
||||
- Return the results as facts for ansible-core < 2.21, and as variables for ansible-core >= 2.21.
|
||||
facts-only:
|
||||
- Return the results always as facts.
|
||||
vars-only:
|
||||
- Return the results always as variables.
|
||||
- Fails for ansible-core < 2.21.
|
||||
version_added: 2.3.0
|
||||
extends_documentation_fragment:
|
||||
- community.sops.sops
|
||||
- community.sops.attributes
|
||||
|
|
@ -61,7 +78,9 @@ attributes:
|
|||
details:
|
||||
- This action does not modify state.
|
||||
facts:
|
||||
support: full
|
||||
support: partial
|
||||
details:
|
||||
- Only returns facts if O(return_method=facts-only), or O(return_method=auto) for ansible-core < 2.21.
|
||||
idempotent:
|
||||
support: N/A
|
||||
details:
|
||||
|
|
@ -105,7 +124,7 @@ EXAMPLES = r"""
|
|||
RETURN = r"""
|
||||
ansible_facts:
|
||||
description: Variables that were included and their values.
|
||||
returned: success
|
||||
returned: success, and O(return_method=facts-only), or O(return_method=auto) for ansible-core < 2.21
|
||||
type: dict
|
||||
sample: {'variable': 'value'}
|
||||
ansible_included_var_files:
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ try:
|
|||
except ImportError:
|
||||
YAML_IMP_ERR = traceback.format_exc()
|
||||
HAS_YAML = False
|
||||
yaml = None
|
||||
yaml = None # type: ignore[assignment] # use better type later...
|
||||
|
||||
|
||||
def get_data_type(module):
|
||||
|
|
@ -122,7 +122,7 @@ def get_data_type(module):
|
|||
return 'json'
|
||||
if module.params['content_yaml'] is not None:
|
||||
return 'yaml'
|
||||
module.fail_json(msg='Internal error: unknown content type')
|
||||
module.fail_json(msg='Internal error: unknown content type') # pragma: no cover
|
||||
|
||||
|
||||
def compare_encoded_content(module, binary_data, content):
|
||||
|
|
@ -144,7 +144,7 @@ def compare_encoded_content(module, binary_data, content):
|
|||
except Exception:
|
||||
# Treat parsing errors as content not equal
|
||||
return False
|
||||
module.fail_json(msg='Internal error: unknown content type')
|
||||
module.fail_json(msg='Internal error: unknown content type') # pragma: no cover
|
||||
|
||||
|
||||
def get_encoded_type_content(module, binary_data):
|
||||
|
|
@ -156,7 +156,7 @@ def get_encoded_type_content(module, binary_data):
|
|||
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')
|
||||
module.fail_json(msg='Internal error: unknown content type') # pragma: no cover
|
||||
|
||||
|
||||
def main():
|
||||
|
|
|
|||
110
ansible_collections/community/sops/plugins/plugin_utils/_args.py
Normal file
110
ansible_collections/community/sops/plugins/plugin_utils/_args.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Copyright (c) 2026 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
|
||||
|
||||
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
|
||||
# Do not use this from other collections or standalone plugins/modules!
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import typing as t
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes as _to_bytes
|
||||
from ansible.module_utils.common.validation import check_type_bool as _check_type_bool
|
||||
from ansible.module_utils.common.validation import check_type_dict as _check_type_dict
|
||||
from ansible.module_utils.common.validation import check_type_int as _check_type_int
|
||||
from ansible.module_utils.common.validation import check_type_float as _check_type_float
|
||||
from ansible.module_utils.common.validation import check_type_list as _check_type_list
|
||||
from ansible.module_utils.common.validation import check_type_path as _check_type_path
|
||||
from ansible.module_utils.common.validation import check_type_str as _check_type_str
|
||||
from ansible.utils.path import unfrackpath as _unfrackpath
|
||||
|
||||
from ansible_collections.community.sops.plugins.module_utils.sops import get_sops_argument_spec as _get_sops_argument_spec
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from collections.abc import Callable, Mapping # pragma: no cover
|
||||
|
||||
|
||||
def wrap_get_option_value(
|
||||
get_option_value: Callable[[str], t.Any],
|
||||
*,
|
||||
overrides: Mapping[str, t.Any],
|
||||
) -> Callable[[str], t.Any]:
|
||||
def new_get_option_value(option: str) -> t.Any:
|
||||
if option in overrides:
|
||||
return overrides[option]
|
||||
return get_option_value(option)
|
||||
|
||||
return new_get_option_value
|
||||
|
||||
|
||||
def wrap_get_option_value_plugin_path(
|
||||
get_option_value: Callable[[str], t.Any],
|
||||
*,
|
||||
sops_binary: t.Any,
|
||||
sops_binary_origin: str | None = "Direct",
|
||||
) -> Callable[[str], t.Any]:
|
||||
if isinstance(sops_binary, str):
|
||||
candidate = sops_binary
|
||||
# ansible.config.manager.resolve_path() handles {{CWD}}:
|
||||
if "{{CWD}}" in candidate:
|
||||
candidate = candidate.replace("{{CWD}}", os.getcwd())
|
||||
basedir = sops_binary_origin if sops_binary_origin and os.path.isabs(sops_binary_origin) and os.path.exists(_to_bytes(sops_binary_origin)) else None
|
||||
candidate = _unfrackpath(candidate, follow=False, basedir=basedir)
|
||||
# Check whether the candidate is a file:
|
||||
if os.path.isfile(candidate):
|
||||
sops_binary = candidate
|
||||
else:
|
||||
# If not, fall back to what a module would do with the path
|
||||
sops_binary = os.path.expanduser(os.path.expandvars(sops_binary))
|
||||
overrides = {"sops_binary": sops_binary}
|
||||
return wrap_get_option_value(get_option_value, overrides=overrides)
|
||||
|
||||
|
||||
_TYPE_CHECKS = {
|
||||
"dict": _check_type_dict,
|
||||
"bool": _check_type_bool,
|
||||
"int": _check_type_int,
|
||||
"float": _check_type_float,
|
||||
"str": lambda value: _check_type_str(value, allow_conversion=False),
|
||||
"path": lambda value: _check_type_path(_check_type_str(value, allow_conversion=False)),
|
||||
}
|
||||
|
||||
|
||||
def _ensure_type_impl(value: t.Any, *, option_type: str) -> t.Any:
|
||||
return _TYPE_CHECKS[option_type](value)
|
||||
|
||||
|
||||
def _ensure_type(value: t.Any, *, option_type: str, elements_type: str | None, sensitive: bool) -> t.Any:
|
||||
if value is None:
|
||||
return None
|
||||
if option_type != "list":
|
||||
return _ensure_type_impl(value, option_type=option_type)
|
||||
value = _check_type_list(value)
|
||||
if elements_type is None:
|
||||
# Our options do not have a list without 'elements', so this is unreachable.
|
||||
# We still want to keep this code though...
|
||||
return value # pragma: no cover
|
||||
return [_ensure_type_impl(v, option_type=elements_type) for v in value]
|
||||
|
||||
|
||||
def wrap_get_option_value_check_types(
|
||||
get_option_value: Callable[[str], t.Any],
|
||||
*,
|
||||
add_encrypt_specific: bool,
|
||||
) -> Callable[[str], t.Any]:
|
||||
overrides: dict[str, t.Any] = {}
|
||||
for option, data in _get_sops_argument_spec(add_encrypt_specific=add_encrypt_specific).items():
|
||||
value = get_option_value(option)
|
||||
try:
|
||||
value = _ensure_type(
|
||||
value,
|
||||
option_type=data.get("type", "str"),
|
||||
elements_type=data.get("elements"),
|
||||
sensitive=data.get("no_log", False),
|
||||
)
|
||||
except TypeError as exc:
|
||||
raise ValueError(f"option {option} has invalid value: {exc}")
|
||||
overrides[option] = value
|
||||
return wrap_get_option_value(get_option_value, overrides=overrides)
|
||||
|
|
@ -16,6 +16,7 @@ from __future__ import annotations
|
|||
import abc
|
||||
import copy
|
||||
import traceback
|
||||
import typing as t
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.basic import SEQUENCETYPE, remove_values
|
||||
|
|
@ -25,8 +26,14 @@ from ansible.plugins.action import ActionBase
|
|||
from ansible.module_utils.common.arg_spec import ArgumentSpecValidator
|
||||
from ansible.module_utils.errors import UnsupportedError
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
safe_eval: Callable[[t.Any, t.Any, t.Any], t.Any] | None
|
||||
|
||||
try:
|
||||
from ansible.module_utils.common.validation import (
|
||||
from ansible.module_utils.common.validation import ( # type: ignore[no-redef]
|
||||
safe_eval,
|
||||
)
|
||||
except ImportError:
|
||||
|
|
@ -207,7 +214,7 @@ class ActionModuleBase(ActionBase, metaclass=abc.ABCMeta):
|
|||
argument_spec, kwargs = self.setup_module()
|
||||
module = argument_spec.create_ansible_module_helper(AnsibleActionModule, (self, ), **kwargs)
|
||||
self.run_module(module)
|
||||
raise AnsibleError('Internal error: action module did not call module.exit_json()')
|
||||
raise AnsibleError('Internal error: action module did not call module.exit_json()') # pragma: no cover
|
||||
except _ModuleExitException as mee:
|
||||
result.update(mee.result)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ options:
|
|||
- name: ANSIBLE_VARS_SOPS_PLUGIN_HANDLE_UNENCRYPTED_FILES
|
||||
extends_documentation_fragment:
|
||||
- ansible.builtin.vars_plugin_staging
|
||||
- community.sops.sops.ansible_plugin # must come before community.sops.sops!
|
||||
- community.sops.sops
|
||||
- community.sops.sops.ansible_env
|
||||
- community.sops.sops.ansible_ini
|
||||
|
|
@ -101,6 +102,7 @@ seealso:
|
|||
"""
|
||||
|
||||
import os
|
||||
import typing as t
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
from ansible.errors import AnsibleParserError
|
||||
|
|
@ -111,6 +113,7 @@ 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
|
||||
from ansible_collections.community.sops.plugins.plugin_utils._args import wrap_get_option_value_plugin_path
|
||||
|
||||
try:
|
||||
from ansible.template import trust_as_template as _trust_as_template
|
||||
|
|
@ -121,11 +124,11 @@ except ImportError:
|
|||
|
||||
display = Display()
|
||||
|
||||
FOUND = {}
|
||||
DECRYPTED = {}
|
||||
FOUND: dict[str, list[str]] = {}
|
||||
DECRYPTED: dict[str, bytes] = {}
|
||||
|
||||
|
||||
def _make_safe(value):
|
||||
def _make_safe(value: t.Any) -> t.Any:
|
||||
if isinstance(value, str):
|
||||
# must come *before* Sequence, as strings are also instances of Sequence
|
||||
if HAS_DATATAGGING and isinstance(value, str):
|
||||
|
|
@ -148,8 +151,17 @@ class VarsModule(BaseVarsPlugin):
|
|||
|
||||
super().get_vars(loader, path, entities)
|
||||
|
||||
def get_option_value(argument_name):
|
||||
return self.get_option(argument_name)
|
||||
try:
|
||||
sops_binary, sops_binary_origin = self.get_option_and_origin("sops_binary")
|
||||
except AttributeError:
|
||||
# Ansible-core 2.15 has no get_option_and_origin()!
|
||||
sops_binary = self.get_option("sops_binary")
|
||||
sops_binary_origin = "Direct"
|
||||
get_option_value = wrap_get_option_value_plugin_path(
|
||||
self.get_option,
|
||||
sops_binary=sops_binary,
|
||||
sops_binary_origin=sops_binary_origin,
|
||||
)
|
||||
|
||||
if cache is None:
|
||||
cache = self.get_option('cache')
|
||||
|
|
|
|||
Loading…
Reference in a new issue