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
1 change: 1 addition & 0 deletions changelog/50755.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed saltclass leaving a literal ``^`` list-override marker in the merged pillar when a list is overridden by a single class and no existing list is present to override.
11 changes: 10 additions & 1 deletion salt/utils/saltclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,16 @@ def dict_merge(a, b, path=None):
else:
a[key] = b[key]
else:
a[key] = b[key]
# Key is absent from a. Descend into dicts so nested override
# markers are still processed, and honour a leading '^' marker on
# a list even when there is no existing list to override.
if isinstance(b[key], list) and b[key] and b[key][0] == "^":
a[key] = b[key][1:]
elif isinstance(b[key], dict):
a[key] = {}
dict_merge(a[key], b[key], path + [str(key)])
else:
a[key] = b[key]
return a


Expand Down
63 changes: 63 additions & 0 deletions tests/pytests/unit/utils/test_saltclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import salt.utils.saltclass as saltclass


def test_dict_merge_list_override_single_class_50755():
"""
A leading '^' override marker on a list must be honoured even when the
target dict has no existing list to override (single-class case).

This mirrors the production call in expanded_dict_from_minion, where the
caller does dict_merge(pillars_dict, exp_dict) with an initially empty
pillars_dict and the override list nested under the "pillars" key.
"""
assert saltclass.dict_merge({}, {"pillars": {"pkgs": ["^", "three"]}}) == {
"pillars": {"pkgs": ["three"]}
}


def test_dict_merge_list_override_deeper_nesting_50755():
"""
The override marker must be stripped for a list nested at arbitrary depth
below an absent key, not only directly under "pillars".
"""
assert saltclass.dict_merge({}, {"a": {"b": ["^", "x"]}}) == {"a": {"b": ["x"]}}


def test_dict_merge_list_override_key_present_50755():
"""
Inverse / must-not-regress: the override marker already worked when the
target dict contained a matching list. This passes with and without the
fix and guards the existing list+list code path.
"""
assert saltclass.dict_merge(
{"pillars": {"pkgs": ["one", "two"]}},
{"pillars": {"pkgs": ["^", "three"]}},
) == {"pillars": {"pkgs": ["three"]}}


def test_dict_merge_plain_list_key_absent_no_marker_50755():
"""
Inverse / must-not-regress: a marker-free list assigned into an absent key
must be copied through verbatim. Passes with and without the fix.
"""
assert saltclass.dict_merge({}, {"pkgs": ["one", "two"]}) == {
"pkgs": ["one", "two"]
}


def test_dict_merge_empty_list_key_absent_50755():
"""
Peripheral coverage: an empty list assigned into an absent key must not
raise IndexError while checking for the '^' marker.
"""
assert saltclass.dict_merge({}, {"pkgs": []}) == {"pkgs": []}


def test_dict_merge_plain_extend_key_present_50755():
"""
Peripheral coverage: marker-free lists on a present key are extended, not
replaced. This is the default (non-override) merge behaviour.
"""
assert saltclass.dict_merge({"pkgs": ["one"]}, {"pkgs": ["two"]}) == {
"pkgs": ["one", "two"]
}
Loading