Skip to content

fix(util): skip jinja config files which fail to render - #7008

Open
rajibade wants to merge 1 commit into
canonical:mainfrom
rajibade:fix-config-crash
Open

fix(util): skip jinja config files which fail to render#7008
rajibade wants to merge 1 commit into
canonical:mainfrom
rajibade:fix-config-crash

Conversation

@rajibade

@rajibade rajibade commented Aug 16, 2026

Copy link
Copy Markdown

Proposed Commit Message

fix(util): skip jinja template config files which fail to render

read_conf doesn't handle the None return value 
from render_jinja_payload_from_file() when a
template render fails. This gets passed into
load_yaml which results in an AttributeError on 
NoneType and crashes the running service.

This check was removed from #5350 as unreachable 
due to the render functions not having any return 
annotations. Adding Optional[str] toavoid this being 
caught again and fix the schema to check None return.

Fixes GH-7007

Additional Context

Test Validation

Reproducer in GH issue above.

Test output with current branch

~/cloud-init fix-config-crash ?1 ❯ tox -e py3 -- tests/unittests/test_util.py -k unrendered 

tests/unittests/test_util.py::TestUtil::test_read_conf_with_config_unrendered_template FAILED                                                                                                                  [100%]

====================================================================================================== FAILURES ======================================================================================================
______________________________________________________________________________ TestUtil.test_read_conf_with_config_unrendered_template _______________________________________________________________________________

self = <tests.unittests.test_util.TestUtil object at 0x10537a780>, mocker = <pytest_mock.plugin.MockerFixture object at 0x1053b9a90>, caplog = <_pytest.logging.LogCaptureFixture object at 0x1053b9e80>

>   ???

tests/unittests/test_util.py:569:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cloudinit/util.py:369: in read_conf
    return load_yaml(config_file, default={})  # pyright: ignore
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cloudinit/util.py:983: in load_yaml
    blob = decode_binary(blob)
           ^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

blob = None, encoding = 'utf-8'

    def decode_binary(blob: Union[str, bytes], encoding="utf-8") -> str:
        # Converts a binary type into a text type using given encoding.
>       return blob if isinstance(blob, str) else blob.decode(encoding=encoding)
                                                  ^^^^^^^^^^^
E       AttributeError: 'NoneType' object has no attribute 'decode'

cloudinit/util.py:143: AttributeError
------------------------------------------------------------------------------------------------- Captured log call --------------------------------------------------------------------------------------------------
2026-08-16 13:46:55 WARNING   cloudinit.handlers.jinja_template:jinja_template.py:150 Ignoring jinja template for cfg_path: 'dict object' has no attribute 'c'
2026-08-16 13:46:55 DEBUG     cloudinit.util:util.py:345 Applied instance data in 'vars_path' to configuration loaded from 'cfg_path'


After fix

tests/unittests/test_util.py::TestUtil::test_read_conf_with_config_unrendered_template PASSED    [100%]

Merge type

  • Squash merge using "Proposed Commit Message"
  • Rebase and merge unique commits. Requires commit messages per-commit each referencing the pull request number (#<PR_NUM>)

Comment thread cloudinit/util.py
fname,
instance_data_file,
)
if rendered is None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the function returns None, would it have already logged an error?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yup here it does.

LOG.warning("Ignoring jinja template for %s: %s", payload_fn, str(e))

@holmanb holmanb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please run tox -e do_format to fix the formatting error.

@rajibade

rajibade commented Aug 17, 2026

Copy link
Copy Markdown
Author

Please run tox -e do_format to fix the formatting error.

So I did but its another mypy issue that was caught by my changes. Seems the schema also doesn't handle the None return value as well. I'll add a fix in a new commit

@rajibade
rajibade requested a review from holmanb August 17, 2026 21:36
@rajibade

Copy link
Copy Markdown
Author

hmm not sure the reason for the failing docs check

@rajibade

Copy link
Copy Markdown
Author

Oh pylint doesn't play well with the overloading.

@holmanb holmanb self-assigned this Aug 18, 2026
Comment thread cloudinit/config/schema.py Outdated
)
if rendered is None:
error("Failed to render templated user-data.", sys_exit=True)
raise SystemExit(1) # error() exits here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why?

@rajibade rajibade Aug 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

why i'm checking in schema? it didn't handle the None return like the original read_conf function

For the raise, okay so mypy complains because error() doesn't have a no return annotation that can be checked when sys_exit=True happens. If I didn't terminate here, the content = rendered assignment below will get a type checking error as well (after I annotated the jinja templating function to fix another mypy checker).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Would this be a better fit?

Suggested change
raise SystemExit(1) # error() exits here
raise SystemExit(error("Failed to render templated user-data."))

@rajibade rajibade Aug 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I tried to add No return overloads to the error function as well but pylint kinda lit up and it was probably touching more files than needed. So I dropped it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Trying to render a jinja template file with nested instance-data attributes that isn't provided by the active DataSource fails the init stage and prevent cloud-init from starting up.

How does throwing an exception here fix the reported bug? Even the PR title says "skip jinja config files which fail to render", yet this proposed code throws an exception which crashes the program.

@rajibade rajibade Aug 20, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is not where the bug happens. The bug was fixed in util.read_conf here.

This exception thrown here is a change made as a consequence of fixing the real bug. The cloud-init schema CLI has a tool that also does jinja-template/cloud-config validation. It calls the same jinja render function that util.read_conf does and it also didn't handle the None return from the renderer. I found this out because after annotating render_jinja_payload_from_file with Optional[str], I got a complaint from mypy that there was another call site not properly handling the return.

So this other proposed code doesn't crash the program because it is a CLI command that only validates and isn't part of the running service, but it still needs to cleanly error out and not have a traceback. The service itself will only log on exceptions and continue the program.

Without the schema fix

root@localhost ~]# sudo cloud-init schema --config-file /etc/cloud/cloud.cfg.d/10_test.cfg
2026-08-20 19:47:46,407 - jinja_template.py[WARNING]: Ignoring jinja template for /etc/cloud/cloud.cfg.d/10_test.cfg: 'dict object' has no attribute 'placement'
2026-08-20 19:47:46,407 - util.py[WARNING]: Skipping jinja config file '/etc/cloud/cloud.cfg.d/10_test.cfg'. Failed to render template.
2026-08-20 19:47:46,417 - jinja_template.py[WARNING]: Ignoring jinja template for /etc/cloud/cloud.cfg.d/10_test.cfg: 'dict object' has no attribute 'placement'
2026-08-20 19:47:46,417 - util.py[WARNING]: Skipping jinja config file '/etc/cloud/cloud.cfg.d/10_test.cfg'. Failed to render template.
2026-08-20 19:47:46,421 - jinja_template.py[WARNING]: Ignoring jinja template for /etc/cloud/cloud.cfg.d/10_test.cfg: 'dict object' has no attribute 'placement'
Traceback (most recent call last):
  File "/usr/sbin/cloud-init", line 9, in <module>
    sys.exit(main.main())
             ~~~~~~~~~^^
  File "/usr/lib/python3.14/site-packages/cloudinit/cmd/main.py", line 1310, in main
    return sub_main(args, parser)
  File "/usr/lib/python3.14/site-packages/cloudinit/cmd/main.py", line 1448, in sub_main
    retval = functor(name, args)
  File "/usr/lib/python3.14/site-packages/cloudinit/config/schema.py", line 1478, in handle_schema_args
    performed_schema_validation = validate_cloudconfig_file(
        cfg_part.config_path,
    ...<3 lines>...
        instance_data_path,
    )
  File "/usr/lib/python3.14/site-packages/cloudinit/config/schema.py", line 1123, in validate_cloudconfig_file
    decoded_config = _get_config_type_and_rendered_userdata(
        config_path, decoded_content, instance_data_path
    )
  File "/usr/lib/python3.14/site-packages/cloudinit/config/schema.py", line 1065, in _get_config_type_and_rendered_userdata
    user_data_type = type_from_starts_with(content)
  File "/usr/lib/python3.14/site-packages/cloudinit/handlers/__init__.py", line 293, in type_from_starts_with
    payload_lc = util.decode_binary(payload).lower()
                 ~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/usr/lib/python3.14/site-packages/cloudinit/util.py", line 143, in decode_binary
    return blob if isinstance(blob, str) else blob.decode(encoding=encoding)
                                              ^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'decode'
[root@localhost ~]#

With the schema fix

[root@localhost ~]# sudo cloud-init schema --config-file /etc/cloud/cloud.cfg.d/10_test.cfg
2026-08-20 19:44:04,811 - jinja_template.py[WARNING]: Ignoring jinja template for /etc/cloud/cloud.cfg.d/10_test.cfg: 'dict object' has no attribute 'placement'
2026-08-20 19:44:04,811 - util.py[WARNING]: Skipping jinja config file '/etc/cloud/cloud.cfg.d/10_test.cfg. Failed to render template.
2026-08-20 19:44:04,821 - jinja_template.py[WARNING]: Ignoring jinja template for /etc/cloud/cloud.cfg.d/10_test.cfg: 'dict object' has no attribute 'placement'
2026-08-20 19:44:04,821 - util.py[WARNING]: Skipping jinja config file '/etc/cloud/cloud.cfg.d/10_test.cfg'. Failed to render template.
2026-08-20 19:44:04,825 - jinja_template.py[WARNING]: Ignoring jinja template for /etc/cloud/cloud.cfg.d/10_test.cfg: 'dict object' has no attribute 'placement'
Error:
Failed to render templated user-data.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I can make updates to the PR description to make this more clearer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In any case, throwing a SystemExit in runtime code is still not what we want - this isn't a pattern used anywhere else.

You can satisfy mypy without throwing the exception by conditionally assigning only when it has a value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks. That makes sense. i'll update it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

updated @holmanb

@rajibade
rajibade requested a review from holmanb August 19, 2026 22:41
read_conf doesn't handle the None return value from
render_jinja_payload_from_file() when a template render fails. This gets
passed into load_yaml which results in an AttributeError on NoneType and
crashes the running service.

This check was removed from canonical#5350 as unreachable due to the render
functions not having any return annotations. Adding Optional[str] to
avoid this being caught again and fix the schema to check None return.

Fixes canonicalGH-7007
@rajibade

Copy link
Copy Markdown
Author

All good on here @holmanb?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants