Make Ansible variable freezing more efficient

We currently iterate over every job/host/etc variable in the freeze
playbook.  The reason is because if any value in any variable is
Undefined according to jinja, the Ansible combine filter throws
an error.  What we want to do in Zuul is merge any variable we can,
but if any is undefined, we skip it.  Thus, the process of combining
the variables one at a time in a task and ignoring errors.

This process can be slow, especially if we have start with a large
amount of data in one of the early variables.  The combine filter
needs to reprocess the large data repeatedly for each additional
variable.

To improve the process, we create a new action plugin, "zuul_freeze"
which takes a list of variables we want to freeze, then templates
them one at a time and stores the result in a cacheable fact.  This
is the essence of what we were trying to accomplish with the combine
filter.

Change-Id: Ie41f404762daa1b1a5ae47f6ec1aa1954ad36a39
This commit is contained in:
James E. Blair
2023-08-22 16:16:58 -07:00
parent 1d07a097ee
commit e55748ba69
5 changed files with 61 additions and 13 deletions

View File

@@ -9,6 +9,9 @@
vars:
latesub: "{{ latefact | default('undefined') }}"
jobvar: "{{ base_secret.secret | default('undefined') }}"
# Make sure we have a top level variable that is undefined to
# ensure that it doesn't cause all values to be omitted.
undefvar: "{{ undefinedvar }}"
run: playbooks/testjob-run.yaml
- job:

View File

@@ -0,0 +1 @@
../../base/action/zuul_freeze.py

View File

@@ -0,0 +1 @@
../../base/action/zuul_freeze.py

View File

@@ -0,0 +1,53 @@
# Copyright (C) 2023 Acme Gating, LLC
#
# This module is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this software. If not, see <http://www.gnu.org/licenses/>.
from ansible.plugins.action import ActionBase
from ansible.template import recursive_check_defined
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
"""This module accepts one parameter:
_zuul_freeze_vars is a list of variable names to template.
It stores the templated variables in a cacheable fact named
_zuul_frozen.
If any variable is undefined or recursively references any
undefined variables, it is omitted from the result.
"""
if task_vars is None:
task_vars = dict()
results = super(ActionModule, self).run(tmp, task_vars)
del tmp # tmp no longer has any effect
varlist = self._task.args.get('_zuul_freeze_vars')
ret = {}
for var in varlist:
try:
# Template the variable (convert_bare means treat a
# bare variable name as {{ var }}.
value = self._templar.template(var, convert_bare=True)
recursive_check_defined(value)
ret[var] = value
except Exception:
pass
results['ansible_facts'] = {'_zuul_frozen': ret}
results['_ansible_facts_cacheable'] = True
return results

View File

@@ -3060,21 +3060,11 @@ class AnsibleJob(object):
}
for host in self.host_list + [localhost]:
tasks = [{
'set_fact': {
'_zuul_frozen': {},
'cacheable': True,
'zuul_freeze': {
'_zuul_freeze_vars': list(
self.original_hostvars[host['name']].keys()),
},
}]
for var in self.original_hostvars[host['name']].keys():
val = "{{ _zuul_frozen | combine({'%s': %s}) }}" % (var, var)
task = {
'set_fact': {
'_zuul_frozen': val,
'cacheable': True,
},
'ignore_errors': True,
}
tasks.append(task)
play = {
'hosts': host['name'],
'tasks': tasks,