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:
Matt Martz 2018-07-15 09:59:30 -05:00 committed by GitHub
parent 5864871fc1
commit 27b4d7ed31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 145 additions and 9 deletions

View file

@ -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.
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
==========

View file

@ -56,6 +56,9 @@ options:
default: 'no'
notes:
- 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 = """

View file

@ -55,8 +55,15 @@ options:
description:
- 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.
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
default: 'no'
version_added: '2.7'
notes:
- 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

View file

@ -233,6 +233,10 @@ class Play(Base, Taggable, Become):
if len(self.roles) > 0:
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))
return block_list

View file

@ -96,7 +96,7 @@ class Role(Base, Become, Conditional, Taggable):
_delegate_to = FieldAttribute(isa='string')
_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_path = None
self._role_params = dict()
@ -108,6 +108,7 @@ class Role(Base, Become, Conditional, Taggable):
self._dependencies = []
self._task_blocks = []
self._handler_blocks = []
self._compiled_handler_blocks = None
self._default_vars = dict()
self._role_vars = dict()
self._had_task_run = dict()
@ -117,6 +118,9 @@ class Role(Base, Become, Conditional, Taggable):
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__()
def __repr__(self):
@ -126,7 +130,7 @@ class Role(Base, Become, Conditional, Taggable):
return self._role_name
@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:
from_files = {}
@ -153,7 +157,7 @@ class Role(Base, Become, Conditional, Taggable):
role_obj.add_parent(parent_role)
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)
if role_include.role not in play.ROLE_CACHE:
@ -359,7 +363,15 @@ class Role(Base, Become, Conditional, Taggable):
return self._task_blocks[:]
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
if dep_chain is None:

View file

@ -46,7 +46,7 @@ class IncludeRole(TaskInclude):
BASE = ('name', 'role') # directly assigned
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
# =================================================================================
@ -55,6 +55,7 @@ class IncludeRole(TaskInclude):
# private as this is a 'module options' vs a task property
_allow_duplicates = FieldAttribute(isa='bool', default=True, 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):
@ -81,9 +82,13 @@ class IncludeRole(TaskInclude):
ri.vars.update(self.vars)
# 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
if self.statically_loaded or self.public:
myplay.roles.append(actual_role)
# save this for later use
self._role_path = actual_role._role_path
@ -119,9 +124,12 @@ class IncludeRole(TaskInclude):
if ir._role_name is None:
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(
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'
)

View file

@ -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'

View file

@ -0,0 +1 @@
dynamic_defaults_var: dynamic_defaults

View file

@ -0,0 +1,5 @@
- debug:
msg: dynamic
- set_fact:
dynamic_tasks_var: dynamic_tasks

View file

@ -0,0 +1 @@
dynamic_vars_var: dynamic_vars

View file

@ -0,0 +1 @@
private_defaults_var: private_defaults

View file

@ -0,0 +1,5 @@
- debug:
msg: private
- set_fact:
private_tasks_var: private_tasks

View file

@ -0,0 +1 @@
private_vars_var: private_vars

View file

@ -0,0 +1 @@
from_defaults_var: from_defaults

View file

@ -0,0 +1,5 @@
- debug:
msg: from
- set_fact:
from_tasks_var: from_tasks

View file

@ -0,0 +1 @@
from_vars_var: from_vars

View file

@ -0,0 +1 @@
regular_defaults_var: regular_defaults

View file

@ -0,0 +1,5 @@
- debug:
msg: regular
- set_fact:
regular_tasks_var: regular_tasks

View file

@ -0,0 +1 @@
regular_vars_var: regular_vars

View file

@ -0,0 +1 @@
static_defaults_var: static_defaults

View file

@ -0,0 +1,5 @@
- debug:
msg: static
- set_fact:
static_tasks_var: static_tasks

View file

@ -0,0 +1 @@
static_vars_var: static_vars

View file

@ -78,4 +78,6 @@ fi
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
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 "$@"