From 96888a06652c65ea3e43416fcf2ad24d25165058 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 16 May 2026 01:02:38 +0200 Subject: [PATCH] feat: deprecation warning for snap_daemon system username Add a SystemUsernames pydantic model with snap_daemon marked as deprecated, so the JSON schema exposes 'deprecated: true' on that key. Also emit a runtime warning via craft-cli when snap_daemon is used. Fixes: https://github.com/canonical/snapcraft/issues/5448 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- snapcraft/meta/snap_yaml.py | 4 ++- snapcraft/models/project.py | 40 +++++++++++++++++++++++++++--- tests/unit/models/test_projects.py | 33 +++++++++++++++++++++++- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 35d2502ad2..4fd2146724 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -487,7 +487,9 @@ def get_metadata_from_project( slots=project.slots, hooks=project.hooks, layout=project.layout, - system_usernames=project.system_usernames, + system_usernames=project.system_usernames.model_dump(exclude_none=True) + if project.system_usernames is not None + else None, provenance=project.provenance, links=links if links else None, components=components, diff --git a/snapcraft/models/project.py b/snapcraft/models/project.py index 48ee26aa29..13a97a1047 100644 --- a/snapcraft/models/project.py +++ b/snapcraft/models/project.py @@ -1258,6 +1258,29 @@ def from_architectures( return platforms +class SystemUsernames(models.CraftBaseModel, extra="allow"): + """System usernames the snap can use to run daemons. + + See :external+snap:ref:`interfaces-system-usernames` in the snap documentation for + more information. + """ + + snap_daemon: Annotated[ + str | dict[str, Any] | None, + pydantic.Field( + default=None, + deprecated=( + "The 'snap_daemon' system username is deprecated. " + "See https://snapcraft.io/docs/system-usernames." + ), + ), + ] = None + """Deprecated system username. + + Use ``snap_microk8s``, ``snap_aziotedge``, or ``snap_aziotdu`` instead. + """ + + class Component(models.CraftBaseModel): """Snapcraft component definition.""" @@ -1791,16 +1814,13 @@ class Project(models.Project): adopting part. """ - system_usernames: dict[str, Any] | None = pydantic.Field( + system_usernames: SystemUsernames | None = pydantic.Field( default=None, description="The system usernames the snap can use to run daemons and services.", examples=["{snap-daemon: shared}"], ) """The system usernames the snap can use to run daemons and services. - This is used to run daemons with the ``snap_daemon`` user defined by snapd. - Otherwise, this is an uncommon key. - See :external+snap:ref:`interfaces-system-usernames` in the snap documentation for more information. """ @@ -1954,6 +1974,18 @@ def _validate_slots(cls, slots: dict[str, Any]) -> dict[str, Any]: return slots + @pydantic.field_validator("system_usernames") + @classmethod + def _validate_system_usernames( + cls, system_usernames: SystemUsernames + ) -> SystemUsernames: + if system_usernames and system_usernames.snap_daemon is not None: + emit.warning( + "The 'snap_daemon' system username is deprecated. " + "See https://snapcraft.io/docs/system-usernames." + ) + return system_usernames + @pydantic.model_validator(mode="after") def _validate_adoptable_fields(self) -> Self: for field in MANDATORY_ADOPTABLE_FIELDS: diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py index c171d37ed6..051b99d98d 100644 --- a/tests/unit/models/test_projects.py +++ b/tests/unit/models/test_projects.py @@ -1654,7 +1654,38 @@ def test_app_sockets_valid_socket_mode(self, socket_mode, socket_yaml_data): ) def test_project_system_usernames_valid(self, system_username, project_yaml_data): project = Project.unmarshal(project_yaml_data(system_usernames=system_username)) - assert project.system_usernames == system_username + assert project.system_usernames is not None + for key, value in system_username.items(): + assert getattr(project.system_usernames, key, None) == value + + @pytest.mark.parametrize( + "system_username", + [ + {"snap_daemon": {"scope": "shared"}}, + {"snap_daemon": "shared"}, + ], + ) + def test_project_system_usernames_snap_daemon_deprecated( + self, system_username, project_yaml_data, emitter + ): + Project.unmarshal(project_yaml_data(system_usernames=system_username)) + emitter.assert_warning( + "The 'snap_daemon' system username is deprecated. " + "See https://snapcraft.io/docs/system-usernames." + ) + + @pytest.mark.parametrize( + "system_username", + [ + {"snap_microk8s": {"scope": "shared"}}, + {"snap_aziotedge": "shared"}, + ], + ) + def test_project_system_usernames_no_warning_for_others( + self, system_username, project_yaml_data, emitter + ): + Project.unmarshal(project_yaml_data(system_usernames=system_username)) + assert not any(i for i in emitter.interactions if i[0] == "warning") @pytest.mark.parametrize( "system_username",