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):
"""
Checks all pillars (external and internal) for errors.
Return an error message, if anywhere or None.
Check pillar for errors.
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 pillar: external pillar
:return: None or an error message
:param pillar: pillar
:return: None or a list of error messages
"""
return (
None
if kwargs.get("force")
else (pillar or {}).get("_errors", __pillar__.get("_errors")) or None
)
return None if kwargs.get("force") else (pillar or __pillar__).get("_errors")
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 logging
import os
from collections import namedtuple
import pytest
import salt.config
import salt.loader
import salt.loader.context
import salt.modules.config as config
import salt.modules.state as state
import salt.state
@ -1201,83 +1203,36 @@ def test_lock_saltenv():
)
def test_get_pillar_errors_CC():
"""
Test _get_pillar_errors function.
CC: External clean, Internal clean
:return:
"""
for int_pillar, ext_pillar in [
({"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)
PillarPair = namedtuple("PillarPair", ["in_memory", "fresh"])
pillar_combinations = [
(PillarPair({"foo": "bar"}, {"fred": "baz"}), None),
(PillarPair({"foo": "bar"}, {"fred": "baz", "_errors": ["Failure"]}), ["Failure"]),
(PillarPair({"foo": "bar"}, None), None),
(PillarPair({"foo": "bar", "_errors": ["Failure"]}, None), ["Failure"]),
(PillarPair({"foo": "bar", "_errors": ["Failure"]}, {"fred": "baz"}), None),
]
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.
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)
test _get_pillar_errors function
def test_get_pillar_errors_EE():
There are three cases to consider:
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.
CC: External erroneous, Internal erroneous
:return:
"""
errors = ["failure", "everywhere"]
for int_pillar, ext_pillar in [
({"foo": "bar", "_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)
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)
ctx = salt.loader.context.LoaderContext()
named_ctx = ctx.named_context("__pillar__", pillar.in_memory)
with patch("salt.modules.state.__pillar__", named_ctx, create=True):
assert (
state._get_pillar_errors(kwargs={"force": True}, pillar=pillar.fresh)
is None
)
assert (
state._get_pillar_errors(kwargs={}, pillar=pillar.fresh) == expected_errors
)
def test_event():