From 764efd360ca99b51bdec454a3a458a387c594524 Mon Sep 17 00:00:00 2001 From: Yusuke Matsubara Date: Thu, 26 Jun 2025 20:45:00 +0900 Subject: [PATCH] Hide stack trace shown on YAML parse error by default Fixes https://github.com/containers/podman-compose/issues/1139 Signed-off-by: Yusuke Matsubara --- .../hide-stack-trace-yaml-parse-error.change | 1 + podman_compose.py | 15 ++++- .../parse-error/docker-compose-error.yml | 4 ++ .../parse-error/docker-compose.yml | 4 ++ .../test_podman_compose_parse_error.py | 66 +++++++++++++++++++ 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 newsfragments/hide-stack-trace-yaml-parse-error.change create mode 100644 tests/integration/parse-error/docker-compose-error.yml create mode 100644 tests/integration/parse-error/docker-compose.yml create mode 100644 tests/integration/parse-error/test_podman_compose_parse_error.py diff --git a/newsfragments/hide-stack-trace-yaml-parse-error.change b/newsfragments/hide-stack-trace-yaml-parse-error.change new file mode 100644 index 0000000..8cb933e --- /dev/null +++ b/newsfragments/hide-stack-trace-yaml-parse-error.change @@ -0,0 +1 @@ +Hide the stack trace on a YAML parse error. diff --git a/podman_compose.py b/podman_compose.py index f7c2682..f710a28 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -1904,6 +1904,15 @@ def rec_merge(target: dict[str, Any], *sources: dict[str, Any]) -> dict[str, Any return ret +def load_yaml_or_die(file_path: str, stream: Any) -> dict[str, Any]: + try: + return yaml.safe_load(stream) + except yaml.scanner.ScannerError as e: + log.fatal("Compose file contains an error:\n%s", e) + log.info("Compose file %s contains an error:", file_path, exc_info=e) + sys.exit(1) + + def resolve_extends( services: dict[str, Any], service_names: list[str], environ: dict[str, Any] ) -> None: @@ -1920,7 +1929,7 @@ def resolve_extends( if filename.startswith("./"): filename = filename[2:] with open(filename, "r", encoding="utf-8") as f: - content = yaml.safe_load(f) or {} + content = load_yaml_or_die(filename, f) or {} if "services" in content: content = content["services"] subdirectory = os.path.dirname(filename) @@ -2229,10 +2238,10 @@ class PodmanCompose: break if filename.strip().split('/')[-1] == '-': - content = yaml.safe_load(sys.stdin) + content = load_yaml_or_die(filename, sys.stdin) else: with open(filename, "r", encoding="utf-8") as f: - content = yaml.safe_load(f) + content = load_yaml_or_die(filename, f) # log(filename, json.dumps(content, indent = 2)) if not isinstance(content, dict): sys.stderr.write( diff --git a/tests/integration/parse-error/docker-compose-error.yml b/tests/integration/parse-error/docker-compose-error.yml new file mode 100644 index 0000000..49aa875 --- /dev/null +++ b/tests/integration/parse-error/docker-compose-error.yml @@ -0,0 +1,4 @@ +version: "3" +services:foo + web1: + image: busybox diff --git a/tests/integration/parse-error/docker-compose.yml b/tests/integration/parse-error/docker-compose.yml new file mode 100644 index 0000000..55c46ec --- /dev/null +++ b/tests/integration/parse-error/docker-compose.yml @@ -0,0 +1,4 @@ +version: "3" +services: + web1: + image: busybox diff --git a/tests/integration/parse-error/test_podman_compose_parse_error.py b/tests/integration/parse-error/test_podman_compose_parse_error.py new file mode 100644 index 0000000..e69614b --- /dev/null +++ b/tests/integration/parse-error/test_podman_compose_parse_error.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: GPL-2.0 + +import os +import unittest + +from tests.integration.test_utils import RunSubprocessMixin +from tests.integration.test_utils import podman_compose_path +from tests.integration.test_utils import test_path + + +def bad_compose_yaml_path() -> str: + base_path = os.path.join(test_path(), "parse-error") + return os.path.join(base_path, "docker-compose-error.yml") + + +def good_compose_yaml_path() -> str: + base_path = os.path.join(test_path(), "parse-error") + return os.path.join(base_path, "docker-compose.yml") + + +class TestComposeBuildParseError(unittest.TestCase, RunSubprocessMixin): + def test_no_error(self) -> None: + try: + _, err = self.run_subprocess_assert_returncode( + [podman_compose_path(), "-f", good_compose_yaml_path(), "config"], 0 + ) + self.assertEqual(b"", err) + + finally: + self.run_subprocess([ + podman_compose_path(), + "-f", + bad_compose_yaml_path(), + "down", + ]) + + def test_simple_parse_error(self) -> None: + try: + _, err = self.run_subprocess_assert_returncode( + [podman_compose_path(), "-f", bad_compose_yaml_path(), "config"], 1 + ) + self.assertIn(b"could not find expected ':'", err) + self.assertNotIn(b"\nTraceback (most recent call last):\n", err) + + finally: + self.run_subprocess([ + podman_compose_path(), + "-f", + bad_compose_yaml_path(), + "down", + ]) + + def test_verbose_parse_error_contains_stack_trace(self) -> None: + try: + _, err = self.run_subprocess_assert_returncode( + [podman_compose_path(), "--verbose", "-f", bad_compose_yaml_path(), "config"], 1 + ) + self.assertIn(b"\nTraceback (most recent call last):\n", err) + + finally: + self.run_subprocess([ + podman_compose_path(), + "-f", + bad_compose_yaml_path(), + "down", + ])