state.apply: don't check for cached pillar errors

state.apply request new pillar data from the server. This done to always
have the most up-to-date pillar to work with. Previously, checking for
pillar errors looked at both the new pillar and the in-memory pillar.
The latter might contain pillar rendering errors even if the former does
not.

For this reason, only the new pillar should be checked, not both.
This commit is contained in:
Alexander Graul 2021-11-05 17:52:12 +01:00 committed by Megan Wilhite
parent 91efaea497
commit dcfe24fed7
6 changed files with 172 additions and 81 deletions

1
changelog/52354.fixed Normal file
View file

@ -0,0 +1 @@
Don't check for cached pillar errors on state.apply

1
changelog/57180.fixed Normal file
View file

@ -0,0 +1 @@
Don't check for cached pillar errors on state.apply

1
changelog/59339.fixed Normal file
View file

@ -0,0 +1 @@
Don't check for cached pillar errors on state.apply

View file

@ -106,18 +106,17 @@ def _set_retcode(ret, highstate=None):
def _get_pillar_errors(kwargs, pillar=None): def _get_pillar_errors(kwargs, pillar=None):
""" """
Checks all pillars (external and internal) for errors. Check pillar for errors.
Return an error message, if anywhere or None.
If a pillar is passed, it will be checked. Otherwise, the in-memory pillar
will checked instead. Passing kwargs['force'] = True short cuts the check
and always returns None, indicating no errors.
:param kwargs: dictionary of options :param kwargs: dictionary of options
:param pillar: external pillar :param pillar: pillar
:return: None or an error message :return: None or a list of error messages
""" """
return ( return None if kwargs.get("force") else (pillar or __pillar__).get("_errors")
None
if kwargs.get("force")
else (pillar or {}).get("_errors", __pillar__.get("_errors")) or None
)
def _wait(jid): def _wait(jid):

View file

@ -0,0 +1,134 @@
import textwrap
import pytest
from saltfactories.utils.functional import StateResult
pytestmark = [
pytest.mark.slow_test,
]
@pytest.fixture(scope="module")
def reset_pillar(salt_call_cli):
try:
# Run tests
yield
finally:
# Refresh pillar once all tests are done.
ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True)
assert ret.exitcode == 0
assert ret.json is True
@pytest.fixture
def testfile_path(tmp_path, base_env_state_tree_root_dir):
testfile = tmp_path / "testfile"
sls_contents = textwrap.dedent(
"""
{}:
file:
- managed
- source: salt://testfile
- makedirs: true
- mode: 644
""".format(
testfile
)
)
with pytest.helpers.temp_file(
"sls-id-test.sls", sls_contents, base_env_state_tree_root_dir
):
yield testfile
@pytest.mark.usefixtures("testfile_path", "reset_pillar")
def test_state_apply_aborts_on_pillar_error(
salt_cli,
salt_minion,
base_env_pillar_tree_root_dir,
):
"""
Test state.apply with error in pillar.
"""
pillar_top_file = textwrap.dedent(
"""
base:
'{}':
- basic
""".format(
salt_minion.id
)
)
basic_pillar_file = textwrap.dedent(
"""
syntax_error
"""
)
with pytest.helpers.temp_file(
"top.sls", pillar_top_file, base_env_pillar_tree_root_dir
), pytest.helpers.temp_file(
"basic.sls", basic_pillar_file, base_env_pillar_tree_root_dir
):
expected_comment = [
"Pillar failed to render with the following messages:",
"SLS 'basic' does not render to a dictionary",
]
shell_result = salt_cli.run(
"state.apply", "sls-id-test", minion_tgt=salt_minion.id
)
assert shell_result.exitcode == 1
assert shell_result.json == expected_comment
@pytest.mark.usefixtures("testfile_path", "reset_pillar")
def test_state_apply_continues_after_pillar_error_is_fixed(
salt_cli,
salt_minion,
base_env_pillar_tree_root_dir,
):
"""
Test state.apply with error in pillar.
"""
pillar_top_file = textwrap.dedent(
"""
base:
'{}':
- basic
"""
).format(salt_minion.id)
basic_pillar_file_error = textwrap.dedent(
"""
syntax_error
"""
)
basic_pillar_file = textwrap.dedent(
"""
syntax_error: Fixed!
"""
)
# save pillar render error in minion's in-memory pillar
with pytest.helpers.temp_file(
"top.sls", pillar_top_file, base_env_pillar_tree_root_dir
), pytest.helpers.temp_file(
"basic.sls", basic_pillar_file_error, base_env_pillar_tree_root_dir
):
shell_result = salt_cli.run(
"saltutil.refresh_pillar", minion_tgt=salt_minion.id
)
assert shell_result.exitcode == 0
# run state.apply with fixed pillar render error
with pytest.helpers.temp_file(
"top.sls", pillar_top_file, base_env_pillar_tree_root_dir
), pytest.helpers.temp_file(
"basic.sls", basic_pillar_file, base_env_pillar_tree_root_dir
):
shell_result = salt_cli.run(
"state.apply", "sls-id-test", minion_tgt=salt_minion.id
)
assert shell_result.exitcode == 0
state_result = StateResult(shell_result.json)
assert state_result.result is True
assert state_result.changes == {"diff": "New file", "mode": "0644"}

View file

@ -5,11 +5,13 @@
import datetime import datetime
import logging import logging
import os import os
from collections import namedtuple
import pytest import pytest
import salt.config import salt.config
import salt.loader import salt.loader
import salt.loader.context
import salt.modules.config as config import salt.modules.config as config
import salt.modules.state as state import salt.modules.state as state
import salt.state import salt.state
@ -1201,83 +1203,36 @@ def test_lock_saltenv():
) )
def test_get_pillar_errors_CC(): PillarPair = namedtuple("PillarPair", ["in_memory", "fresh"])
""" pillar_combinations = [
Test _get_pillar_errors function. (PillarPair({"foo": "bar"}, {"fred": "baz"}), None),
CC: External clean, Internal clean (PillarPair({"foo": "bar"}, {"fred": "baz", "_errors": ["Failure"]}), ["Failure"]),
:return: (PillarPair({"foo": "bar"}, None), None),
""" (PillarPair({"foo": "bar", "_errors": ["Failure"]}, None), ["Failure"]),
for int_pillar, ext_pillar in [ (PillarPair({"foo": "bar", "_errors": ["Failure"]}, {"fred": "baz"}), None),
({"foo": "bar"}, {"fred": "baz"}), ]
({"foo": "bar"}, None),
({}, {"fred": "baz"}),
]:
with patch("salt.modules.state.__pillar__", int_pillar):
for opts, res in [
({"force": True}, None),
({"force": False}, None),
({}, None),
]:
assert res == state._get_pillar_errors(kwargs=opts, pillar=ext_pillar)
def test_get_pillar_errors_EC(): @pytest.mark.parametrize("pillar,expected_errors", pillar_combinations)
def test_get_pillar_errors(pillar: PillarPair, expected_errors):
""" """
Test _get_pillar_errors function. test _get_pillar_errors function
EC: External erroneous, Internal clean
:return:
"""
errors = ["failure", "everywhere"]
for int_pillar, ext_pillar in [
({"foo": "bar"}, {"fred": "baz", "_errors": errors}),
({}, {"fred": "baz", "_errors": errors}),
]:
with patch("salt.modules.state.__pillar__", int_pillar):
for opts, res in [
({"force": True}, None),
({"force": False}, errors),
({}, errors),
]:
assert res == state._get_pillar_errors(kwargs=opts, pillar=ext_pillar)
There are three cases to consider:
def test_get_pillar_errors_EE(): 1. kwargs['force'] is True -> None, no matter what's in pillar/__pillar__
2. pillar kwarg is available -> only check pillar, no matter what's in __pillar__
3. pillar kwarg is not available -> check __pillar__
""" """
Test _get_pillar_errors function. ctx = salt.loader.context.LoaderContext()
CC: External erroneous, Internal erroneous named_ctx = ctx.named_context("__pillar__", pillar.in_memory)
:return: with patch("salt.modules.state.__pillar__", named_ctx, create=True):
""" assert (
errors = ["failure", "everywhere"] state._get_pillar_errors(kwargs={"force": True}, pillar=pillar.fresh)
for int_pillar, ext_pillar in [ is None
({"foo": "bar", "_errors": errors}, {"fred": "baz", "_errors": errors}) )
]: assert (
with patch("salt.modules.state.__pillar__", int_pillar): state._get_pillar_errors(kwargs={}, pillar=pillar.fresh) == expected_errors
for opts, res in [ )
({"force": True}, None),
({"force": False}, errors),
({}, errors),
]:
assert res == state._get_pillar_errors(kwargs=opts, pillar=ext_pillar)
def test_get_pillar_errors_CE():
"""
Test _get_pillar_errors function.
CC: External clean, Internal erroneous
:return:
"""
errors = ["failure", "everywhere"]
for int_pillar, ext_pillar in [
({"foo": "bar", "_errors": errors}, {"fred": "baz"}),
({"foo": "bar", "_errors": errors}, None),
]:
with patch("salt.modules.state.__pillar__", int_pillar):
for opts, res in [
({"force": True}, None),
({"force": False}, errors),
({}, errors),
]:
assert res == state._get_pillar_errors(kwargs=opts, pillar=ext_pillar)
def test_event(): def test_event():