mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Add feature to expose vars/defaults with include/import_role (#41330)
* First pass at making 'private' work on include_role, imports are always public * Prevent dupe task execution and overwriting handlers * New functionality will use public instead of deprecated private * Add tests for public exposure * Validate vars before import/include to ensure they don't expose too early * Add porting guide docs about public argument and change to import_role * Add additional docs about public and vars exposure to module docs * Insert role handlers at parse time, exposing them globally
This commit is contained in:
parent
5864871fc1
commit
27b4d7ed31
23 changed files with 145 additions and 9 deletions
|
@ -26,6 +26,15 @@ Before Ansible 2.7, when loading a role, the variables defined in the role's ``v
|
||||||
|
|
||||||
In Ansible 2.7, role ``vars`` and ``defaults`` are now parsed before ``tasks/main.yml``. This can cause a change in behavior if the same variable is defined at the play level and the role level with different values, and leveraged in ``import_tasks`` or ``import_role`` to define the role or file to import.
|
In Ansible 2.7, role ``vars`` and ``defaults`` are now parsed before ``tasks/main.yml``. This can cause a change in behavior if the same variable is defined at the play level and the role level with different values, and leveraged in ``import_tasks`` or ``import_role`` to define the role or file to import.
|
||||||
|
|
||||||
|
include_role and import_role variable exposure
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
In Ansible 2.7 a new module argument named ``public`` was added to the ``include_role`` module that dictates whether or not the role's ``defaults`` and ``vars`` will be exposed outside of the role, allowing those variables to be used by later tasks. This value defaults to ``public: False``, matching current behavior.
|
||||||
|
|
||||||
|
``import_role`` does not support the ``public`` argument, and will unconditionally expose the role's ``defaults`` and ``vars`` to the rest of the playbook. This functinality brings ``import_role`` into closer alignment with roles listed within the ``roles`` header in a play.
|
||||||
|
|
||||||
|
There is an important difference in the way that ``include_role`` (dynamic) will expose the role's variables, as opposed to ``import_role`` (static). ``import_role`` is a pre-processor, and the ``defaults`` and ``vars`` are evaluated at playbook parsing, making the variables available to tasks and roles listed at any point in the play. ``include_role`` is a conditional task, and the ``defaults`` and ``vars`` are evaluated at execution time, making the variables available to tasks and roles listed *after* the ``include_role`` task.
|
||||||
|
|
||||||
Deprecated
|
Deprecated
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,9 @@ options:
|
||||||
default: 'no'
|
default: 'no'
|
||||||
notes:
|
notes:
|
||||||
- Handlers are made available to the whole play.
|
- Handlers are made available to the whole play.
|
||||||
|
- Variables defined in C(vars) and C(default) for the role are exposed at playbook parsing time. Due to this,
|
||||||
|
these variables will be accessible to roles and tasks executed before the the location of the C(import_role) task.
|
||||||
|
- Unlike C(include_role) variable exposure is not configurable, and will always be exposed.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
|
|
|
@ -55,8 +55,15 @@ options:
|
||||||
description:
|
description:
|
||||||
- This option is a no op, and the functionality described in previous versions was not implemented. This
|
- This option is a no op, and the functionality described in previous versions was not implemented. This
|
||||||
option will be removed in Ansible v2.8.
|
option will be removed in Ansible v2.8.
|
||||||
|
public:
|
||||||
|
description:
|
||||||
|
- This option dictates whether the role's C(vars) and C(defaults) are exposed to the playbook. If set to C(yes)
|
||||||
|
the variables will be available to tasks following the C(include_role) task. This functionality differs from
|
||||||
|
standard variable exposure for roles listed under the C(roles) header or C(import_role) as they are exposed at
|
||||||
|
playbook parsing time, and available to earlier roles and tasks as well.
|
||||||
type: bool
|
type: bool
|
||||||
default: 'no'
|
default: 'no'
|
||||||
|
version_added: '2.7'
|
||||||
notes:
|
notes:
|
||||||
- Handlers are made available to the whole play.
|
- Handlers are made available to the whole play.
|
||||||
- Before Ansible 2.4, as with C(include), this task could be static or dynamic, If static, it implied that it won't
|
- Before Ansible 2.4, as with C(include), this task could be static or dynamic, If static, it implied that it won't
|
||||||
|
|
|
@ -233,6 +233,10 @@ class Play(Base, Taggable, Become):
|
||||||
|
|
||||||
if len(self.roles) > 0:
|
if len(self.roles) > 0:
|
||||||
for r in self.roles:
|
for r in self.roles:
|
||||||
|
# Don't insert tasks from ``import/include_role``, preventing
|
||||||
|
# duplicate execution at the wrong time
|
||||||
|
if r.from_include:
|
||||||
|
continue
|
||||||
block_list.extend(r.compile(play=self))
|
block_list.extend(r.compile(play=self))
|
||||||
|
|
||||||
return block_list
|
return block_list
|
||||||
|
|
|
@ -96,7 +96,7 @@ class Role(Base, Become, Conditional, Taggable):
|
||||||
_delegate_to = FieldAttribute(isa='string')
|
_delegate_to = FieldAttribute(isa='string')
|
||||||
_delegate_facts = FieldAttribute(isa='bool', default=False)
|
_delegate_facts = FieldAttribute(isa='bool', default=False)
|
||||||
|
|
||||||
def __init__(self, play=None, from_files=None):
|
def __init__(self, play=None, from_files=None, from_include=False):
|
||||||
self._role_name = None
|
self._role_name = None
|
||||||
self._role_path = None
|
self._role_path = None
|
||||||
self._role_params = dict()
|
self._role_params = dict()
|
||||||
|
@ -108,6 +108,7 @@ class Role(Base, Become, Conditional, Taggable):
|
||||||
self._dependencies = []
|
self._dependencies = []
|
||||||
self._task_blocks = []
|
self._task_blocks = []
|
||||||
self._handler_blocks = []
|
self._handler_blocks = []
|
||||||
|
self._compiled_handler_blocks = None
|
||||||
self._default_vars = dict()
|
self._default_vars = dict()
|
||||||
self._role_vars = dict()
|
self._role_vars = dict()
|
||||||
self._had_task_run = dict()
|
self._had_task_run = dict()
|
||||||
|
@ -117,6 +118,9 @@ class Role(Base, Become, Conditional, Taggable):
|
||||||
from_files = {}
|
from_files = {}
|
||||||
self._from_files = from_files
|
self._from_files = from_files
|
||||||
|
|
||||||
|
# Indicates whether this role was included via include/import_role
|
||||||
|
self.from_include = from_include
|
||||||
|
|
||||||
super(Role, self).__init__()
|
super(Role, self).__init__()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -126,7 +130,7 @@ class Role(Base, Become, Conditional, Taggable):
|
||||||
return self._role_name
|
return self._role_name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(role_include, play, parent_role=None, from_files=None):
|
def load(role_include, play, parent_role=None, from_files=None, from_include=False):
|
||||||
|
|
||||||
if from_files is None:
|
if from_files is None:
|
||||||
from_files = {}
|
from_files = {}
|
||||||
|
@ -153,7 +157,7 @@ class Role(Base, Become, Conditional, Taggable):
|
||||||
role_obj.add_parent(parent_role)
|
role_obj.add_parent(parent_role)
|
||||||
return role_obj
|
return role_obj
|
||||||
|
|
||||||
r = Role(play=play, from_files=from_files)
|
r = Role(play=play, from_files=from_files, from_include=from_include)
|
||||||
r._load_role_data(role_include, parent_role=parent_role)
|
r._load_role_data(role_include, parent_role=parent_role)
|
||||||
|
|
||||||
if role_include.role not in play.ROLE_CACHE:
|
if role_include.role not in play.ROLE_CACHE:
|
||||||
|
@ -359,7 +363,15 @@ class Role(Base, Become, Conditional, Taggable):
|
||||||
return self._task_blocks[:]
|
return self._task_blocks[:]
|
||||||
|
|
||||||
def get_handler_blocks(self, play, dep_chain=None):
|
def get_handler_blocks(self, play, dep_chain=None):
|
||||||
block_list = []
|
# Do not recreate this list each time ``get_handler_blocks`` is called.
|
||||||
|
# Cache the results so that we don't potentially overwrite with copied duplicates
|
||||||
|
#
|
||||||
|
# ``get_handler_blocks`` may be called when handling ``import_role`` during parsing
|
||||||
|
# as well as with ``Play.compile_roles_handlers`` from ``TaskExecutor``
|
||||||
|
if self._compiled_handler_blocks:
|
||||||
|
return self._compiled_handler_blocks
|
||||||
|
|
||||||
|
self._compiled_handler_blocks = block_list = []
|
||||||
|
|
||||||
# update the dependency chain here
|
# update the dependency chain here
|
||||||
if dep_chain is None:
|
if dep_chain is None:
|
||||||
|
|
|
@ -46,7 +46,7 @@ class IncludeRole(TaskInclude):
|
||||||
|
|
||||||
BASE = ('name', 'role') # directly assigned
|
BASE = ('name', 'role') # directly assigned
|
||||||
FROM_ARGS = ('tasks_from', 'vars_from', 'defaults_from') # used to populate from dict in role
|
FROM_ARGS = ('tasks_from', 'vars_from', 'defaults_from') # used to populate from dict in role
|
||||||
OTHER_ARGS = ('apply', 'private', 'allow_duplicates') # assigned to matching property
|
OTHER_ARGS = ('apply', 'private', 'public', 'allow_duplicates') # assigned to matching property
|
||||||
VALID_ARGS = tuple(frozenset(BASE + FROM_ARGS + OTHER_ARGS)) # all valid args
|
VALID_ARGS = tuple(frozenset(BASE + FROM_ARGS + OTHER_ARGS)) # all valid args
|
||||||
|
|
||||||
# =================================================================================
|
# =================================================================================
|
||||||
|
@ -55,6 +55,7 @@ class IncludeRole(TaskInclude):
|
||||||
# private as this is a 'module options' vs a task property
|
# private as this is a 'module options' vs a task property
|
||||||
_allow_duplicates = FieldAttribute(isa='bool', default=True, private=True)
|
_allow_duplicates = FieldAttribute(isa='bool', default=True, private=True)
|
||||||
_private = FieldAttribute(isa='bool', default=None, private=True)
|
_private = FieldAttribute(isa='bool', default=None, private=True)
|
||||||
|
_public = FieldAttribute(isa='bool', default=False, private=True)
|
||||||
|
|
||||||
def __init__(self, block=None, role=None, task_include=None):
|
def __init__(self, block=None, role=None, task_include=None):
|
||||||
|
|
||||||
|
@ -81,9 +82,13 @@ class IncludeRole(TaskInclude):
|
||||||
ri.vars.update(self.vars)
|
ri.vars.update(self.vars)
|
||||||
|
|
||||||
# build role
|
# build role
|
||||||
actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=self._from_files)
|
actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=self._from_files,
|
||||||
|
from_include=True)
|
||||||
actual_role._metadata.allow_duplicates = self.allow_duplicates
|
actual_role._metadata.allow_duplicates = self.allow_duplicates
|
||||||
|
|
||||||
|
if self.statically_loaded or self.public:
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -119,9 +124,12 @@ class IncludeRole(TaskInclude):
|
||||||
if ir._role_name is None:
|
if ir._role_name is None:
|
||||||
raise AnsibleParserError("'name' is a required field for %s." % ir.action, obj=data)
|
raise AnsibleParserError("'name' is a required field for %s." % ir.action, obj=data)
|
||||||
|
|
||||||
if ir.private is not None:
|
if 'public' in ir.args and ir.action != 'include_role':
|
||||||
|
raise AnsibleParserError('Invalid options for %s: private' % ir.action, obj=data)
|
||||||
|
|
||||||
|
if 'private' in ir.args:
|
||||||
display.deprecated(
|
display.deprecated(
|
||||||
msg='Supplying "private" for "include_role" is a no op, and is deprecated',
|
msg='Supplying "private" for "%s" is a no op, and is deprecated' % ir.action,
|
||||||
version='2.8'
|
version='2.8'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
- hosts: testhost
|
||||||
|
gather_facts: false
|
||||||
|
roles:
|
||||||
|
- regular
|
||||||
|
tasks:
|
||||||
|
- debug:
|
||||||
|
msg: start tasks
|
||||||
|
|
||||||
|
- name: Static imports should expose vars at parse time, not at execution time
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- static_defaults_var == 'static_defaults'
|
||||||
|
- static_vars_var == 'static_vars'
|
||||||
|
- import_role:
|
||||||
|
name: static
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- static_tasks_var == 'static_tasks'
|
||||||
|
- static_defaults_var == 'static_defaults'
|
||||||
|
- static_vars_var == 'static_vars'
|
||||||
|
|
||||||
|
- include_role:
|
||||||
|
name: dynamic_private
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- private_tasks_var == 'private_tasks'
|
||||||
|
- private_defaults_var is undefined
|
||||||
|
- private_vars_var is undefined
|
||||||
|
|
||||||
|
- name: Dynamic include should not expose vars until execution time
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- dynamic_tasks_var is undefined
|
||||||
|
- dynamic_defaults_var is undefined
|
||||||
|
- dynamic_vars_var is undefined
|
||||||
|
- include_role:
|
||||||
|
name: dynamic
|
||||||
|
public: true
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- dynamic_tasks_var == 'dynamic_tasks'
|
||||||
|
- dynamic_defaults_var == 'dynamic_defaults'
|
||||||
|
- dynamic_vars_var == 'dynamic_vars'
|
||||||
|
|
||||||
|
- include_role:
|
||||||
|
name: from
|
||||||
|
public: true
|
||||||
|
tasks_from: from.yml
|
||||||
|
vars_from: from.yml
|
||||||
|
defaults_from: from.yml
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- from_tasks_var == 'from_tasks'
|
||||||
|
- from_defaults_var == 'from_defaults'
|
||||||
|
- from_vars_var == 'from_vars'
|
|
@ -0,0 +1 @@
|
||||||
|
dynamic_defaults_var: dynamic_defaults
|
|
@ -0,0 +1,5 @@
|
||||||
|
- debug:
|
||||||
|
msg: dynamic
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
dynamic_tasks_var: dynamic_tasks
|
|
@ -0,0 +1 @@
|
||||||
|
dynamic_vars_var: dynamic_vars
|
|
@ -0,0 +1 @@
|
||||||
|
private_defaults_var: private_defaults
|
|
@ -0,0 +1,5 @@
|
||||||
|
- debug:
|
||||||
|
msg: private
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
private_tasks_var: private_tasks
|
|
@ -0,0 +1 @@
|
||||||
|
private_vars_var: private_vars
|
|
@ -0,0 +1 @@
|
||||||
|
from_defaults_var: from_defaults
|
|
@ -0,0 +1,5 @@
|
||||||
|
- debug:
|
||||||
|
msg: from
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
from_tasks_var: from_tasks
|
|
@ -0,0 +1 @@
|
||||||
|
from_vars_var: from_vars
|
|
@ -0,0 +1 @@
|
||||||
|
regular_defaults_var: regular_defaults
|
|
@ -0,0 +1,5 @@
|
||||||
|
- debug:
|
||||||
|
msg: regular
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
regular_tasks_var: regular_tasks
|
|
@ -0,0 +1 @@
|
||||||
|
regular_vars_var: regular_vars
|
|
@ -0,0 +1 @@
|
||||||
|
static_defaults_var: static_defaults
|
|
@ -0,0 +1,5 @@
|
||||||
|
- debug:
|
||||||
|
msg: static
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
static_tasks_var: static_tasks
|
|
@ -0,0 +1 @@
|
||||||
|
static_vars_var: static_vars
|
|
@ -78,4 +78,6 @@ fi
|
||||||
ANSIBLE_STRATEGY='linear' ansible-playbook tasks/test_include_dupe_loop.yml -i ../../inventory "$@" | tee test_include_dupe_loop.out
|
ANSIBLE_STRATEGY='linear' ansible-playbook tasks/test_include_dupe_loop.yml -i ../../inventory "$@" | tee test_include_dupe_loop.out
|
||||||
test "$(grep -c '"item=foo"' test_include_dupe_loop.out)" = 3
|
test "$(grep -c '"item=foo"' test_include_dupe_loop.out)" = 3
|
||||||
ANSIBLE_STRATEGY='free' ansible-playbook tasks/test_include_dupe_loop.yml -i ../../inventory "$@" | tee test_include_dupe_loop.out
|
ANSIBLE_STRATEGY='free' ansible-playbook tasks/test_include_dupe_loop.yml -i ../../inventory "$@" | tee test_include_dupe_loop.out
|
||||||
test "$(grep -c '"item=foo"' test_include_dupe_loop.out)" = 3
|
test "$(grep -c '"item=foo"' test_include_dupe_loop.out)" = 3
|
||||||
|
|
||||||
|
ansible-playbook public_exposure/playbook.yml -i ../../inventory "$@"
|
||||||
|
|
Loading…
Reference in a new issue