Skip to content

Commit

Permalink
Improve environment variable handling (#73)
Browse files Browse the repository at this point in the history
* Always provide metadata of ansible.builtin.

* Improve environment variable handling. Create an environment variable index.
  • Loading branch information
felixfontein authored Dec 6, 2022
1 parent 55a97a0 commit 54be8e1
Show file tree
Hide file tree
Showing 34 changed files with 491 additions and 61 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/73-env-vars.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- "Use correct markup (``envvar`` role) for environment variables. Compile an index of all environment variables used by plugins (https://github.com/ansible-community/antsibull-docs/pull/73)."
25 changes: 21 additions & 4 deletions src/antsibull_docs/cli/doc_commands/stable.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
load_all_collection_routing,
remove_redirect_duplicates,
)
from ...env_variables import load_ansible_config, collect_referenced_environment_variables
from ...schemas.docs import DOCS_SCHEMAS
from ...utils.collection_name_transformer import CollectionNameTransformer
from ...write_docs import (
Expand All @@ -47,6 +48,7 @@
output_indexes,
output_plugin_indexes,
output_extra_docs,
output_environment_variables,
)

if t.TYPE_CHECKING:
Expand Down Expand Up @@ -337,12 +339,16 @@ def generate_docs_for_all_collections(venv: t.Union[VenvRunner, FakeVenvRunner],
app_ctx = app_context.app_ctx.get()

# Get the info from the plugins
plugin_info, collection_metadata = asyncio_run(get_ansible_plugin_info(
plugin_info, full_collection_metadata = asyncio_run(get_ansible_plugin_info(
venv, collection_dir, collection_names=collection_names))
flog.notice('Finished parsing info from plugins and collections')
# flog.fields(plugin_info=plugin_info).debug('Plugin data')
# flog.fields(
# collection_metadata=collection_metadata).debug('Collection metadata')
# collection_metadata=full_collection_metadata).debug('Collection metadata')

collection_metadata = dict(full_collection_metadata)
if collection_names is not None and 'ansible.builtin' not in collection_names:
del collection_metadata['ansible.builtin']

# Load collection routing information
collection_routing = asyncio_run(load_all_collection_routing(collection_metadata))
Expand All @@ -363,7 +369,7 @@ def generate_docs_for_all_collections(venv: t.Union[VenvRunner, FakeVenvRunner],
{name: data.path for name, data in collection_metadata.items()}))
flog.debug('Finished getting collection extra docs data')

# Load collection extra docs data
# Load collection links data
link_data = asyncio_run(load_collections_links(
{name: data.path for name, data in collection_metadata.items()}))
flog.debug('Finished getting collection link data')
Expand All @@ -384,6 +390,10 @@ def generate_docs_for_all_collections(venv: t.Union[VenvRunner, FakeVenvRunner],
print(f"{plugin_name} {plugin_type}: {textwrap.indent(error, ' ').lstrip()}")
return 1

# Handle environment variables
ansible_config = load_ansible_config(full_collection_metadata['ansible.builtin'])
referenced_env_vars = collect_referenced_environment_variables(new_plugin_info, ansible_config)

collection_namespaces = get_collection_namespaces(collection_to_plugin_info.keys())

collection_url = CollectionNameTransformer(
Expand Down Expand Up @@ -444,7 +454,14 @@ def generate_docs_for_all_collections(venv: t.Union[VenvRunner, FakeVenvRunner],

asyncio_run(output_extra_docs(dest_dir, extra_docs_data,
squash_hierarchy=squash_hierarchy))
flog.debug('Finished writing extra extra docs docs')
flog.debug('Finished writing extra docs')

if referenced_env_vars:
asyncio_run(output_environment_variables(dest_dir, referenced_env_vars,
squash_hierarchy=squash_hierarchy))
flog.debug('Finished writing environment variables')
else:
flog.debug('Skipping environment variables (as there are none)')
return 0


Expand Down
9 changes: 4 additions & 5 deletions src/antsibull_docs/data/collection-enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,10 @@ def main(args):
collection_name = f'{meta["namespace"]}.{meta["name"]}'
if match_filter(collection_name, coll_filter):
result['collections'][collection_name] = meta
if match_filter('ansible.builtin', coll_filter):
result['collections']['ansible.builtin'] = {
'path': os.path.dirname(ansible_release.__file__),
'version': ansible_release.__version__,
}
result['collections']['ansible.builtin'] = {
'path': os.path.dirname(ansible_release.__file__),
'version': ansible_release.__version__,
}

print(json.dumps(
result, cls=AnsibleJSONEncoder, sort_keys=True, indent=4 if arguments.pretty else None))
Expand Down
39 changes: 39 additions & 0 deletions src/antsibull_docs/data/docsite/list_of_env_variables.rst.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{#
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later
#}

:orphan:

.. _list_of_collection_env_vars:

Index of all Collection Environment Variables
=============================================

The following index documents all environment variables declared by plugins in collections.
Environment variables used by the ansible-core configuation are documented in :ref:`ansible_configuration_settings`.
{# TODO: use label `ansible_configuration_env_vars` once the ansible-core PR is merged #}

{% for _, env_var in env_variables | dictsort %}
.. envvar:: @{ env_var.name }@

{% for paragraph in env_var.description or [] %}
@{ paragraph | replace('\n', '\n ') | rst_ify | indent(4) }@

{% endfor %}
*Used by:*
{% set plugins_ = [] %}
{% for plugin_type, plugins in env_var.plugins.items() %}
{% for plugin_name in plugins %}
{% set _ = plugins_.append((plugin_name, plugin_type)) %}
{% endfor %}
{% endfor %}
{% for plugin_name, plugin_type in plugins_ | unique | sort %}
:ref:`@{ plugin_name | rst_escape }@ {% if plugin_type == 'module' %}module{% else %}@{ plugin_type }@ plugin{% endif %} <ansible_collections.@{ plugin_name }@_@{ plugin_type }@>`
{%- if not loop.last -%}
,
{% endif -%}
{%- endfor %}

{% endfor %}
4 changes: 2 additions & 2 deletions src/antsibull_docs/data/docsite/macros/parameters.rst.j2
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
{% endfor %}
{% endif %}
{% for env in value['env'] %}
- Environment variable: @{ env['name'] | rst_escape }@
- Environment variable: :envvar:`@{ env['name'] | rst_escape(escape_ending_whitespace=true) }@`
{% if env['version_added'] is still_relevant(collection=env['version_added_collection'] or collection) %}

:ansible-option-versionadded:`added in @{ version_added_rst(env['version_added'], env['version_added_collection'] or collection) }@`
Expand Down Expand Up @@ -250,7 +250,7 @@
{% endif %}
{% for env in value['env'] %}
<li>
<p>Environment variable: @{ env['name'] | escape }@</p>
<p>Environment variable: <code class="xref std std-envvar literal notranslate">@{ env['name'] | escape }@</code></p>
{% if env['version_added'] is still_relevant(collection=env['version_added_collection'] or collection) %}
<p><span class="ansible-option-versionadded">added in @{ version_added_html(env['version_added'], env['version_added_collection'] or collection) }@</span></p>
{% endif %}
Expand Down
11 changes: 5 additions & 6 deletions src/antsibull_docs/docs_parsing/ansible_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,11 @@ def get_collection_metadata(venv: t.Union['VenvRunner', 'FakeVenvRunner'],
) -> t.Dict[str, AnsibleCollectionMetadata]:
collection_metadata = {}

# Obtain ansible.builtin version
if collection_names is None or 'ansible.builtin' in collection_names:
venv_ansible = venv.get_command('ansible')
ansible_version_cmd = venv_ansible('--version', _env=env)
raw_result = ansible_version_cmd.stdout.decode('utf-8', errors='surrogateescape')
collection_metadata['ansible.builtin'] = _extract_ansible_builtin_metadata(raw_result)
# Obtain ansible.builtin version and path
venv_ansible = venv.get_command('ansible')
ansible_version_cmd = venv_ansible('--version', _env=env)
raw_result = ansible_version_cmd.stdout.decode('utf-8', errors='surrogateescape')
collection_metadata['ansible.builtin'] = _extract_ansible_builtin_metadata(raw_result)

# Obtain collection versions
venv_ansible_galaxy = venv.get_command('ansible-galaxy')
Expand Down
5 changes: 3 additions & 2 deletions src/antsibull_docs/docs_parsing/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ async def get_ansible_plugin_info(venv: t.Union['VenvRunner', 'FakeVenvRunner'],
{information from ansible-doc --json. See the ansible-doc documentation
for more info.}
The second component is a Mapping of collection names to metadata.
The second component is a Mapping of collection names to metadata. The second mapping
always includes the metadata for ansible.builtin, even if it was not explicitly
mentioned in ``collection_names``.
"""
flog = mlog.fields(func='get_ansible_plugin_info')

Expand Down
124 changes: 124 additions & 0 deletions src/antsibull_docs/env_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Author: Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
# https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2022, Ansible Project
"""Environment variable handling."""

import os
import os.path
import typing as t

from antsibull_core import yaml

from .docs_parsing import AnsibleCollectionMetadata


class EnvironmentVariableInfo:
name: str
description: t.Optional[t.List[str]]
plugins: t.Dict[str, t.List[str]] # maps plugin_type to lists of plugin FQCNs

def __init__(self,
name: str,
description: t.Optional[t.List[str]] = None,
plugins: t.Optional[t.Dict[str, t.List[str]]] = None):
self.name = name
self.description = description
self.plugins = plugins or {}

def __repr__(self):
return f'E({self.name}, description={repr(self.description)}, plugins={self.plugins})'


def load_ansible_config(ansible_builtin_metadata: AnsibleCollectionMetadata
) -> t.Mapping[str, t.Mapping[str, t.Any]]:
"""
Load Ansible base configuration (``lib/ansible/config/base.yml``).
:arg ansible_builtin_metadata: Metadata for the ansible.builtin collection.
:returns: A Mapping of configuration options to information on these options.
"""
return yaml.load_yaml_file(os.path.join(ansible_builtin_metadata.path, 'config', 'base.yml'))


def _find_env_vars(options: t.Mapping[str, t.Mapping[str, t.Any]]
) -> t.Generator[t.Tuple[str, t.Optional[t.List[str]]], None, None]:
for _, option_data in options.items():
if isinstance(option_data.get('env'), list):
description = option_data.get('description')
if isinstance(description, str):
description = [description]
if isinstance(description, list):
description = [str(desc) for desc in description]
else:
description = None
for env_var in option_data['env']:
if isinstance(env_var.get('name'), str):
yield (env_var['name'], description)
if isinstance(option_data.get('suboptions'), dict):
yield from _find_env_vars(option_data['suboptions'])


def _collect_env_vars_and_descriptions(plugin_info: t.Mapping[str, t.Mapping[str, t.Any]],
core_envs: t.Set[str],
) -> t.Tuple[t.Mapping[str, EnvironmentVariableInfo],
t.Mapping[str, t.List[t.List[str]]]]:
other_variables: t.Dict[str, EnvironmentVariableInfo] = {}
other_variable_description: t.Dict[str, t.List[t.List[str]]] = {}
for plugin_type, plugins in plugin_info.items():
for plugin_name, plugin_data in plugins.items():
plugin_options: t.Mapping[str, t.Mapping[str, t.Any]] = (
(plugin_data.get('doc') or {}).get('options') or {}
)
for env_var, env_var_description in _find_env_vars(plugin_options):
if env_var in core_envs:
continue
if env_var not in other_variables:
other_variables[env_var] = EnvironmentVariableInfo(env_var)
other_variable_description[env_var] = []
if plugin_type not in other_variables[env_var].plugins:
other_variables[env_var].plugins[plugin_type] = []
other_variables[env_var].plugins[plugin_type].append(plugin_name)
if env_var_description is not None:
other_variable_description[env_var].append(env_var_description)
return other_variables, other_variable_description


def _augment_env_var_descriptions(other_variables: t.Mapping[str, EnvironmentVariableInfo],
other_variable_description: t.Mapping[str, t.List[t.List[str]]],
) -> None:
for variable, variable_info in other_variables.items():
if other_variable_description[variable]:
value: t.Optional[t.List[str]] = other_variable_description[variable][0]
for other_value in other_variable_description[variable]:
if value != other_value:
value = [
'See the documentations for the options where this environment variable'
' is used.'
]
break
variable_info.description = value


def collect_referenced_environment_variables(plugin_info: t.Mapping[str, t.Mapping[str, t.Any]],
ansible_config: t.Mapping[str, t.Mapping[str, t.Any]],
) -> t.Mapping[str, EnvironmentVariableInfo]:
"""
Collect referenced environment variables that are not defined in the ansible-core
configuration.
:arg plugin_info: Mapping of plugin type to a mapping of plugin name to plugin record.
:arg ansible_config: The Ansible base configuration (``lib/ansible/config/base.yml``).
:returns: A Mapping of environment variable name to an environment variable infomation object.
"""
core_envs = {'ANSIBLE_CONFIG'}
for config in ansible_config.values():
if config.get('env'):
for env in config['env']:
core_envs.add(env['name'])

other_variables, other_variable_description = _collect_env_vars_and_descriptions(
plugin_info, core_envs)
_augment_env_var_descriptions(other_variables, other_variable_description)
return other_variables
48 changes: 46 additions & 2 deletions src/antsibull_docs/write_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

from .jinja2.environment import doc_environment
from .collection_links import CollectionLinks
from .extra_docs import CollectionExtraDocsInfoT
from .docs_parsing import AnsibleCollectionMetadata
from .env_variables import EnvironmentVariableInfo
from .extra_docs import CollectionExtraDocsInfoT
from .utils.collection_name_transformer import CollectionNameTransformer


Expand Down Expand Up @@ -907,7 +908,7 @@ async def output_extra_docs(dest_dir: str,
extra_docs_data: t.Mapping[str, CollectionExtraDocsInfoT],
squash_hierarchy: bool = False) -> None:
"""
Generate collection-level index pages for the collections.
Write extra docs pages for the collections.
:arg dest_dir: The directory to place the documentation in.
:arg extra_docs_data: Dictionary mapping collection names to CollectionExtraDocsInfoT.
Expand Down Expand Up @@ -940,3 +941,46 @@ async def output_extra_docs(dest_dir: str,
await asyncio.gather(*writers)

flog.debug('Leave')


async def output_environment_variables(dest_dir: str,
env_variables: t.Mapping[str, EnvironmentVariableInfo],
squash_hierarchy: bool = False
) -> None:
"""
Write environment variable Generate collection-level index pages for the collections.
:arg dest_dir: The directory to place the documentation in.
:arg env_variables: Mapping of environment variable names to environment variable information.
:arg squash_hierarchy: If set to ``True``, no directory hierarchy will be used.
Undefined behavior if documentation for multiple collections are
created.
"""
flog = mlog.fields(func='write_environment_variables')
flog.debug('Enter')

if not squash_hierarchy:
collection_toplevel = os.path.join(dest_dir, 'collections')
else:
collection_toplevel = dest_dir

env = doc_environment(('antsibull_docs.data', 'docsite'))
# Get the templates
env_var_list_tmpl = env.get_template('list_of_env_variables.rst.j2')

flog.fields(toplevel=collection_toplevel, exists=os.path.isdir(collection_toplevel)).debug(
'collection_toplevel exists?')
# This is only safe because we made sure that the top of the directory tree we're writing to
# (docs/docsite/rst) is only writable by us.
os.makedirs(collection_toplevel, mode=0o755, exist_ok=True)

index_file = os.path.join(collection_toplevel, 'environment_variables.rst')
index_contents = _render_template(
env_var_list_tmpl,
index_file,
env_variables=env_variables,
)

await write_file(index_file, index_contents)

flog.debug('Leave')
Loading

0 comments on commit 54be8e1

Please sign in to comment.