Repository: ansible/ansible-navigator (language: python)

Issue:
Collection documentation inside EE vs local
##### ISSUE TYPE
 - Bug Report

##### SUMMARY
Cannot pull up collection documentation of collections contained within an EE.

##### ANSIBLE-NAVIGATOR VERSION
```
ansible-navigator 1.1.0
```

##### CONFIGURATION
ansible-navigator.yml
```yaml
---
ansible-navigator:
  ansible:
    config: $PWD/ansible.cfg
    # cmdline: "--forks 15"
    # inventories:
    # - /tmp/test_inventory.yml
    # playbook: /tmp/test_playbook.yml

  ansible-runner:
    artifact-dir: $PWD/navigator_artifacts
    rotate-artifacts-count: 10
    timeout: 300

  app: welcome  # ‘collections’, ‘config’, ‘doc’, ‘images’, ‘inventory’, ‘replay’, ‘run’ or ‘welcome’

  collection-doc-cache-path: $PWD/cache.db

  color:
    enable: True
    osc4: True

  editor:
    # command: code
    command: vim +{line_number} {filename}
    # console: False
    console: True

  # documentation:
  #   plugin:
  #     name: shell
  #     type: become

  execution-environment:
    container-engine: podman
    enabled: true
    # environment-variables:
    #   pass:
    #     - ONE
    #     - TWO
    #     - THREE
    #   set:
    #     KEY1: VALUE1
    #     KEY2: VALUE2
    #     KEY3: VALUE3
    image: localhost/kiewit_ee_all:latest
    pull-policy: never
    # volume-mounts:
    # - src: "/test1"
    #   dest: "/test1"
    #   label: "Z"
    # container-options:
    # - "--net=host"

  # help-config: True

  # help-doc: True

  # help-inventory: True

  # help-playbook: False

  # inventory-columns:
  #   - ansible_network_os
  #   - ansible_network_cli_ssh_type
  #   - ansible_connection

  logging:
    level: warning
    append: False
    file: $PWD/navigator.log

  mode: interactive  # interactive, stdout

  playbook-artifact:
    enable: True
    replay: $PWD/navigator_replay.json
    save-as: $PWD/navigator_replay.json

```
ansible.cfg (in CWD)
```ini
[defaults]
forks = 20
precedence = all_plugins_inventory,groups_plugins_inventory,all_inventory,groups_inventory,all_plugins_play,groups_plugins_play
retry_files_enabled = true
host_key_checking = false
#collections_paths = collections/:~/.ansible/collections:/usr/share/ansible/collections
#roles_path = roles/:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
vault_identity_list = vcenter@/home/runner/workspace/kiewit/ansible_vaults/vcenter,azure@/home/runner/workspace/kiewit/ansible_vaults/azure,ansible_user@/home/runner/workspace/kiewit/ansible_vaults/ansible_user,kiewitadmin_user@/home/runner/workspace/kiewit/ansible_vaults/kiewitadmin_user,awx@/home/runner/workspace/kiewit/ansible_vaults/awx,snow@/home/runner/workspace/kiewit/ansible_vaults/snow
ssh_args = -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
callback_whitelist = timer, profile_roles, profile_tasks, junit
gathering = smart
fact_caching = jsonfile
fact_caching_connection = $HOME/ansible/facts
fact_caching_timeout = 600
nocows = true
# Use the YAML callback plugin.
stdout_callback = yaml
# Use the stdout_callback when running ad-hoc commands.
bin_ansible_callbacks = True
```

##### LOG FILE
N/A

##### STEPS TO REPRODUCE
As shown in my configs, if I launch `ansible-navigator` with no arguments, I expect to be at the welcome screen and when I list :images, I expect to be using the specified image.  This works as expected

```shell
NAME                                                                TAG               EXECUTION ENVIRONMENT                                   CREATED                                       SIZE
0│ansible-builder                                                     latest                            False                                   14 hours ago                                  539 MB
1│awx-ee                                                              latest                             True                                   2 hours ago                                   1.27 GB
2│kiewit_ee_all (primary)                                             latest                             True                                   About an hour ago                             5.99 GB
3│kiewit_ee_linux_instance                                            latest                             True                                   2 hours ago                                   3.43 GB
4│kiewit_ee_windows_instance                                          latest                             True                                   2 hours ago                                   3.42 GB
```

Moreover, if I press a number corresponding to the selected image (in this case, 2) and then view Ansible version and collections, I can confirm that the collections specified when building the image are there.

However, when I run the :collections command from the welcome screen, I'm told there are no collections.

```shell
WARNING
                                                          ──────────────────────────────────────────────────────────────────
                                                          humph. no collections were found in the following paths:
                                                          [HINT] Try installing some or try a different execution enviroment
                                                          ──────────────────────────────────────────────────────────────────
```

##### EXPECTED RESULTS

My expectation is that I can browse collection documentation for collections pre-installed "inside" the EE, but it appears that `ansible-navigator` is looking for collections installed locally.

I'm extremely excited about the prospect of self-contained EEs as it will make my job of setting up other developers, CI/CD, and AWX/Tower to have the same environment everywhere.  And `ansible-navigator` as a method to introspect and execute from an EE is amazing.

However, from an Ansible content developmental point of view, if I'm using a provided EE and writing a playbook, I want to pull up documentation specific to the collections contained within the EE to ensure I'm using modules, libraries, plugins, etc. correctly in my new content.

It seems weird, with this new paradigm, to expect that a developer using `ansible-navigator` as a runner/development aid to also have `ansible-galaxy` as part of a normal `ansible` install to obtain the collections locally.

Or am I doing/understanding things horribly wrong?

##### ACTUAL RESULTS

```shell
humph. no collections were found in the following paths:
```

##### ADDITIONAL INFORMATION

N/A



Discussion / hints:
Can you run with `--ll debug` and provide the corresponding `ansible-navigator.log` so we can get a better idea of what is happening here?
Here's a copy of the log when running `ansible-navigator collections` from the command-line
[navigator.log](https://github.com/ansible/ansible-navigator/files/7633767/navigator.log)
.

From what I'm seeing, it is volume mounting local dirs and only searching those for collections?  I've spun up a container instance of the image via `podman run --rm -it cregprod01scusdco.azurecr.io/kiewit/kiewit_ee_all:latest /bin/bash` and can confirm the presence of roles and collections inside the image/container at /usr/share/ansible

```bash
bash-4.4# ls /usr/share/ansible/collections/ansible_collections/
amazon	 awx	      chocolatey     community	 dellemc     frr      hetzner  infinidat	kiewit	    netapp	    ngine_io	 ovirt		 sensu	     t_systems_mms  wti
ansible  azure	      cisco	     containers  f5networks  gluster  hpe      inspur		kubernetes  netapp_eseries  openstack	 purestorage	 servicenow  theforeman
arista	 check_point  cloudscale_ch  cyberark	 fortinet    google   ibm      junipernetworks	mellanox    netbox	    openvswitch  redhatinsights  splunk      vyos
```

@watsonb Do me a favor?  Try again w/o overriding the defautl collection_cache_db location.  There's a duplicate volumne mount and I'm wondering if it is becasue the collection_cache is in the CWD....  I snipped out some of the log info that is relevant:

```
Running with collection_doc_cache_path as '/home/ben/workspace/kiewit/ansible/cache.db' (str/user provided configuration file)


container_volume_mounts:[
    '/home/ben/venv_ansible_navigator/share/ansible_navigator/utils:/home/ben/venv_ansible_navigator/share/ansible_navigator/utils', 
    '/home/ben/workspace/kiewit/ansible/collections:/home/ben/workspace/kiewit/ansible/collections:z', 
    '/home/ben/workspace/kiewit/ansible/cache.db:/home/ben/workspace/kiewit/ansible/cache.db:z']
]


podman run --rm --tty --interactive 
    -v /home/ben/workspace/kiewit/ansible/:/home/ben/workspace/kiewit/ansible/ <<<<<<<<<<<
    --workdir /home/ben/workspace/kiewit/ansible 
    -v /run/user/1302/keyring/:/run/user/1302/keyring/ 
    -e SSH_AUTH_SOCK=/run/user/1302/keyring/ssh 
    -v /home/ben/.ssh/:/home/runner/.ssh/ 
    --group-add=root 
    --ipc=host 
    -v /tmp/ansible-navigator__wayqd59/artifacts/:/runner/artifacts/:Z 
    -v /tmp/ansible-navigator__wayqd59/:/runner/:Z 
    -v /home/ben/venv_ansible_navigator/share/ansible_navigator/utils/:/home/ben/venv_ansible_navigator/share/ansible_navigator/utils/ 
    -v /home/ben/workspace/kiewit/ansible/collections/:/home/ben/workspace/kiewit/ansible/collections/:z 
    -v /home/ben/workspace/kiewit/ansible/:/home/ben/workspace/kiewit/ansible/:z <<<<<<<<<<<<<<<
    --env-file /tmp/ansible-navigator__wayqd59/artifacts/aa6fad89-a149-45aa-b38a-2c932b34f31e/env.list 
    --quiet --name ansible_runner_aa6fad89-a149-45aa-b38a-2c932b34f31e 
    cregprod01scusdco.azurecr.io/kiewit/kiewit_ee_all:latest 
    python3 /home/ben/venv_ansible_navigator/share/ansible_navigator/utils/catalog_collections.py -a /home/ben/workspace/kiewit/ansible/collections -c /home/ben/workspace/kiewit/ansible/cache.db

Error: /home/ben/workspace/kiewit/ansible/: duplicate mount destination
```
well that seems to have worked!  curious, my custom collection has an inventory plugin and a bunch of roles.  The only thing I can browse is the inventory plugin.  I looked at some other collections as well (e.g. awx.awx, and azure.azcollection) and can see their plugins and modules.  Will this be able to browse a collection's roles as well in the future?
@watsonb Yes! https://github.com/ansible/ansible-navigator/issues/231, anything you want to add there would be great.

Thanks for the confirmation, I'll see if I can recreate here.

Curious, did you have a reason to move the collection cache?  (only because I never really expected anyone to do that :) )


honestly I think I copied/pasted the ansible-navigator.yml file and tuned it.  Have no idea why that cache location was specified

Fix patch:
diff --git a/docs/changelog-fragments.d/972.bugfix.md b/docs/changelog-fragments.d/972.bugfix.md
new file mode 100644
index 000000000..277427d66
--- /dev/null
+++ b/docs/changelog-fragments.d/972.bugfix.md
@@ -0,0 +1,4 @@
+Prevented a duplicate volume mount when the user-specified path to the collection doc cache
+is in the playbook directory.
+
+-- by {user}`cidrblock`
diff --git a/src/ansible_navigator/actions/collections.py b/src/ansible_navigator/actions/collections.py
index af03befcf..39357cfb8 100644
--- a/src/ansible_navigator/actions/collections.py
+++ b/src/ansible_navigator/actions/collections.py
@@ -7,6 +7,7 @@
 
 from copy import deepcopy
 from json.decoder import JSONDecodeError
+from pathlib import Path
 from typing import Any
 from typing import Dict
 from typing import List
@@ -23,6 +24,7 @@
 from ..ui_framework import Interaction
 from ..ui_framework import nonblocking_notification
 from ..ui_framework import warning_notification
+from ..utils import path_is_relative_to
 from . import _actions as actions
 from . import run_action
 
@@ -324,9 +326,15 @@ def _run_runner(self) -> None:
                     f"{self._adjacent_collection_dir}:{self._adjacent_collection_dir}:z",
                 )
 
-            container_volume_mounts.append(
-                f"{self._collection_cache_path}:{self._collection_cache_path}:z",
-            )
+            # The playbook directory will be mounted as host_cwd, so don't duplicate
+            if not path_is_relative_to(
+                child=Path(self._collection_cache_path),
+                parent=(Path(playbook_dir)),
+            ):
+                container_volume_mounts.append(
+                    f"{self._collection_cache_path}:{self._collection_cache_path}:z",
+                )
+
             if "container_volume_mounts" in kwargs:
                 kwargs["container_volume_mounts"] += container_volume_mounts
             else:
diff --git a/src/ansible_navigator/utils.py b/src/ansible_navigator/utils.py
index 81982152d..e67f0514f 100644
--- a/src/ansible_navigator/utils.py
+++ b/src/ansible_navigator/utils.py
@@ -12,6 +12,7 @@
 
 from dataclasses import dataclass
 from enum import Enum
+from pathlib import Path
 from typing import Any
 from typing import List
 from typing import Mapping
@@ -384,6 +385,22 @@ def pascal_to_snake(obj):
     return obj
 
 
+def path_is_relative_to(child: Path, parent: Path) -> bool:
+    """Return True if the path is relative to another path or False.
+
+    :param child: The path that may be a child
+    :param parent: The path that may be a parent
+    :returns: Indicates the child is a child of the parent
+    """
+    if sys.version_info >= (3, 9):
+        return child.is_relative_to(parent)
+    try:
+        child.relative_to(parent)
+        return True
+    except ValueError:
+        return False
+
+
 def remove_ansi(string):
     """strip ansi code from a str"""
     ansi_escape = re.compile(


Test patch:
diff --git a/test-requirements.txt b/test-requirements.txt
index 8dd1bf2f3..3058f6a89 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -3,4 +3,5 @@ libtmux
 lxml
 pre-commit
 pytest-cov
+pytest-mock
 pytest-xdist
diff --git a/tests/conftest.py b/tests/conftest.py
index 22ff9e2a0..a11cc6faa 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -30,3 +30,14 @@ def pullable_image(valid_container_engine):
     """A container that can be pulled."""
     yield PULLABLE_IMAGE
     subprocess.run([valid_container_engine, "image", "rm", PULLABLE_IMAGE], check=True)
+
+
+@pytest.fixture
+def patch_curses(monkeypatch):
+    """Patch curses so it doesn't traceback during tests.
+
+    :param monkeypatch: Fixture for patching
+    """
+    monkeypatch.setattr("curses.cbreak", lambda: None)
+    monkeypatch.setattr("curses.nocbreak", lambda: None)
+    monkeypatch.setattr("curses.endwin", lambda: None)
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index bef8ff70f..f2f277f13 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -21,17 +21,6 @@ def test_fixtures_dir():
     return os.path.join(os.path.dirname(__file__), "..", "fixtures")
 
 
-@pytest.fixture
-def patch_curses(monkeypatch):
-    """Patch curses so it doesn't traceback during tests.
-
-    :param monkeypatch: Fixture for patching
-    """
-    monkeypatch.setattr("curses.cbreak", lambda: None)
-    monkeypatch.setattr("curses.nocbreak", lambda: None)
-    monkeypatch.setattr("curses.endwin", lambda: None)
-
-
 @pytest.fixture(scope="session")
 def os_independent_tmp():
     """
diff --git a/tests/unit/actions/collections/test_collection_doc_cache_path.py b/tests/unit/actions/collections/test_collection_doc_cache_path.py
new file mode 100644
index 000000000..070f44bc3
--- /dev/null
+++ b/tests/unit/actions/collections/test_collection_doc_cache_path.py
@@ -0,0 +1,65 @@
+"""Test the use of ``collection-doc-cache-path`` through to runner."""
+import shlex
+
+from pathlib import Path
+from typing import NamedTuple
+
+import pytest
+
+from ansible_navigator import cli
+
+
+class TstData(NamedTuple):
+    """Store test data."""
+
+    description: str
+    path: str
+
+
+DOC_CACHE_PATHS = (
+    TstData(description="cwd", path="./cache.db"),
+    TstData(description="tmp dir", path="../cache.db"),
+)
+
+
+def _id_description(value):
+    """Generate id for a test"""
+    return value.description
+
+
+class DuplicateMountException(RuntimeError):
+    """An exception specific to the duplicate mount test for collections."""
+
+
+@pytest.mark.parametrize("doc_cache_path", DOC_CACHE_PATHS, ids=_id_description)
+@pytest.mark.usefixtures("patch_curses")
+def test_for_duplicates_sources(
+    doc_cache_path,
+    monkeypatch: pytest.MonkeyPatch,
+    tmp_path: Path,
+    mocker,
+):
+    """Ensure duplicate volume mounts are not passed to runner.
+
+    :param doc_cache_path: The test data
+    :param patch_curses: Fixture to patch curses so it doesn't traceback
+    :param monkeypatch: The monkeypatch fixture
+    :param arg_collector: The fixture used to collect argument passed to a function
+    """
+    working_dir = tmp_path / "working_dir"
+    working_dir.mkdir()
+    cdc_full_path = working_dir / doc_cache_path.path
+    command = f"ansible-navigator collections '--cdcp={cdc_full_path!s}' --pp never"
+    monkeypatch.setattr("sys.argv", shlex.split(command))
+    run_cmd_mocked = mocker.patch(
+        "ansible_navigator.runner.command.run_command",
+        side_effect=DuplicateMountException,
+    )
+    monkeypatch.chdir(working_dir)
+    with pytest.raises(DuplicateMountException):
+        cli.main()
+    _args, kwargs = run_cmd_mocked.call_args
+    host_cwd = Path(kwargs["host_cwd"])
+    mounts = kwargs["container_volume_mounts"]
+    sources = [Path(mount.split(":")[0]).parents[0] for mount in mounts]
+    assert host_cwd not in sources
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 4076579d3..80488c722 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -1,6 +1,7 @@
 """Test the functions exposed in the :mod:`~ansible_navigator.utils` subpackage."""
 import os
 
+from pathlib import Path
 from typing import List
 from typing import NamedTuple
 from typing import Optional
@@ -193,3 +194,11 @@ def test_round_half_up(data: RoundHalfUpTestData) -> None:
     """
     result = utils.round_half_up(data.value)
     assert result == data.expected
+
+
+def test_path_is_relative_to():
+    """Ensure path_is_relative_to returns accurate results."""
+    directory = Path("/tmp/test")
+    file_in_directory = Path("/tmp/test/file.txt")
+    assert utils.path_is_relative_to(child=file_in_directory, parent=directory)
+    assert not utils.path_is_relative_to(child=directory, parent=file_in_directory)