Repository: canonical/operator

Issue:
Add charm-user to CharmMeta
charmcraft 3.2 adds support for a charm-user field in charmcraft.yaml. We load/expose all the other charmcraft fields, so should presumably load this one too.

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/publish-ops-scenario.yaml b/.github/workflows/publish-ops-scenario.yaml
index 1efe1906b..9ed2dd0de 100644
--- a/.github/workflows/publish-ops-scenario.yaml
+++ b/.github/workflows/publish-ops-scenario.yaml
@@ -29,7 +29,7 @@ jobs:
         run: python -m build
         working-directory: ./testing
       - name: Attest build provenance
-        uses: actions/attest-build-provenance@v2.2.0
+        uses: actions/attest-build-provenance@v2.2.2
         with:
           subject-path: 'testing/dist/*'
       - name: Publish
diff --git a/.github/workflows/publish-ops.yaml b/.github/workflows/publish-ops.yaml
index a10037e45..9c8c8a069 100644
--- a/.github/workflows/publish-ops.yaml
+++ b/.github/workflows/publish-ops.yaml
@@ -32,7 +32,7 @@ jobs:
       - name: Build
         run: python -m build
       - name: Attest build provenance
-        uses: actions/attest-build-provenance@v2.2.0
+        uses: actions/attest-build-provenance@v2.2.2
         with:
           subject-path: 'dist/*'
       - name: Publish
diff --git a/.github/workflows/test-publish-ops-scenario.yaml b/.github/workflows/test-publish-ops-scenario.yaml
index 39833cc1c..28c6045ca 100644
--- a/.github/workflows/test-publish-ops-scenario.yaml
+++ b/.github/workflows/test-publish-ops-scenario.yaml
@@ -26,7 +26,7 @@ jobs:
         run: python -m build
         working-directory: ./testing
       - name: Attest build provenance
-        uses: actions/attest-build-provenance@v2.2.0
+        uses: actions/attest-build-provenance@v2.2.2
         with:
           subject-path: 'testing/dist/*'
       - name: Publish to test.pypi.org
diff --git a/.github/workflows/test-publish-ops.yaml b/.github/workflows/test-publish-ops.yaml
index cd0831648..43c6247a9 100644
--- a/.github/workflows/test-publish-ops.yaml
+++ b/.github/workflows/test-publish-ops.yaml
@@ -25,7 +25,7 @@ jobs:
       - name: Build
         run: python -m build
       - name: Attest build provenance
-        uses: actions/attest-build-provenance@v2.2.0
+        uses: actions/attest-build-provenance@v2.2.2
         with:
           subject-path: 'dist/*'
       - name: Publish to test.pypi.org
diff --git a/ops/charm.py b/ops/charm.py
index 13184353e..77fb0a81d 100644
--- a/ops/charm.py
+++ b/ops/charm.py
@@ -1451,6 +1451,17 @@ class is mostly for the framework to understand what the charm has defined.
     assumes: 'JujuAssumes'
     """Juju features this charm requires."""
 
+    charm_user: Literal['root', 'sudoer', 'non-root']
+    """Type of user used to run the charm hook code.
+
+    The value of ``root`` ensures the charm runs as root. The value of
+    ``sudoer`` runs the charm as a user other than root with access to sudo to
+    elevate its privileges. ``non-root`` ensures the charm does not run as root
+    and also does not have ``sudo`` privileges.
+
+    .. jujuadded 3.6.0
+    """
+
     containers: Dict[str, 'ContainerMeta']
     """Container metadata for each defined container."""
 
@@ -1524,6 +1535,7 @@ def __init__(
         # Note that metadata v2 does not define min-juju-version ('assumes'
         # should be used instead).
         self.min_juju_version = raw_.get('min-juju-version')
+        self.charm_user = raw_.get('charm-user', 'root')
         self.requires = {
             name: RelationMeta(RelationRole.requires, name, rel)
             for name, rel in raw_.get('requires', {}).items()


Test patch:
diff --git a/test/test_charm.py b/test/test_charm.py
index 934f5c32c..b2c9461af 100644
--- a/test/test_charm.py
+++ b/test/test_charm.py
@@ -1129,3 +1129,19 @@ def test_meta_assumes():
             ops.JujuAssumesCondition.ANY,
         ),
     ]
+
+
+@pytest.mark.parametrize('user', ['root', 'sudoer', 'non-root'])
+def test_meta_charm_user(user: str):
+    meta = ops.CharmMeta.from_yaml(f"""
+name: my-charm
+charm-user: {user}
+""")
+    assert meta.charm_user == user
+
+
+def test_meta_charm_user_default():
+    meta = ops.CharmMeta.from_yaml("""
+name: my-charm
+""")
+    assert meta.charm_user == 'root'