Skip to content

Fix minion crash when grains config option is empty - #69891

Merged
dwoz merged 3 commits into
saltstack:3006.xfrom
BrianHa94:fix-61321-minion-crash-on-invalid-empty-grains
Aug 11, 2026
Merged

Fix minion crash when grains config option is empty#69891
dwoz merged 3 commits into
saltstack:3006.xfrom
BrianHa94:fix-61321-minion-crash-on-invalid-empty-grains

Conversation

@BrianHa94

@BrianHa94 BrianHa94 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a minion crash on startup caused by a grains: config option that isn't a mapping. YAML parses grains: with no value as None, and non-dict values like a string, number, or list are also possible — two separate code paths (apply_minion_config and salt.loader.grains(), which independently re-reads the raw config file off disk) assumed the option was always a dict. That crashed the minion with TypeError: 'NoneType' object is not iterable when building the __grains__ context wrapper.

Both locations now check isinstance(value, dict) and silently default the option to an empty dict ({}) for any non-dict value, not just None. The required shape of grains is documented in conf/minion and doc/ref/configuration/minion.rst rather than surfaced as a runtime warning.

What issues does this PR fix or reference?

Fixes #61321

Previous Behavior

A minion configured with a grains: config option that wasn't a mapping (e.g. grains: with no value, an empty string, or a scalar) would crash on startup with:

TypeError: 'NoneType' object is not iterable

raised from salt.utils.context.NamespacedDictWrapper.__init__ via the loader's __prep_mod_opts.

New Behavior

The minion starts normally. Any non-dict grains value is silently treated as {}. The required shape (grains must be a mapping; use grains: {} for an explicit empty section) is documented in the minion configuration reference and the example minion config file.

Merge requirements satisfied?

Unit tests in tests/pytests/unit/test_config.py and tests/pytests/unit/loader/test_loader.py, plus an integration test in tests/pytests/integration/cli/test_salt_minion.py, all parametrized over a range of non-dict grains values (None, empty string, empty list, string, number, list).

Commits signed with GPG?

No

@BrianHa94
BrianHa94 requested a review from a team as a code owner July 28, 2026 05:34
@BrianHa94 BrianHa94 added the test:full Run the full test suite label Jul 28, 2026
@BrianHa94
BrianHa94 force-pushed the fix-61321-minion-crash-on-invalid-empty-grains branch from 378f500 to 3bb5529 Compare July 28, 2026 05:43
@BrianHa94 BrianHa94 added test:full Run the full test suite and removed test:full Run the full test suite labels Jul 28, 2026
Comment thread salt/config/__init__.py Outdated
Comment thread tests/pytests/integration/cli/test_salt_minion.py Outdated

@dwoz dwoz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not isinstance(opts["grains"], dict) -- catches empty and wrong-type in one guard. No valid grains is anything but a mapping. Drop the warning; document the shape.

conf/minion above #grains::

+#
+# The value of 'grains' must be a mapping. Use 'grains: {}' for an
+# explicit empty section. Any non-dict value is silently defaulted
+# to an empty dict.
 #grains:

doc/ref/configuration/minion.rst inside the grains entry, after the example:

+The value of ``grains`` must be a mapping. Use ``grains: {}`` for an
+explicit empty section. Any non-dict value is silently defaulted to
+an empty dict.
+
 .. conf_minion:: grains_blacklist

Tests: parametrize over ["", '""', "[]", "foo", "42", "[1, 2]"] (all defaulting to {}) in tests/unit/test_config.py + tests/pytests/unit/loader/test_loader.py; same for tests/pytests/integration/cli/test_salt_minion.py with [None, "", [], "foo", 42, [1, 2]], drop the log-file assertions and the import salt.utils.files.

Comment thread salt/config/__init__.py Outdated
Comment thread salt/loader/__init__.py Outdated
Comment thread changelog/61321.fixed.md Outdated
An empty 'grains:' config option parses to None instead of a dict,
which crashed the minion during startup with "TypeError: 'NoneType'
object is not iterable" when the loader tried to build the
__grains__ NamespacedDictWrapper.

Default the option to an empty dict in both places that read it:
apply_minion_config, and salt.loader.grains(), which independently
re-reads the raw config file off disk. Log a warning in each case
pointing out that 'grains: {}' should be used instead.

Fixes saltstack#61321
Only warn/default the 'grains' config option when it is explicitly
present and None, not merely absent from opts. The looser
opts.get("grains") is None check could not distinguish "grains: set
to empty" from "grains key never set at all" (e.g. a sparse defaults
dict passed into apply_minion_config without a 'grains' key), causing
a false-positive warning in that case. This matches the equivalent
check already used in salt.loader.grains().

Also move log_file = factory.config["log_file"] in the new
integration test out of the try block, since it doesn't depend on the
test.ping call succeeding and reads more clearly next to the log file
assertions it's used for.
Broaden the empty-grains guards in apply_minion_config and
salt.loader.grains() from an explicit None check to
isinstance(value, dict), so any non-mapping value (empty string,
list, scalar, ...) is defaulted to an empty dict, not just an
explicitly empty 'grains:' key. Drop the runtime warning in favor of
documenting the required shape in conf/minion and
doc/ref/configuration/minion.rst.

Parametrize the existing tests over a range of non-dict grains values
instead of just None, and drop the now-irrelevant log/warning
assertions.
@welcome

welcome Bot commented Aug 11, 2026

Copy link
Copy Markdown

Congratulations on your first PR being merged! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:full Run the full test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants