Role fixes (#82339)

* Various fixes to roles

  - static property is now properly set
  - role_names and other magic vars now have full list
  - role public/private var loading is now done when adding to play.roles instead of on each var query
  - added tests

Co-authored-by: Felix Fontein <felix@fontein.de>
pull/82465/head
Brian Coca 6 months ago committed by GitHub
parent fa92228b50
commit 55065c0042
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,3 @@
bugfixes:
- roles, the ``static`` property is now correctly set, this will fix issues with ``public`` and ``DEFAULT_PRIVATE_ROLE_VARS`` controls on exporting vars.
- roles, code cleanup and performance optimization of dependencies, now cached, and ``public`` setting is now determined once, at role instantiation.

@ -944,10 +944,12 @@ DEFAULT_PRIVATE_ROLE_VARS:
name: Private role variables name: Private role variables
default: False default: False
description: description:
- Makes role variables inaccessible from other roles. - By default, imported roles publish their variables to the play and other roles, this setting can avoid that.
- This was introduced as a way to reset role variables to default values if - This was introduced as a way to reset role variables to default values if a role is used more than once
a role is used more than once in a playbook. in a playbook.
- Starting in version '2.17' M(ansible.builtin.include_roles) and M(ansible.builtin.import_roles) can override this via the C(public) parameter. - Starting in version '2.17' M(ansible.builtin.include_roles) and M(ansible.builtin.import_roles) can
indivudually override this via the C(public) parameter.
- Included roles only make their variables public at execution, unlike imported roles which happen at playbook compile time.
env: [{name: ANSIBLE_PRIVATE_ROLE_VARS}] env: [{name: ANSIBLE_PRIVATE_ROLE_VARS}]
ini: ini:
- {key: private_role_vars, section: defaults} - {key: private_role_vars, section: defaults}

@ -98,19 +98,30 @@ def hash_params(params):
class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable): class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
def __init__(self, play=None, from_files=None, from_include=False, validate=True, public=True): def __init__(self, play=None, from_files=None, from_include=False, validate=True, public=None, static=True):
self._role_name = None self._role_name = None
self._role_path = None self._role_path = None
self._role_collection = None self._role_collection = None
self._role_params = dict() self._role_params = dict()
self._loader = None self._loader = None
self.public = public self.static = static
self.static = True
# includes (static=false) default to private, while imports (static=true) default to public
# but both can be overriden by global config if set
if public is None:
global_private, origin = C.config.get_config_value_and_origin('DEFAULT_PRIVATE_ROLE_VARS')
if origin == 'default':
self.public = static
else:
self.public = not global_private
else:
self.public = public
self._metadata = RoleMetadata() self._metadata = RoleMetadata()
self._play = play self._play = play
self._parents = [] self._parents = []
self._dependencies = [] self._dependencies = []
self._all_dependencies = None
self._task_blocks = [] self._task_blocks = []
self._handler_blocks = [] self._handler_blocks = []
self._compiled_handler_blocks = None self._compiled_handler_blocks = None
@ -167,7 +178,7 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
return self._get_hash_dict() == other._get_hash_dict() return self._get_hash_dict() == other._get_hash_dict()
@staticmethod @staticmethod
def load(role_include, play, parent_role=None, from_files=None, from_include=False, validate=True, public=True): def load(role_include, play, parent_role=None, from_files=None, from_include=False, validate=True, public=None, static=True):
if from_files is None: if from_files is None:
from_files = {} from_files = {}
try: try:
@ -175,7 +186,7 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
# for the in-flight in role cache as a sentinel that we're already trying to load # for the in-flight in role cache as a sentinel that we're already trying to load
# that role?) # that role?)
# see https://github.com/ansible/ansible/issues/61527 # see https://github.com/ansible/ansible/issues/61527
r = Role(play=play, from_files=from_files, from_include=from_include, validate=validate, public=public) r = Role(play=play, from_files=from_files, from_include=from_include, validate=validate, public=public, static=static)
r._load_role_data(role_include, parent_role=parent_role) r._load_role_data(role_include, parent_role=parent_role)
role_path = r.get_role_path() role_path = r.get_role_path()
@ -426,7 +437,7 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
deps = [] deps = []
for role_include in self._metadata.dependencies: for role_include in self._metadata.dependencies:
r = Role.load(role_include, play=self._play, parent_role=self) r = Role.load(role_include, play=self._play, parent_role=self, static=self.static)
deps.append(r) deps.append(r)
return deps return deps
@ -526,15 +537,15 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
Returns a list of all deps, built recursively from all child dependencies, Returns a list of all deps, built recursively from all child dependencies,
in the proper order in which they should be executed or evaluated. in the proper order in which they should be executed or evaluated.
''' '''
if self._all_dependencies is None:
child_deps = [] self._all_dependencies = []
for dep in self.get_direct_dependencies():
for dep in self.get_direct_dependencies(): for child_dep in dep.get_all_dependencies():
for child_dep in dep.get_all_dependencies(): self._all_dependencies.append(child_dep)
child_deps.append(child_dep) self._all_dependencies.append(dep)
child_deps.append(dep)
return child_deps return self._all_dependencies
def get_task_blocks(self): def get_task_blocks(self):
return self._task_blocks[:] return self._task_blocks[:]

@ -86,11 +86,11 @@ class IncludeRole(TaskInclude):
# build role # build role
actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=from_files, actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=from_files,
from_include=True, validate=self.rolespec_validate, public=self.public) from_include=True, validate=self.rolespec_validate, public=self.public, static=self.statically_loaded)
actual_role._metadata.allow_duplicates = self.allow_duplicates actual_role._metadata.allow_duplicates = self.allow_duplicates
if self.statically_loaded or self.public: # add role to play
myplay.roles.append(actual_role) myplay.roles.append(actual_role)
# save this for later use # save this for later use
self._role_path = actual_role._role_path self._role_path = actual_role._role_path
@ -122,10 +122,6 @@ class IncludeRole(TaskInclude):
ir = IncludeRole(block, role, task_include=task_include).load_data(data, variable_manager=variable_manager, loader=loader) ir = IncludeRole(block, role, task_include=task_include).load_data(data, variable_manager=variable_manager, loader=loader)
# dyanmic role!
if ir.action in C._ACTION_INCLUDE_ROLE:
ir.static = False
# Validate options # Validate options
my_arg_names = frozenset(ir.args.keys()) my_arg_names = frozenset(ir.args.keys())

@ -197,15 +197,11 @@ class VariableManager:
basedirs = [self._loader.get_basedir()] basedirs = [self._loader.get_basedir()]
if play: if play:
for role in play.get_roles(): # get role defaults (lowest precedence)
# role is public and for role in play.roles:
# either static or dynamic and completed if role.public:
# role is not set
# use config option as default
role_is_static_or_completed = role.static or role._completed.get(host.name, False)
if role.public and role_is_static_or_completed or \
role.public is None and not C.DEFAULT_PRIVATE_ROLE_VARS and role_is_static_or_completed:
all_vars = _combine_and_track(all_vars, role.get_default_vars(), "role '%s' defaults" % role.name) all_vars = _combine_and_track(all_vars, role.get_default_vars(), "role '%s' defaults" % role.name)
if task: if task:
# set basedirs # set basedirs
if C.PLAYBOOK_VARS_ROOT == 'all': # should be default if C.PLAYBOOK_VARS_ROOT == 'all': # should be default
@ -221,8 +217,7 @@ class VariableManager:
# (v1) made sure each task had a copy of its roles default vars # (v1) made sure each task had a copy of its roles default vars
# TODO: investigate why we need play or include_role check? # TODO: investigate why we need play or include_role check?
if task._role is not None and (play or task.action in C._ACTION_INCLUDE_ROLE): if task._role is not None and (play or task.action in C._ACTION_INCLUDE_ROLE):
all_vars = _combine_and_track(all_vars, task._role.get_default_vars(dep_chain=task.get_dep_chain()), all_vars = _combine_and_track(all_vars, task._role.get_default_vars(dep_chain=task.get_dep_chain()), "role '%s' defaults" % task._role.name)
"role '%s' defaults" % task._role.name)
if host: if host:
# THE 'all' group and the rest of groups for a host, used below # THE 'all' group and the rest of groups for a host, used below
@ -388,17 +383,9 @@ class VariableManager:
raise AnsibleParserError("Error while reading vars files - please supply a list of file names. " raise AnsibleParserError("Error while reading vars files - please supply a list of file names. "
"Got '%s' of type %s" % (vars_files, type(vars_files))) "Got '%s' of type %s" % (vars_files, type(vars_files)))
# We now merge in all exported vars from all roles in the play, # We now merge in all exported vars from all roles in the play (very high precedence)
# unless the user has disabled this for role in play.roles:
# role is public and if role.public:
# either static or dynamic and completed
# role is not set
# use config option as default
for role in play.get_roles():
role_is_static_or_completed = role.static or role._completed.get(host.name, False)
if role.public and role_is_static_or_completed or \
role.public is None and not C.DEFAULT_PRIVATE_ROLE_VARS and role_is_static_or_completed:
all_vars = _combine_and_track(all_vars, role.get_vars(include_params=False, only_exports=True), "role '%s' exported vars" % role.name) all_vars = _combine_and_track(all_vars, role.get_vars(include_params=False, only_exports=True), "role '%s' exported vars" % role.name)
# next, we merge in the vars from the role, which will specifically # next, we merge in the vars from the role, which will specifically
@ -467,9 +454,8 @@ class VariableManager:
variables['ansible_config_file'] = C.CONFIG_FILE variables['ansible_config_file'] = C.CONFIG_FILE
if play: if play:
# This is a list of all role names of all dependencies for all roles for this play # using role_cache as play.roles only has 'public' roles for vars exporting
dependency_role_names = list({d.get_name() for r in play.roles for d in r.get_all_dependencies()}) dependency_role_names = list({d.get_name() for r in play.roles for d in r.get_all_dependencies()})
# This is a list of all role names of all roles for this play
play_role_names = [r.get_name() for r in play.roles] play_role_names = [r.get_name() for r in play.roles]
# ansible_role_names includes all role names, dependent or directly referenced by the play # ansible_role_names includes all role names, dependent or directly referenced by the play
@ -481,7 +467,7 @@ class VariableManager:
# dependencies that are also explicitly named as roles are included in this list # dependencies that are also explicitly named as roles are included in this list
variables['ansible_dependent_role_names'] = dependency_role_names variables['ansible_dependent_role_names'] = dependency_role_names
# DEPRECATED: role_names should be deprecated in favor of ansible_role_names or ansible_play_role_names # TODO: data tagging!!! DEPRECATED: role_names should be deprecated in favor of ansible_ prefixed ones
variables['role_names'] = variables['ansible_play_role_names'] variables['role_names'] = variables['ansible_play_role_names']
variables['ansible_play_name'] = play.get_name() variables['ansible_play_name'] = play.get_name()

@ -0,0 +1,82 @@
# use this to debug issues
#- debug: msg={{ is_private ~ ', ' ~ is_default ~ ', ' ~ privacy|default('nope')}}
- hosts: localhost
name: test global privacy setting
gather_facts: false
roles:
- a
pre_tasks:
- name: 'test roles: privacy'
assert:
that:
- is_private and privacy is undefined or not is_private and privacy is defined
- not is_default or is_default and privacy is defined
- hosts: localhost
name: test import_role privacy
gather_facts: false
tasks:
- import_role: name=a
- name: role is private, var should be undefined
assert:
that:
- is_private and privacy is undefined or not is_private and privacy is defined
- not is_default or is_default and privacy is defined
- hosts: localhost
name: test public no always overrides global on import_role
gather_facts: false
tasks:
- import_role: name=a public=no
- name: role is private, var should be undefined
assert:
that:
- privacy is undefined
- hosts: localhost
name: test public yes always overrides global on import_role
gather_facts: false
tasks:
- import_role: name=a public=yes
- name: role is private, var should be undefined
assert:
that:
- privacy is defined
- hosts: localhost
name: test global privacy setting on includes
gather_facts: false
tasks:
- include_role: name=a
- name: test include_role privacy
assert:
that:
- not is_default and (is_private and privacy is undefined or not is_private and privacy is defined) or is_default and privacy is undefined
- hosts: localhost
name: test public yes always overrides global privacy setting on includes
gather_facts: false
tasks:
- include_role: name=a public=yes
- name: test include_role privacy
assert:
that:
- privacy is defined
- hosts: localhost
name: test public no always overrides global privacy setting on includes
gather_facts: false
tasks:
- include_role: name=a public=no
- name: test include_role privacy
assert:
that:
- privacy is undefined

@ -42,3 +42,8 @@ ansible-playbook vars_scope.yml -i ../../inventory "$@"
# ensure import_role called from include_role has the include_role in the dep chain # ensure import_role called from include_role has the include_role in the dep chain
ansible-playbook role_dep_chain.yml -i ../../inventory "$@" ansible-playbook role_dep_chain.yml -i ../../inventory "$@"
# global role privacy setting test, set to private, set to not private, default
ANSIBLE_PRIVATE_ROLE_VARS=1 ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@"
ANSIBLE_PRIVATE_ROLE_VARS=0 ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@"
ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@"

@ -0,0 +1,2 @@
is_private: "{{lookup('config', 'DEFAULT_PRIVATE_ROLE_VARS')}}"
is_default: "{{lookup('env', 'ANSIBLE_PRIVATE_ROLE_VARS') == ''}}"
Loading…
Cancel
Save