Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to expose vars/defaults with include/import_role #41330

Merged
merged 8 commits into from Jul 15, 2018
9 changes: 9 additions & 0 deletions docs/docsite/rst/porting_guides/porting_guide_2.7.rst
Expand Up @@ -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
==========

Expand Down
3 changes: 3 additions & 0 deletions lib/ansible/modules/utilities/logic/import_role.py
Expand Up @@ -56,6 +56,9 @@
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 = """
Expand Down
7 changes: 7 additions & 0 deletions lib/ansible/modules/utilities/logic/include_role.py
Expand Up @@ -55,8 +55,15 @@
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
Expand Down
4 changes: 4 additions & 0 deletions lib/ansible/playbook/play.py
Expand Up @@ -233,6 +233,10 @@ def _compile_roles(self):

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
Expand Down
20 changes: 16 additions & 4 deletions lib/ansible/playbook/role/__init__.py
Expand Up @@ -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()
Expand All @@ -108,6 +108,7 @@ def __init__(self, play=None, from_files=None):
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()
Expand All @@ -117,6 +118,9 @@ def __init__(self, play=None, from_files=None):
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):
Expand All @@ -126,7 +130,7 @@ def get_name(self):
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 = {}
Expand All @@ -153,7 +157,7 @@ def load(role_include, play, parent_role=None, from_files=None):
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:
Expand Down Expand Up @@ -359,7 +363,15 @@ def get_task_blocks(self):
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:
Expand Down
16 changes: 12 additions & 4 deletions lib/ansible/playbook/role_include.py
Expand Up @@ -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

# =================================================================================
Expand All @@ -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):

Expand All @@ -81,9 +82,13 @@ def get_block_list(self, play=None, variable_manager=None, loader=None):
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

Expand Down Expand Up @@ -119,9 +124,12 @@ def load(data, block=None, role=None, task_include=None, variable_manager=None,
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'
)

Expand Down
@@ -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
4 changes: 3 additions & 1 deletion test/integration/targets/include_import/runme.sh
Expand Up @@ -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 "$@"