Repository: ansible/ansible
Pull request: Allow a collection role to call a standalone role by default
##### SUMMARY
Fixes #69101

Allow a collection to call a stand alone role, without needing
to explicitly add `ansible.legacy` to the collection search order within the
collection role.

##### ISSUE TYPE
- Bugfix Pull Request

##### COMPONENT NAME
lib/ansible/playbook/role/definition.py

##### ADDITIONAL INFORMATION
<!--- Include additional information to help people understand the change here -->
<!--- A step-by-step reproduction of the problem is helpful if there is no related issue -->

<!--- Paste verbatim command output below, e.g. before and after your change -->
```paste below

```


Problem statement:
Collection roles cannot call stand alone roles
##### SUMMARY
A role within a collection, cannot easily call a standalone role

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
lib/ansible/playbook/role/definition.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
2.9
2.10
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below

```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->


##### STEPS TO REPRODUCE

Assume a collection role like the following:

collections/ansible_collections/ns/col/roles/nscolrole/tasks/main.yml
```
- include_role:
    name: standalone
```

And a standalone like:

roles/standalone/tasks/main.yml
```
- debug:
    msg: standalone
```

You can work around this, by explicitly adding `ansible.legacy` to the collection search for the `include_role` like:

```
- include_role:
    name: standalone
  collections:
    - ansible.legacy
```

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->


##### ACTUAL RESULTS
```
TASK [include_role : standalone] *************************************************************************************
ERROR! the role 'standalone' was not found in ns.col:ansible.builtin

The error appears to be in '/Users/matt/projects/ansibledev/playbooks/ericsysmin/collections/ansible_collections/ns/col/roles/nscolrole/tasks/main.yml': line 2, column 11, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

- include_role:
    name: standalone
          ^ here
```

Hints from issue discussion:
@geerlingguy , @tima, and @s-hertel, one to keep watch on.
@ericsysmin watching as well. Thanks, @sivel

Commit message: tweaked changelog text

Patch to review:
diff --git a/changelogs/fragments/69101-collection-role-to-standalone-role.yml b/changelogs/fragments/69101-collection-role-to-standalone-role.yml
new file mode 100644
index 00000000000000..04e8275af97bda
--- /dev/null
+++ b/changelogs/fragments/69101-collection-role-to-standalone-role.yml
@@ -0,0 +1,5 @@
+bugfixes:
+- Collections - Allow a collection role to call a stand alone role, without
+  needing to explicitly add ``ansible.legacy`` to the collection search
+  order within the collection role.
+  (https://github.com/ansible/ansible/issues/69101)
diff --git a/lib/ansible/playbook/role/definition.py b/lib/ansible/playbook/role/definition.py
index 1f154c694cf212..ea1f3df5319e83 100644
--- a/lib/ansible/playbook/role/definition.py
+++ b/lib/ansible/playbook/role/definition.py
@@ -162,9 +162,9 @@ def _load_role_path(self, role_name):
             self._role_collection = role_tuple[2]
             return role_tuple[0:2]
 
-        # FUTURE: refactor this to be callable from internal so we can properly order ansible.legacy searches with the collections keyword
-        if self._collection_list and 'ansible.legacy' not in self._collection_list:
-            raise AnsibleError("the role '%s' was not found in %s" % (role_name, ":".join(self._collection_list)), obj=self._ds)
+        # We didn't find a collection role, look in defined role paths
+        # FUTURE: refactor this to be callable from internal so we can properly order
+        # ansible.legacy searches with the collections keyword
 
         # we always start the search for roles in the base directory of the playbook
         role_search_paths = [
@@ -198,7 +198,8 @@ def _load_role_path(self, role_name):
             role_name = os.path.basename(role_name)
             return (role_name, role_path)
 
-        raise AnsibleError("the role '%s' was not found in %s" % (role_name, ":".join(role_search_paths)), obj=self._ds)
+        searches = self._collection_list + role_search_paths
+        raise AnsibleError("the role '%s' was not found in %s" % (role_name, ":".join(searches)), obj=self._ds)
 
     def _split_role_params(self, ds):
         '''
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml
new file mode 100644
index 00000000000000..f5dcc0fccc8ee1
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml
@@ -0,0 +1,6 @@
+- include_role:
+    name: standalone
+
+- assert:
+    that:
+      - standalone_role_var is defined
diff --git a/test/integration/targets/collections/posix.yml b/test/integration/targets/collections/posix.yml
index d1ea2f9c04ac23..0d7c7089c4b591 100644
--- a/test/integration/targets/collections/posix.yml
+++ b/test/integration/targets/collections/posix.yml
@@ -401,3 +401,8 @@
     handler_counter: 0
   roles:
     - testns.testcoll.test_fqcn_handlers
+
+- name: Ensure a collection role can call a standalone role
+  hosts: testhost
+  roles:
+    - testns.testcoll.call_standalone
diff --git a/test/integration/targets/collections/roles/standalone/tasks/main.yml b/test/integration/targets/collections/roles/standalone/tasks/main.yml
new file mode 100644
index 00000000000000..b4dd23db98b0a5
--- /dev/null
+++ b/test/integration/targets/collections/roles/standalone/tasks/main.yml
@@ -0,0 +1,2 @@
+- set_fact:
+    standalone_role_var: True


Code review comments:
diff_hunk: @@ -198,7 +198,8 @@ def _load_role_path(self, role_name):
             role_name = os.path.basename(role_name)
             return (role_name, role_path)
 
-        raise AnsibleError("the role '%s' was not found in %s" % (role_name, ":".join(role_search_paths)), obj=self._ds)
+        searches = self._collection_list + role_search_paths
path: lib/ansible/playbook/role/definition.py
text: @user1:
This will go bang if `self._collection_list` is None, which it will be for classic roles that don't define a search order

@author:
+1 I can take care of that