Krikri::RoleLoader
RoleLoader - resolves and loads roles: entries for a play.
A role is a directory (roles/<name>/) with a conventional layout: tasks/main.yml, handlers/main.yml, defaults/main.yml, vars/main.yml, meta/main.yml, files/, templates/
Role search path mirrors Ansible's common case: <playbook_dir>/roles/<name>, then ./roles/<name> relative to the working directory. Ansible also searches ANSIBLE_ROLES_PATH and a few other locations; not implemented.
Constants
ansible-core's RoleMetadata field-attribute set (taken from the installed core's RoleMetadata.fattributes, not from anything krikri implements): Ansible refuses any top-level meta/main.yml key outside it at role load, before any task runs, raising on the FIRST offending key in document order.
Class methods
Ansible accepts .yml/.yaml/.json for argument_specs (same extension list as find_main_file), in that order, and does NOT combine them or consult meta/main when one exists - only when none of the three exists do the specs come from the meta main file's argument_specs: key (stephdewit.sshca ships meta/argument_specs.yaml, issue #638).
Public: PluginManager's connection-plugin resolution (fix "the
connection plugin 'X' was not found") searches the same controller
collection locations for plugins/connection/<name>.py.
Loads a role's defaults/ or vars/ - Ansible supports EITHER a
single main.yml file OR a main/ directory of multiple *.yml
files (same convention tasks/main/ uses), merged together in
alphabetical filename order (later files win on a key collision -
matching Ansible's own main/ directory loading, which reads
files in sorted order and merges each into the accumulated dict).
Only ONE of the two forms is ever present for a given role.
Real bug found benchmarking kyl191.openvpn (round 160): its own
defaults/main/openvpn.yml (no defaults/main.yml at all) was
never read - load_vars_file alone always looked for exactly
defaults/main.yml, silently returning an empty hash for a role
using the directory form - every one of its own defaults
(openvpn_server_network, openvpn_server_ipv6_network, ...)
came back undefined, tripping the role's own "fail if both
tunnel networks are disabled" validation check that Ansible
never reaches (both are non-empty by default).
include_vars (unlike defaults/ or vars/ loading) refuses a file whose top level is anything but a mapping - e.g. a tasks-style list - with "<path> must be stored as a dictionary/hash". An empty file is fine.
Loads every entry in a play's roles: list (plus their meta/main.yml
dependencies, recursively), in order. Returns {tasks, handlers} to
prepend to the play - Ansible runs role tasks before the play's own
tasks: (pre_tasks:/post_tasks: aren't implemented).
Loads a single role by name (plus its meta/main.yml dependencies) - used by TaskExecutor#execute_include_role for include_role:, the dynamic (execution-time) counterpart to a static roles: entry. Each call gets its own fresh "seen" set, so - matching include_role's allow_duplicates: true default - repeated include_role calls for the same role name each load it again rather than being silently deduplicated the way a role listed twice under roles: would be. allow_duplicates: false on the include itself isn't honored (dedup only happens within a single call's own meta dependency chain).
Public: TaskExecutor#execute_include_vars loads the same shape of YAML vars file that roles do, and must parse it identically (including Vault decryption of individual values).
A STATIC import_role:'s target role is compiled by real ansible at
PLAYBOOK LOAD time: the whole static import tree is expanded before
any play runs, ignoring when:, so a removed bare include:
anywhere in that tree refuses the whole run (rc=1) instead of
surfacing as a per-task "Failed to load role" at runtime - or never
surfacing at all when a false when: skips the branch (issue #598,
ovirt.infra). This pre-loads the role's entry tasks (and handlers,
which real loads with the role's data) purely to surface that
refusal: only RemovedActionError propagates, every other parse
failure keeps today's behaviour (the runtime load still owns it),
and the parsed tasks are discarded, so runtime behaviour is
unchanged. A role not found on disk pre-loads nothing - the runtime
load reports that failure exactly as before.
The role search path list exactly as ansible-core's
RoleDefinition._load_role_path builds and REPORTS it
(definition.py: "The role '<name>' was not found in: <paths>"):
the playbook dir's own roles/ subtree first, then the configured
roles paths (ANSIBLE_ROLES_PATH or the ~/.ansible:/usr/share:/
/etc/ansible defaults), then the playbook dir itself. Live-verified
against 2.21.6: a missing role errors as "The role 'x' was not
found in: /work/roles:/root/.ansible/roles:/usr/share/ansible/roles:
/etc/ansible/roles:/work". When the play declares collections:,
ansible reports those collection names (plus "ansible.legacy") as a
prefix in front of the paths - live-verified vs 2.21.6 with
collections: [community.general, ansible.posix]:
"community.general:ansible.posix:ansible.legacy:/pb/roles:...:/pb"
Display-only - resolve_role_dir above keeps its own (superset) search
order.
The search-path list a role's OWN meta/main.yml DEPENDENCY reports
when IT names a missing role - ansible-core's RoleDefinition loads
dependencies through the collection-aware loader, which prefixes the
joined list with the "ansible.legacy" collection name and appends the
declaring role's own parent directory as an extra search root before
the playbook dir (live-verified vs 2.21.6:
play-level roles: [missing] ->
"/pb/roles:/home/.../.ansible/roles:/usr/share/ansible/roles:
/etc/ansible/roles:/pb"
meta/main.yml dependencies: [missing] ->
"ansible.legacy:/pb/roles:/home/.../.ansible/roles:
/usr/share/ansible/roles:/etc/ansible/roles:/pb/roles:/pb"
with the role installed via ANSIBLE_ROLES_PATH=/rl:
"ansible.legacy:/pb/roles:/rl:/rl:/pb"
- the inserted entry is the declaring role dir's own parent). Display only - resolve_role_dir's own (superset) search order is unchanged.
Whether a role's tasks/<tasks_from> file does not exist - public so PlaybookParser can reproduce real's parse-time refusal of a STATIC import_role: whose tasks_from: points nowhere (see RoleTasksFromFileError's own comment). tasks_from is taken literally, exactly as real builds the path (an explicit extension is honored, a bare name gets ".yml"/".yaml"/".json" tried in that order by #resolve_role_tasks_path).