mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2025-10-26 13:56:09 -07:00 
			
		
		
		
	Support 'apply' to apply attributes to included tasks - Impl 1 (#39236)
* Support 'apply' to apply attributes to included tasks * Cannot validate args for task_include * Only allow apply on include_ * Re-enable arg validation, but only for include_tasks and import_tasks * s/task/ir/ * Add tests for include_ apply * Include context with AnsibleParserError * Add docs for apply * version_added * Add free-form documentation back * Add example of free-form with apply
This commit is contained in:
		
					parent
					
						
							
								961484e00d
							
						
					
				
			
			
				commit
				
					
						da4ff18406
					
				
			
		
					 9 changed files with 175 additions and 4 deletions
				
			
		|  | @ -26,6 +26,10 @@ description: | ||||||
|   - This module is also supported for Windows targets. |   - This module is also supported for Windows targets. | ||||||
| version_added: "2.2" | version_added: "2.2" | ||||||
| options: | options: | ||||||
|  |   apply: | ||||||
|  |     description: | ||||||
|  |       - Accepts a hash of task keywords (e.g. C(tags), C(become)) that will be applied to the tasks within the include. | ||||||
|  |     version_added: '2.7' | ||||||
|   name: |   name: | ||||||
|     description: |     description: | ||||||
|       - The name of the role to be executed. |       - The name of the role to be executed. | ||||||
|  | @ -89,6 +93,15 @@ EXAMPLES = """ | ||||||
|   include_role: |   include_role: | ||||||
|     name: myrole |     name: myrole | ||||||
|   when: not idontwanttorun |   when: not idontwanttorun | ||||||
|  | 
 | ||||||
|  | - name: Apply tags to tasks within included file | ||||||
|  |   include_role: | ||||||
|  |     name: install | ||||||
|  |     apply: | ||||||
|  |       tags: | ||||||
|  |         - install | ||||||
|  |   tags: | ||||||
|  |     - always | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| RETURN = """ | RETURN = """ | ||||||
|  |  | ||||||
|  | @ -23,10 +23,20 @@ description: | ||||||
|   - Includes a file with a list of tasks to be executed in the current playbook. |   - Includes a file with a list of tasks to be executed in the current playbook. | ||||||
| version_added: "2.4" | version_added: "2.4" | ||||||
| options: | options: | ||||||
|   free-form: |   file: | ||||||
|     description: |     description: | ||||||
|       - The name of the imported file is specified directly without any other option. |       - The name of the imported file is specified directly without any other option. | ||||||
|       - Unlike M(import_tasks), most keywords, including loops and conditionals, apply to this statement. |       - Unlike M(import_tasks), most keywords, including loops and conditionals, apply to this statement. | ||||||
|  |     version_added: '2.7' | ||||||
|  |   apply: | ||||||
|  |     description: | ||||||
|  |       - Accepts a hash of task keywords (e.g. C(tags), C(become)) that will be applied to the tasks within the include. | ||||||
|  |     version_added: '2.7' | ||||||
|  |   free-form: | ||||||
|  |     description: | ||||||
|  |       - | | ||||||
|  |         Supplying a file name via free-form C(- include_tasks: file.yml) of a file to be included is the equivalent | ||||||
|  |         of specifying an argument of I(file). | ||||||
| notes: | notes: | ||||||
|   - This is a core feature of the Ansible, rather than a module, and cannot be overridden like a module. |   - This is a core feature of the Ansible, rather than a module, and cannot be overridden like a module. | ||||||
| ''' | ''' | ||||||
|  | @ -51,6 +61,24 @@ EXAMPLES = """ | ||||||
|     - name: Include task list in play only if the condition is true |     - name: Include task list in play only if the condition is true | ||||||
|       include_tasks: "{{ hostvar }}.yaml" |       include_tasks: "{{ hostvar }}.yaml" | ||||||
|       when: hostvar is defined |       when: hostvar is defined | ||||||
|  | 
 | ||||||
|  | - name: Apply tags to tasks within included file | ||||||
|  |   include_tasks: | ||||||
|  |     file: install.yml | ||||||
|  |     apply: | ||||||
|  |       tags: | ||||||
|  |         - install | ||||||
|  |   tags: | ||||||
|  |     - always | ||||||
|  | 
 | ||||||
|  | - name: Apply tags to tasks within included file when using free-form | ||||||
|  |   include_tasks: install.yml | ||||||
|  |   args: | ||||||
|  |     apply: | ||||||
|  |       tags: | ||||||
|  |         - install | ||||||
|  |   tags: | ||||||
|  |     - always | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| RETURN = """ | RETURN = """ | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ from os.path import basename | ||||||
| 
 | 
 | ||||||
| from ansible.errors import AnsibleParserError | from ansible.errors import AnsibleParserError | ||||||
| from ansible.playbook.attribute import FieldAttribute | from ansible.playbook.attribute import FieldAttribute | ||||||
|  | from ansible.playbook.block import Block | ||||||
| from ansible.playbook.task_include import TaskInclude | from ansible.playbook.task_include import TaskInclude | ||||||
| from ansible.playbook.role import Role | from ansible.playbook.role import Role | ||||||
| from ansible.playbook.role.include import RoleInclude | from ansible.playbook.role.include import RoleInclude | ||||||
|  | @ -45,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 = ('private', 'allow_duplicates')  # assigned to matching property |     OTHER_ARGS = ('apply', 'private', '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 | ||||||
| 
 | 
 | ||||||
|     # ================================================================================= |     # ================================================================================= | ||||||
|  | @ -134,6 +135,23 @@ class IncludeRole(TaskInclude): | ||||||
|             from_key = key.replace('_from', '') |             from_key = key.replace('_from', '') | ||||||
|             ir._from_files[from_key] = basename(ir.args.get(key)) |             ir._from_files[from_key] = basename(ir.args.get(key)) | ||||||
| 
 | 
 | ||||||
|  |         apply_attrs = ir.args.pop('apply', {}) | ||||||
|  |         if apply_attrs and ir.action != 'include_role': | ||||||
|  |             raise AnsibleParserError('Invalid options for %s: apply' % ir.action, obj=data) | ||||||
|  |         elif apply_attrs: | ||||||
|  |             apply_attrs['block'] = [] | ||||||
|  |             p_block = Block.load( | ||||||
|  |                 apply_attrs, | ||||||
|  |                 play=block._play, | ||||||
|  |                 parent_block=block, | ||||||
|  |                 role=role, | ||||||
|  |                 task_include=task_include, | ||||||
|  |                 use_handlers=block._use_handlers, | ||||||
|  |                 variable_manager=variable_manager, | ||||||
|  |                 loader=loader, | ||||||
|  |             ) | ||||||
|  |             ir._parent = p_block | ||||||
|  | 
 | ||||||
|         # manual list as otherwise the options would set other task parameters we don't want. |         # manual list as otherwise the options would set other task parameters we don't want. | ||||||
|         for option in my_arg_names.intersection(IncludeRole.OTHER_ARGS): |         for option in my_arg_names.intersection(IncludeRole.OTHER_ARGS): | ||||||
|             setattr(ir, option, ir.args.get(option)) |             setattr(ir, option, ir.args.get(option)) | ||||||
|  |  | ||||||
|  | @ -19,7 +19,9 @@ | ||||||
| from __future__ import (absolute_import, division, print_function) | from __future__ import (absolute_import, division, print_function) | ||||||
| __metaclass__ = type | __metaclass__ = type | ||||||
| 
 | 
 | ||||||
|  | from ansible.errors import AnsibleParserError | ||||||
| from ansible.playbook.attribute import FieldAttribute | from ansible.playbook.attribute import FieldAttribute | ||||||
|  | from ansible.playbook.block import Block | ||||||
| from ansible.playbook.task import Task | from ansible.playbook.task import Task | ||||||
| 
 | 
 | ||||||
| try: | try: | ||||||
|  | @ -38,6 +40,10 @@ class TaskInclude(Task): | ||||||
|     circumstances related to the `- include: ...` task. |     circumstances related to the `- include: ...` task. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     BASE = frozenset(('file', '_raw_params'))  # directly assigned | ||||||
|  |     OTHER_ARGS = frozenset(('apply',))  # assigned to matching property | ||||||
|  |     VALID_ARGS = BASE.union(OTHER_ARGS)  # all valid args | ||||||
|  | 
 | ||||||
|     # ================================================================================= |     # ================================================================================= | ||||||
|     # ATTRIBUTES |     # ATTRIBUTES | ||||||
| 
 | 
 | ||||||
|  | @ -49,8 +55,38 @@ class TaskInclude(Task): | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): |     def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): | ||||||
|         t = TaskInclude(block=block, role=role, task_include=task_include) |         ti = TaskInclude(block=block, role=role, task_include=task_include) | ||||||
|         return t.load_data(data, variable_manager=variable_manager, loader=loader) |         task = ti.load_data(data, variable_manager=variable_manager, loader=loader) | ||||||
|  | 
 | ||||||
|  |         # Validate options | ||||||
|  |         my_arg_names = frozenset(task.args.keys()) | ||||||
|  | 
 | ||||||
|  |         # validate bad args, otherwise we silently ignore | ||||||
|  |         bad_opts = my_arg_names.difference(TaskInclude.VALID_ARGS) | ||||||
|  |         if bad_opts and task.action in ('include_tasks', 'import_tasks'): | ||||||
|  |             raise AnsibleParserError('Invalid options for %s: %s' % (task.action, ','.join(list(bad_opts))), obj=data) | ||||||
|  | 
 | ||||||
|  |         if not task.args.get('_raw_params'): | ||||||
|  |             task.args['_raw_params'] = task.args.pop('file') | ||||||
|  | 
 | ||||||
|  |         apply_attrs = task.args.pop('apply', {}) | ||||||
|  |         if apply_attrs and task.action != 'include_tasks': | ||||||
|  |             raise AnsibleParserError('Invalid options for %s: apply' % task.action, obj=data) | ||||||
|  |         elif apply_attrs: | ||||||
|  |             apply_attrs['block'] = [] | ||||||
|  |             p_block = Block.load( | ||||||
|  |                 apply_attrs, | ||||||
|  |                 play=block._play, | ||||||
|  |                 parent_block=block, | ||||||
|  |                 role=role, | ||||||
|  |                 task_include=task_include, | ||||||
|  |                 use_handlers=block._use_handlers, | ||||||
|  |                 variable_manager=variable_manager, | ||||||
|  |                 loader=loader, | ||||||
|  |             ) | ||||||
|  |             task._parent = p_block | ||||||
|  | 
 | ||||||
|  |         return task | ||||||
| 
 | 
 | ||||||
|     def copy(self, exclude_parent=False, exclude_tasks=False): |     def copy(self, exclude_parent=False, exclude_tasks=False): | ||||||
|         new_me = super(TaskInclude, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) |         new_me = super(TaskInclude, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | --- | ||||||
|  | - hosts: testhost | ||||||
|  |   gather_facts: false | ||||||
|  |   tasks: | ||||||
|  |     - import_tasks: | ||||||
|  |         file: import_tasks.yml | ||||||
|  |         apply: | ||||||
|  |           tags: | ||||||
|  |             - foo | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | 
 | ||||||
|  |     - assert: | ||||||
|  |         that: | ||||||
|  |           - include_tasks_result is defined | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | 
 | ||||||
|  |     - import_role: | ||||||
|  |         name: import_role | ||||||
|  |         apply: | ||||||
|  |           tags: | ||||||
|  |             - foo | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | 
 | ||||||
|  |     - assert: | ||||||
|  |         that: | ||||||
|  |           - include_role_result is defined | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | --- | ||||||
|  | - hosts: testhost | ||||||
|  |   gather_facts: false | ||||||
|  |   tasks: | ||||||
|  |     - include_tasks: | ||||||
|  |         file: include_tasks.yml | ||||||
|  |         apply: | ||||||
|  |           tags: | ||||||
|  |             - foo | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | 
 | ||||||
|  |     - assert: | ||||||
|  |         that: | ||||||
|  |           - include_tasks_result is defined | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | 
 | ||||||
|  |     - include_role: | ||||||
|  |         name: include_role | ||||||
|  |         apply: | ||||||
|  |           tags: | ||||||
|  |             - foo | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | 
 | ||||||
|  |     - assert: | ||||||
|  |         that: | ||||||
|  |           - include_role_result is defined | ||||||
|  |       tags: | ||||||
|  |         - always | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | - set_fact: | ||||||
|  |     include_tasks_result: true | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | - set_fact: | ||||||
|  |     include_role_result: true | ||||||
|  | @ -67,3 +67,13 @@ ANSIBLE_STRATEGY='free' ansible-playbook undefined_var/playbook.yml  -i ../../in | ||||||
| # Include path inheritance using host var for include file path | # Include path inheritance using host var for include file path | ||||||
| ANSIBLE_STRATEGY='linear' ansible-playbook include_path_inheritance/playbook.yml  -i ../../inventory "$@" | ANSIBLE_STRATEGY='linear' ansible-playbook include_path_inheritance/playbook.yml  -i ../../inventory "$@" | ||||||
| ANSIBLE_STRATEGY='free' ansible-playbook include_path_inheritance/playbook.yml  -i ../../inventory "$@" | ANSIBLE_STRATEGY='free' ansible-playbook include_path_inheritance/playbook.yml  -i ../../inventory "$@" | ||||||
|  | 
 | ||||||
|  | # include_ + apply (explicit inheritance) | ||||||
|  | ANSIBLE_STRATEGY='linear' ansible-playbook apply/include_apply.yml -i ../../inventory "$@" --tags foo | ||||||
|  | set +e | ||||||
|  | OUT=$(ANSIBLE_STRATEGY='linear' ansible-playbook apply/import_apply.yml -i ../../inventory "$@" --tags foo 2>&1 | grep 'ERROR! Invalid options for import_tasks: apply') | ||||||
|  | set -e | ||||||
|  | if [[ -z "$OUT" ]]; then | ||||||
|  |     echo "apply on import_tasks did not cause error" | ||||||
|  |     exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue