Skip to content
29 changes: 29 additions & 0 deletions charmcraft/extensions/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"""Gunicorn based extensions."""

import copy
from pathlib import Path
from typing import Any

from overrides import override
Expand Down Expand Up @@ -69,6 +70,8 @@
},
}

COS_SUBDIRS = {"grafana_dashboards", "loki_alert_rules", "prometheus_alert_rules"}


class _AppBase(Extension):
"""A base class for 12-factor applications."""
Expand Down Expand Up @@ -129,6 +132,7 @@ def _check_input(self) -> None:
f"the '{self.framework}-framework' extension is incompatible with "
f"type {charm_type!r}"
)
self._validate_cos_custom_dir()
parts = self.yaml_data.get("parts")
if parts and "charm" in parts:
raise ExtensionError(
Expand Down Expand Up @@ -191,6 +195,31 @@ def _check_input(self) -> None:
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)."
)

def _validate_cos_custom_dir(self) -> None:
"""Validate the custom COS directory if present."""
custom_dir = Path(self.project_root) / "cos_custom"
if not custom_dir.is_dir():
return
root_files: list[str] = []
invalid_dirs: list[str] = []

for entry in custom_dir.iterdir():
if entry.is_file():
root_files.append(entry.name)
elif entry.is_dir() and entry.name not in COS_SUBDIRS:
invalid_dirs.append(entry.name)

if root_files or invalid_dirs:
details: list[str] = []
if root_files:
details.append("root files: " + ", ".join(root_files))
if invalid_dirs:
details.append("invalid subdirectories: " + ", ".join(invalid_dirs))
Comment thread
lengau marked this conversation as resolved.
Comment thread
lengau marked this conversation as resolved.
raise ExtensionError(
"custom COS directory must only contain the following subdirectories: "
f"{COS_SUBDIRS}. Found {'; '.join(details)}"
Comment thread
lengau marked this conversation as resolved.
)

def _get_root_snippet(self) -> dict[str, Any]:
"""Return the root snippet to be merged into the user charmcraft.yaml.

Expand Down
40 changes: 40 additions & 0 deletions tests/extensions/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#
# For further info, check https://github.com/canonical/charmcraft
import copy
import pathlib

import pytest

Expand Down Expand Up @@ -627,6 +628,45 @@ def test_flask_incompatible_fields(modification, flask_input_yaml, tmp_path):
extensions.apply_extensions(tmp_path, copy.deepcopy(charm))


def test_cos_custom_rejects_root_file(flask_input_yaml, tmp_path: pathlib.Path):
cos_dir = tmp_path / "cos_custom"
cos_dir.mkdir()
(cos_dir / "bad.txt").write_text("oops")

with pytest.raises(ExtensionError) as exc:
extensions.apply_extensions(tmp_path, flask_input_yaml)

error = str(exc.value)
assert "custom COS directory must only contain" in error
assert "root files: bad.txt" in error


def test_cos_custom_rejects_unknown_subdir(flask_input_yaml, tmp_path: pathlib.Path):
cos_dir = tmp_path / "cos_custom"
(cos_dir / "unknown").mkdir(parents=True)

with pytest.raises(ExtensionError) as exc:
extensions.apply_extensions(tmp_path, flask_input_yaml)

error = str(exc.value)
assert "custom COS directory must only contain" in error
assert "invalid subdirectories: unknown" in error


def test_cos_custom_allows_known_subdir(flask_input_yaml, tmp_path: pathlib.Path):
grafana_dir = tmp_path / "cos_custom" / "grafana_dashboards"
loki_dir = tmp_path / "cos_custom" / "loki_alert_rules"
prometheus_dir = tmp_path / "cos_custom" / "prometheus_alert_rules"
grafana_dir.mkdir(parents=True)
loki_dir.mkdir()
prometheus_dir.mkdir()
(grafana_dir / "dash.json").write_text("{}")
(loki_dir / "rules.yaml").write_text("groups: []")
(prometheus_dir / "rules.yaml").write_text("groups: []")

extensions.apply_extensions(tmp_path, flask_input_yaml)


def test_handle_charm_part_requires_no_parts(flask_input_yaml, tmp_path):
# Currently, in the flask-framework extension, we will reject any project that
# includes a charm part. This is to prevent issues where a non-default charm part is
Expand Down
Loading