diff --git a/changelogs/fragments/81450-list-filters.yml b/changelogs/fragments/81450-list-filters.yml new file mode 100644 index 00000000000..f307f92c457 --- /dev/null +++ b/changelogs/fragments/81450-list-filters.yml @@ -0,0 +1,4 @@ +bugfixes: + - "ansible-console - fix filtering by collection names when a collection search path was set (https://github.com/ansible/ansible/pull/81450)." +minor_changes: + - "ansible-doc - allow to filter listing of collections and metadata dump by more than one collection (https://github.com/ansible/ansible/pull/81450)." diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index 06c931272b7..4a5c8928160 100755 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -163,8 +163,8 @@ class RoleMixin(object): might be fully qualified with the collection name (e.g., community.general.roleA) or not (e.g., roleA). - :param collection_filter: A string containing the FQCN of a collection which will be - used to limit results. This filter will take precedence over the name_filters. + :param collection_filter: A list of strings containing the FQCN of a collection which will + be used to limit results. This filter will take precedence over the name_filters. :returns: A set of tuples consisting of: role name, collection name, collection path """ @@ -678,12 +678,11 @@ class DocCLI(CLI, RoleMixin): def _get_collection_filter(self): coll_filter = None - if len(context.CLIARGS['args']) == 1: - coll_filter = context.CLIARGS['args'][0] - if not AnsibleCollectionRef.is_valid_collection_name(coll_filter): - raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_filter)) - elif len(context.CLIARGS['args']) > 1: - raise AnsibleOptionsError("Only a single collection filter is supported.") + if len(context.CLIARGS['args']) >= 1: + coll_filter = context.CLIARGS['args'] + for coll_name in coll_filter: + if not AnsibleCollectionRef.is_valid_collection_name(coll_name): + raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_name)) return coll_filter diff --git a/lib/ansible/collections/list.py b/lib/ansible/collections/list.py index dd428e11f8b..ef858ae999c 100644 --- a/lib/ansible/collections/list.py +++ b/lib/ansible/collections/list.py @@ -32,16 +32,31 @@ def list_collection_dirs(search_paths=None, coll_filter=None, artifacts_manager= namespace_filter = None collection_filter = None + has_pure_namespace_filter = False # whether at least one coll_filter is a namespace-only filter if coll_filter is not None: - if '.' in coll_filter: - try: - namespace_filter, collection_filter = coll_filter.split('.') - except ValueError: - raise AnsibleError("Invalid collection pattern supplied: %s" % coll_filter) - else: - namespace_filter = coll_filter + if isinstance(coll_filter, str): + coll_filter = [coll_filter] + namespace_filter = set() + for coll_name in coll_filter: + if '.' in coll_name: + try: + namespace, collection = coll_name.split('.') + except ValueError: + raise AnsibleError("Invalid collection pattern supplied: %s" % coll_name) + namespace_filter.add(namespace) + if not has_pure_namespace_filter: + if collection_filter is None: + collection_filter = [] + collection_filter.append(collection) + else: + namespace_filter.add(coll_name) + has_pure_namespace_filter = True + collection_filter = None + namespace_filter = sorted(namespace_filter) for req in find_existing_collections(search_paths, artifacts_manager, namespace_filter=namespace_filter, collection_filter=collection_filter, dedupe=dedupe): + if not has_pure_namespace_filter and coll_filter is not None and req.fqcn not in coll_filter: + continue yield to_bytes(req.src) diff --git a/lib/ansible/galaxy/collection/__init__.py b/lib/ansible/galaxy/collection/__init__.py index cf726538755..88718e0fe5d 100644 --- a/lib/ansible/galaxy/collection/__init__.py +++ b/lib/ansible/galaxy/collection/__init__.py @@ -1420,6 +1420,10 @@ def find_existing_collections(path_filter, artifacts_manager, namespace_filter=N if path_filter and not is_sequence(path_filter): path_filter = [path_filter] + if namespace_filter and not is_sequence(namespace_filter): + namespace_filter = [namespace_filter] + if collection_filter and not is_sequence(collection_filter): + collection_filter = [collection_filter] paths = set() for path in files('ansible_collections').glob('*/*/'): @@ -1441,9 +1445,9 @@ def find_existing_collections(path_filter, artifacts_manager, namespace_filter=N for path in paths: namespace = path.parent.name name = path.name - if namespace_filter and namespace != namespace_filter: + if namespace_filter and namespace not in namespace_filter: continue - if collection_filter and name != collection_filter: + if collection_filter and name not in collection_filter: continue if dedupe: diff --git a/lib/ansible/plugins/list.py b/lib/ansible/plugins/list.py index bd05b1a0039..cd4d51f5832 100644 --- a/lib/ansible/plugins/list.py +++ b/lib/ansible/plugins/list.py @@ -171,28 +171,32 @@ def list_collection_plugins(ptype, collections, search_paths=None): return plugins -def list_plugins(ptype, collection=None, search_paths=None): +def list_plugins(ptype, collections=None, search_paths=None): + if isinstance(collections, str): + collections = [collections] # {plugin_name: (filepath, class), ...} plugins = {} - collections = {} - if collection is None: + plugin_collections = {} + if collections is None: # list all collections, add synthetic ones - collections['ansible.builtin'] = b'' - collections['ansible.legacy'] = b'' - collections.update(list_collections(search_paths=search_paths, dedupe=True)) - elif collection == 'ansible.legacy': - # add builtin, since legacy also resolves to these - collections[collection] = b'' - collections['ansible.builtin'] = b'' + plugin_collections['ansible.builtin'] = b'' + plugin_collections['ansible.legacy'] = b'' + plugin_collections.update(list_collections(search_paths=search_paths, dedupe=True)) else: - try: - collections[collection] = to_bytes(_get_collection_path(collection)) - except ValueError as e: - raise AnsibleError("Cannot use supplied collection {0}: {1}".format(collection, to_native(e)), orig_exc=e) + for collection in collections: + if collection == 'ansible.legacy': + # add builtin, since legacy also resolves to these + plugin_collections[collection] = b'' + plugin_collections['ansible.builtin'] = b'' + else: + try: + plugin_collections[collection] = to_bytes(_get_collection_path(collection)) + except ValueError as e: + raise AnsibleError("Cannot use supplied collection {0}: {1}".format(collection, to_native(e)), orig_exc=e) - if collections: - plugins.update(list_collection_plugins(ptype, collections)) + if plugin_collections: + plugins.update(list_collection_plugins(ptype, plugin_collections)) return plugins diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol3/galaxy.yml b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol3/galaxy.yml new file mode 100644 index 00000000000..bd6c15a449c --- /dev/null +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol3/galaxy.yml @@ -0,0 +1,6 @@ +namespace: testns +name: testcol3 +version: 0.1.0 +readme: README.md +authors: + - me diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol3/plugins/modules/test1.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol3/plugins/modules/test1.py new file mode 100644 index 00000000000..02dfb89d569 --- /dev/null +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol3/plugins/modules/test1.py @@ -0,0 +1,27 @@ +#!/usr/bin/python +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = """ +module: test1 +short_description: Foo module in testcol3 +description: + - This is a foo module. +author: + - me +""" + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec=dict(), + ) + + module.exit_json() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol4/galaxy.yml b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol4/galaxy.yml new file mode 100644 index 00000000000..7894d393caa --- /dev/null +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol4/galaxy.yml @@ -0,0 +1,6 @@ +namespace: testns +name: testcol4 +version: 1.0.0 +readme: README.md +authors: + - me diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol4/plugins/modules/test2.py b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol4/plugins/modules/test2.py new file mode 100644 index 00000000000..ddb0c114fa2 --- /dev/null +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol4/plugins/modules/test2.py @@ -0,0 +1,27 @@ +#!/usr/bin/python +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = """ +module: test2 +short_description: Foo module in testcol4 +description: + - This is a foo module. +author: + - me +""" + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec=dict(), + ) + + module.exit_json() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ansible-doc/runme.sh b/test/integration/targets/ansible-doc/runme.sh index 14f746c60b3..b525766cfa7 100755 --- a/test/integration/targets/ansible-doc/runme.sh +++ b/test/integration/targets/ansible-doc/runme.sh @@ -60,6 +60,14 @@ ansible-doc --list testns.testcol --playbook-dir ./ 2>&1 | grep $GREP_OPTS -v "I echo "ensure we dont break on invalid collection name for list" ansible-doc --list testns.testcol.fakemodule --playbook-dir ./ 2>&1 | grep $GREP_OPTS "Invalid collection name" +echo "filter list with more than one collection (1/2)" +output=$(ansible-doc --list testns.testcol3 testns.testcol4 --playbook-dir ./ 2>&1 | wc -l) +test "$output" -eq 2 + +echo "filter list with more than one collection (2/2)" +output=$(ansible-doc --list testns.testcol testns.testcol4 --playbook-dir ./ 2>&1 | wc -l) +test "$output" -eq 5 + echo "testing ansible-doc output for various plugin types" for ptype in cache inventory lookup vars filter module do @@ -114,6 +122,11 @@ echo "testing multiple role entrypoints" output=$(ansible-doc -t role -l --playbook-dir . testns.testcol | wc -l) test "$output" -eq 2 +echo "test listing roles with multiple collection filters" +# Two collection roles are defined, but only 1 has a role arg spec with 2 entry points +output=$(ansible-doc -t role -l --playbook-dir . testns.testcol2 testns.testcol | wc -l) +test "$output" -eq 2 + echo "testing standalone roles" # Include normal roles (no collection filter) output=$(ansible-doc -t role -l --playbook-dir . | wc -l)