Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/trellis/plugins/filter/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ def underscore(value):
''' Convert dots to underscore in a string '''
return value.replace('.', '_')

def php_extensions(value, php_version, package_state):
"""Normalize php_extensions_custom into a dict.

Accepts either a dict (passed through unchanged for back-compat) or a
list of short names. Each short name is expanded to
"php<version>-<name>": "<package_state>".
"""
if isinstance(value, dict):
return value
if isinstance(value, list):
return {f"php{php_version}-{name}": package_state for name in value}
return {}

def get_nested_attr(data, attr_path):
"""Helper to safely get a nested attribute from a dict."""
keys = attr_path.split('.')
Expand Down Expand Up @@ -58,4 +71,5 @@ def filters(self):
'select_sites': select_sites,
'to_env': to_env,
'underscore': underscore,
'php_extensions': php_extensions,
}
22 changes: 19 additions & 3 deletions roles/common/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,32 @@
msg: "{{ lookup('template', 'package_vars_wrong_format_msg.j2') }}"
when: package_vars_wrong_format | count > 0
vars:
package_vars:
# All package vars must remain dicts except php_extensions_custom,
# which additionally accepts a list of short names.
package_vars_dict_only:
apt_packages_default: "{{ apt_packages_default }}"
apt_packages_custom: "{{ apt_packages_custom }}"
memcached_packages_default: "{{ memcached_packages_default }}"
memcached_packages_custom: "{{ memcached_packages_custom }}"
php_extensions_default: "{{ php_extensions_default }}"
php_extensions_custom: "{{ php_extensions_custom }}"
sshd_packages_default: "{{ sshd_packages_default }}"
sshd_packages_custom: "{{ sshd_packages_custom }}"
package_vars_wrong_format: "{{ package_vars | dict2items | rejectattr('value', 'mapping') | map(attribute='key') | list }}"
package_vars_list_or_dict:
php_extensions_custom: "{{ php_extensions_custom }}"
# dict-only wrong: anything that is not a mapping
package_vars_dict_only_wrong: "{{ package_vars_dict_only | dict2items | rejectattr('value', 'mapping') | map(attribute='key') | list }}"
# list-or-dict wrong: not a mapping and not a (sequence that is not a string)
package_vars_list_or_dict_valid_keys: >-
{{ package_vars_list_or_dict | dict2items
| selectattr('value', 'mapping') | map(attribute='key') | list
| union(package_vars_list_or_dict | dict2items
| selectattr('value', 'sequence')
| rejectattr('value', 'string')
| map(attribute='key') | list) }}
package_vars_list_or_dict_wrong: >-
{{ package_vars_list_or_dict | dict2items | map(attribute='key') | list
| difference(package_vars_list_or_dict_valid_keys) }}
package_vars_wrong_format: "{{ package_vars_dict_only_wrong | union(package_vars_list_or_dict_wrong) }}"
tags: [memcached, php, sshd]

- name: Verify dict format for package combined variables
Expand Down
5 changes: 3 additions & 2 deletions roles/php/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ memcached_sessions: false
redis_sessions: false
redis_sessions_database: 1

php_extensions_custom: {}
php_extensions: "{{ php_extensions_default | combine(php_extensions_custom) }}"
php_extensions_custom: []
php_extensions_custom_normalized: "{{ php_extensions_custom | php_extensions(php_version, apt_package_state) }}"
php_extensions: "{{ php_extensions_default | combine(php_extensions_custom_normalized) }}"

php_error_reporting: 'E_ALL & ~E_DEPRECATED & ~E_STRICT'
php_display_errors: 'Off'
Expand Down
41 changes: 41 additions & 0 deletions tests/templates/test_php_extensions_custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Tests for the php_extensions_custom normalization (issue #1666).

The defaults file (roles/php/defaults/main.yml) calls the
`php_extensions` filter on the raw input. The filter is the single
place where list vs. dict coercion happens, so testing the filter
covers both the new list form and the legacy dict form.
"""

from lib.trellis.plugins.filter.filters import php_extensions


def test_filter_expands_list_of_short_names() -> None:
result = php_extensions(["soap", "gd"], "8.3", "present")
assert result == {
"php8.3-soap": "present",
"php8.3-gd": "present",
}


def test_filter_passes_legacy_dict_through_unchanged() -> None:
legacy = {"php8.3-soap": "present"}
assert php_extensions(legacy, "8.3", "present") is legacy


def test_filter_returns_empty_dict_for_empty_input() -> None:
assert php_extensions([], "8.3", "present") == {}


def test_filter_returns_empty_dict_for_unexpected_types() -> None:
assert php_extensions("not-a-list-or-dict", "8.3", "present") == {}
assert php_extensions(None, "8.3", "present") == {}


def test_filter_uses_provided_php_version() -> None:
result = php_extensions(["soap"], "8.2", "present")
assert result == {"php8.2-soap": "present"}


def test_filter_uses_provided_package_state() -> None:
result = php_extensions(["soap"], "8.3", "latest")
assert result == {"php8.3-soap": "latest"}