diff --git a/.github/.copilot-collections.yaml b/.github/.copilot-collections.yaml index e09312f3a..887b507f0 100644 --- a/.github/.copilot-collections.yaml +++ b/.github/.copilot-collections.yaml @@ -4,4 +4,4 @@ copilot: version: v0.12.0 collections: - starcraft-core - - starcraft-docs \ No newline at end of file + - starcraft-docs diff --git a/.github/workflows/copilot-collections-update.yaml b/.github/workflows/copilot-collections-update.yaml index 9d57e3ee6..df28b1aff 100644 --- a/.github/workflows/copilot-collections-update.yaml +++ b/.github/workflows/copilot-collections-update.yaml @@ -8,4 +8,4 @@ jobs: check-update: # Pinned to @main to get the latest logic, but the content version is controlled by .github/.copilot-collections.yaml uses: canonical/copilot-collections/.github/workflows/auto_update_collections.yaml@main - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 4b61ab04e..39d2f02aa 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -20,6 +20,7 @@ from pathlib import Path from typing import Any +import yaml from overrides import override from ..errors import ExtensionError @@ -71,6 +72,8 @@ } COS_SUBDIRS = {"grafana_dashboards", "loki_alert_rules", "prometheus_alert_rules"} +PAAS_CONFIG_FILE = "paas-config.yaml" +JSON_LOGGING_SUPPORTED_FRAMEWORKS = {"fastapi", "flask", "django"} class _AppBase(Extension): @@ -194,6 +197,34 @@ def _check_input(self) -> None: "Non-optional configuration options can not have default values.\n" f"Please either remove the default value or set optional field to true or remove it for the {', '.join(invalid_non_optionals)} configuration option(s)." ) + self._check_paas_config() + + def _check_paas_config(self) -> None: + """Validate ``paas-config.yaml`` syntax and framework logging compatibility.""" + config_path = self.project_root / PAAS_CONFIG_FILE + if not config_path.exists(): + return + + try: + parsed = yaml.safe_load(config_path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + raise ExtensionError(f"invalid YAML in {PAAS_CONFIG_FILE}: {exc}") from exc + + if parsed is None: + return + if not isinstance(parsed, dict): + raise ExtensionError(f"{PAAS_CONFIG_FILE} must contain a top-level mapping") + + framework_logging_format = parsed.get("framework_logging_format") + if ( + isinstance(framework_logging_format, str) + and framework_logging_format.lower() == "json" + and self.framework not in JSON_LOGGING_SUPPORTED_FRAMEWORKS + ): + raise ExtensionError( + f"framework_logging_format: json in {PAAS_CONFIG_FILE} is not supported " + f"for '{self.framework}-framework'" + ) def _validate_cos_custom_dir(self) -> None: """Validate the custom COS directory if present.""" diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 6854ff5eb..17778a1e7 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -687,6 +687,46 @@ def test_handle_charm_part_adds_part(flask_input_yaml, tmp_path): } +def test_invalid_paas_config_yaml_fails_pack(flask_input_yaml, tmp_path): + (tmp_path / "paas-config.yaml").write_text( + "framework_logging_format: [json", encoding="utf-8" + ) + + with pytest.raises(ExtensionError, match=r"invalid YAML in paas-config.yaml"): + extensions.apply_extensions(tmp_path, flask_input_yaml) + + +def test_json_framework_logging_disallowed_framework_fails_pack(tmp_path): + go_input_yaml = { + "type": "charm", + "name": "test-go", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@24.04", + "platforms": { + "amd64": None, + }, + "extensions": ["go-framework"], + "config": NON_OPTIONAL_OPTIONS, + } + (tmp_path / "paas-config.yaml").write_text( + "framework_logging_format: json\n", encoding="utf-8" + ) + + with pytest.raises( + ExtensionError, + match=r"framework_logging_format: json in paas-config.yaml is not supported for 'go-framework'", + ): + extensions.apply_extensions(tmp_path, go_input_yaml) + + +def test_json_framework_logging_supported_framework_passes(flask_input_yaml, tmp_path): + (tmp_path / "paas-config.yaml").write_text( + "framework_logging_format: json\n", encoding="utf-8" + ) + extensions.apply_extensions(tmp_path, flask_input_yaml) + + @pytest.mark.parametrize( ("input_yaml", "requires", "expected_options"), [