This commit is contained in:
Brian Coca 2026-05-27 22:06:50 -05:00 committed by GitHub
commit 2749adc80d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 123 additions and 4 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- CLI - Tools that execute tasks can now pass one or more ``-E`` options to define key-value pairs (k=v or YAML/JSON mapping) to be added to the ``environment`` task keyword for all hosts.

View file

@ -19,13 +19,14 @@ import yaml
import ansible
from ansible import constants as C
from ansible._internal import _templating
from ansible._internal._datatag._tags import TrustedAsTemplate, Origin
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.common.yaml import HAS_LIBYAML, yaml_load
from ansible.release import __version__
from ansible.parsing.splitter import parse_kv
from ansible.parsing.utils.yaml import from_yaml
from ansible.utils.path import unfrackpath
from ansible._internal._datatag._tags import TrustedAsTemplate, Origin
#
# Special purpose OptionParsers
@ -233,6 +234,46 @@ def maybe_unfrack_path(beacon):
return inner
def parse_env_vars(value: str) -> dict[str, str]:
"""
Parse command line args to set the 'environment' keyword
"""
if value.startswith('{'):
data = from_yaml(value)
elif value.startswith('@'):
filename = unfrackpath(value[1:])
try:
with open(filename, errors='strict') as f:
data = from_yaml(f.read(), file_name=filename)
except OSError as e:
raise ValueError(f"Cannot access environment file {filename!r}") from e
except ValueError as e:
raise ValueError(f"Cannot decode file {filename!r}") from e
elif '=' in value:
data = parse_kv(value)
else:
raise ValueError(f"Unable to parse environment option, not YAML/JSON nor k=v pairs nor a file: {value!r}")
if not isinstance(data, dict):
raise TypeError(f"Error while parsing environment values, expected a dictionary, got a {type(data)!r}")
final = {}
for k, v in data.items():
if not isinstance(k, str):
raise TypeError(f"Environment key is required to be a string, but {k!r} is a {type(k)!r} instead.")
try:
final[k] = TrustedAsTemplate().tag(str(v))
except UnicodeError as e:
raise ValueError(f"Environment values are required to be strings, {k!r}'s value could not be converted.") from e
# TODO: add Origin
return final
def _git_repo_info(repo_path):
""" returns a string containing git branch, commit id and commit date """
result = None
@ -502,6 +543,8 @@ def add_runtask_options(parser):
"""Add options for commands that run a task"""
parser.add_argument('-e', '--extra-vars', dest="extra_vars", action="append", type=maybe_unfrack_path('@'),
help="set additional variables as key=value or YAML/JSON, if filename prepend with @", default=[])
parser.add_argument('-E', '--environment', dest='environment', action='append', default=[], type=parse_env_vars,
help="Set environment variables (key=value or YAML/JSON formatted or @filename.yml) when executing a task on the target.")
def add_tasknoplay_options(parser):

View file

@ -271,6 +271,8 @@ class PullCLI(CLI):
repo_opts, limit_opts)
for ev in context.CLIARGS['extra_vars']:
cmd += ' -e %s' % shlex.quote(ev)
for env_var in context.CLIARGS.get('environment', []):
cmd += ' ' + shlex.join(('-E', str(dict(env_var))))
# Nap?
if context.CLIARGS['sleep']:
@ -318,6 +320,8 @@ class PullCLI(CLI):
for ev in context.CLIARGS['extra_vars']:
cmd += ' -e %s' % shlex.quote(ev)
for env_var in context.CLIARGS.get('environment', []):
cmd += ' -E %s' % shlex.quote(str(dict(env_var)))
if context.CLIARGS['become_ask_pass']:
cmd += ' --ask-become-pass'

View file

@ -2032,6 +2032,17 @@ TARGET_LOG_INFO:
vars:
- name: ansible_target_log_info
version_added: "2.17"
TASK_ENVIRONMENT:
name: Task environment
description:
- Environment variables to be provided for the task upon execution on the target.
- This can ONLY affects module execution, it does not affect any other type of plugin nor Ansible itself nor its configuration.
- This is not a recommended way to pass in confidential data.
type: list
keyword:
- name: environment
cli:
- name: environment
TASK_TIMEOUT:
name: Task Timeout
default: 0

View file

@ -20,8 +20,7 @@ from __future__ import annotations
import functools as _functools
import pathlib as _pathlib
from ansible import constants as C
from ansible import context
from ansible import context, constants as C
from ansible.errors import AnsibleError
from ansible.errors import AnsibleParserError, AnsibleAssertionError, AnsibleValueOmittedError
from ansible.module_utils.common.collections import is_sequence
@ -173,6 +172,16 @@ class Play(Base, Taggable, CollectionSearch):
ds['remote_user'] = ds['user']
del ds['user']
# prepend possible CLI environment vars (tuple of immutable dicts), these are not a default values
cli = C.config.get_config_value('TASK_ENVIRONMENT')
if cli:
play_env = ds.get('environment')
ds['environment'] = [dict(d) for d in cli] # from ImmutableDict
if play_env:
if not isinstance(play_env, list):
play_env = [play_env]
ds['environment'].extend(play_env)
return super(Play, self).preprocess_data(ds)
def _load(self, attr: str, ds: object) -> list[Block]:

View file

@ -28,3 +28,7 @@ ansible localhost -m assert -a '{"that": "ansible_facts.distribution is defined"
ansible --flush-cache localhost -m debug -a "msg={{ ansible_facts }}" | grep '"msg": {}'
# test meta end_play
ansible localhost -m include_role -a name=end_play
# test environment variable setting with -E option (basic KEY=VALUE)
ansible localhost -m gather_facts -a 'gather_subset="env"' -E 'TEST_ENV_VAR=ansible_test_value' | grep '"TEST_ENV_VAR": "ansible_test_value"'
# test environment variable setting with -E option (YAML/JSON format)
ansible localhost -m gather_facts -a 'gather_subset="env"' -E '{"TEST_JSON_VAR": "json_test_value"}' | grep '"TEST_JSON_VAR": "json_test_value"'

View file

@ -14,3 +14,8 @@ if grep -q "ERROR" err.txt; then
echo "Failed to execute end_play"
exit 1
fi
# test environment variable setting with -E option (basic KEY=VALUE)
echo 'gather_facts gather_subset=env' | ansible-console localhost -E 'TEST_CONSOLE_VAR=console_test_value' | grep '"TEST_CONSOLE_VAR": "console_test_value"'
# test environment variable setting with -E option (JSON format)
echo 'gather_facts gather_subset=env' | ansible-console localhost -E '{"TEST_CONSOLE_JSON": "console_json_value"}' | grep '"TEST_CONSOLE_JSON": "console_json_value"'

View file

@ -0,0 +1,13 @@
---
- hosts: localhost
gather_facts: false
module_defaults:
gather_facts:
gather_subset: ['env']
tasks:
- name: Verify all environment variables are accessible via ansible_env
assert:
that:
- ansible_env.TEST_PULL is defined
- ansible_env.TEST_PULL == "pull_test_value"
fail_msg: "Environment variables not accessible via -E option in ansible-pull"

View file

@ -142,3 +142,11 @@ pass_tests
if [ "${ORIG_CONFIG}" != "" ]; then
export ANSIBLE_CONFIG="${ORIG_CONFIG}"
fi
# test environment variable setting with -E option (basic KEY=VALUE)
ansible-pull -d "${pull_dir}" -U "${repo_dir}" env_var_test.yml \
-e 'ansble_python_interpreter={{ansible_playbook_python}}' -E 'TEST_PULL=pull_test_value' "$@"
# test environment variable setting with -E option (JSON format)
ansible-pull -d "${pull_dir}" -U "${repo_dir}" env_var_test.yml \
-e 'ansble_python_interpreter={{ansible_playbook_python}}' -E '{"TEST_PULL": "pull_test_value"}' "$@"

View file

@ -0,0 +1,10 @@
---
- hosts: localhost
gather_facts: yes
tasks:
- name: Verify all environment variables are accessible via ansible_env
assert:
that:
- ansible_env.TEST_PLAYBOOK is defined
- ansible_env.TEST_PLAYBOOK == "playbook_test_value"
fail_msg: "Environment variables not accessible via -E option"

View file

@ -0,0 +1 @@
TEST_PLAYBOOK: playbook_test_value

View file

@ -90,3 +90,12 @@ ansible-playbook -i ../../inventory vars_files_null.yml -v "$@"
# test vars_files: filename.yml
ansible-playbook -i ../../inventory vars_files_string.yml -v "$@"
# test environment variable setting with -E option (basic KEY=VALUE)
ansible-playbook -i ../../inventory env_var_test.yml -E '@playbook_env_vars.yml' "$@"
# test environment variable setting with -E option (basic KEY=VALUE)
ansible-playbook -i ../../inventory env_var_test.yml -E 'TEST_PLAYBOOK=playbook_test_value' "$@"
# test environment variable setting with -E option (JSON format)
ansible-playbook -i ../../inventory env_var_test.yml -E '{"TEST_PLAYBOOK": "playbook_test_value"}' "$@"