From 45f10b417ac6547afc29602b0903c2d12fbc1cc2 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Fri, 12 Jun 2026 15:39:23 +0300 Subject: [PATCH 01/26] Feat: extension dispatch for 12-factor extensions. --- charmcraft/const.py | 1 + charmcraft/extensions/__init__.py | 24 +- charmcraft/extensions/app.py | 405 +++++++++++++++++- charmcraft/extensions/extension.py | 98 +++-- .../init-django-framework/charmcraft.yaml.j2 | 2 +- .../init-django-framework/pyproject.toml.j2 | 29 ++ .../init-django-framework/requirements.txt.j2 | 2 - .../charmcraft.yaml.j2 | 2 +- .../pyproject.toml.j2 | 29 ++ .../requirements.txt.j2 | 2 - .../init-fastapi-framework/charmcraft.yaml.j2 | 2 +- .../init-fastapi-framework/pyproject.toml.j2 | 29 ++ .../requirements.txt.j2 | 2 - .../init-flask-framework/charmcraft.yaml.j2 | 2 +- .../init-flask-framework/pyproject.toml.j2 | 29 ++ .../init-flask-framework/requirements.txt.j2 | 2 - .../init-go-framework/charmcraft.yaml.j2 | 5 +- .../init-go-framework/pyproject.toml.j2 | 29 ++ .../init-go-framework/requirements.txt.j2 | 2 - .../charmcraft.yaml.j2 | 2 +- .../pyproject.toml.j2 | 29 ++ .../requirements.txt.j2 | 2 - tests/extensions/test_app.py | 380 +++++++++++++++- tests/extensions/test_registry.py | 96 +++++ .../charmcraft.yaml | 13 - .../errors.json | 18 - .../resolute-charm-plugin/charmcraft.yaml | 12 - .../resolute-charm-plugin/errors.json | 18 - .../platforms-resolute-charm/charmcraft.yaml | 12 + .../platforms-resolute-charm/expected.yaml | 16 + tests/integration/test_application.py | 12 - tests/unit/models/test_project.py | 32 +- 32 files changed, 1170 insertions(+), 168 deletions(-) delete mode 100644 charmcraft/templates/init-django-framework/requirements.txt.j2 delete mode 100644 charmcraft/templates/init-expressjs-framework/requirements.txt.j2 delete mode 100644 charmcraft/templates/init-fastapi-framework/requirements.txt.j2 delete mode 100644 charmcraft/templates/init-flask-framework/requirements.txt.j2 delete mode 100644 charmcraft/templates/init-go-framework/requirements.txt.j2 delete mode 100644 charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 delete mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml delete mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json delete mode 100644 tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml delete mode 100644 tests/integration/invalid-charms/resolute-charm-plugin/errors.json create mode 100644 tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml create mode 100644 tests/integration/sample-charms/platforms-resolute-charm/expected.yaml diff --git a/charmcraft/const.py b/charmcraft/const.py index f02a87261..f4ae68a37 100644 --- a/charmcraft/const.py +++ b/charmcraft/const.py @@ -68,6 +68,7 @@ "ubuntu@24.04", "ubuntu@24.10", "ubuntu@25.04", + "ubuntu@26.04", "ubuntu@26.10", ) ) diff --git a/charmcraft/extensions/__init__.py b/charmcraft/extensions/__init__.py index dac921242..2085d4cfc 100644 --- a/charmcraft/extensions/__init__.py +++ b/charmcraft/extensions/__init__.py @@ -18,12 +18,12 @@ from charmcraft.extensions._utils import apply_extensions from charmcraft.extensions.app import ( - DjangoFramework, - FastAPIFramework, - FlaskFramework, - GoFramework, - ExpressJSFramework, - SpringBootFramework, + django_framework_factory, + expressjs_framework_factory, + fastapi_framework_factory, + flask_framework_factory, + go_framework_factory, + springboot_framework_factory, ) from charmcraft.extensions.extension import Extension from charmcraft.extensions.registry import ( @@ -46,9 +46,9 @@ "unregister", ] -register("flask-framework", FlaskFramework) -register("django-framework", DjangoFramework) -register("go-framework", GoFramework) -register("fastapi-framework", FastAPIFramework) -register("expressjs-framework", ExpressJSFramework) -register("spring-boot-framework", SpringBootFramework) +register("flask-framework", flask_framework_factory) +register("django-framework", django_framework_factory) +register("go-framework", go_framework_factory) +register("fastapi-framework", fastapi_framework_factory) +register("expressjs-framework", expressjs_framework_factory) +register("spring-boot-framework", springboot_framework_factory) diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index ff15be707..aedd8fe14 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -23,7 +23,7 @@ from overrides import override from ..errors import ExtensionError -from .extension import Extension +from .extension import Extension, get_project_bases APP_PORT_OPTION = { "app-port": { @@ -73,6 +73,70 @@ COS_SUBDIRS = {"grafana_dashboards", "loki_alert_rules", "prometheus_alert_rules"} +class _FrameworkFactory: + """Route to a V1 or V2 extension class based on the project's target bases. + + Instances are callable and expose get_supported_bases and is_experimental + so they can be registered and introspected like an Extension subclass. + """ + + def __init__(self, v1_cls: type[Extension], v2_cls: type[Extension]) -> None: + """Store the V1 and V2 extension classes. + + :param v1_cls: the V1 extension class. + :param v2_cls: the V2 extension class. + """ + self._v1_cls = v1_cls + self._v2_cls = v2_cls + + def __call__(self, *, project_root: Path, yaml_data: dict[str, Any]) -> Extension: + """Route to V1 or V2 based on project bases. + + :param project_root: the project root directory. + :param yaml_data: the raw yaml data. + :return: an Extension instance from the appropriate version. + """ + bases = get_project_bases(yaml_data) + if ("ubuntu", "26.04") in bases: + return self._v2_cls(project_root=project_root, yaml_data=yaml_data) + return self._v1_cls(project_root=project_root, yaml_data=yaml_data) + + def get_supported_bases(self) -> list[tuple[str, str]]: + """Return merged supported bases from both V1 and V2, deduped and ordered. + + :return: list of supported (distribution, series) tuples. + """ + return list( + dict.fromkeys( + self._v1_cls.get_supported_bases() + self._v2_cls.get_supported_bases() + ) + ) + + def is_experimental(self, base: tuple[str, str] | None) -> bool: + """Check if experimental, delegating to the class that supports the base. + + :param base: the target base tuple or None. + :return: True if the base is experimental, False otherwise. + """ + if base in self._v2_cls.get_supported_bases(): + return self._v2_cls.is_experimental(base) + return self._v1_cls.is_experimental(base) + + +def _make_framework_factory(v1_cls: type[Extension], v2_cls: type[Extension]): + """Create a factory that routes to V1 or V2 based on target base. + + Routes to V2 if ubuntu@26.04 is in the project bases, otherwise to V1. + Merges supported bases from both versions (deduped) and delegates experimental + status checks to the appropriate class. + + :param v1_cls: the V1 extension class. + :param v2_cls: the V2 extension class. + :return: a factory callable with get_supported_bases and is_experimental methods. + """ + return _FrameworkFactory(v1_cls, v2_cls) + + class _AppBase(Extension): """A base class for 12-factor applications.""" @@ -325,6 +389,273 @@ def get_image_name(self) -> str: return f"{self.framework}-app-image" +class _AppBaseV2(Extension): + """A base class for 12-factor applications.""" + + _CHARM_LIBS = [ + {"lib": "traefik_k8s.ingress", "version": "2"}, + {"lib": "observability_libs.juju_topology", "version": "0"}, + {"lib": "grafana_k8s.grafana_dashboard", "version": "0"}, + {"lib": "loki_k8s.loki_push_api", "version": "1"}, + {"lib": "data_platform_libs.data_interfaces", "version": "0"}, + {"lib": "prometheus_k8s.prometheus_scrape", "version": "0"}, + {"lib": "redis_k8s.redis", "version": "0"}, + {"lib": "data_platform_libs.s3", "version": "0"}, + {"lib": "saml_integrator.saml", "version": "0"}, + {"lib": "tempo_coordinator_k8s.tracing", "version": "0"}, + {"lib": "smtp_integrator.smtp", "version": "0"}, + {"lib": "openfga_k8s.openfga", "version": "1"}, + {"lib": "hydra.oauth", "version": "0"}, + {"lib": "squid_forward_proxy.http_proxy", "version": "0"}, + ] + + @staticmethod + @override + def get_supported_bases() -> list[tuple[str, str]]: + """Return supported bases.""" + return [("ubuntu", "26.04")] + + @staticmethod + @override + def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 + """Check if the extension is in an experimental state.""" + return True + + framework: str + actions: dict = { + "rotate-secret-key": { + "description": "Rotate the secret key. Users will be forced to log in again. This might be useful if a security breach occurs." + } + } + + options: dict + + endpoint_dynamic_options: dict[str, dict[str, Any]] = { + "oauth": OAUTH_DYNAMIC_OPTIONS + } + + def _get_nested(self, obj: dict, path: str) -> dict: + """Get a nested object using a path (a dot-separated list of keys).""" + for key in path.split("."): + obj = obj.get(key, {}) + return obj + + def _check_input(self) -> None: + """Check if the extension is applicable for user input charmcraft project file.""" + charm_type = self.yaml_data.get("type") + if charm_type != "charm": + raise ExtensionError( + 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( + f"the '{self.framework}-framework' extension is incompatible with " + f"customized charm part" + ) + incompatible_fields = { + "devices", + "extra-bindings", + "storage", + } & self.yaml_data.keys() + if incompatible_fields: + raise ExtensionError( + f"the '{self.framework}-framework' extension is incompatible with the provided " + f"field(s): {', '.join(sorted(incompatible_fields))}" + ) + root_snippet = self._get_root_snippet() + for protected in ("assumes", "containers", "resources", "peers"): + if ( + protected in self.yaml_data + and self.yaml_data[protected] != root_snippet[protected] + ): + raise ExtensionError( + f"{protected!r} in charmcraft.yaml conflicts with a reserved field " + f"in the {self.framework}-framework extension, please remove it." + ) + for merging in ("actions", "requires", "provides", "config.options"): + user_provided: dict[str, Any] = self._get_nested(self.yaml_data, merging) + if not user_provided: + continue + overlap = ( + user_provided.keys() & self._get_nested(root_snippet, merging).keys() + ) + if overlap: + raise ExtensionError( + f"overlapping keys {overlap} in {merging} of charmcraft.yaml " + f"which conflict with the {self.framework}-framework extension, " + "please rename or remove it" + ) + invalid_non_optionals = [] + for config in self._get_nested(self.yaml_data, "config.options"): + for reserved_config_prefix in ("webserver-", f"{self.framework}-"): + if config.startswith(reserved_config_prefix): + raise ExtensionError( + f"config.options {config!r} starts with {self.framework}-framework" + f" reserved configuration prefix {reserved_config_prefix!r}, " + "please rename or remove it" + ) + config_option_dict = self._get_nested( + self.yaml_data, f"config.options.{config}" + ) + if config_option_dict.get("optional") is False and config_option_dict.get( + "default" + ): + invalid_non_optionals.append(config) + + if invalid_non_optionals: + raise ExtensionError( + "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)." + ) + + 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)) + raise ExtensionError( + "custom COS directory must only contain the following subdirectories: " + f"{COS_SUBDIRS}. Found {'; '.join(details)}" + ) + + def _get_root_snippet(self) -> dict[str, Any]: + """Return the root snippet to be merged into the user charmcraft.yaml. + + This method differs from get_root_snippet because it doesn't perform any check. + """ + return { + "assumes": ["k8s-api"], + "containers": { + self.get_container_name(): {"resource": self.get_image_name()}, + }, + "resources": { + self.get_image_name(): { + "type": "oci-image", + "description": f"{self.framework} application image for COS v2.", + }, + }, + "charm-libs": self._CHARM_LIBS, + "peers": {"secret-storage": {"interface": "secret-storage"}}, + "actions": self.actions, + "requires": { + "logging": {"interface": "loki_push_api"}, + "ingress": {"interface": "ingress", "limit": 1}, + }, + "provides": { + "metrics-endpoint": {"interface": "prometheus_scrape"}, + "grafana-dashboard": {"interface": "grafana_dashboard"}, + }, + "config": {"options": copy.deepcopy(self.options)}, + "parts": { + "charm": { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], # Needed to build pydantic. + "override-build": ["rustup default stable", "craftctl default"], + "uv-groups": ["charmlibs-pydeps"], + }, + **self.get_config_part(), + }, + } + + def get_config_part(self) -> dict[str, Any]: + """Get config part if paas-config.yaml is present.""" + config_file = Path(self.project_root) / "paas-config.yaml" + if not config_file.is_file(): + return {} + return { + "config": { + "plugin": "dump", + "source": ".", + "stage": ["paas-config.yaml"], + } + } + + @override + def get_root_snippet(self) -> dict[str, Any]: + """Return the root snippet to be merged into the user charmcraft.yaml.""" + self._check_input() + root_snippet = self._get_root_snippet() + for interface_name, config_options in self.endpoint_dynamic_options.items(): + dynamic_config_options = self._get_dynamic_config_options( + root_snippet, interface_name, config_options + ) + root_snippet["config"]["options"].update(dynamic_config_options) + return root_snippet + + def _get_dynamic_config_options( + self, + root_snippet: dict[str, Any], + interface_name: str, + config_options: dict[str, Any], + ) -> dict[str, Any]: + dynamic_endpoint_names = [] + requires = self._get_nested(self.yaml_data, "requires") + for endpoint_name, require in requires.items(): + current_interface_name = require.get("interface") + if current_interface_name == interface_name: + dynamic_endpoint_names.append(endpoint_name) + + dynamic_config_options = {} + for endpoint_name in dynamic_endpoint_names: + updated_config_options = self._get_updated_dynamic_config_options( + endpoint_name, config_options + ) + dynamic_config_options.update(updated_config_options) + return dynamic_config_options + + def _get_updated_dynamic_config_options( + self, endpoint_name: str, config_options: dict[str, dict[str, Any]] + ) -> dict[str, dict[str, Any]]: + updated_config_options = {} + for option, value in config_options.items(): + updated_option = option.format(endpoint_name=endpoint_name) + updated_value = copy.deepcopy(value) + for value_key, value_item in value.items(): + if isinstance(value_item, str): + updated_value[value_key] = value_item.format( + endpoint_name=endpoint_name + ) + updated_config_options[updated_option] = updated_value + return updated_config_options + + @override + def get_part_snippet(self) -> dict[str, Any]: + """Return the part snippet to apply to existing parts.""" + return {} + + @override + def get_parts_snippet(self) -> dict[str, Any]: + """Return the parts to add to parts.""" + return {} + + def get_image_name(self) -> str: + """Return name of the app image.""" + return "app-image" + + def get_container_name(self) -> str: + """Return name of the container for the app image.""" + return "app" + + GUNICORN_WEBSERVER_OPTIONS = { "webserver-keepalive": { "type": "int", @@ -400,6 +731,16 @@ def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 return False +class FlaskFrameworkV2(_AppBaseV2): + """Extension v2 for 12-factor Flask applications.""" + + framework = "flask" + options = FlaskFramework.options + + +flask_framework_factory = _make_framework_factory(FlaskFramework, FlaskFrameworkV2) + + class DjangoFramework(_AppBase): """Extension for 12-factor Django applications.""" @@ -443,6 +784,17 @@ def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 return False +class DjangoFrameworkV2(_AppBaseV2): + """Extension v2 for 12-factor Django applications.""" + + framework = "django" + actions = {**DjangoFramework.actions} + options = DjangoFramework.options + + +django_framework_factory = _make_framework_factory(DjangoFramework, DjangoFrameworkV2) + + class GoFramework(_AppBase): """Extension for 12-factor Go applications.""" @@ -470,6 +822,20 @@ def get_container_name(self) -> str: return "app" +class GoFrameworkV2(_AppBaseV2): + """Extension v2 for 12-factor Go applications.""" + + framework = "go" + options = { + **APP_PORT_OPTION, + **METRICS_OPTIONS, + **SECRET_OPTIONS, + } + + +go_framework_factory = _make_framework_factory(GoFramework, GoFrameworkV2) + + class FastAPIFramework(_AppBase): """Extension for 12-factor FastAPI applications.""" @@ -511,6 +877,18 @@ def get_container_name(self) -> str: return "app" +class FastAPIFrameworkV2(_AppBaseV2): + """Extension v2 for 12-factor FastAPI applications.""" + + framework = "fastapi" + options = FastAPIFramework.options + + +fastapi_framework_factory = _make_framework_factory( + FastAPIFramework, FastAPIFrameworkV2 +) + + class ExpressJSFramework(_AppBase): """Extension for 12-factor ExpressJS applications.""" @@ -538,6 +916,18 @@ def get_container_name(self) -> str: return "app" +class ExpressJSFrameworkV2(_AppBaseV2): + """Extension v2 for 12-factor ExpressJS applications.""" + + framework = "expressjs" + options = ExpressJSFramework.options + + +expressjs_framework_factory = _make_framework_factory( + ExpressJSFramework, ExpressJSFrameworkV2 +) + + class SpringBootFramework(_AppBase): """Extension for 12-factor Spring Boot applications.""" @@ -598,3 +988,16 @@ def get_image_name(self) -> str: def get_container_name(self) -> str: """Return name of the container for the app image.""" return "app" + + +class SpringBootFrameworkV2(_AppBaseV2): + """Extension v2 for 12-factor Spring Boot applications.""" + + framework = "spring-boot" + options = SpringBootFramework.options + endpoint_dynamic_options = SpringBootFramework.endpoint_dynamic_options + + +springboot_framework_factory = _make_framework_factory( + SpringBootFramework, SpringBootFrameworkV2 +) diff --git a/charmcraft/extensions/extension.py b/charmcraft/extensions/extension.py index 7e4a96355..6e07f332c 100644 --- a/charmcraft/extensions/extension.py +++ b/charmcraft/extensions/extension.py @@ -30,6 +30,60 @@ from charmcraft import const, errors +def get_project_bases(yaml_data: dict[str, Any]) -> set[tuple[str, str]]: + """Extract and normalize all bases used in the project. + + Handles the `base` field, `platforms` with labels and `build-for`, + and legacy `bases` in both short and long formats. + + :param yaml_data: the raw yaml data. + :return: a set of normalized (distribution, series) tuples. + """ + bases: set[tuple[str, str]] = set() + + if base_str := yaml_data.get("base"): + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + name, _, channel = base_str.partition("@") + bases.add((name, channel)) + + if platforms := yaml_data.get("platforms", {}): + for label, data in platforms.items(): + if base := craft_platforms.parse_base_and_name(label)[0]: + bases.add((base.distribution, base.series)) + elif data and (build_for := data.get("build-for")): + build_for_items = ( + build_for if isinstance(build_for, list) else [build_for] + ) + for item in build_for_items: + if base := craft_platforms.parse_base_and_architecture(item)[0]: + bases.add((base.distribution, base.series)) + + if legacy_bases := yaml_data.get("bases"): + for b in legacy_bases: + # Handle both short form ({name, channel}) and long form ({build-on: [...]}) + if "build-on" in b: + for build_on in b.get("build-on", []): + name = build_on.get("name") + channel = build_on.get("channel") + base_str = f"{name}@{channel}" + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + bases.add((name, channel)) + elif "name" in b and "channel" in b: + name = b["name"] + channel = b["channel"] + base_str = f"{name}@{channel}" + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + bases.add((name, channel)) + + return bases + + class Extension(abc.ABC): """Extension is the class from which all extensions inherit. @@ -73,49 +127,7 @@ def get_parts_snippet(self) -> dict[str, Any]: def _get_project_bases(self) -> set[tuple[str, str]]: """Extract and normalize all bases used in the project.""" - bases: set[tuple[str, str]] = set() - - if base_str := self.yaml_data.get("base"): - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - name, _, channel = base_str.partition("@") - bases.add((name, channel)) - - if platforms := self.yaml_data.get("platforms", {}): - for label, data in platforms.items(): - if base := craft_platforms.parse_base_and_name(label)[0]: - bases.add((base.distribution, base.series)) - elif data and (build_for := data.get("build-for")): - build_for_items = ( - build_for if isinstance(build_for, list) else [build_for] - ) - for item in build_for_items: - if base := craft_platforms.parse_base_and_architecture(item)[0]: - bases.add((base.distribution, base.series)) - - if legacy_bases := self.yaml_data.get("bases"): - for b in legacy_bases: - # Handle both short form ({name, channel}) and long form ({build-on: [...]}) - if "build-on" in b: - for build_on in b.get("build-on", []): - name = build_on.get("name") - channel = build_on.get("channel") - base_str = f"{name}@{channel}" - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - bases.add((name, channel)) - elif "name" in b and "channel" in b: - name = b["name"] - channel = b["channel"] - base_str = f"{name}@{channel}" - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - bases.add((name, channel)) - - return bases + return get_project_bases(self.yaml_data) def validate(self, extension_name: str): """Validate that the extension can be used with the current project. diff --git a/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 index 5bccbe6ee..0e8af5956 100644 --- a/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@24.04 +base: ubuntu@26.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-django-framework/pyproject.toml.j2 b/charmcraft/templates/init-django-framework/pyproject.toml.j2 index 660ac9996..85441d1bd 100644 --- a/charmcraft/templates/init-django-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-django-framework/pyproject.toml.j2 @@ -1,3 +1,32 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-django-framework/requirements.txt.j2 b/charmcraft/templates/init-django-framework/requirements.txt.j2 deleted file mode 100644 index d58a30c21..000000000 --- a/charmcraft/templates/init-django-framework/requirements.txt.j2 +++ /dev/null @@ -1,2 +0,0 @@ -ops ~= 2.17 -paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 index 7af066512..3908c4202 100644 --- a/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@24.04 +base: ubuntu@26.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 b/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 index 660ac9996..85441d1bd 100644 --- a/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 @@ -1,3 +1,32 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-expressjs-framework/requirements.txt.j2 b/charmcraft/templates/init-expressjs-framework/requirements.txt.j2 deleted file mode 100644 index 113bb427b..000000000 --- a/charmcraft/templates/init-expressjs-framework/requirements.txt.j2 +++ /dev/null @@ -1,2 +0,0 @@ -ops>2.17,<4 -paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 index f64467e1a..11c2f10fe 100644 --- a/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@24.04 +base: ubuntu@26.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 b/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 index 660ac9996..85441d1bd 100644 --- a/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 @@ -1,3 +1,32 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-fastapi-framework/requirements.txt.j2 b/charmcraft/templates/init-fastapi-framework/requirements.txt.j2 deleted file mode 100644 index d58a30c21..000000000 --- a/charmcraft/templates/init-fastapi-framework/requirements.txt.j2 +++ /dev/null @@ -1,2 +0,0 @@ -ops ~= 2.17 -paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 index 005f12446..421dbea64 100644 --- a/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@24.04 +base: ubuntu@26.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-flask-framework/pyproject.toml.j2 b/charmcraft/templates/init-flask-framework/pyproject.toml.j2 index 660ac9996..85441d1bd 100644 --- a/charmcraft/templates/init-flask-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-flask-framework/pyproject.toml.j2 @@ -1,3 +1,32 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-flask-framework/requirements.txt.j2 b/charmcraft/templates/init-flask-framework/requirements.txt.j2 deleted file mode 100644 index d58a30c21..000000000 --- a/charmcraft/templates/init-flask-framework/requirements.txt.j2 +++ /dev/null @@ -1,2 +0,0 @@ -ops ~= 2.17 -paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 index bb44201f1..3865729fc 100644 --- a/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 @@ -1,12 +1,12 @@ # This file configures Charmcraft. # See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} type: charm -base: ubuntu@24.04 +base: ubuntu@26.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` @@ -78,4 +78,3 @@ extensions: # interface: http_proxy # optional: true # limit: 1 - diff --git a/charmcraft/templates/init-go-framework/pyproject.toml.j2 b/charmcraft/templates/init-go-framework/pyproject.toml.j2 index 660ac9996..85441d1bd 100644 --- a/charmcraft/templates/init-go-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-go-framework/pyproject.toml.j2 @@ -1,3 +1,32 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-go-framework/requirements.txt.j2 b/charmcraft/templates/init-go-framework/requirements.txt.j2 deleted file mode 100644 index d58a30c21..000000000 --- a/charmcraft/templates/init-go-framework/requirements.txt.j2 +++ /dev/null @@ -1,2 +0,0 @@ -ops ~= 2.17 -paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 index e955623d5..737fe302b 100644 --- a/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@24.04 +base: ubuntu@26.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 b/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 index 3cb1ce223..46991a3ea 100644 --- a/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 @@ -1,3 +1,32 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 b/charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 deleted file mode 100644 index d58a30c21..000000000 --- a/charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 +++ /dev/null @@ -1,2 +0,0 @@ -ops ~= 2.17 -paas-charm>=1.0,<2 diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 6854ff5eb..3557d9151 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -18,7 +18,7 @@ import pytest -from charmcraft import extensions +from charmcraft import errors, extensions from charmcraft.errors import ExtensionError from charmcraft.extensions.app import ( DjangoFramework, @@ -27,6 +27,10 @@ FlaskFramework, GoFramework, SpringBootFramework, + expressjs_framework_factory, + fastapi_framework_factory, + flask_framework_factory, + go_framework_factory, ) NON_OPTIONAL_OPTIONS = { @@ -520,6 +524,380 @@ def test_apply_extensions_correct( assert applied == expected +def test_go_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-go-v2", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["go-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + assert applied["containers"] == {"app": {"resource": "app-image"}} + assert applied["resources"] == { + "app-image": { + "type": "oci-image", + "description": "go application image for COS v2.", + } + } + assert applied["parts"]["charm"] == { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], + "override-build": ["rustup default stable", "craftctl default"], + "uv-groups": ["charmlibs-pydeps"], + } + + +def test_flask_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-flask-v2", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["flask-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + assert applied["containers"] == {"app": {"resource": "app-image"}} + assert applied["resources"] == { + "app-image": { + "type": "oci-image", + "description": "flask application image for COS v2.", + } + } + assert applied["parts"]["charm"] == { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], + "override-build": ["rustup default stable", "craftctl default"], + "uv-groups": ["charmlibs-pydeps"], + } + + +def test_django_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-django-v2", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["django-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + assert applied["containers"] == {"app": {"resource": "app-image"}} + assert applied["resources"] == { + "app-image": { + "type": "oci-image", + "description": "django application image for COS v2.", + } + } + assert applied["parts"]["charm"] == { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], + "override-build": ["rustup default stable", "craftctl default"], + "uv-groups": ["charmlibs-pydeps"], + } + + +def test_fastapi_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-fastapi-v2", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["fastapi-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + assert applied["containers"] == {"app": {"resource": "app-image"}} + assert applied["resources"] == { + "app-image": { + "type": "oci-image", + "description": "fastapi application image for COS v2.", + } + } + assert applied["parts"]["charm"] == { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], + "override-build": ["rustup default stable", "craftctl default"], + "uv-groups": ["charmlibs-pydeps"], + } + + +def test_expressjs_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-expressjs-v2", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["expressjs-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + assert applied["containers"] == {"app": {"resource": "app-image"}} + assert applied["resources"] == { + "app-image": { + "type": "oci-image", + "description": "expressjs application image for COS v2.", + } + } + assert applied["parts"]["charm"] == { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], + "override-build": ["rustup default stable", "craftctl default"], + "uv-groups": ["charmlibs-pydeps"], + } + + +def test_spring_boot_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-springboot-v2", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["spring-boot-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + assert applied["containers"] == {"app": {"resource": "app-image"}} + assert applied["resources"] == { + "app-image": { + "type": "oci-image", + "description": "spring-boot application image for COS v2.", + } + } + assert applied["parts"]["charm"] == { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], + "override-build": ["rustup default stable", "craftctl default"], + "uv-groups": ["charmlibs-pydeps"], + } + + +def test_go_framework_platforms_only_routes_to_v2(monkeypatch, tmp_path): + """Test that go on 26.04 via platforms (no top-level base) routes to V2 (defect 3 fix).""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-go-platforms-v2", + "summary": "test summary", + "description": "test description", + "platforms": {"ubuntu@26.04:amd64": None}, + "extensions": ["go-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + # V2 uses uv plugin and different image name + assert applied["parts"]["charm"]["plugin"] == "uv" + assert ( + applied["resources"]["app-image"]["description"] + == "go application image for COS v2." + ) + + +def test_go_framework_24_04_still_routes_to_v1(monkeypatch, tmp_path): + """Test that go on 24.04 still routes to V1 with charm plugin.""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-go-v1", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@24.04", + "platforms": {"amd64": None}, + "extensions": ["go-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + # V1 uses charm plugin + assert applied["parts"]["charm"]["plugin"] == "charm" + assert applied["resources"]["app-image"]["description"] == "go application image." + + +def test_v2_extension_experimental_gating_enforced(monkeypatch, tmp_path): + """Test that V2 extensions require CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS env var.""" + monkeypatch.delenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", raising=False) + input_yaml = { + "type": "charm", + "name": "test-flask-v2-no-env", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["flask-framework"], + } + + with pytest.raises( + errors.ExtensionError, + match=".*experimental on base.*ubuntu@26.04.*", + ): + extensions.apply_extensions(tmp_path, input_yaml) + + +def test_v2_extension_experimental_gating_passes_with_env(monkeypatch, tmp_path): + """Test that V2 extensions work with CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS env var.""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-flask-v2-with-env", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["flask-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + # Should succeed and produce V2 output + assert applied["parts"]["charm"]["plugin"] == "uv" + + +def test_flask_framework_factory_get_supported_bases_no_duplicates(): + """Test that flask factory supported bases are deduped (defect 1 fix).""" + bases = flask_framework_factory.get_supported_bases() + assert bases == [("ubuntu", "22.04"), ("ubuntu", "26.04")] + assert len(bases) == len(set(bases)), "Supported bases should not have duplicates" + + +def test_go_framework_factory_is_experimental_correct(monkeypatch): + """Test that go factory is_experimental delegates to correct class per base (defect 2 fix).""" + # Go V1 on 24.04 should still be experimental + assert go_framework_factory.is_experimental(("ubuntu", "24.04")) is True + # Go V2 on 26.04 should be experimental + assert go_framework_factory.is_experimental(("ubuntu", "26.04")) is True + + +def test_fastapi_framework_factory_is_experimental_24_04(monkeypatch): + """Test that fastapi on 24.04 is experimental (defect 2: not regressed to stable).""" + # FastAPI V1 on 24.04 should still be experimental (not regressed to stable) + assert fastapi_framework_factory.is_experimental(("ubuntu", "24.04")) is True + + +def test_expressjs_framework_factory_is_experimental_24_04(monkeypatch): + """Test that expressjs on 24.04 is experimental (defect 2: not regressed to stable).""" + # ExpressJS V1 on 24.04 should still be experimental (not regressed to stable) + assert expressjs_framework_factory.is_experimental(("ubuntu", "24.04")) is True + + +def test_v2_check_input_rejects_non_charm_type(monkeypatch, tmp_path): + """Test that V2 _check_input validates type == charm.""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "bundle", # Invalid: not a charm + "name": "test-bundle", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["flask-framework"], + } + + with pytest.raises( + errors.ExtensionError, + match=".*incompatible with type 'bundle'", + ): + extensions.apply_extensions(tmp_path, input_yaml) + + +def test_v2_check_input_rejects_customized_charm_part(monkeypatch, tmp_path): + """Test that V2 _check_input rejects customized charm part.""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-custom-charm", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "parts": {"charm": {"plugin": "dump", "source": "."}}, # Custom + "extensions": ["flask-framework"], + } + + with pytest.raises( + errors.ExtensionError, + match=".*incompatible with customized charm part", + ): + extensions.apply_extensions(tmp_path, input_yaml) + + +def test_v2_paas_config_part_present(monkeypatch, tmp_path): + """Test that V2 generates paas-config dump part when paas-config.yaml exists.""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + + # Create paas-config.yaml in the project + (tmp_path / "paas-config.yaml").write_text("key: value\n") + + input_yaml = { + "type": "charm", + "name": "test-paas-config", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["flask-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + # V2 should include config part for paas-config.yaml + assert "config" in applied["parts"] + assert applied["parts"]["config"]["plugin"] == "dump" + assert applied["parts"]["config"]["stage"] == ["paas-config.yaml"] + + +def test_v2_paas_config_part_absent_when_no_file(monkeypatch, tmp_path): + """Test that V2 does not generate paas-config part when file is absent.""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + + input_yaml = { + "type": "charm", + "name": "test-no-paas-config", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "extensions": ["flask-framework"], + } + + applied = extensions.apply_extensions(tmp_path, input_yaml) + + # V2 should not include config part when paas-config.yaml is absent + assert "config" not in applied["parts"] + + PROTECTED_FIELDS_TEST_PARAMETERS = [ pytest.param({"type": "bundle"}, id="type"), pytest.param({"containers": {"foobar": {"resource": "foobar"}}}, id="containers"), diff --git a/tests/extensions/test_registry.py b/tests/extensions/test_registry.py index 50647b4ac..ffd8fe82c 100644 --- a/tests/extensions/test_registry.py +++ b/tests/extensions/test_registry.py @@ -18,6 +18,14 @@ import pytest from charmcraft import errors, extensions +from charmcraft.extensions.app import ( + django_framework_factory, + expressjs_framework_factory, + fastapi_framework_factory, + flask_framework_factory, + go_framework_factory, + springboot_framework_factory, +) from charmcraft.extensions.extension import Extension @@ -110,3 +118,91 @@ def test_unregister(fake_extensions): extensions.unregister(FakeExtension1.name) with pytest.raises(errors.ExtensionError): extensions.get_extension_class(FakeExtension1.name) + + +def test_real_framework_factories_no_duplicate_experimental_bases(): + """Verify real framework factories have no duplicate experimental_bases (defect 1 fix).""" + # Import the actual factories directly + factories = [ + ("flask-framework", flask_framework_factory), + ("django-framework", django_framework_factory), + ("go-framework", go_framework_factory), + ("fastapi-framework", fastapi_framework_factory), + ("expressjs-framework", expressjs_framework_factory), + ("spring-boot-framework", springboot_framework_factory), + ] + + for name, factory in factories: + bases = factory.get_supported_bases() + # Verify no duplicates + assert len(bases) == len(set(bases)), ( + f"{name} has duplicate supported_bases: {bases}" + ) + + +def test_real_framework_factories_experimental_status_correct(): + """Verify experimental status matches framework requirements (defect 2 fix).""" + # Map factories to expected experimental status per base + test_cases = [ + ( + "flask", + flask_framework_factory, + [ + (("ubuntu", "22.04"), False), # V1: stable + (("ubuntu", "26.04"), True), # V2: experimental + ], + ), + ( + "django", + django_framework_factory, + [ + (("ubuntu", "22.04"), False), # V1: stable + (("ubuntu", "26.04"), True), # V2: experimental + ], + ), + ( + "go", + go_framework_factory, + [ + (("ubuntu", "24.04"), True), # V1: experimental + (("ubuntu", "26.04"), True), # V2: experimental + ], + ), + ( + "fastapi", + fastapi_framework_factory, + [ + ( + ("ubuntu", "24.04"), + True, + ), # V1: experimental (NOT regressed to stable) + (("ubuntu", "26.04"), True), # V2: experimental + ], + ), + ( + "expressjs", + expressjs_framework_factory, + [ + ( + ("ubuntu", "24.04"), + True, + ), # V1: experimental (NOT regressed to stable) + (("ubuntu", "26.04"), True), # V2: experimental + ], + ), + ( + "spring-boot", + springboot_framework_factory, + [ + (("ubuntu", "24.04"), False), # V1: stable + (("ubuntu", "26.04"), True), # V2: experimental + ], + ), + ] + + for name, factory, assertions in test_cases: + for base, expected_experimental in assertions: + actual = factory.is_experimental(base) + assert actual == expected_experimental, ( + f"{name} on {base}: expected is_experimental={expected_experimental}, got {actual}" + ) diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml deleted file mode 100644 index ca161f914..000000000 --- a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: invalid-charm -summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. -description: | - Charm plugin is not supported on Ubuntu 26.04 LTS. -type: charm -platforms: - resolute: - build-on: ubuntu@26.04:amd64 - build-for: ubuntu@26.04:amd64 -parts: - my-part: - plugin: charm - source: . diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json deleted file mode 100644 index b016b7e19..000000000 --- a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "value_error", - "loc": [ - "parts" - ], - "msg": "Value error, Cannot use 'charm' plugin with base 'ubuntu@26.04'", - "input": { - "my-part": { - "plugin": "charm", - "source": "." - } - }, - "ctx": { - "error": "Cannot use 'charm' plugin with base 'ubuntu@26.04'" - } - } -] diff --git a/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml deleted file mode 100644 index 27680d916..000000000 --- a/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: invalid-charm -summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. -description: | - Charm plugin is not supported on Ubuntu 26.04 LTS. -type: charm -base: ubuntu@26.04 -platforms: - amd64: -parts: - my-part: - plugin: charm - source: . diff --git a/tests/integration/invalid-charms/resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/resolute-charm-plugin/errors.json deleted file mode 100644 index b016b7e19..000000000 --- a/tests/integration/invalid-charms/resolute-charm-plugin/errors.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "value_error", - "loc": [ - "parts" - ], - "msg": "Value error, Cannot use 'charm' plugin with base 'ubuntu@26.04'", - "input": { - "my-part": { - "plugin": "charm", - "source": "." - } - }, - "ctx": { - "error": "Cannot use 'charm' plugin with base 'ubuntu@26.04'" - } - } -] diff --git a/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml b/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml new file mode 100644 index 000000000..5e7f8eb31 --- /dev/null +++ b/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml @@ -0,0 +1,12 @@ +name: example-charm +summary: An example charm with platforms using charm plugin +description: | + A description for an example charm with platforms using the charm plugin. +type: charm +base: ubuntu@26.04 +platforms: + amd64: + +parts: + charm: + plugin: charm diff --git a/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml b/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml new file mode 100644 index 000000000..2a4fc921c --- /dev/null +++ b/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml @@ -0,0 +1,16 @@ +name: example-charm +summary: An example charm with platforms using charm plugin +description: | + A description for an example charm with platforms using the charm plugin. +base: ubuntu@26.04 +platforms: + amd64: + build-on: + - amd64 + build-for: + - amd64 +parts: + charm: + plugin: charm + source: . +type: charm diff --git a/tests/integration/test_application.py b/tests/integration/test_application.py index bb40fc7d0..93d41159d 100644 --- a/tests/integration/test_application.py +++ b/tests/integration/test_application.py @@ -129,12 +129,6 @@ def test_load_invalid_charm(in_project_path: pathlib.Path, charm_dir: pathlib.Pa "plugin not registered: 'reactive'", id="multibase-questing-charm-plugin-reactive", ), - pytest.param( - "multibase-resolute-charm-plugin", - "charm", - "plugin not registered: 'charm'", - id="multibase-resolute-charm-plugin-charm", - ), pytest.param( "questing-charm-plugin", "charm", @@ -159,12 +153,6 @@ def test_load_invalid_charm(in_project_path: pathlib.Path, charm_dir: pathlib.Pa "plugin not registered: 'reactive'", id="questing-reactive-plugin-reactive", ), - pytest.param( - "resolute-charm-plugin", - "charm", - "plugin not registered: 'charm'", - id="resolute-charm-plugin-charm", - ), ], ) def test_remove_charm_reactive_plugins( diff --git a/tests/unit/models/test_project.py b/tests/unit/models/test_project.py index ad1b432e9..c87bd8966 100644 --- a/tests/unit/models/test_project.py +++ b/tests/unit/models/test_project.py @@ -754,22 +754,18 @@ def test_resolute_base_supports_reactive_plugin(): ) -def test_resolute_base_rejects_charm_plugin(): - with pytest.raises( - pydantic.ValidationError, - match="Cannot use 'charm' plugin with base 'ubuntu@26.04'", - ): - project.PlatformCharm.unmarshal( - { - "type": "charm", - "name": "test-charm", - "summary": "", - "description": "", - "base": "ubuntu@26.04", - "platforms": {"amd64": None}, - "parts": {"charm": {"plugin": "charm"}}, - } - ) +def test_resolute_base_supports_charm_plugin(): + project.PlatformCharm.unmarshal( + { + "type": "charm", + "name": "test-charm", + "summary": "", + "description": "", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "parts": {"charm": {"plugin": "charm"}}, + } + ) def test_legacy_plugins_are_checked_against_build_base(): @@ -794,7 +790,7 @@ def test_legacy_plugins_are_checked_against_build_base(): def test_charm_plugin_is_checked_against_build_base(): with pytest.raises( pydantic.ValidationError, - match="Cannot use 'charm' plugin with base 'ubuntu@26.04'", + match="Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@25.10'", ): project.PlatformCharm.unmarshal( { @@ -803,7 +799,7 @@ def test_charm_plugin_is_checked_against_build_base(): "summary": "", "description": "", "base": "ubuntu@24.04", - "build-base": "ubuntu@26.04", + "build-base": "ubuntu@25.10", "platforms": {"amd64": None}, "parts": {"charm": {"plugin": "charm"}}, } From 91704fd7eba7f1ad7e8b207b7c93cfef32b2f4c4 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 15 Jun 2026 07:43:24 +0300 Subject: [PATCH 02/26] chore: cleanup --- charmcraft/extensions/app.py | 2 +- charmcraft/extensions/extension.py | 98 ++++++++++++--------------- tests/extensions/test_app.py | 17 ++--- tests/integration/test_application.py | 6 ++ tests/unit/models/test_project.py | 19 ------ 5 files changed, 56 insertions(+), 86 deletions(-) diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index aedd8fe14..48923f5cc 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -549,7 +549,7 @@ def _get_root_snippet(self) -> dict[str, Any]: "resources": { self.get_image_name(): { "type": "oci-image", - "description": f"{self.framework} application image for COS v2.", + "description": f"{self.framework} application image.", }, }, "charm-libs": self._CHARM_LIBS, diff --git a/charmcraft/extensions/extension.py b/charmcraft/extensions/extension.py index 6e07f332c..7e4a96355 100644 --- a/charmcraft/extensions/extension.py +++ b/charmcraft/extensions/extension.py @@ -30,60 +30,6 @@ from charmcraft import const, errors -def get_project_bases(yaml_data: dict[str, Any]) -> set[tuple[str, str]]: - """Extract and normalize all bases used in the project. - - Handles the `base` field, `platforms` with labels and `build-for`, - and legacy `bases` in both short and long formats. - - :param yaml_data: the raw yaml data. - :return: a set of normalized (distribution, series) tuples. - """ - bases: set[tuple[str, str]] = set() - - if base_str := yaml_data.get("base"): - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - name, _, channel = base_str.partition("@") - bases.add((name, channel)) - - if platforms := yaml_data.get("platforms", {}): - for label, data in platforms.items(): - if base := craft_platforms.parse_base_and_name(label)[0]: - bases.add((base.distribution, base.series)) - elif data and (build_for := data.get("build-for")): - build_for_items = ( - build_for if isinstance(build_for, list) else [build_for] - ) - for item in build_for_items: - if base := craft_platforms.parse_base_and_architecture(item)[0]: - bases.add((base.distribution, base.series)) - - if legacy_bases := yaml_data.get("bases"): - for b in legacy_bases: - # Handle both short form ({name, channel}) and long form ({build-on: [...]}) - if "build-on" in b: - for build_on in b.get("build-on", []): - name = build_on.get("name") - channel = build_on.get("channel") - base_str = f"{name}@{channel}" - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - bases.add((name, channel)) - elif "name" in b and "channel" in b: - name = b["name"] - channel = b["channel"] - base_str = f"{name}@{channel}" - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - bases.add((name, channel)) - - return bases - - class Extension(abc.ABC): """Extension is the class from which all extensions inherit. @@ -127,7 +73,49 @@ def get_parts_snippet(self) -> dict[str, Any]: def _get_project_bases(self) -> set[tuple[str, str]]: """Extract and normalize all bases used in the project.""" - return get_project_bases(self.yaml_data) + bases: set[tuple[str, str]] = set() + + if base_str := self.yaml_data.get("base"): + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + name, _, channel = base_str.partition("@") + bases.add((name, channel)) + + if platforms := self.yaml_data.get("platforms", {}): + for label, data in platforms.items(): + if base := craft_platforms.parse_base_and_name(label)[0]: + bases.add((base.distribution, base.series)) + elif data and (build_for := data.get("build-for")): + build_for_items = ( + build_for if isinstance(build_for, list) else [build_for] + ) + for item in build_for_items: + if base := craft_platforms.parse_base_and_architecture(item)[0]: + bases.add((base.distribution, base.series)) + + if legacy_bases := self.yaml_data.get("bases"): + for b in legacy_bases: + # Handle both short form ({name, channel}) and long form ({build-on: [...]}) + if "build-on" in b: + for build_on in b.get("build-on", []): + name = build_on.get("name") + channel = build_on.get("channel") + base_str = f"{name}@{channel}" + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + bases.add((name, channel)) + elif "name" in b and "channel" in b: + name = b["name"] + channel = b["channel"] + base_str = f"{name}@{channel}" + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + bases.add((name, channel)) + + return bases def validate(self, extension_name: str): """Validate that the extension can be used with the current project. diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 3557d9151..f7fd967fd 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -542,7 +542,7 @@ def test_go_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): assert applied["resources"] == { "app-image": { "type": "oci-image", - "description": "go application image for COS v2.", + "description": "go application image.", } } assert applied["parts"]["charm"] == { @@ -572,7 +572,7 @@ def test_flask_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): assert applied["resources"] == { "app-image": { "type": "oci-image", - "description": "flask application image for COS v2.", + "description": "flask application image.", } } assert applied["parts"]["charm"] == { @@ -602,7 +602,7 @@ def test_django_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): assert applied["resources"] == { "app-image": { "type": "oci-image", - "description": "django application image for COS v2.", + "description": "django application image.", } } assert applied["parts"]["charm"] == { @@ -632,7 +632,7 @@ def test_fastapi_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): assert applied["resources"] == { "app-image": { "type": "oci-image", - "description": "fastapi application image for COS v2.", + "description": "fastapi application image.", } } assert applied["parts"]["charm"] == { @@ -662,7 +662,7 @@ def test_expressjs_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): assert applied["resources"] == { "app-image": { "type": "oci-image", - "description": "expressjs application image for COS v2.", + "description": "expressjs application image.", } } assert applied["parts"]["charm"] == { @@ -692,7 +692,7 @@ def test_spring_boot_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): assert applied["resources"] == { "app-image": { "type": "oci-image", - "description": "spring-boot application image for COS v2.", + "description": "spring-boot application image.", } } assert applied["parts"]["charm"] == { @@ -720,10 +720,6 @@ def test_go_framework_platforms_only_routes_to_v2(monkeypatch, tmp_path): # V2 uses uv plugin and different image name assert applied["parts"]["charm"]["plugin"] == "uv" - assert ( - applied["resources"]["app-image"]["description"] - == "go application image for COS v2." - ) def test_go_framework_24_04_still_routes_to_v1(monkeypatch, tmp_path): @@ -743,7 +739,6 @@ def test_go_framework_24_04_still_routes_to_v1(monkeypatch, tmp_path): # V1 uses charm plugin assert applied["parts"]["charm"]["plugin"] == "charm" - assert applied["resources"]["app-image"]["description"] == "go application image." def test_v2_extension_experimental_gating_enforced(monkeypatch, tmp_path): diff --git a/tests/integration/test_application.py b/tests/integration/test_application.py index 93d41159d..382642fd0 100644 --- a/tests/integration/test_application.py +++ b/tests/integration/test_application.py @@ -129,6 +129,12 @@ def test_load_invalid_charm(in_project_path: pathlib.Path, charm_dir: pathlib.Pa "plugin not registered: 'reactive'", id="multibase-questing-charm-plugin-reactive", ), + pytest.param( + "multibase-resolute-charm-plugin", + "charm", + "plugin not registered: 'charm'", + id="multibase-resolute-charm-plugin-charm", + ), pytest.param( "questing-charm-plugin", "charm", diff --git a/tests/unit/models/test_project.py b/tests/unit/models/test_project.py index c87bd8966..b884877c4 100644 --- a/tests/unit/models/test_project.py +++ b/tests/unit/models/test_project.py @@ -787,25 +787,6 @@ def test_legacy_plugins_are_checked_against_build_base(): ) -def test_charm_plugin_is_checked_against_build_base(): - with pytest.raises( - pydantic.ValidationError, - match="Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@25.10'", - ): - project.PlatformCharm.unmarshal( - { - "type": "charm", - "name": "test-charm", - "summary": "", - "description": "", - "base": "ubuntu@24.04", - "build-base": "ubuntu@25.10", - "platforms": {"amd64": None}, - "parts": {"charm": {"plugin": "charm"}}, - } - ) - - @pytest.mark.parametrize( "filename", [f.name for f in (pathlib.Path(__file__).parent / "valid_charms_yaml").iterdir()], From ad71489b1e821282517055e4d26f46894d776dfb Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 15 Jun 2026 07:48:06 +0300 Subject: [PATCH 03/26] chore: addback the resolute invalid --- .../charmcraft.yaml | 13 +++++++++++++ .../multibase-resolute-charm-plugin/errors.json | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml create mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml new file mode 100644 index 000000000..ca161f914 --- /dev/null +++ b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml @@ -0,0 +1,13 @@ +name: invalid-charm +summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. +description: | + Charm plugin is not supported on Ubuntu 26.04 LTS. +type: charm +platforms: + resolute: + build-on: ubuntu@26.04:amd64 + build-for: ubuntu@26.04:amd64 +parts: + my-part: + plugin: charm + source: . diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json new file mode 100644 index 000000000..9807c1630 --- /dev/null +++ b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json @@ -0,0 +1,16 @@ +[ + { + "type": "value_error", + "loc": ["parts"], + "msg": "Value error, Cannot use 'charm' plugin with base 'ubuntu@26.04'", + "input": { + "my-part": { + "plugin": "charm", + "source": "." + } + }, + "ctx": { + "error": "Cannot use 'charm' plugin with base 'ubuntu@26.04'" + } + } +] From 687afdc335c381f81865284fc6cbf057f197a35c Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 15 Jun 2026 07:49:27 +0300 Subject: [PATCH 04/26] chore: revert fmt --- .../multibase-resolute-charm-plugin/errors.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json index 9807c1630..b016b7e19 100644 --- a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json +++ b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json @@ -1,7 +1,9 @@ [ { "type": "value_error", - "loc": ["parts"], + "loc": [ + "parts" + ], "msg": "Value error, Cannot use 'charm' plugin with base 'ubuntu@26.04'", "input": { "my-part": { From d4dbd67fcdd967d9635030336975591ab14a6157 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 15 Jun 2026 09:25:28 +0300 Subject: [PATCH 05/26] chore: create get_project_bases back --- charmcraft/extensions/extension.py | 98 +++++++++++++++++------------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/charmcraft/extensions/extension.py b/charmcraft/extensions/extension.py index 7e4a96355..6e07f332c 100644 --- a/charmcraft/extensions/extension.py +++ b/charmcraft/extensions/extension.py @@ -30,6 +30,60 @@ from charmcraft import const, errors +def get_project_bases(yaml_data: dict[str, Any]) -> set[tuple[str, str]]: + """Extract and normalize all bases used in the project. + + Handles the `base` field, `platforms` with labels and `build-for`, + and legacy `bases` in both short and long formats. + + :param yaml_data: the raw yaml data. + :return: a set of normalized (distribution, series) tuples. + """ + bases: set[tuple[str, str]] = set() + + if base_str := yaml_data.get("base"): + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + name, _, channel = base_str.partition("@") + bases.add((name, channel)) + + if platforms := yaml_data.get("platforms", {}): + for label, data in platforms.items(): + if base := craft_platforms.parse_base_and_name(label)[0]: + bases.add((base.distribution, base.series)) + elif data and (build_for := data.get("build-for")): + build_for_items = ( + build_for if isinstance(build_for, list) else [build_for] + ) + for item in build_for_items: + if base := craft_platforms.parse_base_and_architecture(item)[0]: + bases.add((base.distribution, base.series)) + + if legacy_bases := yaml_data.get("bases"): + for b in legacy_bases: + # Handle both short form ({name, channel}) and long form ({build-on: [...]}) + if "build-on" in b: + for build_on in b.get("build-on", []): + name = build_on.get("name") + channel = build_on.get("channel") + base_str = f"{name}@{channel}" + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + bases.add((name, channel)) + elif "name" in b and "channel" in b: + name = b["name"] + channel = b["channel"] + base_str = f"{name}@{channel}" + if parsed := craft_platforms.parse_base_and_name(base_str)[0]: + bases.add((parsed.distribution, parsed.series)) + else: + bases.add((name, channel)) + + return bases + + class Extension(abc.ABC): """Extension is the class from which all extensions inherit. @@ -73,49 +127,7 @@ def get_parts_snippet(self) -> dict[str, Any]: def _get_project_bases(self) -> set[tuple[str, str]]: """Extract and normalize all bases used in the project.""" - bases: set[tuple[str, str]] = set() - - if base_str := self.yaml_data.get("base"): - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - name, _, channel = base_str.partition("@") - bases.add((name, channel)) - - if platforms := self.yaml_data.get("platforms", {}): - for label, data in platforms.items(): - if base := craft_platforms.parse_base_and_name(label)[0]: - bases.add((base.distribution, base.series)) - elif data and (build_for := data.get("build-for")): - build_for_items = ( - build_for if isinstance(build_for, list) else [build_for] - ) - for item in build_for_items: - if base := craft_platforms.parse_base_and_architecture(item)[0]: - bases.add((base.distribution, base.series)) - - if legacy_bases := self.yaml_data.get("bases"): - for b in legacy_bases: - # Handle both short form ({name, channel}) and long form ({build-on: [...]}) - if "build-on" in b: - for build_on in b.get("build-on", []): - name = build_on.get("name") - channel = build_on.get("channel") - base_str = f"{name}@{channel}" - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - bases.add((name, channel)) - elif "name" in b and "channel" in b: - name = b["name"] - channel = b["channel"] - base_str = f"{name}@{channel}" - if parsed := craft_platforms.parse_base_and_name(base_str)[0]: - bases.add((parsed.distribution, parsed.series)) - else: - bases.add((name, channel)) - - return bases + return get_project_bases(self.yaml_data) def validate(self, extension_name: str): """Validate that the extension can be used with the current project. From 15fd4bf50c5d8d864a7278af961e39d4019076cf Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 15 Jun 2026 11:27:44 +0300 Subject: [PATCH 06/26] chore: cleanup --- charmcraft/extensions/app.py | 238 ++--------------------------------- tests/extensions/test_app.py | 12 +- 2 files changed, 19 insertions(+), 231 deletions(-) diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 48923f5cc..1ad7aa2f4 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -97,7 +97,7 @@ def __call__(self, *, project_root: Path, yaml_data: dict[str, Any]) -> Extensio :return: an Extension instance from the appropriate version. """ bases = get_project_bases(yaml_data) - if ("ubuntu", "26.04") in bases: + if any(base in self._v2_cls.get_supported_bases() for base in bases): return self._v2_cls(project_root=project_root, yaml_data=yaml_data) return self._v1_cls(project_root=project_root, yaml_data=yaml_data) @@ -123,20 +123,6 @@ def is_experimental(self, base: tuple[str, str] | None) -> bool: return self._v1_cls.is_experimental(base) -def _make_framework_factory(v1_cls: type[Extension], v2_cls: type[Extension]): - """Create a factory that routes to V1 or V2 based on target base. - - Routes to V2 if ubuntu@26.04 is in the project bases, otherwise to V1. - Merges supported bases from both versions (deduped) and delegates experimental - status checks to the appropriate class. - - :param v1_cls: the V1 extension class. - :param v2_cls: the V2 extension class. - :return: a factory callable with get_supported_bases and is_experimental methods. - """ - return _FrameworkFactory(v1_cls, v2_cls) - - class _AppBase(Extension): """A base class for 12-factor applications.""" @@ -389,25 +375,8 @@ def get_image_name(self) -> str: return f"{self.framework}-app-image" -class _AppBaseV2(Extension): - """A base class for 12-factor applications.""" - - _CHARM_LIBS = [ - {"lib": "traefik_k8s.ingress", "version": "2"}, - {"lib": "observability_libs.juju_topology", "version": "0"}, - {"lib": "grafana_k8s.grafana_dashboard", "version": "0"}, - {"lib": "loki_k8s.loki_push_api", "version": "1"}, - {"lib": "data_platform_libs.data_interfaces", "version": "0"}, - {"lib": "prometheus_k8s.prometheus_scrape", "version": "0"}, - {"lib": "redis_k8s.redis", "version": "0"}, - {"lib": "data_platform_libs.s3", "version": "0"}, - {"lib": "saml_integrator.saml", "version": "0"}, - {"lib": "tempo_coordinator_k8s.tracing", "version": "0"}, - {"lib": "smtp_integrator.smtp", "version": "0"}, - {"lib": "openfga_k8s.openfga", "version": "1"}, - {"lib": "hydra.oauth", "version": "0"}, - {"lib": "squid_forward_proxy.http_proxy", "version": "0"}, - ] +class _AppBaseV2(_AppBase): + """V2 base class for 12-factor applications using uv.""" @staticmethod @override @@ -415,132 +384,9 @@ def get_supported_bases() -> list[tuple[str, str]]: """Return supported bases.""" return [("ubuntu", "26.04")] - @staticmethod @override - def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 - """Check if the extension is in an experimental state.""" - return True - - framework: str - actions: dict = { - "rotate-secret-key": { - "description": "Rotate the secret key. Users will be forced to log in again. This might be useful if a security breach occurs." - } - } - - options: dict - - endpoint_dynamic_options: dict[str, dict[str, Any]] = { - "oauth": OAUTH_DYNAMIC_OPTIONS - } - - def _get_nested(self, obj: dict, path: str) -> dict: - """Get a nested object using a path (a dot-separated list of keys).""" - for key in path.split("."): - obj = obj.get(key, {}) - return obj - - def _check_input(self) -> None: - """Check if the extension is applicable for user input charmcraft project file.""" - charm_type = self.yaml_data.get("type") - if charm_type != "charm": - raise ExtensionError( - 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( - f"the '{self.framework}-framework' extension is incompatible with " - f"customized charm part" - ) - incompatible_fields = { - "devices", - "extra-bindings", - "storage", - } & self.yaml_data.keys() - if incompatible_fields: - raise ExtensionError( - f"the '{self.framework}-framework' extension is incompatible with the provided " - f"field(s): {', '.join(sorted(incompatible_fields))}" - ) - root_snippet = self._get_root_snippet() - for protected in ("assumes", "containers", "resources", "peers"): - if ( - protected in self.yaml_data - and self.yaml_data[protected] != root_snippet[protected] - ): - raise ExtensionError( - f"{protected!r} in charmcraft.yaml conflicts with a reserved field " - f"in the {self.framework}-framework extension, please remove it." - ) - for merging in ("actions", "requires", "provides", "config.options"): - user_provided: dict[str, Any] = self._get_nested(self.yaml_data, merging) - if not user_provided: - continue - overlap = ( - user_provided.keys() & self._get_nested(root_snippet, merging).keys() - ) - if overlap: - raise ExtensionError( - f"overlapping keys {overlap} in {merging} of charmcraft.yaml " - f"which conflict with the {self.framework}-framework extension, " - "please rename or remove it" - ) - invalid_non_optionals = [] - for config in self._get_nested(self.yaml_data, "config.options"): - for reserved_config_prefix in ("webserver-", f"{self.framework}-"): - if config.startswith(reserved_config_prefix): - raise ExtensionError( - f"config.options {config!r} starts with {self.framework}-framework" - f" reserved configuration prefix {reserved_config_prefix!r}, " - "please rename or remove it" - ) - config_option_dict = self._get_nested( - self.yaml_data, f"config.options.{config}" - ) - if config_option_dict.get("optional") is False and config_option_dict.get( - "default" - ): - invalid_non_optionals.append(config) - - if invalid_non_optionals: - raise ExtensionError( - "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)." - ) - - 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)) - raise ExtensionError( - "custom COS directory must only contain the following subdirectories: " - f"{COS_SUBDIRS}. Found {'; '.join(details)}" - ) - def _get_root_snippet(self) -> dict[str, Any]: - """Return the root snippet to be merged into the user charmcraft.yaml. - - This method differs from get_root_snippet because it doesn't perform any check. - """ + """Return the root snippet to be merged into the user charmcraft.yaml.""" return { "assumes": ["k8s-api"], "containers": { @@ -568,8 +414,8 @@ def _get_root_snippet(self) -> dict[str, Any]: "charm": { "plugin": "uv", "source": ".", - "build-snaps": ["astral-uv", "rustup"], # Needed to build pydantic. - "override-build": ["rustup default stable", "craftctl default"], + "build-snaps": ["astral-uv", "rustup"], + "override-build": ["rustup default stable\ncraftctl default"], "uv-groups": ["charmlibs-pydeps"], }, **self.get_config_part(), @@ -590,67 +436,11 @@ def get_config_part(self) -> dict[str, Any]: } @override - def get_root_snippet(self) -> dict[str, Any]: - """Return the root snippet to be merged into the user charmcraft.yaml.""" - self._check_input() - root_snippet = self._get_root_snippet() - for interface_name, config_options in self.endpoint_dynamic_options.items(): - dynamic_config_options = self._get_dynamic_config_options( - root_snippet, interface_name, config_options - ) - root_snippet["config"]["options"].update(dynamic_config_options) - return root_snippet - - def _get_dynamic_config_options( - self, - root_snippet: dict[str, Any], - interface_name: str, - config_options: dict[str, Any], - ) -> dict[str, Any]: - dynamic_endpoint_names = [] - requires = self._get_nested(self.yaml_data, "requires") - for endpoint_name, require in requires.items(): - current_interface_name = require.get("interface") - if current_interface_name == interface_name: - dynamic_endpoint_names.append(endpoint_name) - - dynamic_config_options = {} - for endpoint_name in dynamic_endpoint_names: - updated_config_options = self._get_updated_dynamic_config_options( - endpoint_name, config_options - ) - dynamic_config_options.update(updated_config_options) - return dynamic_config_options - - def _get_updated_dynamic_config_options( - self, endpoint_name: str, config_options: dict[str, dict[str, Any]] - ) -> dict[str, dict[str, Any]]: - updated_config_options = {} - for option, value in config_options.items(): - updated_option = option.format(endpoint_name=endpoint_name) - updated_value = copy.deepcopy(value) - for value_key, value_item in value.items(): - if isinstance(value_item, str): - updated_value[value_key] = value_item.format( - endpoint_name=endpoint_name - ) - updated_config_options[updated_option] = updated_value - return updated_config_options - - @override - def get_part_snippet(self) -> dict[str, Any]: - """Return the part snippet to apply to existing parts.""" - return {} - - @override - def get_parts_snippet(self) -> dict[str, Any]: - """Return the parts to add to parts.""" - return {} - def get_image_name(self) -> str: """Return name of the app image.""" return "app-image" + @override def get_container_name(self) -> str: """Return name of the container for the app image.""" return "app" @@ -738,7 +528,7 @@ class FlaskFrameworkV2(_AppBaseV2): options = FlaskFramework.options -flask_framework_factory = _make_framework_factory(FlaskFramework, FlaskFrameworkV2) +flask_framework_factory = _FrameworkFactory(FlaskFramework, FlaskFrameworkV2) class DjangoFramework(_AppBase): @@ -792,7 +582,7 @@ class DjangoFrameworkV2(_AppBaseV2): options = DjangoFramework.options -django_framework_factory = _make_framework_factory(DjangoFramework, DjangoFrameworkV2) +django_framework_factory = _FrameworkFactory(DjangoFramework, DjangoFrameworkV2) class GoFramework(_AppBase): @@ -833,7 +623,7 @@ class GoFrameworkV2(_AppBaseV2): } -go_framework_factory = _make_framework_factory(GoFramework, GoFrameworkV2) +go_framework_factory = _FrameworkFactory(GoFramework, GoFrameworkV2) class FastAPIFramework(_AppBase): @@ -884,9 +674,7 @@ class FastAPIFrameworkV2(_AppBaseV2): options = FastAPIFramework.options -fastapi_framework_factory = _make_framework_factory( - FastAPIFramework, FastAPIFrameworkV2 -) +fastapi_framework_factory = _FrameworkFactory(FastAPIFramework, FastAPIFrameworkV2) class ExpressJSFramework(_AppBase): @@ -923,7 +711,7 @@ class ExpressJSFrameworkV2(_AppBaseV2): options = ExpressJSFramework.options -expressjs_framework_factory = _make_framework_factory( +expressjs_framework_factory = _FrameworkFactory( ExpressJSFramework, ExpressJSFrameworkV2 ) @@ -998,6 +786,6 @@ class SpringBootFrameworkV2(_AppBaseV2): endpoint_dynamic_options = SpringBootFramework.endpoint_dynamic_options -springboot_framework_factory = _make_framework_factory( +springboot_framework_factory = _FrameworkFactory( SpringBootFramework, SpringBootFrameworkV2 ) diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index f7fd967fd..8e80a1151 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -549,7 +549,7 @@ def test_go_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable", "craftctl default"], + "override-build": ["rustup default stable\ncraftctl default"], "uv-groups": ["charmlibs-pydeps"], } @@ -579,7 +579,7 @@ def test_flask_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable", "craftctl default"], + "override-build": ["rustup default stable\ncraftctl default"], "uv-groups": ["charmlibs-pydeps"], } @@ -609,7 +609,7 @@ def test_django_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable", "craftctl default"], + "override-build": ["rustup default stable\ncraftctl default"], "uv-groups": ["charmlibs-pydeps"], } @@ -639,7 +639,7 @@ def test_fastapi_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable", "craftctl default"], + "override-build": ["rustup default stable\ncraftctl default"], "uv-groups": ["charmlibs-pydeps"], } @@ -669,7 +669,7 @@ def test_expressjs_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable", "craftctl default"], + "override-build": ["rustup default stable\ncraftctl default"], "uv-groups": ["charmlibs-pydeps"], } @@ -699,7 +699,7 @@ def test_spring_boot_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable", "craftctl default"], + "override-build": ["rustup default stable\ncraftctl default"], "uv-groups": ["charmlibs-pydeps"], } From 4ab80ecf0d5469257e5b57c0c28453941177f230 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 15 Jun 2026 15:36:12 +0300 Subject: [PATCH 07/26] chore: Bring back old init templates and version the new ones --- .../init-django-framework-26.04/.gitignore.j2 | 9 +++ .../charmcraft.yaml.j2 | 81 +++++++++++++++++++ .../pyproject.toml.j2 | 70 ++++++++++++++++ .../src/charm.py.j2 | 30 +++++++ .../init-django-framework-26.04/tox.ini.j2 | 80 ++++++++++++++++++ .../init-django-framework/charmcraft.yaml.j2 | 2 +- .../init-django-framework/pyproject.toml.j2 | 29 ------- .../init-django-framework/requirements.txt.j2 | 2 + .../.gitignore.j2 | 9 +++ .../charmcraft.yaml.j2 | 80 ++++++++++++++++++ .../pyproject.toml.j2 | 70 ++++++++++++++++ .../src/charm.py.j2 | 30 +++++++ .../init-expressjs-framework-26.04/tox.ini.j2 | 80 ++++++++++++++++++ .../charmcraft.yaml.j2 | 2 +- .../pyproject.toml.j2 | 29 ------- .../requirements.txt.j2 | 2 + .../.gitignore.j2 | 9 +++ .../charmcraft.yaml.j2 | 81 +++++++++++++++++++ .../pyproject.toml.j2 | 70 ++++++++++++++++ .../src/charm.py.j2 | 30 +++++++ .../init-fastapi-framework-26.04/tox.ini.j2 | 80 ++++++++++++++++++ .../init-fastapi-framework/charmcraft.yaml.j2 | 2 +- .../init-fastapi-framework/pyproject.toml.j2 | 29 ------- .../requirements.txt.j2 | 2 + .../init-flask-framework-26.04/.gitignore.j2 | 9 +++ .../charmcraft.yaml.j2 | 80 ++++++++++++++++++ .../pyproject.toml.j2 | 70 ++++++++++++++++ .../src/charm.py.j2 | 30 +++++++ .../init-flask-framework-26.04/tox.ini.j2 | 80 ++++++++++++++++++ .../init-flask-framework/charmcraft.yaml.j2 | 2 +- .../init-flask-framework/pyproject.toml.j2 | 29 ------- .../init-flask-framework/requirements.txt.j2 | 2 + .../init-go-framework-26.04/.gitignore.j2 | 9 +++ .../charmcraft.yaml.j2 | 80 ++++++++++++++++++ .../init-go-framework-26.04/pyproject.toml.j2 | 70 ++++++++++++++++ .../init-go-framework-26.04/src/charm.py.j2 | 30 +++++++ .../init-go-framework-26.04/tox.ini.j2 | 80 ++++++++++++++++++ .../init-go-framework/charmcraft.yaml.j2 | 5 +- .../init-go-framework/pyproject.toml.j2 | 29 ------- .../init-go-framework/requirements.txt.j2 | 2 + .../.gitignore.j2 | 9 +++ .../charmcraft.yaml.j2 | 81 +++++++++++++++++++ .../pyproject.toml.j2 | 70 ++++++++++++++++ .../src/charm.py.j2 | 30 +++++++ .../tox.ini.j2 | 80 ++++++++++++++++++ .../charmcraft.yaml.j2 | 2 +- .../pyproject.toml.j2 | 29 ------- .../requirements.txt.j2 | 2 + 48 files changed, 1637 insertions(+), 181 deletions(-) create mode 100644 charmcraft/templates/init-django-framework-26.04/.gitignore.j2 create mode 100644 charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 create mode 100644 charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 create mode 100755 charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 create mode 100644 charmcraft/templates/init-django-framework-26.04/tox.ini.j2 create mode 100644 charmcraft/templates/init-django-framework/requirements.txt.j2 create mode 100644 charmcraft/templates/init-expressjs-framework-26.04/.gitignore.j2 create mode 100644 charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 create mode 100644 charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 create mode 100755 charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 create mode 100644 charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 create mode 100644 charmcraft/templates/init-expressjs-framework/requirements.txt.j2 create mode 100644 charmcraft/templates/init-fastapi-framework-26.04/.gitignore.j2 create mode 100644 charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 create mode 100644 charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 create mode 100755 charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 create mode 100644 charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 create mode 100644 charmcraft/templates/init-fastapi-framework/requirements.txt.j2 create mode 100644 charmcraft/templates/init-flask-framework-26.04/.gitignore.j2 create mode 100644 charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 create mode 100644 charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 create mode 100755 charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 create mode 100644 charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 create mode 100644 charmcraft/templates/init-flask-framework/requirements.txt.j2 create mode 100644 charmcraft/templates/init-go-framework-26.04/.gitignore.j2 create mode 100644 charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 create mode 100644 charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 create mode 100755 charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 create mode 100644 charmcraft/templates/init-go-framework-26.04/tox.ini.j2 create mode 100644 charmcraft/templates/init-go-framework/requirements.txt.j2 create mode 100644 charmcraft/templates/init-spring-boot-framework-26.04/.gitignore.j2 create mode 100644 charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 create mode 100644 charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 create mode 100755 charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 create mode 100644 charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 create mode 100644 charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 diff --git a/charmcraft/templates/init-django-framework-26.04/.gitignore.j2 b/charmcraft/templates/init-django-framework-26.04/.gitignore.j2 new file mode 100644 index 000000000..a26d707f9 --- /dev/null +++ b/charmcraft/templates/init-django-framework-26.04/.gitignore.j2 @@ -0,0 +1,9 @@ +venv/ +build/ +*.charm +.tox/ +.coverage +__pycache__/ +*.py[cod] +.idea +.vscode/ diff --git a/charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 new file mode 100644 index 000000000..0e8af5956 --- /dev/null +++ b/charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 @@ -0,0 +1,81 @@ +# This file configures Charmcraft. +# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com + +name: {{ name }} + +type: charm + +base: ubuntu@26.04 + +# the platforms this charm should be built on and run on. +# you can check your architecture with `dpkg --print-architecture` +platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + +# (Required) +summary: A very short one-line summary of the Django application. + +# (Required) +description: | + A comprehensive overview of your Django application. + +extensions: + - django-framework + +# Uncomment the integrations used by your application +# Integrations set to "optional: false" will block the charm +# until the applications are integrated. +# requires: +# mysql: +# interface: mysql_client +# optional: false +# limit: 1 +# postgresql: +# interface: postgresql_client +# optional: false +# limit: 1 +# mongodb: +# interface: mongodb_client +# optional: false +# limit: 1 +# redis: +# interface: redis +# optional: false +# limit: 1 +# valkey: +# interface: valkey_client +# optional: true +# limit: 1 +# s3: +# interface: s3 +# optional: false +# limit: 1 +# saml: +# interface: saml +# optional: false +# limit: 1 +# rabbitmq: +# interface: rabbitmq +# optional: false +# limit: 1 +# tracing: +# interface: tracing +# optional: true +# limit: 1 +# smtp: +# interface: smtp +# optional: false +# limit: 1 +# openfga: +# interface: openfga +# optional: false +# limit: 1 +# http-proxy: +# interface: http_proxy +# optional: true +# limit: 1 + diff --git a/charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 new file mode 100644 index 000000000..85441d1bd --- /dev/null +++ b/charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 @@ -0,0 +1,70 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Linting tools configuration +[tool.ruff] +line-length = 99 +lint.select = ["E", "W", "F", "C", "N", "D", "I001"] +lint.ignore = [ + "D105", + "D107", + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +extend-exclude = ["__pycache__", "*.egg_info"] +lint.per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.pyright] +include = ["src/**.py"] diff --git a/charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 new file mode 100755 index 000000000..359b47307 --- /dev/null +++ b/charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +"""Django Charm entrypoint.""" + +import logging +import typing + +import ops + +import paas_charm.django + +logger = logging.getLogger(__name__) + + +class {{ class_name }}(paas_charm.django.Charm): + """Django Charm service.""" + + def __init__(self, *args: typing.Any) -> None: + """Initialize the instance. + + Args: + args: passthrough to CharmBase. + """ + super().__init__(*args) + + +if __name__ == "__main__": + ops.main({{ class_name }}) diff --git a/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 new file mode 100644 index 000000000..bd15d819c --- /dev/null +++ b/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 @@ -0,0 +1,80 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, static +min_version = 4.0.0 + +[vars] +src_path = {tox_root}/src +;tests_path = {tox_root}/tests +all_path = {[vars]src_path} + +[testenv] +set_env = + PYTHONPATH = {tox_root}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:format] +description = Apply coding style standards to code +deps = + ruff +commands = + ruff format {[vars]all_path} + ruff check --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + ruff + codespell +commands = + codespell {tox_root} + ruff check {[vars]all_path} + ruff format --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + coverage[toml] + -r {tox_root}/requirements.txt +commands = + coverage run --source={[vars]src_path} \ + -m pytest \ + --tb native \ + -v \ + -s \ + {posargs} \ + {[vars]tests_path}/unit + coverage report + +[testenv:static] +description = Run static type checks +deps = + pyright + -r {tox_root}/requirements.txt +commands = + pyright {posargs} + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration diff --git a/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 index 0e8af5956..5bccbe6ee 100644 --- a/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-django-framework/pyproject.toml.j2 b/charmcraft/templates/init-django-framework/pyproject.toml.j2 index 85441d1bd..660ac9996 100644 --- a/charmcraft/templates/init-django-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-django-framework/pyproject.toml.j2 @@ -1,32 +1,3 @@ -# Copyright {{ year }} {{ author }} -# See LICENSE file for licensing details. - -[project] -name = "{{ name }}" -version = "0.0.1" -requires-python = ">=3.10" - -# Dependencies of the charm code -# You should list the dependencies of the code in src/, including any charm libraries from PyPI. -# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. -# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a -# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, -# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. -dependencies = [ - "dpcharmlibs-interfaces==1.0.2", - "jinja2==3.1.6", - "jsonschema==4.26", - "ops==3.7.1", - "paas-charm>=1.0", -] -[dependency-groups] -# PYDEPS from libraries that the charm uses. -charmlibs-pydeps = [ - "cosl==1.9.1", - "pydantic==2.13.3", -] - - # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-django-framework/requirements.txt.j2 b/charmcraft/templates/init-django-framework/requirements.txt.j2 new file mode 100644 index 000000000..d58a30c21 --- /dev/null +++ b/charmcraft/templates/init-django-framework/requirements.txt.j2 @@ -0,0 +1,2 @@ +ops ~= 2.17 +paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-expressjs-framework-26.04/.gitignore.j2 b/charmcraft/templates/init-expressjs-framework-26.04/.gitignore.j2 new file mode 100644 index 000000000..a26d707f9 --- /dev/null +++ b/charmcraft/templates/init-expressjs-framework-26.04/.gitignore.j2 @@ -0,0 +1,9 @@ +venv/ +build/ +*.charm +.tox/ +.coverage +__pycache__/ +*.py[cod] +.idea +.vscode/ diff --git a/charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 new file mode 100644 index 000000000..3908c4202 --- /dev/null +++ b/charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 @@ -0,0 +1,80 @@ +# This file configures Charmcraft. +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com + +name: {{ name }} + +type: charm + +base: ubuntu@26.04 + +# the platforms this charm should be built on and run on. +# you can check your architecture with `dpkg --print-architecture` +platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + +# (Required) +summary: A very short one-line summary of the ExpressJS application. + +# (Required) +description: | + A comprehensive overview of your ExpressJS application. + +extensions: + - expressjs-framework + +# Uncomment the integrations used by your application +# Integrations set to "optional: false" will block the charm +# until the applications are integrated. +# requires: +# mysql: +# interface: mysql_client +# optional: false +# limit: 1 +# postgresql: +# interface: postgresql_client +# optional: false +# limit: 1 +# mongodb: +# interface: mongodb_client +# optional: false +# limit: 1 +# redis: +# interface: redis +# optional: false +# limit: 1 +# valkey: +# interface: valkey_client +# optional: true +# limit: 1 +# s3: +# interface: s3 +# optional: false +# limit: 1 +# saml: +# interface: saml +# optional: false +# limit: 1 +# rabbitmq: +# interface: rabbitmq +# optional: false +# limit: 1 +# tracing: +# interface: tracing +# optional: true +# limit: 1 +# smtp: +# interface: smtp +# optional: false +# limit: 1 +# openfga: +# interface: openfga +# optional: false +# limit: 1 +# http-proxy: +# interface: http_proxy +# optional: true +# limit: 1 diff --git a/charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 new file mode 100644 index 000000000..85441d1bd --- /dev/null +++ b/charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 @@ -0,0 +1,70 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Linting tools configuration +[tool.ruff] +line-length = 99 +lint.select = ["E", "W", "F", "C", "N", "D", "I001"] +lint.ignore = [ + "D105", + "D107", + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +extend-exclude = ["__pycache__", "*.egg_info"] +lint.per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.pyright] +include = ["src/**.py"] diff --git a/charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 new file mode 100755 index 000000000..798b68376 --- /dev/null +++ b/charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +"""ExpressJS Charm entrypoint.""" + +import logging +import typing + +import ops + +import paas_charm.expressjs + +logger = logging.getLogger(__name__) + + +class {{ class_name }}(paas_charm.expressjs.Charm): + """ExpressJS Charm service.""" + + def __init__(self, *args: typing.Any) -> None: + """Initialize the instance. + + Args: + args: passthrough to CharmBase. + """ + super().__init__(*args) + + +if __name__ == "__main__": + ops.main({{ class_name }}) diff --git a/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 new file mode 100644 index 000000000..bd15d819c --- /dev/null +++ b/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 @@ -0,0 +1,80 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, static +min_version = 4.0.0 + +[vars] +src_path = {tox_root}/src +;tests_path = {tox_root}/tests +all_path = {[vars]src_path} + +[testenv] +set_env = + PYTHONPATH = {tox_root}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:format] +description = Apply coding style standards to code +deps = + ruff +commands = + ruff format {[vars]all_path} + ruff check --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + ruff + codespell +commands = + codespell {tox_root} + ruff check {[vars]all_path} + ruff format --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + coverage[toml] + -r {tox_root}/requirements.txt +commands = + coverage run --source={[vars]src_path} \ + -m pytest \ + --tb native \ + -v \ + -s \ + {posargs} \ + {[vars]tests_path}/unit + coverage report + +[testenv:static] +description = Run static type checks +deps = + pyright + -r {tox_root}/requirements.txt +commands = + pyright {posargs} + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration diff --git a/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 index 3908c4202..7af066512 100644 --- a/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 b/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 index 85441d1bd..660ac9996 100644 --- a/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-expressjs-framework/pyproject.toml.j2 @@ -1,32 +1,3 @@ -# Copyright {{ year }} {{ author }} -# See LICENSE file for licensing details. - -[project] -name = "{{ name }}" -version = "0.0.1" -requires-python = ">=3.10" - -# Dependencies of the charm code -# You should list the dependencies of the code in src/, including any charm libraries from PyPI. -# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. -# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a -# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, -# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. -dependencies = [ - "dpcharmlibs-interfaces==1.0.2", - "jinja2==3.1.6", - "jsonschema==4.26", - "ops==3.7.1", - "paas-charm>=1.0", -] -[dependency-groups] -# PYDEPS from libraries that the charm uses. -charmlibs-pydeps = [ - "cosl==1.9.1", - "pydantic==2.13.3", -] - - # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-expressjs-framework/requirements.txt.j2 b/charmcraft/templates/init-expressjs-framework/requirements.txt.j2 new file mode 100644 index 000000000..113bb427b --- /dev/null +++ b/charmcraft/templates/init-expressjs-framework/requirements.txt.j2 @@ -0,0 +1,2 @@ +ops>2.17,<4 +paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-fastapi-framework-26.04/.gitignore.j2 b/charmcraft/templates/init-fastapi-framework-26.04/.gitignore.j2 new file mode 100644 index 000000000..a26d707f9 --- /dev/null +++ b/charmcraft/templates/init-fastapi-framework-26.04/.gitignore.j2 @@ -0,0 +1,9 @@ +venv/ +build/ +*.charm +.tox/ +.coverage +__pycache__/ +*.py[cod] +.idea +.vscode/ diff --git a/charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 new file mode 100644 index 000000000..11c2f10fe --- /dev/null +++ b/charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 @@ -0,0 +1,81 @@ +# This file configures Charmcraft. +# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com + +name: {{ name }} + +type: charm + +base: ubuntu@26.04 + +# the platforms this charm should be built on and run on. +# you can check your architecture with `dpkg --print-architecture` +platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + +# (Required) +summary: A very short one-line summary of the FastAPI application. + +# (Required) +description: | + A comprehensive overview of your FastAPI application. + +extensions: + - fastapi-framework + +# Uncomment the integrations used by your application +# Integrations set to "optional: false" will block the charm +# until the applications are integrated. +# requires: +# mysql: +# interface: mysql_client +# optional: false +# limit: 1 +# postgresql: +# interface: postgresql_client +# optional: false +# limit: 1 +# mongodb: +# interface: mongodb_client +# optional: false +# limit: 1 +# redis: +# interface: redis +# optional: false +# limit: 1 +# valkey: +# interface: valkey_client +# optional: true +# limit: 1 +# s3: +# interface: s3 +# optional: false +# limit: 1 +# saml: +# interface: saml +# optional: false +# limit: 1 +# rabbitmq: +# interface: rabbitmq +# optional: false +# limit: 1 +# tracing: +# interface: tracing +# optional: true +# limit: 1 +# smtp: +# interface: smtp +# optional: false +# limit: 1 +# openfga: +# interface: openfga +# optional: false +# limit: 1 +# http-proxy: +# interface: http_proxy +# optional: true +# limit: 1 + diff --git a/charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 new file mode 100644 index 000000000..85441d1bd --- /dev/null +++ b/charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 @@ -0,0 +1,70 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Linting tools configuration +[tool.ruff] +line-length = 99 +lint.select = ["E", "W", "F", "C", "N", "D", "I001"] +lint.ignore = [ + "D105", + "D107", + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +extend-exclude = ["__pycache__", "*.egg_info"] +lint.per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.pyright] +include = ["src/**.py"] diff --git a/charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 new file mode 100755 index 000000000..84f9fa77a --- /dev/null +++ b/charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +"""FastAPI Charm entrypoint.""" + +import logging +import typing + +import ops + +import paas_charm.fastapi + +logger = logging.getLogger(__name__) + + +class {{ class_name }}(paas_charm.fastapi.Charm): + """FastAPI Charm service.""" + + def __init__(self, *args: typing.Any) -> None: + """Initialize the instance. + + Args: + args: passthrough to CharmBase. + """ + super().__init__(*args) + + +if __name__ == "__main__": + ops.main({{ class_name }}) diff --git a/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 new file mode 100644 index 000000000..bd15d819c --- /dev/null +++ b/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 @@ -0,0 +1,80 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, static +min_version = 4.0.0 + +[vars] +src_path = {tox_root}/src +;tests_path = {tox_root}/tests +all_path = {[vars]src_path} + +[testenv] +set_env = + PYTHONPATH = {tox_root}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:format] +description = Apply coding style standards to code +deps = + ruff +commands = + ruff format {[vars]all_path} + ruff check --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + ruff + codespell +commands = + codespell {tox_root} + ruff check {[vars]all_path} + ruff format --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + coverage[toml] + -r {tox_root}/requirements.txt +commands = + coverage run --source={[vars]src_path} \ + -m pytest \ + --tb native \ + -v \ + -s \ + {posargs} \ + {[vars]tests_path}/unit + coverage report + +[testenv:static] +description = Run static type checks +deps = + pyright + -r {tox_root}/requirements.txt +commands = + pyright {posargs} + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration diff --git a/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 index 11c2f10fe..f64467e1a 100644 --- a/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 b/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 index 85441d1bd..660ac9996 100644 --- a/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-fastapi-framework/pyproject.toml.j2 @@ -1,32 +1,3 @@ -# Copyright {{ year }} {{ author }} -# See LICENSE file for licensing details. - -[project] -name = "{{ name }}" -version = "0.0.1" -requires-python = ">=3.10" - -# Dependencies of the charm code -# You should list the dependencies of the code in src/, including any charm libraries from PyPI. -# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. -# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a -# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, -# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. -dependencies = [ - "dpcharmlibs-interfaces==1.0.2", - "jinja2==3.1.6", - "jsonschema==4.26", - "ops==3.7.1", - "paas-charm>=1.0", -] -[dependency-groups] -# PYDEPS from libraries that the charm uses. -charmlibs-pydeps = [ - "cosl==1.9.1", - "pydantic==2.13.3", -] - - # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-fastapi-framework/requirements.txt.j2 b/charmcraft/templates/init-fastapi-framework/requirements.txt.j2 new file mode 100644 index 000000000..d58a30c21 --- /dev/null +++ b/charmcraft/templates/init-fastapi-framework/requirements.txt.j2 @@ -0,0 +1,2 @@ +ops ~= 2.17 +paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-flask-framework-26.04/.gitignore.j2 b/charmcraft/templates/init-flask-framework-26.04/.gitignore.j2 new file mode 100644 index 000000000..a26d707f9 --- /dev/null +++ b/charmcraft/templates/init-flask-framework-26.04/.gitignore.j2 @@ -0,0 +1,9 @@ +venv/ +build/ +*.charm +.tox/ +.coverage +__pycache__/ +*.py[cod] +.idea +.vscode/ diff --git a/charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 new file mode 100644 index 000000000..421dbea64 --- /dev/null +++ b/charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 @@ -0,0 +1,80 @@ +# This file configures Charmcraft. +# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com + +name: {{ name }} + +type: charm + +base: ubuntu@26.04 + +# the platforms this charm should be built on and run on. +# you can check your architecture with `dpkg --print-architecture` +platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + +# (Required) +summary: A very short one-line summary of the Flask application. + +# (Required) +description: | + A comprehensive overview of your Flask application. + +extensions: + - flask-framework + +# Uncomment the integrations used by your application +# Integrations set to "optional: false" will block the charm +# until the applications are integrated. +# requires: +# mysql: +# interface: mysql_client +# optional: false +# limit: 1 +# postgresql: +# interface: postgresql_client +# optional: false +# limit: 1 +# mongodb: +# interface: mongodb_client +# optional: false +# limit: 1 +# redis: +# interface: redis +# optional: false +# limit: 1 +# valkey: +# interface: valkey_client +# optional: true +# limit: 1 +# s3: +# interface: s3 +# optional: false +# limit: 1 +# saml: +# interface: saml +# optional: false +# limit: 1 +# rabbitmq: +# interface: rabbitmq +# optional: false +# limit: 1 +# tracing: +# interface: tracing +# optional: true +# limit: 1 +# smtp: +# interface: smtp +# optional: false +# limit: 1 +# openfga: +# interface: openfga +# optional: false +# limit: 1 +# http-proxy: +# interface: http_proxy +# optional: true +# limit: 1 diff --git a/charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 new file mode 100644 index 000000000..85441d1bd --- /dev/null +++ b/charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 @@ -0,0 +1,70 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Linting tools configuration +[tool.ruff] +line-length = 99 +lint.select = ["E", "W", "F", "C", "N", "D", "I001"] +lint.ignore = [ + "D105", + "D107", + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +extend-exclude = ["__pycache__", "*.egg_info"] +lint.per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.pyright] +include = ["src/**.py"] diff --git a/charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 new file mode 100755 index 000000000..94cb3f33f --- /dev/null +++ b/charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +"""Flask Charm entrypoint.""" + +import logging +import typing + +import ops + +import paas_charm.flask + +logger = logging.getLogger(__name__) + + +class {{ class_name }}(paas_charm.flask.Charm): + """Flask Charm service.""" + + def __init__(self, *args: typing.Any) -> None: + """Initialize the instance. + + Args: + args: passthrough to CharmBase. + """ + super().__init__(*args) + + +if __name__ == "__main__": + ops.main({{ class_name }}) diff --git a/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 new file mode 100644 index 000000000..bd15d819c --- /dev/null +++ b/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 @@ -0,0 +1,80 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, static +min_version = 4.0.0 + +[vars] +src_path = {tox_root}/src +;tests_path = {tox_root}/tests +all_path = {[vars]src_path} + +[testenv] +set_env = + PYTHONPATH = {tox_root}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:format] +description = Apply coding style standards to code +deps = + ruff +commands = + ruff format {[vars]all_path} + ruff check --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + ruff + codespell +commands = + codespell {tox_root} + ruff check {[vars]all_path} + ruff format --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + coverage[toml] + -r {tox_root}/requirements.txt +commands = + coverage run --source={[vars]src_path} \ + -m pytest \ + --tb native \ + -v \ + -s \ + {posargs} \ + {[vars]tests_path}/unit + coverage report + +[testenv:static] +description = Run static type checks +deps = + pyright + -r {tox_root}/requirements.txt +commands = + pyright {posargs} + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration diff --git a/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 index 421dbea64..005f12446 100644 --- a/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-flask-framework/pyproject.toml.j2 b/charmcraft/templates/init-flask-framework/pyproject.toml.j2 index 85441d1bd..660ac9996 100644 --- a/charmcraft/templates/init-flask-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-flask-framework/pyproject.toml.j2 @@ -1,32 +1,3 @@ -# Copyright {{ year }} {{ author }} -# See LICENSE file for licensing details. - -[project] -name = "{{ name }}" -version = "0.0.1" -requires-python = ">=3.10" - -# Dependencies of the charm code -# You should list the dependencies of the code in src/, including any charm libraries from PyPI. -# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. -# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a -# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, -# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. -dependencies = [ - "dpcharmlibs-interfaces==1.0.2", - "jinja2==3.1.6", - "jsonschema==4.26", - "ops==3.7.1", - "paas-charm>=1.0", -] -[dependency-groups] -# PYDEPS from libraries that the charm uses. -charmlibs-pydeps = [ - "cosl==1.9.1", - "pydantic==2.13.3", -] - - # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-flask-framework/requirements.txt.j2 b/charmcraft/templates/init-flask-framework/requirements.txt.j2 new file mode 100644 index 000000000..d58a30c21 --- /dev/null +++ b/charmcraft/templates/init-flask-framework/requirements.txt.j2 @@ -0,0 +1,2 @@ +ops ~= 2.17 +paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-go-framework-26.04/.gitignore.j2 b/charmcraft/templates/init-go-framework-26.04/.gitignore.j2 new file mode 100644 index 000000000..a26d707f9 --- /dev/null +++ b/charmcraft/templates/init-go-framework-26.04/.gitignore.j2 @@ -0,0 +1,9 @@ +venv/ +build/ +*.charm +.tox/ +.coverage +__pycache__/ +*.py[cod] +.idea +.vscode/ diff --git a/charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 new file mode 100644 index 000000000..3865729fc --- /dev/null +++ b/charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 @@ -0,0 +1,80 @@ +# This file configures Charmcraft. +# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com + +name: {{ name }} + +type: charm + +base: ubuntu@26.04 + +# the platforms this charm should be built on and run on. +# you can check your architecture with `dpkg --print-architecture` +platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + +# (Required) +summary: A very short one-line summary of the Go application. + +# (Required) +description: | + A comprehensive overview of your Go application. + +extensions: + - go-framework + +# Uncomment the integrations used by your application +# Integrations set to "optional: false" will block the charm +# until the applications are integrated. +# requires: +# mysql: +# interface: mysql_client +# optional: false +# limit: 1 +# postgresql: +# interface: postgresql_client +# optional: false +# limit: 1 +# mongodb: +# interface: mongodb_client +# optional: false +# limit: 1 +# redis: +# interface: redis +# optional: false +# limit: 1 +# valkey: +# interface: valkey_client +# optional: true +# limit: 1 +# s3: +# interface: s3 +# optional: false +# limit: 1 +# saml: +# interface: saml +# optional: false +# limit: 1 +# rabbitmq: +# interface: rabbitmq +# optional: false +# limit: 1 +# tracing: +# interface: tracing +# optional: true +# limit: 1 +# smtp: +# interface: smtp +# optional: false +# limit: 1 +# openfga: +# interface: openfga +# optional: false +# limit: 1 +# http-proxy: +# interface: http_proxy +# optional: true +# limit: 1 diff --git a/charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 new file mode 100644 index 000000000..85441d1bd --- /dev/null +++ b/charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 @@ -0,0 +1,70 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Linting tools configuration +[tool.ruff] +line-length = 99 +lint.select = ["E", "W", "F", "C", "N", "D", "I001"] +lint.ignore = [ + "D105", + "D107", + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +extend-exclude = ["__pycache__", "*.egg_info"] +lint.per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.pyright] +include = ["src/**.py"] diff --git a/charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 new file mode 100755 index 000000000..c32223b7e --- /dev/null +++ b/charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +"""Go Charm entrypoint.""" + +import logging +import typing + +import ops + +import paas_charm.go + +logger = logging.getLogger(__name__) + + +class {{ class_name }}(paas_charm.go.Charm): + """Go Charm service.""" + + def __init__(self, *args: typing.Any) -> None: + """Initialize the instance. + + Args: + args: passthrough to CharmBase. + """ + super().__init__(*args) + + +if __name__ == "__main__": + ops.main({{ class_name }}) diff --git a/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 new file mode 100644 index 000000000..bd15d819c --- /dev/null +++ b/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 @@ -0,0 +1,80 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, static +min_version = 4.0.0 + +[vars] +src_path = {tox_root}/src +;tests_path = {tox_root}/tests +all_path = {[vars]src_path} + +[testenv] +set_env = + PYTHONPATH = {tox_root}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:format] +description = Apply coding style standards to code +deps = + ruff +commands = + ruff format {[vars]all_path} + ruff check --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + ruff + codespell +commands = + codespell {tox_root} + ruff check {[vars]all_path} + ruff format --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + coverage[toml] + -r {tox_root}/requirements.txt +commands = + coverage run --source={[vars]src_path} \ + -m pytest \ + --tb native \ + -v \ + -s \ + {posargs} \ + {[vars]tests_path}/unit + coverage report + +[testenv:static] +description = Run static type checks +deps = + pyright + -r {tox_root}/requirements.txt +commands = + pyright {posargs} + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration diff --git a/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 index 3865729fc..bb44201f1 100644 --- a/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 @@ -1,12 +1,12 @@ # This file configures Charmcraft. # See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` @@ -78,3 +78,4 @@ extensions: # interface: http_proxy # optional: true # limit: 1 + diff --git a/charmcraft/templates/init-go-framework/pyproject.toml.j2 b/charmcraft/templates/init-go-framework/pyproject.toml.j2 index 85441d1bd..660ac9996 100644 --- a/charmcraft/templates/init-go-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-go-framework/pyproject.toml.j2 @@ -1,32 +1,3 @@ -# Copyright {{ year }} {{ author }} -# See LICENSE file for licensing details. - -[project] -name = "{{ name }}" -version = "0.0.1" -requires-python = ">=3.10" - -# Dependencies of the charm code -# You should list the dependencies of the code in src/, including any charm libraries from PyPI. -# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. -# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a -# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, -# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. -dependencies = [ - "dpcharmlibs-interfaces==1.0.2", - "jinja2==3.1.6", - "jsonschema==4.26", - "ops==3.7.1", - "paas-charm>=1.0", -] -[dependency-groups] -# PYDEPS from libraries that the charm uses. -charmlibs-pydeps = [ - "cosl==1.9.1", - "pydantic==2.13.3", -] - - # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-go-framework/requirements.txt.j2 b/charmcraft/templates/init-go-framework/requirements.txt.j2 new file mode 100644 index 000000000..d58a30c21 --- /dev/null +++ b/charmcraft/templates/init-go-framework/requirements.txt.j2 @@ -0,0 +1,2 @@ +ops ~= 2.17 +paas-charm>=1.0,<2 diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/.gitignore.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/.gitignore.j2 new file mode 100644 index 000000000..a26d707f9 --- /dev/null +++ b/charmcraft/templates/init-spring-boot-framework-26.04/.gitignore.j2 @@ -0,0 +1,9 @@ +venv/ +build/ +*.charm +.tox/ +.coverage +__pycache__/ +*.py[cod] +.idea +.vscode/ diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 new file mode 100644 index 000000000..737fe302b --- /dev/null +++ b/charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 @@ -0,0 +1,81 @@ +# This file configures Charmcraft. +# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com + +name: {{ name }} + +type: charm + +base: ubuntu@26.04 + +# the platforms this charm should be built on and run on. +# you can check your architecture with `dpkg --print-architecture` +platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + +# (Required) +summary: A very short one-line summary of the Spring Boot application. + +# (Required) +description: | + A comprehensive overview of your Spring Boot application. + +extensions: + - spring-boot-framework + +# Uncomment the integrations used by your application +# Integrations set to "optional: false" will block the charm +# until the applications are integrated. +# requires: +# mysql: +# interface: mysql_client +# optional: false +# limit: 1 +# postgresql: +# interface: postgresql_client +# optional: false +# limit: 1 +# mongodb: +# interface: mongodb_client +# optional: false +# limit: 1 +# redis: +# interface: redis +# optional: false +# limit: 1 +# valkey: +# interface: valkey_client +# optional: true +# limit: 1 +# s3: +# interface: s3 +# optional: false +# limit: 1 +# saml: +# interface: saml +# optional: false +# limit: 1 +# rabbitmq: +# interface: rabbitmq +# optional: false +# limit: 1 +# tracing: +# interface: tracing +# optional: true +# limit: 1 +# smtp: +# interface: smtp +# optional: false +# limit: 1 +# openfga: +# interface: openfga +# optional: false +# limit: 1 +# http-proxy: +# interface: http_proxy +# optional: true +# limit: 1 + diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 new file mode 100644 index 000000000..46991a3ea --- /dev/null +++ b/charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 @@ -0,0 +1,70 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[project] +name = "{{ name }}" +version = "0.0.1" +requires-python = ">=3.10" + +# Dependencies of the charm code +# You should list the dependencies of the code in src/, including any charm libraries from PyPI. +# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. +# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a +# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, +# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. +dependencies = [ + "dpcharmlibs-interfaces==1.0.2", + "jinja2==3.1.6", + "jsonschema==4.26", + "ops==3.7.1", + "paas-charm>=1.0", +] +[dependency-groups] +# PYDEPS from libraries that the charm uses. +charmlibs-pydeps = [ + "cosl==1.9.1", + "pydantic==2.13.3", +] + + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Linting tools configuration +[tool.ruff] +line-length = 99 +lint.select = ["E", "W", "F", "C", "N", "D", "I001"] +lint.extend-ignore = [ + "D105", + "D107", + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", +] +extend-exclude = ["__pycache__", "*.egg_info"] +lint.per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.codespell] +skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" + +[tool.pyright] +include = ["src/**.py"] diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 new file mode 100755 index 000000000..cb3178463 --- /dev/null +++ b/charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +"""Spring Boot Charm entrypoint.""" + +import logging +import typing + +import ops + +import paas_charm.springboot + +logger = logging.getLogger(__name__) + + +class {{ class_name }}(paas_charm.springboot.Charm): + """Spring Boot Charm service.""" + + def __init__(self, *args: typing.Any) -> None: + """Initialize the instance. + + Args: + args: passthrough to CharmBase. + """ + super().__init__(*args) + + +if __name__ == "__main__": + ops.main({{ class_name }}) diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 new file mode 100644 index 000000000..bd15d819c --- /dev/null +++ b/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 @@ -0,0 +1,80 @@ +# Copyright {{ year }} {{ author }} +# See LICENSE file for licensing details. + +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, static +min_version = 4.0.0 + +[vars] +src_path = {tox_root}/src +;tests_path = {tox_root}/tests +all_path = {[vars]src_path} + +[testenv] +set_env = + PYTHONPATH = {tox_root}/lib:{[vars]src_path} + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + CHARM_BUILD_DIR + MODEL_SETTINGS + +[testenv:format] +description = Apply coding style standards to code +deps = + ruff +commands = + ruff format {[vars]all_path} + ruff check --fix {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + ruff + codespell +commands = + codespell {tox_root} + ruff check {[vars]all_path} + ruff format --check --diff {[vars]all_path} + +[testenv:unit] +description = Run unit tests +deps = + pytest + coverage[toml] + -r {tox_root}/requirements.txt +commands = + coverage run --source={[vars]src_path} \ + -m pytest \ + --tb native \ + -v \ + -s \ + {posargs} \ + {[vars]tests_path}/unit + coverage report + +[testenv:static] +description = Run static type checks +deps = + pyright + -r {tox_root}/requirements.txt +commands = + pyright {posargs} + +[testenv:integration] +description = Run integration tests +deps = + pytest + juju + pytest-operator + -r {tox_root}/requirements.txt +commands = + pytest -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {posargs} \ + {[vars]tests_path}/integration diff --git a/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 index 737fe302b..e955623d5 100644 --- a/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 @@ -6,7 +6,7 @@ name: {{ name }} type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 # the platforms this charm should be built on and run on. # you can check your architecture with `dpkg --print-architecture` diff --git a/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 b/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 index 46991a3ea..3cb1ce223 100644 --- a/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 +++ b/charmcraft/templates/init-spring-boot-framework/pyproject.toml.j2 @@ -1,32 +1,3 @@ -# Copyright {{ year }} {{ author }} -# See LICENSE file for licensing details. - -[project] -name = "{{ name }}" -version = "0.0.1" -requires-python = ">=3.10" - -# Dependencies of the charm code -# You should list the dependencies of the code in src/, including any charm libraries from PyPI. -# We recommend using uv to maintain this list. For example, 'uv add charmlibs-pathops'. -# If your code uses any libraries from Charmhub, don't list those libraries here. Instead, add a -# 'charm-libs' block in charmcraft.yaml, run `charmcraft fetch-libs` to download the libraries, -# then inspect the libraries for dependencies specified in PYDEPS. List those dependencies here. -dependencies = [ - "dpcharmlibs-interfaces==1.0.2", - "jinja2==3.1.6", - "jsonschema==4.26", - "ops==3.7.1", - "paas-charm>=1.0", -] -[dependency-groups] -# PYDEPS from libraries that the charm uses. -charmlibs-pydeps = [ - "cosl==1.9.1", - "pydantic==2.13.3", -] - - # Testing tools configuration [tool.coverage.run] branch = true diff --git a/charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 b/charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 new file mode 100644 index 000000000..d58a30c21 --- /dev/null +++ b/charmcraft/templates/init-spring-boot-framework/requirements.txt.j2 @@ -0,0 +1,2 @@ +ops ~= 2.17 +paas-charm>=1.0,<2 From 80d88cdbbb806b1b014416c47620d6981b1079b7 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 15 Jun 2026 16:11:25 +0300 Subject: [PATCH 08/26] chore: set 26.04 and 26.10 as experimental for charm plugin --- charmcraft/application/main.py | 15 +++++++++++++-- charmcraft/const.py | 10 ++++++++++ charmcraft/extensions/app.py | 8 ++++++++ charmcraft/models/project.py | 15 +++++++++++++-- .../multibase-charm-plugin/charmcraft.yaml | 3 --- .../charmcraft.yaml | 13 ------------- .../errors.json | 18 ------------------ .../platforms-resolute-charm/charmcraft.yaml | 2 +- .../platforms-resolute-charm/expected.yaml | 2 +- .../charmcraft.yaml | 2 +- .../platforms-resolute-reactive/expected.yaml | 2 +- tests/integration/test_application.py | 7 +------ tests/unit/models/test_project.py | 6 ++++-- 13 files changed, 53 insertions(+), 50 deletions(-) delete mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml delete mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json diff --git a/charmcraft/application/main.py b/charmcraft/application/main.py index 7727d960d..47abad77c 100644 --- a/charmcraft/application/main.py +++ b/charmcraft/application/main.py @@ -18,6 +18,7 @@ from __future__ import annotations import datetime +import os from typing import Any import craft_application @@ -131,9 +132,19 @@ def _get_app_plugins(self) -> dict[str, PluginType]: except ProjectFileMissingError: return plugins bases = {str(build_info.build_base) for build_info in full_build_plan} - if any(base not in const.CHARM_PLUGIN_BASES for base in bases): + effective_charm_bases = const.CHARM_PLUGIN_BASES + if os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR): + effective_charm_bases = ( + effective_charm_bases | const.CHARM_PLUGIN_EXPERIMENTAL_BASES + ) + if any(base not in effective_charm_bases for base in bases): plugins.pop("charm", None) - if any(base not in const.REACTIVE_PLUGIN_BASES for base in bases): + effective_reactive_bases = const.REACTIVE_PLUGIN_BASES + if os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR): + effective_reactive_bases = ( + effective_reactive_bases | const.REACTIVE_PLUGIN_EXPERIMENTAL_BASES + ) + if any(base not in effective_reactive_bases for base in bases): plugins.pop("reactive", None) return plugins diff --git a/charmcraft/const.py b/charmcraft/const.py index f4ae68a37..4b813d32d 100644 --- a/charmcraft/const.py +++ b/charmcraft/const.py @@ -68,6 +68,11 @@ "ubuntu@24.04", "ubuntu@24.10", "ubuntu@25.04", + ) +) + +CHARM_PLUGIN_EXPERIMENTAL_BASES = frozenset( # Experimental bases with the 'charm' plugin. + ( "ubuntu@26.04", "ubuntu@26.10", ) @@ -76,6 +81,11 @@ REACTIVE_PLUGIN_BASES = frozenset( # Bases with the 'reactive' plugin. ( *CHARM_PLUGIN_BASES, + ) +) + +REACTIVE_PLUGIN_EXPERIMENTAL_BASES = frozenset( # Experimental bases with the 'reactive' plugin. + ( "ubuntu@26.04", "ubuntu@26.10", ) diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 1ad7aa2f4..1be9aa418 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -78,6 +78,8 @@ class _FrameworkFactory: Instances are callable and expose get_supported_bases and is_experimental so they can be registered and introspected like an Extension subclass. + + The V2 classes will always supersedes V1 if the base is supported by V2 class. """ def __init__(self, v1_cls: type[Extension], v2_cls: type[Extension]) -> None: @@ -445,6 +447,12 @@ def get_container_name(self) -> str: """Return name of the container for the app image.""" return "app" + @staticmethod + @override + def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 + """Check if the extension is in an experimental state.""" + return True + GUNICORN_WEBSERVER_OPTIONS = { "webserver-keepalive": { diff --git a/charmcraft/models/project.py b/charmcraft/models/project.py index 1e4f485ac..4c135c1f4 100644 --- a/charmcraft/models/project.py +++ b/charmcraft/models/project.py @@ -17,6 +17,7 @@ import abc import datetime +import os import pathlib import re import textwrap @@ -965,7 +966,12 @@ def _validate_removed_questing_plugins( ) } - if invalid_bases := build_bases - const.REACTIVE_PLUGIN_BASES: + effective_reactive_bases = const.REACTIVE_PLUGIN_BASES + if os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR): + effective_reactive_bases = ( + effective_reactive_bases | const.REACTIVE_PLUGIN_EXPERIMENTAL_BASES + ) + if invalid_bases := build_bases - effective_reactive_bases: if len(invalid_bases) == 1: raise ValueError( f"Cannot use 'charm' or 'reactive' plugins with base {invalid_bases.pop()!r}" @@ -976,7 +982,12 @@ def _validate_removed_questing_plugins( ) if "charm" in legacy_plugins: - if invalid_bases := build_bases - const.CHARM_PLUGIN_BASES: + effective_charm_bases = const.CHARM_PLUGIN_BASES + if os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR): + effective_charm_bases = ( + effective_charm_bases | const.CHARM_PLUGIN_EXPERIMENTAL_BASES + ) + if invalid_bases := build_bases - effective_charm_bases: if len(invalid_bases) == 1: raise ValueError( f"Cannot use 'charm' plugin with base {invalid_bases.pop()!r}" diff --git a/tests/integration/invalid-charms/multibase-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/multibase-charm-plugin/charmcraft.yaml index 0b25d11f0..ec59ac38a 100644 --- a/tests/integration/invalid-charms/multibase-charm-plugin/charmcraft.yaml +++ b/tests/integration/invalid-charms/multibase-charm-plugin/charmcraft.yaml @@ -10,9 +10,6 @@ platforms: questing: build-on: ubuntu@25.10:amd64 build-for: ubuntu@25.10:amd64 - resolute: - build-on: ubuntu@26.04:amd64 - build-for: ubuntu@26.04:amd64 parts: my-part: diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml deleted file mode 100644 index ca161f914..000000000 --- a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: invalid-charm -summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. -description: | - Charm plugin is not supported on Ubuntu 26.04 LTS. -type: charm -platforms: - resolute: - build-on: ubuntu@26.04:amd64 - build-for: ubuntu@26.04:amd64 -parts: - my-part: - plugin: charm - source: . diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json deleted file mode 100644 index b016b7e19..000000000 --- a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "value_error", - "loc": [ - "parts" - ], - "msg": "Value error, Cannot use 'charm' plugin with base 'ubuntu@26.04'", - "input": { - "my-part": { - "plugin": "charm", - "source": "." - } - }, - "ctx": { - "error": "Cannot use 'charm' plugin with base 'ubuntu@26.04'" - } - } -] diff --git a/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml b/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml index 5e7f8eb31..bef908014 100644 --- a/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml +++ b/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml @@ -3,7 +3,7 @@ summary: An example charm with platforms using charm plugin description: | A description for an example charm with platforms using the charm plugin. type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 platforms: amd64: diff --git a/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml b/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml index 2a4fc921c..d5df1451f 100644 --- a/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml +++ b/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml @@ -2,7 +2,7 @@ name: example-charm summary: An example charm with platforms using charm plugin description: | A description for an example charm with platforms using the charm plugin. -base: ubuntu@26.04 +base: ubuntu@24.04 platforms: amd64: build-on: diff --git a/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml b/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml index c5ec3373f..93a97a0a4 100644 --- a/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml +++ b/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml @@ -3,7 +3,7 @@ summary: An example charm with platforms description: | A description for an example charm with platforms. type: charm -base: ubuntu@26.04 +base: ubuntu@24.04 platforms: amd64: diff --git a/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml b/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml index 629f42c2d..9a26c8e87 100644 --- a/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml +++ b/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml @@ -2,7 +2,7 @@ name: example-charm summary: An example charm with platforms description: | A description for an example charm with platforms. -base: ubuntu@26.04 +base: ubuntu@24.04 platforms: amd64: build-on: diff --git a/tests/integration/test_application.py b/tests/integration/test_application.py index 382642fd0..e63877ade 100644 --- a/tests/integration/test_application.py +++ b/tests/integration/test_application.py @@ -129,12 +129,7 @@ def test_load_invalid_charm(in_project_path: pathlib.Path, charm_dir: pathlib.Pa "plugin not registered: 'reactive'", id="multibase-questing-charm-plugin-reactive", ), - pytest.param( - "multibase-resolute-charm-plugin", - "charm", - "plugin not registered: 'charm'", - id="multibase-resolute-charm-plugin-charm", - ), + pytest.param( "questing-charm-plugin", "charm", diff --git a/tests/unit/models/test_project.py b/tests/unit/models/test_project.py index b884877c4..5d5ac08c4 100644 --- a/tests/unit/models/test_project.py +++ b/tests/unit/models/test_project.py @@ -740,7 +740,8 @@ def test_resolute_base_does_not_need_build_base(): ) -def test_resolute_base_supports_reactive_plugin(): +def test_resolute_base_supports_reactive_plugin(monkeypatch): + monkeypatch.setenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR, "1") project.PlatformCharm.unmarshal( { "type": "charm", @@ -754,7 +755,8 @@ def test_resolute_base_supports_reactive_plugin(): ) -def test_resolute_base_supports_charm_plugin(): +def test_resolute_base_supports_charm_plugin(monkeypatch): + monkeypatch.setenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR, "1") project.PlatformCharm.unmarshal( { "type": "charm", From f280400511c67d4ba4649fefdf36940c2d218f8d Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Tue, 16 Jun 2026 11:59:34 +0300 Subject: [PATCH 09/26] chore: apply comments --- charmcraft/application/commands/init.py | 4 ++ charmcraft/const.py | 24 ++++--- charmcraft/extensions/__init__.py | 16 ++--- charmcraft/extensions/app.py | 72 +++++-------------- charmcraft/extensions/extension.py | 4 +- tests/extensions/test_app.py | 54 +++++++++----- tests/extensions/test_registry.py | 10 +-- .../charmcraft.yaml | 13 ++++ .../errors.json | 18 +++++ .../resolute-charm-plugin/charmcraft.yaml | 12 ++++ .../resolute-charm-plugin/errors.json | 18 +++++ .../charmcraft.yaml | 0 .../expected.yaml | 0 .../charmcraft.yaml | 0 .../expected.yaml | 0 tests/integration/test_application.py | 13 +++- tests/unit/models/test_project.py | 58 +++++++++++++++ 17 files changed, 216 insertions(+), 100 deletions(-) create mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml create mode 100644 tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json create mode 100644 tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml create mode 100644 tests/integration/invalid-charms/resolute-charm-plugin/errors.json rename tests/integration/sample-charms/{platforms-resolute-charm => platforms-noble-charm}/charmcraft.yaml (100%) rename tests/integration/sample-charms/{platforms-resolute-charm => platforms-noble-charm}/expected.yaml (100%) rename tests/integration/sample-charms/{platforms-resolute-reactive => platforms-noble-reactive}/charmcraft.yaml (100%) rename tests/integration/sample-charms/{platforms-resolute-reactive => platforms-noble-reactive}/expected.yaml (100%) diff --git a/charmcraft/application/commands/init.py b/charmcraft/application/commands/init.py index a4cde0de2..57910bb03 100644 --- a/charmcraft/application/commands/init.py +++ b/charmcraft/application/commands/init.py @@ -33,6 +33,10 @@ pwd = None # type: ignore[assignment] # the available profiles and in which directory the template can be found +# NOTE: init--framework-26.04 template dirs exist but are intentionally +# not wired into PROFILES yet. They are staged for when ubuntu@26.04 + the V2 +# (uv-based) 12-factor extensions become the default. Wiring them in will also +# require handling the experimental-extensions gating during `init`/`tox` tests. PROFILES = { "kubernetes": "init-kubernetes", "machine": "init-machine", diff --git a/charmcraft/const.py b/charmcraft/const.py index 4b813d32d..20dd29562 100644 --- a/charmcraft/const.py +++ b/charmcraft/const.py @@ -71,23 +71,25 @@ ) ) -CHARM_PLUGIN_EXPERIMENTAL_BASES = frozenset( # Experimental bases with the 'charm' plugin. - ( - "ubuntu@26.04", - "ubuntu@26.10", +CHARM_PLUGIN_EXPERIMENTAL_BASES = ( + frozenset( # Experimental bases with the 'charm' plugin. + ( + "ubuntu@26.04", + "ubuntu@26.10", + ) ) ) REACTIVE_PLUGIN_BASES = frozenset( # Bases with the 'reactive' plugin. - ( - *CHARM_PLUGIN_BASES, - ) + (*CHARM_PLUGIN_BASES,) ) -REACTIVE_PLUGIN_EXPERIMENTAL_BASES = frozenset( # Experimental bases with the 'reactive' plugin. - ( - "ubuntu@26.04", - "ubuntu@26.10", +REACTIVE_PLUGIN_EXPERIMENTAL_BASES = ( + frozenset( # Experimental bases with the 'reactive' plugin. + ( + "ubuntu@26.04", + "ubuntu@26.10", + ) ) ) diff --git a/charmcraft/extensions/__init__.py b/charmcraft/extensions/__init__.py index 2085d4cfc..78d3f2a6c 100644 --- a/charmcraft/extensions/__init__.py +++ b/charmcraft/extensions/__init__.py @@ -35,9 +35,7 @@ ) __all__ = [ - "DjangoFramework", "Extension", - "FlaskFramework", "get_extension_class", "get_extension_names", "get_extensions", @@ -46,9 +44,11 @@ "unregister", ] -register("flask-framework", flask_framework_factory) -register("django-framework", django_framework_factory) -register("go-framework", go_framework_factory) -register("fastapi-framework", fastapi_framework_factory) -register("expressjs-framework", expressjs_framework_factory) -register("spring-boot-framework", springboot_framework_factory) +# Factory instances are registered in place of Extension subclasses for the +# 12-factor app extensions only, until craft-wide extensions land (CRAFT-5152). +register("flask-framework", flask_framework_factory) # type: ignore[arg-type] +register("django-framework", django_framework_factory) # type: ignore[arg-type] +register("go-framework", go_framework_factory) # type: ignore[arg-type] +register("fastapi-framework", fastapi_framework_factory) # type: ignore[arg-type] +register("expressjs-framework", expressjs_framework_factory) # type: ignore[arg-type] +register("spring-boot-framework", springboot_framework_factory) # type: ignore[arg-type] diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 1be9aa418..3e1c14439 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -23,7 +23,7 @@ from overrides import override from ..errors import ExtensionError -from .extension import Extension, get_project_bases +from .extension import Extension, SinglePlatformExtension, get_project_bases APP_PORT_OPTION = { "app-port": { @@ -125,7 +125,7 @@ def is_experimental(self, base: tuple[str, str] | None) -> bool: return self._v1_cls.is_experimental(base) -class _AppBase(Extension): +class _AppBase(SinglePlatformExtension): """A base class for 12-factor applications.""" _CHARM_LIBS = [ @@ -153,9 +153,9 @@ def get_supported_bases() -> list[tuple[str, str]]: @staticmethod @override - def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 + def is_experimental(base: tuple[str, str] | None) -> bool: # noqa: ARG004 """Check if the extension is in an experimental state.""" - return True + return False framework: str actions: dict = { @@ -389,40 +389,18 @@ def get_supported_bases() -> list[tuple[str, str]]: @override def _get_root_snippet(self) -> dict[str, Any]: """Return the root snippet to be merged into the user charmcraft.yaml.""" - return { - "assumes": ["k8s-api"], - "containers": { - self.get_container_name(): {"resource": self.get_image_name()}, - }, - "resources": { - self.get_image_name(): { - "type": "oci-image", - "description": f"{self.framework} application image.", - }, - }, - "charm-libs": self._CHARM_LIBS, - "peers": {"secret-storage": {"interface": "secret-storage"}}, - "actions": self.actions, - "requires": { - "logging": {"interface": "loki_push_api"}, - "ingress": {"interface": "ingress", "limit": 1}, - }, - "provides": { - "metrics-endpoint": {"interface": "prometheus_scrape"}, - "grafana-dashboard": {"interface": "grafana_dashboard"}, - }, - "config": {"options": copy.deepcopy(self.options)}, - "parts": { - "charm": { - "plugin": "uv", - "source": ".", - "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable\ncraftctl default"], - "uv-groups": ["charmlibs-pydeps"], - }, - **self.get_config_part(), + snippet = super()._get_root_snippet() + snippet["parts"] = { + "charm": { + "plugin": "uv", + "source": ".", + "build-snaps": ["astral-uv", "rustup"], + "override-build": "rustup default stable\ncraftctl default", + "uv-groups": ["charmlibs-pydeps"], }, + **self.get_config_part(), } + return snippet def get_config_part(self) -> dict[str, Any]: """Get config part if paas-config.yaml is present.""" @@ -449,8 +427,8 @@ def get_container_name(self) -> str: @staticmethod @override - def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 - """Check if the extension is in an experimental state.""" + def is_experimental(base: tuple[str, str] | None) -> bool: # noqa: ARG004 + """Check if the extension is_experimental is always True for V2.""" return True @@ -522,12 +500,6 @@ class FlaskFramework(_AppBase): }, } - @staticmethod - @override - def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 - """Check if the extension is in an experimental state.""" - return False - class FlaskFrameworkV2(_AppBaseV2): """Extension v2 for 12-factor Flask applications.""" @@ -575,12 +547,6 @@ class DjangoFramework(_AppBase): }, } - @staticmethod - @override - def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 - """Check if the extension is in an experimental state.""" - return False - class DjangoFrameworkV2(_AppBaseV2): """Extension v2 for 12-factor Django applications.""" @@ -769,12 +735,6 @@ def get_supported_bases() -> list[tuple[str, str]]: """Return supported bases.""" return [("ubuntu", "24.04")] - @staticmethod - @override - def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004 - """Check if the extension is in an experimental state.""" - return False - @override def get_image_name(self) -> str: """Return name of the app image.""" diff --git a/charmcraft/extensions/extension.py b/charmcraft/extensions/extension.py index 6e07f332c..98267f609 100644 --- a/charmcraft/extensions/extension.py +++ b/charmcraft/extensions/extension.py @@ -185,8 +185,6 @@ class SinglePlatformExtension(Extension): @override def validate(self, extension_name: str) -> None: """Validate that the extension is only used with a single base.""" - super().validate(extension_name) - bases = self._get_project_bases() if len(bases) > 1: bases_str = ", ".join(f"{n}@{c}" for n, c in sorted(bases)) @@ -194,6 +192,8 @@ def validate(self, extension_name: str) -> None: f"Extension does not support multiple bases: {bases_str}" ) + super().validate(extension_name) + def get_extensions_data_dir() -> Path: """Return the path to the extension data directory.""" diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 8e80a1151..143d911c5 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -236,7 +236,7 @@ def make_spring_boot_input_yaml(): "extensions": ["go-framework"], "config": NON_OPTIONAL_OPTIONS, }, - True, + False, { "actions": GoFramework.actions, "assumes": ["k8s-api"], @@ -311,7 +311,7 @@ def make_spring_boot_input_yaml(): "extensions": ["fastapi-framework"], "config": NON_OPTIONAL_OPTIONS, }, - True, + False, { "actions": FastAPIFramework.actions, "assumes": ["k8s-api"], @@ -386,7 +386,7 @@ def make_spring_boot_input_yaml(): "extensions": ["expressjs-framework"], "config": NON_OPTIONAL_OPTIONS, }, - True, + False, { "actions": ExpressJSFramework.actions, "assumes": ["k8s-api"], @@ -549,7 +549,7 @@ def test_go_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable\ncraftctl default"], + "override-build": "rustup default stable\ncraftctl default", "uv-groups": ["charmlibs-pydeps"], } @@ -579,7 +579,7 @@ def test_flask_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable\ncraftctl default"], + "override-build": "rustup default stable\ncraftctl default", "uv-groups": ["charmlibs-pydeps"], } @@ -609,7 +609,7 @@ def test_django_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable\ncraftctl default"], + "override-build": "rustup default stable\ncraftctl default", "uv-groups": ["charmlibs-pydeps"], } @@ -639,7 +639,7 @@ def test_fastapi_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable\ncraftctl default"], + "override-build": "rustup default stable\ncraftctl default", "uv-groups": ["charmlibs-pydeps"], } @@ -669,7 +669,7 @@ def test_expressjs_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable\ncraftctl default"], + "override-build": "rustup default stable\ncraftctl default", "uv-groups": ["charmlibs-pydeps"], } @@ -699,7 +699,7 @@ def test_spring_boot_framework_26_04_uses_v2_snippet(monkeypatch, tmp_path): "plugin": "uv", "source": ".", "build-snaps": ["astral-uv", "rustup"], - "override-build": ["rustup default stable\ncraftctl default"], + "override-build": "rustup default stable\ncraftctl default", "uv-groups": ["charmlibs-pydeps"], } @@ -741,6 +741,26 @@ def test_go_framework_24_04_still_routes_to_v1(monkeypatch, tmp_path): assert applied["parts"]["charm"]["plugin"] == "charm" +def test_12factor_extension_rejects_multi_base(monkeypatch, tmp_path): + """Test that 12-factor extensions reject projects with multiple bases.""" + monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + input_yaml = { + "type": "charm", + "name": "test-multibase", + "summary": "test summary", + "description": "test description", + "base": "ubuntu@24.04", + "platforms": {"ubuntu@26.04:amd64": None}, + "extensions": ["flask-framework"], + } + + with pytest.raises( + errors.ExtensionError, + match="does not support multiple bases", + ): + extensions.apply_extensions(tmp_path, input_yaml) + + def test_v2_extension_experimental_gating_enforced(monkeypatch, tmp_path): """Test that V2 extensions require CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS env var.""" monkeypatch.delenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", raising=False) @@ -789,22 +809,22 @@ def test_flask_framework_factory_get_supported_bases_no_duplicates(): def test_go_framework_factory_is_experimental_correct(monkeypatch): """Test that go factory is_experimental delegates to correct class per base (defect 2 fix).""" - # Go V1 on 24.04 should still be experimental - assert go_framework_factory.is_experimental(("ubuntu", "24.04")) is True + # Go V1 on 24.04 is now stable (not experimental) + assert go_framework_factory.is_experimental(("ubuntu", "24.04")) is False # Go V2 on 26.04 should be experimental assert go_framework_factory.is_experimental(("ubuntu", "26.04")) is True def test_fastapi_framework_factory_is_experimental_24_04(monkeypatch): - """Test that fastapi on 24.04 is experimental (defect 2: not regressed to stable).""" - # FastAPI V1 on 24.04 should still be experimental (not regressed to stable) - assert fastapi_framework_factory.is_experimental(("ubuntu", "24.04")) is True + """Test that fastapi on 24.04 is stable (was experimental, now GA).""" + # FastAPI V1 on 24.04 is now stable + assert fastapi_framework_factory.is_experimental(("ubuntu", "24.04")) is False def test_expressjs_framework_factory_is_experimental_24_04(monkeypatch): - """Test that expressjs on 24.04 is experimental (defect 2: not regressed to stable).""" - # ExpressJS V1 on 24.04 should still be experimental (not regressed to stable) - assert expressjs_framework_factory.is_experimental(("ubuntu", "24.04")) is True + """Test that expressjs on 24.04 is stable (was experimental, now GA).""" + # ExpressJS V1 on 24.04 is now stable + assert expressjs_framework_factory.is_experimental(("ubuntu", "24.04")) is False def test_v2_check_input_rejects_non_charm_type(monkeypatch, tmp_path): diff --git a/tests/extensions/test_registry.py b/tests/extensions/test_registry.py index ffd8fe82c..e5d9ea046 100644 --- a/tests/extensions/test_registry.py +++ b/tests/extensions/test_registry.py @@ -164,7 +164,7 @@ def test_real_framework_factories_experimental_status_correct(): "go", go_framework_factory, [ - (("ubuntu", "24.04"), True), # V1: experimental + (("ubuntu", "24.04"), False), # V1: stable (("ubuntu", "26.04"), True), # V2: experimental ], ), @@ -174,8 +174,8 @@ def test_real_framework_factories_experimental_status_correct(): [ ( ("ubuntu", "24.04"), - True, - ), # V1: experimental (NOT regressed to stable) + False, + ), # V1: stable (("ubuntu", "26.04"), True), # V2: experimental ], ), @@ -185,8 +185,8 @@ def test_real_framework_factories_experimental_status_correct(): [ ( ("ubuntu", "24.04"), - True, - ), # V1: experimental (NOT regressed to stable) + False, + ), # V1: stable (("ubuntu", "26.04"), True), # V2: experimental ], ), diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml new file mode 100644 index 000000000..ca161f914 --- /dev/null +++ b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/charmcraft.yaml @@ -0,0 +1,13 @@ +name: invalid-charm +summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. +description: | + Charm plugin is not supported on Ubuntu 26.04 LTS. +type: charm +platforms: + resolute: + build-on: ubuntu@26.04:amd64 + build-for: ubuntu@26.04:amd64 +parts: + my-part: + plugin: charm + source: . diff --git a/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json new file mode 100644 index 000000000..8941bc0fd --- /dev/null +++ b/tests/integration/invalid-charms/multibase-resolute-charm-plugin/errors.json @@ -0,0 +1,18 @@ +[ + { + "type": "value_error", + "loc": [ + "parts" + ], + "msg": "Value error, Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'", + "input": { + "my-part": { + "plugin": "charm", + "source": "." + } + }, + "ctx": { + "error": "Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'" + } + } +] diff --git a/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml b/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml new file mode 100644 index 000000000..27680d916 --- /dev/null +++ b/tests/integration/invalid-charms/resolute-charm-plugin/charmcraft.yaml @@ -0,0 +1,12 @@ +name: invalid-charm +summary: An invalid charm based on Ubuntu Resolute (26.04 LTS) with the charm plugin. +description: | + Charm plugin is not supported on Ubuntu 26.04 LTS. +type: charm +base: ubuntu@26.04 +platforms: + amd64: +parts: + my-part: + plugin: charm + source: . diff --git a/tests/integration/invalid-charms/resolute-charm-plugin/errors.json b/tests/integration/invalid-charms/resolute-charm-plugin/errors.json new file mode 100644 index 000000000..8941bc0fd --- /dev/null +++ b/tests/integration/invalid-charms/resolute-charm-plugin/errors.json @@ -0,0 +1,18 @@ +[ + { + "type": "value_error", + "loc": [ + "parts" + ], + "msg": "Value error, Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'", + "input": { + "my-part": { + "plugin": "charm", + "source": "." + } + }, + "ctx": { + "error": "Cannot use 'charm' or 'reactive' plugins with base 'ubuntu@26.04'" + } + } +] diff --git a/tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml b/tests/integration/sample-charms/platforms-noble-charm/charmcraft.yaml similarity index 100% rename from tests/integration/sample-charms/platforms-resolute-charm/charmcraft.yaml rename to tests/integration/sample-charms/platforms-noble-charm/charmcraft.yaml diff --git a/tests/integration/sample-charms/platforms-resolute-charm/expected.yaml b/tests/integration/sample-charms/platforms-noble-charm/expected.yaml similarity index 100% rename from tests/integration/sample-charms/platforms-resolute-charm/expected.yaml rename to tests/integration/sample-charms/platforms-noble-charm/expected.yaml diff --git a/tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml b/tests/integration/sample-charms/platforms-noble-reactive/charmcraft.yaml similarity index 100% rename from tests/integration/sample-charms/platforms-resolute-reactive/charmcraft.yaml rename to tests/integration/sample-charms/platforms-noble-reactive/charmcraft.yaml diff --git a/tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml b/tests/integration/sample-charms/platforms-noble-reactive/expected.yaml similarity index 100% rename from tests/integration/sample-charms/platforms-resolute-reactive/expected.yaml rename to tests/integration/sample-charms/platforms-noble-reactive/expected.yaml diff --git a/tests/integration/test_application.py b/tests/integration/test_application.py index e63877ade..bb40fc7d0 100644 --- a/tests/integration/test_application.py +++ b/tests/integration/test_application.py @@ -129,7 +129,12 @@ def test_load_invalid_charm(in_project_path: pathlib.Path, charm_dir: pathlib.Pa "plugin not registered: 'reactive'", id="multibase-questing-charm-plugin-reactive", ), - + pytest.param( + "multibase-resolute-charm-plugin", + "charm", + "plugin not registered: 'charm'", + id="multibase-resolute-charm-plugin-charm", + ), pytest.param( "questing-charm-plugin", "charm", @@ -154,6 +159,12 @@ def test_load_invalid_charm(in_project_path: pathlib.Path, charm_dir: pathlib.Pa "plugin not registered: 'reactive'", id="questing-reactive-plugin-reactive", ), + pytest.param( + "resolute-charm-plugin", + "charm", + "plugin not registered: 'charm'", + id="resolute-charm-plugin-charm", + ), ], ) def test_remove_charm_reactive_plugins( diff --git a/tests/unit/models/test_project.py b/tests/unit/models/test_project.py index 5d5ac08c4..74e040571 100644 --- a/tests/unit/models/test_project.py +++ b/tests/unit/models/test_project.py @@ -770,6 +770,64 @@ def test_resolute_base_supports_charm_plugin(monkeypatch): ) +@pytest.mark.xfail(strict=True, reason="craft-application#1092") +def test_resolute_base_rejects_charm_plugin(): + with pytest.raises( + pydantic.ValidationError, + match="Cannot use 'charm' plugin with base 'ubuntu@26.04'", + ): + project.PlatformCharm.unmarshal( + { + "type": "charm", + "name": "test-charm", + "summary": "", + "description": "", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "parts": {"charm": {"plugin": "charm"}}, + } + ) + + +@pytest.mark.xfail(strict=True, reason="craft-application#1092") +def test_charm_plugin_is_checked_against_build_base(): + with pytest.raises( + pydantic.ValidationError, + match="Cannot use 'charm' plugin with base 'ubuntu@26.04'", + ): + project.PlatformCharm.unmarshal( + { + "type": "charm", + "name": "test-charm", + "summary": "", + "description": "", + "base": "ubuntu@24.04", + "build-base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "parts": {"charm": {"plugin": "charm"}}, + } + ) + + +@pytest.mark.xfail(strict=True, reason="craft-application#1092") +def test_resolute_base_rejects_charm_plugin_without_env_var(): + with pytest.raises( + pydantic.ValidationError, + match="Cannot use 'charm' plugin with base 'ubuntu@26.04'", + ): + project.PlatformCharm.unmarshal( + { + "type": "charm", + "name": "test-charm", + "summary": "", + "description": "", + "base": "ubuntu@26.04", + "platforms": {"amd64": None}, + "parts": {"charm": {"plugin": "charm"}}, + } + ) + + def test_legacy_plugins_are_checked_against_build_base(): with pytest.raises( pydantic.ValidationError, From c4d347ec838c80b163b658d31fae51d1e27c135b Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Wed, 17 Jun 2026 07:54:41 +0300 Subject: [PATCH 10/26] chore: use pascalcase for class --- charmcraft/extensions/__init__.py | 24 ++++++++++----------- charmcraft/extensions/app.py | 12 +++++------ tests/extensions/test_app.py | 18 ++++++++-------- tests/extensions/test_registry.py | 36 +++++++++++++++---------------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/charmcraft/extensions/__init__.py b/charmcraft/extensions/__init__.py index 78d3f2a6c..63471d8ed 100644 --- a/charmcraft/extensions/__init__.py +++ b/charmcraft/extensions/__init__.py @@ -18,12 +18,12 @@ from charmcraft.extensions._utils import apply_extensions from charmcraft.extensions.app import ( - django_framework_factory, - expressjs_framework_factory, - fastapi_framework_factory, - flask_framework_factory, - go_framework_factory, - springboot_framework_factory, + DjangoFrameworkFactory, + ExpressJSFrameworkFactory, + FastAPIFrameworkFactory, + FlaskFrameworkFactory, + GoFrameworkFactory, + SpringBootFrameworkFactory, ) from charmcraft.extensions.extension import Extension from charmcraft.extensions.registry import ( @@ -46,9 +46,9 @@ # Factory instances are registered in place of Extension subclasses for the # 12-factor app extensions only, until craft-wide extensions land (CRAFT-5152). -register("flask-framework", flask_framework_factory) # type: ignore[arg-type] -register("django-framework", django_framework_factory) # type: ignore[arg-type] -register("go-framework", go_framework_factory) # type: ignore[arg-type] -register("fastapi-framework", fastapi_framework_factory) # type: ignore[arg-type] -register("expressjs-framework", expressjs_framework_factory) # type: ignore[arg-type] -register("spring-boot-framework", springboot_framework_factory) # type: ignore[arg-type] +register("flask-framework", FlaskFrameworkFactory) # type: ignore[arg-type] +register("django-framework", DjangoFrameworkFactory) # type: ignore[arg-type] +register("go-framework", GoFrameworkFactory) # type: ignore[arg-type] +register("fastapi-framework", FastAPIFrameworkFactory) # type: ignore[arg-type] +register("expressjs-framework", ExpressJSFrameworkFactory) # type: ignore[arg-type] +register("spring-boot-framework", SpringBootFrameworkFactory) # type: ignore[arg-type] diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 3e1c14439..e244662da 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -508,7 +508,7 @@ class FlaskFrameworkV2(_AppBaseV2): options = FlaskFramework.options -flask_framework_factory = _FrameworkFactory(FlaskFramework, FlaskFrameworkV2) +FlaskFrameworkFactory = _FrameworkFactory(FlaskFramework, FlaskFrameworkV2) class DjangoFramework(_AppBase): @@ -556,7 +556,7 @@ class DjangoFrameworkV2(_AppBaseV2): options = DjangoFramework.options -django_framework_factory = _FrameworkFactory(DjangoFramework, DjangoFrameworkV2) +DjangoFrameworkFactory = _FrameworkFactory(DjangoFramework, DjangoFrameworkV2) class GoFramework(_AppBase): @@ -597,7 +597,7 @@ class GoFrameworkV2(_AppBaseV2): } -go_framework_factory = _FrameworkFactory(GoFramework, GoFrameworkV2) +GoFrameworkFactory = _FrameworkFactory(GoFramework, GoFrameworkV2) class FastAPIFramework(_AppBase): @@ -648,7 +648,7 @@ class FastAPIFrameworkV2(_AppBaseV2): options = FastAPIFramework.options -fastapi_framework_factory = _FrameworkFactory(FastAPIFramework, FastAPIFrameworkV2) +FastAPIFrameworkFactory = _FrameworkFactory(FastAPIFramework, FastAPIFrameworkV2) class ExpressJSFramework(_AppBase): @@ -685,7 +685,7 @@ class ExpressJSFrameworkV2(_AppBaseV2): options = ExpressJSFramework.options -expressjs_framework_factory = _FrameworkFactory( +ExpressJSFrameworkFactory = _FrameworkFactory( ExpressJSFramework, ExpressJSFrameworkV2 ) @@ -754,6 +754,6 @@ class SpringBootFrameworkV2(_AppBaseV2): endpoint_dynamic_options = SpringBootFramework.endpoint_dynamic_options -springboot_framework_factory = _FrameworkFactory( +SpringBootFrameworkFactory = _FrameworkFactory( SpringBootFramework, SpringBootFrameworkV2 ) diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 143d911c5..9b68bdd9a 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -27,10 +27,10 @@ FlaskFramework, GoFramework, SpringBootFramework, - expressjs_framework_factory, - fastapi_framework_factory, - flask_framework_factory, - go_framework_factory, + ExpressJSFrameworkFactory, + FastAPIFrameworkFactory, + FlaskFrameworkFactory, + GoFrameworkFactory, ) NON_OPTIONAL_OPTIONS = { @@ -802,7 +802,7 @@ def test_v2_extension_experimental_gating_passes_with_env(monkeypatch, tmp_path) def test_flask_framework_factory_get_supported_bases_no_duplicates(): """Test that flask factory supported bases are deduped (defect 1 fix).""" - bases = flask_framework_factory.get_supported_bases() + bases = FlaskFrameworkFactory.get_supported_bases() assert bases == [("ubuntu", "22.04"), ("ubuntu", "26.04")] assert len(bases) == len(set(bases)), "Supported bases should not have duplicates" @@ -810,21 +810,21 @@ def test_flask_framework_factory_get_supported_bases_no_duplicates(): def test_go_framework_factory_is_experimental_correct(monkeypatch): """Test that go factory is_experimental delegates to correct class per base (defect 2 fix).""" # Go V1 on 24.04 is now stable (not experimental) - assert go_framework_factory.is_experimental(("ubuntu", "24.04")) is False + assert GoFrameworkFactory.is_experimental(("ubuntu", "24.04")) is False # Go V2 on 26.04 should be experimental - assert go_framework_factory.is_experimental(("ubuntu", "26.04")) is True + assert GoFrameworkFactory.is_experimental(("ubuntu", "26.04")) is True def test_fastapi_framework_factory_is_experimental_24_04(monkeypatch): """Test that fastapi on 24.04 is stable (was experimental, now GA).""" # FastAPI V1 on 24.04 is now stable - assert fastapi_framework_factory.is_experimental(("ubuntu", "24.04")) is False + assert FastAPIFrameworkFactory.is_experimental(("ubuntu", "24.04")) is False def test_expressjs_framework_factory_is_experimental_24_04(monkeypatch): """Test that expressjs on 24.04 is stable (was experimental, now GA).""" # ExpressJS V1 on 24.04 is now stable - assert expressjs_framework_factory.is_experimental(("ubuntu", "24.04")) is False + assert ExpressJSFrameworkFactory.is_experimental(("ubuntu", "24.04")) is False def test_v2_check_input_rejects_non_charm_type(monkeypatch, tmp_path): diff --git a/tests/extensions/test_registry.py b/tests/extensions/test_registry.py index e5d9ea046..ded0421c8 100644 --- a/tests/extensions/test_registry.py +++ b/tests/extensions/test_registry.py @@ -19,12 +19,12 @@ from charmcraft import errors, extensions from charmcraft.extensions.app import ( - django_framework_factory, - expressjs_framework_factory, - fastapi_framework_factory, - flask_framework_factory, - go_framework_factory, - springboot_framework_factory, + DjangoFrameworkFactory, + ExpressJSFrameworkFactory, + FastAPIFrameworkFactory, + FlaskFrameworkFactory, + GoFrameworkFactory, + SpringBootFrameworkFactory, ) from charmcraft.extensions.extension import Extension @@ -124,12 +124,12 @@ def test_real_framework_factories_no_duplicate_experimental_bases(): """Verify real framework factories have no duplicate experimental_bases (defect 1 fix).""" # Import the actual factories directly factories = [ - ("flask-framework", flask_framework_factory), - ("django-framework", django_framework_factory), - ("go-framework", go_framework_factory), - ("fastapi-framework", fastapi_framework_factory), - ("expressjs-framework", expressjs_framework_factory), - ("spring-boot-framework", springboot_framework_factory), + ("flask-framework", FlaskFrameworkFactory), + ("django-framework", DjangoFrameworkFactory), + ("go-framework", GoFrameworkFactory), + ("fastapi-framework", FastAPIFrameworkFactory), + ("expressjs-framework", ExpressJSFrameworkFactory), + ("spring-boot-framework", SpringBootFrameworkFactory), ] for name, factory in factories: @@ -146,7 +146,7 @@ def test_real_framework_factories_experimental_status_correct(): test_cases = [ ( "flask", - flask_framework_factory, + FlaskFrameworkFactory, [ (("ubuntu", "22.04"), False), # V1: stable (("ubuntu", "26.04"), True), # V2: experimental @@ -154,7 +154,7 @@ def test_real_framework_factories_experimental_status_correct(): ), ( "django", - django_framework_factory, + DjangoFrameworkFactory, [ (("ubuntu", "22.04"), False), # V1: stable (("ubuntu", "26.04"), True), # V2: experimental @@ -162,7 +162,7 @@ def test_real_framework_factories_experimental_status_correct(): ), ( "go", - go_framework_factory, + GoFrameworkFactory, [ (("ubuntu", "24.04"), False), # V1: stable (("ubuntu", "26.04"), True), # V2: experimental @@ -170,7 +170,7 @@ def test_real_framework_factories_experimental_status_correct(): ), ( "fastapi", - fastapi_framework_factory, + FastAPIFrameworkFactory, [ ( ("ubuntu", "24.04"), @@ -181,7 +181,7 @@ def test_real_framework_factories_experimental_status_correct(): ), ( "expressjs", - expressjs_framework_factory, + ExpressJSFrameworkFactory, [ ( ("ubuntu", "24.04"), @@ -192,7 +192,7 @@ def test_real_framework_factories_experimental_status_correct(): ), ( "spring-boot", - springboot_framework_factory, + SpringBootFrameworkFactory, [ (("ubuntu", "24.04"), False), # V1: stable (("ubuntu", "26.04"), True), # V2: experimental From 98431e5351913750d41d725400a4dcc49fb07957 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Wed, 17 Jun 2026 08:19:56 +0300 Subject: [PATCH 11/26] chore: doc links --- .../templates/init-django-framework-26.04/charmcraft.yaml.j2 | 4 ++-- charmcraft/templates/init-django-framework/charmcraft.yaml.j2 | 4 ++-- .../init-expressjs-framework-26.04/charmcraft.yaml.j2 | 2 +- .../templates/init-expressjs-framework/charmcraft.yaml.j2 | 2 +- .../templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 | 4 ++-- .../templates/init-fastapi-framework/charmcraft.yaml.j2 | 4 ++-- .../templates/init-flask-framework-26.04/charmcraft.yaml.j2 | 2 +- charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 | 2 +- .../templates/init-go-framework-26.04/charmcraft.yaml.j2 | 2 +- charmcraft/templates/init-go-framework/charmcraft.yaml.j2 | 4 ++-- .../init-spring-boot-framework-26.04/charmcraft.yaml.j2 | 4 ++-- .../templates/init-spring-boot-framework/charmcraft.yaml.j2 | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 index 0e8af5956..a08b1cb84 100644 --- a/charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-django-framework-26.04/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 index 5bccbe6ee..de33834a2 100644 --- a/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-django-framework/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 index 3908c4202..0f83827e8 100644 --- a/charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-expressjs-framework-26.04/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. # See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 index 7af066512..d44e815d2 100644 --- a/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-expressjs-framework/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. # See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 index 11c2f10fe..f9ce94d84 100644 --- a/charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-fastapi-framework-26.04/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 index f64467e1a..c29ebee1d 100644 --- a/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 index 421dbea64..e4b2276d9 100644 --- a/charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-flask-framework-26.04/charmcraft.yaml.j2 @@ -1,5 +1,5 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. # For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 index 005f12446..ddfde47ce 100644 --- a/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-flask-framework/charmcraft.yaml.j2 @@ -1,5 +1,5 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. # For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 index 3865729fc..5686435af 100644 --- a/charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-go-framework-26.04/charmcraft.yaml.j2 @@ -1,5 +1,5 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. # For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 index bb44201f1..a2ef6f9b7 100644 --- a/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-go-framework/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 index 737fe302b..0ea2f0fd9 100644 --- a/charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-spring-boot-framework-26.04/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} diff --git a/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 b/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 index e955623d5..b2c06f8dc 100644 --- a/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 +++ b/charmcraft/templates/init-spring-boot-framework/charmcraft.yaml.j2 @@ -1,6 +1,6 @@ # This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com +# See https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/ for guidance. +# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com name: {{ name }} From 51209762892a26bc39fc4bbb851cdc97ec3751c1 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Thu, 18 Jun 2026 13:22:56 +0300 Subject: [PATCH 12/26] chore: apply comments --- charmcraft/application/main.py | 2 +- charmcraft/extensions/__init__.py | 12 ++++++------ charmcraft/extensions/app.py | 4 +--- tests/extensions/test_app.py | 8 ++++---- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/charmcraft/application/main.py b/charmcraft/application/main.py index 47abad77c..f0646fc20 100644 --- a/charmcraft/application/main.py +++ b/charmcraft/application/main.py @@ -140,7 +140,7 @@ def _get_app_plugins(self) -> dict[str, PluginType]: if any(base not in effective_charm_bases for base in bases): plugins.pop("charm", None) effective_reactive_bases = const.REACTIVE_PLUGIN_BASES - if os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR): + if util.strtobool(str(os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR))): effective_reactive_bases = ( effective_reactive_bases | const.REACTIVE_PLUGIN_EXPERIMENTAL_BASES ) diff --git a/charmcraft/extensions/__init__.py b/charmcraft/extensions/__init__.py index 63471d8ed..9e0f611c6 100644 --- a/charmcraft/extensions/__init__.py +++ b/charmcraft/extensions/__init__.py @@ -46,9 +46,9 @@ # Factory instances are registered in place of Extension subclasses for the # 12-factor app extensions only, until craft-wide extensions land (CRAFT-5152). -register("flask-framework", FlaskFrameworkFactory) # type: ignore[arg-type] -register("django-framework", DjangoFrameworkFactory) # type: ignore[arg-type] -register("go-framework", GoFrameworkFactory) # type: ignore[arg-type] -register("fastapi-framework", FastAPIFrameworkFactory) # type: ignore[arg-type] -register("expressjs-framework", ExpressJSFrameworkFactory) # type: ignore[arg-type] -register("spring-boot-framework", SpringBootFrameworkFactory) # type: ignore[arg-type] +register("flask-framework", FlaskFrameworkFactory) # type: ignore +register("django-framework", DjangoFrameworkFactory) # type: ignore +register("go-framework", GoFrameworkFactory) # type: ignore +register("fastapi-framework", FastAPIFrameworkFactory) # type: ignore +register("expressjs-framework", ExpressJSFrameworkFactory) # type: ignore +register("spring-boot-framework", SpringBootFrameworkFactory) # type: ignore diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index e244662da..6a95a547d 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -685,9 +685,7 @@ class ExpressJSFrameworkV2(_AppBaseV2): options = ExpressJSFramework.options -ExpressJSFrameworkFactory = _FrameworkFactory( - ExpressJSFramework, ExpressJSFrameworkV2 -) +ExpressJSFrameworkFactory = _FrameworkFactory(ExpressJSFramework, ExpressJSFrameworkV2) class SpringBootFramework(_AppBase): diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 9b68bdd9a..65e6fb277 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -23,14 +23,14 @@ from charmcraft.extensions.app import ( DjangoFramework, ExpressJSFramework, - FastAPIFramework, - FlaskFramework, - GoFramework, - SpringBootFramework, ExpressJSFrameworkFactory, + FastAPIFramework, FastAPIFrameworkFactory, + FlaskFramework, FlaskFrameworkFactory, + GoFramework, GoFrameworkFactory, + SpringBootFramework, ) NON_OPTIONAL_OPTIONS = { From ffabbf48f861c71a80abdf264de61e0a7e1c6b86 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Thu, 18 Jun 2026 13:38:34 +0300 Subject: [PATCH 13/26] fix: experimental --- charmcraft/application/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/charmcraft/application/main.py b/charmcraft/application/main.py index f0646fc20..63e634759 100644 --- a/charmcraft/application/main.py +++ b/charmcraft/application/main.py @@ -140,7 +140,8 @@ def _get_app_plugins(self) -> dict[str, PluginType]: if any(base not in effective_charm_bases for base in bases): plugins.pop("charm", None) effective_reactive_bases = const.REACTIVE_PLUGIN_BASES - if util.strtobool(str(os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR))): + experimental_env = os.getenv(const.EXPERIMENTAL_EXTENSIONS_ENV_VAR) + if experimental_env and util.strtobool(str(experimental_env)): effective_reactive_bases = ( effective_reactive_bases | const.REACTIVE_PLUGIN_EXPERIMENTAL_BASES ) From de0a12bdc2ebac5d94490fa89c1bc4244087d4ac Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Thu, 18 Jun 2026 16:06:25 +0300 Subject: [PATCH 14/26] fix: default to v2 and rename v1 frameworks --- charmcraft/extensions/app.py | 52 +++++++++++++++++++----------------- tests/extensions/test_app.py | 48 ++++++++++++++++----------------- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 6a95a547d..6b45020fc 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -99,9 +99,9 @@ def __call__(self, *, project_root: Path, yaml_data: dict[str, Any]) -> Extensio :return: an Extension instance from the appropriate version. """ bases = get_project_bases(yaml_data) - if any(base in self._v2_cls.get_supported_bases() for base in bases): - return self._v2_cls(project_root=project_root, yaml_data=yaml_data) - return self._v1_cls(project_root=project_root, yaml_data=yaml_data) + if any(base in self._v1_cls.get_supported_bases() for base in bases): + return self._v1_cls(project_root=project_root, yaml_data=yaml_data) + return self._v2_cls(project_root=project_root, yaml_data=yaml_data) def get_supported_bases(self) -> list[tuple[str, str]]: """Return merged supported bases from both V1 and V2, deduped and ordered. @@ -120,9 +120,9 @@ def is_experimental(self, base: tuple[str, str] | None) -> bool: :param base: the target base tuple or None. :return: True if the base is experimental, False otherwise. """ - if base in self._v2_cls.get_supported_bases(): - return self._v2_cls.is_experimental(base) - return self._v1_cls.is_experimental(base) + if base in self._v1_cls.get_supported_bases(): + return self._v1_cls.is_experimental(base) + return self._v2_cls.is_experimental(base) class _AppBase(SinglePlatformExtension): @@ -456,7 +456,7 @@ def is_experimental(base: tuple[str, str] | None) -> bool: # noqa: ARG004 } -class FlaskFramework(_AppBase): +class FlaskFrameworkV1(_AppBase): """Extension for 12-factor Flask applications.""" framework = "flask" @@ -505,13 +505,13 @@ class FlaskFrameworkV2(_AppBaseV2): """Extension v2 for 12-factor Flask applications.""" framework = "flask" - options = FlaskFramework.options + options = FlaskFrameworkV1.options -FlaskFrameworkFactory = _FrameworkFactory(FlaskFramework, FlaskFrameworkV2) +FlaskFrameworkFactory = _FrameworkFactory(FlaskFrameworkV1, FlaskFrameworkV2) -class DjangoFramework(_AppBase): +class DjangoFrameworkV1(_AppBase): """Extension for 12-factor Django applications.""" framework = "django" @@ -552,14 +552,14 @@ class DjangoFrameworkV2(_AppBaseV2): """Extension v2 for 12-factor Django applications.""" framework = "django" - actions = {**DjangoFramework.actions} - options = DjangoFramework.options + actions = {**DjangoFrameworkV1.actions} + options = DjangoFrameworkV1.options -DjangoFrameworkFactory = _FrameworkFactory(DjangoFramework, DjangoFrameworkV2) +DjangoFrameworkFactory = _FrameworkFactory(DjangoFrameworkV1, DjangoFrameworkV2) -class GoFramework(_AppBase): +class GoFrameworkV1(_AppBase): """Extension for 12-factor Go applications.""" framework = "go" @@ -597,10 +597,10 @@ class GoFrameworkV2(_AppBaseV2): } -GoFrameworkFactory = _FrameworkFactory(GoFramework, GoFrameworkV2) +GoFrameworkFactory = _FrameworkFactory(GoFrameworkV1, GoFrameworkV2) -class FastAPIFramework(_AppBase): +class FastAPIFrameworkV1(_AppBase): """Extension for 12-factor FastAPI applications.""" framework = "fastapi" @@ -645,13 +645,13 @@ class FastAPIFrameworkV2(_AppBaseV2): """Extension v2 for 12-factor FastAPI applications.""" framework = "fastapi" - options = FastAPIFramework.options + options = FastAPIFrameworkV1.options -FastAPIFrameworkFactory = _FrameworkFactory(FastAPIFramework, FastAPIFrameworkV2) +FastAPIFrameworkFactory = _FrameworkFactory(FastAPIFrameworkV1, FastAPIFrameworkV2) -class ExpressJSFramework(_AppBase): +class ExpressJSFrameworkV1(_AppBase): """Extension for 12-factor ExpressJS applications.""" framework = "expressjs" @@ -682,13 +682,15 @@ class ExpressJSFrameworkV2(_AppBaseV2): """Extension v2 for 12-factor ExpressJS applications.""" framework = "expressjs" - options = ExpressJSFramework.options + options = ExpressJSFrameworkV1.options -ExpressJSFrameworkFactory = _FrameworkFactory(ExpressJSFramework, ExpressJSFrameworkV2) +ExpressJSFrameworkFactory = _FrameworkFactory( + ExpressJSFrameworkV1, ExpressJSFrameworkV2 +) -class SpringBootFramework(_AppBase): +class SpringBootFrameworkV1(_AppBase): """Extension for 12-factor Spring Boot applications.""" framework = "spring-boot" @@ -748,10 +750,10 @@ class SpringBootFrameworkV2(_AppBaseV2): """Extension v2 for 12-factor Spring Boot applications.""" framework = "spring-boot" - options = SpringBootFramework.options - endpoint_dynamic_options = SpringBootFramework.endpoint_dynamic_options + options = SpringBootFrameworkV1.options + endpoint_dynamic_options = SpringBootFrameworkV1.endpoint_dynamic_options SpringBootFrameworkFactory = _FrameworkFactory( - SpringBootFramework, SpringBootFrameworkV2 + SpringBootFrameworkV1, SpringBootFrameworkV2 ) diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 65e6fb277..6a4b1ac08 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -21,16 +21,16 @@ from charmcraft import errors, extensions from charmcraft.errors import ExtensionError from charmcraft.extensions.app import ( - DjangoFramework, - ExpressJSFramework, + DjangoFrameworkV1, ExpressJSFrameworkFactory, - FastAPIFramework, + ExpressJSFrameworkV1, FastAPIFrameworkFactory, - FlaskFramework, + FastAPIFrameworkV1, FlaskFrameworkFactory, - GoFramework, + FlaskFrameworkV1, GoFrameworkFactory, - SpringBootFramework, + GoFrameworkV1, + SpringBootFrameworkV1, ) NON_OPTIONAL_OPTIONS = { @@ -81,7 +81,7 @@ def make_spring_boot_input_yaml(): make_flask_input_yaml(), False, { - "actions": FlaskFramework.actions, + "actions": FlaskFrameworkV1.actions, "assumes": ["k8s-api"], "bases": [{"channel": "22.04", "name": "ubuntu"}], "containers": { @@ -107,7 +107,7 @@ def make_spring_boot_input_yaml(): ], "config": { "options": { - **FlaskFramework.options, + **FlaskFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], }, }, @@ -158,7 +158,7 @@ def make_spring_boot_input_yaml(): }, False, { - "actions": DjangoFramework.actions, + "actions": DjangoFrameworkV1.actions, "assumes": ["k8s-api"], "base": "ubuntu@22.04", "platforms": { @@ -192,7 +192,7 @@ def make_spring_boot_input_yaml(): ], "config": { "options": { - **DjangoFramework.options, + **DjangoFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], }, }, @@ -238,7 +238,7 @@ def make_spring_boot_input_yaml(): }, False, { - "actions": GoFramework.actions, + "actions": GoFrameworkV1.actions, "assumes": ["k8s-api"], "base": "ubuntu@24.04", "platforms": { @@ -267,7 +267,7 @@ def make_spring_boot_input_yaml(): ], "config": { "options": { - **GoFramework.options, + **GoFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], }, }, @@ -313,7 +313,7 @@ def make_spring_boot_input_yaml(): }, False, { - "actions": FastAPIFramework.actions, + "actions": FastAPIFrameworkV1.actions, "assumes": ["k8s-api"], "base": "ubuntu@24.04", "platforms": { @@ -342,7 +342,7 @@ def make_spring_boot_input_yaml(): ], "config": { "options": { - **FastAPIFramework.options, + **FastAPIFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], }, }, @@ -388,7 +388,7 @@ def make_spring_boot_input_yaml(): }, False, { - "actions": ExpressJSFramework.actions, + "actions": ExpressJSFrameworkV1.actions, "assumes": ["k8s-api"], "base": "ubuntu@24.04", "platforms": { @@ -417,7 +417,7 @@ def make_spring_boot_input_yaml(): ], "config": { "options": { - **ExpressJSFramework.options, + **ExpressJSFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], }, }, @@ -452,7 +452,7 @@ def make_spring_boot_input_yaml(): make_spring_boot_input_yaml(), True, { - "actions": SpringBootFramework.actions, + "actions": SpringBootFrameworkV1.actions, "assumes": ["k8s-api"], "base": "ubuntu@24.04", "platforms": { @@ -481,7 +481,7 @@ def make_spring_boot_input_yaml(): ], "config": { "options": { - **SpringBootFramework.options, + **SpringBootFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], }, }, @@ -934,7 +934,7 @@ def test_flask_merge_options(flask_input_yaml, tmp_path): applied = extensions.apply_extensions(tmp_path, flask_input_yaml) assert applied["config"] == { "options": { - **FlaskFramework.options, + **FlaskFrameworkV1.options, **added_options, } } @@ -944,7 +944,7 @@ def test_flask_merge_action(flask_input_yaml, tmp_path): added_actions = {"foobar": {}} flask_input_yaml["actions"] = added_actions applied = extensions.apply_extensions(tmp_path, flask_input_yaml) - assert applied["actions"] == {**FlaskFramework.actions, **added_actions} + assert applied["actions"] == {**FlaskFrameworkV1.actions, **added_actions} def test_flask_merge_relation(flask_input_yaml, tmp_path): @@ -969,7 +969,7 @@ def test_flask_merge_charm_libs(flask_input_yaml, tmp_path): added_charm_libs = [{"lib": "smtp_integrator.smtp", "version": "0"}] flask_input_yaml["charm-libs"] = added_charm_libs applied = extensions.apply_extensions(tmp_path, flask_input_yaml) - assert applied["charm-libs"] == [*FlaskFramework._CHARM_LIBS, *added_charm_libs] + assert applied["charm-libs"] == [*FlaskFrameworkV1._CHARM_LIBS, *added_charm_libs] INCOMPATIBLE_FIELDS_TEST_PARAMETERS = [ @@ -1087,7 +1087,7 @@ def test_handle_charm_part_adds_part(flask_input_yaml, tmp_path): make_flask_input_yaml(), {"oidc-foobar": {"interface": "oauth"}}, { - **FlaskFramework.options, + **FlaskFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], "oidc-foobar-redirect-path": { "type": "string", @@ -1106,7 +1106,7 @@ def test_handle_charm_part_adds_part(flask_input_yaml, tmp_path): make_spring_boot_input_yaml(), {"oidc-foobar": {"interface": "oauth"}}, { - **SpringBootFramework.options, + **SpringBootFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], "oidc-foobar-redirect-path": { "type": "string", @@ -1134,7 +1134,7 @@ def test_handle_charm_part_adds_part(flask_input_yaml, tmp_path): "other-oidc": {"interface": "oauth"}, }, { - **FlaskFramework.options, + **FlaskFrameworkV1.options, **NON_OPTIONAL_OPTIONS["options"], "oidc-foobar-redirect-path": { "type": "string", From ba30292b0b0c92c7fb14531ede89a6d4a387b245 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Wed, 24 Jun 2026 09:00:43 +0300 Subject: [PATCH 15/26] docs: Update docs for v2 12-factor extensions. --- docs/howto/manage-extensions.rst | 551 +++++++++++++----- .../use-web-app-charm.rst | 428 ++++++++++---- 2 files changed, 721 insertions(+), 258 deletions(-) diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst index 8ab98965c..b3460b4f3 100644 --- a/docs/howto/manage-extensions.rst +++ b/docs/howto/manage-extensions.rst @@ -19,7 +19,7 @@ For example: $ charmcraft list-extensions Extension name Supported bases Experimental bases ---------------- ----------------- -------------------- - flask-framework ubuntu@22.04 + flask-framework ubuntu@22.04 ubuntu@26.04 View details about the extension in use --------------------------------------- @@ -31,71 +31,156 @@ extension. .. dropdown:: Example - .. code-block:: bash + .. tab-set:: - mkdir my-flask-app-k8s - cd my-flask-app-k8s/ - charmcraft init --profile flask-framework + .. tab-item:: V1 + :sync: v1 - .. terminal:: + .. code-block:: bash - Charmed operator package file and directory tree initialised. + mkdir my-flask-app-k8s + cd my-flask-app-k8s/ + charmcraft init --profile flask-framework - Now edit the following package files to provide fundamental charm metadata - and other information: + .. terminal:: - charmcraft.yaml - src/charm.py - README.md + Charmed operator package file and directory tree initialised. - .. code-block:: bash + Now edit the following package files to provide fundamental charm metadata + and other information: - ls -R + charmcraft.yaml + src/charm.py + README.md - .. terminal:: + .. code-block:: bash - .: - charmcraft.yaml requirements.txt src + ls -R - ./src: - charm.py + .. terminal:: - .. code-block:: bash + .: + charmcraft.yaml requirements.txt src - cat charmcraft.yaml + ./src: + charm.py - .. code-block:: yaml + .. code-block:: bash - name: my-flask-app-k8s + cat charmcraft.yaml - type: charm + .. code-block:: yaml - bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" + name: my-flask-app-k8s - # (Required) - summary: A very short one-line summary of the flask application. + type: charm - # (Required) - description: | - A comprehensive overview of your Flask application. + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" - extensions: - - flask-framework + # (Required) + summary: A very short one-line summary of the flask application. - # Uncomment the integrations used by your application - # requires: - # mysql: - # interface: mysql_client - # limit: 1 - # postgresql: - # interface: postgresql_client - # limit: 1 + # (Required) + description: | + A comprehensive overview of your Flask application. + + extensions: + - flask-framework + + # Uncomment the integrations used by your application + # requires: + # mysql: + # interface: mysql_client + # limit: 1 + # postgresql: + # interface: postgresql_client + # limit: 1 + + .. tab-item:: V2 + :sync: v2 + + + .. code-block:: bash + + mkdir my-flask-app-k8s + cd my-flask-app-k8s/ + charmcraft init --profile flask-framework + + .. terminal:: + + Charmed operator package file and directory tree initialised. + + Now edit the following package files to provide fundamental charm metadata + and other information: + + charmcraft.yaml + src/charm.py + README.md + + .. code-block:: bash + + ls -R + + .. terminal:: + + .: + charmcraft.yaml requirements.txt src + + ./src: + charm.py + + .. code-block:: bash + + cat charmcraft.yaml + + .. code-block:: yaml + + name: my-flask-app-k8s + + type: charm + + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" + + # (Required) + summary: A very short one-line summary of the flask application. + + # (Required) + description: | + A comprehensive overview of your Flask application. + + extensions: + - flask-framework + + # Uncomment the integrations used by your application + # requires: + # mysql: + # interface: mysql_client + # limit: 1 + # postgresql: + # interface: postgresql_client + # limit: 1 + + To activate V2 version you need to update the base to 26.04. + + .. code-block:: yaml + + name: my-flask-app-k8s + + type: charm + + base: ubuntu@26.04 To view details about what that extension is adding to your charm, set the ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` environment variable to ``1``, @@ -103,113 +188,273 @@ then run ``charmcraft expand-extensions``. For example: .. dropdown:: Expanding an extension - .. code-block:: bash - - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions - - .. terminal:: - - *EXPERIMENTAL* extension 'flask-framework' enabled - name: my-flask-app-k8s - summary: A very short one-line summary of the flask application. - description: | - A comprehensive overview of your Flask application. - parts: - charm: - source: . - charm-entrypoint: src/charm.py - charm-binary-python-packages: [] - charm-python-packages: [] - charm-requirements: - - requirements.txt - charm-strict-dependencies: false - plugin: charm - type: charm - bases: - - build-on: - - name: ubuntu - channel: '22.04' - run-on: - - name: ubuntu - channel: '22.04' - actions: - rotate-secret-key: - description: Rotate the flask secret key. Users will be forced to log in again. - This might be useful if a security breach occurs. - assumes: - - k8s-api - containers: - flask-app: - resource: flask-app-image - peers: - secret-storage: - interface: secret-storage - provides: - metrics-endpoint: - interface: prometheus_scrape - grafana-dashboard: - interface: grafana_dashboard - requires: - logging: - interface: loki_push_api - ingress: - interface: ingress - limit: 1 - resources: - flask-app-image: - type: oci-image - description: flask application image. - config: - options: - webserver-keepalive: - type: int - description: Time in seconds for webserver to wait for requests on a Keep-Alive - connection. - webserver-threads: - type: int - description: Run each webserver worker with the specified number of threads. - webserver-timeout: - type: int - description: Time in seconds to kill and restart silent webserver workers. - webserver-workers: - type: int - description: The number of webserver worker processes for handling requests. - flask-application-root: - type: string - description: Path in which the application / web server is mounted. This configuration - will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-debug: - type: boolean - description: Whether Flask debug mode is enabled. - flask-env: - type: string - description: What environment the Flask app is running in, by default it's 'production'. - flask-permanent-session-lifetime: - type: int - description: Time in seconds for the cookie to expire in the Flask application - permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME - environment variable. Run app.config.from_prefixed_env() in your Flask application - in order to receive this configuration. - flask-preferred-url-scheme: - type: string - default: HTTPS - description: Scheme for generating external URLs when not in a request context - in the Flask application. By default, it's "HTTPS". This configuration will - set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-secret-key: - type: string - description: The secret key used for securely signing the session cookie and - for any other security related needs by your Flask application. This configuration - will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-session-cookie-secure: - type: boolean - description: Set the secure attribute in the Flask application cookies. This - configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. - Run app.config.from_prefixed_env() in your Flask application in order to - receive this configuration. + .. tab-set:: + + .. tab-item:: V1 + :sync: v1 + + .. code-block:: bash + + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions + + .. terminal:: + + *EXPERIMENTAL* extension 'flask-framework' enabled + name: my-flask-app-k8s + summary: A very short one-line summary of the flask application. + description: | + A comprehensive overview of your Flask application. + parts: + charm: + source: . + charm-entrypoint: src/charm.py + charm-binary-python-packages: [] + charm-python-packages: [] + charm-requirements: + - requirements.txt + charm-strict-dependencies: false + plugin: charm + type: charm + bases: + - build-on: + - name: ubuntu + channel: '22.04' + run-on: + - name: ubuntu + channel: '22.04' + actions: + rotate-secret-key: + description: Rotate the flask secret key. Users will be forced to log in again. + This might be useful if a security breach occurs. + assumes: + - k8s-api + containers: + flask-app: + resource: flask-app-image + peers: + secret-storage: + interface: secret-storage + provides: + metrics-endpoint: + interface: prometheus_scrape + grafana-dashboard: + interface: grafana_dashboard + requires: + logging: + interface: loki_push_api + ingress: + interface: ingress + limit: 1 + resources: + flask-app-image: + type: oci-image + description: flask application image. + config: + options: + webserver-keepalive: + type: int + description: Time in seconds for webserver to wait for requests on a Keep-Alive + connection. + webserver-threads: + type: int + description: Run each webserver worker with the specified number of threads. + webserver-timeout: + type: int + description: Time in seconds to kill and restart silent webserver workers. + webserver-workers: + type: int + description: The number of webserver worker processes for handling requests. + flask-application-root: + type: string + description: Path in which the application / web server is mounted. This configuration + will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-debug: + type: boolean + description: Whether Flask debug mode is enabled. + flask-env: + type: string + description: What environment the Flask app is running in, by default it's 'production'. + flask-permanent-session-lifetime: + type: int + description: Time in seconds for the cookie to expire in the Flask application + permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME + environment variable. Run app.config.from_prefixed_env() in your Flask application + in order to receive this configuration. + flask-preferred-url-scheme: + type: string + default: HTTPS + description: Scheme for generating external URLs when not in a request context + in the Flask application. By default, it's "HTTPS". This configuration will + set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-secret-key: + type: string + description: The secret key used for securely signing the session cookie and + for any other security related needs by your Flask application. This configuration + will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-session-cookie-secure: + type: boolean + description: Set the secure attribute in the Flask application cookies. This + configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. + Run app.config.from_prefixed_env() in your Flask application in order to + receive this configuration. + + .. tab-item:: V2 + :sync: v2 + + .. code-block:: bash + + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions + + .. terminal:: + + *EXPERIMENTAL* extension 'flask-framework' enabled for base(s): ubuntu@26.04 + name: my-flask-app-k8s + summary: A very short one-line summary of the Flask application. + description: | + A comprehensive overview of your Flask application. + base: ubuntu@26.04 + platforms: + amd64: + build-on: + - amd64 + build-for: + - amd64 + parts: + charm: + plugin: uv + source: . + uv-groups: + - charmlibs-pydeps + build-snaps: + - astral-uv + - rustup + override-build: |- + rustup default stable + craftctl default + type: charm + charm-libs: + - lib: traefik-k8s.ingress + version: '2' + - lib: observability-libs.juju_topology + version: '0' + - lib: grafana-k8s.grafana_dashboard + version: '0' + - lib: loki-k8s.loki_push_api + version: '1' + - lib: data-platform-libs.data_interfaces + version: '0' + - lib: prometheus-k8s.prometheus_scrape + version: '0' + - lib: redis-k8s.redis + version: '0' + - lib: data-platform-libs.s3 + version: '0' + - lib: saml-integrator.saml + version: '0' + - lib: tempo-coordinator-k8s.tracing + version: '0' + - lib: smtp-integrator.smtp + version: '0' + - lib: openfga-k8s.openfga + version: '1' + - lib: hydra.oauth + version: '0' + - lib: squid-forward-proxy.http_proxy + version: '0' + actions: + rotate-secret-key: + description: Rotate the secret key. Users will be forced to log in again. This + might be useful if a security breach occurs. + assumes: + - k8s-api + containers: + app: + resource: app-image + peers: + secret-storage: + interface: secret-storage + provides: + metrics-endpoint: + interface: prometheus_scrape + grafana-dashboard: + interface: grafana_dashboard + requires: + logging: + interface: loki_push_api + ingress: + interface: ingress + limit: 1 + resources: + app-image: + type: oci-image + description: flask application image. + config: + options: + webserver-keepalive: + type: int + description: Time in seconds for webserver to wait for requests on a Keep-Alive + connection. + webserver-threads: + type: int + description: Run each webserver worker with the specified number of threads. + webserver-timeout: + type: int + description: Time in seconds to kill and restart silent webserver workers. + webserver-workers: + type: int + description: The number of webserver worker processes for handling requests. + webserver-worker-class: + type: string + description: The webserver worker process class for handling requests. Can be + either 'gevent' or 'sync'. + flask-application-root: + type: string + description: Path in which the application / web server is mounted. This configuration + will set the FLASK_APPLICATION_ROOT environment variable. Run `app.config.from_prefixed_env()` + in your Flask application in order to receive this configuration. + flask-debug: + type: boolean + description: Whether Flask debug mode is enabled. + flask-env: + type: string + description: What environment the Flask app is running in, by default it's 'production'. + flask-permanent-session-lifetime: + type: int + description: Time in seconds for the cookie to expire in the Flask application + permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME + environment variable. Run `app.config.from_prefixed_env()` in your Flask application + in order to receive this configuration. + flask-preferred-url-scheme: + type: string + default: HTTPS + description: Scheme for generating external URLs when not in a request context + in the Flask application. By default, it's "HTTPS". This configuration will + set the FLASK_PREFERRED_URL_SCHEME environment variable. Run `app.config.from_prefixed_env()` + in your Flask application in order to receive this configuration. + flask-secret-key: + type: string + description: The secret key used for securely signing the session cookie and + for any other security related needs by your Flask application. This configuration + will set the FLASK_SECRET_KEY environment variable. Run `app.config.from_prefixed_env()` + in your Flask application in order to receive this configuration. + flask-secret-key-id: + type: secret + description: 'This configuration is similar to `flask-secret-key`, but instead + accepts a Juju user secret ID. The secret should contain a single key, "value", + which maps to the actual Flask secret key. To create the secret, run the following + command: `juju add-secret my-flask-secret-key value= && juju + grant-secret my-flask-secret-key flask-k8s`, and use the output secret ID + to configure this option.' + flask-session-cookie-secure: + type: boolean + description: Set the secure attribute in the Flask application cookies. This + configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. + Run `app.config.from_prefixed_env()` in your Flask application in order to + receive this configuration. + To expand ``charmcraft.yaml`` using the extensions specified in the file and output the resulting configuration to the terminal, run diff --git a/docs/howto/manage-web-app-charms/use-web-app-charm.rst b/docs/howto/manage-web-app-charms/use-web-app-charm.rst index 5dbf5550f..7be3f025b 100644 --- a/docs/howto/manage-web-app-charms/use-web-app-charm.rst +++ b/docs/howto/manage-web-app-charms/use-web-app-charm.rst @@ -61,48 +61,101 @@ To view the Pebble logs for a deployed web app, run: .. tab-set:: - .. tab-item:: Django - :sync: django + .. tab-item:: V1 + :sync: v1 - .. code-block:: bash + .. tab-set:: - juju ssh --container django-app /0 pebble logs + .. tab-item:: Django + :sync: django - .. tab-item:: Express - :sync: express + .. code-block:: bash - .. code-block:: bash + juju ssh --container django-app /0 pebble logs - juju ssh --container app /0 pebble logs + .. tab-item:: Express + :sync: express - .. tab-item:: FastAPI - :sync: fastapi + .. code-block:: bash - .. code-block:: bash + juju ssh --container app /0 pebble logs - juju ssh --container app /0 pebble logs + .. tab-item:: FastAPI + :sync: fastapi - .. tab-item:: Flask - :sync: flask + .. code-block:: bash - .. code-block:: bash + juju ssh --container app /0 pebble logs - juju ssh --container flask-app /0 pebble logs + .. tab-item:: Flask + :sync: flask - .. tab-item:: Go - :sync: go + .. code-block:: bash - .. code-block:: bash + juju ssh --container flask-app /0 pebble logs - juju ssh --container app /0 pebble logs + .. tab-item:: Go + :sync: go - .. tab-item:: Spring Boot - :sync: spring-boot + .. code-block:: bash - .. code-block:: bash + juju ssh --container app /0 pebble logs - juju ssh /0 \ - PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble logs + .. tab-item:: Spring Boot + :sync: spring-boot + + .. code-block:: bash + + juju ssh /0 \ + PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble logs + + .. tab-item:: V2 + :sync: v2 + + .. tab-set:: + + .. tab-item:: Django + :sync: django + + .. code-block:: bash + + juju ssh --container app /0 pebble logs + + .. tab-item:: Express + :sync: express + + .. code-block:: bash + + juju ssh --container app /0 pebble logs + + .. tab-item:: FastAPI + :sync: fastapi + + .. code-block:: bash + + juju ssh --container app /0 pebble logs + + .. tab-item:: Flask + :sync: flask + + .. code-block:: bash + + juju ssh --container app /0 pebble logs + + .. tab-item:: Go + :sync: go + + .. code-block:: bash + + juju ssh --container app /0 pebble logs + + .. tab-item:: Spring Boot + :sync: spring-boot + + .. code-block:: bash + + juju ssh /0 \ + PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble logs .. seealso:: @@ -116,48 +169,101 @@ To view more details about the web app itself, run: .. tab-set:: - .. tab-item:: Django - :sync: django + .. tab-item:: V1 + :sync: v1 + + .. tab-set:: - .. code-block:: bash + .. tab-item:: Django + :sync: django - juju ssh --container django-app /0 pebble plan + .. code-block:: bash - .. tab-item:: Express - :sync: express + juju ssh --container django-app /0 pebble plan - .. code-block:: bash + .. tab-item:: Express + :sync: express - juju ssh --container app /0 pebble plan + .. code-block:: bash - .. tab-item:: FastAPI - :sync: fastapi + juju ssh --container app /0 pebble plan - .. code-block:: bash + .. tab-item:: FastAPI + :sync: fastapi - juju ssh --container app /0 pebble plan + .. code-block:: bash - .. tab-item:: Flask - :sync: flask + juju ssh --container app /0 pebble plan - .. code-block:: bash + .. tab-item:: Flask + :sync: flask - juju ssh --container flask-app /0 pebble plan + .. code-block:: bash - .. tab-item:: Go - :sync: go + juju ssh --container flask-app /0 pebble plan - .. code-block:: bash + .. tab-item:: Go + :sync: go - juju ssh --container app /0 pebble plan + .. code-block:: bash - .. tab-item:: Spring Boot - :sync: spring-boot + juju ssh --container app /0 pebble plan - .. code-block:: bash + .. tab-item:: Spring Boot + :sync: spring-boot - juju ssh /0 \ - PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble plan + .. code-block:: bash + + juju ssh /0 \ + PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble plan + + .. tab-item:: V2 + :sync: v2 + + .. tab-set:: + + .. tab-item:: Django + :sync: django + + .. code-block:: bash + + juju ssh --container app /0 pebble plan + + .. tab-item:: Express + :sync: express + + .. code-block:: bash + + juju ssh --container app /0 pebble plan + + .. tab-item:: FastAPI + :sync: fastapi + + .. code-block:: bash + + juju ssh --container app /0 pebble plan + + .. tab-item:: Flask + :sync: flask + + .. code-block:: bash + + juju ssh --container app /0 pebble plan + + .. tab-item:: Go + :sync: go + + .. code-block:: bash + + juju ssh --container app /0 pebble plan + + .. tab-item:: Spring Boot + :sync: spring-boot + + .. code-block:: bash + + juju ssh /0 \ + PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble plan This command provides information on what services you may start in your app and what environment variables exist (i.e., what is available for the app to @@ -176,54 +282,114 @@ Juju container: .. tab-set:: - .. tab-item:: Django - :sync: django + .. tab-item:: V1 + :sync: v1 + + .. tab-set:: - .. code-block:: bash + .. tab-item:: Django + :sync: django - juju ssh --container django-app /0 \ - pebble exec --context=django -- bash + .. code-block:: bash - .. tab-item:: Express - :sync: express + juju ssh --container django-app /0 \ + pebble exec --context=django -- bash - .. code-block:: bash + .. tab-item:: Express + :sync: express - juju ssh --container app /0 \ - pebble exec --context=expressjs -- bash + .. code-block:: bash - .. tab-item:: FastAPI - :sync: fastapi + juju ssh --container app /0 \ + pebble exec --context=expressjs -- bash - .. code-block:: bash + .. tab-item:: FastAPI + :sync: fastapi - juju ssh --container app /0 \ - pebble exec --context=fastapi -- bash + .. code-block:: bash - .. tab-item:: Flask - :sync: flask + juju ssh --container app /0 \ + pebble exec --context=fastapi -- bash - .. code-block:: bash + .. tab-item:: Flask + :sync: flask - juju ssh --container flask-app /0 \ - pebble exec --context=flask -- bash + .. code-block:: bash - .. tab-item:: Go - :sync: go + juju ssh --container flask-app /0 \ + pebble exec --context=flask -- bash - .. code-block:: bash + .. tab-item:: Go + :sync: go - juju ssh --container app /0 \ - pebble exec --context=go -- bash + .. code-block:: bash - .. tab-item:: Spring Boot - :sync: spring-boot + juju ssh --container app /0 \ + pebble exec --context=go -- bash - .. code-block:: bash + .. tab-item:: Spring Boot + :sync: spring-boot - juju ssh /0 \ - PEBBLE_SOCKET=/charm/containers/app/pebble.socket \ - /charm/bin/pebble exec --context=spring-boot -- bash + .. code-block:: bash + + juju ssh /0 \ + PEBBLE_SOCKET=/charm/containers/app/pebble.socket \ + /charm/bin/pebble exec --context=spring-boot -- bash + + + .. tab-item:: V2 + :sync: v2 + + .. tab-set:: + + .. tab-item:: Django + :sync: django + + .. code-block:: bash + + juju ssh --container app /0 \ + pebble exec --context=django -- bash + + .. tab-item:: Express + :sync: express + + .. code-block:: bash + + juju ssh --container app /0 \ + pebble exec --context=expressjs -- bash + + .. tab-item:: FastAPI + :sync: fastapi + + .. code-block:: bash + + juju ssh --container app /0 \ + pebble exec --context=fastapi -- bash + + .. tab-item:: Flask + :sync: flask + + .. code-block:: bash + + juju ssh --container app /0 \ + pebble exec --context=flask -- bash + + .. tab-item:: Go + :sync: go + + .. code-block:: bash + + juju ssh --container app /0 \ + pebble exec --context=go -- bash + + .. tab-item:: Spring Boot + :sync: spring-boot + + .. code-block:: bash + + juju ssh /0 \ + PEBBLE_SOCKET=/charm/containers/app/pebble.socket \ + /charm/bin/pebble exec --context=spring-boot -- bash .. important:: @@ -269,47 +435,99 @@ name of the web app with the ``-c`` option. .. tab-set:: - .. tab-item:: Django - :sync: django + .. tab-item:: V1 + :sync: v1 + + .. tab-set:: + + .. tab-item:: Django + :sync: django + + .. code-block:: bash + + microk8s kubectl logs -n -c django-app + + .. tab-item:: Express + :sync: express + + .. code-block:: bash + + microk8s kubectl logs -n -c app + + .. tab-item:: FastAPI + :sync: fastapi + + .. code-block:: bash + + microk8s kubectl logs -n -c app + + .. tab-item:: Flask + :sync: flask + + .. code-block:: bash + + microk8s kubectl logs -n -c flask-app + + .. tab-item:: Go + :sync: go + + .. code-block:: bash + + microk8s kubectl logs -n -c app + + .. tab-item:: Spring Boot + :sync: spring-boot + + .. code-block:: bash + + microk8s kubectl logs -n -c app + + .. tab-item:: V2 + :sync: v2 + + .. tab-set:: + + .. tab-item:: Django + :sync: django - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c django-app + microk8s kubectl logs -n -c app - .. tab-item:: Express - :sync: express + .. tab-item:: Express + :sync: express - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app - .. tab-item:: FastAPI - :sync: fastapi + .. tab-item:: FastAPI + :sync: fastapi - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app - .. tab-item:: Flask - :sync: flask + .. tab-item:: Flask + :sync: flask - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c flask-app + microk8s kubectl logs -n -c app - .. tab-item:: Go - :sync: go + .. tab-item:: Go + :sync: go - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app - .. tab-item:: Spring Boot - :sync: spring-boot + .. tab-item:: Spring Boot + :sync: spring-boot - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app .. seealso:: From e0306a4898ab6855af8ea75f2d8c401844a014e0 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Fri, 26 Jun 2026 13:00:54 +0300 Subject: [PATCH 16/26] docs: add linting and testing dependencies to project templates --- .../pyproject.toml.j2 | 19 ++++++ .../init-django-framework-26.04/tox.ini.j2 | 64 +++++++++---------- .../pyproject.toml.j2 | 19 ++++++ .../init-expressjs-framework-26.04/tox.ini.j2 | 64 +++++++++---------- .../pyproject.toml.j2 | 19 ++++++ .../init-fastapi-framework-26.04/tox.ini.j2 | 64 +++++++++---------- .../pyproject.toml.j2 | 19 ++++++ .../init-flask-framework-26.04/tox.ini.j2 | 64 +++++++++---------- .../init-go-framework-26.04/pyproject.toml.j2 | 19 ++++++ .../init-go-framework-26.04/tox.ini.j2 | 64 +++++++++---------- .../pyproject.toml.j2 | 19 ++++++ .../tox.ini.j2 | 64 +++++++++---------- 12 files changed, 294 insertions(+), 204 deletions(-) diff --git a/charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 index 85441d1bd..a3c32be91 100644 --- a/charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 +++ b/charmcraft/templates/init-django-framework-26.04/pyproject.toml.j2 @@ -25,6 +25,25 @@ charmlibs-pydeps = [ "cosl==1.9.1", "pydantic==2.13.3", ] +# Dependencies of linting and static type checks +lint = [ + "ruff", + "codespell", + "pyright", +] +# Dependencies of unit tests +unit = [ + "coverage[toml]", + "ops[testing]", + "pytest", +] +# Dependencies of integration tests +integration = [ + "jubilant>=1.8,<2", + "pytest", + "pytest-jubilant>=2.0.1,<3", + "PyYAML", +] # Testing tools configuration diff --git a/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 index bd15d819c..e1e05a12b 100644 --- a/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 @@ -31,50 +31,46 @@ commands = ruff check --fix {[vars]all_path} [testenv:lint] -description = Check code against coding style standards -deps = - ruff - codespell +description = Check code against coding style standards, and static checks +runner = uv-venv-lock-runner +dependency_groups = + lint + unit + integration commands = codespell {tox_root} ruff check {[vars]all_path} ruff format --check --diff {[vars]all_path} + pyright {posargs} [testenv:unit] description = Run unit tests -deps = - pytest - coverage[toml] - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + unit commands = - coverage run --source={[vars]src_path} \ - -m pytest \ - --tb native \ - -v \ - -s \ - {posargs} \ - {[vars]tests_path}/unit + coverage run --source={[vars]src_path} -m pytest \ + -v \ + -s \ + --tb native \ + {[vars]tests_path}/unit \ + {posargs} coverage report -[testenv:static] -description = Run static type checks -deps = - pyright - -r {tox_root}/requirements.txt -commands = - pyright {posargs} - [testenv:integration] description = Run integration tests -deps = - pytest - juju - pytest-operator - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + integration +pass_env = + # The integration tests don't pack the charm. If CHARM_PATH is set, the tests deploy the + # specified .charm file. Otherwise, the tests look for a .charm file in the project dir. + CHARM_PATH commands = - pytest -v \ - -s \ - --tb native \ - --log-cli-level=INFO \ - {posargs} \ - {[vars]tests_path}/integration + pytest \ + -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tests_path}/integration \ + {posargs} diff --git a/charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 index 85441d1bd..a3c32be91 100644 --- a/charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 +++ b/charmcraft/templates/init-expressjs-framework-26.04/pyproject.toml.j2 @@ -25,6 +25,25 @@ charmlibs-pydeps = [ "cosl==1.9.1", "pydantic==2.13.3", ] +# Dependencies of linting and static type checks +lint = [ + "ruff", + "codespell", + "pyright", +] +# Dependencies of unit tests +unit = [ + "coverage[toml]", + "ops[testing]", + "pytest", +] +# Dependencies of integration tests +integration = [ + "jubilant>=1.8,<2", + "pytest", + "pytest-jubilant>=2.0.1,<3", + "PyYAML", +] # Testing tools configuration diff --git a/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 index bd15d819c..e1e05a12b 100644 --- a/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 @@ -31,50 +31,46 @@ commands = ruff check --fix {[vars]all_path} [testenv:lint] -description = Check code against coding style standards -deps = - ruff - codespell +description = Check code against coding style standards, and static checks +runner = uv-venv-lock-runner +dependency_groups = + lint + unit + integration commands = codespell {tox_root} ruff check {[vars]all_path} ruff format --check --diff {[vars]all_path} + pyright {posargs} [testenv:unit] description = Run unit tests -deps = - pytest - coverage[toml] - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + unit commands = - coverage run --source={[vars]src_path} \ - -m pytest \ - --tb native \ - -v \ - -s \ - {posargs} \ - {[vars]tests_path}/unit + coverage run --source={[vars]src_path} -m pytest \ + -v \ + -s \ + --tb native \ + {[vars]tests_path}/unit \ + {posargs} coverage report -[testenv:static] -description = Run static type checks -deps = - pyright - -r {tox_root}/requirements.txt -commands = - pyright {posargs} - [testenv:integration] description = Run integration tests -deps = - pytest - juju - pytest-operator - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + integration +pass_env = + # The integration tests don't pack the charm. If CHARM_PATH is set, the tests deploy the + # specified .charm file. Otherwise, the tests look for a .charm file in the project dir. + CHARM_PATH commands = - pytest -v \ - -s \ - --tb native \ - --log-cli-level=INFO \ - {posargs} \ - {[vars]tests_path}/integration + pytest \ + -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tests_path}/integration \ + {posargs} diff --git a/charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 index 85441d1bd..a3c32be91 100644 --- a/charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 +++ b/charmcraft/templates/init-fastapi-framework-26.04/pyproject.toml.j2 @@ -25,6 +25,25 @@ charmlibs-pydeps = [ "cosl==1.9.1", "pydantic==2.13.3", ] +# Dependencies of linting and static type checks +lint = [ + "ruff", + "codespell", + "pyright", +] +# Dependencies of unit tests +unit = [ + "coverage[toml]", + "ops[testing]", + "pytest", +] +# Dependencies of integration tests +integration = [ + "jubilant>=1.8,<2", + "pytest", + "pytest-jubilant>=2.0.1,<3", + "PyYAML", +] # Testing tools configuration diff --git a/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 index bd15d819c..e1e05a12b 100644 --- a/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 @@ -31,50 +31,46 @@ commands = ruff check --fix {[vars]all_path} [testenv:lint] -description = Check code against coding style standards -deps = - ruff - codespell +description = Check code against coding style standards, and static checks +runner = uv-venv-lock-runner +dependency_groups = + lint + unit + integration commands = codespell {tox_root} ruff check {[vars]all_path} ruff format --check --diff {[vars]all_path} + pyright {posargs} [testenv:unit] description = Run unit tests -deps = - pytest - coverage[toml] - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + unit commands = - coverage run --source={[vars]src_path} \ - -m pytest \ - --tb native \ - -v \ - -s \ - {posargs} \ - {[vars]tests_path}/unit + coverage run --source={[vars]src_path} -m pytest \ + -v \ + -s \ + --tb native \ + {[vars]tests_path}/unit \ + {posargs} coverage report -[testenv:static] -description = Run static type checks -deps = - pyright - -r {tox_root}/requirements.txt -commands = - pyright {posargs} - [testenv:integration] description = Run integration tests -deps = - pytest - juju - pytest-operator - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + integration +pass_env = + # The integration tests don't pack the charm. If CHARM_PATH is set, the tests deploy the + # specified .charm file. Otherwise, the tests look for a .charm file in the project dir. + CHARM_PATH commands = - pytest -v \ - -s \ - --tb native \ - --log-cli-level=INFO \ - {posargs} \ - {[vars]tests_path}/integration + pytest \ + -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tests_path}/integration \ + {posargs} diff --git a/charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 index 85441d1bd..a3c32be91 100644 --- a/charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 +++ b/charmcraft/templates/init-flask-framework-26.04/pyproject.toml.j2 @@ -25,6 +25,25 @@ charmlibs-pydeps = [ "cosl==1.9.1", "pydantic==2.13.3", ] +# Dependencies of linting and static type checks +lint = [ + "ruff", + "codespell", + "pyright", +] +# Dependencies of unit tests +unit = [ + "coverage[toml]", + "ops[testing]", + "pytest", +] +# Dependencies of integration tests +integration = [ + "jubilant>=1.8,<2", + "pytest", + "pytest-jubilant>=2.0.1,<3", + "PyYAML", +] # Testing tools configuration diff --git a/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 index bd15d819c..e1e05a12b 100644 --- a/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 @@ -31,50 +31,46 @@ commands = ruff check --fix {[vars]all_path} [testenv:lint] -description = Check code against coding style standards -deps = - ruff - codespell +description = Check code against coding style standards, and static checks +runner = uv-venv-lock-runner +dependency_groups = + lint + unit + integration commands = codespell {tox_root} ruff check {[vars]all_path} ruff format --check --diff {[vars]all_path} + pyright {posargs} [testenv:unit] description = Run unit tests -deps = - pytest - coverage[toml] - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + unit commands = - coverage run --source={[vars]src_path} \ - -m pytest \ - --tb native \ - -v \ - -s \ - {posargs} \ - {[vars]tests_path}/unit + coverage run --source={[vars]src_path} -m pytest \ + -v \ + -s \ + --tb native \ + {[vars]tests_path}/unit \ + {posargs} coverage report -[testenv:static] -description = Run static type checks -deps = - pyright - -r {tox_root}/requirements.txt -commands = - pyright {posargs} - [testenv:integration] description = Run integration tests -deps = - pytest - juju - pytest-operator - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + integration +pass_env = + # The integration tests don't pack the charm. If CHARM_PATH is set, the tests deploy the + # specified .charm file. Otherwise, the tests look for a .charm file in the project dir. + CHARM_PATH commands = - pytest -v \ - -s \ - --tb native \ - --log-cli-level=INFO \ - {posargs} \ - {[vars]tests_path}/integration + pytest \ + -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tests_path}/integration \ + {posargs} diff --git a/charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 index 85441d1bd..a3c32be91 100644 --- a/charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 +++ b/charmcraft/templates/init-go-framework-26.04/pyproject.toml.j2 @@ -25,6 +25,25 @@ charmlibs-pydeps = [ "cosl==1.9.1", "pydantic==2.13.3", ] +# Dependencies of linting and static type checks +lint = [ + "ruff", + "codespell", + "pyright", +] +# Dependencies of unit tests +unit = [ + "coverage[toml]", + "ops[testing]", + "pytest", +] +# Dependencies of integration tests +integration = [ + "jubilant>=1.8,<2", + "pytest", + "pytest-jubilant>=2.0.1,<3", + "PyYAML", +] # Testing tools configuration diff --git a/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 index bd15d819c..e1e05a12b 100644 --- a/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 @@ -31,50 +31,46 @@ commands = ruff check --fix {[vars]all_path} [testenv:lint] -description = Check code against coding style standards -deps = - ruff - codespell +description = Check code against coding style standards, and static checks +runner = uv-venv-lock-runner +dependency_groups = + lint + unit + integration commands = codespell {tox_root} ruff check {[vars]all_path} ruff format --check --diff {[vars]all_path} + pyright {posargs} [testenv:unit] description = Run unit tests -deps = - pytest - coverage[toml] - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + unit commands = - coverage run --source={[vars]src_path} \ - -m pytest \ - --tb native \ - -v \ - -s \ - {posargs} \ - {[vars]tests_path}/unit + coverage run --source={[vars]src_path} -m pytest \ + -v \ + -s \ + --tb native \ + {[vars]tests_path}/unit \ + {posargs} coverage report -[testenv:static] -description = Run static type checks -deps = - pyright - -r {tox_root}/requirements.txt -commands = - pyright {posargs} - [testenv:integration] description = Run integration tests -deps = - pytest - juju - pytest-operator - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + integration +pass_env = + # The integration tests don't pack the charm. If CHARM_PATH is set, the tests deploy the + # specified .charm file. Otherwise, the tests look for a .charm file in the project dir. + CHARM_PATH commands = - pytest -v \ - -s \ - --tb native \ - --log-cli-level=INFO \ - {posargs} \ - {[vars]tests_path}/integration + pytest \ + -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tests_path}/integration \ + {posargs} diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 index 46991a3ea..96b231689 100644 --- a/charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 +++ b/charmcraft/templates/init-spring-boot-framework-26.04/pyproject.toml.j2 @@ -25,6 +25,25 @@ charmlibs-pydeps = [ "cosl==1.9.1", "pydantic==2.13.3", ] +# Dependencies of linting and static type checks +lint = [ + "ruff", + "codespell", + "pyright", +] +# Dependencies of unit tests +unit = [ + "coverage[toml]", + "ops[testing]", + "pytest", +] +# Dependencies of integration tests +integration = [ + "jubilant>=1.8,<2", + "pytest", + "pytest-jubilant>=2.0.1,<3", + "PyYAML", +] # Testing tools configuration diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 index bd15d819c..e1e05a12b 100644 --- a/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 @@ -31,50 +31,46 @@ commands = ruff check --fix {[vars]all_path} [testenv:lint] -description = Check code against coding style standards -deps = - ruff - codespell +description = Check code against coding style standards, and static checks +runner = uv-venv-lock-runner +dependency_groups = + lint + unit + integration commands = codespell {tox_root} ruff check {[vars]all_path} ruff format --check --diff {[vars]all_path} + pyright {posargs} [testenv:unit] description = Run unit tests -deps = - pytest - coverage[toml] - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + unit commands = - coverage run --source={[vars]src_path} \ - -m pytest \ - --tb native \ - -v \ - -s \ - {posargs} \ - {[vars]tests_path}/unit + coverage run --source={[vars]src_path} -m pytest \ + -v \ + -s \ + --tb native \ + {[vars]tests_path}/unit \ + {posargs} coverage report -[testenv:static] -description = Run static type checks -deps = - pyright - -r {tox_root}/requirements.txt -commands = - pyright {posargs} - [testenv:integration] description = Run integration tests -deps = - pytest - juju - pytest-operator - -r {tox_root}/requirements.txt +runner = uv-venv-lock-runner +dependency_groups = + integration +pass_env = + # The integration tests don't pack the charm. If CHARM_PATH is set, the tests deploy the + # specified .charm file. Otherwise, the tests look for a .charm file in the project dir. + CHARM_PATH commands = - pytest -v \ - -s \ - --tb native \ - --log-cli-level=INFO \ - {posargs} \ - {[vars]tests_path}/integration + pytest \ + -v \ + -s \ + --tb native \ + --log-cli-level=INFO \ + {[vars]tests_path}/integration \ + {posargs} From 5a3acee8b6b9b58a8c93d7add549f8c327b6efe0 Mon Sep 17 00:00:00 2001 From: David Wilding Date: Fri, 26 Jun 2026 18:29:31 +0800 Subject: [PATCH 17/26] fix(templates): organize imports in new templates --- charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 | 1 - .../templates/init-expressjs-framework-26.04/src/charm.py.j2 | 1 - .../templates/init-fastapi-framework-26.04/src/charm.py.j2 | 1 - charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 | 1 - charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 | 1 - .../templates/init-spring-boot-framework-26.04/src/charm.py.j2 | 1 - 6 files changed, 6 deletions(-) diff --git a/charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 index 359b47307..4a994d611 100755 --- a/charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 +++ b/charmcraft/templates/init-django-framework-26.04/src/charm.py.j2 @@ -8,7 +8,6 @@ import logging import typing import ops - import paas_charm.django logger = logging.getLogger(__name__) diff --git a/charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 index 798b68376..c64fe9ee8 100755 --- a/charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 +++ b/charmcraft/templates/init-expressjs-framework-26.04/src/charm.py.j2 @@ -8,7 +8,6 @@ import logging import typing import ops - import paas_charm.expressjs logger = logging.getLogger(__name__) diff --git a/charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 index 84f9fa77a..59042cc4a 100755 --- a/charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 +++ b/charmcraft/templates/init-fastapi-framework-26.04/src/charm.py.j2 @@ -8,7 +8,6 @@ import logging import typing import ops - import paas_charm.fastapi logger = logging.getLogger(__name__) diff --git a/charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 index 94cb3f33f..76f7e7d62 100755 --- a/charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 +++ b/charmcraft/templates/init-flask-framework-26.04/src/charm.py.j2 @@ -8,7 +8,6 @@ import logging import typing import ops - import paas_charm.flask logger = logging.getLogger(__name__) diff --git a/charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 index c32223b7e..969734515 100755 --- a/charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 +++ b/charmcraft/templates/init-go-framework-26.04/src/charm.py.j2 @@ -8,7 +8,6 @@ import logging import typing import ops - import paas_charm.go logger = logging.getLogger(__name__) diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 index cb3178463..a90bbe2de 100755 --- a/charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 +++ b/charmcraft/templates/init-spring-boot-framework-26.04/src/charm.py.j2 @@ -8,7 +8,6 @@ import logging import typing import ops - import paas_charm.springboot logger = logging.getLogger(__name__) From a7236768c8d7dd5a3ce3bcf306275ed2d89bf93c Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Tue, 30 Jun 2026 16:55:45 +0300 Subject: [PATCH 18/26] fix(tox): enable tests path in tox.ini templates for various frameworks --- charmcraft/templates/init-django-framework-26.04/tox.ini.j2 | 2 +- charmcraft/templates/init-django-framework/tox.ini.j2 | 2 +- charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 | 2 +- charmcraft/templates/init-expressjs-framework/tox.ini.j2 | 2 +- charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 | 2 +- charmcraft/templates/init-fastapi-framework/tox.ini.j2 | 2 +- charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 | 2 +- charmcraft/templates/init-flask-framework/tox.ini.j2 | 2 +- charmcraft/templates/init-go-framework-26.04/tox.ini.j2 | 2 +- charmcraft/templates/init-go-framework/tox.ini.j2 | 2 +- .../templates/init-spring-boot-framework-26.04/tox.ini.j2 | 2 +- charmcraft/templates/init-spring-boot-framework/tox.ini.j2 | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 index e1e05a12b..17bbc92fb 100644 --- a/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-django-framework-26.04/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-django-framework/tox.ini.j2 b/charmcraft/templates/init-django-framework/tox.ini.j2 index bd15d819c..bacc920a4 100644 --- a/charmcraft/templates/init-django-framework/tox.ini.j2 +++ b/charmcraft/templates/init-django-framework/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 index e1e05a12b..17bbc92fb 100644 --- a/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-expressjs-framework-26.04/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-expressjs-framework/tox.ini.j2 b/charmcraft/templates/init-expressjs-framework/tox.ini.j2 index bd15d819c..bacc920a4 100644 --- a/charmcraft/templates/init-expressjs-framework/tox.ini.j2 +++ b/charmcraft/templates/init-expressjs-framework/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 index e1e05a12b..17bbc92fb 100644 --- a/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-fastapi-framework-26.04/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-fastapi-framework/tox.ini.j2 b/charmcraft/templates/init-fastapi-framework/tox.ini.j2 index bd15d819c..bacc920a4 100644 --- a/charmcraft/templates/init-fastapi-framework/tox.ini.j2 +++ b/charmcraft/templates/init-fastapi-framework/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 index e1e05a12b..17bbc92fb 100644 --- a/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-flask-framework-26.04/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-flask-framework/tox.ini.j2 b/charmcraft/templates/init-flask-framework/tox.ini.j2 index bd15d819c..bacc920a4 100644 --- a/charmcraft/templates/init-flask-framework/tox.ini.j2 +++ b/charmcraft/templates/init-flask-framework/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 index e1e05a12b..17bbc92fb 100644 --- a/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-go-framework-26.04/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-go-framework/tox.ini.j2 b/charmcraft/templates/init-go-framework/tox.ini.j2 index bd15d819c..bacc920a4 100644 --- a/charmcraft/templates/init-go-framework/tox.ini.j2 +++ b/charmcraft/templates/init-go-framework/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 b/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 index e1e05a12b..17bbc92fb 100644 --- a/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 +++ b/charmcraft/templates/init-spring-boot-framework-26.04/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] diff --git a/charmcraft/templates/init-spring-boot-framework/tox.ini.j2 b/charmcraft/templates/init-spring-boot-framework/tox.ini.j2 index bd15d819c..bacc920a4 100644 --- a/charmcraft/templates/init-spring-boot-framework/tox.ini.j2 +++ b/charmcraft/templates/init-spring-boot-framework/tox.ini.j2 @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests all_path = {[vars]src_path} [testenv] From 0b76ad822d3c273c8a25838dac27290028cfbb66 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 6 Jul 2026 08:30:54 +0300 Subject: [PATCH 19/26] fix(docs): standardize tab indentation for Kubernetes log commands in use-web-app-charm.rst --- .../use-web-app-charm.rst | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/howto/manage-web-app-charms/use-web-app-charm.rst b/docs/howto/manage-web-app-charms/use-web-app-charm.rst index ad56431bb..a36cf4dfd 100644 --- a/docs/howto/manage-web-app-charms/use-web-app-charm.rst +++ b/docs/howto/manage-web-app-charms/use-web-app-charm.rst @@ -514,116 +514,116 @@ name of the web app with the ``-c`` option. .. tab-item:: Django :sync: django - .. tab-set:: + .. tab-set:: - .. tab-item:: MicroK8s - :sync: microk8s + .. tab-item:: MicroK8s + :sync: microk8s - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c django-app + microk8s kubectl logs -n -c django-app - .. tab-item:: Canonical K8s - :sync: canonical-k8s + .. tab-item:: Canonical K8s + :sync: canonical-k8s - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c django-app + sudo k8s kubectl logs -n -c django-app .. tab-item:: Express :sync: express - .. tab-set:: + .. tab-set:: - .. tab-item:: MicroK8s - :sync: microk8s + .. tab-item:: MicroK8s + :sync: microk8s - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app - .. tab-item:: Canonical K8s - :sync: canonical-k8s + .. tab-item:: Canonical K8s + :sync: canonical-k8s - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: FastAPI :sync: fastapi - .. tab-set:: + .. tab-set:: - .. tab-item:: MicroK8s - :sync: microk8s + .. tab-item:: MicroK8s + :sync: microk8s - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app - .. tab-item:: Canonical K8s - :sync: canonical-k8s + .. tab-item:: Canonical K8s + :sync: canonical-k8s - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Flask :sync: flask - .. tab-set:: + .. tab-set:: - .. tab-item:: MicroK8s - :sync: microk8s + .. tab-item:: MicroK8s + :sync: microk8s - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c flask-app + microk8s kubectl logs -n -c flask-app - .. tab-item:: Canonical K8s - :sync: canonical-k8s + .. tab-item:: Canonical K8s + :sync: canonical-k8s - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c flask-app + sudo k8s kubectl logs -n -c flask-app .. tab-item:: Go :sync: go - .. tab-set:: + .. tab-set:: - .. tab-item:: MicroK8s - :sync: microk8s + .. tab-item:: MicroK8s + :sync: microk8s - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app - .. tab-item:: Canonical K8s - :sync: canonical-k8s + .. tab-item:: Canonical K8s + :sync: canonical-k8s - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. tab-item:: Spring Boot :sync: spring-boot - .. tab-set:: + .. tab-set:: - .. tab-item:: MicroK8s - :sync: microk8s + .. tab-item:: MicroK8s + :sync: microk8s - .. code-block:: bash + .. code-block:: bash - microk8s kubectl logs -n -c app + microk8s kubectl logs -n -c app - .. tab-item:: Canonical K8s - :sync: canonical-k8s + .. tab-item:: Canonical K8s + :sync: canonical-k8s - .. code-block:: bash + .. code-block:: bash - sudo k8s kubectl logs -n -c app + sudo k8s kubectl logs -n -c app .. seealso:: From 982f3657076667421e6a83b7851352981e72c691 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Mon, 6 Jul 2026 08:52:36 +0300 Subject: [PATCH 20/26] fix(docs): correct indentation and formatting in manage-extensions.rst --- docs/howto/manage-extensions.rst | 94 ++++++++++++++++---------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst index b3460b4f3..99ef5e03c 100644 --- a/docs/howto/manage-extensions.rst +++ b/docs/howto/manage-extensions.rst @@ -76,11 +76,11 @@ extension. type: charm bases: - - build-on: + - build-on: - name: ubuntu - channel: "22.04" + channel: "22.04" run-on: - - name: ubuntu + - name: ubuntu channel: "22.04" # (Required) @@ -146,11 +146,11 @@ extension. type: charm bases: - - build-on: + - build-on: - name: ubuntu - channel: "22.04" + channel: "22.04" run-on: - - name: ubuntu + - name: ubuntu channel: "22.04" # (Required) @@ -172,7 +172,7 @@ extension. # interface: postgresql_client # limit: 1 - To activate V2 version you need to update the base to 26.04. + To activate V2, set ``base`` to ``ubuntu@26.04``. .. code-block:: yaml @@ -205,98 +205,98 @@ then run ``charmcraft expand-extensions``. For example: description: | A comprehensive overview of your Flask application. parts: - charm: + charm: source: . charm-entrypoint: src/charm.py charm-binary-python-packages: [] charm-python-packages: [] charm-requirements: - - requirements.txt + - requirements.txt charm-strict-dependencies: false plugin: charm type: charm bases: - build-on: - - name: ubuntu + - name: ubuntu channel: '22.04' - run-on: + run-on: - name: ubuntu - channel: '22.04' + channel: '22.04' actions: - rotate-secret-key: - description: Rotate the flask secret key. Users will be forced to log in again. - This might be useful if a security breach occurs. + rotate-secret-key: + description: Rotate the flask secret key. Users will be forced to log in again. + This might be useful if a security breach occurs. assumes: - - k8s-api + - k8s-api containers: - flask-app: + flask-app: resource: flask-app-image peers: - secret-storage: + secret-storage: interface: secret-storage provides: - metrics-endpoint: + metrics-endpoint: interface: prometheus_scrape - grafana-dashboard: + grafana-dashboard: interface: grafana_dashboard requires: - logging: + logging: interface: loki_push_api - ingress: + ingress: interface: ingress limit: 1 resources: - flask-app-image: + flask-app-image: type: oci-image description: flask application image. config: - options: + options: webserver-keepalive: - type: int - description: Time in seconds for webserver to wait for requests on a Keep-Alive + type: int + description: Time in seconds for webserver to wait for requests on a Keep-Alive connection. webserver-threads: - type: int - description: Run each webserver worker with the specified number of threads. + type: int + description: Run each webserver worker with the specified number of threads. webserver-timeout: - type: int - description: Time in seconds to kill and restart silent webserver workers. + type: int + description: Time in seconds to kill and restart silent webserver workers. webserver-workers: - type: int - description: The number of webserver worker processes for handling requests. + type: int + description: The number of webserver worker processes for handling requests. flask-application-root: - type: string - description: Path in which the application / web server is mounted. This configuration + type: string + description: Path in which the application / web server is mounted. This configuration will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() in your Flask application in order to receive this configuration. flask-debug: - type: boolean - description: Whether Flask debug mode is enabled. + type: boolean + description: Whether Flask debug mode is enabled. flask-env: - type: string - description: What environment the Flask app is running in, by default it's 'production'. + type: string + description: What environment the Flask app is running in, by default it's 'production'. flask-permanent-session-lifetime: - type: int - description: Time in seconds for the cookie to expire in the Flask application + type: int + description: Time in seconds for the cookie to expire in the Flask application permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME environment variable. Run app.config.from_prefixed_env() in your Flask application in order to receive this configuration. flask-preferred-url-scheme: - type: string - default: HTTPS - description: Scheme for generating external URLs when not in a request context + type: string + default: HTTPS + description: Scheme for generating external URLs when not in a request context in the Flask application. By default, it's "HTTPS". This configuration will set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() in your Flask application in order to receive this configuration. flask-secret-key: - type: string - description: The secret key used for securely signing the session cookie and + type: string + description: The secret key used for securely signing the session cookie and for any other security related needs by your Flask application. This configuration will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() in your Flask application in order to receive this configuration. flask-session-cookie-secure: - type: boolean - description: Set the secure attribute in the Flask application cookies. This + type: boolean + description: Set the secure attribute in the Flask application cookies. This configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. Run app.config.from_prefixed_env() in your Flask application in order to receive this configuration. From 5ad567114512d8890ebc6364372936aa19cf2153 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Wed, 8 Jul 2026 07:22:54 +0300 Subject: [PATCH 21/26] fix(docs): update paas-charm library version information and standardize container names in logs --- docs/howto/manage-extensions.rst | 13 ++++++++++++ .../use-web-app-charm.rst | 21 +++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst index 99ef5e03c..a75d04b6f 100644 --- a/docs/howto/manage-extensions.rst +++ b/docs/howto/manage-extensions.rst @@ -3,6 +3,19 @@ Manage extensions ================= +.. important:: + + There are two versions of the ``paas-charm`` library used to build 12-factor + app charms: + + - **V1** supports Ubuntu 22.04 (Jammy) and 24.04 (Noble) bases. + - **V2** supports Ubuntu 26.04 (Questing) bases and introduces improvements + that align configuration and behaviour across all supported frameworks. + + If you are targeting Ubuntu 26.04, you must use the V2 charm templates (the + ``-26.04`` variants). The guides on this page apply to both versions unless + noted otherwise. + This guide shows how to view available :ref:`extensions ` and view details about extensions in use. diff --git a/docs/howto/manage-web-app-charms/use-web-app-charm.rst b/docs/howto/manage-web-app-charms/use-web-app-charm.rst index a36cf4dfd..b75736fb6 100644 --- a/docs/howto/manage-web-app-charms/use-web-app-charm.rst +++ b/docs/howto/manage-web-app-charms/use-web-app-charm.rst @@ -3,6 +3,19 @@ Use a 12-factor app charm ========================= +.. important:: + + There are two versions of the ``paas-charm`` library used to build 12-factor + app charms: + + - **V1** supports Ubuntu 22.04 (Jammy) and 24.04 (Noble) bases. + - **V2** supports Ubuntu 26.04 (Questing) bases and introduces improvements + that align configuration and behaviour across all supported frameworks. + + If you are targeting Ubuntu 26.04, you must use the V2 charm templates (the + ``-26.04`` variants). The guides on this page apply to both versions unless + noted otherwise. + .. _use-12-factor-charms-admin-user-django: Create an admin user for a Django app charm @@ -521,14 +534,14 @@ name of the web app with the ``-c`` option. .. code-block:: bash - microk8s kubectl logs -n -c django-app + microk8s kubectl logs -n -c app .. tab-item:: Canonical K8s :sync: canonical-k8s .. code-block:: bash - sudo k8s kubectl logs -n -c django-app + sudo k8s kubectl logs -n -c app .. tab-item:: Express :sync: express @@ -578,14 +591,14 @@ name of the web app with the ``-c`` option. .. code-block:: bash - microk8s kubectl logs -n -c flask-app + microk8s kubectl logs -n -c app .. tab-item:: Canonical K8s :sync: canonical-k8s .. code-block:: bash - sudo k8s kubectl logs -n -c flask-app + sudo k8s kubectl logs -n -c app .. tab-item:: Go :sync: go From 37a906ea6996f122a784178ec148b8c350e1e959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20U=C4=9EUR?= <39213991+alithethird@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:39:09 +0300 Subject: [PATCH 22/26] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Erin Conley Signed-off-by: Ali UĞUR <39213991+alithethird@users.noreply.github.com> --- docs/howto/manage-extensions.rst | 6 +++--- docs/howto/manage-web-app-charms/use-web-app-charm.rst | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst index a75d04b6f..46a806e16 100644 --- a/docs/howto/manage-extensions.rst +++ b/docs/howto/manage-extensions.rst @@ -8,11 +8,11 @@ Manage extensions There are two versions of the ``paas-charm`` library used to build 12-factor app charms: - - **V1** supports Ubuntu 22.04 (Jammy) and 24.04 (Noble) bases. - - **V2** supports Ubuntu 26.04 (Questing) bases and introduces improvements + - **V1** supports Ubuntu 22.04 LTS (Jammy) and Ubuntu 24.04 LTS (Noble) bases. + - **V2** supports Ubuntu 26.04 LTS (Resolute) bases and introduces improvements that align configuration and behaviour across all supported frameworks. - If you are targeting Ubuntu 26.04, you must use the V2 charm templates (the + If you are targeting Ubuntu 26.04 LTS, you must use the V2 charm templates (the ``-26.04`` variants). The guides on this page apply to both versions unless noted otherwise. diff --git a/docs/howto/manage-web-app-charms/use-web-app-charm.rst b/docs/howto/manage-web-app-charms/use-web-app-charm.rst index b75736fb6..dd3849ac8 100644 --- a/docs/howto/manage-web-app-charms/use-web-app-charm.rst +++ b/docs/howto/manage-web-app-charms/use-web-app-charm.rst @@ -8,11 +8,11 @@ Use a 12-factor app charm There are two versions of the ``paas-charm`` library used to build 12-factor app charms: - - **V1** supports Ubuntu 22.04 (Jammy) and 24.04 (Noble) bases. - - **V2** supports Ubuntu 26.04 (Questing) bases and introduces improvements + - **V1** supports Ubuntu 22.04 LTS (Jammy) and Ubuntu 24.04 LTS (Noble) bases. + - **V2** supports Ubuntu 26.04 LTS (Resolute) bases and introduces improvements that align configuration and behaviour across all supported frameworks. - If you are targeting Ubuntu 26.04, you must use the V2 charm templates (the + If you are targeting Ubuntu 26.04 LTS, you must use the V2 charm templates (the ``-26.04`` variants). The guides on this page apply to both versions unless noted otherwise. From 584a0cc15b910efcd575b660e436e47dc21f6506 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Wed, 15 Jul 2026 12:06:25 +0300 Subject: [PATCH 23/26] docs: update extension management guidance for Ubuntu versions --- docs/howto/manage-extensions.rst | 213 ++++++------------ .../use-web-app-charm.rst | 50 ++-- 2 files changed, 94 insertions(+), 169 deletions(-) diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst index 46a806e16..d225e05bb 100644 --- a/docs/howto/manage-extensions.rst +++ b/docs/howto/manage-extensions.rst @@ -1,3 +1,6 @@ +.. meta:: + :description: How to view and manage extensions. + .. _manage-extensions: Manage extensions @@ -5,16 +8,15 @@ Manage extensions .. important:: - There are two versions of the ``paas-charm`` library used to build 12-factor - app charms: + Extension behaviour differs between Ubuntu base versions. - - **V1** supports Ubuntu 22.04 LTS (Jammy) and Ubuntu 24.04 LTS (Noble) bases. - - **V2** supports Ubuntu 26.04 LTS (Resolute) bases and introduces improvements - that align configuration and behaviour across all supported frameworks. + - Ubuntu 22.04 LTS (Jammy) and Ubuntu 24.04 LTS (Noble) use the original + ``paas-charm`` templates. + - Ubuntu 26.04 LTS (Resolute) and higher use the ``-26.04`` templates, with + updated configuration and behaviour. - If you are targeting Ubuntu 26.04 LTS, you must use the V2 charm templates (the - ``-26.04`` variants). The guides on this page apply to both versions unless - noted otherwise. + If you are targeting Ubuntu 26.04 LTS or higher, use the ``-26.04`` templates. + The guides on this page apply to both base groups unless noted otherwise. This guide shows how to view available :ref:`extensions ` and view details about extensions in use. @@ -30,9 +32,15 @@ For example: .. code-block:: bash $ charmcraft list-extensions - Extension name Supported bases Experimental bases - ---------------- ----------------- -------------------- - flask-framework ubuntu@22.04 ubuntu@26.04 + Extension name Supported bases Experimental bases + --------------------- -------------------------- -------------------- + django-framework ubuntu@22.04, ubuntu@24.04 ubuntu@26.04 + expressjs-framework ubuntu@24.04 ubuntu@26.04 + fastapi-framework ubuntu@24.04 ubuntu@26.04 + flask-framework ubuntu@22.04, ubuntu@24.04 ubuntu@26.04 + go-framework ubuntu@24.04 ubuntu@26.04 + spring-boot-framework ubuntu@24.04 ubuntu@26.04 + View details about the extension in use --------------------------------------- @@ -44,156 +52,71 @@ extension. .. dropdown:: Example - .. tab-set:: - - .. tab-item:: V1 - :sync: v1 - - .. code-block:: bash - - mkdir my-flask-app-k8s - cd my-flask-app-k8s/ - charmcraft init --profile flask-framework - - .. terminal:: - - Charmed operator package file and directory tree initialised. - - Now edit the following package files to provide fundamental charm metadata - and other information: - - charmcraft.yaml - src/charm.py - README.md - - .. code-block:: bash - - ls -R - - .. terminal:: - - .: - charmcraft.yaml requirements.txt src - - ./src: - charm.py - - .. code-block:: bash - - cat charmcraft.yaml - - .. code-block:: yaml - - name: my-flask-app-k8s - - type: charm - - bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" - - # (Required) - summary: A very short one-line summary of the flask application. - - # (Required) - description: | - A comprehensive overview of your Flask application. - - extensions: - - flask-framework - - # Uncomment the integrations used by your application - # requires: - # mysql: - # interface: mysql_client - # limit: 1 - # postgresql: - # interface: postgresql_client - # limit: 1 - - .. tab-item:: V2 - :sync: v2 - - - .. code-block:: bash - - mkdir my-flask-app-k8s - cd my-flask-app-k8s/ - charmcraft init --profile flask-framework - - .. terminal:: - - Charmed operator package file and directory tree initialised. + .. code-block:: bash - Now edit the following package files to provide fundamental charm metadata - and other information: + mkdir my-flask-app-k8s + cd my-flask-app-k8s/ + charmcraft init --profile flask-framework - charmcraft.yaml - src/charm.py - README.md + .. terminal:: - .. code-block:: bash + Charmed operator package file and directory tree initialised. - ls -R + Now edit the following package files to provide fundamental charm metadata + and other information: - .. terminal:: + charmcraft.yaml + src/charm.py + README.md - .: - charmcraft.yaml requirements.txt src + .. code-block:: bash - ./src: - charm.py + ls -R - .. code-block:: bash + .. terminal:: - cat charmcraft.yaml + .: + charmcraft.yaml requirements.txt src - .. code-block:: yaml + ./src: + charm.py - name: my-flask-app-k8s + .. code-block:: bash - type: charm + cat charmcraft.yaml - bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" + .. code-block:: yaml - # (Required) - summary: A very short one-line summary of the flask application. + name: my-flask-app-k8s - # (Required) - description: | - A comprehensive overview of your Flask application. + type: charm - extensions: - - flask-framework + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" - # Uncomment the integrations used by your application - # requires: - # mysql: - # interface: mysql_client - # limit: 1 - # postgresql: - # interface: postgresql_client - # limit: 1 + # (Required) + summary: A very short one-line summary of the flask application. - To activate V2, set ``base`` to ``ubuntu@26.04``. + # (Required) + description: | + A comprehensive overview of your Flask application. - .. code-block:: yaml - - name: my-flask-app-k8s + extensions: + - flask-framework - type: charm - - base: ubuntu@26.04 + # Uncomment the integrations used by your application + # requires: + # mysql: + # interface: mysql_client + # limit: 1 + # postgresql: + # interface: postgresql_client + # limit: 1 To view details about what that extension is adding to your charm, set the ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` environment variable to ``1``, @@ -203,8 +126,8 @@ then run ``charmcraft expand-extensions``. For example: .. tab-set:: - .. tab-item:: V1 - :sync: v1 + .. tab-item:: Ubuntu 22.04 and 24.04 + :sync: base-22-24 .. code-block:: bash @@ -314,8 +237,8 @@ then run ``charmcraft expand-extensions``. For example: Run app.config.from_prefixed_env() in your Flask application in order to receive this configuration. - .. tab-item:: V2 - :sync: v2 + .. tab-item:: Ubuntu 26.04 and higher + :sync: base-26-plus .. code-block:: bash diff --git a/docs/howto/manage-web-app-charms/use-web-app-charm.rst b/docs/howto/manage-web-app-charms/use-web-app-charm.rst index dd3849ac8..89dd0897e 100644 --- a/docs/howto/manage-web-app-charms/use-web-app-charm.rst +++ b/docs/howto/manage-web-app-charms/use-web-app-charm.rst @@ -1,3 +1,6 @@ +.. meta:: + :description: How to use a 12-factor app charm, including troubleshooting, observability, and TLS. + .. _use-12-factor-charms: Use a 12-factor app charm @@ -5,16 +8,15 @@ Use a 12-factor app charm .. important:: - There are two versions of the ``paas-charm`` library used to build 12-factor - app charms: + Extension behaviour differs between Ubuntu base versions. - - **V1** supports Ubuntu 22.04 LTS (Jammy) and Ubuntu 24.04 LTS (Noble) bases. - - **V2** supports Ubuntu 26.04 LTS (Resolute) bases and introduces improvements - that align configuration and behaviour across all supported frameworks. + - Ubuntu 22.04 LTS (Jammy) and Ubuntu 24.04 LTS (Noble) use the original + ``paas-charm`` templates. + - Ubuntu 26.04 LTS (Resolute) and higher use the ``-26.04`` templates, with + updated configuration and behaviour. - If you are targeting Ubuntu 26.04 LTS, you must use the V2 charm templates (the - ``-26.04`` variants). The guides on this page apply to both versions unless - noted otherwise. + If you are targeting Ubuntu 26.04 LTS or higher, use the ``-26.04`` templates. + The guides on this page apply to both base groups unless noted otherwise. .. _use-12-factor-charms-admin-user-django: @@ -74,8 +76,8 @@ To view the Pebble logs for a deployed web app, run: .. tab-set:: - .. tab-item:: V1 - :sync: v1 + .. tab-item:: Ubuntu 22.04 and 24.04 + :sync: base-22-24 .. tab-set:: @@ -122,8 +124,8 @@ To view the Pebble logs for a deployed web app, run: juju ssh /0 \ PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble logs - .. tab-item:: V2 - :sync: v2 + .. tab-item:: Ubuntu 26.04 and higher + :sync: base-26-plus .. tab-set:: @@ -182,8 +184,8 @@ To view more details about the web app itself, run: .. tab-set:: - .. tab-item:: V1 - :sync: v1 + .. tab-item:: Ubuntu 22.04 and 24.04 + :sync: base-22-24 .. tab-set:: @@ -230,8 +232,8 @@ To view more details about the web app itself, run: juju ssh /0 \ PEBBLE_SOCKET=/charm/containers/app/pebble.socket /charm/bin/pebble plan - .. tab-item:: V2 - :sync: v2 + .. tab-item:: Ubuntu 26.04 and higher + :sync: base-26-plus .. tab-set:: @@ -295,8 +297,8 @@ Juju container: .. tab-set:: - .. tab-item:: V1 - :sync: v1 + .. tab-item:: Ubuntu 22.04 and 24.04 + :sync: base-22-24 .. tab-set:: @@ -350,8 +352,8 @@ Juju container: /charm/bin/pebble exec --context=spring-boot -- bash - .. tab-item:: V2 - :sync: v2 + .. tab-item:: Ubuntu 26.04 and higher + :sync: base-26-plus .. tab-set:: @@ -472,8 +474,8 @@ name of the web app with the ``-c`` option. .. tab-set:: - .. tab-item:: V1 - :sync: v1 + .. tab-item:: Ubuntu 22.04 and 24.04 + :sync: base-22-24 .. tab-set:: @@ -519,8 +521,8 @@ name of the web app with the ``-c`` option. microk8s kubectl logs -n -c app - .. tab-item:: V2 - :sync: v2 + .. tab-item:: Ubuntu 26.04 and higher + :sync: base-26-plus .. tab-set:: From 54f95338b919ceea49d7ea01570435c60ac4dcf5 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Thu, 16 Jul 2026 09:36:06 +0300 Subject: [PATCH 24/26] docs: remove outdated extension behavior notes for Ubuntu versions --- docs/howto/manage-extensions.rst | 384 +++++++++---------------------- 1 file changed, 106 insertions(+), 278 deletions(-) diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst index d225e05bb..a2854d514 100644 --- a/docs/howto/manage-extensions.rst +++ b/docs/howto/manage-extensions.rst @@ -6,18 +6,6 @@ Manage extensions ================= -.. important:: - - Extension behaviour differs between Ubuntu base versions. - - - Ubuntu 22.04 LTS (Jammy) and Ubuntu 24.04 LTS (Noble) use the original - ``paas-charm`` templates. - - Ubuntu 26.04 LTS (Resolute) and higher use the ``-26.04`` templates, with - updated configuration and behaviour. - - If you are targeting Ubuntu 26.04 LTS or higher, use the ``-26.04`` templates. - The guides on this page apply to both base groups unless noted otherwise. - This guide shows how to view available :ref:`extensions ` and view details about extensions in use. @@ -124,273 +112,113 @@ then run ``charmcraft expand-extensions``. For example: .. dropdown:: Expanding an extension - .. tab-set:: - - .. tab-item:: Ubuntu 22.04 and 24.04 - :sync: base-22-24 - - .. code-block:: bash - - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions - - .. terminal:: - - *EXPERIMENTAL* extension 'flask-framework' enabled - name: my-flask-app-k8s - summary: A very short one-line summary of the flask application. - description: | - A comprehensive overview of your Flask application. - parts: - charm: - source: . - charm-entrypoint: src/charm.py - charm-binary-python-packages: [] - charm-python-packages: [] - charm-requirements: - - requirements.txt - charm-strict-dependencies: false - plugin: charm - type: charm - bases: - - build-on: - - name: ubuntu - channel: '22.04' - run-on: - - name: ubuntu - channel: '22.04' - actions: - rotate-secret-key: - description: Rotate the flask secret key. Users will be forced to log in again. - This might be useful if a security breach occurs. - assumes: - - k8s-api - containers: - flask-app: - resource: flask-app-image - peers: - secret-storage: - interface: secret-storage - provides: - metrics-endpoint: - interface: prometheus_scrape - grafana-dashboard: - interface: grafana_dashboard - requires: - logging: - interface: loki_push_api - ingress: - interface: ingress - limit: 1 - resources: - flask-app-image: - type: oci-image - description: flask application image. - config: - options: - webserver-keepalive: - type: int - description: Time in seconds for webserver to wait for requests on a Keep-Alive - connection. - webserver-threads: - type: int - description: Run each webserver worker with the specified number of threads. - webserver-timeout: - type: int - description: Time in seconds to kill and restart silent webserver workers. - webserver-workers: - type: int - description: The number of webserver worker processes for handling requests. - flask-application-root: - type: string - description: Path in which the application / web server is mounted. This configuration - will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-debug: - type: boolean - description: Whether Flask debug mode is enabled. - flask-env: - type: string - description: What environment the Flask app is running in, by default it's 'production'. - flask-permanent-session-lifetime: - type: int - description: Time in seconds for the cookie to expire in the Flask application - permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME - environment variable. Run app.config.from_prefixed_env() in your Flask application - in order to receive this configuration. - flask-preferred-url-scheme: - type: string - default: HTTPS - description: Scheme for generating external URLs when not in a request context - in the Flask application. By default, it's "HTTPS". This configuration will - set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-secret-key: - type: string - description: The secret key used for securely signing the session cookie and - for any other security related needs by your Flask application. This configuration - will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-session-cookie-secure: - type: boolean - description: Set the secure attribute in the Flask application cookies. This - configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. - Run app.config.from_prefixed_env() in your Flask application in order to - receive this configuration. - - .. tab-item:: Ubuntu 26.04 and higher - :sync: base-26-plus - - .. code-block:: bash - - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions - - .. terminal:: - - *EXPERIMENTAL* extension 'flask-framework' enabled for base(s): ubuntu@26.04 - name: my-flask-app-k8s - summary: A very short one-line summary of the Flask application. - description: | - A comprehensive overview of your Flask application. - base: ubuntu@26.04 - platforms: - amd64: - build-on: - - amd64 - build-for: - - amd64 - parts: - charm: - plugin: uv - source: . - uv-groups: - - charmlibs-pydeps - build-snaps: - - astral-uv - - rustup - override-build: |- - rustup default stable - craftctl default - type: charm - charm-libs: - - lib: traefik-k8s.ingress - version: '2' - - lib: observability-libs.juju_topology - version: '0' - - lib: grafana-k8s.grafana_dashboard - version: '0' - - lib: loki-k8s.loki_push_api - version: '1' - - lib: data-platform-libs.data_interfaces - version: '0' - - lib: prometheus-k8s.prometheus_scrape - version: '0' - - lib: redis-k8s.redis - version: '0' - - lib: data-platform-libs.s3 - version: '0' - - lib: saml-integrator.saml - version: '0' - - lib: tempo-coordinator-k8s.tracing - version: '0' - - lib: smtp-integrator.smtp - version: '0' - - lib: openfga-k8s.openfga - version: '1' - - lib: hydra.oauth - version: '0' - - lib: squid-forward-proxy.http_proxy - version: '0' - actions: - rotate-secret-key: - description: Rotate the secret key. Users will be forced to log in again. This - might be useful if a security breach occurs. - assumes: - - k8s-api - containers: - app: - resource: app-image - peers: - secret-storage: - interface: secret-storage - provides: - metrics-endpoint: - interface: prometheus_scrape - grafana-dashboard: - interface: grafana_dashboard - requires: - logging: - interface: loki_push_api - ingress: - interface: ingress - limit: 1 - resources: - app-image: - type: oci-image - description: flask application image. - config: - options: - webserver-keepalive: - type: int - description: Time in seconds for webserver to wait for requests on a Keep-Alive - connection. - webserver-threads: - type: int - description: Run each webserver worker with the specified number of threads. - webserver-timeout: - type: int - description: Time in seconds to kill and restart silent webserver workers. - webserver-workers: - type: int - description: The number of webserver worker processes for handling requests. - webserver-worker-class: - type: string - description: The webserver worker process class for handling requests. Can be - either 'gevent' or 'sync'. - flask-application-root: - type: string - description: Path in which the application / web server is mounted. This configuration - will set the FLASK_APPLICATION_ROOT environment variable. Run `app.config.from_prefixed_env()` - in your Flask application in order to receive this configuration. - flask-debug: - type: boolean - description: Whether Flask debug mode is enabled. - flask-env: - type: string - description: What environment the Flask app is running in, by default it's 'production'. - flask-permanent-session-lifetime: - type: int - description: Time in seconds for the cookie to expire in the Flask application - permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME - environment variable. Run `app.config.from_prefixed_env()` in your Flask application - in order to receive this configuration. - flask-preferred-url-scheme: - type: string - default: HTTPS - description: Scheme for generating external URLs when not in a request context - in the Flask application. By default, it's "HTTPS". This configuration will - set the FLASK_PREFERRED_URL_SCHEME environment variable. Run `app.config.from_prefixed_env()` - in your Flask application in order to receive this configuration. - flask-secret-key: - type: string - description: The secret key used for securely signing the session cookie and - for any other security related needs by your Flask application. This configuration - will set the FLASK_SECRET_KEY environment variable. Run `app.config.from_prefixed_env()` - in your Flask application in order to receive this configuration. - flask-secret-key-id: - type: secret - description: 'This configuration is similar to `flask-secret-key`, but instead - accepts a Juju user secret ID. The secret should contain a single key, "value", - which maps to the actual Flask secret key. To create the secret, run the following - command: `juju add-secret my-flask-secret-key value= && juju - grant-secret my-flask-secret-key flask-k8s`, and use the output secret ID - to configure this option.' - flask-session-cookie-secure: - type: boolean - description: Set the secure attribute in the Flask application cookies. This - configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. - Run `app.config.from_prefixed_env()` in your Flask application in order to - receive this configuration. + .. code-block:: bash + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions + + .. terminal:: + + *EXPERIMENTAL* extension 'flask-framework' enabled + name: my-flask-app-k8s + summary: A very short one-line summary of the flask application. + description: | + A comprehensive overview of your Flask application. + parts: + charm: + source: . + charm-entrypoint: src/charm.py + charm-binary-python-packages: [] + charm-python-packages: [] + charm-requirements: + - requirements.txt + charm-strict-dependencies: false + plugin: charm + type: charm + bases: + - build-on: + - name: ubuntu + channel: '22.04' + run-on: + - name: ubuntu + channel: '22.04' + actions: + rotate-secret-key: + description: Rotate the flask secret key. Users will be forced to log in again. + This might be useful if a security breach occurs. + assumes: + - k8s-api + containers: + flask-app: + resource: flask-app-image + peers: + secret-storage: + interface: secret-storage + provides: + metrics-endpoint: + interface: prometheus_scrape + grafana-dashboard: + interface: grafana_dashboard + requires: + logging: + interface: loki_push_api + ingress: + interface: ingress + limit: 1 + resources: + flask-app-image: + type: oci-image + description: flask application image. + config: + options: + webserver-keepalive: + type: int + description: Time in seconds for webserver to wait for requests on a Keep-Alive + connection. + webserver-threads: + type: int + description: Run each webserver worker with the specified number of threads. + webserver-timeout: + type: int + description: Time in seconds to kill and restart silent webserver workers. + webserver-workers: + type: int + description: The number of webserver worker processes for handling requests. + flask-application-root: + type: string + description: Path in which the application / web server is mounted. This configuration + will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-debug: + type: boolean + description: Whether Flask debug mode is enabled. + flask-env: + type: string + description: What environment the Flask app is running in, by default it's 'production'. + flask-permanent-session-lifetime: + type: int + description: Time in seconds for the cookie to expire in the Flask application + permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME + environment variable. Run app.config.from_prefixed_env() in your Flask application + in order to receive this configuration. + flask-preferred-url-scheme: + type: string + default: HTTPS + description: Scheme for generating external URLs when not in a request context + in the Flask application. By default, it's "HTTPS". This configuration will + set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-secret-key: + type: string + description: The secret key used for securely signing the session cookie and + for any other security related needs by your Flask application. This configuration + will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-session-cookie-secure: + type: boolean + description: Set the secure attribute in the Flask application cookies. This + configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. + Run app.config.from_prefixed_env() in your Flask application in order to + receive this configuration. To expand ``charmcraft.yaml`` using the extensions specified in the file and output the resulting configuration to the terminal, run From b7c4747bd40f1f9015ca6637a3cb0eeaec92c6a1 Mon Sep 17 00:00:00 2001 From: Ali Ugur Date: Thu, 16 Jul 2026 09:40:59 +0300 Subject: [PATCH 25/26] docs: improve formatting and consistency in manage-extensions guide --- docs/howto/manage-extensions.rst | 310 +++++++++++++++---------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/docs/howto/manage-extensions.rst b/docs/howto/manage-extensions.rst index a2854d514..6bcdfa939 100644 --- a/docs/howto/manage-extensions.rst +++ b/docs/howto/manage-extensions.rst @@ -40,71 +40,71 @@ extension. .. dropdown:: Example - .. code-block:: bash + .. code-block:: bash - mkdir my-flask-app-k8s - cd my-flask-app-k8s/ - charmcraft init --profile flask-framework + mkdir my-flask-app-k8s + cd my-flask-app-k8s/ + charmcraft init --profile flask-framework - .. terminal:: + .. terminal:: - Charmed operator package file and directory tree initialised. + Charmed operator package file and directory tree initialised. - Now edit the following package files to provide fundamental charm metadata - and other information: + Now edit the following package files to provide fundamental charm metadata + and other information: - charmcraft.yaml - src/charm.py - README.md + charmcraft.yaml + src/charm.py + README.md - .. code-block:: bash + .. code-block:: bash - ls -R + ls -R - .. terminal:: + .. terminal:: - .: - charmcraft.yaml requirements.txt src + .: + charmcraft.yaml requirements.txt src - ./src: - charm.py + ./src: + charm.py - .. code-block:: bash + .. code-block:: bash - cat charmcraft.yaml + cat charmcraft.yaml - .. code-block:: yaml + .. code-block:: yaml - name: my-flask-app-k8s + name: my-flask-app-k8s - type: charm + type: charm - bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" - - # (Required) - summary: A very short one-line summary of the flask application. - - # (Required) - description: | - A comprehensive overview of your Flask application. - - extensions: - - flask-framework - - # Uncomment the integrations used by your application - # requires: - # mysql: - # interface: mysql_client - # limit: 1 - # postgresql: - # interface: postgresql_client - # limit: 1 + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" + + # (Required) + summary: A very short one-line summary of the flask application. + + # (Required) + description: | + A comprehensive overview of your Flask application. + + extensions: + - flask-framework + + # Uncomment the integrations used by your application + # requires: + # mysql: + # interface: mysql_client + # limit: 1 + # postgresql: + # interface: postgresql_client + # limit: 1 To view details about what that extension is adding to your charm, set the ``CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` environment variable to ``1``, @@ -112,113 +112,113 @@ then run ``charmcraft expand-extensions``. For example: .. dropdown:: Expanding an extension - .. code-block:: bash - - CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions - - .. terminal:: - - *EXPERIMENTAL* extension 'flask-framework' enabled - name: my-flask-app-k8s - summary: A very short one-line summary of the flask application. - description: | - A comprehensive overview of your Flask application. - parts: - charm: - source: . - charm-entrypoint: src/charm.py - charm-binary-python-packages: [] - charm-python-packages: [] - charm-requirements: - - requirements.txt - charm-strict-dependencies: false - plugin: charm - type: charm - bases: - - build-on: - - name: ubuntu - channel: '22.04' - run-on: - - name: ubuntu - channel: '22.04' - actions: - rotate-secret-key: - description: Rotate the flask secret key. Users will be forced to log in again. + .. code-block:: bash + + CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1 charmcraft expand-extensions + + .. terminal:: + + *EXPERIMENTAL* extension 'flask-framework' enabled + name: my-flask-app-k8s + summary: A very short one-line summary of the flask application. + description: | + A comprehensive overview of your Flask application. + parts: + charm: + source: . + charm-entrypoint: src/charm.py + charm-binary-python-packages: [] + charm-python-packages: [] + charm-requirements: + - requirements.txt + charm-strict-dependencies: false + plugin: charm + type: charm + bases: + - build-on: + - name: ubuntu + channel: '22.04' + run-on: + - name: ubuntu + channel: '22.04' + actions: + rotate-secret-key: + description: Rotate the flask secret key. Users will be forced to log in again. This might be useful if a security breach occurs. - assumes: - - k8s-api - containers: - flask-app: - resource: flask-app-image - peers: - secret-storage: - interface: secret-storage - provides: - metrics-endpoint: - interface: prometheus_scrape - grafana-dashboard: - interface: grafana_dashboard - requires: - logging: - interface: loki_push_api - ingress: - interface: ingress - limit: 1 - resources: - flask-app-image: - type: oci-image - description: flask application image. - config: - options: - webserver-keepalive: - type: int - description: Time in seconds for webserver to wait for requests on a Keep-Alive - connection. - webserver-threads: - type: int - description: Run each webserver worker with the specified number of threads. - webserver-timeout: - type: int - description: Time in seconds to kill and restart silent webserver workers. - webserver-workers: - type: int - description: The number of webserver worker processes for handling requests. - flask-application-root: - type: string - description: Path in which the application / web server is mounted. This configuration - will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-debug: - type: boolean - description: Whether Flask debug mode is enabled. - flask-env: - type: string - description: What environment the Flask app is running in, by default it's 'production'. - flask-permanent-session-lifetime: - type: int - description: Time in seconds for the cookie to expire in the Flask application - permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME - environment variable. Run app.config.from_prefixed_env() in your Flask application - in order to receive this configuration. - flask-preferred-url-scheme: - type: string - default: HTTPS - description: Scheme for generating external URLs when not in a request context - in the Flask application. By default, it's "HTTPS". This configuration will - set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-secret-key: - type: string - description: The secret key used for securely signing the session cookie and - for any other security related needs by your Flask application. This configuration - will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() - in your Flask application in order to receive this configuration. - flask-session-cookie-secure: - type: boolean - description: Set the secure attribute in the Flask application cookies. This - configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. - Run app.config.from_prefixed_env() in your Flask application in order to - receive this configuration. + assumes: + - k8s-api + containers: + flask-app: + resource: flask-app-image + peers: + secret-storage: + interface: secret-storage + provides: + metrics-endpoint: + interface: prometheus_scrape + grafana-dashboard: + interface: grafana_dashboard + requires: + logging: + interface: loki_push_api + ingress: + interface: ingress + limit: 1 + resources: + flask-app-image: + type: oci-image + description: flask application image. + config: + options: + webserver-keepalive: + type: int + description: Time in seconds for webserver to wait for requests on a Keep-Alive + connection. + webserver-threads: + type: int + description: Run each webserver worker with the specified number of threads. + webserver-timeout: + type: int + description: Time in seconds to kill and restart silent webserver workers. + webserver-workers: + type: int + description: The number of webserver worker processes for handling requests. + flask-application-root: + type: string + description: Path in which the application / web server is mounted. This configuration + will set the FLASK_APPLICATION_ROOT environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-debug: + type: boolean + description: Whether Flask debug mode is enabled. + flask-env: + type: string + description: What environment the Flask app is running in, by default it's 'production'. + flask-permanent-session-lifetime: + type: int + description: Time in seconds for the cookie to expire in the Flask application + permanent sessions. This configuration will set the FLASK_PERMANENT_SESSION_LIFETIME + environment variable. Run app.config.from_prefixed_env() in your Flask application + in order to receive this configuration. + flask-preferred-url-scheme: + type: string + default: HTTPS + description: Scheme for generating external URLs when not in a request context + in the Flask application. By default, it's "HTTPS". This configuration will + set the FLASK_PREFERRED_URL_SCHEME environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-secret-key: + type: string + description: The secret key used for securely signing the session cookie and + for any other security related needs by your Flask application. This configuration + will set the FLASK_SECRET_KEY environment variable. Run app.config.from_prefixed_env() + in your Flask application in order to receive this configuration. + flask-session-cookie-secure: + type: boolean + description: Set the secure attribute in the Flask application cookies. This + configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable. + Run app.config.from_prefixed_env() in your Flask application in order to + receive this configuration. To expand ``charmcraft.yaml`` using the extensions specified in the file and output the resulting configuration to the terminal, run From cc1d132741bc5fc6c5b24518becac5079de21e3b Mon Sep 17 00:00:00 2001 From: erinecon Date: Tue, 21 Jul 2026 07:52:41 -0400 Subject: [PATCH 26/26] retrigger CI