Skip to content

Add role-scoped-variables ansible-lint rule - #729

Open
ehelms wants to merge 2 commits into
theforeman:masterfrom
ehelms:role-scoped-variables-lint-rule
Open

Add role-scoped-variables ansible-lint rule#729
ehelms wants to merge 2 commits into
theforeman:masterfrom
ehelms:role-scoped-variables-lint-rule

Conversation

@ehelms

@ehelms ehelms commented Aug 5, 2026

Copy link
Copy Markdown
Member

Why are you introducing these changes? (Problem description, related links)

Roles currently reference shared variables (e.g. database_host, ca_certificate) directly, coupling them to the playbook's variable namespace. This makes it hard to tell which variables a role actually needs, creates implicit dependencies between roles, and means renaming a shared variable silently breaks every role that uses it.

This PR adds a custom ansible-lint rule that enforces role-scoped variables and fixes the existing violations in addressed roles.

What are the changes introduced in this pull request?

  • New custom ansible-lint rule role-scoped-variables that flags variables inside role tasks not prefixed with the role name

    • Uses ansible-lint's nested_items_path to walk all task values — no manual key enumeration
    • Uses Jinja2 AST analysis to distinguish function calls (lookup, query, now) from variable references — no function allowlist needed
    • Allows: role-prefixed variables, ansible_* facts, Ansible builtins (item, omit, inventory_hostname, etc.), _-prefixed internal variables, and project globals
    • Test fixtures covering violation cases (module arg, when condition, loop, environment) and passing cases (role-prefixed, builtins, item, allowed globals, custom loop_var, task-local vars, underscore internal vars, function calls)
  • Map shared variables to role-prefixed names at playbook invocation sites:

    • deploy.yaml, deploy-proxy.yaml, checks.yaml, backup.yaml, deploy-dev.yaml
    • Parent roles pass scoped vars through to sub-roles (e.g. checks passes checks_database_* to check_database_index)
  • Rename role-internal variables (loop vars, include_tasks vars) to use leading underscore convention:

    • db_item to _db_item in check_database_connection
    • feature_name/feature_enabled to _feature_name/_feature_enabled in foreman_proxy
    • tuning_vars to _tuning_vars in check_system_requirements
  • Remaining violations to address in follow-up: certificates (6), restore (26), and 7 dynamically-included check roles where execute_check.yml uses include_role: name: "{{ item }}" with no way to pass per-role vars

How to test this pull request

Steps to reproduce:

  • Run rule unit tests: python -m pytest tests/ansible_lint/test_role_scoped_variables.py -vv
  • Run ansible-lint on src: ANSIBLE_COLLECTIONS_PATH="$PWD/build/collections/foremanctl" ANSIBLE_COLLECTIONS_SCAN_SYS_PATH=false bash -c '(cd src && ansible-lint)'
  • Run ansible-lint on development: ANSIBLE_COLLECTIONS_PATH="$PWD/build/collections/forge" ANSIBLE_COLLECTIONS_SCAN_SYS_PATH=false bash -c '(cd development && ansible-lint --exclude ../build/collections)'
  • Deploy and verify functionality is unchanged: ./foremanctl deploy --foreman-initial-admin-password=changeme --tuning development

Checklist

  • Tests added/updated (if applicable)
  • Documentation updated (if applicable)

@ehelms

ehelms commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

I expect this to fail and will then need to apply a commit on top that fixes those. As I have kept seeing these in PRs and having to comment on them, I decided to create a rule instead. Right now there is only one exception obsah_state_path.

@ehelms
ehelms force-pushed the role-scoped-variables-lint-rule branch 2 times, most recently from ca3b687 to 05f99dc Compare August 6, 2026 20:13
"""Check if a variable reference is allowed inside this role."""
if var.startswith(role_prefix) or var.lstrip("_").startswith(role_prefix):
return True
if var.startswith("_"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we allow any _ prefix var? The line before seems to say we only want _role_prefix to be allowed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I was torn between whether we want to allow "internal" (_ prefixed) variables to exist however without convention other than being prefixed to allow potentially shorter names or if we want to enforce prefixing of the role name in all cases.

Comment on lines +35 to +37
"lookup",
"query",
"q",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those aren't variables, but functions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in -- split the functions out to their own list and constant? I think the tricky part was how to only identify variables by the lint rule.

Should it instead only consider the first "token" before a pipe | ?

@evgeni evgeni Aug 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe not looking at the rest of a pipe is a good idea. There still can be some {{ a | combine(b) }} and we'd not notice the b being bad, but we probably also don't in the current code

@evgeni

evgeni commented Aug 10, 2026

Copy link
Copy Markdown
Member

I said elsewhere that I like the rule, but dislike the code. I think it boils down to:

  • The whole need to interact with Jinja2 directly (and doing weird things for implicit/explicit templates)
  • The fact that this looks at a fixed set of places where variables can show up (loop etc), making it a bit of whack-a-mole
  • The fact that we need to ignore special terms that are no real variables (lookup etc), making it again a whack-a-mole

I wonder if there is a better way to obtain this information from Ansible (instead of Jinja).
If there isn't - fine, but we should try :D

Also, we might have the same problem in template files, which are uncovered today too. (Not saying it belongs into this PR!).

@ehelms
ehelms force-pushed the role-scoped-variables-lint-rule branch from 05f99dc to 2875952 Compare August 10, 2026 20:09
Co-Authored-By: Claude <noreply@anthropic.com>
@ehelms
ehelms force-pushed the role-scoped-variables-lint-rule branch from 2875952 to 53a9cfa Compare August 10, 2026 20:43
@ehelms

ehelms commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

I said elsewhere that I like the rule, but dislike the code. I think it boils down to:

  • The whole need to interact with Jinja2 directly (and doing weird things for implicit/explicit templates)

I think this is unavoidable as that's what Ansible lint rules do, but I did include updates to make how it interacts more clean with the standard ansible lint rules.

  • The fact that this looks at a fixed set of places where variables can show up (loop etc), making it a bit of whack-a-mole
  • The fact that we need to ignore special terms that are no real variables (lookup etc), making it again a whack-a-mole

I wonder if there is a better way to obtain this information from Ansible (instead of Jinja). If there isn't - fine, but we should try :D

Across the board I think this should be better relying on more builtin methods from Ansibles python code.

Also, we might have the same problem in template files, which are uncovered today too. (Not saying it belongs into this PR!).

Map shared variables to role-prefixed names at playbook invocation
sites and pass them through parent roles. Use leading underscore
convention for role-internal variables (loop vars, include_tasks vars).

Co-Authored-By: Claude <noreply@anthropic.com>
@ehelms
ehelms force-pushed the role-scoped-variables-lint-rule branch from 53a9cfa to 5c4061d Compare August 11, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants