galaxy-uninstall: removing Ansible collections

A command that removes installed Ansible collections, shows which copy Ansible loads, and refuses to delete collections that others still depend on.

The problem

ansible-galaxy can install collections but has no command to remove them. Collections can sit in several places: the paths in COLLECTIONS_PATHS (configured in ansible.cfg or ANSIBLE_COLLECTIONS_PATH), project collections/ directories, and ansible_collections in site-packages. To remove one, users delete its directory with rm -rf in every place a copy might be, and nothing checks whether another collection still depends on it.

The demand is documented:

How it works

galaxy-uninstall asks Ansible where collections are: ansible-config dump gives the configured search paths in order, and ansible-galaxy collection list --format json gives every installed copy. It does not import Ansible, so ansible.cfg, ANSIBLE_COLLECTIONS_PATH and -p are honoured as Ansible itself reads them. From this it determines which copy Ansible loads and which copies are shadowed by an earlier path. Before removing a collection it reads the dependencies from MANIFEST.json and refuses if another installed collection depends on it and no other copy stays installed. --deps also removes dependencies that no remaining collection needs, and --shadowed removes copies that Ansible never loads.

Removing a copy deletes ansible_collections/<ns>/<name>, the .info directory that ansible-galaxy writes, and the namespace directory if it is then empty. Collections that belong to a pip package and directories with a .git checkout are not deleted, and symlinked collections are unlinked without touching the target.

The README shows a real run (paths shortened) for a project with demo.app, which depends on demo.base, and two more copies of demo.base:

$ galaxy-uninstall --list
demo.app   1.0.0  ~/proj/collections/ansible_collections  [active]
demo.base  1.0.0  ~/proj/collections/ansible_collections  [active]
demo.base  2.0.0  ~/.ansible/collections/ansible_collections  [shadowed by ~/proj/collections/ansible_collections]
demo.base  2.0.0  /venv/lib/python3.12/site-packages/ansible_collections  [shadowed by ~/proj/collections/ansible_collections]

$ galaxy-uninstall demo.base --all
refusing: demo.app 1.0.0 in ~/proj/collections/ansible_collections depends on demo.base
remove the dependents too, or pass --force

$ galaxy-uninstall demo.app --deps -n
Will remove:
  demo.app 1.0.0  ~/proj/collections/ansible_collections/demo/app
  demo.base 1.0.0  ~/proj/collections/ansible_collections/demo/base
Afterwards Ansible loads demo.base 2.0.0 in ~/.ansible/collections/ansible_collections

The tool is a Python CLI that uses only the standard library.

Testing

The unit tests build directory trees with collections and replace the Ansible commands with fixed output. They cover the search order and shadowing, the selection of a copy when several are installed, the dependency check, transitive orphans for --deps, the detection of pip-owned collections and git checkouts, removal of the .info and namespace directories, symlinks, dry runs, and the refusal to delete without -y when stdin is not a terminal.

The end-to-end test uses a real ansible-core. It builds demo.base 1.0.0 and 2.0.0 and demo.app with ansible-galaxy, installs them offline into two paths, removes copies with galaxy-uninstall and checks with ansible localhost which copy a filter plugin is loaded from. CI runs all tests with ansible-core 2.15, 2.17 and the latest release (2.21). One difference turned up there: ansible-core 2.15 and 2.17 abort ansible-galaxy collection list with an error when none of the collection paths exists yet, where newer versions print an empty list. The tool treats that error as an empty listing.

Limitations

  • The search order is the one ansible and ansible-playbook use outside a playbook directory. A collections/ directory next to a playbook is only considered when it is passed with -p.
  • Version ranges in dependencies are not evaluated. A dependency counts as satisfied while any copy of it stays installed.
  • --deps cannot know which collections were installed on purpose, because ansible-galaxy keeps no record of that. -n shows the plan first, and --keep protects a collection.
  • Roles and requirements.yml files are not handled.

The code and installation instructions are at github.com/Jwrede/galaxy-uninstall.