Repository: canonical/charmcraft

Issue:
Add `charm-user` key
### What needs to get done

Juju 3.6+ supports a `charm-user` key in `metadata.yaml`. We need to provide a way to generate that from charmcraft.yaml.

https://juju.is/docs/sdk/metadata-yaml#heading--charm-user

### Why it needs to get done

feature parity

Discussion:

Patch:
diff --git a/charmcraft/models/metadata.py b/charmcraft/models/metadata.py
index 4133680d..13a6a498 100644
--- a/charmcraft/models/metadata.py
+++ b/charmcraft/models/metadata.py
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any
 
 import pydantic
 from craft_application import models
+from craft_application.models import base
 from craft_cli import CraftError
 from typing_extensions import Self, override
 
@@ -37,12 +38,20 @@ class CharmMetadata(models.BaseMetadata):
     of a charm.
     """
 
+    model_config = pydantic.ConfigDict(
+        validate_assignment=True,
+        extra="ignore",
+        populate_by_name=True,
+        alias_generator=base.alias_generator,
+    )
+
     name: models.ProjectName
     display_name: models.ProjectTitle | None = None
     summary: pydantic.StrictStr
     description: pydantic.StrictStr
     maintainers: list[pydantic.StrictStr] | None = None
     assumes: list[str | dict[str, list | dict]] | None = None
+    charm_user: str | None = None
     containers: dict[str, Any] | None = None
     devices: dict[str, Any] | None = None
     docs: pydantic.AnyHttpUrl | None = None
@@ -65,12 +74,12 @@ class CharmMetadata(models.BaseMetadata):
         Performs the necessary renaming and reorganisation.
         """
         charm_dict = charm.model_dump(
-            include={"title"} | const.METADATA_YAML_KEYS,
             exclude_none=True,
             by_alias=True,
+            exclude_defaults=False,
         )
 
-        # Flatten links and match to the appropriate metadata.yaml name
+        # Flatten links and match to the appropriate metadata.yaml schema
         if charm.links is not None:
             links = charm.links.marshal()
             if "documentation" in links:
diff --git a/charmcraft/models/project.py b/charmcraft/models/project.py
index a09a2752..1ff81127 100644
--- a/charmcraft/models/project.py
+++ b/charmcraft/models/project.py
@@ -630,6 +630,24 @@ class CharmProject(CharmcraftProject):
             """
         ),
     )
+    charm_user: Literal["root", "sudoer", "non-root"] | None = pydantic.Field(
+        default=None,
+        description=textwrap.dedent(
+            """\
+            Specifies that the charm code does not need to be run as root. Possible
+            values are:
+
+            - ``root``: the charm will run as root
+            - ``sudoer``: the charm will run as a non-root user with access to root
+              privileges using ``sudo``.
+            - ``non-root``: the charm will run as a non-root user without ``sudo``.
+
+            Only affects Kubernetes charms on Juju 3.6.0 or later. If not specified,
+            Juju will use
+            `its default behaviour <https://juju.is/docs/sdk/metadata-yaml#heading--charm-user>`_.
+            """
+        ),
+    )
     containers: dict[str, Any] | None = pydantic.Field(
         default=None,
         description=textwrap.dedent(


Test patch:
diff --git a/tests/integration/services/sample_projects/complex-self-contained/prime/metadata.yaml b/tests/integration/services/sample_projects/complex-self-contained/prime/metadata.yaml
index 3afb8287..1b7aa9d9 100644
--- a/tests/integration/services/sample_projects/complex-self-contained/prime/metadata.yaml
+++ b/tests/integration/services/sample_projects/complex-self-contained/prime/metadata.yaml
@@ -5,6 +5,7 @@ assumes:
     - juju >= 3.0
     - juju < 4.0
 - k8s-api
+charm-user: non-root
 containers:
   super-app:
     mounts:
@@ -26,6 +27,8 @@ devices:
     description: Some sweet AMD GPU
     type: amd.com/gpu
 docs: https://juju.is/docs/sdk/charmcraft-yaml
+extra-bindings:
+  Ring of Power: null
 issues:
 - https://launchpad.net/~charmcraft-team
 - https://github.com/canonical/charmcraft/issues
diff --git a/tests/integration/services/sample_projects/complex-self-contained/project/charmcraft.yaml b/tests/integration/services/sample_projects/complex-self-contained/project/charmcraft.yaml
index b6261af6..c4030627 100644
--- a/tests/integration/services/sample_projects/complex-self-contained/project/charmcraft.yaml
+++ b/tests/integration/services/sample_projects/complex-self-contained/project/charmcraft.yaml
@@ -94,6 +94,7 @@ assumes:
     - juju >= 3.0
     - juju < 4.0
 - k8s-api
+charm-user: non-root
 containers:
   super-app:
     resource: super-app-image
diff --git a/tests/unit/models/test_metadata.py b/tests/unit/models/test_metadata.py
index 2da3f3e8..63fdeaa5 100644
--- a/tests/unit/models/test_metadata.py
+++ b/tests/unit/models/test_metadata.py
@@ -72,6 +72,11 @@ BASIC_BUNDLE_DICT = {
             dict(**BASIC_CHARM_DICT, title="Title becomes display name"),
             {**BASIC_CHARM_METADATA_DICT, "display-name": "Title becomes display name"},
         ),
+        pytest.param(
+            {**BASIC_CHARM_DICT, "charm-user": "sudoer"},
+            {**BASIC_CHARM_METADATA_DICT, "charm-user": "sudoer"},
+            id="sudoer",
+        ),
     ],
 )
 def test_charm_metadata_from_charm_success(charm_dict, expected):