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

Handle collection redirects for filters/tests #77210

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,2 @@
bugfixes:
- Fix collection filter/test plugin redirects (https://github.com/ansible/ansible/issues/77192).
89 changes: 53 additions & 36 deletions lib/ansible/template/__init__.py
Expand Up @@ -455,6 +455,7 @@ def _load_ansible_plugins(self):
# FUTURE: we can cache FQ filter/test calls for the entire duration of a run, since a given collection's impl's
# aren't supposed to change during a run
def __getitem__(self, key):
original_key = key
self._load_ansible_plugins()

try:
Expand All @@ -469,56 +470,61 @@ def __getitem__(self, key):
if func:
return func

# didn't find it in the pre-built Jinja env, assume it's a former builtin and follow the normal routing path
leaf_key = key
key = 'ansible.builtin.' + key
else:
leaf_key = key.split('.')[-1]
key, leaf_key = get_fqcr_and_name(key)
seen = set()

while True:
if key in seen:
raise TemplateSyntaxError(
'recursive collection redirect found for %r' % original_key,
0
)
seen.add(key)

acr = AnsibleCollectionRef.try_parse_fqcr(key, self._dirname)
acr = AnsibleCollectionRef.try_parse_fqcr(key, self._dirname)

if not acr:
raise KeyError('invalid plugin name: {0}'.format(key))
if not acr:
raise KeyError('invalid plugin name: {0}'.format(key))

ts = _get_collection_metadata(acr.collection)
ts = _get_collection_metadata(acr.collection)

# TODO: implement support for collection-backed redirect (currently only builtin)
# TODO: implement cycle detection (unified across collection redir as well)
# TODO: implement cycle detection (unified across collection redir as well)

routing_entry = ts.get('plugin_routing', {}).get(self._dirname, {}).get(leaf_key, {})
routing_entry = ts.get('plugin_routing', {}).get(self._dirname, {}).get(leaf_key, {})

deprecation_entry = routing_entry.get('deprecation')
if deprecation_entry:
warning_text = deprecation_entry.get('warning_text')
removal_date = deprecation_entry.get('removal_date')
removal_version = deprecation_entry.get('removal_version')
deprecation_entry = routing_entry.get('deprecation')
if deprecation_entry:
warning_text = deprecation_entry.get('warning_text')
removal_date = deprecation_entry.get('removal_date')
removal_version = deprecation_entry.get('removal_version')

if not warning_text:
warning_text = '{0} "{1}" is deprecated'.format(self._dirname, key)
if not warning_text:
warning_text = '{0} "{1}" is deprecated'.format(self._dirname, key)

display.deprecated(warning_text, version=removal_version, date=removal_date, collection_name=acr.collection)
display.deprecated(warning_text, version=removal_version, date=removal_date, collection_name=acr.collection)

tombstone_entry = routing_entry.get('tombstone')
tombstone_entry = routing_entry.get('tombstone')

if tombstone_entry:
warning_text = tombstone_entry.get('warning_text')
removal_date = tombstone_entry.get('removal_date')
removal_version = tombstone_entry.get('removal_version')
if tombstone_entry:
warning_text = tombstone_entry.get('warning_text')
removal_date = tombstone_entry.get('removal_date')
removal_version = tombstone_entry.get('removal_version')

if not warning_text:
warning_text = '{0} "{1}" has been removed'.format(self._dirname, key)
if not warning_text:
warning_text = '{0} "{1}" has been removed'.format(self._dirname, key)

exc_msg = display.get_deprecation_message(warning_text, version=removal_version, date=removal_date,
collection_name=acr.collection, removed=True)
exc_msg = display.get_deprecation_message(warning_text, version=removal_version, date=removal_date,
collection_name=acr.collection, removed=True)

raise AnsiblePluginRemovedError(exc_msg)
raise AnsiblePluginRemovedError(exc_msg)

redirect_fqcr = routing_entry.get('redirect', None)
if redirect_fqcr:
acr = AnsibleCollectionRef.from_fqcr(ref=redirect_fqcr, ref_type=self._dirname)
display.vvv('redirecting {0} {1} to {2}.{3}'.format(self._dirname, key, acr.collection, acr.resource))
key = redirect_fqcr
# TODO: handle recursive forwarding (not necessary for builtin, but definitely for further collection redirs)
redirect = routing_entry.get('redirect', None)
if redirect:
next_key, leaf_key = get_fqcr_and_name(redirect, collection=acr.collection)
display.vvv('redirecting (type: {0}) {1}.{2} to {3}'.format(self._dirname, acr.collection, acr.resource, next_key))
key = next_key
else:
break

func = self._collection_jinja_func_cache.get(key)

Expand Down Expand Up @@ -593,6 +599,17 @@ def __len__(self):
return len(self._delegatee)


def get_fqcr_and_name(resource, collection='ansible.builtin'):
if '.' not in resource:
name = resource
fqcr = collection + '.' + resource
else:
name = resource.split('.')[-1]
fqcr = resource

return fqcr, name


@_unroll_iterator
def _ansible_finalize(thing):
"""A custom finalize function for jinja2, which prevents None from being
Expand Down
Expand Up @@ -2,3 +2,20 @@ plugin_routing:
modules:
ping:
redirect: testns.testcoll.ping
filter:
multi_redirect_filter:
redirect: testns.testredirect.redirect_filter1
deprecation:
warning_text: deprecation1
redirect_filter1:
redirect: redirect_filter2
deprecation:
warning_text: deprecation2
redirect_filter2:
redirect: testns.testcoll.testfilter
deprecation:
warning_text: deprecation3
dead_end:
redirect: bad_redirect
recursive_redirect:
redirect: recursive_redirect
10 changes: 10 additions & 0 deletions test/integration/targets/collections/runme.sh
Expand Up @@ -65,6 +65,16 @@ else
ansible-playbook -i "${INVENTORY_PATH}" collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml "$@"
fi

# test redirects and warnings for filter redirects
echo "testing redirect and deprecation display"
ANSIBLE_DEPRECATION_WARNINGS=yes ansible localhost -m debug -a msg='{{ "data" | testns.testredirect.multi_redirect_filter }}' -vvvvv 2>&1 | tee out.txt
cat out.txt

test "$(grep out.txt -ce 'deprecation1' -ce 'deprecation2' -ce 'deprecation3')" == 3
grep out.txt -e 'redirecting (type: filter) testns.testredirect.multi_redirect_filter to testns.testredirect.redirect_filter1'
grep out.txt -e 'redirecting (type: filter) testns.testredirect.redirect_filter1 to testns.testredirect.redirect_filter2'
grep out.txt -e 'redirecting (type: filter) testns.testredirect.redirect_filter2 to testns.testcoll.testfilter'

echo "--- validating collections support in playbooks/roles"
# run test playbooks
ansible-playbook -i "${INVENTORY_PATH}" -v "${TEST_PLAYBOOK}" "$@"
Expand Down
19 changes: 19 additions & 0 deletions test/integration/targets/collections/test_collection_meta.yml
Expand Up @@ -21,6 +21,25 @@
# redirect filter
- assert:
that: ('yes' | formerly_core_filter) == True
# redirect filter (multiple levels)
- assert:
that: ('data' | testns.testredirect.multi_redirect_filter) == 'data_via_testfilter_from_userdir'
# invalid filter redirect
- debug: msg="{{ 'data' | testns.testredirect.dead_end }}"
ignore_errors: yes
register: redirect_failure
- assert:
that:
- redirect_failure is failed
- '"No filter named ''testns.testredirect.dead_end''" in redirect_failure.msg'
# recursive filter redirect
- debug: msg="{{ 'data' | testns.testredirect.recursive_redirect }}"
ignore_errors: yes
register: redirect_failure
- assert:
that:
- redirect_failure is failed
- '"recursive collection redirect found for ''testns.testredirect.recursive_redirect''" in redirect_failure.msg'
# legacy filter should mask redirected
- assert:
that: ('' | formerly_core_masked_filter) == 'hello from overridden formerly_core_masked_filter'
Expand Down